forked from peter/volvo2mqtt
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67320c32f6 | ||
|
|
30f415cb1f | ||
|
|
241b2f7ba4 | ||
|
|
80017f64e3 | ||
|
|
47357fb582 | ||
|
|
9f1209e628 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name: "Volvo2Mqtt"
|
name: "Volvo2Mqtt"
|
||||||
description: "Volvo AAOS MQTT bridge"
|
description: "Volvo AAOS MQTT bridge"
|
||||||
version: "1.6.0"
|
version: "1.6.1"
|
||||||
slug: "volvo2mqtt"
|
slug: "volvo2mqtt"
|
||||||
init: false
|
init: false
|
||||||
url: "https://github.com/Dielee/volvo2mqtt"
|
url: "https://github.com/Dielee/volvo2mqtt"
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,6 @@
|
|||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
VERSION = "v1.6.0"
|
VERSION = "v1.6.1"
|
||||||
|
|
||||||
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"
|
||||||
@@ -19,6 +19,8 @@ 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"
|
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"
|
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"
|
||||||
|
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 = {
|
units = {
|
||||||
"en_GB": {
|
"en_GB": {
|
||||||
@@ -79,5 +81,7 @@ supported_entities = [
|
|||||||
{"name": "Engine State", "domain": "sensor", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
|
{"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": "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 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 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},
|
||||||
|
{"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},
|
||||||
]
|
]
|
||||||
|
|||||||
+28
-8
@@ -16,6 +16,7 @@ subscribed_topics = []
|
|||||||
assumed_climate_state = {}
|
assumed_climate_state = {}
|
||||||
last_data_update = None
|
last_data_update = None
|
||||||
climate_timer = {}
|
climate_timer = {}
|
||||||
|
door_status = {}
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
def connect():
|
||||||
@@ -59,29 +60,48 @@ 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
|
global assumed_climate_state, climate_timer, door_status
|
||||||
if payload == "ON":
|
if payload == "ON":
|
||||||
api_thread = Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin))
|
# Start the api call in another thread for HA performance
|
||||||
api_thread.start()
|
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start()
|
||||||
assumed_climate_state[vin] = "ON"
|
|
||||||
|
# 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
|
# Starting timer to disable climate after 30 mins
|
||||||
|
|
||||||
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, ))
|
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, ))
|
||||||
climate_timer[vin].start()
|
climate_timer[vin].start()
|
||||||
|
# Set and update switch status
|
||||||
|
|
||||||
|
assumed_climate_state[vin] = "ON"
|
||||||
update_car_data()
|
update_car_data()
|
||||||
elif payload == "OFF":
|
elif payload == "OFF":
|
||||||
api_thread = Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin))
|
# Start the api call in another thread for HA performance
|
||||||
api_thread.start()
|
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start()
|
||||||
assumed_climate_state[vin] = "OFF"
|
|
||||||
# Stop timer if active
|
# 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():
|
if climate_timer[vin].is_alive():
|
||||||
climate_timer[vin].cancel()
|
climate_timer[vin].cancel()
|
||||||
|
|
||||||
|
# Set and update switch status
|
||||||
|
assumed_climate_state[vin] = "OFF"
|
||||||
update_car_data()
|
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)
|
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"})
|
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)
|
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"})
|
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":
|
||||||
|
|||||||
+39
-9
@@ -1,13 +1,15 @@
|
|||||||
import requests
|
import requests
|
||||||
import mqtt
|
import mqtt
|
||||||
import util
|
import util
|
||||||
|
import time
|
||||||
|
from threading import Thread, currentThread
|
||||||
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
|
||||||
from const import charging_system_states, charging_connection_states, door_states, window_states, \
|
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, \
|
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, \
|
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 = requests.Session()
|
||||||
session.headers = {
|
session.headers = {
|
||||||
@@ -65,7 +67,13 @@ def refresh_auth():
|
|||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"refresh_token": refresh_token
|
"refresh_token": refresh_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
auth = requests.post(OAUTH_URL, data=body, headers=headers)
|
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:
|
if auth.status_code == 200:
|
||||||
data = auth.json()
|
data = auth.json()
|
||||||
session.headers.update({'authorization': "Bearer " + data["access_token"]})
|
session.headers.update({'authorization': "Bearer " + data["access_token"]})
|
||||||
@@ -162,10 +170,24 @@ def initialize_climate(vins):
|
|||||||
|
|
||||||
def disable_climate(vin):
|
def disable_climate(vin):
|
||||||
print("Turning climate off by timer!")
|
print("Turning climate off by timer!")
|
||||||
|
mqtt.door_status[vin].do_run = False
|
||||||
mqtt.assumed_climate_state[vin] = "OFF"
|
mqtt.assumed_climate_state[vin] = "OFF"
|
||||||
mqtt.update_car_data()
|
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):
|
def api_call(url, method, vin, sensor_id=None, force_update=False):
|
||||||
global token_expires_at
|
global token_expires_at
|
||||||
if datetime.now(util.TZ) >= token_expires_at:
|
if datetime.now(util.TZ) >= token_expires_at:
|
||||||
@@ -183,17 +205,17 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
|
|||||||
response = session.get(url.format(vin), timeout=15)
|
response = session.get(url.format(vin), timeout=15)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print("Error getting data: " + str(e))
|
print("Error getting data: " + str(e))
|
||||||
return ""
|
return None
|
||||||
elif method == "POST":
|
elif method == "POST":
|
||||||
print("Starting " + method + " call against " + url)
|
print("Starting " + method + " call against " + url)
|
||||||
try:
|
try:
|
||||||
response = session.post(url.format(vin), timeout=20)
|
response = session.post(url.format(vin), timeout=20)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print("Error getting data: " + str(e))
|
print("Error getting data: " + str(e))
|
||||||
return ""
|
return None
|
||||||
else:
|
else:
|
||||||
print("Unkown method posted: " + method + ". Returning nothing")
|
print("Unkown method posted: " + method + ". Returning nothing")
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -205,9 +227,12 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
|
|||||||
print("Car in use, cannot start pre climatization")
|
print("Car in use, cannot start pre climatization")
|
||||||
mqtt.assumed_climate_state[vin] = "OFF"
|
mqtt.assumed_climate_state[vin] = "OFF"
|
||||||
mqtt.update_car_data()
|
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:
|
else:
|
||||||
print("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
|
print("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
|
||||||
return ""
|
return None
|
||||||
return parse_api_data(data, sensor_id)
|
return parse_api_data(data, sensor_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -225,10 +250,10 @@ def cached_request(url, method, vin, force_update=False):
|
|||||||
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
data = {"response": response, "last_update": datetime.now(util.TZ)}
|
||||||
cached_requests[vin + "_" + url] = data
|
cached_requests[vin + "_" + url] = data
|
||||||
else:
|
else:
|
||||||
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= settings[
|
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() \
|
||||||
"updateInterval"] \
|
>= settings["updateInterval"] or (force_update and
|
||||||
or (force_update and (
|
(datetime.now(util.TZ) - cached_requests[vin + "_" + url]
|
||||||
datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= 2):
|
["last_update"]).total_seconds() >= 2):
|
||||||
# Old Data in Cache, or force mode active, updating
|
# Old Data in Cache, or force mode active, updating
|
||||||
print("Starting " + method + " call against " + url)
|
print("Starting " + method + " call against " + url)
|
||||||
try:
|
try:
|
||||||
@@ -380,5 +405,10 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
"latitude": raw_data["coordinates"][1],
|
"latitude": raw_data["coordinates"][1],
|
||||||
"gps_accuracy": 1}
|
"gps_accuracy": 1}
|
||||||
return coordinates
|
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:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user