Compare commits

...
14 Commits
7 changed files with 103 additions and 63 deletions
+1
View File
@@ -32,6 +32,7 @@ If you like my work:<br>
- XC60 PHEV (2022) - XC60 PHEV (2022)
- XC90 PHEV T8 (2023) - XC90 PHEV T8 (2023)
- XC90 PHEV T8 (2024) - XC90 PHEV T8 (2024)
- XC90 B5 Mildhybrid (2024)
- V90 PHEV T8 (2019)* - V90 PHEV T8 (2019)*
*only partly working *only partly working
+35
View File
@@ -1,3 +1,38 @@
## v1.8.14
### 🐛 Bug Fixes:
- Fix distance to empty sensor #127
- Remove unused service warning trigger sensor
## v1.8.13
### 🐛 Bug Fixes:
- Add "Time to Service" sensor #125<p>
<b>Breaking change: The "Months to Service" sensor will be automatically deleted.<br>
"Time to Service" contains this information now.
## v1.8.12
### 🐛 Bug Fixes:
- Fix `AJAR` state for windows
- Fix deprecation from `currentThread` to `current_thread`
## v1.8.11
### 🐛 Bug Fixes:
- Remove old code from checks for running engine sensor
- Move battery charge level sensor from v2 to v1, as v2 returns 404
## v1.8.10
### 🐛 Bug Fixes:
- Add `AJAR` state for sunroof #123
## v1.8.9
### 🐛 Bug Fixes:
- Fix api changes from volvo #121
## v1.8.8 ## v1.8.8
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
+2
View File
@@ -4,8 +4,10 @@ profile volvo2mqtt flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base> #include <abstractions/base>
# Capabilities # Capabilities
capability,
file, file,
signal (send) set=(kill,term,int,hup,cont), signal (send) set=(kill,term,int,hup,cont),
network,
# S6-Overlay # S6-Overlay
/init ix, /init ix,
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt" name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge" description: "Volvo AAOS MQTT bridge"
version: "1.8.7" version: "1.8.14"
slug: "volvo2mqtt" slug: "volvo2mqtt"
init: false init: false
url: "https://github.com/Dielee/volvo2mqtt" url: "https://github.com/Dielee/volvo2mqtt"
+8 -8
View File
@@ -1,6 +1,6 @@
from config import settings from config import settings
VERSION = "v1.8.8" VERSION = "v1.8.14"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2" OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles" VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles"
@@ -16,7 +16,7 @@ ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0
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"
ENGINE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine-status" ENGINE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine-status"
BATTERY_CHARGE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/battery-charge-level" 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_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/fuel"
STATISTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/statistics" 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" ENGINE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine"
@@ -50,8 +50,8 @@ charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected",
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC", "CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC",
"CONNECTION_STATUS_FAULT": "Fault"} "CONNECTION_STATUS_FAULT": "Fault"}
window_states = {"CLOSED": "OFF", "OPEN": "ON"} window_states = {"CLOSED": "OFF", "OPEN": "ON", "AJAR": "ON"}
door_states = {"CLOSED": "OFF", "OPEN": "ON"} door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
engine_states = {"RUNNING": "ON", "STOPPED": "OFF", "true": "ON", "false": "OFF"} engine_states = {"RUNNING": "ON", "STOPPED": "OFF", "true": "ON", "false": "OFF"}
icon_states = { icon_states = {
@@ -108,14 +108,14 @@ supported_entities = [
{"name": "Tire Rear Left", "domain": "sensor", "id": "tyre_rear_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL}, {"name": "Tire Rear Left", "domain": "sensor", "id": "tyre_rear_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL}, {"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL}, {"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_DIAGNOSTICS_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_STATE_URL, "state_class": "measurement"},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL}, {"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL},
{"name": "Distance to Empty", "domain": "sensor", "id": "distance_to_empty", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"}, {"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 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": "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": "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": "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": "Months to Service", "domain": "sensor", "id": "months_to_service", "unit": "month", "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": "Service warning trigger", "domain": "sensor", "id": "service_warning_trigger", "icon": "alert-outline", "url": VEHICLE_DIAGNOSTICS_URL}
] ]
old_entity_ids = ["months_to_service", "service_warning_trigger"]
+8 -3
View File
@@ -10,8 +10,7 @@ from datetime import datetime
from babel.dates import format_datetime from babel.dates import format_datetime
from config import settings from config import settings
from const import CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \ from const import CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \
CAR_UNLOCK_URL, availability_topic, icon_states CAR_UNLOCK_URL, availability_topic, icon_states, old_entity_ids
mqtt_client: mqtt.Client mqtt_client: mqtt.Client
subscribed_topics = [] subscribed_topics = []
@@ -25,7 +24,7 @@ active_schedules = {}
def connect(): 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"]) else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", ""))
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"]:
@@ -361,3 +360,9 @@ def send_heartbeat():
def send_offline(): def send_offline():
mqtt_client.publish(availability_topic, "offline") mqtt_client.publish(availability_topic, "offline")
def delete_old_entities():
for vin in volvo.vins:
for entity in old_entity_ids:
topic = f"homeassistant/sensor/volvoAAOS2mqtt/{vin}_{entity}/config"
mqtt_client.publish(topic, payload="", retain=True)
+35 -38
View File
@@ -4,7 +4,7 @@ import mqtt
import util import util
import time import time
import re import re
from threading import currentThread from threading import current_thread
from datetime import datetime, timedelta from datetime import datetime, timedelta
from config import settings from config import settings
from babel.dates import format_datetime from babel.dates import format_datetime
@@ -116,6 +116,7 @@ def get_vehicles():
else: else:
initialize_climate(vins) initialize_climate(vins)
initialize_scheduler(vins) initialize_scheduler(vins)
mqtt.delete_old_entities()
logging.info("Vin: " + str(vins) + " found!") logging.info("Vin: " + str(vins) + " found!")
@@ -245,11 +246,6 @@ def check_supported_endpoints():
# If battery charge level could be found in recharge-api, skip the second battery charge sensor # If battery charge level could be found in recharge-api, skip the second battery charge sensor
continue continue
if entity["id"] == "engine_state" and entity["url"] == ENGINE_DIAGNOSTICS_URL \
and any("engine_state" in d["id"] for d in supported_endpoints[vin]):
# If engine state could be found in engine state endpoint, skip the second engine running sensor
continue
if entity.get('url'): if entity.get('url'):
state = api_call(entity["url"], "GET", vin, entity["id"]) state = api_call(entity["url"], "GET", vin, entity["id"])
else: else:
@@ -308,7 +304,7 @@ def check_engine_status(vin):
# Exit thread as engine state is unsupported by car # Exit thread as engine state is unsupported by car
return None return None
t = currentThread() t = current_thread()
while getattr(t, "do_run", True): while getattr(t, "do_run", True):
engine_state = api_call(endpoint_url, "GET", vin, "engine_state", True) engine_state = api_call(endpoint_url, "GET", vin, "engine_state", True)
if engine_state == "ON": if engine_state == "ON":
@@ -369,7 +365,7 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
return None return None
elif response.status_code == 403 and "message" in data: elif response.status_code == 403 and "message" in data:
if "Out of call volume quota" in data["message"]: if "Out of call volume quota" in data["message"]:
logging.warn("Quota extended. Try to change VCCAPIKEY!") logging.warning("Quota extended. Try to change VCCAPIKEY!")
change_vcc_api_key() change_vcc_api_key()
api_call(url, method, vin, sensor_id, force_update, True) api_call(url, method, vin, sensor_id, force_update, True)
else: else:
@@ -448,7 +444,7 @@ def parse_api_data(data, sensor_id=None):
return "" return ""
return None return None
elif sensor_id == "lock_status": elif sensor_id == "lock_status":
return data["carLocked"]["value"] if util.keys_exists(data, "carLocked") else None return data["centralLock"]["value"] if util.keys_exists(data, "centralLock") else None
elif sensor_id == "odometer": elif sensor_id == "odometer":
multiplier = 1 multiplier = 1
if util.keys_exists(settings["volvoData"], "odometerMultiplier"): if util.keys_exists(settings["volvoData"], "odometerMultiplier"):
@@ -460,44 +456,44 @@ def parse_api_data(data, sensor_id=None):
return util.convert_metric_values(int(data["odometer"]["value"]) * multiplier) \ return util.convert_metric_values(int(data["odometer"]["value"]) * multiplier) \
if util.keys_exists(data, "odometer") else None if util.keys_exists(data, "odometer") else None
elif sensor_id == "window_front_left": elif sensor_id == "window_front_left":
return window_states[data["frontLeftWindowOpen"]["value"]] if util.keys_exists(data, "frontLeftWindowOpen") \ return window_states[data["frontLeftWindow"]["value"]] if util.keys_exists(data, "frontLeftWindow") \
else None else None
elif sensor_id == "window_front_right": elif sensor_id == "window_front_right":
return window_states[data["frontRightWindowOpen"]["value"]] if util.keys_exists(data, "frontRightWindowOpen") \ return window_states[data["frontRightWindow"]["value"]] if util.keys_exists(data, "frontRightWindow") \
else None else None
elif sensor_id == "window_rear_left": elif sensor_id == "window_rear_left":
return window_states[data["rearLeftWindowOpen"]["value"]] if util.keys_exists(data, "rearLeftWindowOpen") \ return window_states[data["rearLeftWindow"]["value"]] if util.keys_exists(data, "rearLeftWindow") \
else None else None
elif sensor_id == "window_rear_right": elif sensor_id == "window_rear_right":
return window_states[data["rearRightWindowOpen"]["value"]] if util.keys_exists(data, "rearRightWindowOpen") \ return window_states[data["rearRightWindow"]["value"]] if util.keys_exists(data, "rearRightWindow") \
else None else None
elif sensor_id == "door_front_left": elif sensor_id == "door_front_left":
return door_states[data["frontLeftDoorOpen"]["value"]] if util.keys_exists(data, "frontLeftDoorOpen") else None return door_states[data["frontLeftDoor"]["value"]] if util.keys_exists(data, "frontLeftDoor") else None
elif sensor_id == "door_front_right": elif sensor_id == "door_front_right":
return door_states[data["frontRightDoorOpen"]["value"]] \ return door_states[data["frontRightDoor"]["value"]] \
if util.keys_exists(data, "frontRightDoorOpen") else None if util.keys_exists(data, "frontRightDoor") else None
elif sensor_id == "door_rear_left": elif sensor_id == "door_rear_left":
return door_states[data["rearLeftDoorOpen"]["value"]] if util.keys_exists(data, "rearLeftDoorOpen") else None return door_states[data["rearLeftDoor"]["value"]] if util.keys_exists(data, "rearLeftDoor") else None
elif sensor_id == "door_rear_right": elif sensor_id == "door_rear_right":
return door_states[data["rearRightDoorOpen"]["value"]] if util.keys_exists(data, "rearRightDoorOpen") else None return door_states[data["rearRightDoor"]["value"]] if util.keys_exists(data, "rearRightDoor") else None
elif sensor_id == "tailgate": elif sensor_id == "tailgate":
return door_states[data["tailGateOpen"]["value"]] if util.keys_exists(data, "tailGateOpen") else None return door_states[data["tailgate"]["value"]] if util.keys_exists(data, "tailgate") else None
elif sensor_id == "sunroof": elif sensor_id == "sunroof":
return door_states[data["sunRoofOpen"]["value"]] if util.keys_exists(data, "sunRoofOpen") else None return door_states[data["sunroof"]["value"]] if util.keys_exists(data, "sunroof") else None
elif sensor_id == "engine_hood": elif sensor_id == "engine_hood":
return door_states[data["hoodOpen"]["value"]] if util.keys_exists(data, "hoodOpen") else None return door_states[data["hood"]["value"]] if util.keys_exists(data, "hood") else None
elif sensor_id == "tank_lid": elif sensor_id == "tank_lid":
return door_states[data["tankLidOpen"]["value"]] if util.keys_exists(data, "tankLidOpen") else None return door_states[data["tankLid"]["value"]] if util.keys_exists(data, "tankLid") else None
elif sensor_id == "tyre_front_left": elif sensor_id == "tyre_front_left":
return data["frontLeftTyrePressure"]["value"] if util.keys_exists(data, "frontLeftTyrePressure") else None return data["frontLeft"]["value"] if util.keys_exists(data, "frontLeft") else None
elif sensor_id == "tyre_front_right": elif sensor_id == "tyre_front_right":
return data["frontRightTyrePressure"]["value"] if util.keys_exists(data, "frontRightTyrePressure") else None return data["frontRight"]["value"] if util.keys_exists(data, "frontRight") else None
elif sensor_id == "tyre_rear_left": elif sensor_id == "tyre_rear_left":
return data["rearLeftTyrePressure"]["value"] if util.keys_exists(data, "rearLeftTyrePressure") else None return data["rearLeft"]["value"] if util.keys_exists(data, "rearLeft") else None
elif sensor_id == "tyre_rear_right": elif sensor_id == "tyre_rear_right":
return data["rearRightTyrePressure"]["value"] if util.keys_exists(data, "rearRightTyrePressure") else None return data["rearRight"]["value"] if util.keys_exists(data, "rearRight") else None
elif sensor_id == "engine_state": elif sensor_id == "engine_state":
return engine_states[data["engineRunning"]["value"]] if util.keys_exists(data, "engineRunning") else None return engine_states[data["engineStatus"]["value"]] if util.keys_exists(data, "engineStatus") else None
elif sensor_id == "fuel_level": elif sensor_id == "fuel_level":
if util.keys_exists(data, "fuelAmount"): if util.keys_exists(data, "fuelAmount"):
fuel_amount = float(data["fuelAmount"]["value"]) fuel_amount = float(data["fuelAmount"]["value"])
@@ -540,24 +536,25 @@ def parse_api_data(data, sensor_id=None):
"gps_accuracy": 1} "gps_accuracy": 1}
return coordinates return coordinates
elif sensor_id == "distance_to_empty": elif sensor_id == "distance_to_empty":
if util.keys_exists(data, "distanceToEmpty"): if util.keys_exists(data, "distanceToEmptyTank"):
distance_to_empty = int(data["distanceToEmpty"]["value"]) distance_to_empty = int(data["distanceToEmptyTank"]["value"])
if distance_to_empty > 0: if distance_to_empty > 0:
return util.convert_metric_values(data["distanceToEmpty"]["value"]) return util.convert_metric_values(data["distanceToEmptyTank"]["value"])
return None return None
elif sensor_id == "hours_to_service": elif sensor_id == "hours_to_service":
return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None
elif sensor_id == "km_to_service": elif sensor_id == "km_to_service":
if util.keys_exists(data, "kmToService"): if util.keys_exists(data, "distanceToService"):
km_to_service = int(data["kmToService"]["value"]) km_to_service = int(data["distanceToService"]["value"])
if km_to_service > 0: if km_to_service > 0:
return util.convert_metric_values(data["kmToService"]["value"]) return util.convert_metric_values(data["distanceToService"]["value"])
return None return None
elif sensor_id == "months_to_service": elif sensor_id == "time_to_service":
return data["monthsToService"]["value"] if util.keys_exists(data, "monthsToService") else None if util.keys_exists(data, "timeToService"):
return str(data["timeToService"]["value"]) + " " + data["timeToService"]["unit"]
else:
return None
elif sensor_id == "service_warning_status": elif sensor_id == "service_warning_status":
return data["serviceWarningStatus"]["value"] if util.keys_exists(data, "serviceWarningStatus") else None return data["serviceWarning"]["value"] if util.keys_exists(data, "serviceWarning") else None
elif sensor_id == "service_warning_trigger":
return data["serviceWarningTrigger"]["value"] if util.keys_exists(data, "serviceWarningTrigger") else None
else: else:
return None return None