forked from peter/volvo2mqtt
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)
|
||||
- V60 T8 PHEV (2023)
|
||||
- V60 T6 PHEV (2025)
|
||||
- V60 CC B5 Mildhybrid (2024)
|
||||
- C40 BEV (2023)
|
||||
- C40 BEV (2022)
|
||||
- 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.
|
||||
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>
|
||||
|
||||
@@ -140,14 +151,15 @@ Here is what every option means:
|
||||
| ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- |
|
||||
| `CONF_updateInterval` | `int` | | **required** | Update intervall in seconds. |
|
||||
| `CONF_babelLocale` | `string` | | **required** | Select your country from this [list](https://www.ibm.com/docs/en/radfws/9.7?topic=overview-locales-code-pages-supported). "Locale name" is the column you need! |
|
||||
| `CONF_mqtt` | `json` | `broker` | **required** | Your MQTT Broker IP. Eg. 192.168.0.5.
|
||||
| `CONF_mqtt` | `json` | `broker` | **required** | Your MQTT Broker IP. Eg. 192.168.0.5. |
|
||||
| `CONF_mqtt` | `json` | `port` | 1883 | Your MQTT Broker Port. If no value is given, port 1883 will be used. |
|
||||
| `CONF_mqtt` | `json` | `username` | optional | MQTT Username for your broker.
|
||||
| `CONF_mqtt` | `json` | `password` | optional | MQTT Password for your broker.
|
||||
| `CONF_volvoData` | `json` | `username` | **required** | Normally your email address to login into the Volvo App.
|
||||
| `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App.
|
||||
| `CONF_volvoData` | `json` | `vin` | optional | A single VIN like "VIN1" or a list of VINs like "["VIN1", "VIN2"]". Leave this empty if you don't know your VIN. The addon will use every car that is tied to your account.
|
||||
| `CONF_volvoData` | `json` | `vccapikey` | **required** | 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_mqtt` | `json` | `username` | optional | MQTT Username for your broker. |
|
||||
| `CONF_mqtt` | `json` | `password` | optional | MQTT Password for your broker. |
|
||||
| `CONF_mqtt` | `json` | `logging` | optional | Enable MQTT logging (true/false). Default don't enable MQTT log. |
|
||||
| `CONF_volvoData` | `json` | `username` | **required** | Normally your email address to login into the Volvo App. |
|
||||
| `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App. |
|
||||
| `CONF_volvoData` | `json` | `vin` | optional | A single VIN like "VIN1" or a list of VINs like "["VIN1", "VIN2"]". Leave this empty if you don't know your VIN. The addon will use every car that is tied to your account. |
|
||||
| `CONF_volvoData` | `json` | `vccapikey` | **required** | 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. |
|
||||
| `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
|
||||
[releases]: https://github.com/Dielee/volvo2mqtt/releases
|
||||
[releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg
|
||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/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]: https://github.com/Dielee/volvo2mqtt/commits/main
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
requests~=2.32.0
|
||||
dynaconf~=3.1.12
|
||||
paho-mqtt~=1.6.1
|
||||
Babel~=2.12.1
|
||||
pytz~=2023.3
|
||||
dynaconf~=3.2.13
|
||||
paho-mqtt~=2.1.0
|
||||
Babel~=2.18.0
|
||||
pytz~=2026.1.post1
|
||||
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
|
||||
|
||||
### 🐛 Bug Fixes:
|
||||
|
||||
- Fix missing refresh_token and API changes by volvo. Thanks to @esusxunil.
|
||||
|
||||
|
||||
## v1.10.5
|
||||
|
||||
### 🐛 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"
|
||||
description: "Volvo AAOS MQTT bridge"
|
||||
version: "1.10.6"
|
||||
version: "1.13.5"
|
||||
slug: "volvo2mqtt"
|
||||
init: false
|
||||
url: "https://github.com/Dielee/volvo2mqtt"
|
||||
@@ -21,6 +21,7 @@ options:
|
||||
port: "auto_port"
|
||||
username: "auto_user"
|
||||
password: "auto_password"
|
||||
logging: false
|
||||
volvoData:
|
||||
username: null
|
||||
password: null
|
||||
@@ -38,6 +39,7 @@ schema:
|
||||
port: str
|
||||
username: str?
|
||||
password: str?
|
||||
logging: bool
|
||||
volvoData:
|
||||
username: match(^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)|(\+\d{5,20})$)
|
||||
password: str
|
||||
|
||||
+21
-13
@@ -1,6 +1,6 @@
|
||||
from config import settings
|
||||
|
||||
VERSION = "v1.10.6"
|
||||
VERSION = "v1.13.5"
|
||||
|
||||
OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.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"
|
||||
CAR_LOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/lock"
|
||||
CAR_UNLOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/unlock"
|
||||
RECHARGE_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"
|
||||
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"
|
||||
@@ -41,7 +41,9 @@ units = {
|
||||
"odometer": {"unit": LENGTH_MILES},
|
||||
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
||||
"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": {
|
||||
"divider": 1.60934,
|
||||
@@ -49,19 +51,21 @@ units = {
|
||||
"odometer": {"unit": LENGTH_MILES},
|
||||
"average_speed": {"unit": SPEED_MILES_PER_HOUR},
|
||||
"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"
|
||||
|
||||
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle",
|
||||
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified",
|
||||
"CHARGING_SYSTEM_DONE": "Done", "CHARGING_SYSTEM_SCHEDULED": "Scheduled"}
|
||||
charging_system_states = {"CHARGING": "Charging", "IDLE": "Idle",
|
||||
"FAULT": "Fault", "UNSPECIFIED": "Unspecified",
|
||||
"DONE": "Done", "SCHEDULED": "Scheduled", "ERROR": "Error",
|
||||
"DISCHARGING": "Discharging"}
|
||||
|
||||
charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected", "CONNECTION_STATUS_UNSPECIFIED": "Unspecified",
|
||||
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC",
|
||||
"CONNECTION_STATUS_FAULT": "Fault"}
|
||||
charging_connection_states = {"DISCONNECTED": "Disconnected", "UNSPECIFIED": "Unspecified",
|
||||
"FAULT": "Fault", "CONNECTED": "Connected"}
|
||||
|
||||
window_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": "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": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL},
|
||||
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "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, "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": "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": "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": "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"]
|
||||
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 os
|
||||
import requests
|
||||
import threading
|
||||
from threading import Thread, Timer
|
||||
from datetime import datetime
|
||||
from babel.dates import format_datetime
|
||||
@@ -24,8 +25,21 @@ active_schedules = {}
|
||||
otp_code = None
|
||||
|
||||
def connect():
|
||||
client = mqtt.Client("volvoAAOS2mqtt") if os.environ.get("IS_HA_ADDON") \
|
||||
else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", ""))
|
||||
client_id = "volvoAAOS2mqtt" if os.environ.get("IS_HA_ADDON") \
|
||||
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)
|
||||
if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
|
||||
@@ -37,11 +51,9 @@ def connect():
|
||||
if conf_port > 0:
|
||||
port = settings["mqtt"]["port"]
|
||||
client.connect(settings["mqtt"]["broker"], port)
|
||||
|
||||
client.loop_start()
|
||||
client.subscribe("volvoAAOS2mqtt/otp_code")
|
||||
client.on_message = on_message
|
||||
client.on_disconnect = on_disconnect
|
||||
client.on_connect = on_connect
|
||||
|
||||
global mqtt_client
|
||||
mqtt_client = client
|
||||
@@ -51,7 +63,7 @@ def create_otp_input():
|
||||
state_topic = otp_mqtt_topic + "/state"
|
||||
config = {
|
||||
"name": "Volvo OTP",
|
||||
"object_id": f"volvo_otp",
|
||||
"default_entity_id": "volvo_otp",
|
||||
"schema": "state",
|
||||
"command_topic": otp_mqtt_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"
|
||||
config = {
|
||||
"name": entity["name"],
|
||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
||||
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||
"schema": "state",
|
||||
"icon": "mdi:image-area",
|
||||
"content_type": "image/png",
|
||||
@@ -128,6 +140,10 @@ def send_car_images(vin, data, device):
|
||||
|
||||
|
||||
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()
|
||||
if len(subscribed_topics) > 0:
|
||||
for topic in subscribed_topics:
|
||||
@@ -135,9 +151,16 @@ def on_connect(client, userdata, flags, 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):
|
||||
payload = msg.payload.decode("UTF-8")
|
||||
if msg.topic == otp_mqtt_topic:
|
||||
@@ -152,40 +175,56 @@ def on_message(client, userdata, msg):
|
||||
try:
|
||||
vin = msg.topic.split('/')[2].split('_')[0]
|
||||
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
|
||||
|
||||
if "climate_status" in msg.topic:
|
||||
if payload == "ON":
|
||||
start_climate(vin)
|
||||
return None
|
||||
elif payload == "OFF":
|
||||
stop_climate(vin)
|
||||
return None
|
||||
return None
|
||||
elif "lock_status" in msg.topic:
|
||||
if payload == "LOCK":
|
||||
lock_car(vin)
|
||||
return None
|
||||
elif payload == "UNLOCK":
|
||||
unlock_car(vin)
|
||||
return None
|
||||
return None
|
||||
elif "update_data" in msg.topic:
|
||||
if payload == "PRESS":
|
||||
update_car_data(True)
|
||||
return None
|
||||
return None
|
||||
elif "update_interval" in msg.topic:
|
||||
update_interval = int(payload)
|
||||
if update_interval >= 60 or update_interval == -1:
|
||||
settings.update({"updateInterval": int(update_interval)})
|
||||
else:
|
||||
logging.warning("Interval " + str(update_interval) + " seconds is to low. Doing nothing!")
|
||||
update_car_data()
|
||||
try:
|
||||
update_interval = int(payload)
|
||||
if update_interval >= 60 or update_interval == -1:
|
||||
settings.update({"updateInterval": int(update_interval)})
|
||||
else:
|
||||
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:
|
||||
try:
|
||||
d = json.loads(payload)
|
||||
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
|
||||
|
||||
if d["mode"] == "timer":
|
||||
start_climate_timer(d, vin)
|
||||
return None
|
||||
else:
|
||||
logging.warning("No schedule mode found, doing nothing")
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
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)
|
||||
timer_seconds = (start_datetime - local_datetime).total_seconds()
|
||||
except Exception as e:
|
||||
logging.error("Error creating climate timer: " + str(e))
|
||||
logging.error(f"Error creating climate timer: {e}")
|
||||
return None
|
||||
|
||||
if timer_seconds > 0:
|
||||
Timer(timer_seconds, activate_climate_timer, (vin, start_datetime.isoformat(),)).start()
|
||||
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()
|
||||
else:
|
||||
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):
|
||||
# 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
|
||||
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"})
|
||||
# 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):
|
||||
# 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
|
||||
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"})
|
||||
# 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):
|
||||
global assumed_climate_state, climate_timer, engine_status
|
||||
# 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
|
||||
if engine_status[vin].is_alive():
|
||||
@@ -256,10 +295,10 @@ def activate_climate_timer(vin, start_time):
|
||||
def start_climate(vin):
|
||||
global assumed_climate_state, climate_timer, engine_status
|
||||
# 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
|
||||
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()
|
||||
engine_status[vin] = check_engine_thread
|
||||
|
||||
@@ -277,9 +316,12 @@ def update_loop():
|
||||
while True:
|
||||
if settings["updateInterval"] > 0:
|
||||
logging.info("Sending mqtt update...")
|
||||
send_heartbeat()
|
||||
update_car_data()
|
||||
logging.info("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
|
||||
try:
|
||||
send_heartbeat()
|
||||
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"])
|
||||
else:
|
||||
logging.info("Data update is disabled, doing nothing for 30 seconds")
|
||||
@@ -338,6 +380,7 @@ def update_car_data(force_update=False, overwrite={}):
|
||||
else:
|
||||
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
|
||||
|
||||
logging.info(f"update_car_data {topic} {state}")
|
||||
if state or state == 0:
|
||||
mqtt_client.publish(
|
||||
topic,
|
||||
@@ -367,7 +410,7 @@ def update_ha_device(entity, vin, state):
|
||||
logging.debug("Updating icon to " + icon + " for " + entity["id"])
|
||||
config = {
|
||||
"name": entity['name'],
|
||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
||||
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||
"schema": "state",
|
||||
"icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}",
|
||||
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
||||
@@ -408,7 +451,7 @@ def create_ha_devices():
|
||||
for entity in volvo.supported_endpoints[vin]:
|
||||
config = {
|
||||
"name": entity['name'],
|
||||
"object_id": f"volvo_{vin}_{entity['id']}",
|
||||
"default_entity_id": f"volvo_{vin}_{entity['id']}",
|
||||
"schema": "state",
|
||||
"icon": f"mdi:{entity['icon']}",
|
||||
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
|
||||
|
||||
+3
-5
@@ -8,15 +8,13 @@
|
||||
"broker": "",
|
||||
"port": 1883,
|
||||
"username": "",
|
||||
"password": ""
|
||||
"password": "",
|
||||
"logging": false
|
||||
},
|
||||
"volvoData": {
|
||||
"username": "",
|
||||
"password": "",
|
||||
"vin": "",
|
||||
"vccapikey": [],
|
||||
"odometerMultiplier": "",
|
||||
"averageSpeedDivider": "",
|
||||
"averageFuelConsumptionMultiplier": ""
|
||||
"vccapikey": []
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
volvoData:
|
||||
name: Volvo configuration options
|
||||
description: You have to enter your Volvo app credentials. The username is normally your email, and the password is the password you use for your Volvo app. The vin can stay empty. The Add-On will use any vin inside your Volvo account. VCCAPI key is required and comes from your Volvo developer account. 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()
|
||||
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
|
||||
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')
|
||||
file_log_handler.setFormatter(formatter)
|
||||
|
||||
|
||||
+259
-198
@@ -47,6 +47,11 @@ def authorize(renew_tokenfile=False):
|
||||
except ValueError:
|
||||
logging.warning("Detected corrupted token file, restarting auth process")
|
||||
authorize(True)
|
||||
return
|
||||
except KeyError:
|
||||
logging.warning("Token file missing refresh_token, restarting auth process")
|
||||
authorize(True)
|
||||
return
|
||||
else:
|
||||
logging.info("Starting login with OTP")
|
||||
auth_session = requests.session()
|
||||
@@ -105,11 +110,11 @@ def authorize(renew_tokenfile=False):
|
||||
get_vcc_api_keys()
|
||||
get_vehicles()
|
||||
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):
|
||||
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:
|
||||
return auth.json()
|
||||
@@ -170,8 +175,9 @@ def send_otp(auth_session, data):
|
||||
|
||||
|
||||
def refresh_auth():
|
||||
logging.info("Refreshing credentials")
|
||||
global refresh_token
|
||||
logging.info("Refreshing credentials with token " + refresh_token)
|
||||
|
||||
headers = {
|
||||
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
@@ -191,24 +197,35 @@ def refresh_auth():
|
||||
|
||||
if auth.status_code == 200:
|
||||
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()
|
||||
|
||||
f = open(token_path)
|
||||
try:
|
||||
data = json.load(f)
|
||||
refresh_token = data["refresh_token"]
|
||||
except ValueError:
|
||||
raise Exception("Cannot refresh token!")
|
||||
if not "refresh_token" in refresh_data:
|
||||
f = open(token_path)
|
||||
try:
|
||||
data = json.load(f)
|
||||
refresh_data["refresh_token"] = data["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)
|
||||
session.headers.update({"authorization": "Bearer " + refresh_data["access_token"]})
|
||||
|
||||
global token_expires_at
|
||||
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(refresh_data["expires_in"] - 30))
|
||||
refresh_token = refresh_data["refresh_token"]
|
||||
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
|
||||
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
|
||||
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)
|
||||
authorize(renew_tokenfile=True)
|
||||
return None
|
||||
@@ -445,31 +462,44 @@ def check_engine_status(vin):
|
||||
mqtt.assumed_climate_state[vin] = "OFF"
|
||||
mqtt.update_car_data()
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
return None
|
||||
|
||||
|
||||
def backend_status_loop():
|
||||
logging.debug("backend_status_loop started")
|
||||
while True:
|
||||
get_backend_status()
|
||||
# Update every hour
|
||||
time.sleep(3600)
|
||||
last_status = get_backend_status()
|
||||
# Update every hour when not error, 10 minutes when got server error
|
||||
time.sleep(3600 if not last_status.startswith("APIERR_") else 600)
|
||||
|
||||
|
||||
def get_backend_status():
|
||||
global backend_status
|
||||
response = session.get(API_BACKEND_STATUS, timeout=15)
|
||||
try:
|
||||
data = response.json()
|
||||
if util.keys_exists(data, "message"):
|
||||
if data["message"]:
|
||||
backend_status = data["message"]
|
||||
response = session.get(API_BACKEND_STATUS, timeout=15)
|
||||
try:
|
||||
data = response.json()
|
||||
if util.keys_exists(data, "message"):
|
||||
if data["message"]:
|
||||
backend_status = data["message"]
|
||||
else:
|
||||
backend_status = "NO_WARNING"
|
||||
else:
|
||||
backend_status = "NO_WARNING"
|
||||
else:
|
||||
backend_status = "NO_WARNING"
|
||||
|
||||
except JSONDecodeError as e:
|
||||
backend_status = "NO_WARNING"
|
||||
except JSONDecodeError as e:
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
return None
|
||||
elif method == "GET":
|
||||
logging.debug("Starting " + method + " call against " + url)
|
||||
logging.debug(f"Starting {method} call against {url}")
|
||||
try:
|
||||
response = session.get(url.format(vin), timeout=15)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error("Error getting data: " + str(e))
|
||||
logging.error(f"Error getting data: {e} from {url}")
|
||||
return None
|
||||
elif method == "POST":
|
||||
logging.debug("Starting " + method + " call against " + url)
|
||||
logging.debug(f"Starting {method} call against {url}")
|
||||
try:
|
||||
response = session.post(url.format(vin), timeout=20)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error("Error getting data: " + str(e))
|
||||
logging.error(f"Error getting data from {url} : {e}")
|
||||
return None
|
||||
else:
|
||||
logging.error("Unkown method posted: " + method + ". Returning nothing")
|
||||
logging.error(f"Unkown method posted: {method}. Returning nothing")
|
||||
return None
|
||||
|
||||
logging.debug("Response status code: " + str(response.status_code))
|
||||
logging.debug(f"Response status code: {response.status_code}")
|
||||
try:
|
||||
data = response.json()
|
||||
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
|
||||
|
||||
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)
|
||||
else:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
@@ -539,11 +569,11 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
|
||||
global cached_requests
|
||||
if not util.keys_exists(cached_requests, vin + "_" + url):
|
||||
# No API Data cached, get fresh data from API
|
||||
logging.debug("Starting " + method + " call against " + url)
|
||||
logging.debug(f"Starting {method} call against {url}")
|
||||
try:
|
||||
response = session.get(url.format(vin), timeout=15)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error("Error getting data: " + str(e))
|
||||
logging.error(f"Error getting data from {url}: {e}")
|
||||
return None
|
||||
|
||||
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) \
|
||||
or key_change:
|
||||
# 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:
|
||||
response = session.get(url.format(vin), timeout=15)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error("Error getting data: " + str(e))
|
||||
logging.error(f"Error getting data from {url} : {e}")
|
||||
return None
|
||||
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
||||
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):
|
||||
if sensor_id != "api_backend_status":
|
||||
data = data["data"]
|
||||
if sensor_id == "battery_charge_level":
|
||||
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
|
||||
elif sensor_id == "battery_capacity":
|
||||
return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") else None
|
||||
elif sensor_id == "electric_range":
|
||||
return util.convert_metric_values(data["electricRange"]["value"]) \
|
||||
if util.keys_exists(data, "electricRange") else None
|
||||
elif sensor_id == "charging_system_status":
|
||||
return charging_system_states[data["chargingSystemStatus"]["value"]] \
|
||||
if util.keys_exists(data, "chargingSystemStatus") else None
|
||||
elif sensor_id == "charging_connection_status":
|
||||
return charging_connection_states[data["chargingConnectionStatus"]["value"]] \
|
||||
if util.keys_exists(data, "chargingConnectionStatus") else None
|
||||
elif sensor_id == "estimated_charging_time":
|
||||
if util.keys_exists(data, "chargingSystemStatus"):
|
||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
||||
if charging_system_state == "Charging":
|
||||
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else ""
|
||||
if sensor_id in ["battery_charge_level", "electric_range", "charging_system_status",
|
||||
"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
|
||||
elif sensor_id == "electric_range":
|
||||
return util.convert_metric_values(data["electricRange"]["value"]) \
|
||||
if util.keys_exists(data, "electricRange") else None
|
||||
elif sensor_id == "charging_system_status":
|
||||
return charging_system_states[data["chargingStatus"]["value"]] \
|
||||
if util.keys_exists(data, "chargingStatus") else None
|
||||
elif sensor_id == "charging_connection_status":
|
||||
return charging_connection_states[data["chargerConnectionStatus"]["value"]] \
|
||||
if util.keys_exists(data, "chargerConnectionStatus") else None
|
||||
elif sensor_id == "estimated_charging_time":
|
||||
if util.keys_exists(data, "chargingStatus"):
|
||||
charging_system_state = charging_system_states[data["chargingStatus"]["value"]]
|
||||
if charging_system_state == "Charging":
|
||||
return data["estimatedChargingTimeToTargetBatteryChargeLevel"]["value"] if util.keys_exists(data,
|
||||
"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:
|
||||
return 0
|
||||
return None
|
||||
elif sensor_id == "estimated_charging_finish_time":
|
||||
if util.keys_exists(data, "chargingSystemStatus"):
|
||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
||||
if charging_system_state == "Charging":
|
||||
charging_time = int(
|
||||
data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime")
|
||||
else 0)
|
||||
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time)
|
||||
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
|
||||
else:
|
||||
return ""
|
||||
return None
|
||||
elif sensor_id == "lock_status":
|
||||
return data["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"])
|
||||
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_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_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"]
|
||||
|
||||
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"]
|
||||
return cleaned_data if warnings > 0 else None
|
||||
elif sensor_id == "trip_fuel_consumption":
|
||||
trip_fuel_con = 0
|
||||
if util.keys_exists(data, "averageFuelConsumptionAutomatic"):
|
||||
trip_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
|
||||
if trip_fuel_con > 0:
|
||||
return trip_fuel_con
|
||||
return None
|
||||
elif sensor_id == "trip_distance":
|
||||
return util.convert_metric_values(int(data["tripMeterAutomatic"]["value"])) \
|
||||
if util.keys_exists(data, "tripMeterAutomatic") else None
|
||||
elif sensor_id == "trip_energy_consumption":
|
||||
trip_energy_con = 0
|
||||
if util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
|
||||
trip_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
|
||||
if trip_energy_con != 0:
|
||||
return trip_energy_con
|
||||
return None
|
||||
elif sensor_id == "trip_speed":
|
||||
trip_speed = 0
|
||||
if util.keys_exists(data, "averageSpeedAutomatic"):
|
||||
trip_speed = float(data["averageSpeedAutomatic"]["value"])
|
||||
if trip_speed != 0:
|
||||
return util.convert_metric_values(trip_speed)
|
||||
return None
|
||||
else:
|
||||
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