forked from peter/volvo2mqtt
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7b711219 | ||
|
|
fb8c5ed269 | ||
|
|
a7dd6fac00 | ||
|
|
818d68675f | ||
|
|
9412d837d0 | ||
|
|
d49afa5bed | ||
|
|
f5c8d06aa1 | ||
|
|
6627a024a9 | ||
|
|
9de14bd70b | ||
|
|
880005fdb6 | ||
|
|
8bacc1a76a | ||
|
|
96805d439d | ||
|
|
10c3e7bd97 | ||
|
|
ad691b6153 | ||
|
|
b6e423839a | ||
|
|
044e71d862 |
@@ -29,6 +29,7 @@ If you like my work:<br>
|
|||||||
- XC60 PHEV (2023)
|
- XC60 PHEV (2023)
|
||||||
- XC60 PHEV (2022)
|
- XC60 PHEV (2022)
|
||||||
- XC90 PHEV T8 (2023)
|
- XC90 PHEV T8 (2023)
|
||||||
|
- XC90 PHEV T8 (2024)
|
||||||
- V90 PHEV T8 (2019)*
|
- V90 PHEV T8 (2019)*
|
||||||
|
|
||||||
*only partly working
|
*only partly working
|
||||||
@@ -42,7 +43,7 @@ Please let me know if your car works with this addon so I can expand the list!<b
|
|||||||
- Sensor "Battery Charge Level"
|
- Sensor "Battery Charge Level"
|
||||||
- Sensor "Electric Range"
|
- Sensor "Electric Range"
|
||||||
- Sensor "Charging System Status"
|
- Sensor "Charging System Status"
|
||||||
- Sensor "Charging Connection Status"
|
- Sensor "Charging Connection Status" (partly broken since 2.10 car update)
|
||||||
- Sensor "Estimated Charging Finish Time"
|
- Sensor "Estimated Charging Finish Time"
|
||||||
- Sensor "Door Lock Status (All doors, tank lid, and engine hood)"
|
- Sensor "Door Lock Status (All doors, tank lid, and engine hood)"
|
||||||
- Sensor "Window Lock Status (All windows and sunroof - Thanks to @navet)"
|
- Sensor "Window Lock Status (All windows and sunroof - Thanks to @navet)"
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
## v1.7.9
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Added vehicle diagnostics sensors #75. Thanks to @aerrow5!
|
||||||
|
|
||||||
|
## v1.7.8
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fix "Estimated Charging Finish Time" Sensor
|
||||||
|
|
||||||
|
## v1.7.7
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Added long term statistics for some sensors #68. Thanks to @bogdanbujdea!
|
||||||
|
- Automatic redact sensitive data from debug log
|
||||||
|
- Perpare for scheduler option (not yet ready)
|
||||||
|
|
||||||
|
## v1.7.6
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Added support for interior and exterior images via the new [MQTT Image](https://www.home-assistant.io/integrations/image.mqtt/) entity. #62
|
||||||
|
|
||||||
|
<b>NOTE: This feature is only available using homeassistant version 2023.7.1 or greater.</b>
|
||||||
|
|
||||||
|
## v1.7.5
|
||||||
|
### 🚀 Features:
|
||||||
|
|
||||||
|
- Added support for `LOCKING` and `UNLOCKING` state (Lock entity)
|
||||||
|
|
||||||
|
## v1.7.4
|
||||||
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
- Fixed undocumented charging connection state `CONNECTION_STATUS_FAULT` '#57
|
||||||
|
|
||||||
## v1.7.3
|
## v1.7.3
|
||||||
### 🐛 Bug Fixes:
|
### 🐛 Bug Fixes:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name: "Volvo2Mqtt"
|
name: "Volvo2Mqtt"
|
||||||
description: "Volvo AAOS MQTT bridge"
|
description: "Volvo AAOS MQTT bridge"
|
||||||
version: "1.7.3"
|
version: "1.7.9"
|
||||||
slug: "volvo2mqtt"
|
slug: "volvo2mqtt"
|
||||||
init: false
|
init: false
|
||||||
url: "https://github.com/Dielee/volvo2mqtt"
|
url: "https://github.com/Dielee/volvo2mqtt"
|
||||||
|
|||||||
+19
-11
@@ -1,6 +1,6 @@
|
|||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
VERSION = "v1.7.3"
|
VERSION = "v1.7.9"
|
||||||
|
|
||||||
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"
|
||||||
@@ -20,6 +20,7 @@ BATTERY_CHARGE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehic
|
|||||||
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"
|
||||||
|
VEHICLE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/diagnostics"
|
||||||
|
|
||||||
units = {
|
units = {
|
||||||
"en_GB": {
|
"en_GB": {
|
||||||
@@ -44,14 +45,15 @@ charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYST
|
|||||||
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified"}
|
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified"}
|
||||||
|
|
||||||
charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected", "CONNECTION_STATUS_UNSPECIFIED": "Unspecified",
|
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_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC",
|
||||||
|
"CONNECTION_STATUS_FAULT": "Fault"}
|
||||||
|
|
||||||
window_states = {"CLOSED": "OFF", "OPEN": "ON"}
|
window_states = {"CLOSED": "OFF", "OPEN": "ON"}
|
||||||
door_states = {"CLOSED": "OFF", "OPEN": "ON"}
|
door_states = {"CLOSED": "OFF", "OPEN": "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 = {
|
||||||
"lock_status": {"UNLOCKED": "lock-open-alert", "LOCKED": "lock"},
|
"lock_status": {"UNLOCKED": "lock-open-alert", "LOCKED": "lock", "UNLOCKING": "lock-reset", "LOCKING": "lock-reset"},
|
||||||
"door_front_left": {"ON": "car-door", "OFF": "car-door-lock"},
|
"door_front_left": {"ON": "car-door", "OFF": "car-door-lock"},
|
||||||
"door_front_right": {"ON": "car-door", "OFF": "car-door-lock"},
|
"door_front_right": {"ON": "car-door", "OFF": "car-door-lock"},
|
||||||
"door_rear_left": {"ON": "car-door", "OFF": "car-door-lock"},
|
"door_rear_left": {"ON": "car-door", "OFF": "car-door-lock"},
|
||||||
@@ -73,15 +75,16 @@ icon_states = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
supported_entities = [
|
supported_entities = [
|
||||||
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL},
|
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
|
||||||
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": BATTERY_CHARGE_STATE_URL},
|
{"name": "Battery Charge Level", "domain": "sensor", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": BATTERY_CHARGE_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},
|
{"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},
|
{"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},
|
{"name": "Charging System Status", "domain": "sensor", "id": "charging_system_status", "icon": "ev-station", "url": RECHARGE_STATE_URL},
|
||||||
{"name": "Charging Connection Status", "domain": "sensor", "id": "charging_connection_status", "icon": "ev-plug-ccs2", "url": RECHARGE_STATE_URL},
|
{"name": "Charging Connection Status", "domain": "sensor", "id": "charging_connection_status", "icon": "ev-plug-ccs2", "url": RECHARGE_STATE_URL},
|
||||||
{"name": "Estimated Charging Finish Time", "domain": "sensor", "id": "estimated_charging_finish_time", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL},
|
{"name": "Estimated Charging Finish Time", "domain": "sensor", "id": "estimated_charging_finish_time", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL},
|
||||||
{"name": "Odometer", "domain": "sensor", "id": "odometer", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["odometer"]["unit"], "icon": "counter", "url": ODOMETER_STATE_URL},
|
{"name": "Odometer", "domain": "sensor", "id": "odometer", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["odometer"]["unit"], "icon": "counter", "url": ODOMETER_STATE_URL, "state_class":"total_increasing"},
|
||||||
{"name": "Last Data Update", "domain": "sensor", "id": "last_data_update", "icon": "timer", "url": ""},
|
{"name": "Last Data Update", "domain": "sensor", "id": "last_data_update", "icon": "timer", "url": ""},
|
||||||
|
{"name": "Active schedules", "domain": "sensor", "id": "active_schedules", "icon": "timer", "url": ""},
|
||||||
{"name": "Window Front Left", "domain": "binary_sensor", "device_class": "window", "id": "window_front_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
{"name": "Window Front Left", "domain": "binary_sensor", "device_class": "window", "id": "window_front_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
||||||
{"name": "Window Front Right", "domain": "binary_sensor", "device_class": "window", "id": "window_front_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
{"name": "Window Front Right", "domain": "binary_sensor", "device_class": "window", "id": "window_front_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
||||||
{"name": "Window Rear Left", "domain": "binary_sensor", "device_class": "window", "id": "window_rear_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
{"name": "Window Rear Left", "domain": "binary_sensor", "device_class": "window", "id": "window_rear_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
|
||||||
@@ -104,8 +107,13 @@ supported_entities = [
|
|||||||
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
|
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
|
||||||
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
|
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
|
||||||
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_DIAGNOSTICS_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},
|
{"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},
|
{"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}
|
{"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": "Months to Service", "domain": "sensor", "id": "months_to_service", "unit": "month", "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 trigger", "domain": "sensor", "id": "service_warning_trigger", "icon": "alert-outline", "url": VEHICLE_DIAGNOSTICS_URL}
|
||||||
]
|
]
|
||||||
|
|||||||
+151
-43
@@ -19,6 +19,7 @@ last_data_update = None
|
|||||||
climate_timer = {}
|
climate_timer = {}
|
||||||
engine_status = {}
|
engine_status = {}
|
||||||
devices = {}
|
devices = {}
|
||||||
|
active_schedules = {}
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
def connect():
|
||||||
@@ -42,6 +43,43 @@ def connect():
|
|||||||
mqtt_client = client
|
mqtt_client = client
|
||||||
|
|
||||||
|
|
||||||
|
def send_car_images(vin, data, device):
|
||||||
|
if util.keys_exists(data, "images"):
|
||||||
|
for entity in [{"name": "Exterior Image", "id": "exterior_image"},
|
||||||
|
{"name": "Interior Image", "id": "interior_image"}]:
|
||||||
|
url_topic = f"homeassistant/image/{vin}_{entity['id']}/image_url"
|
||||||
|
config = {
|
||||||
|
"name": entity["name"],
|
||||||
|
"object_id": f"volvo_{vin}_{entity['id']}",
|
||||||
|
"schema": "state",
|
||||||
|
"icon": "mdi:image-area",
|
||||||
|
"url_topic": url_topic,
|
||||||
|
"device": device,
|
||||||
|
"unique_id": f"volvoAAOS2mqtt_{vin}_{entity['id']}",
|
||||||
|
"availability_topic": availability_topic
|
||||||
|
}
|
||||||
|
|
||||||
|
mqtt_client.publish(
|
||||||
|
f"homeassistant/image/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
|
||||||
|
json.dumps(config),
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity["id"] == "exterior_image":
|
||||||
|
mqtt_client.publish(
|
||||||
|
url_topic,
|
||||||
|
data["images"]["exteriorDefaultUrl"],
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity["id"] == "interior_image":
|
||||||
|
mqtt_client.publish(
|
||||||
|
url_topic,
|
||||||
|
data["images"]["interiorDefaultUrl"],
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def on_connect(client, userdata, flags, rc):
|
def on_connect(client, userdata, flags, rc):
|
||||||
send_heartbeat()
|
send_heartbeat()
|
||||||
if len(subscribed_topics) > 0:
|
if len(subscribed_topics) > 0:
|
||||||
@@ -62,52 +100,113 @@ def on_message(client, userdata, msg):
|
|||||||
|
|
||||||
payload = msg.payload.decode("UTF-8")
|
payload = msg.payload.decode("UTF-8")
|
||||||
if "climate_status" in msg.topic:
|
if "climate_status" in msg.topic:
|
||||||
global assumed_climate_state, climate_timer, engine_status
|
|
||||||
if payload == "ON":
|
if payload == "ON":
|
||||||
# Start the api call in another thread for HA performance
|
start_climate(vin)
|
||||||
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).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.start()
|
|
||||||
engine_status[vin] = check_engine_thread
|
|
||||||
|
|
||||||
# Starting timer to disable climate after 30 mins
|
|
||||||
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, ))
|
|
||||||
climate_timer[vin].start()
|
|
||||||
|
|
||||||
# Set and update switch status
|
|
||||||
assumed_climate_state[vin] = "ON"
|
|
||||||
update_car_data()
|
|
||||||
elif payload == "OFF":
|
elif payload == "OFF":
|
||||||
# Start the api call in another thread for HA performance
|
stop_climate(vin)
|
||||||
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
|
|
||||||
|
|
||||||
# Stop door check thread if running
|
|
||||||
if engine_status[vin].is_alive():
|
|
||||||
engine_status[vin].do_run = False
|
|
||||||
|
|
||||||
# Stop climate timer if active
|
|
||||||
if climate_timer[vin].is_alive():
|
|
||||||
climate_timer[vin].cancel()
|
|
||||||
|
|
||||||
# Set and update switch status
|
|
||||||
assumed_climate_state[vin] = "OFF"
|
|
||||||
update_car_data()
|
|
||||||
elif "lock_status" in msg.topic:
|
elif "lock_status" in msg.topic:
|
||||||
if payload == "LOCK":
|
if payload == "LOCK":
|
||||||
volvo.api_call(CAR_LOCK_URL, "POST", vin)
|
lock_car(vin)
|
||||||
|
|
||||||
# Force set unlocked state in HA because of slow api response
|
|
||||||
update_car_data(True, {"entity_id": "lock_status", "vin": vin, "state": "LOCKED"})
|
|
||||||
elif payload == "UNLOCK":
|
elif payload == "UNLOCK":
|
||||||
volvo.api_call(CAR_UNLOCK_URL, "POST", vin)
|
unlock_car(vin)
|
||||||
|
|
||||||
# Force set unlocked state in HA because of slow api response
|
|
||||||
update_car_data(True, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKED"})
|
|
||||||
elif "update_data" in msg.topic:
|
elif "update_data" in msg.topic:
|
||||||
if payload == "PRESS":
|
if payload == "PRESS":
|
||||||
update_car_data(True)
|
update_car_data(True)
|
||||||
|
elif "schedule" in msg.topic:
|
||||||
|
try:
|
||||||
|
d = json.loads(payload)
|
||||||
|
except ValueError as e:
|
||||||
|
logging.error("Can't set timer. Error: " + str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if d["mode"] == "timer":
|
||||||
|
start_climate_timer(d, vin)
|
||||||
|
else:
|
||||||
|
logging.warning("No schedule mode found, doing nothing")
|
||||||
|
|
||||||
|
|
||||||
|
def start_climate_timer(d, vin):
|
||||||
|
global active_schedules
|
||||||
|
try:
|
||||||
|
minute = int(d["start_time"].split(":")[1])
|
||||||
|
hour = int(d["start_time"].split(":")[0])
|
||||||
|
local_datetime = datetime.now(util.TZ)
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
update_car_data()
|
||||||
|
else:
|
||||||
|
logging.warning("Timer can not be set. Unusable start time entered")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Stop door check thread if running
|
||||||
|
if engine_status[vin].is_alive():
|
||||||
|
engine_status[vin].do_run = False
|
||||||
|
|
||||||
|
# Stop climate timer if active
|
||||||
|
if climate_timer[vin].is_alive():
|
||||||
|
climate_timer[vin].cancel()
|
||||||
|
|
||||||
|
# Set and update switch status
|
||||||
|
assumed_climate_state[vin] = "OFF"
|
||||||
|
update_car_data()
|
||||||
|
|
||||||
|
|
||||||
|
def activate_climate_timer(vin, start_time):
|
||||||
|
start_climate(vin)
|
||||||
|
active_schedules[vin]["timers"].remove(start_time)
|
||||||
|
update_car_data()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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.start()
|
||||||
|
engine_status[vin] = check_engine_thread
|
||||||
|
|
||||||
|
# Starting timer to disable climate after 30 mins
|
||||||
|
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin,))
|
||||||
|
climate_timer[vin].start()
|
||||||
|
|
||||||
|
# Set and update switch status
|
||||||
|
assumed_climate_state[vin] = "ON"
|
||||||
|
update_car_data()
|
||||||
|
|
||||||
|
|
||||||
def update_loop():
|
def update_loop():
|
||||||
@@ -140,20 +239,22 @@ def update_car_data(force_update=False, overwrite={}):
|
|||||||
state = assumed_climate_state[vin]
|
state = assumed_climate_state[vin]
|
||||||
elif entity["id"] == "last_data_update":
|
elif entity["id"] == "last_data_update":
|
||||||
state = last_data_update
|
state = last_data_update
|
||||||
|
elif entity["id"] == "active_schedules":
|
||||||
|
state = active_schedules[vin]
|
||||||
else:
|
else:
|
||||||
if entity["id"] == ov_entity_id and vin == ov_vin:
|
if entity["id"] == ov_entity_id and vin == ov_vin:
|
||||||
state = ov_state
|
state = ov_state
|
||||||
else:
|
else:
|
||||||
state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update)
|
state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update)
|
||||||
|
|
||||||
if entity["domain"] == "device_tracker":
|
if entity["domain"] == "device_tracker" or entity["id"] == "active_schedules":
|
||||||
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
||||||
else:
|
else:
|
||||||
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
|
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
|
||||||
|
|
||||||
mqtt_client.publish(
|
mqtt_client.publish(
|
||||||
topic,
|
topic,
|
||||||
json.dumps(state) if isinstance(state, dict) else state
|
json.dumps(state) if isinstance(state, dict) or isinstance(state, list) else state
|
||||||
)
|
)
|
||||||
update_ha_device(entity, vin, state)
|
update_ha_device(entity, vin, state)
|
||||||
|
|
||||||
@@ -186,6 +287,9 @@ def update_ha_device(entity, vin, state):
|
|||||||
if entity.get("unit"):
|
if entity.get("unit"):
|
||||||
config["unit_of_measurement"] = entity["unit"]
|
config["unit_of_measurement"] = entity["unit"]
|
||||||
|
|
||||||
|
if entity.get("state_class"):
|
||||||
|
config["state_class"] = entity["state_class"]
|
||||||
|
|
||||||
if entity.get("domain") == "device_tracker":
|
if entity.get("domain") == "device_tracker":
|
||||||
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
||||||
|
|
||||||
@@ -224,14 +328,18 @@ def create_ha_devices():
|
|||||||
if entity.get("unit"):
|
if entity.get("unit"):
|
||||||
config["unit_of_measurement"] = entity["unit"]
|
config["unit_of_measurement"] = entity["unit"]
|
||||||
|
|
||||||
if entity.get("domain") == "device_tracker":
|
if entity.get("state_class"):
|
||||||
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
config["state_class"] = entity["state_class"]
|
||||||
|
|
||||||
if entity.get("domain") in ["switch", "lock", "button"]:
|
if entity.get("domain") == "device_tracker" or entity.get("id") == "active_schedules":
|
||||||
|
config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
|
||||||
|
elif entity.get("domain") in ["switch", "lock", "button"]:
|
||||||
command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command"
|
command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command"
|
||||||
config["command_topic"] = command_topic
|
config["command_topic"] = command_topic
|
||||||
subscribed_topics.append(command_topic)
|
subscribed_topics.append(command_topic)
|
||||||
mqtt_client.subscribe(command_topic)
|
mqtt_client.subscribe(command_topic)
|
||||||
|
elif entity.get("domain") == "image":
|
||||||
|
config["url_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/image_url"
|
||||||
|
|
||||||
mqtt_client.publish(
|
mqtt_client.publish(
|
||||||
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
|
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
|
||||||
|
|||||||
+19
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import pytz
|
import pytz
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import config
|
import config
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
@@ -10,6 +11,21 @@ from config import settings
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
TZ = None
|
TZ = None
|
||||||
|
SENSITIVE_PATTERNS = [
|
||||||
|
r"[A-Z0-9]{17}", # VIN
|
||||||
|
r"\d{1,2}\.\d{5,16}" # Location
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SensitiveDataFilter(logging.Filter):
|
||||||
|
def __init__(self, patterns=None):
|
||||||
|
super().__init__()
|
||||||
|
self.patterns = patterns or []
|
||||||
|
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
for pattern in self.patterns:
|
||||||
|
record.msg = re.sub(pattern, "<REDACTED>", record.msg)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_icon_between(icon_list, state):
|
def get_icon_between(icon_list, state):
|
||||||
@@ -32,6 +48,8 @@ def setup_logging():
|
|||||||
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s',
|
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s',
|
||||||
'%b %d %H:%M:%S')
|
'%b %d %H:%M:%S')
|
||||||
file_log_handler.setFormatter(formatter)
|
file_log_handler.setFormatter(formatter)
|
||||||
|
sensitive_data_filter = SensitiveDataFilter(SENSITIVE_PATTERNS)
|
||||||
|
file_log_handler.addFilter(sensitive_data_filter)
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
console_log_handler = logging.StreamHandler(sys.stdout)
|
console_log_handler = logging.StreamHandler(sys.stdout)
|
||||||
@@ -39,6 +57,7 @@ def setup_logging():
|
|||||||
|
|
||||||
logger.addHandler(console_log_handler)
|
logger.addHandler(console_log_handler)
|
||||||
logger.addHandler(file_log_handler)
|
logger.addHandler(file_log_handler)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
if "debug" in settings:
|
if "debug" in settings:
|
||||||
|
|||||||
+38
-1
@@ -111,6 +111,7 @@ def get_vehicles():
|
|||||||
raise Exception("No vehicle found, exiting application!")
|
raise Exception("No vehicle found, exiting application!")
|
||||||
else:
|
else:
|
||||||
initialize_climate(vins)
|
initialize_climate(vins)
|
||||||
|
initialize_scheduler(vins)
|
||||||
logging.info("Vin: " + str(vins) + " found!")
|
logging.info("Vin: " + str(vins) + " found!")
|
||||||
|
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ def get_vehicle_details(vin):
|
|||||||
"model": data['descriptions']['model'],
|
"model": data['descriptions']['model'],
|
||||||
"name": f"{data['descriptions']['model']} ({data['modelYear']}) - {vin}",
|
"name": f"{data['descriptions']['model']} ({data['modelYear']}) - {vin}",
|
||||||
}
|
}
|
||||||
|
mqtt.send_car_images(vin, data, device)
|
||||||
elif response.status_code == 500 and not settings.volvoData["vin"]:
|
elif response.status_code == 500 and not settings.volvoData["vin"]:
|
||||||
# Workaround for some cars that are not returning vehicle details
|
# Workaround for some cars that are not returning vehicle details
|
||||||
device = {
|
device = {
|
||||||
@@ -167,6 +169,12 @@ def check_supported_endpoints():
|
|||||||
logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
|
logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_scheduler(vins):
|
||||||
|
for vin in vins:
|
||||||
|
mqtt.active_schedules[vin] = {"timers": []}
|
||||||
|
mqtt.subscribed_topics = [f"homeassistant/schedule/{vin}/command"]
|
||||||
|
|
||||||
|
|
||||||
def initialize_climate(vins):
|
def initialize_climate(vins):
|
||||||
for vin in vins:
|
for vin in vins:
|
||||||
mqtt.assumed_climate_state[vin] = "OFF"
|
mqtt.assumed_climate_state[vin] = "OFF"
|
||||||
@@ -179,6 +187,20 @@ def disable_climate(vin):
|
|||||||
mqtt.update_car_data()
|
mqtt.update_car_data()
|
||||||
|
|
||||||
|
|
||||||
|
def check_lock_status(vin, old_state):
|
||||||
|
max_runs = 10
|
||||||
|
done_runs = 0
|
||||||
|
lock_state = api_call(LOCK_STATE_URL, "GET", vin, "lock_status", True)
|
||||||
|
while lock_state == old_state:
|
||||||
|
done_runs = done_runs + 1
|
||||||
|
if done_runs >= max_runs:
|
||||||
|
break
|
||||||
|
lock_state = api_call(LOCK_STATE_URL, "GET", vin, "lock_status", True)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
mqtt.update_car_data()
|
||||||
|
|
||||||
|
|
||||||
def check_engine_status(vin):
|
def check_engine_status(vin):
|
||||||
endpoint_url = ""
|
endpoint_url = ""
|
||||||
engine_state_supported = False
|
engine_state_supported = False
|
||||||
@@ -206,7 +228,8 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
|
|||||||
if datetime.now(util.TZ) >= token_expires_at:
|
if datetime.now(util.TZ) >= token_expires_at:
|
||||||
refresh_auth()
|
refresh_auth()
|
||||||
|
|
||||||
if url in [RECHARGE_STATE_URL, WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, STATISTICS_URL]:
|
if url in [RECHARGE_STATE_URL, WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL,
|
||||||
|
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL]:
|
||||||
# Minimize API calls for endpoints with multiple values
|
# Minimize API calls for endpoints with multiple values
|
||||||
response = cached_request(url, method, vin, force_update)
|
response = cached_request(url, method, vin, force_update)
|
||||||
if response is None:
|
if response is None:
|
||||||
@@ -414,5 +437,19 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
if distance_to_empty > 0:
|
if distance_to_empty > 0:
|
||||||
return util.convert_metric_values(data["distanceToEmpty"]["value"])
|
return util.convert_metric_values(data["distanceToEmpty"]["value"])
|
||||||
return None
|
return None
|
||||||
|
elif sensor_id == "hours_to_service":
|
||||||
|
return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None
|
||||||
|
elif sensor_id == "km_to_service":
|
||||||
|
if util.keys_exists(data, "kmToService"):
|
||||||
|
km_to_service = int(data["kmToService"]["value"])
|
||||||
|
if km_to_service > 0:
|
||||||
|
return util.convert_metric_values(data["kmToService"]["value"])
|
||||||
|
return None
|
||||||
|
elif sensor_id == "months_to_service":
|
||||||
|
return data["monthsToService"]["value"] if util.keys_exists(data, "monthsToService") else None
|
||||||
|
elif sensor_id == "service_warning_status":
|
||||||
|
return data["serviceWarningStatus"]["value"] if util.keys_exists(data, "serviceWarningStatus") 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
|
||||||
|
|||||||
Reference in New Issue
Block a user