Compare commits

..
10 Commits
Author SHA1 Message Date
Linus DietzandGitHub 67320c32f6 Optimize climate timer (#40)
* Add logic to turn climate switch off if driver door is opened

* Update car data after climate state change

* Climate stop on left or right door open

* Bump version
2023-06-29 17:46:34 +02:00
Dielee 30f415cb1f Merge branch 'main' of https://github.com/Dielee/volvo2mqtt into main 2023-06-29 15:16:05 +02:00
Dielee 241b2f7ba4 Fix app crash on credential refresh 2023-06-29 15:15:49 +02:00
Linus Dietz 80017f64e3 Fix app crash on credential refresh 2023-06-29 15:14:24 +02:00
Linus Dietz 47357fb582 Add "Washer Fluid Level" and "Distance to Empty" sensors 2023-06-29 08:36:23 +02:00
Linus Dietz 9f1209e628 Fix wrong return values for not working endpoints 2023-06-29 08:06:31 +02:00
Linus DietzandGitHub f0d4d68792 Add value conversion from metric to imperial (#36)
* Convert km to miles, km/h to mp/h if babelLocale is en_GB or en_US #34

* Remove unused variable

* Change speed unit for imperial system // Bump version
2023-06-27 15:29:48 +02:00
Linus DietzandGitHub 140a735773 Update README.md 2023-06-27 14:24:43 +02:00
Linus Dietz ebcabc6ff4 rework error message for get_vehicles 2023-06-27 09:11:07 +02:00
Linus Dietz c995c2a44d Optimize error message for get_vehicles 2023-06-27 09:09:28 +02:00
7 changed files with 476 additions and 391 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.5.6"
version: "1.6.1"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
+23 -4
View File
@@ -1,6 +1,6 @@
from config import settings
VERSION = "v1.5.6"
VERSION = "v1.6.1"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles"
@@ -19,6 +19,23 @@ ENGINE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/
BATTERY_CHARGE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/battery-charge-level"
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"
DISTANCE_TO_EMPTY_URL = "https://api.volvocars.com/extended-vehicle/v1/vehicles/{0}/resources/distanceToEmpty"
WASHER_FLUID_LEVEL_URL = "https://api.volvocars.com/extended-vehicle/v1/vehicles/{0}/resources/washerFluidLevel"
units = {
"en_GB": {
"divider": 1.60934,
"electric_range": {"unit": "mi"},
"odometer": {"unit": "mi"},
"average_speed": {"unit": "mph"}
},
"en_US": {
"divider": 1.60934,
"electric_range": {"unit": "mi"},
"odometer": {"unit": "mi"},
"average_speed": {"unit": "mph"}
}
}
availability_topic = "volvoAAOS2mqtt/availability"
@@ -34,12 +51,12 @@ door_states = {"CLOSED": "OFF", "OPEN": "ON"}
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": BATTERY_CHARGE_STATE_URL},
{"name": "Electric Range", "domain": "sensor", "id": "electric_range", "unit": "km" if settings["babelLocale"] != "en_US" else "mi", "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},
{"name": "Estimated Charging Time", "domain": "sensor", "id": "estimated_charging_time", "unit": "minutes", "icon": "timer-sync-outline", "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": "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 settings["babelLocale"] != "en_US" else "mi", "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},
{"name": "Last Data Update", "domain": "sensor", "id": "last_data_update", "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 Right", "domain": "binary_sensor", "device_class": "window", "id": "window_front_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
@@ -64,5 +81,7 @@ supported_entities = [
{"name": "Engine State", "domain": "sensor", "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},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL},
{"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": "km/h" if settings["babelLocale"] != "en_US" else "mp/h", "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},
{"name": "Distance to Empty", "domain": "sensor", "id": "distance_to_empty", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["odometer"]["unit"], "icon": "map-marker-distance", "url": DISTANCE_TO_EMPTY_URL},
{"name": "Washer Fluid Level", "domain": "sensor", "id": "washer_fluid_level", "icon": "wiper-wash", "url": WASHER_FLUID_LEVEL_URL},
]
-1
View File
@@ -3,7 +3,6 @@ from mqtt import update_loop, connect
from const import VERSION
from util import set_tz
if __name__ == '__main__':
print("Starting volvo2mqtt version " + VERSION)
set_tz()
+28 -8
View File
@@ -16,6 +16,7 @@ subscribed_topics = []
assumed_climate_state = {}
last_data_update = None
climate_timer = {}
door_status = {}
def connect():
@@ -59,29 +60,48 @@ def on_message(client, userdata, msg):
payload = msg.payload.decode("UTF-8")
if "climate_status" in msg.topic:
global assumed_climate_state, climate_timer
global assumed_climate_state, climate_timer, door_status
if payload == "ON":
api_thread = Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin))
api_thread.start()
assumed_climate_state[vin] = "ON"
# 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
door_thread = Thread(target=volvo.check_door_status, args=(vin, ))
door_thread.start()
door_status[vin] = door_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":
api_thread = Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin))
api_thread.start()
assumed_climate_state[vin] = "OFF"
# Stop timer if active
# 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 door_status[vin].is_alive():
door_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:
if payload == "LOCK":
volvo.api_call(CAR_LOCK_URL, "POST", 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":
volvo.api_call(CAR_UNLOCK_URL, "POST", 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:
if payload == "PRESS":
+9
View File
@@ -1,5 +1,6 @@
import pytz
import os
from const import units
from config import settings
TZ = None
@@ -34,3 +35,11 @@ def set_tz():
TZ = pytz.timezone(settings_tz)
else:
raise Exception("No timezone setting found! Please read the README!")
def convert_metric_values(value):
if keys_exists(units, settings["babelLocale"]):
divider = units[settings["babelLocale"]]["divider"]
return round((float(value) / divider), 2)
else:
return value
+55 -17
View File
@@ -1,13 +1,15 @@
import requests
import mqtt
import util
import time
from threading import Thread, currentThread
from datetime import datetime, timedelta
from config import settings
from babel.dates import format_datetime
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
STATISTICS_URL, CLIMATE_STOP_URL
session = requests.Session()
session.headers = {
@@ -65,7 +67,13 @@ def refresh_auth():
"grant_type": "refresh_token",
"refresh_token": refresh_token
}
try:
auth = requests.post(OAUTH_URL, data=body, headers=headers)
except requests.exceptions.RequestException as e:
print("Error refreshing credentials data: " + str(e))
return None
if auth.status_code == 200:
data = auth.json()
session.headers.update({'authorization': "Bearer " + data["access_token"]})
@@ -87,7 +95,9 @@ def get_vehicles():
else:
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
else:
raise Exception("Error getting vehicles: " + str(vehicles.status_code))
error = vehicles.json()
raise Exception(
"Error getting vehicles: " + str(vehicles.status_code) + ". " + str(error["error"].get("message")))
else:
if isinstance(settings.volvoData["vin"], list):
# If setting is a list, copy
@@ -160,10 +170,24 @@ def initialize_climate(vins):
def disable_climate(vin):
print("Turning climate off by timer!")
mqtt.door_status[vin].do_run = False
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
def check_door_status(vin):
t = currentThread()
while getattr(t, "do_run", True):
lock_door_left = api_call(LOCK_STATE_URL, "GET", vin, "door_front_left", True)
lock_door_right = api_call(LOCK_STATE_URL, "GET", vin, "door_front_right", True)
if lock_door_left == "ON" or lock_door_right == "ON":
Thread(target=api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
break
time.sleep(5)
def api_call(url, method, vin, sensor_id=None, force_update=False):
global token_expires_at
if datetime.now(util.TZ) >= token_expires_at:
@@ -181,17 +205,17 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return ""
return None
elif method == "POST":
print("Starting " + method + " call against " + url)
try:
response = session.post(url.format(vin), timeout=20)
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return ""
return None
else:
print("Unkown method posted: " + method + ". Returning nothing")
return ""
return None
if response.status_code == 200:
data = response.json()
@@ -203,9 +227,12 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
print("Car in use, cannot start pre climatization")
mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data()
elif "extended-vehicle" in url and response.status_code == 403:
# Suppress 403 errors for unsupported extended-vehicle api cars
return None
else:
print("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
return ""
return None
return parse_api_data(data, sensor_id)
@@ -223,8 +250,10 @@ def cached_request(url, method, vin, force_update=False):
data = {"response": response, "last_update": datetime.now(util.TZ)}
cached_requests[vin + "_" + url] = data
else:
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= settings["updateInterval"] \
or (force_update and (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= 2):
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() \
>= settings["updateInterval"] or (force_update and
(datetime.now(util.TZ) - cached_requests[vin + "_" + url]
["last_update"]).total_seconds() >= 2):
# Old Data in Cache, or force mode active, updating
print("Starting " + method + " call against " + url)
try:
@@ -245,13 +274,14 @@ def parse_api_data(data, sensor_id=None):
if sensor_id == "battery_charge_level":
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
elif sensor_id == "electric_range":
return data["electricRange"]["value"] if util.keys_exists(data, "electricRange") else None
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
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
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"]]
@@ -265,7 +295,8 @@ def parse_api_data(data, sensor_id=None):
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")
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"])
@@ -283,7 +314,8 @@ def parse_api_data(data, sensor_id=None):
multiplier = 1
elif multiplier < 1:
multiplier = 1
return int(data["odometer"]["value"]) * multiplier if util.keys_exists(data, "odometer") else None
return util.convert_metric_values(int(data["odometer"]["value"]) * multiplier) \
if util.keys_exists(data, "odometer") else None
elif sensor_id == "window_front_left":
return window_states[data["frontLeftWindowOpen"]["value"]] if util.keys_exists(data, "frontLeftWindowOpen") \
else None
@@ -299,7 +331,8 @@ def parse_api_data(data, sensor_id=None):
elif sensor_id == "door_front_left":
return door_states[data["frontLeftDoorOpen"]["value"]] if util.keys_exists(data, "frontLeftDoorOpen") else None
elif sensor_id == "door_front_right":
return door_states[data["frontRightDoorOpen"]["value"]] if util.keys_exists(data, "frontRightDoorOpen") else None
return door_states[data["frontRightDoorOpen"]["value"]] \
if util.keys_exists(data, "frontRightDoorOpen") else None
elif sensor_id == "door_rear_left":
return door_states[data["rearLeftDoorOpen"]["value"]] if util.keys_exists(data, "rearLeftDoorOpen") else None
elif sensor_id == "door_rear_right":
@@ -358,7 +391,7 @@ def parse_api_data(data, sensor_id=None):
divider = 1
elif divider < 1:
divider = 1
return average_speed / divider
return util.convert_metric_values(average_speed / divider)
else:
return None
else:
@@ -372,5 +405,10 @@ def parse_api_data(data, sensor_id=None):
"latitude": raw_data["coordinates"][1],
"gps_accuracy": 1}
return coordinates
elif sensor_id == "distance_to_empty":
return util.convert_metric_values(data["distanceToEmpty"]["value"]) \
if util.keys_exists(data, "distanceToEmpty") else None
elif sensor_id == "washer_fluid_level":
return data["washerFluidLevel"]["value"] if util.keys_exists(data, "washerFluidLevel") else None
else:
return None