Compare commits

...
18 Commits
Author SHA1 Message Date
linus 9d11dff24b Fix #318 2026-03-23 07:47:15 +01:00
bazza2000andGitHub 3a4160451c Fix: use POST for continueAuthentication endpoint (#317)
Volvo's PingFederate auth API now requires a POST request for the
continueAuthentication step after OTP verification. Using GET returns
the same OTP_VERIFIED state in a loop, never progressing to COMPLETED,
which causes a KeyError on 'authorizeResponse'.

Changing auth_session.get() to auth_session.post() with an empty JSON
body causes the API to return COMPLETED with the expected authorizeResponse.
2026-03-23 07:43:53 +01:00
linus 515e1a8d27 Bump addon version 2026-01-02 18:17:31 +01:00
linus 4961f7bfad Move readme to 2026 maintenance 2026-01-02 18:16:30 +01:00
linus a68216bdd5 Move object_id to default_entity_id for mqtt entities according to #312 2026-01-02 18:12:47 +01:00
linus 98d8dd7e65 Fix missing charging system state 2026-01-02 18:06:30 +01:00
linus 7acfdea1d8 Move energy api to v2 2025-11-16 08:36:30 +01:00
linus f95348769b Fix missing local refresh file while loading from filesystem 2025-11-12 14:04:52 +01:00
linus 4673f14a1c Add more logging for refresh process // renew refresh token if returned from api 2025-08-23 11:25:38 +02:00
linus 1a2f35f1b5 Add more logging for refresh process // renew refresh token if returned from api 2025-08-23 11:22:37 +02:00
Gert-Andry KäärameesandGitHub 33ee258f3f handle missing refresh_token key in token file (#306) 2025-07-28 07:39:44 +02:00
Russell StephensandGitHub 54db72df94 Update README.md (#304)
Improve readability and copy-paste functionality of docker command
2025-07-16 10:58:03 +02:00
linus 70b027fb70 Fix and unit for and 2025-07-13 20:03:27 +02:00
linus 1c07ec1b46 Make 1.12.0 ready.. version chaos 2025-07-12 17:51:17 +02:00
linus 2fa154e023 Make 1.2.0 ready 2025-07-12 17:49:51 +02:00
linus ae5e3074ed Add logging options for plugin 2025-07-12 17:45:25 +02:00
Luka GraandGitHub f5a1e48e26 TA trip statistics (#283)
Add separate sensors for TA statistics
averageFuelConsumptionAutomatic.value
averageEnergyConsumptionAutomatic.value
averageSpeedAutomatic.value
tripMeterAutomatic.value
2025-07-12 17:38:00 +02:00
ee9d3cdef9 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

---------

Co-authored-by: Linus Dietz <45101649+Dielee@users.noreply.github.com>
2025-07-12 17:37:46 +02:00
8 changed files with 414 additions and 249 deletions
+20 -9
View File
@@ -129,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>
@@ -141,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)|
@@ -160,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
+45 -1
View File
@@ -1,10 +1,54 @@
## 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:
+3 -1
View File
@@ -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.2"
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
+18 -10
View File
@@ -1,6 +1,6 @@
from config import settings from config import settings
VERSION = "v1.10.6" VERSION = "v1.13.2"
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"}
@@ -136,6 +140,10 @@ 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"]
+66 -28
View File
@@ -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
@@ -27,6 +28,14 @@ def connect():
client = mqtt.Client("volvoAAOS2mqtt") if os.environ.get("IS_HA_ADDON") \ client = mqtt.Client("volvoAAOS2mqtt") if os.environ.get("IS_HA_ADDON") \
else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", "")) else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", ""))
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"]:
client.username_pw_set(settings["mqtt"]["username"], settings["mqtt"]["password"]) client.username_pw_set(settings["mqtt"]["username"], settings["mqtt"]["password"])
@@ -37,11 +46,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 +58,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 +95,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 +135,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 +146,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 +170,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 +231,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 +245,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 +290,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 +311,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 +375,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 +405,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 +446,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",
+2 -1
View File
@@ -8,7 +8,8 @@
"broker": "", "broker": "",
"port": 1883, "port": 1883,
"username": "", "username": "",
"password": "" "password": "",
"logging": false
}, },
"volvoData": { "volvoData": {
"username": "", "username": "",
+1 -1
View File
@@ -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
View File
@@ -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