Compare commits

...
106 Commits
Author SHA1 Message Date
Linus Dietz ebc3ba4e16 Fix missing key for en_GB and en_US locales 2023-07-02 09:03:11 +02:00
Linus DietzandGitHub a1881a87d8 Added dynamic icons lock, battery and window state (#45)
#43
2023-07-01 15:06:23 +02:00
Linus Dietz 8c0ce8976e get distanceToEmpty sensor from connected api 2023-06-30 12:21:28 +02:00
Linus DietzandGitHub 34a0b9b2a7 Optimize logging (#41)
* Move print statements to logger

* Add external log location for addon usage

* Add more detailed logs for debug

* Bump version
2023-06-30 11:16:57 +02:00
Linus DietzandGitHub 67320c32f6 Optimize climate timer (#40)
* Add logic to turn climate switch off if driver door is opened

* Update car data after climate state change

* Climate stop on left or right door open

* Bump version
2023-06-29 17:46:34 +02:00
Dielee 30f415cb1f Merge branch 'main' of https://github.com/Dielee/volvo2mqtt into main 2023-06-29 15:16:05 +02:00
Dielee 241b2f7ba4 Fix app crash on credential refresh 2023-06-29 15:15:49 +02:00
Linus Dietz 80017f64e3 Fix app crash on credential refresh 2023-06-29 15:14:24 +02:00
Linus Dietz 47357fb582 Add "Washer Fluid Level" and "Distance to Empty" sensors 2023-06-29 08:36:23 +02:00
Linus Dietz 9f1209e628 Fix wrong return values for not working endpoints 2023-06-29 08:06:31 +02:00
Linus DietzandGitHub f0d4d68792 Add value conversion from metric to imperial (#36)
* Convert km to miles, km/h to mp/h if babelLocale is en_GB or en_US #34

* Remove unused variable

* Change speed unit for imperial system // Bump version
2023-06-27 15:29:48 +02:00
Linus DietzandGitHub 140a735773 Update README.md 2023-06-27 14:24:43 +02:00
Linus Dietz ebcabc6ff4 rework error message for get_vehicles 2023-06-27 09:11:07 +02:00
Linus Dietz c995c2a44d Optimize error message for get_vehicles 2023-06-27 09:09:28 +02:00
Linus Dietz f661985865 Add average fuel consumption multiplier for #30 // Rewrite README.md settings section 2023-06-27 07:56:54 +02:00
Linus Dietz da2d8395ec Optimize filter for average speed 2023-06-27 07:16:29 +02:00
Linus Dietz 4bd6a7a991 Add divider option for average speed 2023-06-26 14:23:53 +02:00
Linus Dietz 988fb3bd21 Add new empty setting options to settings.json 2023-06-26 14:12:24 +02:00
Linus DietzandGitHub 15fd137cc6 Add thanks to for sunroof endpoint 2023-06-26 12:59:01 +02:00
Linus Dietz af4eeb3a72 Change sunroof data endpoint 2023-06-26 12:37:56 +02:00
Linus Dietz cae32d7745 Bump version 2023-06-26 12:24:10 +02:00
Linus DietzandGitHub f1b265740b Add XC90 2023 T8 2023-06-26 12:10:52 +02:00
Linus Dietz bb689c3ee2 Fix sunroof data path 2023-06-26 11:44:14 +02:00
Linus DietzandGitHub 75363b5c0f Add new sensors 2023-06-26 11:28:27 +02:00
Linus DietzandGitHub 91eb8a9429 Add badges 2023-06-26 10:45:22 +02:00
Linus DietzandGitHub 638cfcd730 Add readme image 2023-06-26 09:32:27 +02:00
Linus Dietz 983e84e6fe Add readme image 2023-06-26 09:27:29 +02:00
Linus DietzandGitHub f8eb7cee41 Add lovelace sample 2023-06-26 09:25:46 +02:00
Linus Dietz 1e071154c5 Bump version 2023-06-26 08:55:23 +02:00
Linus Dietz 6d938eab7a Filter zero data from volvo api 2023-06-26 08:50:30 +02:00
Linus Dietz 86980408e7 Filter zero data from volvo api 2023-06-26 08:43:20 +02:00
Linus Dietz a0800db907 Change average speed icon 2023-06-26 08:37:41 +02:00
Linus Dietz d5565cd52a Move windows and doors to binary_sensor domain 2023-06-26 08:36:50 +02:00
Linus DietzandGitHub 9f5b4f4017 Only build on new release 2023-06-26 08:17:51 +02:00
Linus Dietz 689d3e84e7 Add statistics endpoint to cached responses 2023-06-26 08:11:08 +02:00
Linus Dietz c095c39462 Bump version for release #30, #31, #32 2023-06-26 07:59:18 +02:00
Linus Dietz c72e99bcf8 Add average fuel consumption and average speed #30 2023-06-26 07:57:11 +02:00
Linus Dietz c5ed7b7971 Fix cached wget requirements.txt for HA Addon 2023-06-26 07:30:09 +02:00
Linus Dietz 43d09cb2f4 Send heartbeat after device creation timeout 2023-06-26 07:18:31 +02:00
Linus Dietz b7794d837b Fix offline devices after HA reboot 2023-06-26 07:16:41 +02:00
Linus Dietz 5d57979849 Add timezone option for HA Addon 2023-06-25 20:17:51 +02:00
Linus DietzandGitHub b1ef7e8e46 Add TZ link 2023-06-25 19:49:38 +02:00
Linus DietzandGitHub 07213aba18 Update README.md 2023-06-25 12:33:37 +02:00
Linus Dietz 71b8c59ba8 Fix unlockstate for multiple car usage 2023-06-25 07:22:15 +02:00
Linus Dietz 50b5eb9863 Add mqtt availability 2023-06-24 20:06:59 +02:00
Linus DietzandGitHub 02793accfa Update README.md 2023-06-24 19:53:03 +02:00
Linus Dietz 4d8b5e5006 Optimize lock/unlock behaviour 2023-06-24 19:47:02 +02:00
Linus Dietz e8315ba0fd Merge remote-tracking branch 'origin/main' into main 2023-06-24 18:08:09 +02:00
Linus Dietz 4fca54b19f Add broker port to ha addon conf 2023-06-24 18:07:59 +02:00
Linus DietzandGitHub a411a1a59d Add broker Port 2023-06-24 18:06:15 +02:00
Linus Dietz a51ba25061 Add mqtt broker port option 2023-06-24 18:02:56 +02:00
Linus Dietz 1d37b3aca7 Revert "Bump version"
This reverts commit 1574fa96
2023-06-24 17:54:09 +02:00
Linus Dietz 1574fa968c Bump version 2023-06-24 17:53:12 +02:00
Linus Dietz aeea34b438 Bump version 2023-06-24 16:54:06 +02:00
Linus Dietz 1ccd140400 Reduce api calls in force update mode 2023-06-24 16:51:29 +02:00
Linus Dietz f4aef07e55 Optimize climate timer for multiple cars 2023-06-24 16:37:51 +02:00
Linus Dietz 8f6d664264 Fix wrong climate time duration 2023-06-24 16:35:44 +02:00
Linus DietzandGitHub 2b510d4e1b Add xc40 BEV 2022 2023-06-24 15:27:49 +02:00
Linus Dietz 5c0216465e Fix missing entities after HA restart 2023-06-24 11:44:50 +02:00
Linus Dietz d9ad484c69 Improve lock entity response time while locking/unlocking 2023-06-24 10:08:56 +02:00
Linus Dietz 28721384f7 Rework caching storage 2023-06-24 10:07:51 +02:00
Linus Dietz c7a883d781 Catch volvo api exceptions 2023-06-24 08:54:44 +02:00
Linus Dietz 023d8d2096 Rework response caching 2023-06-24 08:34:40 +02:00
Linus DietzandGitHub 64d0640282 Merge pull request #26 from jverdeyen/docker-compose-example
Added docker-compose.yml example
2023-06-23 14:09:04 +02:00
Joeri Verdeyen 39c748fe1a Added docker-compose.yml example 2023-06-23 13:56:30 +02:00
Linus Dietz 429c49c3f8 Merge remote-tracking branch 'origin/main' into main 2023-06-23 12:43:29 +02:00
Linus Dietz 68de908e05 Fix if multiplier is an empty string 2023-06-23 12:43:22 +02:00
Linus DietzandGitHub 5027b54de0 Update README.md 2023-06-23 12:40:31 +02:00
Linus DietzandGitHub 13cf419022 Update README.md 2023-06-23 12:36:39 +02:00
Linus DietzandGitHub d5a542b8cf Update README.md 2023-06-23 12:35:23 +02:00
Linus Dietz 7103b30da2 Make odometer multiplier configurable 2023-06-23 12:31:38 +02:00
Linus Dietz c34e090bfd Rename tyre to tire 2023-06-23 12:04:36 +02:00
Linus Dietz ec8c3cea5f Add fuel level 2023-06-23 12:01:14 +02:00
Linus DietzandGitHub 7bbf39d8a9 Update issue templates 2023-06-23 11:50:33 +02:00
Linus DietzandGitHub 67f0d382b8 Update issue templates 2023-06-23 11:50:05 +02:00
Linus Dietz 506be2a145 Bump version 2023-06-23 10:59:35 +02:00
Linus DietzandGitHub 92c67e31aa Update README.md 2023-06-23 10:26:04 +02:00
Linus Dietz 42c9349ca8 Rework try to catch battery charge state from connected-vehicle v2 if no energy api is available 2023-06-23 10:13:57 +02:00
Linus Dietz 1c4fbb649d Try to catch battery charge state from connected-vehicle v2 if no energy api is available // Fix assumed climate state 2023-06-23 09:47:04 +02:00
Linus DietzandGitHub 5275b53685 Update README.md 2023-06-23 09:31:58 +02:00
Linus Dietz ce1f779e3e Add support for charging connection state, tyre state, and engine state 2023-06-23 09:24:15 +02:00
Linus Dietz 17853210c3 Create only sensors that are supported by the car // Rework sensor creation 2023-06-23 08:58:24 +02:00
Linus Dietz b3ab5acf0f Fix #22 - multiply odometer by ten 2023-06-23 07:13:41 +02:00
Linus DietzandGitHub 4a2f9bb6f8 Update README.md 2023-06-22 20:51:03 +02:00
Linus Dietz 53114e9d6c Bump version 2023-06-22 19:33:04 +02:00
Linus Dietz bbc6ccb840 cleanup api data handling 2023-06-22 19:22:54 +02:00
Linus Dietz 1d07644a9c Rework api data handling 2023-06-22 16:45:00 +02:00
Linus Dietz e0152fa82d Breaking change: change mqtt entity ids 2023-06-22 15:48:03 +02:00
Linus Dietz e1e31776c2 Bump addon version 2023-06-22 15:14:39 +02:00
Linus Dietz 43f5bbaadf Bump version 2023-06-22 15:05:31 +02:00
Linus Dietz dd6e4f8cb7 Add car location device_tracker 2023-06-22 14:50:24 +02:00
Linus Dietz 17e29e44fc Add tank lid, engine hood and tail gate sensors 2023-06-22 14:10:39 +02:00
Linus Dietz 93494eaa43 Add door sensors 2023-06-22 14:02:36 +02:00
Linus Dietz 5555ddc5cc Add window sensors 2023-06-22 13:39:03 +02:00
Linus Dietz 5ea045f1cf Add odometer sensor 2023-06-22 13:18:37 +02:00
Linus Dietz 6a01f70637 Fix imperial distance unit for en_US locales 2023-06-22 13:14:42 +02:00
Linus Dietz 2c52f0ad58 Rename window url variable 2023-06-22 13:05:22 +02:00
Linus Dietz d5e02b4a3e Rename recharge url variable 2023-06-22 13:04:29 +02:00
Linus Dietz fbe05f6aa2 Workaround for vehicle details error 500 2023-06-22 11:20:27 +02:00
Linus DietzandGitHub 385a500dab Update README.md 2023-06-22 10:33:33 +02:00
Linus Dietz 54c459ff39 Merge remote-tracking branch 'origin/main' into main 2023-06-22 10:32:51 +02:00
Linus Dietz 931a2f1523 Stop climate timer if climate is turned off manually 2023-06-22 10:32:42 +02:00
Linus DietzandGitHub 22d421f5aa Update README.md 2023-06-21 17:40:17 +02:00
Linus Dietz 18d17cff58 Fix typo 2023-06-21 15:49:54 +02:00
Linus Dietz c09dd42e76 Add addon translations 2023-06-21 15:47:26 +02:00
Linus DietzandGitHub 563e5508ae Update README.md 2023-06-21 14:08:46 +02:00
20 changed files with 1032 additions and 392 deletions
+5
View File
@@ -0,0 +1,5 @@
CONF_updateInterval=300
CONF_babelLocale='de'
CONF_mqtt='@json {"broker": "mqtt", "username": "", "password": ""}'
CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": ""}'
TZ='Europe/Berlin'
+30
View File
@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: Dielee
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version info**
The version you are using and the method you are using (Docker container/HA Addon/Cloned repo)
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1 -1
View File
@@ -11,7 +11,7 @@ name: Create and publish a Docker image
on:
release:
types: [created, edited]
types: [created]
workflow_dispatch:
env:
+6 -1
View File
@@ -1 +1,6 @@
.idea
.idea
venv
src/settings.dev.json
.env
/src/volvo2mqtt.log
/src/volvo2mqtt.log.1
+132 -14
View File
@@ -1,15 +1,37 @@
# Volvo2Mqtt
![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield]
![Supports armhf Architecture][armhf-shield]
![Supports armv7 Architecture][armv7-shield]
![Supports i386 Architecture][i386-shield]
<br>
![Project Maintenance][maintenance-shield]
[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
This component establishes a connection between the newer AAOS Volvo cars and Home Assistant via MQTT.<br>
Maybe this component works also with other Volvo cars. Please try out the native Volvo [integration](https://www.home-assistant.io/integrations/volvooncall/) before using this component! If the native component doesn't work for your car, try this mqtt bridge.
<p>
Home Assistant thread can be found [here](https://community.home-assistant.io/t/volvo2mqtt-connect-your-aaos-volvo/585699).
## Confirmed working with
- XC40 BEV (2023)
- XC40 BEV (2022)
- XC40 PHEV (2021)
- V60 T8 PHEV (2023)
- C40 PHEV (2023)
- C40 PHEV (2022)
- XC90 T8 PHEV (2023)
- XC60 PHEV (2023)
- XC60 PHEV (2022)
- XC90 PHEV T8 (2023)
- V90 PHEV T8 (2019)*
*only partly working
Please let me know if your car works with this addon so I can expand the list!<br>
Please let me know if your car works with this addon so I can expand the list!
## Supported features
- Lock/unlock car
@@ -17,34 +39,130 @@ Please let me know if your car works with this addon so I can expand the list!
- Sensor "Battery Charge Level"
- Sensor "Electric Range"
- Sensor "Charging System Status"
- Sensor "Charging Connection Status"
- Sensor "Estimated Charging Finish Time"
- Sensor "Door Lock Status (All doors, tank lid, and engine hood)"
- Sensor "Window Lock Status (All windows and sunroof - Thanks to @navet)"
- Sensor "Engine Status"
- Sensor "Odometer"
- Sensor "Tire Status"
- Sensor "Fuel Status"
- Sensor "Average Fuel Consumption"
- Sensor "Average Speed"
- Car Device Tracker
- Multiple cars
NOTE: Energy status currently available only for cars in the Europe / Middle East / Africa regions. [source](https://developer.volvocars.com/apis/energy/v1/overview/#availability)
## Setup
Docker:
<b>Docker:</b>
Just install this addon with the following command.
Please note to fill in your settings inside the environment variables.
`docker run --pull=always -e CONF_updateInterval=300 -e CONF_babelLocale='de' -e CONF_mqtt='@json {"broker": "", "username": "", "password": ""}' -e CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": ""}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest`
`docker run -d --pull=always -e CONF_updateInterval=300 -e CONF_babelLocale='de' -e CONF_mqtt='@json {"broker": "", "username": "", "password": "", "port": 1883}' -e CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": "", "odometerMultiplier": 1, "averageSpeedDivider": 1, "averageFuelConsumptionMultiplier": 1}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest`
HA Add-On<br>
<b>HA Add-On:</b><br>
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2FDielee%2Fvolvo2mqtt)
Here is what every option means:
| Name | Type | Default | Description |
| -------------------- | :-------: | :----------: | --------------------------------------------------------------- |
| `CONF_updateInterval` | `int` | **required** | Update intervall in seconds. |
| `CONF_babelLocale` | `string` | **required** | Select your country from this [list](https://www.ibm.com/docs/en/radfws/9.7?topic=overview-locales-code-pages-supported). "Locale name" is the column you need! |
| `CONF_mqtt` | `json` | **required** | Broker = Mqtt Broker IP / Username and Passwort are optional! |
| `CONF_volvoData` | `json` | **required** | Username and password are REQUIRED. Car vin can be a single vin or a list of multiple vins like `["vin1", "vin2"]`. If no vin is provided, <b>ALL</b> of your vehicles will be used. Vccapi key is REQUIRED. Get your Vccapi key from [here](https://developer.volvocars.com/account/). |
| `CONF_debug` | `string` | | Debug option (true/false) - optional! |
| `TZ` | `string` | | Container timezone eg "Europe/Berlin" |
| Environment Variable Name | Type | Json Option | Default | Description |
| ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- |
| `CONF_updateInterval` | `int` | | **required** | Update intervall in seconds. |
| `CONF_babelLocale` | `string` | | **required** | Select your country from this [list](https://www.ibm.com/docs/en/radfws/9.7?topic=overview-locales-code-pages-supported). "Locale name" is the column you need! |
| `CONF_mqtt` | `json` | `broker` | **required** | Your MQTT Broker IP. Eg. 192.168.0.5.
| `CONF_mqtt` | `json` | `port` | 1883 | Your MQTT Broker Port. If no value is given, port 1883 will be used. |
| `CONF_mqtt` | `json` | `username` | optional | MQTT Username for your broker.
| `CONF_mqtt` | `json` | `password` | optional | MQTT Password for your broker.
| `CONF_volvoData` | `json` | `username` | **required** | Normally your email address to login into the Volvo App.
| `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App.
| `CONF_volvoData` | `json` | `vin` | optional | A single VIN like "VIN1" or a list of VINs like "["VIN1", "VIN2"]". Leave this empty if you don't know your VIN. The addon will use every car that is tied to your account.
| `CONF_volvoData` | `json` | `vccapikey` | **required** | API key linked with your volvo developer account. Get your Vccapi key from [here](https://developer.volvocars.com/account/)
| `CONF_volvoData` | `json` | `odometerMultiplier` | optional | The multiplier value for the odometer value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the multiplier will be 1.
| `CONF_volvoData` | `json` | `averageSpeedDivider` | optional | The divider value for the average speed value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the divider will be 1.
| `CONF_volvoData` | `json` | `averageFuelConsumptionMultiplier` | optional | The multiplier value for the average fuel consumption value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the multiplier will be 1.
| `CONF_debug` | `string` | | optional | Debug option (true/false). Normally you don't need this. |
| `TZ` | `string` | | **required** | Container timezone eg "Europe/Berlin" from [here](https://docs.diladele.com/docker/timezones.html)|
If you like my work:
## Lovelace sample card
<details>
<summary>Show me more!</summary><blockquote>
<br>
![alt text](https://raw.githubusercontent.com/Dielee/volvo2mqtt/main/img/lovelace_sample.png)<br>
<details>
<summary>Lovelace card sample</summary>
```
type: vertical-stack
title: Autostatus
cards:
- type: custom:vertical-stack-in-card
cards:
- type: custom:mushroom-lock-card
entity: lock.volvo_<your vin>_lock_status
name: Verrigelungsstatus
- type: horizontal-stack
cards:
- type: custom:mushroom-entity-card
entity: sensor.volvo_<your vin>_electric_range
name: Reichweite
layout: vertical
- type: custom:mushroom-entity-card
entity: sensor.volvo_<your vin>_battery_charge_level
name: Batteriestatus
layout: vertical
- type: custom:mushroom-entity-card
entity: sensor.volvo_<your vin>_estimated_charging_time
layout: vertical
name: Ladezeit
- type: horizontal-stack
cards:
- type: custom:mushroom-entity-card
entity: switch.volvo_<your vin>_climate_status
tap_action:
action: toggle
layout: vertical
name: Klimatisieren/Heizen
- type: custom:mushroom-template-card
primary: Daten aktualisieren
secondary: '{{ states(''sensor.volvo_<your vin>_last_data_update'')}}'
icon: mdi:update
layout: vertical
entity: button.volvo_<your vin>_update_data
- type: conditional
conditions:
- entity: sensor.volvo_<your vin>_estimated_charging_time
state_not: '0'
card:
type: custom:mushroom-entity-card
entity: sensor.volvo_<your vin>_estimated_charging_finish_time
name: Ladung vorraussichtlich abgeschlossen
show_name: true
- type: map
entities:
- entity: device_tracker.volvo_<your vin>_location
default_zoom: 16
dark_mode: false
hours_to_show: 0
auto_fit: true
aspect_ratio: '16:9'
```
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/U7U8MFXCF)
</details>
</blockquote></details>
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg
[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg
[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg
[releases]: https://github.com/Dielee/volvo2mqtt/releases
[releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg
[maintenance-shield]: https://img.shields.io/maintenance/yes/2023.svg
[commits-shield]: https://img.shields.io/github/commit-activity/y/Dielee/volvo2mqtt.svg
[commits]: https://github.com/Dielee/volvo2mqtt/commits/main
+24
View File
@@ -0,0 +1,24 @@
version: '3'
services:
app:
build: .
env_file:
- .env
# environment:
# CONF_updateInterval: 300
# CONF_babelLocale: 'de'
# CONF_mqtt: '@json {"broker": "mqtt", "username": "", "password": ""}'
# CONF_volvoData: '@json {"username": "", "password": "", "vin": "", "vccapikey": ""}'
# TZ: 'Europe/Berlin'
links:
- mqtt
depends_on:
- mqtt
mqtt:
container_name: mqtt
image: eclipse-mosquitto
expose:
- 1883
command: [ "/usr/sbin/mosquitto", "-c", "/mosquitto-no-auth.conf" ]
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+2 -1
View File
@@ -1,4 +1,5 @@
requests~=2.29.0
dynaconf~=3.1.12
paho-mqtt~=1.6.1
Babel~=2.12.1
Babel~=2.12.1
pytz~=2023.3
+3 -2
View File
@@ -8,13 +8,14 @@ RUN apk add --no-cache python3 bash py3-pip
WORKDIR /volvoAAOS2mqtt
# download requirements.txt
RUN wget https://raw.githubusercontent.com/Dielee/volvo2mqtt/main/requirements.txt
RUN wget --no-cache https://raw.githubusercontent.com/Dielee/volvo2mqtt/main/requirements.txt
# install dependencies
RUN pip install -r requirements.txt
# copy the content of the local src directory to the working directory
COPY / .
RUN chmod a+x /volvoAAOS2mqtt/run.sh
# command to run on container start
CMD [ "python", "-u", "./main.py" ]
CMD [ "/volvoAAOS2mqtt/run.sh" ]
+1 -1
View File
@@ -2,5 +2,5 @@ from dynaconf import Dynaconf
settings = Dynaconf(
envvar_prefix="CONF",
settings_files=["settings.json", "/data/options.json"],
settings_files=["settings.json", "/data/options.json", "settings.dev.json"],
)
+13 -1
View File
@@ -1,16 +1,20 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.1.0"
version: "1.6.5"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
apparmor: true
map:
- addons:rw
options:
updateInterval: 300
babelLocale: null
TZ: null
debug: false
mqtt:
broker: null
port: 1883
username: ""
password: ""
volvoData:
@@ -18,12 +22,17 @@ options:
password: null
vin: ""
vccapikey: null
odometerMultiplier: 1
averageSpeedDivider: 1
averageFuelConsumptionMultiplier: 1
schema:
updateInterval: int(10,)
babelLocale: str
TZ: str
debug: bool
mqtt:
broker: str
port: int(1,)
username: str?
password: str?
volvoData:
@@ -31,6 +40,9 @@ schema:
password: str
vin: str?
vccapikey: str
odometerMultiplier: int(1,)
averageSpeedDivider: int(1,)
averageFuelConsumptionMultiplier: int(1,)
arch:
- aarch64
- amd64
+92 -21
View File
@@ -1,36 +1,107 @@
VERSION = "v1.1.0"
from config import settings
VERSION = "v1.6.5"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles"
VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}"
WINDOW_STATUS_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}/windows"
WINDOWS_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/windows"
CLIMATE_START_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/climatization-start"
CLIMATE_STOP_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/climatization-stop"
CAR_LOCK_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/doors"
LOCK_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/doors"
CAR_LOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/lock"
CAR_UNLOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/unlock"
RECHARGE_STATUS_URL = "https://api.volvocars.com/energy/v1/vehicles/{0}/recharge-status"
RECHARGE_STATE_URL = "https://api.volvocars.com/energy/v1/vehicles/{0}/recharge-status"
ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/odometer"
LOCATION_STATE_URL = "https://api.volvocars.com/location/v1/vehicles/{0}/location"
TYRE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/tyres"
ENGINE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine-status"
BATTERY_CHARGE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/battery-charge-level"
FUEL_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/fuel"
STATISTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/statistics"
units = {
"en_GB": {
"divider": 1.60934,
"electric_range": {"unit": "mi"},
"odometer": {"unit": "mi"},
"average_speed": {"unit": "mph"},
"distance_to_empty": {"unit": "mi"}
},
"en_US": {
"divider": 1.60934,
"electric_range": {"unit": "mi"},
"odometer": {"unit": "mi"},
"average_speed": {"unit": "mph"},
"distance_to_empty": {"unit": "mi"}
}
}
availability_topic = "volvoAAOS2mqtt/availability"
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle",
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified"}
supported_sensors = [
{"name": "Battery Charge Level", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATUS_URL},
{"name": "Electric Range", "id": "electric_range", "unit": "km", "icon": "map-marker-distance", "url": RECHARGE_STATUS_URL},
{"name": "Estimated Charging Time", "id": "estimated_charging_time", "unit": "minutes", "icon": "timer-sync-outline", "url": RECHARGE_STATUS_URL},
{"name": "Charging System Status", "id": "charging_system_status", "icon": "ev-plug-ccs2", "url": RECHARGE_STATUS_URL},
{"name": "Estimated Charging Finish Time", "id": "estimated_charging_finish_time", "icon": "timer-sync-outline", "url": RECHARGE_STATUS_URL},
{"name": "Last Data Update", "id": "last_data_update", "icon": "timer", "url": ""}
]
charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected", "CONNECTION_STATUS_UNSPECIFIED": "Unspecified",
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC"}
supported_switches = [
{"name": "Air Conditioning", "id": "climate_status", "icon": "air-conditioner"},
]
window_states = {"CLOSED": "OFF", "OPEN": "ON"}
door_states = {"CLOSED": "OFF", "OPEN": "ON"}
supported_locks = [
{"name": "Lock state", "id": "lock_status", "icon": "lock", "url": CAR_LOCK_STATE_URL}
]
icon_states = {
"lock_status": {"UNLOCKED": "lock-open-alert", "LOCKED": "lock"},
"door_front_left": {"ON": "car-door", "OFF": "car-door-lock"},
"door_front_right": {"ON": "car-door", "OFF": "car-door-lock"},
"door_rear_left": {"ON": "car-door", "OFF": "car-door-lock"},
"door_rear_right": {"ON": "car-door", "OFF": "car-door-lock"},
"battery_charge_level": [
{"from": 100, "to": 100, "icon": "battery"},
{"from": 99, "to": 90, "icon": "battery-90"},
{"from": 89, "to": 80, "icon": "battery-80"},
{"from": 79, "to": 70, "icon": "battery-70"},
{"from": 69, "to": 60, "icon": "battery-60"},
{"from": 59, "to": 50, "icon": "battery-50"},
{"from": 49, "to": 40, "icon": "battery-40"},
{"from": 39, "to": 30, "icon": "battery-30"},
{"from": 29, "to": 20, "icon": "battery-20"},
{"from": 19, "to": 10, "icon": "battery-10"},
{"from": 9, "to": 0, "icon": "battery-alert-variant-outline"},
]
}
supported_buttons = [
{"name": "Update Data", "id": "update_data", "icon": "update", "url": ""}
]
supported_entities = [
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL},
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": BATTERY_CHARGE_STATE_URL},
{"name": "Electric Range", "domain": "sensor", "id": "electric_range", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["electric_range"]["unit"], "icon": "map-marker-distance", "url": RECHARGE_STATE_URL},
{"name": "Estimated Charging Time", "domain": "sensor", "id": "estimated_charging_time", "unit": "minutes", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL},
{"name": "Charging System Status", "domain": "sensor", "id": "charging_system_status", "icon": "ev-station", "url": RECHARGE_STATE_URL},
{"name": "Charging Connection Status", "domain": "sensor", "id": "charging_connection_status", "icon": "ev-plug-ccs2", "url": RECHARGE_STATE_URL},
{"name": "Estimated Charging Finish Time", "domain": "sensor", "id": "estimated_charging_finish_time", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL},
{"name": "Odometer", "domain": "sensor", "id": "odometer", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["odometer"]["unit"], "icon": "counter", "url": ODOMETER_STATE_URL},
{"name": "Last Data Update", "domain": "sensor", "id": "last_data_update", "icon": "timer", "url": ""},
{"name": "Window Front Left", "domain": "binary_sensor", "device_class": "window", "id": "window_front_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Window Front Right", "domain": "binary_sensor", "device_class": "window", "id": "window_front_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Window Rear Left", "domain": "binary_sensor", "device_class": "window", "id": "window_rear_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Window Rear Right", "domain": "binary_sensor", "device_class": "window", "id": "window_rear_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Door Front Left", "domain": "binary_sensor", "device_class": "door", "id": "door_front_left", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Door Front Right", "domain": "binary_sensor", "device_class": "door", "id": "door_front_right", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Door Rear Left", "domain": "binary_sensor", "device_class": "door", "id": "door_rear_left", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Door Rear Right", "domain": "binary_sensor", "device_class": "door", "id": "door_rear_right", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Tailgate", "domain": "binary_sensor", "device_class": "door", "id": "tailgate", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Engine Hood", "domain": "binary_sensor", "device_class": "door", "id": "engine_hood", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Tank Lid", "domain": "binary_sensor", "device_class": "door", "id": "tank_lid", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Sunroof", "domain": "binary_sensor", "device_class": "door", "id": "sunroof", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Air Conditioning", "domain": "switch", "id": "climate_status", "icon": "air-conditioner"},
{"name": "Lock state", "domain": "lock", "id": "lock_status", "icon": "lock", "url": LOCK_STATE_URL},
{"name": "Force Update Data", "domain": "button", "id": "update_data", "icon": "update", "url": ""},
{"name": "Location", "domain": "device_tracker", "id": "location", "icon": "map-marker-radius", "url": LOCATION_STATE_URL},
{"name": "Tire Front Left", "domain": "sensor", "id": "tyre_front_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Front Right", "domain": "sensor", "id": "tyre_front_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Left", "domain": "sensor", "id": "tyre_rear_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Engine State", "domain": "sensor", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": "liters", "icon": "fuel", "url": FUEL_STATE_URL},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL},
{"name": "Distance to Empty", "domain": "sensor", "id": "distance_to_empty", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL},
{"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": "km/h" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL}
]
+6 -1
View File
@@ -1,9 +1,14 @@
import logging
from volvo import authorize
from mqtt import update_loop, connect
from const import VERSION
from util import set_tz, setup_logging
if __name__ == '__main__':
print("Starting volvo2mqtt version " + VERSION)
set_tz()
setup_logging()
logging.info("Starting volvo2mqtt version " + VERSION)
authorize()
connect()
update_loop()
+175 -144
View File
@@ -1,26 +1,38 @@
import logging
import time
import paho.mqtt.client as mqtt
import json
import volvo
import threading
import util
from threading import Thread, Timer
from datetime import datetime
from babel.dates import format_datetime
from config import settings
from const import VEHICLE_DETAILS_URL, CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \
CAR_UNLOCK_URL, supported_sensors, supported_buttons, supported_switches, supported_locks
from const import CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \
CAR_UNLOCK_URL, availability_topic, icon_states
mqtt_client: mqtt.Client
subscribed_topics = []
assumed_climate_state = {}
last_data_update = None
climate_timer = {}
door_status = {}
devices = {}
def connect():
client = mqtt.Client("volvoAAOS2mqtt")
client.will_set(availability_topic, "offline", 0, False)
if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
client.username_pw_set(settings["mqtt"]["username"], settings["mqtt"]["password"])
client.connect(settings["mqtt"]["broker"])
port = 1883
if util.keys_exists(settings["mqtt"], "port"):
conf_port = settings["mqtt"]["port"]
if isinstance(conf_port, int):
if conf_port > 0:
port = settings["mqtt"]["port"]
client.connect(settings["mqtt"]["broker"], port)
client.loop_start()
client.on_message = on_message
client.on_disconnect = on_disconnect
@@ -31,185 +43,204 @@ def connect():
def on_connect(client, userdata, flags, rc):
send_heartbeat()
if len(subscribed_topics) > 0:
for topic in subscribed_topics:
mqtt_client.subscribe(topic)
def on_disconnect(client, userdata, rc):
print("MQTT disconnected, reconnecting automatically")
logging.warning("MQTT disconnected, reconnecting automatically")
def on_message(client, userdata, msg):
if msg.topic in subscribed_topics:
try:
vin = msg.topic.split('/')[2].split('_')[0]
payload = msg.payload.decode("UTF-8")
if "climate_status" in msg.topic:
global assumed_climate_state
if payload == "ON":
api_thread = threading.Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin))
api_thread.start()
assumed_climate_state[vin] = "ON"
# Starting timer to disable climate after 30 mins
threading.Timer(30 * 60, volvo.disable_climate, (vin, )).start()
update_car_data()
elif payload == "OFF":
api_thread = threading.Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin))
api_thread.start()
assumed_climate_state[vin] = "OFF"
update_car_data()
elif "lock_status" in msg.topic:
if payload == "LOCK":
volvo.api_call(CAR_LOCK_URL, "POST", vin)
update_car_data()
elif payload == "UNLOCK":
volvo.api_call(CAR_UNLOCK_URL, "POST", vin)
update_car_data()
elif "update_data" in msg.topic:
if payload == "PRESS":
update_car_data()
except IndexError:
logging.error("Error - Cannot get vin from MQTT topic!")
return None
payload = msg.payload.decode("UTF-8")
if "climate_status" in msg.topic:
global assumed_climate_state, climate_timer, door_status
if payload == "ON":
# Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start()
# Start door check thread to turn off climate if driver door is opened
door_thread = Thread(target=volvo.check_door_status, args=(vin, ))
door_thread.start()
door_status[vin] = door_thread
# Starting timer to disable climate after 30 mins
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, ))
climate_timer[vin].start()
# Set and update switch status
assumed_climate_state[vin] = "ON"
update_car_data()
elif payload == "OFF":
# Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
# Stop door check thread if running
if door_status[vin].is_alive():
door_status[vin].do_run = False
# Stop climate timer if active
if climate_timer[vin].is_alive():
climate_timer[vin].cancel()
# Set and update switch status
assumed_climate_state[vin] = "OFF"
update_car_data()
elif "lock_status" in msg.topic:
if payload == "LOCK":
volvo.api_call(CAR_LOCK_URL, "POST", vin)
# Force set unlocked state in HA because of slow api response
update_car_data(True, {"entity_id": "lock_status", "vin": vin, "state": "LOCKED"})
elif payload == "UNLOCK":
volvo.api_call(CAR_UNLOCK_URL, "POST", vin)
# Force set unlocked state in HA because of slow api response
update_car_data(True, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKED"})
elif "update_data" in msg.topic:
if payload == "PRESS":
update_car_data(True)
def update_loop():
create_ha_devices()
while True:
print("Sending mqtt update...")
logging.info("Sending mqtt update...")
send_heartbeat()
update_car_data()
print("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
logging.info("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
time.sleep(settings["updateInterval"])
def update_car_data():
def update_car_data(force_update=False, overwrite={}):
global last_data_update
last_data_update = format_datetime(datetime.now(), format="medium", locale=settings["babelLocale"])
last_data_update = format_datetime(datetime.now(util.TZ), format="medium", locale=settings["babelLocale"])
for vin in volvo.vins:
for lock in supported_locks:
state = volvo.api_call(lock["url"], "GET", vin, lock["id"])
mqtt_client.publish(
f"homeassistant/lock/{vin}_{lock['id']}/state",
state
)
for entity in volvo.supported_endpoints[vin]:
if entity["domain"] == "button":
continue
for switch in supported_switches:
if switch["id"] == "climate_status":
ov_entity_id = ""
ov_vin = ""
ov_state = ""
if bool(overwrite):
ov_entity_id = overwrite["entity_id"]
ov_vin = overwrite["vin"]
ov_state = overwrite["state"]
if entity["id"] == "climate_status":
state = assumed_climate_state[vin]
else:
state = "OFF"
mqtt_client.publish(
f"homeassistant/switch/{vin}_{switch['id']}/state",
state
)
for sensor in supported_sensors:
if sensor["id"] == "last_data_update":
elif entity["id"] == "last_data_update":
state = last_data_update
else:
state = volvo.api_call(sensor["url"], "GET", vin, sensor["id"])
if entity["id"] == ov_entity_id and vin == ov_vin:
state = ov_state
else:
state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update)
if entity["domain"] == "device_tracker":
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
else:
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
mqtt_client.publish(
f"homeassistant/sensor/{vin}_{sensor['id']}/state",
state
topic,
json.dumps(state) if isinstance(state, dict) else state
)
update_ha_device(entity, vin, state)
def update_ha_device(entity, vin, state):
icon_config = icon_states.get(entity["id"])
if icon_config and state:
if state.replace(".", "").isnumeric():
state = float(state)
icon = util.get_icon_between(icon_config, state)
else:
icon = icon_config[state]
else:
return None
logging.debug("Updating icon to " + icon + " for " + entity["id"])
config = {
"name": entity['name'],
"object_id": f"volvo_{vin}_{entity['id']}",
"schema": "state",
"icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}",
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
"device": devices[vin],
"unique_id": f"volvoAAOS2mqtt_{vin}_{entity['id']}",
"availability_topic": availability_topic
}
if entity.get("device_class"):
config["device_class"] = entity["device_class"]
if entity.get("unit"):
config["unit_of_measurement"] = entity["unit"]
if entity.get("domain") == "device_tracker":
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
if entity.get("domain") in ["switch", "lock", "button"]:
command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command"
config["command_topic"] = command_topic
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
mqtt_client.publish(
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
json.dumps(config),
retain=True
)
def create_ha_devices():
global subscribed_topics, devices
for vin in volvo.vins:
car_details = volvo.api_call(VEHICLE_DETAILS_URL, "GET", vin)
for button in supported_buttons:
command_topic = f"homeassistant/button/{vin}_{button['id']}/command"
device = volvo.get_vehicle_details(vin)
devices[vin] = device
for entity in volvo.supported_endpoints[vin]:
config = {
"name": button['name'],
"object_id": button['id'],
"name": entity['name'],
"object_id": f"volvo_{vin}_{entity['id']}",
"schema": "state",
"icon": f"mdi:{button['icon']}",
"state_topic": f"homeassistant/button/{vin}_{button['id']}/state",
"command_topic": command_topic,
"device": {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
},
"unique_id": f"volvoAAOS2mqtt_{vin}_{button['id']}",
"icon": f"mdi:{entity['icon']}",
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
"device": device,
"unique_id": f"volvoAAOS2mqtt_{vin}_{entity['id']}",
"availability_topic": availability_topic
}
mqtt_client.publish(
f"homeassistant/button/volvoAAOS2mqtt/{vin}_{button['id']}/config",
json.dumps(config),
)
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
if entity.get("device_class"):
config["device_class"] = entity["device_class"]
for lock in supported_locks:
command_topic = f"homeassistant/lock/{vin}_{lock['id']}/command"
config = {
"name": lock['name'],
"object_id": lock['id'],
"schema": "state",
"icon": f"mdi:{lock['icon']}",
"state_topic": f"homeassistant/lock/{vin}_{lock['id']}/state",
"command_topic": command_topic,
"optimistic": False,
"device": {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
},
"unique_id": f"volvoAAOS2mqtt_{vin}_{lock['id']}",
}
mqtt_client.publish(
f"homeassistant/lock/volvoAAOS2mqtt/{vin}_{lock['id']}/config",
json.dumps(config),
)
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
if entity.get("unit"):
config["unit_of_measurement"] = entity["unit"]
for switch in supported_switches:
command_topic = f"homeassistant/switch/{vin}_{switch['id']}/command"
config = {
"name": switch['name'],
"object_id": switch['id'],
"schema": "state",
"icon": f"mdi:{switch['icon']}",
"state_topic": f"homeassistant/switch/{vin}_{switch['id']}/state",
"command_topic": command_topic,
"optimistic": False,
"device": {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
},
"unique_id": f"volvoAAOS2mqtt_{vin}_{switch['id']}",
}
mqtt_client.publish(
f"homeassistant/switch/volvoAAOS2mqtt/{vin}_{switch['id']}/config",
json.dumps(config),
)
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
if entity.get("domain") == "device_tracker":
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
for sensor in supported_sensors:
config = {
"name": sensor['name'],
"object_id": sensor['id'],
"schema": "state",
"icon": f"mdi:{sensor['icon']}",
"state_topic": f"homeassistant/sensor/{vin}_{sensor['id']}/state",
"device": {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
},
"unique_id": f"volvoAAOS2mqtt_{vin}_{sensor['id']}",
}
if "unit" in sensor:
config["unit_of_measurement"] = sensor["unit"]
if entity.get("domain") in ["switch", "lock", "button"]:
command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command"
config["command_topic"] = command_topic
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
mqtt_client.publish(
f"homeassistant/sensor/volvoAAOS2mqtt/{vin}_{sensor['id']}/config",
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
json.dumps(config),
retain=True
)
time.sleep(2)
send_heartbeat()
def send_heartbeat():
mqtt_client.publish(availability_topic, "online")
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
export IS_HA_ADDON="true"
python -u main.py
+6 -1
View File
@@ -1,8 +1,10 @@
{
"updateInterval": 300,
"babelLocale": "de",
"TZ": "Europe/Berlin",
"mqtt": {
"broker": "",
"port": 1883,
"username": "",
"password": ""
},
@@ -10,6 +12,9 @@
"username": "",
"password": "",
"vin": "",
"vccapikey": ""
"vccapikey": "",
"odometerMultiplier": "",
"averageSpeedDivider": "",
"averageFuelConsumptionMultiplier": ""
}
}
+19
View File
@@ -0,0 +1,19 @@
configuration:
updateInterval:
name: Update intervall
description: Time to wait between data pulls
babelLocale:
name: Babel locale
description: Select your country code or take a look at the GitHub repo for more infos!
TZ:
name: Timezone
description: Set your local timezone eg. "Europe/Berlin" or "America/New_York"
debug:
name: API debug mode
description: Enable Volvo API debug, normaly this can stay off
mqtt:
name: MQTT Broker settings
description: Broker is your mqtt broker IP eg "192.168.0.1". Mqtt username and password are optional. Broker port can be changed. If no value is given, port 1883 will be used.
volvoData:
name: Volvo configuration options
description: You have to enter your Volvo app credentials. The username is normally your email, and the password is the password you use for your Volvo app. The vin can stay empty. The Add-On will use any vin inside your Volvo account. VCCAPI key is required and comes from your Volvo developer account. Odometer multiplier is sometimes 10, sometimes 1. The same is applicable for average speed divider and average fuel consumption. Leave it as it is if you don't know what's right. Take a look at the GitHub repo for more information!
+89
View File
@@ -0,0 +1,89 @@
import logging
import pytz
import os
import sys
from logging import handlers
from datetime import datetime
from const import units
from config import settings
from pathlib import Path
TZ = None
def get_icon_between(icon_list, state):
icon = None
for s in icon_list:
if s["to"] <= state <= s["from"]:
icon = s["icon"]
return icon
def setup_logging():
log_location = "volvo2mqtt.log"
if os.environ.get("IS_HA_ADDON"):
check_existing_folder()
log_location = "/addons/volvo2mqtt/log/volvo2mqtt.log"
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
formatter = logging.Formatter(
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s',
'%b %d %H:%M:%S')
file_log_handler.setFormatter(formatter)
logger = logging.getLogger()
console_log_handler = logging.StreamHandler(sys.stdout)
console_log_handler.setFormatter(formatter)
logger.addHandler(console_log_handler)
logger.addHandler(file_log_handler)
logger.setLevel(logging.INFO)
if "debug" in settings:
if settings["debug"]:
logger.setLevel(logging.DEBUG)
def check_existing_folder():
Path("/addons/volvo2mqtt/log/").mkdir(parents=True, exist_ok=True)
def keys_exists(element, *keys):
""""
Check if *keys (nested) exists in `element` (dict).
Thanks stackoverflow: https://stackoverflow.com/questions/43491287/elegant-way-to-check-if-a-nested-key-exists-in-a-dict
"""
if not isinstance(element, dict):
raise AttributeError('keys_exists() expects dict as first argument.')
if len(keys) == 0:
raise AttributeError('keys_exists() expects at least two arguments, one given.')
_element = element
for key in keys:
try:
_element = _element[key]
except KeyError:
return False
return True
def set_tz():
global TZ
env_tz = os.environ.get("TZ")
settings_tz = settings.get("TZ")
if env_tz:
TZ = pytz.timezone(env_tz)
elif settings_tz:
TZ = pytz.timezone(settings_tz)
else:
raise Exception("No timezone setting found! Please read the README!")
def convert_metric_values(value):
if keys_exists(units, settings["babelLocale"]):
divider = units[settings["babelLocale"]]["divider"]
return round((float(value) / divider), 2)
else:
return value
+404 -204
View File
@@ -1,204 +1,404 @@
import requests
from datetime import datetime, timedelta
import mqtt
from config import settings
from babel.dates import format_datetime
from const import charging_system_states, CLIMATE_START_URL, \
OAUTH_URL, VEHICLES_URL, VEHICLE_DETAILS_URL, RECHARGE_STATUS_URL
session = requests.Session()
session.headers = {
"vcc-api-key": settings["volvoData"]["vccapikey"],
"content-type": "application/json",
"accept": "*/*"
}
token_expires_at: datetime
refresh_token = None
vins = []
recharge_response = {}
recharge_last_update = {}
def authorize():
headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json"
}
body = {
"username": settings.volvoData["username"],
"password": settings.volvoData["password"],
"grant_type": "password",
"scope": "openid email profile care_by_volvo:financial_information:invoice:read care_by_volvo:financial_information:payment_method care_by_volvo:subscription:read customer:attributes customer:attributes:write order:attributes vehicle:attributes tsp_customer_api:all conve:brake_status conve:climatization_start_stop conve:command_accessibility conve:commands conve:diagnostics_engine_status conve:diagnostics_workshop conve:doors_status conve:engine_status conve:environment conve:fuel_status conve:honk_flash conve:lock conve:lock_status conve:navigation conve:odometer_status conve:trip_statistics conve:tyre_status conve:unlock conve:vehicle_relation conve:warnings conve:windows_status energy:battery_charge_level energy:charging_connection_status energy:charging_system_status energy:electric_range energy:estimated_charging_time energy:recharge_status vehicle:attributes"
}
auth = requests.post(OAUTH_URL, data=body, headers=headers)
if auth.status_code == 200:
data = auth.json()
session.headers.update({'authorization': "Bearer " + data["access_token"]})
global token_expires_at, refresh_token
token_expires_at = datetime.now() + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"]
get_vehicles()
else:
message = auth.json()
raise Exception(message["error_description"])
def refresh_auth():
print("Refreshing credentials")
global refresh_token
headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json"
}
body = {
"grant_type": "refresh_token",
"refresh_token": refresh_token
}
auth = requests.post(OAUTH_URL, data=body, headers=headers)
if auth.status_code == 200:
data = auth.json()
session.headers.update({'authorization': "Bearer " + data["access_token"]})
global token_expires_at
token_expires_at = datetime.now() + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"]
def get_vehicles():
global vins
if not settings.volvoData["vin"]:
vehicles = session.get(VEHICLES_URL)
if vehicles.status_code == 200:
data = vehicles.json()
if len(data["data"]) > 0:
for vehicle in data["data"]:
vins.append(vehicle["vin"])
else:
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
else:
raise Exception("Error getting vehicles: " + str(vehicles.status_code))
else:
if isinstance(settings.volvoData["vin"], list):
# If setting is a list, copy
vins = settings.volvoData["vin"]
else:
# If setting is a string, append to list
vins.append(settings.volvoData["vin"])
if len(vins) == 0:
raise Exception("No vehicle found, exiting application!")
else:
initialize_climate(vins)
print("Vin: " + str(vins) + " found!")
def initialize_climate(vins):
for vin in vins:
mqtt.assumed_climate_state[vin] = "OFF"
def disable_climate(vin):
print("Turning climate off by timer!")
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
def api_call(url, method, vin, sensor_id=None):
global token_expires_at
if datetime.now() >= token_expires_at:
refresh_auth()
global recharge_response, recharge_last_update
if url == RECHARGE_STATUS_URL:
# Minimize API calls for recharge API
if not vin in recharge_response:
# No API Data for vin cached, get fresh data from API
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
recharge_response[vin] = response
recharge_last_update[vin] = datetime.now()
else:
if (datetime.now() - recharge_last_update[vin]).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updating
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
recharge_response[vin] = response
recharge_last_update[vin] = datetime.now()
else:
# Data is up do date, returning cached data
response = recharge_response[vin]
elif method == "GET":
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
elif method == "POST":
print("Starting " + method + " call against " + url)
response = session.post(url.format(vin), timeout=20)
else:
print("Unkown method posted: " + method + ". Returning nothing")
return ""
if response.status_code == 200:
data = response.json()
if "debug" in settings:
if settings["debug"]:
print(response.text)
else:
if url == CLIMATE_START_URL and response.status_code == 503:
print("Car in use, cannot start pre climatization")
mqtt.assumed_climate_state = "OFF"
mqtt.update_car_data()
else:
print("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
return ""
return parse_api_data(url, data, sensor_id)
def parse_api_data(url, data, sensor_id=None):
if url == VEHICLE_DETAILS_URL:
return data["data"]
elif sensor_id == "battery_charge_level":
if "batteryChargeLevel" in data["data"]:
return data["data"]["batteryChargeLevel"]["value"]
else:
return ""
elif sensor_id == "electric_range":
if "electricRange" in data["data"]:
return data["data"]["electricRange"]["value"]
else:
return ""
elif sensor_id == "charging_system_status":
if "chargingSystemStatus" in data["data"]:
return charging_system_states[data["data"]["chargingSystemStatus"]["value"]]
else:
return ""
elif sensor_id == "estimated_charging_time":
if "chargingSystemStatus" in data["data"]:
charging_system_state = charging_system_states[data["data"]["chargingSystemStatus"]["value"]]
if charging_system_state == "Charging":
return data["data"]["estimatedChargingTime"]["value"]
else:
return 0
else:
return ""
elif sensor_id == "estimated_charging_finish_time":
if "chargingSystemStatus" in data["data"]:
charging_system_state = charging_system_states[data["data"]["chargingSystemStatus"]["value"]]
if charging_system_state == "Charging":
charging_time = int(data["data"]["estimatedChargingTime"]["value"])
charging_finished = datetime.now() + timedelta(minutes=charging_time)
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
else:
return None
else:
return None
elif sensor_id == "lock_status":
return data["data"]["carLocked"]["value"]
else:
return ""
import logging
import requests
import mqtt
import util
import time
from threading import Thread, currentThread
from datetime import datetime, timedelta
from config import settings
from babel.dates import format_datetime
from const import charging_system_states, charging_connection_states, door_states, window_states, \
OAUTH_URL, VEHICLES_URL, VEHICLE_DETAILS_URL, RECHARGE_STATE_URL, CLIMATE_START_URL, \
WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, supported_entities, BATTERY_CHARGE_STATE_URL, \
STATISTICS_URL, CLIMATE_STOP_URL
session = requests.Session()
session.headers = {
"vcc-api-key": settings["volvoData"]["vccapikey"],
"content-type": "application/json",
"accept": "*/*"
}
token_expires_at: datetime
refresh_token = None
vins = []
supported_endpoints = {}
cached_requests = {}
def authorize():
headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json"
}
body = {
"username": settings.volvoData["username"],
"password": settings.volvoData["password"],
"grant_type": "password",
"scope": "openid email profile care_by_volvo:financial_information:invoice:read care_by_volvo:financial_information:payment_method care_by_volvo:subscription:read customer:attributes customer:attributes:write order:attributes vehicle:attributes tsp_customer_api:all conve:brake_status conve:climatization_start_stop conve:command_accessibility conve:commands conve:diagnostics_engine_status conve:diagnostics_workshop conve:doors_status conve:engine_status conve:environment conve:fuel_status conve:honk_flash conve:lock conve:lock_status conve:navigation conve:odometer_status conve:trip_statistics conve:tyre_status conve:unlock conve:vehicle_relation conve:warnings conve:windows_status energy:battery_charge_level energy:charging_connection_status energy:charging_system_status energy:electric_range energy:estimated_charging_time energy:recharge_status vehicle:attributes"
}
auth = requests.post(OAUTH_URL, data=body, headers=headers)
if auth.status_code == 200:
data = auth.json()
session.headers.update({'authorization': "Bearer " + data["access_token"]})
global token_expires_at, refresh_token
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"]
get_vehicles()
check_supported_endpoints()
else:
message = auth.json()
raise Exception(message["error_description"])
def refresh_auth():
logging.info("Refreshing credentials")
global refresh_token
headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json"
}
body = {
"grant_type": "refresh_token",
"refresh_token": refresh_token
}
try:
auth = requests.post(OAUTH_URL, data=body, headers=headers)
except requests.exceptions.RequestException as e:
logging.error("Error refreshing credentials data: " + str(e))
return None
if auth.status_code == 200:
data = auth.json()
session.headers.update({'authorization': "Bearer " + data["access_token"]})
global token_expires_at
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"]
def get_vehicles():
global vins
if not settings.volvoData["vin"]:
vehicles = session.get(VEHICLES_URL)
if vehicles.status_code == 200:
data = vehicles.json()
if len(data["data"]) > 0:
for vehicle in data["data"]:
vins.append(vehicle["vin"])
else:
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
else:
error = vehicles.json()
raise Exception(
"Error getting vehicles: " + str(vehicles.status_code) + ". " + str(error["error"].get("message")))
else:
if isinstance(settings.volvoData["vin"], list):
# If setting is a list, copy
vins = settings.volvoData["vin"]
else:
# If setting is a string, append to list
vins.append(settings.volvoData["vin"])
if len(vins) == 0:
raise Exception("No vehicle found, exiting application!")
else:
initialize_climate(vins)
logging.info("Vin: " + str(vins) + " found!")
def get_vehicle_details(vin):
response = session.get(VEHICLE_DETAILS_URL.format(vin), timeout=15)
if response.status_code == 200:
data = response.json()["data"]
logging.debug(response.text)
device = {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": data['descriptions']['model'],
"name": f"{data['descriptions']['model']} ({data['modelYear']}) - {vin}",
}
elif response.status_code == 500 and not settings.volvoData["vin"]:
# Workaround for some cars that are not returning vehicle details
device = {
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo",
"model": vin,
"name": f"Volvo - {vin}",
}
else:
raise Exception("Getting vehicle details failed. Status Code: " + str(response.status_code) +
". Error: " + response.text)
return device
def check_supported_endpoints():
global supported_endpoints
for vin in vins:
supported_endpoints[vin] = []
for entity in supported_entities:
if entity["id"] == "battery_charge_level" and entity["url"] == BATTERY_CHARGE_STATE_URL \
and any("battery_charge_level" in d["id"] for d in supported_endpoints[vin]):
# If battery charge level could be found in recharge-api, skip the second battery charge sensor
continue
if entity.get('url'):
state = api_call(entity["url"], "GET", vin, entity["id"])
else:
state = ""
if state is not None:
logging.info("Success! " + entity["name"] + " is supported by your vehicle.")
supported_endpoints[vin].append(entity)
else:
logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
def initialize_climate(vins):
for vin in vins:
mqtt.assumed_climate_state[vin] = "OFF"
def disable_climate(vin):
logging.info("Turning climate off by timer!")
mqtt.door_status[vin].do_run = False
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
def check_door_status(vin):
t = currentThread()
while getattr(t, "do_run", True):
lock_door_left = api_call(LOCK_STATE_URL, "GET", vin, "door_front_left", True)
lock_door_right = api_call(LOCK_STATE_URL, "GET", vin, "door_front_right", True)
if lock_door_left == "ON" or lock_door_right == "ON":
Thread(target=api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
break
time.sleep(5)
def api_call(url, method, vin, sensor_id=None, force_update=False):
global token_expires_at
if datetime.now(util.TZ) >= token_expires_at:
refresh_auth()
if url in [RECHARGE_STATE_URL, WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, STATISTICS_URL]:
# Minimize API calls for endpoints with multiple values
response = cached_request(url, method, vin, force_update)
if response is None:
# Exception caught while getting data from volvo api, doing nothing
return None
elif method == "GET":
logging.debug("Starting " + method + " call against " + url)
try:
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
logging.error("Error getting data: " + str(e))
return None
elif method == "POST":
logging.debug("Starting " + method + " call against " + url)
try:
response = session.post(url.format(vin), timeout=20)
except requests.exceptions.RequestException as e:
logging.error("Error getting data: " + str(e))
return None
else:
logging.error("Unkown method posted: " + method + ". Returning nothing")
return None
logging.debug("Response status code: " + str(response.status_code))
if response.status_code == 200:
data = response.json()
logging.debug(response.text)
else:
logging.debug(response.text)
if url == CLIMATE_START_URL and response.status_code == 503:
logging.warning("Car in use, cannot start pre climatization")
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
elif "extended-vehicle" in url and response.status_code == 403:
# Suppress 403 errors for unsupported extended-vehicle api cars
logging.debug("Suppressed 403 for extended-vehicle API")
return None
else:
logging.error("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
return None
return parse_api_data(data, sensor_id)
def cached_request(url, method, vin, force_update=False):
global cached_requests
if not util.keys_exists(cached_requests, vin + "_" + url):
# No API Data cached, get fresh data from API
logging.debug("Starting " + method + " call against " + url)
try:
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
logging.error("Error getting data: " + str(e))
return None
data = {"response": response, "last_update": datetime.now(util.TZ)}
cached_requests[vin + "_" + url] = data
else:
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() \
>= settings["updateInterval"] or (force_update and
(datetime.now(util.TZ) - cached_requests[vin + "_" + url]
["last_update"]).total_seconds() >= 2):
# Old Data in Cache, or force mode active, updating
logging.debug("Starting " + method + " call against " + url)
try:
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
logging.error("Error getting data: " + str(e))
return None
data = {"response": response, "last_update": datetime.now(util.TZ)}
cached_requests[vin + "_" + url] = data
else:
# Data is up do date, returning cached data
response = cached_requests[vin + "_" + url]["response"]
return response
def parse_api_data(data, sensor_id=None):
data = data["data"]
if sensor_id == "battery_charge_level":
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
elif sensor_id == "electric_range":
return util.convert_metric_values(data["electricRange"]["value"]) \
if util.keys_exists(data, "electricRange") else None
elif sensor_id == "charging_system_status":
return charging_system_states[data["chargingSystemStatus"]["value"]] \
if util.keys_exists(data, "chargingSystemStatus") else None
elif sensor_id == "charging_connection_status":
return charging_connection_states[data["chargingConnectionStatus"]["value"]] \
if util.keys_exists(data, "chargingConnectionStatus") else None
elif sensor_id == "estimated_charging_time":
if util.keys_exists(data, "chargingSystemStatus"):
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
if charging_system_state == "Charging":
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else ""
else:
return 0
return None
elif sensor_id == "estimated_charging_finish_time":
if util.keys_exists(data, "chargingSystemStatus"):
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
if charging_system_state == "Charging":
charging_time = int(
data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime")
else 0)
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time)
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
else:
return ""
return None
elif sensor_id == "lock_status":
return data["carLocked"]["value"] if util.keys_exists(data, "carLocked") else None
elif sensor_id == "odometer":
multiplier = 1
if util.keys_exists(settings["volvoData"], "odometerMultiplier"):
multiplier = settings["volvoData"]["odometerMultiplier"]
if isinstance(multiplier, str):
multiplier = 1
elif multiplier < 1:
multiplier = 1
return util.convert_metric_values(int(data["odometer"]["value"]) * multiplier) \
if util.keys_exists(data, "odometer") else None
elif sensor_id == "window_front_left":
return window_states[data["frontLeftWindowOpen"]["value"]] if util.keys_exists(data, "frontLeftWindowOpen") \
else None
elif sensor_id == "window_front_right":
return window_states[data["frontRightWindowOpen"]["value"]] if util.keys_exists(data, "frontRightWindowOpen") \
else None
elif sensor_id == "window_rear_left":
return window_states[data["rearLeftWindowOpen"]["value"]] if util.keys_exists(data, "rearLeftWindowOpen") \
else None
elif sensor_id == "window_rear_right":
return window_states[data["rearRightWindowOpen"]["value"]] if util.keys_exists(data, "rearRightWindowOpen") \
else None
elif sensor_id == "door_front_left":
return door_states[data["frontLeftDoorOpen"]["value"]] if util.keys_exists(data, "frontLeftDoorOpen") else None
elif sensor_id == "door_front_right":
return door_states[data["frontRightDoorOpen"]["value"]] \
if util.keys_exists(data, "frontRightDoorOpen") else None
elif sensor_id == "door_rear_left":
return door_states[data["rearLeftDoorOpen"]["value"]] if util.keys_exists(data, "rearLeftDoorOpen") else None
elif sensor_id == "door_rear_right":
return door_states[data["rearRightDoorOpen"]["value"]] if util.keys_exists(data, "rearRightDoorOpen") else None
elif sensor_id == "tailgate":
return door_states[data["tailGateOpen"]["value"]] if util.keys_exists(data, "tailGateOpen") else None
elif sensor_id == "sunroof":
return door_states[data["sunRoofOpen"]["value"]] if util.keys_exists(data, "sunRoofOpen") else None
elif sensor_id == "engine_hood":
return door_states[data["hoodOpen"]["value"]] if util.keys_exists(data, "hoodOpen") else None
elif sensor_id == "tank_lid":
return door_states[data["tankLidOpen"]["value"]] if util.keys_exists(data, "tankLidOpen") else None
elif sensor_id == "tyre_front_left":
return data["frontLeftTyrePressure"]["value"] if util.keys_exists(data, "frontLeftTyrePressure") else None
elif sensor_id == "tyre_front_right":
return data["frontRightTyrePressure"]["value"] if util.keys_exists(data, "frontRightTyrePressure") else None
elif sensor_id == "tyre_rear_left":
return data["rearLeftTyrePressure"]["value"] if util.keys_exists(data, "rearLeftTyrePressure") else None
elif sensor_id == "tyre_rear_right":
return data["rearRightTyrePressure"]["value"] if util.keys_exists(data, "rearRightTyrePressure") else None
elif sensor_id == "engine_state":
return data["engineRunning"]["value"] if util.keys_exists(data, "engineRunning") else None
elif sensor_id == "fuel_level":
if util.keys_exists(data, "fuelAmount"):
fuel_amount = int(data["fuelAmount"]["value"])
if fuel_amount > 0:
return fuel_amount
return None
elif sensor_id == "average_fuel_consumption":
if util.keys_exists(data, "averageFuelConsumption"):
average_fuel_con = float(data["averageFuelConsumption"]["value"])
if average_fuel_con > 0:
multiplier = 1
if util.keys_exists(settings["volvoData"], "averageFuelConsumptionMultiplier"):
multiplier = settings["volvoData"]["averageFuelConsumptionMultiplier"]
if isinstance(multiplier, str):
multiplier = 1
elif multiplier < 1:
multiplier = 1
return average_fuel_con * multiplier
return None
elif sensor_id == "average_speed":
if util.keys_exists(data, "averageSpeed"):
average_speed = int(data["averageSpeed"]["value"])
if average_speed > 1:
divider = 1
if util.keys_exists(settings["volvoData"], "averageSpeedDivider"):
divider = settings["volvoData"]["averageSpeedDivider"]
if isinstance(divider, str):
divider = 1
elif divider < 1:
divider = 1
return util.convert_metric_values(average_speed / divider)
return None
elif sensor_id == "location":
coordinates = {}
if util.keys_exists(data, "geometry"):
raw_data = data["geometry"]
if util.keys_exists(raw_data, "coordinates"):
coordinates = {"longitude": raw_data["coordinates"][0],
"latitude": raw_data["coordinates"][1],
"gps_accuracy": 1}
return coordinates
elif sensor_id == "distance_to_empty":
if util.keys_exists(data, "distanceToEmpty"):
distance_to_empty = int(data["distanceToEmpty"]["value"])
if distance_to_empty > 0:
return util.convert_metric_values(data["distanceToEmpty"]["value"])
return None
else:
return None