Compare commits

...
Author SHA1 Message Date
Linus Dietz 016a0193e2 Rename engine runtime sensor 2023-11-22 10:07:49 +01:00
Linus Dietz c3a798e397 Bump version 2023-11-22 08:51:31 +01:00
Linus Dietz 89417696bd Add engine start and stop logic 2023-11-22 08:49:36 +01:00
Linus Dietz d55dc25647 Add engine runtime number and engine start switch entities 2023-11-22 08:14:20 +01:00
Linus Dietz 1d193270a4 Add available commands check 2023-11-22 07:37:43 +01:00
5 changed files with 102 additions and 20 deletions
+6
View File
@@ -1,3 +1,9 @@
## v1.9.0
### 🚀 Features:
- Add check for supported commands
- Add Engine start/stop and Engine runtime minutes for supported cars
## v1.8.19
### 🐛 Bug Fixes:
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.8.19"
version: "1.9.0"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
+10 -5
View File
@@ -1,6 +1,6 @@
from config import settings
VERSION = "v1.8.19"
VERSION = "v1.9.0"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles"
@@ -20,7 +20,10 @@ FUEL_BATTERY_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicle
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"
VEHICLE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/diagnostics"
API_BACKEND_STATUS = "https://oip-dev-bff.euwest1.production.volvo.care/api/v1/backend-status"
API_BACKEND_STATUS_URL = "https://oip-dev-bff.euwest1.production.volvo.care/api/v1/backend-status"
SUPPORTED_COMMANDS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands"
ENGINE_START_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/engine-start"
ENGINE_STOP_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/engine-stop"
units = {
"en_GB": {
@@ -99,15 +102,17 @@ supported_entities = [
{"name": "Engine Hood", "domain": "binary_sensor", "device_class": "door", "id": "engine_hood", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Tank Lid", "domain": "binary_sensor", "device_class": "door", "id": "tank_lid", "icon": "car-door-lock", "url": LOCK_STATE_URL},
{"name": "Sunroof", "domain": "binary_sensor", "device_class": "door", "id": "sunroof", "icon": "car-door-lock", "url": WINDOWS_STATE_URL},
{"name": "Air Conditioning", "domain": "switch", "id": "climate_status", "icon": "air-conditioner"},
{"name": "Lock state", "domain": "lock", "id": "lock_status", "icon": "lock", "url": LOCK_STATE_URL},
{"name": "Air Conditioning", "domain": "switch", "id": "climate_status", "icon": "air-conditioner", "commands": ["CLIMATIZATION_START", "CLIMATIZATION_STOP"]},
{"name": "Engine State", "domain": "switch", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL ,"commands": ["ENGINE_START", "ENGINE_STOP"]},
{"name": "Leave in", "domain": "number", "id": "engine_runtime", "unit": "minutes", "icon": "timer-sand", "commands": ["ENGINE_START", "ENGINE_STOP"], "min": 1, "max": 15, "mode": "box"},
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
{"name": "Lock state", "domain": "lock", "id": "lock_status", "icon": "lock", "url": LOCK_STATE_URL, "commands": ["LOCK", "UNLOCK"]},
{"name": "Force Update Data", "domain": "button", "id": "update_data", "icon": "update", "url": ""},
{"name": "Location", "domain": "device_tracker", "id": "location", "icon": "map-marker-radius", "url": LOCATION_STATE_URL},
{"name": "Tire Front Left", "domain": "sensor", "id": "tyre_front_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Front Right", "domain": "sensor", "id": "tyre_front_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Left", "domain": "sensor", "id": "tyre_rear_left", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "icon": "car-tire-alert", "url": TYRE_STATE_URL},
{"name": "Engine State", "domain": "binary_sensor", "device_class": "running", "id": "engine_state", "icon": "engine", "url": ENGINE_STATE_URL},
{"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": "liters", "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": "liters", "icon": "fuel", "url": STATISTICS_URL},
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": "kwh", "icon": "car-electric", "url": STATISTICS_URL},
+36 -3
View File
@@ -10,7 +10,7 @@ from datetime import datetime
from babel.dates import format_datetime
from config import settings
from const import CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \
CAR_UNLOCK_URL, availability_topic, icon_states, old_entity_ids
CAR_UNLOCK_URL, ENGINE_START_URL, ENGINE_STOP_URL, availability_topic, icon_states, old_entity_ids
mqtt_client: mqtt.Client
subscribed_topics = []
@@ -20,6 +20,7 @@ climate_timer = {}
engine_status = {}
devices = {}
active_schedules = {}
engine_runtime = 1
def connect():
@@ -114,6 +115,15 @@ def on_message(client, userdata, msg):
elif "update_data" in msg.topic:
if payload == "PRESS":
update_car_data(True)
elif "engine_runtime" in msg.topic:
global engine_runtime
engine_runtime = int(payload)
logging.debug("Updated engine runtime to " + str(engine_runtime) + " minutes!")
elif "engine_state" in msg.topic:
if payload == "ON":
start_engine(vin)
elif payload == "OFF":
stop_engine(vin)
elif "schedule" in msg.topic:
try:
d = json.loads(payload)
@@ -211,6 +221,23 @@ def start_climate(vin):
update_car_data()
def start_engine(vin):
body = {"runtimeMinutes": engine_runtime}
# Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(ENGINE_START_URL, "POST", vin, None, None, None, body)).start()
# Set and update switch status
update_car_data(False, {"entity_id": "engine_state", "vin": vin, "state": "ON"})
def stop_engine(vin):
# Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(ENGINE_STOP_URL, "POST", vin)).start()
# Set and update switch status
update_car_data(False, {"entity_id": "engine_state", "vin": vin, "state": "OFF"})
def update_loop():
create_ha_devices()
while True:
@@ -226,7 +253,7 @@ def update_car_data(force_update=False, overwrite={}):
last_data_update = format_datetime(datetime.now(util.TZ), format="medium", locale=settings["babelLocale"])
for vin in volvo.vins:
for entity in volvo.supported_endpoints[vin]:
if entity["domain"] == "button":
if entity["domain"] in ["button", "number"]:
continue
ov_entity_id = ""
@@ -344,11 +371,17 @@ def create_ha_devices():
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"]:
elif entity.get("domain") in ["switch", "lock", "button", "number"]:
command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command"
config["command_topic"] = command_topic
subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic)
if entity["domain"] == "number":
config["min"] = entity["min"]
config["max"] = entity["max"]
config["mode"] = entity["mode"]
elif entity.get("domain") == "image":
config["url_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/image_url"
+49 -11
View File
@@ -1,3 +1,4 @@
import json
import logging
import requests
import mqtt
@@ -12,7 +13,8 @@ from json import JSONDecodeError
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, FUEL_BATTERY_STATE_URL, \
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, API_BACKEND_STATUS, engine_states
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, API_BACKEND_STATUS_URL, SUPPORTED_COMMANDS_URL, \
ENGINE_STATE_URL, engine_states
session = requests.Session()
session.headers = {
@@ -27,6 +29,7 @@ vins = []
supported_endpoints = {}
cached_requests = {}
vcc_api_keys = []
supported_commands = []
backend_status = ""
@@ -54,6 +57,7 @@ def authorize():
get_vcc_api_keys()
get_vehicles()
get_supported_commands()
check_supported_endpoints()
Thread(target=backend_status_loop).start()
else:
@@ -90,6 +94,19 @@ def refresh_auth():
refresh_token = data["refresh_token"]
def get_supported_commands():
global supported_commands
for vin in vins:
commands = session.get(SUPPORTED_COMMANDS_URL.format(vin))
data = commands.json()
if commands.status_code == 200:
for command in data["data"]:
supported_commands.append(command["command"])
else:
logging.error("Error getting supported commands: " + str(commands.status_code) + ". " + str(data["error"].get("message")))
logging.debug("Supported commands: " + str(supported_commands))
def get_vehicles():
global vins
if not settings.volvoData["vin"]:
@@ -102,9 +119,8 @@ def get_vehicles():
else:
raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
else:
error = vehicles.json()
raise Exception(
"Error getting vehicles: " + str(vehicles.status_code) + ". " + str(error["error"].get("message")))
"Error getting vehicles: " + str(vehicles.status_code) + ". " + str(data["error"].get("message")))
else:
if isinstance(settings.volvoData["vin"], list):
# If setting is a list, copy
@@ -248,16 +264,38 @@ def check_supported_endpoints():
# If battery charge level could be found in recharge-api, skip the second battery charge sensor
continue
if entity.get('url'):
state = api_call(entity["url"], "GET", vin, entity["id"])
if entity["id"] == "engine_state" and entity["url"] == ENGINE_STATE_URL \
and any("engine_state" in d["id"] for d in supported_endpoints[vin]):
# If engine state is supported with commands, skip engine state sensor, as switch
# represents the actual engine state
continue
if entity["domain"] in ["switch", "lock", "number"]:
state = check_supported_command(entity["commands"])
else:
state = ""
if entity.get('url'):
state = api_call(entity["url"], "GET", vin, entity["id"])
else:
state = ""
if state is not None:
logging.info("Success! " + entity["name"] + " is supported by your vehicle.")
logging.info("Success! " + entity["name"] + " (" + entity["domain"] + ") is supported by your vehicle.")
supported_endpoints[vin].append(entity)
else:
logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.")
logging.info("Failed, " + entity["name"] + " (" + entity["domain"] + ") is unfortunately not supported by your vehicle.")
def check_supported_command(entity_commands):
commands_supported = True
for entity_command in entity_commands:
if entity_command not in supported_commands:
commands_supported = False
break
if commands_supported:
return ""
else:
return None
def initialize_scheduler(vins):
@@ -325,7 +363,7 @@ def backend_status_loop():
def get_backend_status():
global backend_status
response = session.get(API_BACKEND_STATUS, timeout=15)
response = session.get(API_BACKEND_STATUS_URL, timeout=15)
try:
data = response.json()
if util.keys_exists(data, "message"):
@@ -341,7 +379,7 @@ def get_backend_status():
return backend_status
def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=False):
def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=False, body=None):
if datetime.now(util.TZ) >= token_expires_at:
refresh_auth()
@@ -362,7 +400,7 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
elif method == "POST":
logging.debug("Starting " + method + " call against " + url)
try:
response = session.post(url.format(vin), timeout=20)
response = session.post(url.format(vin), data=json.dumps(body), timeout=20)
except requests.exceptions.RequestException as e:
logging.error("Error getting data: " + str(e))
return None