Compare commits

...
14 Commits
6 changed files with 122 additions and 28 deletions
+1
View File
@@ -34,6 +34,7 @@ If you like my work:<br>
- XC90 PHEV T8 (2024)
- XC90 B5 Mildhybrid (2024)
- V90 PHEV T8 (2019)*
- V90 PHEV T6 (2024)
*only partly working
+39
View File
@@ -1,3 +1,42 @@
## v1.8.20
### 🐛 Bug Fixes:
- Fix Addon crash if icon state is `UNKOWN`
- Add `UNSPECIFIED` state for windows
## v1.8.19
### 🐛 Bug Fixes:
- Fix Estimated Charging Time not changing to zero #133
## v1.8.18
### 🐛 Bug Fixes:
- Fix Backend State Sensor returning `null`
## v1.8.17
### 🚀 Features:
- Move vehicle and vehicle details to v2 endpoint
### 🐛 Bug Fixes:
- Fix Battery Charge Level icon update
## v1.8.16
### 🚀 Features:
- Add `Average energy consumption`, `Distance to empty battery` and `Washer fluid level` sensors
- Support force update for `Backend state` Sensor
- Use cached requests for fuel endpoint
## v1.8.15
### 🚀 Features:
- Move `batteryChargeLevel` to the new v2 fuel endpoint
- Add api backend status sensor
## v1.8.14
### 🐛 Bug Fixes:
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.8.14"
version: "1.8.20"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
+15 -11
View File
@@ -1,10 +1,10 @@
from config import settings
VERSION = "v1.8.14"
VERSION = "v1.8.20"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles"
VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles"
VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}"
WINDOWS_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/windows"
CLIMATE_START_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/climatization-start"
CLIMATE_STOP_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/climatization-stop"
@@ -16,11 +16,11 @@ ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0
LOCATION_STATE_URL = "https://api.volvocars.com/location/v1/vehicles/{0}/location"
TYRE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/tyres"
ENGINE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine-status"
BATTERY_CHARGE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}/battery-charge-level"
FUEL_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/fuel"
FUEL_BATTERY_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/fuel"
STATISTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/statistics"
ENGINE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine"
VEHICLE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/diagnostics"
API_BACKEND_STATUS = "https://oip-dev-bff.euwest1.production.volvo.care/api/v1/backend-status"
units = {
"en_GB": {
@@ -50,7 +50,7 @@ charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected",
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC",
"CONNECTION_STATUS_FAULT": "Fault"}
window_states = {"CLOSED": "OFF", "OPEN": "ON", "AJAR": "ON"}
window_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
engine_states = {"RUNNING": "ON", "STOPPED": "OFF", "true": "ON", "false": "OFF"}
@@ -78,7 +78,7 @@ icon_states = {
supported_entities = [
{"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": BATTERY_CHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
{"name": "Electric Range", "domain": "sensor", "id": "electric_range", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["electric_range"]["unit"], "icon": "map-marker-distance", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Estimated Charging Time", "domain": "sensor", "id": "estimated_charging_time", "unit": "minutes", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Charging System Status", "domain": "sensor", "id": "charging_system_status", "icon": "ev-station", "url": RECHARGE_STATE_URL},
@@ -108,14 +108,18 @@ supported_entities = [
{"name": "Tire Rear Left", "domain": "sensor", "id": "tyre_rear_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": "liters", "icon": "fuel", "url": FUEL_STATE_URL, "state_class": "measurement"},
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": "liters", "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL},
{"name": "Distance to Empty", "domain": "sensor", "id": "distance_to_empty", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": "kwh", "icon": "car-electric", "url": STATISTICS_URL},
{"name": "Distance to Empty Tank", "domain": "sensor", "id": "distance_to_empty_tank", "unit": "km" 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": "km" 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": "km/h" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Hours to Service", "domain": "sensor", "id": "hours_to_service", "unit": "hour", "icon": "wrench-clock", "url": VEHICLE_DIAGNOSTICS_URL},
{"name": "Distance to Service", "domain": "sensor", "id": "km_to_service", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_service"]["unit"], "icon": "wrench-clock", "url": VEHICLE_DIAGNOSTICS_URL},
{"name": "Time to Service", "domain": "sensor", "id": "time_to_service", "icon": "wrench-clock", "url": VEHICLE_DIAGNOSTICS_URL},
{"name": "Service warning status", "domain": "sensor", "id": "service_warning_status", "icon": "alert-outline", "url": VEHICLE_DIAGNOSTICS_URL}
{"name": "Service warning status", "domain": "sensor", "id": "service_warning_status", "icon": "alert-outline", "url": VEHICLE_DIAGNOSTICS_URL},
{"name": "Washer Fluid Level warning", "domain": "sensor", "id": "washer_fluid_warning", "icon": "alert-outline", "url": VEHICLE_DIAGNOSTICS_URL},
{"name": "API Backend status", "domain": "sensor", "id": "api_backend_status", "icon": "alert"}
]
old_entity_ids = ["months_to_service", "service_warning_trigger"]
old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"]
+20 -9
View File
@@ -70,14 +70,14 @@ def send_car_images(vin, data, device):
if entity["id"] == "exterior_image":
mqtt_client.publish(
url_topic,
data["images"]["exteriorDefaultUrl"],
data["images"]["exteriorImageUrl"],
retain=True
)
if entity["id"] == "interior_image":
mqtt_client.publish(
url_topic,
data["images"]["interiorDefaultUrl"],
data["images"]["internalImageUrl"],
retain=True
)
@@ -243,6 +243,11 @@ def update_car_data(force_update=False, overwrite={}):
state = last_data_update
elif entity["id"] == "active_schedules":
state = active_schedules[vin]
elif entity["id"] == "api_backend_status":
if force_update:
state = volvo.get_backend_status()
else:
state = volvo.backend_status
else:
if entity["id"] == ov_entity_id and vin == ov_vin:
state = ov_state
@@ -254,7 +259,7 @@ def update_car_data(force_update=False, overwrite={}):
else:
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
if state:
if state or state == 0:
mqtt_client.publish(
topic,
json.dumps(state) if isinstance(state, dict) or isinstance(state, list) else state
@@ -264,13 +269,19 @@ def update_car_data(force_update=False, overwrite={}):
def update_ha_device(entity, vin, state):
icon_config = icon_states.get(entity["id"])
if icon_config and state:
if state.replace(".", "").isnumeric():
state = float(state)
icon = util.get_icon_between(icon_config, state)
try:
if icon_config and state:
logging.debug("Entity " + entity["id"] + " state " + str(state))
if isinstance(state, float):
icon = util.get_icon_between(icon_config, state)
elif state.replace(".", "").isnumeric():
state = float(state)
icon = util.get_icon_between(icon_config, state)
else:
icon = icon_config[state]
else:
icon = icon_config[state]
else:
return None
except:
return None
logging.debug("Updating icon to " + icon + " for " + entity["id"])
+46 -7
View File
@@ -4,15 +4,15 @@ import mqtt
import util
import time
import re
from threading import current_thread
from threading import current_thread, Thread
from datetime import datetime, timedelta
from config import settings
from babel.dates import format_datetime
from json import JSONDecodeError
from const import charging_system_states, charging_connection_states, door_states, window_states, \
OAUTH_URL, VEHICLES_URL, VEHICLE_DETAILS_URL, RECHARGE_STATE_URL, CLIMATE_START_URL, \
WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, supported_entities, BATTERY_CHARGE_STATE_URL, \
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, engine_states
WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, supported_entities, FUEL_BATTERY_STATE_URL, \
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, API_BACKEND_STATUS, engine_states
session = requests.Session()
session.headers = {
@@ -27,6 +27,7 @@ vins = []
supported_endpoints = {}
cached_requests = {}
vcc_api_keys = []
backend_status = ""
def authorize():
@@ -54,6 +55,7 @@ def authorize():
get_vcc_api_keys()
get_vehicles()
check_supported_endpoints()
Thread(target=backend_status_loop).start()
else:
message = auth.json()
raise Exception(message["error_description"])
@@ -241,7 +243,7 @@ def check_supported_endpoints():
for vin in vins:
supported_endpoints[vin] = []
for entity in supported_entities:
if entity["id"] == "battery_charge_level" and entity["url"] == BATTERY_CHARGE_STATE_URL \
if entity["id"] == "battery_charge_level" and entity["url"] == FUEL_BATTERY_STATE_URL \
and any("battery_charge_level" in d["id"] for d in supported_endpoints[vin]):
# If battery charge level could be found in recharge-api, skip the second battery charge sensor
continue
@@ -314,12 +316,37 @@ def check_engine_status(vin):
time.sleep(5)
def backend_status_loop():
while True:
get_backend_status()
# Update every hour
time.sleep(3600)
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"]
else:
backend_status = "NO_WARNING"
else:
backend_status = "NO_WARNING"
except JSONDecodeError as e:
backend_status = "NO_WARNING"
return backend_status
def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=False):
if datetime.now(util.TZ) >= token_expires_at:
refresh_auth()
if url in [RECHARGE_STATE_URL, WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL,
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL]:
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, FUEL_BATTERY_STATE_URL]:
# Minimize API calls for endpoints with multiple values
response = cached_request(url, method, vin, force_update, key_change)
if response is None:
@@ -411,7 +438,9 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
def parse_api_data(data, sensor_id=None):
data = data["data"]
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 == "electric_range":
@@ -535,12 +564,18 @@ def parse_api_data(data, sensor_id=None):
"latitude": raw_data["coordinates"][1],
"gps_accuracy": 1}
return coordinates
elif sensor_id == "distance_to_empty":
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":
@@ -556,5 +591,9 @@ def parse_api_data(data, sensor_id=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":
return data["averageEnergyConsumption"]["value"] if util.keys_exists(data, "averageEnergyConsumption") else None
elif sensor_id == "washer_fluid_warning":
return data["washerFluidLevelWarning"]["value"] if util.keys_exists(data, "washerFluidLevelWarning") else None
else:
return None