forked from peter/volvo2mqtt
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c0ce8976e | ||
|
|
34a0b9b2a7 | ||
|
|
67320c32f6 | ||
|
|
30f415cb1f | ||
|
|
241b2f7ba4 | ||
|
|
80017f64e3 | ||
|
|
47357fb582 | ||
|
|
9f1209e628 | ||
|
|
f0d4d68792 | ||
|
|
140a735773 | ||
|
|
ebcabc6ff4 | ||
|
|
c995c2a44d |
@@ -2,3 +2,5 @@
|
|||||||
venv
|
venv
|
||||||
src/settings.dev.json
|
src/settings.dev.json
|
||||||
.env
|
.env
|
||||||
|
/src/volvo2mqtt.log
|
||||||
|
/src/volvo2mqtt.log.1
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@ RUN pip install -r requirements.txt
|
|||||||
|
|
||||||
# copy the content of the local src directory to the working directory
|
# copy the content of the local src directory to the working directory
|
||||||
COPY / .
|
COPY / .
|
||||||
|
RUN chmod a+x /volvoAAOS2mqtt/run.sh
|
||||||
|
|
||||||
# command to run on container start
|
# command to run on container start
|
||||||
CMD [ "python", "-u", "./main.py" ]
|
CMD [ "/volvoAAOS2mqtt/run.sh" ]
|
||||||
+3
-1
@@ -1,10 +1,12 @@
|
|||||||
name: "Volvo2Mqtt"
|
name: "Volvo2Mqtt"
|
||||||
description: "Volvo AAOS MQTT bridge"
|
description: "Volvo AAOS MQTT bridge"
|
||||||
version: "1.5.6"
|
version: "1.6.3"
|
||||||
slug: "volvo2mqtt"
|
slug: "volvo2mqtt"
|
||||||
init: false
|
init: false
|
||||||
url: "https://github.com/Dielee/volvo2mqtt"
|
url: "https://github.com/Dielee/volvo2mqtt"
|
||||||
apparmor: true
|
apparmor: true
|
||||||
|
map:
|
||||||
|
- addons:rw
|
||||||
options:
|
options:
|
||||||
updateInterval: 300
|
updateInterval: 300
|
||||||
babelLocale: null
|
babelLocale: null
|
||||||
|
|||||||
+20
-4
@@ -1,6 +1,6 @@
|
|||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
VERSION = "v1.5.6"
|
VERSION = "v1.6.3"
|
||||||
|
|
||||||
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,21 @@ 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"
|
||||||
|
|
||||||
|
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"
|
availability_topic = "volvoAAOS2mqtt/availability"
|
||||||
|
|
||||||
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle",
|
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle",
|
||||||
@@ -34,12 +49,12 @@ door_states = {"CLOSED": "OFF", "OPEN": "ON"}
|
|||||||
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},
|
||||||
{"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},
|
||||||
{"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": "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 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 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": "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 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},
|
||||||
@@ -64,5 +79,6 @@ 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 settings["babelLocale"] != "en_US" else "mp/h", "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": 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}
|
||||||
]
|
]
|
||||||
|
|||||||
+4
-2
@@ -1,12 +1,14 @@
|
|||||||
|
import logging
|
||||||
from volvo import authorize
|
from volvo import authorize
|
||||||
from mqtt import update_loop, connect
|
from mqtt import update_loop, connect
|
||||||
from const import VERSION
|
from const import VERSION
|
||||||
from util import set_tz
|
from util import set_tz, setup_logging
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("Starting volvo2mqtt version " + VERSION)
|
|
||||||
set_tz()
|
set_tz()
|
||||||
|
setup_logging()
|
||||||
|
logging.info("Starting volvo2mqtt version " + VERSION)
|
||||||
authorize()
|
authorize()
|
||||||
connect()
|
connect()
|
||||||
update_loop()
|
update_loop()
|
||||||
|
|||||||
+33
-12
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
import json
|
import json
|
||||||
@@ -16,6 +17,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():
|
||||||
@@ -47,41 +49,60 @@ def on_connect(client, userdata, flags, rc):
|
|||||||
|
|
||||||
|
|
||||||
def on_disconnect(client, userdata, rc):
|
def on_disconnect(client, userdata, rc):
|
||||||
print("MQTT disconnected, reconnecting automatically")
|
logging.warning("MQTT disconnected, reconnecting automatically")
|
||||||
|
|
||||||
|
|
||||||
def on_message(client, userdata, msg):
|
def on_message(client, userdata, msg):
|
||||||
try:
|
try:
|
||||||
vin = msg.topic.split('/')[2].split('_')[0]
|
vin = msg.topic.split('/')[2].split('_')[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
print("Error - Cannot get vin from MQTT topic!")
|
logging.error("Error - Cannot get vin from MQTT topic!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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":
|
||||||
@@ -91,10 +112,10 @@ def on_message(client, userdata, msg):
|
|||||||
def update_loop():
|
def update_loop():
|
||||||
create_ha_devices()
|
create_ha_devices()
|
||||||
while True:
|
while True:
|
||||||
print("Sending mqtt update...")
|
logging.info("Sending mqtt update...")
|
||||||
send_heartbeat()
|
send_heartbeat()
|
||||||
update_car_data()
|
update_car_data()
|
||||||
print("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
|
logging.info("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
|
||||||
time.sleep(settings["updateInterval"])
|
time.sleep(settings["updateInterval"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export IS_HA_ADDON="true"
|
||||||
|
|
||||||
|
python -u main.py
|
||||||
+45
@@ -1,10 +1,46 @@
|
|||||||
|
import logging
|
||||||
import pytz
|
import pytz
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from logging import handlers
|
||||||
|
from datetime import datetime
|
||||||
|
from const import units
|
||||||
from config import settings
|
from config import settings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
TZ = None
|
TZ = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
log_location = "volvo2mqtt.log"
|
||||||
|
if os.environ.get("IS_HA_ADDON"):
|
||||||
|
check_existing_folder()
|
||||||
|
log_location = "/addons/volvo2mqtt/log/volvo2mqtt.log"
|
||||||
|
|
||||||
|
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
|
||||||
|
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s',
|
||||||
|
'%b %d %H:%M:%S')
|
||||||
|
file_log_handler.setFormatter(formatter)
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
console_log_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_log_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(console_log_handler)
|
||||||
|
logger.addHandler(file_log_handler)
|
||||||
|
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
if "debug" in settings:
|
||||||
|
if settings["debug"]:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def check_existing_folder():
|
||||||
|
Path("/addons/volvo2mqtt/log/").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def keys_exists(element, *keys):
|
def keys_exists(element, *keys):
|
||||||
""""
|
""""
|
||||||
Check if *keys (nested) exists in `element` (dict).
|
Check if *keys (nested) exists in `element` (dict).
|
||||||
@@ -34,3 +70,12 @@ def set_tz():
|
|||||||
TZ = pytz.timezone(settings_tz)
|
TZ = pytz.timezone(settings_tz)
|
||||||
else:
|
else:
|
||||||
raise Exception("No timezone setting found! Please read the README!")
|
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
|
||||||
|
|
||||||
|
|||||||
+78
-50
@@ -1,13 +1,16 @@
|
|||||||
|
import logging
|
||||||
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 = {
|
||||||
@@ -53,7 +56,7 @@ def authorize():
|
|||||||
|
|
||||||
|
|
||||||
def refresh_auth():
|
def refresh_auth():
|
||||||
print("Refreshing credentials")
|
logging.info("Refreshing credentials")
|
||||||
global refresh_token
|
global refresh_token
|
||||||
headers = {
|
headers = {
|
||||||
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
|
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
|
||||||
@@ -65,7 +68,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:
|
||||||
|
logging.error("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"]})
|
||||||
@@ -87,7 +96,9 @@ def get_vehicles():
|
|||||||
else:
|
else:
|
||||||
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
|
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
|
||||||
else:
|
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:
|
else:
|
||||||
if isinstance(settings.volvoData["vin"], list):
|
if isinstance(settings.volvoData["vin"], list):
|
||||||
# If setting is a list, copy
|
# If setting is a list, copy
|
||||||
@@ -100,16 +111,14 @@ 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)
|
||||||
print("Vin: " + str(vins) + " found!")
|
logging.info("Vin: " + str(vins) + " found!")
|
||||||
|
|
||||||
|
|
||||||
def get_vehicle_details(vin):
|
def get_vehicle_details(vin):
|
||||||
response = session.get(VEHICLE_DETAILS_URL.format(vin), timeout=15)
|
response = session.get(VEHICLE_DETAILS_URL.format(vin), timeout=15)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()["data"]
|
data = response.json()["data"]
|
||||||
if "debug" in settings:
|
logging.debug(response.text)
|
||||||
if settings["debug"]:
|
|
||||||
print(response.text)
|
|
||||||
device = {
|
device = {
|
||||||
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
|
"identifiers": [f"volvoAAOS2mqtt_{vin}"],
|
||||||
"manufacturer": "Volvo",
|
"manufacturer": "Volvo",
|
||||||
@@ -147,10 +156,10 @@ def check_supported_endpoints():
|
|||||||
state = ""
|
state = ""
|
||||||
|
|
||||||
if state is not None:
|
if state is not None:
|
||||||
print("Success! " + entity["name"] + " is supported by your vehicle.")
|
logging.info("Success! " + entity["name"] + " is supported by your vehicle.")
|
||||||
supported_endpoints[vin].append(entity)
|
supported_endpoints[vin].append(entity)
|
||||||
else:
|
else:
|
||||||
print("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
|
logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
|
||||||
|
|
||||||
|
|
||||||
def initialize_climate(vins):
|
def initialize_climate(vins):
|
||||||
@@ -159,11 +168,25 @@ def initialize_climate(vins):
|
|||||||
|
|
||||||
|
|
||||||
def disable_climate(vin):
|
def disable_climate(vin):
|
||||||
print("Turning climate off by timer!")
|
logging.info("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:
|
||||||
@@ -176,36 +199,40 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
|
|||||||
# Exception caught while getting data from volvo api, doing nothing
|
# Exception caught while getting data from volvo api, doing nothing
|
||||||
return None
|
return None
|
||||||
elif method == "GET":
|
elif method == "GET":
|
||||||
print("Starting " + method + " call against " + url)
|
logging.debug("Starting " + method + " call against " + url)
|
||||||
try:
|
try:
|
||||||
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))
|
logging.error("Error getting data: " + str(e))
|
||||||
return ""
|
return None
|
||||||
elif method == "POST":
|
elif method == "POST":
|
||||||
print("Starting " + method + " call against " + url)
|
logging.debug("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))
|
logging.error("Error getting data: " + str(e))
|
||||||
return ""
|
return None
|
||||||
else:
|
else:
|
||||||
print("Unkown method posted: " + method + ". Returning nothing")
|
logging.error("Unkown method posted: " + method + ". Returning nothing")
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
|
logging.debug("Response status code: " + str(response.status_code))
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if "debug" in settings:
|
logging.debug(response.text)
|
||||||
if settings["debug"]:
|
|
||||||
print(response.text)
|
|
||||||
else:
|
else:
|
||||||
|
logging.debug(response.text)
|
||||||
if url == CLIMATE_START_URL and response.status_code == 503:
|
if url == CLIMATE_START_URL and response.status_code == 503:
|
||||||
print("Car in use, cannot start pre climatization")
|
logging.warning("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
|
||||||
|
logging.debug("Suppressed 403 for extended-vehicle API")
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
print("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text)
|
logging.error("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)
|
||||||
|
|
||||||
|
|
||||||
@@ -213,24 +240,26 @@ def cached_request(url, method, vin, force_update=False):
|
|||||||
global cached_requests
|
global cached_requests
|
||||||
if not util.keys_exists(cached_requests, vin + "_" + url):
|
if not util.keys_exists(cached_requests, vin + "_" + url):
|
||||||
# No API Data cached, get fresh data from API
|
# No API Data cached, get fresh data from API
|
||||||
print("Starting " + method + " call against " + url)
|
logging.debug("Starting " + method + " call against " + url)
|
||||||
try:
|
try:
|
||||||
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))
|
logging.error("Error getting data: " + str(e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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["updateInterval"] \
|
if (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() \
|
||||||
or (force_update and (datetime.now(util.TZ) - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= 2):
|
>= 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
|
# Old Data in Cache, or force mode active, updating
|
||||||
print("Starting " + method + " call against " + url)
|
logging.debug("Starting " + method + " call against " + url)
|
||||||
try:
|
try:
|
||||||
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))
|
logging.error("Error getting data: " + str(e))
|
||||||
return None
|
return None
|
||||||
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
|
||||||
@@ -245,13 +274,14 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
if sensor_id == "battery_charge_level":
|
if sensor_id == "battery_charge_level":
|
||||||
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
|
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
|
||||||
elif sensor_id == "electric_range":
|
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":
|
elif sensor_id == "charging_system_status":
|
||||||
return charging_system_states[data["chargingSystemStatus"]["value"]] if util.keys_exists(data,
|
return charging_system_states[data["chargingSystemStatus"]["value"]] \
|
||||||
"chargingSystemStatus") else None
|
if util.keys_exists(data, "chargingSystemStatus") else None
|
||||||
elif sensor_id == "charging_connection_status":
|
elif sensor_id == "charging_connection_status":
|
||||||
return charging_connection_states[data["chargingConnectionStatus"]["value"]] if util.keys_exists(data,
|
return charging_connection_states[data["chargingConnectionStatus"]["value"]] \
|
||||||
"chargingConnectionStatus") else None
|
if util.keys_exists(data, "chargingConnectionStatus") else None
|
||||||
elif sensor_id == "estimated_charging_time":
|
elif sensor_id == "estimated_charging_time":
|
||||||
if util.keys_exists(data, "chargingSystemStatus"):
|
if util.keys_exists(data, "chargingSystemStatus"):
|
||||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
||||||
@@ -259,19 +289,18 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else ""
|
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else ""
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "estimated_charging_finish_time":
|
elif sensor_id == "estimated_charging_finish_time":
|
||||||
if util.keys_exists(data, "chargingSystemStatus"):
|
if util.keys_exists(data, "chargingSystemStatus"):
|
||||||
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
|
||||||
if charging_system_state == "Charging":
|
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)
|
else 0)
|
||||||
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time)
|
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time)
|
||||||
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
|
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
else:
|
|
||||||
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["carLocked"]["value"] if util.keys_exists(data, "carLocked") else None
|
||||||
@@ -283,7 +312,8 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
multiplier = 1
|
multiplier = 1
|
||||||
elif multiplier < 1:
|
elif multiplier < 1:
|
||||||
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":
|
elif sensor_id == "window_front_left":
|
||||||
return window_states[data["frontLeftWindowOpen"]["value"]] if util.keys_exists(data, "frontLeftWindowOpen") \
|
return window_states[data["frontLeftWindowOpen"]["value"]] if util.keys_exists(data, "frontLeftWindowOpen") \
|
||||||
else None
|
else None
|
||||||
@@ -299,7 +329,8 @@ def parse_api_data(data, sensor_id=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["frontLeftDoorOpen"]["value"]] if util.keys_exists(data, "frontLeftDoorOpen") else None
|
||||||
elif sensor_id == "door_front_right":
|
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":
|
elif sensor_id == "door_rear_left":
|
||||||
return door_states[data["rearLeftDoorOpen"]["value"]] if util.keys_exists(data, "rearLeftDoorOpen") else None
|
return door_states[data["rearLeftDoorOpen"]["value"]] if util.keys_exists(data, "rearLeftDoorOpen") else None
|
||||||
elif sensor_id == "door_rear_right":
|
elif sensor_id == "door_rear_right":
|
||||||
@@ -327,9 +358,6 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
fuel_amount = int(data["fuelAmount"]["value"])
|
fuel_amount = int(data["fuelAmount"]["value"])
|
||||||
if fuel_amount > 0:
|
if fuel_amount > 0:
|
||||||
return fuel_amount
|
return fuel_amount
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "average_fuel_consumption":
|
elif sensor_id == "average_fuel_consumption":
|
||||||
if util.keys_exists(data, "averageFuelConsumption"):
|
if util.keys_exists(data, "averageFuelConsumption"):
|
||||||
@@ -343,9 +371,6 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
elif multiplier < 1:
|
elif multiplier < 1:
|
||||||
multiplier = 1
|
multiplier = 1
|
||||||
return average_fuel_con * multiplier
|
return average_fuel_con * multiplier
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "average_speed":
|
elif sensor_id == "average_speed":
|
||||||
if util.keys_exists(data, "averageSpeed"):
|
if util.keys_exists(data, "averageSpeed"):
|
||||||
@@ -358,10 +383,7 @@ def parse_api_data(data, sensor_id=None):
|
|||||||
divider = 1
|
divider = 1
|
||||||
elif divider < 1:
|
elif divider < 1:
|
||||||
divider = 1
|
divider = 1
|
||||||
return average_speed / divider
|
return util.convert_metric_values(average_speed / divider)
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
elif sensor_id == "location":
|
elif sensor_id == "location":
|
||||||
coordinates = {}
|
coordinates = {}
|
||||||
@@ -372,5 +394,11 @@ 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":
|
||||||
|
if util.keys_exists(data, "distanceToEmpty"):
|
||||||
|
distance_to_empty = int(data["distanceToEmpty"]["value"])
|
||||||
|
if distance_to_empty > 0:
|
||||||
|
return util.convert_metric_values(data["distanceToEmpty"]["value"])
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user