mirror of
https://github.com/Dielee/volvo2mqtt.git
synced 2026-08-12 10:49:52 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8b6f9cf53 | ||
|
|
7c32ca59be | ||
|
|
67948b0900 | ||
|
|
48663d2e0d | ||
|
|
76cfa21db7 | ||
|
|
9d11dff24b | ||
|
|
3a4160451c | ||
|
|
515e1a8d27 | ||
|
|
4961f7bfad | ||
|
|
a68216bdd5 | ||
|
|
98d8dd7e65 | ||
|
|
7acfdea1d8 | ||
|
|
f95348769b | ||
|
|
4673f14a1c | ||
|
|
1a2f35f1b5 | ||
|
|
33ee258f3f | ||
|
|
54db72df94 | ||
|
|
70b027fb70 | ||
|
|
1c07ec1b46 | ||
|
|
2fa154e023 | ||
|
|
ae5e3074ed | ||
|
|
f5a1e48e26 | ||
|
|
ee9d3cdef9 | ||
|
|
f1914e99c3 | ||
|
|
7a63f4d00f | ||
|
|
b644fe32fb |
@@ -29,6 +29,7 @@ If you like my work:<br>
|
|||||||
- XC40 PHEV (2021)
|
- XC40 PHEV (2021)
|
||||||
- V60 T8 PHEV (2023)
|
- V60 T8 PHEV (2023)
|
||||||
- V60 T6 PHEV (2025)
|
- V60 T6 PHEV (2025)
|
||||||
|
- V60 CC B5 Mildhybrid (2024)
|
||||||
- C40 BEV (2023)
|
- C40 BEV (2023)
|
||||||
- C40 BEV (2022)
|
- C40 BEV (2022)
|
||||||
- C40 BEV (2024)
|
- C40 BEV (2024)
|
||||||
@@ -128,7 +129,17 @@ The following steps are required for authentication in exactly this order:
|
|||||||
Just install this addon with the following command.
|
Just install this addon with the following command.
|
||||||
Please note to fill in your settings inside the environment variables.
|
Please note to fill in your settings inside the environment variables.
|
||||||
|
|
||||||
`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": ["key1", "key2"]}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest`
|
<pre>
|
||||||
|
<code>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": ["key1", "key2"]}' \
|
||||||
|
-e TZ='Europe/Berlin' \
|
||||||
|
--name volvo2mqtt \
|
||||||
|
ghcr.io/dielee/volvo2mqtt:latest
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
<b>HA Add-On:</b><br>
|
<b>HA Add-On:</b><br>
|
||||||
|
|
||||||
@@ -140,14 +151,15 @@ Here is what every option means:
|
|||||||
| ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- |
|
| ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- |
|
||||||
| `CONF_updateInterval` | `int` | | **required** | Update intervall in seconds. |
|
| `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_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` | `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` | `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` | `username` | optional | MQTT Username for your broker. |
|
||||||
| `CONF_mqtt` | `json` | `password` | optional | MQTT Password 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_mqtt` | `json` | `logging` | optional | Enable MQTT logging (true/false). Default don't enable MQTT log. |
|
||||||
| `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App.
|
| `CONF_volvoData` | `json` | `username` | **required** | Normally your email address 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` | `password` | **required** | Your password to login into the Volvo App. |
|
||||||
| `CONF_volvoData` | `json` | `vccapikey` | **required** | VCCAPIKEY linked with your volvo developer account. Get your Vccapi key from [here](https://developer.volvocars.com/account/). <b>Starting version 1.8.0, it is possible to define multiple keys, like this: `["vccapikey1", "vccapikey2", "vccapikey3", "etc..."]`</b>
|
| `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** | VCCAPIKEY linked with your volvo developer account. Get your Vccapi key from [here](https://developer.volvocars.com/account/). <b>Starting version 1.8.0, it is possible to define multiple keys, like this: `["vccapikey1", "vccapikey2", "vccapikey3", "etc..."]`</b> |
|
||||||
| `CONF_debug` | `string` | | optional | Debug option (true/false). Normally you don't need this. |
|
| `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)|
|
| `TZ` | `string` | | **required** | Container timezone eg "Europe/Berlin" from [here](https://docs.diladele.com/docker/timezones.html)|
|
||||||
|
|
||||||
@@ -159,6 +171,6 @@ Here is what every option means:
|
|||||||
[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg
|
[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg
|
||||||
[releases]: https://github.com/Dielee/volvo2mqtt/releases
|
[releases]: https://github.com/Dielee/volvo2mqtt/releases
|
||||||
[releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg
|
[releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg
|
||||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg
|
[maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
|
||||||
[commits-shield]: https://img.shields.io/github/commit-activity/y/Dielee/volvo2mqtt.svg
|
[commits-shield]: https://img.shields.io/github/commit-activity/y/Dielee/volvo2mqtt.svg
|
||||||
[commits]: https://github.com/Dielee/volvo2mqtt/commits/main
|
[commits]: https://github.com/Dielee/volvo2mqtt/commits/main
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
requests~=2.32.0
|
requests~=2.32.0
|
||||||
dynaconf~=3.1.12
|
dynaconf~=3.2.13
|
||||||
paho-mqtt~=1.6.1
|
paho-mqtt~=2.1.0
|
||||||
Babel~=2.12.1
|
Babel~=2.18.0
|
||||||
pytz~=2023.3
|
pytz~=2026.1.post1
|
||||||
google_play_scraper~=1.2.6
|
google_play_scraper~=1.2.6
|
||||||
+63
-1
@@ -1,10 +1,72 @@
|
|||||||
|
## v1.13.5
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Add build.yaml for homeassistant version > 2026.04.X
|
||||||
|
|
||||||
|
## v1.13.4
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fix `AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'` when paho-mqtt < 2.0 is installed
|
||||||
|
|
||||||
|
## v1.13.3
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Bump pypi packages
|
||||||
|
|
||||||
|
## v1.13.2
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fix KeyError: 'authorizeResponse' in auth flow #318
|
||||||
|
|
||||||
|
## v1.13.1
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fix missing charging system state `error` #314
|
||||||
|
- Move `object_id` to `default_entity_id` for mqtt entities according to #312
|
||||||
|
|
||||||
|
## v1.13.0
|
||||||
|
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Move energy api to v2 (https://developer.volvocars.com/apis/energy/v2/release-notes/)
|
||||||
|
|
||||||
|
## v1.12.2
|
||||||
|
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Add more logging for refresh process
|
||||||
|
- Update refresh token, if volvo API returns a new one
|
||||||
|
|
||||||
|
## v1.12.1
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fix `trip_speed` and `trip_distance` unit for `en_US` and `en_GB` #301
|
||||||
|
|
||||||
|
## v1.12.0
|
||||||
|
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Some minor corrections and more checking for exceptions #267
|
||||||
|
- Better logging, indicate URL used on API errors, check exceptions in more places to dont kill threads, add option for mqtt logging
|
||||||
|
- Dont force token renew on 5xx errors
|
||||||
|
- Indicate payload on error message to understand better issue #270
|
||||||
|
- Better checking on received update_interval #270
|
||||||
|
- Add TA trip statistics #283
|
||||||
|
|
||||||
|
Thanks to @luka6000 and @ivanfmartinez for your PRs!
|
||||||
|
|
||||||
## v1.10.6
|
## v1.10.6
|
||||||
|
|
||||||
### 🐛 Bug Fixes:
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
- Fix missing refresh_token and API changes by volvo. Thanks to @esusxunil.
|
- Fix missing refresh_token and API changes by volvo. Thanks to @esusxunil.
|
||||||
|
|
||||||
|
|
||||||
## v1.10.5
|
## v1.10.5
|
||||||
|
|
||||||
### 🐛 Bug Fixes:
|
### 🐛 Bug Fixes:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
build_from:
|
||||||
|
amd64: ghcr.io/home-assistant/base-python:latest
|
||||||
|
aarch64: ghcr.io/home-assistant/base-python:latest
|
||||||
|
armv7: ghcr.io/home-assistant/base-python:latest
|
||||||
|
armhf: ghcr.io/home-assistant/base-python:latest
|
||||||
|
i386: ghcr.io/home-assistant/base-python:latest
|
||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
name: "Volvo2Mqtt"
|
name: "Volvo2Mqtt"
|
||||||
description: "Volvo AAOS MQTT bridge"
|
description: "Volvo AAOS MQTT bridge"
|
||||||
version: "1.10.6"
|
version: "1.13.5"
|
||||||
slug: "volvo2mqtt"
|
slug: "volvo2mqtt"
|
||||||
init: false
|
init: false
|
||||||
url: "https://github.com/Dielee/volvo2mqtt"
|
url: "https://github.com/Dielee/volvo2mqtt"
|
||||||
@@ -21,6 +21,7 @@ options:
|
|||||||
port: "auto_port"
|
port: "auto_port"
|
||||||
username: "auto_user"
|
username: "auto_user"
|
||||||
password: "auto_password"
|
password: "auto_password"
|
||||||
|
logging: false
|
||||||
volvoData:
|
volvoData:
|
||||||
username: null
|
username: null
|
||||||
password: null
|
password: null
|
||||||
@@ -38,6 +39,7 @@ schema:
|
|||||||
port: str
|
port: str
|
||||||
username: str?
|
username: str?
|
||||||
password: str?
|
password: str?
|
||||||
|
logging: bool
|
||||||
volvoData:
|
volvoData:
|
||||||
username: match(^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)|(\+\d{5,20})$)
|
username: match(^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)|(\+\d{5,20})$)
|
||||||
password: str
|
password: str
|
||||||
|
|||||||
+21
-13
@@ -1,6 +1,6 @@
|
|||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
VERSION = "v1.10.6"
|
VERSION = "v1.13.5"
|
||||||
|
|
||||||
OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
|
OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
|
||||||
OAUTH_AUTH_URL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
|
OAUTH_AUTH_URL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
|
||||||
@@ -12,7 +12,7 @@ CLIMATE_STOP_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/
|
|||||||
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_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"
|
CAR_UNLOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/unlock"
|
||||||
RECHARGE_STATE_URL = "https://api.volvocars.com/energy/v1/vehicles/{0}/recharge-status"
|
RECHARGE_STATE_URL = "https://api.volvocars.com/energy/v2/vehicles/{0}/state"
|
||||||
ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/odometer"
|
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"
|
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"
|
TYRE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/tyres"
|
||||||
@@ -41,7 +41,9 @@ units = {
|
|||||||
"odometer": {"unit": LENGTH_MILES},
|
"odometer": {"unit": LENGTH_MILES},
|
||||||
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
||||||
"distance_to_empty": {"unit": LENGTH_MILES},
|
"distance_to_empty": {"unit": LENGTH_MILES},
|
||||||
"distance_to_service": {"unit": LENGTH_MILES}
|
"distance_to_service": {"unit": LENGTH_MILES},
|
||||||
|
"trip_distance": {"unit": LENGTH_MILES},
|
||||||
|
"trip_speed": {"unit": SPEED_MILES_PER_HOUR}
|
||||||
},
|
},
|
||||||
"en_US": {
|
"en_US": {
|
||||||
"divider": 1.60934,
|
"divider": 1.60934,
|
||||||
@@ -49,19 +51,21 @@ units = {
|
|||||||
"odometer": {"unit": LENGTH_MILES},
|
"odometer": {"unit": LENGTH_MILES},
|
||||||
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
||||||
"distance_to_empty": {"unit": LENGTH_MILES},
|
"distance_to_empty": {"unit": LENGTH_MILES},
|
||||||
"distance_to_service": {"unit": LENGTH_MILES}
|
"distance_to_service": {"unit": LENGTH_MILES},
|
||||||
|
"trip_distance": {"unit": LENGTH_MILES},
|
||||||
|
"trip_speed": {"unit": SPEED_MILES_PER_HOUR}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
availability_topic = "volvoAAOS2mqtt/availability"
|
availability_topic = "volvoAAOS2mqtt/availability"
|
||||||
|
|
||||||
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle",
|
charging_system_states = {"CHARGING": "Charging", "IDLE": "Idle",
|
||||||
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified",
|
"FAULT": "Fault", "UNSPECIFIED": "Unspecified",
|
||||||
"CHARGING_SYSTEM_DONE": "Done", "CHARGING_SYSTEM_SCHEDULED": "Scheduled"}
|
"DONE": "Done", "SCHEDULED": "Scheduled", "ERROR": "Error",
|
||||||
|
"DISCHARGING": "Discharging"}
|
||||||
|
|
||||||
charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected", "CONNECTION_STATUS_UNSPECIFIED": "Unspecified",
|
charging_connection_states = {"DISCONNECTED": "Disconnected", "UNSPECIFIED": "Unspecified",
|
||||||
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC",
|
"FAULT": "Fault", "CONNECTED": "Connected"}
|
||||||
"CONNECTION_STATUS_FAULT": "Fault"}
|
|
||||||
|
|
||||||
window_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
|
window_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
|
||||||
door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
|
door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
|
||||||
@@ -123,8 +127,8 @@ supported_entities = [
|
|||||||
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "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": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
|
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
|
||||||
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": VOLUME_LITERS, "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
|
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": VOLUME_LITERS, "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
|
||||||
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL},
|
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL, "state_class": "measurement"},
|
||||||
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL},
|
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL, "state_class": "measurement"},
|
||||||
{"name": "Distance to Empty Tank", "domain": "sensor", "id": "distance_to_empty_tank", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
|
{"name": "Distance to Empty Tank", "domain": "sensor", "id": "distance_to_empty_tank", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
|
||||||
{"name": "Distance to Empty Battery", "domain": "sensor", "id": "distance_to_empty_battery", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
|
{"name": "Distance to Empty Battery", "domain": "sensor", "id": "distance_to_empty_battery", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
|
||||||
{"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"},
|
{"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"},
|
||||||
@@ -136,8 +140,12 @@ supported_entities = [
|
|||||||
{"name": "API Backend status", "domain": "sensor", "id": "api_backend_status", "icon": "alert"},
|
{"name": "API Backend status", "domain": "sensor", "id": "api_backend_status", "icon": "alert"},
|
||||||
{"name": "Update Interval", "domain": "number", "id": "update_interval", "unit": "seconds", "icon": "timer", "min": -1, "max": 600, "mode": "box"},
|
{"name": "Update Interval", "domain": "number", "id": "update_interval", "unit": "seconds", "icon": "timer", "min": -1, "max": 600, "mode": "box"},
|
||||||
{"name": "Warnings", "domain": "sensor", "id": "warnings", "icon": "alert", "url": WARNINGS_URL}
|
{"name": "Warnings", "domain": "sensor", "id": "warnings", "icon": "alert", "url": WARNINGS_URL}
|
||||||
|
,{"name": "Trip Fuel Consumption", "domain": "sensor", "id": "trip_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL}
|
||||||
|
,{"name": "Trip distance", "domain": "sensor", "id": "trip_distance", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["trip_distance"]["unit"], "icon": "counter", "url": STATISTICS_URL, "state_class":"measurement"}
|
||||||
|
,{"name": "Trip Energy Consumption", "domain": "sensor", "id": "trip_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL}
|
||||||
|
,{"name": "Trip Speed", "domain": "sensor", "id": "trip_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["trip_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"}
|
||||||
]
|
]
|
||||||
|
|
||||||
old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"]
|
old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"]
|
||||||
otp_max_loops = 24
|
otp_max_loops = 24
|
||||||
otp_mqtt_topic = "volvoAAOS2mqtt/otp_code"
|
otp_mqtt_topic = "volvoAAOS2mqtt/otp_code"
|
||||||
|
|||||||
+73
-30
@@ -6,6 +6,7 @@ import volvo
|
|||||||
import util
|
import util
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
|
import threading
|
||||||
from threading import Thread, Timer
|
from threading import Thread, Timer
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime
|
||||||
@@ -24,8 +25,21 @@ active_schedules = {}
|
|||||||
otp_code = None
|
otp_code = None
|
||||||
|
|
||||||
def connect():
|
def connect():
|
||||||
client = mqtt.Client("volvoAAOS2mqtt") if os.environ.get("IS_HA_ADDON") \
|
client_id = "volvoAAOS2mqtt" if os.environ.get("IS_HA_ADDON") \
|
||||||
else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", ""))
|
else "volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", "")
|
||||||
|
try:
|
||||||
|
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, client_id)
|
||||||
|
except AttributeError:
|
||||||
|
# paho-mqtt < 2.0 does not have CallbackAPIVersion
|
||||||
|
client = mqtt.Client(client_id)
|
||||||
|
|
||||||
|
if "logging" in settings["mqtt"] and settings["mqtt"]["logging"]:
|
||||||
|
mqtt_logger = logging.getLogger("mqtt")
|
||||||
|
client.enable_logger(mqtt_logger)
|
||||||
|
|
||||||
|
client.on_message = safe_on_message
|
||||||
|
client.on_disconnect = on_disconnect
|
||||||
|
client.on_connect = on_connect
|
||||||
|
|
||||||
client.will_set(availability_topic, "offline", 0, False)
|
client.will_set(availability_topic, "offline", 0, False)
|
||||||
if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
|
if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
|
||||||
@@ -37,11 +51,9 @@ def connect():
|
|||||||
if conf_port > 0:
|
if conf_port > 0:
|
||||||
port = settings["mqtt"]["port"]
|
port = settings["mqtt"]["port"]
|
||||||
client.connect(settings["mqtt"]["broker"], port)
|
client.connect(settings["mqtt"]["broker"], port)
|
||||||
|
|
||||||
client.loop_start()
|
client.loop_start()
|
||||||
client.subscribe("volvoAAOS2mqtt/otp_code")
|
client.subscribe("volvoAAOS2mqtt/otp_code")
|
||||||
client.on_message = on_message
|
|
||||||
client.on_disconnect = on_disconnect
|
|
||||||
client.on_connect = on_connect
|
|
||||||
|
|
||||||
global mqtt_client
|
global mqtt_client
|
||||||
mqtt_client = client
|
mqtt_client = client
|
||||||
@@ -51,7 +63,7 @@ def create_otp_input():
|
|||||||
state_topic = otp_mqtt_topic + "/state"
|
state_topic = otp_mqtt_topic + "/state"
|
||||||
config = {
|
config = {
|
||||||
"name": "Volvo OTP",
|
"name": "Volvo OTP",
|
||||||
"object_id": f"volvo_otp",
|
"default_entity_id": "volvo_otp",
|
||||||
"schema": "state",
|
"schema": "state",
|
||||||
"command_topic": otp_mqtt_topic,
|
"command_topic": otp_mqtt_topic,
|
||||||
"state_topic": state_topic,
|
"state_topic": state_topic,
|
||||||
@@ -88,7 +100,7 @@ def send_car_images(vin, data, device):
|
|||||||
image_topic = f"homeassistant/image/{vin}_{entity['id']}/image_topic"
|
image_topic = f"homeassistant/image/{vin}_{entity['id']}/image_topic"
|
||||||
config = {
|
config = {
|
||||||
"name": entity["name"],
|
"name": entity["name"],
|
||||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||||
"schema": "state",
|
"schema": "state",
|
||||||
"icon": "mdi:image-area",
|
"icon": "mdi:image-area",
|
||||||
"content_type": "image/png",
|
"content_type": "image/png",
|
||||||
@@ -128,6 +140,10 @@ def send_car_images(vin, data, device):
|
|||||||
|
|
||||||
|
|
||||||
def on_connect(client, userdata, flags, rc):
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
# set a better name for the mqtt_loop Thread
|
||||||
|
threading.current_thread().name = 'mqtt_thread'
|
||||||
|
logging.info("MQTT connected")
|
||||||
|
|
||||||
send_heartbeat()
|
send_heartbeat()
|
||||||
if len(subscribed_topics) > 0:
|
if len(subscribed_topics) > 0:
|
||||||
for topic in subscribed_topics:
|
for topic in subscribed_topics:
|
||||||
@@ -135,9 +151,16 @@ def on_connect(client, userdata, flags, rc):
|
|||||||
|
|
||||||
|
|
||||||
def on_disconnect(client, userdata, rc):
|
def on_disconnect(client, userdata, rc):
|
||||||
logging.warning("MQTT disconnected, reconnecting automatically")
|
logging.warning(f"MQTT disconnected, reconnecting automatically rc={rc}")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_on_message(client, userdata, msg):
|
||||||
|
try:
|
||||||
|
on_message(client, userdata, msg)
|
||||||
|
except Exception as error:
|
||||||
|
logging.error(f"Exception {error} processing received message on topic {msg.topic}")
|
||||||
|
return None
|
||||||
|
|
||||||
def on_message(client, userdata, msg):
|
def on_message(client, userdata, msg):
|
||||||
payload = msg.payload.decode("UTF-8")
|
payload = msg.payload.decode("UTF-8")
|
||||||
if msg.topic == otp_mqtt_topic:
|
if msg.topic == otp_mqtt_topic:
|
||||||
@@ -152,40 +175,56 @@ def on_message(client, userdata, msg):
|
|||||||
try:
|
try:
|
||||||
vin = msg.topic.split('/')[2].split('_')[0]
|
vin = msg.topic.split('/')[2].split('_')[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.error("Error - Cannot get vin from MQTT topic!")
|
logging.error(f"Error - Cannot get vin from MQTT topic : {msg.topic}!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if "climate_status" in msg.topic:
|
if "climate_status" in msg.topic:
|
||||||
if payload == "ON":
|
if payload == "ON":
|
||||||
start_climate(vin)
|
start_climate(vin)
|
||||||
|
return None
|
||||||
elif payload == "OFF":
|
elif payload == "OFF":
|
||||||
stop_climate(vin)
|
stop_climate(vin)
|
||||||
|
return None
|
||||||
|
return None
|
||||||
elif "lock_status" in msg.topic:
|
elif "lock_status" in msg.topic:
|
||||||
if payload == "LOCK":
|
if payload == "LOCK":
|
||||||
lock_car(vin)
|
lock_car(vin)
|
||||||
|
return None
|
||||||
elif payload == "UNLOCK":
|
elif payload == "UNLOCK":
|
||||||
unlock_car(vin)
|
unlock_car(vin)
|
||||||
|
return None
|
||||||
|
return None
|
||||||
elif "update_data" in msg.topic:
|
elif "update_data" in msg.topic:
|
||||||
if payload == "PRESS":
|
if payload == "PRESS":
|
||||||
update_car_data(True)
|
update_car_data(True)
|
||||||
|
return None
|
||||||
|
return None
|
||||||
elif "update_interval" in msg.topic:
|
elif "update_interval" in msg.topic:
|
||||||
update_interval = int(payload)
|
try:
|
||||||
if update_interval >= 60 or update_interval == -1:
|
update_interval = int(payload)
|
||||||
settings.update({"updateInterval": int(update_interval)})
|
if update_interval >= 60 or update_interval == -1:
|
||||||
else:
|
settings.update({"updateInterval": int(update_interval)})
|
||||||
logging.warning("Interval " + str(update_interval) + " seconds is to low. Doing nothing!")
|
else:
|
||||||
update_car_data()
|
logging.warning(f"Interval {update_interval} seconds is to low. Doing nothing!")
|
||||||
|
update_car_data()
|
||||||
|
return None
|
||||||
|
except ValueError as error:
|
||||||
|
logging.error(f"Unable to change update_interval {error} payload={payload}")
|
||||||
|
return None
|
||||||
elif "schedule" in msg.topic:
|
elif "schedule" in msg.topic:
|
||||||
try:
|
try:
|
||||||
d = json.loads(payload)
|
d = json.loads(payload)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logging.error("Can't set timer. Error: " + str(e))
|
logging.error(f"Can't set timer. Error: {e}, payload={payload}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if d["mode"] == "timer":
|
if d["mode"] == "timer":
|
||||||
start_climate_timer(d, vin)
|
start_climate_timer(d, vin)
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
logging.warning("No schedule mode found, doing nothing")
|
logging.warning("No schedule mode found, doing nothing")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def start_climate_timer(d, vin):
|
def start_climate_timer(d, vin):
|
||||||
@@ -197,13 +236,13 @@ def start_climate_timer(d, vin):
|
|||||||
start_datetime = local_datetime.replace(hour=hour, minute=minute, second=0)
|
start_datetime = local_datetime.replace(hour=hour, minute=minute, second=0)
|
||||||
timer_seconds = (start_datetime - local_datetime).total_seconds()
|
timer_seconds = (start_datetime - local_datetime).total_seconds()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Error creating climate timer: " + str(e))
|
logging.error(f"Error creating climate timer: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if timer_seconds > 0:
|
if timer_seconds > 0:
|
||||||
Timer(timer_seconds, activate_climate_timer, (vin, start_datetime.isoformat(),)).start()
|
Timer(timer_seconds, activate_climate_timer, (vin, start_datetime.isoformat(),)).start()
|
||||||
active_schedules[vin]["timers"].append(start_datetime.isoformat())
|
active_schedules[vin]["timers"].append(start_datetime.isoformat())
|
||||||
logging.debug("Climate timer set to " + str(start_datetime))
|
logging.debug(f"Climate timer set to {start_datetime}")
|
||||||
update_car_data()
|
update_car_data()
|
||||||
else:
|
else:
|
||||||
logging.warning("Timer can not be set. Unusable start time entered")
|
logging.warning("Timer can not be set. Unusable start time entered")
|
||||||
@@ -211,28 +250,28 @@ def start_climate_timer(d, vin):
|
|||||||
|
|
||||||
def unlock_car(vin):
|
def unlock_car(vin):
|
||||||
# Start the api call in another thread for HA performance
|
# Start the api call in another thread for HA performance
|
||||||
Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin)).start()
|
Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin), name="unlock_car_thread").start()
|
||||||
|
|
||||||
# Force set unlocking state
|
# Force set unlocking state
|
||||||
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"})
|
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"})
|
||||||
# Fetch API lock state until unlocking finished
|
# Fetch API lock state until unlocking finished
|
||||||
Thread(target=volvo.check_lock_status, args=(vin, "LOCKED")).start()
|
Thread(target=volvo.check_lock_status, args=(vin, "LOCKED"), name="check_lock_status_thread").start()
|
||||||
|
|
||||||
|
|
||||||
def lock_car(vin):
|
def lock_car(vin):
|
||||||
# Start the api call in another thread for HA performance
|
# Start the api call in another thread for HA performance
|
||||||
Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin)).start()
|
Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin), name="lock_car_thread").start()
|
||||||
|
|
||||||
# Force set locking state
|
# Force set locking state
|
||||||
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"})
|
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"})
|
||||||
# Fetch API lock state until locking finished
|
# Fetch API lock state until locking finished
|
||||||
Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED")).start()
|
Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED"), name="check_lock_status_thread").start()
|
||||||
|
|
||||||
|
|
||||||
def stop_climate(vin):
|
def stop_climate(vin):
|
||||||
global assumed_climate_state, climate_timer, engine_status
|
global assumed_climate_state, climate_timer, engine_status
|
||||||
# Start the api call in another thread for HA performance
|
# Start the api call in another thread for HA performance
|
||||||
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
|
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin), name="stop_climate_thread").start()
|
||||||
|
|
||||||
# Stop door check thread if running
|
# Stop door check thread if running
|
||||||
if engine_status[vin].is_alive():
|
if engine_status[vin].is_alive():
|
||||||
@@ -256,10 +295,10 @@ def activate_climate_timer(vin, start_time):
|
|||||||
def start_climate(vin):
|
def start_climate(vin):
|
||||||
global assumed_climate_state, climate_timer, engine_status
|
global assumed_climate_state, climate_timer, engine_status
|
||||||
# Start the api call in another thread for HA performance
|
# Start the api call in another thread for HA performance
|
||||||
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start()
|
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin), name="start_climate_thread").start()
|
||||||
|
|
||||||
# Start door check thread to turn off climate if driver door is opened
|
# Start door check thread to turn off climate if driver door is opened
|
||||||
check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin,))
|
check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin,), name="check_engine_status_thread")
|
||||||
check_engine_thread.start()
|
check_engine_thread.start()
|
||||||
engine_status[vin] = check_engine_thread
|
engine_status[vin] = check_engine_thread
|
||||||
|
|
||||||
@@ -277,9 +316,12 @@ def update_loop():
|
|||||||
while True:
|
while True:
|
||||||
if settings["updateInterval"] > 0:
|
if settings["updateInterval"] > 0:
|
||||||
logging.info("Sending mqtt update...")
|
logging.info("Sending mqtt update...")
|
||||||
send_heartbeat()
|
try:
|
||||||
update_car_data()
|
send_heartbeat()
|
||||||
logging.info("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
|
update_car_data()
|
||||||
|
logging.info(f"Mqtt update done. Next run in {settings['updateInterval']} seconds.")
|
||||||
|
except Exception as error:
|
||||||
|
logging.info(f"Exception {error} in Mqtt update. Next run in {settings['updateInterval']} seconds.")
|
||||||
time.sleep(settings["updateInterval"])
|
time.sleep(settings["updateInterval"])
|
||||||
else:
|
else:
|
||||||
logging.info("Data update is disabled, doing nothing for 30 seconds")
|
logging.info("Data update is disabled, doing nothing for 30 seconds")
|
||||||
@@ -338,6 +380,7 @@ def update_car_data(force_update=False, overwrite={}):
|
|||||||
else:
|
else:
|
||||||
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
|
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
|
||||||
|
|
||||||
|
logging.info(f"update_car_data {topic} {state}")
|
||||||
if state or state == 0:
|
if state or state == 0:
|
||||||
mqtt_client.publish(
|
mqtt_client.publish(
|
||||||
topic,
|
topic,
|
||||||
@@ -367,7 +410,7 @@ def update_ha_device(entity, vin, state):
|
|||||||
logging.debug("Updating icon to " + icon + " for " + entity["id"])
|
logging.debug("Updating icon to " + icon + " for " + entity["id"])
|
||||||
config = {
|
config = {
|
||||||
"name": entity['name'],
|
"name": entity['name'],
|
||||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||||
"schema": "state",
|
"schema": "state",
|
||||||
"icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}",
|
"icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}",
|
||||||
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
||||||
@@ -408,7 +451,7 @@ def create_ha_devices():
|
|||||||
for entity in volvo.supported_endpoints[vin]:
|
for entity in volvo.supported_endpoints[vin]:
|
||||||
config = {
|
config = {
|
||||||
"name": entity['name'],
|
"name": entity['name'],
|
||||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||||
"schema": "state",
|
"schema": "state",
|
||||||
"icon": f"mdi:{entity['icon']}",
|
"icon": f"mdi:{entity['icon']}",
|
||||||
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
||||||
|
|||||||
+3
-5
@@ -8,15 +8,13 @@
|
|||||||
"broker": "",
|
"broker": "",
|
||||||
"port": 1883,
|
"port": 1883,
|
||||||
"username": "",
|
"username": "",
|
||||||
"password": ""
|
"password": "",
|
||||||
|
"logging": false
|
||||||
},
|
},
|
||||||
"volvoData": {
|
"volvoData": {
|
||||||
"username": "",
|
"username": "",
|
||||||
"password": "",
|
"password": "",
|
||||||
"vin": "",
|
"vin": "",
|
||||||
"vccapikey": [],
|
"vccapikey": []
|
||||||
"odometerMultiplier": "",
|
|
||||||
"averageSpeedDivider": "",
|
|
||||||
"averageFuelConsumptionMultiplier": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,4 @@ configuration:
|
|||||||
description: Leave the settings as they are if you are using the MQTT Mosquitto Addon. If not, take a look at the readme from volvo2mqtt.
|
description: Leave the settings as they are if you are using the MQTT Mosquitto Addon. If not, take a look at the readme from volvo2mqtt.
|
||||||
volvoData:
|
volvoData:
|
||||||
name: Volvo configuration options
|
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. This setting works as list, so it is possible to define multiple VCCAPI keys. 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!
|
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. This setting works as list, so it is possible to define multiple VCCAPI keys. Take a look at the GitHub repo for more information!
|
||||||
+1
-1
@@ -71,7 +71,7 @@ def setup_logging():
|
|||||||
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
|
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
|
||||||
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
|
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
|
||||||
formatter = logging.Formatter(
|
formatter = logging.Formatter(
|
||||||
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s',
|
'%(asctime)s volvo2mqtt [%(threadName)s] [%(process)d] - %(levelname)s: %(message)s',
|
||||||
'%b %d %H:%M:%S')
|
'%b %d %H:%M:%S')
|
||||||
file_log_handler.setFormatter(formatter)
|
file_log_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
|||||||
+259
-198
@@ -47,6 +47,11 @@ def authorize(renew_tokenfile=False):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
logging.warning("Detected corrupted token file, restarting auth process")
|
logging.warning("Detected corrupted token file, restarting auth process")
|
||||||
authorize(True)
|
authorize(True)
|
||||||
|
return
|
||||||
|
except KeyError:
|
||||||
|
logging.warning("Token file missing refresh_token, restarting auth process")
|
||||||
|
authorize(True)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
logging.info("Starting login with OTP")
|
logging.info("Starting login with OTP")
|
||||||
auth_session = requests.session()
|
auth_session = requests.session()
|
||||||
@@ -105,11 +110,11 @@ def authorize(renew_tokenfile=False):
|
|||||||
get_vcc_api_keys()
|
get_vcc_api_keys()
|
||||||
get_vehicles()
|
get_vehicles()
|
||||||
check_supported_endpoints()
|
check_supported_endpoints()
|
||||||
Thread(target=backend_status_loop).start()
|
Thread(target=backend_status_loop,name="backend_status_thread").start()
|
||||||
|
|
||||||
def continue_auth(auth_session, data):
|
def continue_auth(auth_session, data):
|
||||||
next_url = data["_links"]["continueAuthentication"]["href"].replace("http://", "https://") + "?action=continueAuthentication"
|
next_url = data["_links"]["continueAuthentication"]["href"].replace("http://", "https://") + "?action=continueAuthentication"
|
||||||
auth = auth_session.get(next_url)
|
auth = auth_session.post(next_url, data="{}")
|
||||||
|
|
||||||
if auth.status_code == 200:
|
if auth.status_code == 200:
|
||||||
return auth.json()
|
return auth.json()
|
||||||
@@ -170,8 +175,9 @@ def send_otp(auth_session, data):
|
|||||||
|
|
||||||
|
|
||||||
def refresh_auth():
|
def refresh_auth():
|
||||||
logging.info("Refreshing credentials")
|
|
||||||
global refresh_token
|
global refresh_token
|
||||||
|
logging.info("Refreshing credentials with token " + refresh_token)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
|
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
|
||||||
"content-type": "application/x-www-form-urlencoded",
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
@@ -191,24 +197,35 @@ def refresh_auth():
|
|||||||
|
|
||||||
if auth.status_code == 200:
|
if auth.status_code == 200:
|
||||||
token_path = util.get_token_path()
|
token_path = util.get_token_path()
|
||||||
|
if os.path.exists(token_path):
|
||||||
|
with open(token_path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
refresh_data = auth.json()
|
refresh_data = auth.json()
|
||||||
|
|
||||||
f = open(token_path)
|
if not "refresh_token" in refresh_data:
|
||||||
try:
|
f = open(token_path)
|
||||||
data = json.load(f)
|
try:
|
||||||
refresh_token = data["refresh_token"]
|
data = json.load(f)
|
||||||
except ValueError:
|
refresh_data["refresh_token"] = data["refresh_token"]
|
||||||
raise Exception("Cannot refresh token!")
|
except ValueError:
|
||||||
|
raise Exception("Cannot refresh token!")
|
||||||
|
else:
|
||||||
|
logging.info("New refresh token found, updating!")
|
||||||
|
|
||||||
refresh_data["refresh_token"] = refresh_token
|
|
||||||
util.save_to_json(refresh_data, token_path)
|
util.save_to_json(refresh_data, token_path)
|
||||||
session.headers.update({"authorization": "Bearer " + refresh_data["access_token"]})
|
session.headers.update({"authorization": "Bearer " + refresh_data["access_token"]})
|
||||||
|
|
||||||
global token_expires_at
|
global token_expires_at
|
||||||
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(refresh_data["expires_in"] - 30))
|
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
|
||||||
refresh_token = refresh_data["refresh_token"]
|
refresh_token = data["refresh_token"]
|
||||||
|
return None
|
||||||
|
elif int(auth.status_code / 100) == 5:
|
||||||
|
# Server error try again later
|
||||||
|
logging.warning("Refreshing credentials failed!: " + str(auth.status_code) + " Message: " + auth.text)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
# TODO check other codes that dont have to force the renew_tokenfile
|
||||||
logging.warning("Refreshing credentials failed!: " + str(auth.status_code) + " Message: " + auth.text)
|
logging.warning("Refreshing credentials failed!: " + str(auth.status_code) + " Message: " + auth.text)
|
||||||
authorize(renew_tokenfile=True)
|
authorize(renew_tokenfile=True)
|
||||||
return None
|
return None
|
||||||
@@ -445,31 +462,44 @@ def check_engine_status(vin):
|
|||||||
mqtt.assumed_climate_state[vin] = "OFF"
|
mqtt.assumed_climate_state[vin] = "OFF"
|
||||||
mqtt.update_car_data()
|
mqtt.update_car_data()
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def backend_status_loop():
|
def backend_status_loop():
|
||||||
|
logging.debug("backend_status_loop started")
|
||||||
while True:
|
while True:
|
||||||
get_backend_status()
|
last_status = get_backend_status()
|
||||||
# Update every hour
|
# Update every hour when not error, 10 minutes when got server error
|
||||||
time.sleep(3600)
|
time.sleep(3600 if not last_status.startswith("APIERR_") else 600)
|
||||||
|
|
||||||
|
|
||||||
def get_backend_status():
|
def get_backend_status():
|
||||||
global backend_status
|
global backend_status
|
||||||
response = session.get(API_BACKEND_STATUS, timeout=15)
|
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
response = session.get(API_BACKEND_STATUS, timeout=15)
|
||||||
if util.keys_exists(data, "message"):
|
try:
|
||||||
if data["message"]:
|
data = response.json()
|
||||||
backend_status = data["message"]
|
if util.keys_exists(data, "message"):
|
||||||
|
if data["message"]:
|
||||||
|
backend_status = data["message"]
|
||||||
|
else:
|
||||||
|
backend_status = "NO_WARNING"
|
||||||
else:
|
else:
|
||||||
backend_status = "NO_WARNING"
|
backend_status = "NO_WARNING"
|
||||||
else:
|
|
||||||
backend_status = "NO_WARNING"
|
|
||||||
|
|
||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
backend_status = "NO_WARNING"
|
logging.error(f"Invalid json response from API Backend Status - {response} - {e}")
|
||||||
|
backend_status = "APIERR_INVALID_JSON_RESPONSE"
|
||||||
|
|
||||||
|
except requests.exceptions.ReadTimeout as e:
|
||||||
|
logging.error(f"API Backend Status - timeout - {e}")
|
||||||
|
backend_status = "APIERR_TIMEOUT"
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"API Backend Status - Error - {e}")
|
||||||
|
backend_status = "APIERR_REQUEST_ERROR"
|
||||||
|
|
||||||
return backend_status
|
return backend_status
|
||||||
|
|
||||||
|
|
||||||
@@ -485,28 +515,28 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
|
|||||||
# Exception caught while getting data from volvo api, doing nothing
|
# Exception caught while getting data from volvo api, doing nothing
|
||||||
return None
|
return None
|
||||||
elif method == "GET":
|
elif method == "GET":
|
||||||
logging.debug("Starting " + method + " call against " + url)
|
logging.debug(f"Starting {method} call against {url}")
|
||||||
try:
|
try:
|
||||||
response = session.get(url.format(vin), timeout=15)
|
response = session.get(url.format(vin), timeout=15)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logging.error("Error getting data: " + str(e))
|
logging.error(f"Error getting data: {e} from {url}")
|
||||||
return None
|
return None
|
||||||
elif method == "POST":
|
elif method == "POST":
|
||||||
logging.debug("Starting " + method + " call against " + url)
|
logging.debug(f"Starting {method} call against {url}")
|
||||||
try:
|
try:
|
||||||
response = session.post(url.format(vin), timeout=20)
|
response = session.post(url.format(vin), timeout=20)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logging.error("Error getting data: " + str(e))
|
logging.error(f"Error getting data from {url} : {e}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
logging.error("Unkown method posted: " + method + ". Returning nothing")
|
logging.error(f"Unkown method posted: {method}. Returning nothing")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logging.debug("Response status code: " + str(response.status_code))
|
logging.debug(f"Response status code: {response.status_code}")
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
logging.error("Fetched json decode error, Volvo API seems to return garbage. Skipping update. Error: " + str(e))
|
logging.error(f"Fetched json decode error, Volvo API seems to return garbage. Skipping update. Error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -529,9 +559,9 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
|
|||||||
api_call(url, method, vin, sensor_id, force_update, True)
|
api_call(url, method, vin, sensor_id, force_update, True)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
|
f"API Call failed to {url}. Status Code: {response.status_code}. Error: {response.text}")
|
||||||
else:
|
else:
|
||||||
logging.error("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
|
logging.error(f"API Call failed to {url}. Status Code: {response.status_code}. Error: {response.text}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -539,11 +569,11 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
|
|||||||
global cached_requests
|
global cached_requests
|
||||||
if not util.keys_exists(cached_requests, vin + "_" + url):
|
if not util.keys_exists(cached_requests, vin + "_" + url):
|
||||||
# No API Data cached, get fresh data from API
|
# No API Data cached, get fresh data from API
|
||||||
logging.debug("Starting " + method + " call against " + url)
|
logging.debug(f"Starting {method} call against {url}")
|
||||||
try:
|
try:
|
||||||
response = session.get(url.format(vin), timeout=15)
|
response = session.get(url.format(vin), timeout=15)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logging.error("Error getting data: " + str(e))
|
logging.error(f"Error getting data from {url}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
||||||
@@ -555,11 +585,11 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
|
|||||||
"last_update"]).total_seconds() >= 2) \
|
"last_update"]).total_seconds() >= 2) \
|
||||||
or key_change:
|
or key_change:
|
||||||
# Old Data in Cache, or force mode active, updating
|
# Old Data in Cache, or force mode active, updating
|
||||||
logging.debug("Starting " + method + " call against " + url)
|
logging.debug(f"Starting {method} call against {url}")
|
||||||
try:
|
try:
|
||||||
response = session.get(url.format(vin), timeout=15)
|
response = session.get(url.format(vin), timeout=15)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logging.error("Error getting data: " + str(e))
|
logging.error(f"Error getting data from {url} : {e}")
|
||||||
return None
|
return None
|
||||||
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
||||||
cached_requests[vin + "_" + url] = data
|
cached_requests[vin + "_" + url] = data
|
||||||
@@ -570,171 +600,202 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
|
|||||||
|
|
||||||
|
|
||||||
def parse_api_data(data, sensor_id=None):
|
def parse_api_data(data, sensor_id=None):
|
||||||
if sensor_id != "api_backend_status":
|
if sensor_id in ["battery_charge_level", "electric_range", "charging_system_status",
|
||||||
data = data["data"]
|
"charging_connection_status", "estimated_charging_time", "estimated_charging_finish_time"]:
|
||||||
if sensor_id == "battery_charge_level":
|
|
||||||
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
|
if sensor_id == "battery_charge_level":
|
||||||
elif sensor_id == "battery_capacity":
|
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
|
||||||
return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") else None
|
elif sensor_id == "electric_range":
|
||||||
elif sensor_id == "electric_range":
|
return util.convert_metric_values(data["electricRange"]["value"]) \
|
||||||
return util.convert_metric_values(data["electricRange"]["value"]) \
|
if util.keys_exists(data, "electricRange") else None
|
||||||
if util.keys_exists(data, "electricRange") else None
|
elif sensor_id == "charging_system_status":
|
||||||
elif sensor_id == "charging_system_status":
|
return charging_system_states[data["chargingStatus"]["value"]] \
|
||||||
return charging_system_states[data["chargingSystemStatus"]["value"]] \
|
if util.keys_exists(data, "chargingStatus") else None
|
||||||
if util.keys_exists(data, "chargingSystemStatus") else None
|
elif sensor_id == "charging_connection_status":
|
||||||
elif sensor_id == "charging_connection_status":
|
return charging_connection_states[data["chargerConnectionStatus"]["value"]] \
|
||||||
return charging_connection_states[data["chargingConnectionStatus"]["value"]] \
|
if util.keys_exists(data, "chargerConnectionStatus") else None
|
||||||
if util.keys_exists(data, "chargingConnectionStatus") else None
|
elif sensor_id == "estimated_charging_time":
|
||||||
elif sensor_id == "estimated_charging_time":
|
if util.keys_exists(data, "chargingStatus"):
|
||||||
if util.keys_exists(data, "chargingSystemStatus"):
|
charging_system_state = charging_system_states[data["chargingStatus"]["value"]]
|
||||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
if charging_system_state == "Charging":
|
||||||
if charging_system_state == "Charging":
|
return data["estimatedChargingTimeToTargetBatteryChargeLevel"]["value"] if util.keys_exists(data,
|
||||||
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else ""
|
"estimatedChargingTimeToTargetBatteryChargeLevel") else ""
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return None
|
||||||
|
elif sensor_id == "estimated_charging_finish_time":
|
||||||
|
if util.keys_exists(data, "estimatedChargingTimeToTargetBatteryChargeLevel"):
|
||||||
|
charging_system_state = charging_system_states[data["chargingStatus"]["value"]]
|
||||||
|
if charging_system_state == "Charging":
|
||||||
|
charging_time = int(
|
||||||
|
data["estimatedChargingTimeToTargetBatteryChargeLevel"]["value"] if util.keys_exists(data, "estimatedChargingTimeToTargetBatteryChargeLevel")
|
||||||
|
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
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if sensor_id != "api_backend_status":
|
||||||
|
data = data["data"]
|
||||||
|
|
||||||
|
if sensor_id == "lock_status":
|
||||||
|
return data["centralLock"]["value"] if util.keys_exists(data, "centralLock") else None
|
||||||
|
elif sensor_id == "battery_capacity":
|
||||||
|
return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") else None
|
||||||
|
elif sensor_id == "odometer":
|
||||||
|
return util.convert_metric_values(int(data["odometer"]["value"])) \
|
||||||
|
if util.keys_exists(data, "odometer") else None
|
||||||
|
elif sensor_id == "window_front_left":
|
||||||
|
return window_states[data["frontLeftWindow"]["value"]] if util.keys_exists(data, "frontLeftWindow") \
|
||||||
|
else None
|
||||||
|
elif sensor_id == "window_front_right":
|
||||||
|
return window_states[data["frontRightWindow"]["value"]] if util.keys_exists(data, "frontRightWindow") \
|
||||||
|
else None
|
||||||
|
elif sensor_id == "window_rear_left":
|
||||||
|
return window_states[data["rearLeftWindow"]["value"]] if util.keys_exists(data, "rearLeftWindow") \
|
||||||
|
else None
|
||||||
|
elif sensor_id == "window_rear_right":
|
||||||
|
return window_states[data["rearRightWindow"]["value"]] if util.keys_exists(data, "rearRightWindow") \
|
||||||
|
else None
|
||||||
|
elif sensor_id == "door_front_left":
|
||||||
|
return door_states[data["frontLeftDoor"]["value"]] if util.keys_exists(data, "frontLeftDoor") else None
|
||||||
|
elif sensor_id == "door_front_right":
|
||||||
|
return door_states[data["frontRightDoor"]["value"]] \
|
||||||
|
if util.keys_exists(data, "frontRightDoor") else None
|
||||||
|
elif sensor_id == "door_rear_left":
|
||||||
|
return door_states[data["rearLeftDoor"]["value"]] if util.keys_exists(data, "rearLeftDoor") else None
|
||||||
|
elif sensor_id == "door_rear_right":
|
||||||
|
return door_states[data["rearRightDoor"]["value"]] if util.keys_exists(data, "rearRightDoor") else None
|
||||||
|
elif sensor_id == "tailgate":
|
||||||
|
return door_states[data["tailgate"]["value"]] if util.keys_exists(data, "tailgate") else None
|
||||||
|
elif sensor_id == "sunroof":
|
||||||
|
return door_states[data["sunroof"]["value"]] if util.keys_exists(data, "sunroof") else None
|
||||||
|
elif sensor_id == "engine_hood":
|
||||||
|
return door_states[data["hood"]["value"]] if util.keys_exists(data, "hood") else None
|
||||||
|
elif sensor_id == "tank_lid":
|
||||||
|
return door_states[data["tankLid"]["value"]] if util.keys_exists(data, "tankLid") else None
|
||||||
|
elif sensor_id == "tyre_front_left":
|
||||||
|
return data["frontLeft"]["value"] if util.keys_exists(data, "frontLeft") else None
|
||||||
|
elif sensor_id == "tyre_front_right":
|
||||||
|
return data["frontRight"]["value"] if util.keys_exists(data, "frontRight") else None
|
||||||
|
elif sensor_id == "tyre_rear_left":
|
||||||
|
return data["rearLeft"]["value"] if util.keys_exists(data, "rearLeft") else None
|
||||||
|
elif sensor_id == "tyre_rear_right":
|
||||||
|
return data["rearRight"]["value"] if util.keys_exists(data, "rearRight") else None
|
||||||
|
elif sensor_id == "engine_state":
|
||||||
|
return engine_states[data["engineStatus"]["value"]] if util.keys_exists(data, "engineStatus") else None
|
||||||
|
elif sensor_id == "fuel_level":
|
||||||
|
if util.keys_exists(data, "fuelAmount"):
|
||||||
|
fuel_amount = float(data["fuelAmount"]["value"])
|
||||||
|
if fuel_amount > 0:
|
||||||
|
return fuel_amount
|
||||||
|
return None
|
||||||
|
elif sensor_id == "average_fuel_consumption":
|
||||||
|
average_fuel_con = 0
|
||||||
|
if util.keys_exists(data, "averageFuelConsumption"):
|
||||||
|
average_fuel_con = float(data["averageFuelConsumption"]["value"])
|
||||||
|
elif util.keys_exists(data, "averageFuelConsumptionAutomatic"):
|
||||||
|
average_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
|
||||||
|
|
||||||
|
if average_fuel_con > 0:
|
||||||
|
return average_fuel_con
|
||||||
|
return None
|
||||||
|
elif sensor_id == "average_speed":
|
||||||
|
average_speed = 0
|
||||||
|
if util.keys_exists(data, "averageSpeed"):
|
||||||
|
average_speed = float(data["averageSpeed"]["value"])
|
||||||
|
elif util.keys_exists(data, "averageSpeedAutomatic"):
|
||||||
|
average_speed = float(data["averageSpeedAutomatic"]["value"])
|
||||||
|
|
||||||
|
if average_speed != 0:
|
||||||
|
return util.convert_metric_values(average_speed)
|
||||||
|
|
||||||
|
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_tank":
|
||||||
|
if util.keys_exists(data, "distanceToEmptyTank"):
|
||||||
|
distance_to_empty = int(data["distanceToEmptyTank"]["value"])
|
||||||
|
if distance_to_empty > 0:
|
||||||
|
return util.convert_metric_values(data["distanceToEmptyTank"]["value"])
|
||||||
|
return None
|
||||||
|
elif sensor_id == "distance_to_empty_battery":
|
||||||
|
if util.keys_exists(data, "distanceToEmptyBattery"):
|
||||||
|
distance_to_empty = int(data["distanceToEmptyBattery"]["value"])
|
||||||
|
if distance_to_empty > 0:
|
||||||
|
return util.convert_metric_values(data["distanceToEmptyBattery"]["value"])
|
||||||
|
return None
|
||||||
|
elif sensor_id == "hours_to_service":
|
||||||
|
return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None
|
||||||
|
elif sensor_id == "km_to_service":
|
||||||
|
if util.keys_exists(data, "distanceToService"):
|
||||||
|
km_to_service = int(data["distanceToService"]["value"])
|
||||||
|
if km_to_service > 0:
|
||||||
|
return util.convert_metric_values(data["distanceToService"]["value"])
|
||||||
|
return None
|
||||||
|
elif sensor_id == "time_to_service":
|
||||||
|
if util.keys_exists(data, "timeToService"):
|
||||||
|
return str(data["timeToService"]["value"]) + " " + data["timeToService"]["unit"]
|
||||||
else:
|
else:
|
||||||
return 0
|
return None
|
||||||
return None
|
elif sensor_id == "service_warning_status":
|
||||||
elif sensor_id == "estimated_charging_finish_time":
|
return data["serviceWarning"]["value"] if util.keys_exists(data, "serviceWarning") else None
|
||||||
if util.keys_exists(data, "chargingSystemStatus"):
|
elif sensor_id == "average_energy_consumption":
|
||||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
average_energy_con = 0
|
||||||
if charging_system_state == "Charging":
|
if util.keys_exists(data, "averageEnergyConsumption"):
|
||||||
charging_time = int(
|
average_energy_con = data["averageEnergyConsumption"]["value"]
|
||||||
data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime")
|
elif util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
|
||||||
else 0)
|
average_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
|
||||||
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["centralLock"]["value"] if util.keys_exists(data, "centralLock") else None
|
|
||||||
elif sensor_id == "odometer":
|
|
||||||
return util.convert_metric_values(int(data["odometer"]["value"])) \
|
|
||||||
if util.keys_exists(data, "odometer") else None
|
|
||||||
elif sensor_id == "window_front_left":
|
|
||||||
return window_states[data["frontLeftWindow"]["value"]] if util.keys_exists(data, "frontLeftWindow") \
|
|
||||||
else None
|
|
||||||
elif sensor_id == "window_front_right":
|
|
||||||
return window_states[data["frontRightWindow"]["value"]] if util.keys_exists(data, "frontRightWindow") \
|
|
||||||
else None
|
|
||||||
elif sensor_id == "window_rear_left":
|
|
||||||
return window_states[data["rearLeftWindow"]["value"]] if util.keys_exists(data, "rearLeftWindow") \
|
|
||||||
else None
|
|
||||||
elif sensor_id == "window_rear_right":
|
|
||||||
return window_states[data["rearRightWindow"]["value"]] if util.keys_exists(data, "rearRightWindow") \
|
|
||||||
else None
|
|
||||||
elif sensor_id == "door_front_left":
|
|
||||||
return door_states[data["frontLeftDoor"]["value"]] if util.keys_exists(data, "frontLeftDoor") else None
|
|
||||||
elif sensor_id == "door_front_right":
|
|
||||||
return door_states[data["frontRightDoor"]["value"]] \
|
|
||||||
if util.keys_exists(data, "frontRightDoor") else None
|
|
||||||
elif sensor_id == "door_rear_left":
|
|
||||||
return door_states[data["rearLeftDoor"]["value"]] if util.keys_exists(data, "rearLeftDoor") else None
|
|
||||||
elif sensor_id == "door_rear_right":
|
|
||||||
return door_states[data["rearRightDoor"]["value"]] if util.keys_exists(data, "rearRightDoor") else None
|
|
||||||
elif sensor_id == "tailgate":
|
|
||||||
return door_states[data["tailgate"]["value"]] if util.keys_exists(data, "tailgate") else None
|
|
||||||
elif sensor_id == "sunroof":
|
|
||||||
return door_states[data["sunroof"]["value"]] if util.keys_exists(data, "sunroof") else None
|
|
||||||
elif sensor_id == "engine_hood":
|
|
||||||
return door_states[data["hood"]["value"]] if util.keys_exists(data, "hood") else None
|
|
||||||
elif sensor_id == "tank_lid":
|
|
||||||
return door_states[data["tankLid"]["value"]] if util.keys_exists(data, "tankLid") else None
|
|
||||||
elif sensor_id == "tyre_front_left":
|
|
||||||
return data["frontLeft"]["value"] if util.keys_exists(data, "frontLeft") else None
|
|
||||||
elif sensor_id == "tyre_front_right":
|
|
||||||
return data["frontRight"]["value"] if util.keys_exists(data, "frontRight") else None
|
|
||||||
elif sensor_id == "tyre_rear_left":
|
|
||||||
return data["rearLeft"]["value"] if util.keys_exists(data, "rearLeft") else None
|
|
||||||
elif sensor_id == "tyre_rear_right":
|
|
||||||
return data["rearRight"]["value"] if util.keys_exists(data, "rearRight") else None
|
|
||||||
elif sensor_id == "engine_state":
|
|
||||||
return engine_states[data["engineStatus"]["value"]] if util.keys_exists(data, "engineStatus") else None
|
|
||||||
elif sensor_id == "fuel_level":
|
|
||||||
if util.keys_exists(data, "fuelAmount"):
|
|
||||||
fuel_amount = float(data["fuelAmount"]["value"])
|
|
||||||
if fuel_amount > 0:
|
|
||||||
return fuel_amount
|
|
||||||
return None
|
|
||||||
elif sensor_id == "average_fuel_consumption":
|
|
||||||
average_fuel_con = 0
|
|
||||||
if util.keys_exists(data, "averageFuelConsumption"):
|
|
||||||
average_fuel_con = float(data["averageFuelConsumption"]["value"])
|
|
||||||
elif util.keys_exists(data, "averageFuelConsumptionAutomatic"):
|
|
||||||
average_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
|
|
||||||
|
|
||||||
if average_fuel_con > 0:
|
if average_energy_con != 0:
|
||||||
return average_fuel_con
|
return average_energy_con
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "average_speed":
|
elif sensor_id == "washer_fluid_warning":
|
||||||
average_speed = 0
|
return data["washerFluidLevelWarning"]["value"] if util.keys_exists(data, "washerFluidLevelWarning") else None
|
||||||
if util.keys_exists(data, "averageSpeed"):
|
elif sensor_id == "warnings":
|
||||||
average_speed = float(data["averageSpeed"]["value"])
|
warnings = 0
|
||||||
elif util.keys_exists(data, "averageSpeedAutomatic"):
|
cleaned_data = {}
|
||||||
average_speed = float(data["averageSpeedAutomatic"]["value"])
|
for key, dicts in data.items():
|
||||||
|
contains_data = sum(value == "NO_WARNING" or value == "FAILURE" for value in dicts.values())
|
||||||
|
if contains_data:
|
||||||
|
warnings = warnings + 1
|
||||||
|
cleaned_data[key] = dicts["value"]
|
||||||
|
|
||||||
if average_speed != 0:
|
return cleaned_data if warnings > 0 else None
|
||||||
return util.convert_metric_values(average_speed)
|
elif sensor_id == "trip_fuel_consumption":
|
||||||
|
trip_fuel_con = 0
|
||||||
return None
|
if util.keys_exists(data, "averageFuelConsumptionAutomatic"):
|
||||||
elif sensor_id == "location":
|
trip_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
|
||||||
coordinates = {}
|
if trip_fuel_con > 0:
|
||||||
if util.keys_exists(data, "geometry"):
|
return trip_fuel_con
|
||||||
raw_data = data["geometry"]
|
return None
|
||||||
if util.keys_exists(raw_data, "coordinates"):
|
elif sensor_id == "trip_distance":
|
||||||
coordinates = {"longitude": raw_data["coordinates"][0],
|
return util.convert_metric_values(int(data["tripMeterAutomatic"]["value"])) \
|
||||||
"latitude": raw_data["coordinates"][1],
|
if util.keys_exists(data, "tripMeterAutomatic") else None
|
||||||
"gps_accuracy": 1}
|
elif sensor_id == "trip_energy_consumption":
|
||||||
return coordinates
|
trip_energy_con = 0
|
||||||
elif sensor_id == "distance_to_empty_tank":
|
if util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
|
||||||
if util.keys_exists(data, "distanceToEmptyTank"):
|
trip_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
|
||||||
distance_to_empty = int(data["distanceToEmptyTank"]["value"])
|
if trip_energy_con != 0:
|
||||||
if distance_to_empty > 0:
|
return trip_energy_con
|
||||||
return util.convert_metric_values(data["distanceToEmptyTank"]["value"])
|
return None
|
||||||
return None
|
elif sensor_id == "trip_speed":
|
||||||
elif sensor_id == "distance_to_empty_battery":
|
trip_speed = 0
|
||||||
if util.keys_exists(data, "distanceToEmptyBattery"):
|
if util.keys_exists(data, "averageSpeedAutomatic"):
|
||||||
distance_to_empty = int(data["distanceToEmptyBattery"]["value"])
|
trip_speed = float(data["averageSpeedAutomatic"]["value"])
|
||||||
if distance_to_empty > 0:
|
if trip_speed != 0:
|
||||||
return util.convert_metric_values(data["distanceToEmptyBattery"]["value"])
|
return util.convert_metric_values(trip_speed)
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "hours_to_service":
|
|
||||||
return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None
|
|
||||||
elif sensor_id == "km_to_service":
|
|
||||||
if util.keys_exists(data, "distanceToService"):
|
|
||||||
km_to_service = int(data["distanceToService"]["value"])
|
|
||||||
if km_to_service > 0:
|
|
||||||
return util.convert_metric_values(data["distanceToService"]["value"])
|
|
||||||
return None
|
|
||||||
elif sensor_id == "time_to_service":
|
|
||||||
if util.keys_exists(data, "timeToService"):
|
|
||||||
return str(data["timeToService"]["value"]) + " " + data["timeToService"]["unit"]
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "service_warning_status":
|
|
||||||
return data["serviceWarning"]["value"] if util.keys_exists(data, "serviceWarning") else None
|
|
||||||
elif sensor_id == "average_energy_consumption":
|
|
||||||
average_energy_con = 0
|
|
||||||
if util.keys_exists(data, "averageEnergyConsumption"):
|
|
||||||
average_energy_con = data["averageEnergyConsumption"]["value"]
|
|
||||||
elif util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
|
|
||||||
average_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
|
|
||||||
|
|
||||||
if average_energy_con != 0:
|
|
||||||
return average_energy_con
|
|
||||||
return None
|
|
||||||
elif sensor_id == "washer_fluid_warning":
|
|
||||||
return data["washerFluidLevelWarning"]["value"] if util.keys_exists(data, "washerFluidLevelWarning") else None
|
|
||||||
elif sensor_id == "warnings":
|
|
||||||
warnings = 0
|
|
||||||
cleaned_data = {}
|
|
||||||
for key, dicts in data.items():
|
|
||||||
contains_data = sum(value == "NO_WARNING" or value == "FAILURE" for value in dicts.values())
|
|
||||||
if contains_data:
|
|
||||||
warnings = warnings + 1
|
|
||||||
cleaned_data[key] = dicts["value"]
|
|
||||||
|
|
||||||
return cleaned_data if warnings > 0 else None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user