Compare commits

...
25 Commits
Author SHA1 Message Date
Linus Dietz 4d8b5e5006 Optimize lock/unlock behaviour 2023-06-24 19:47:02 +02:00
Linus Dietz e8315ba0fd Merge remote-tracking branch 'origin/main' into main 2023-06-24 18:08:09 +02:00
Linus Dietz 4fca54b19f Add broker port to ha addon conf 2023-06-24 18:07:59 +02:00
Linus DietzandGitHub a411a1a59d Add broker Port 2023-06-24 18:06:15 +02:00
Linus Dietz a51ba25061 Add mqtt broker port option 2023-06-24 18:02:56 +02:00
Linus Dietz 1d37b3aca7 Revert "Bump version"
This reverts commit 1574fa96
2023-06-24 17:54:09 +02:00
Linus Dietz 1574fa968c Bump version 2023-06-24 17:53:12 +02:00
Linus Dietz aeea34b438 Bump version 2023-06-24 16:54:06 +02:00
Linus Dietz 1ccd140400 Reduce api calls in force update mode 2023-06-24 16:51:29 +02:00
Linus Dietz f4aef07e55 Optimize climate timer for multiple cars 2023-06-24 16:37:51 +02:00
Linus Dietz 8f6d664264 Fix wrong climate time duration 2023-06-24 16:35:44 +02:00
Linus DietzandGitHub 2b510d4e1b Add xc40 BEV 2022 2023-06-24 15:27:49 +02:00
Linus Dietz 5c0216465e Fix missing entities after HA restart 2023-06-24 11:44:50 +02:00
Linus Dietz d9ad484c69 Improve lock entity response time while locking/unlocking 2023-06-24 10:08:56 +02:00
Linus Dietz 28721384f7 Rework caching storage 2023-06-24 10:07:51 +02:00
Linus Dietz c7a883d781 Catch volvo api exceptions 2023-06-24 08:54:44 +02:00
Linus Dietz 023d8d2096 Rework response caching 2023-06-24 08:34:40 +02:00
Linus DietzandGitHub 64d0640282 Merge pull request #26 from jverdeyen/docker-compose-example
Added docker-compose.yml example
2023-06-23 14:09:04 +02:00
Joeri Verdeyen 39c748fe1a Added docker-compose.yml example 2023-06-23 13:56:30 +02:00
Linus Dietz 429c49c3f8 Merge remote-tracking branch 'origin/main' into main 2023-06-23 12:43:29 +02:00
Linus Dietz 68de908e05 Fix if multiplier is an empty string 2023-06-23 12:43:22 +02:00
Linus DietzandGitHub 5027b54de0 Update README.md 2023-06-23 12:40:31 +02:00
Linus DietzandGitHub 13cf419022 Update README.md 2023-06-23 12:36:39 +02:00
Linus DietzandGitHub d5a542b8cf Update README.md 2023-06-23 12:35:23 +02:00
Linus Dietz 7103b30da2 Make odometer multiplier configurable 2023-06-23 12:31:38 +02:00
11 changed files with 143 additions and 112 deletions
+5
View File
@@ -0,0 +1,5 @@
CONF_updateInterval=300
CONF_babelLocale='de'
CONF_mqtt='@json {"broker": "mqtt", "username": "", "password": ""}'
CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": ""}'
TZ='Europe/Berlin'
+2 -1
View File
@@ -1,3 +1,4 @@
.idea
venv
src/settings.dev.json
src/settings.dev.json
.env
+12 -3
View File
@@ -5,6 +5,7 @@ Maybe this component works also with other Volvo cars. Please try out the native
## Confirmed working with
- XC40 BEV (2023)
- XC40 BEV (2022)
- V60 T8 PHEV (2023)
- C40 PHEV (2023)
- C40 PHEV (2022)
@@ -21,7 +22,15 @@ Please let me know if your car works with this addon so I can expand the list!
- Sensor "Battery Charge Level"
- Sensor "Electric Range"
- Sensor "Charging System Status"
- Sensor "Charging Connection Status"
- Sensor "Estimated Charging Finish Time"
- Sensor "Door Lock Status"
- Sensor "Engine Status"
- Sensor "Window Lock Status"
- Sensor "Odometer"
- Sensor "Tire Status"
- Sensor "Fuel Status"
- Car Device Tracker
- Multiple cars
NOTE: Energy status currently available only for cars in the Europe / Middle East / Africa regions. [source](https://developer.volvocars.com/apis/energy/v1/overview/#availability)
@@ -32,7 +41,7 @@ NOTE: Energy status currently available only for cars in the Europe / Middle Eas
Just install this addon with the following command.
Please note to fill in your settings inside the environment variables.
`docker run -d --pull=always -e CONF_updateInterval=300 -e CONF_babelLocale='de' -e CONF_mqtt='@json {"broker": "", "username": "", "password": ""}' -e CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": ""}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest`
`docker run -d --pull=always -e CONF_updateInterval=300 -e CONF_babelLocale='de' -e CONF_mqtt='@json {"broker": "", "username": "", "password": "", "port": 1883}' -e CONF_volvoData='@json {"username": "", "password": "", "vin": "", "vccapikey": "", "odometerMultiplier": 1}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest`
<b>HA Add-On:</b><br>
@@ -44,8 +53,8 @@ Here is what every option means:
| -------------------- | :-------: | :----------: | --------------------------------------------------------------- |
| `CONF_updateInterval` | `int` | **required** | Update intervall in seconds. |
| `CONF_babelLocale` | `string` | **required** | Select your country from this [list](https://www.ibm.com/docs/en/radfws/9.7?topic=overview-locales-code-pages-supported). "Locale name" is the column you need! |
| `CONF_mqtt` | `json` | **required** | Broker = Mqtt Broker IP / Username and Passwort are optional! |
| `CONF_volvoData` | `json` | **required** | Username and password are REQUIRED. Car vin can be a single vin or a list of multiple vins like `["vin1", "vin2"]`. If no vin is provided, <b>ALL</b> of your vehicles will be used. Vccapi key is REQUIRED. Get your Vccapi key from [here](https://developer.volvocars.com/account/). |
| `CONF_mqtt` | `json` | **required** | Broker = Mqtt Broker IP / Username and Passwort are optional! Broker port can be changed. If no value is given, port 1883 will be used. |
| `CONF_volvoData` | `json` | **required** | Username and password are REQUIRED. Car vin can be a single vin or a list of multiple vins like `["vin1", "vin2"]`. If no vin is provided, <b>ALL</b> of your vehicles will be used. Vccapi key is REQUIRED. Get your Vccapi key from [here](https://developer.volvocars.com/account/). Odometer Multiplier is sometimes 10, sometimes 1. Try what's right for your car. If you leave it empty, the multiplier will be 1. |
| `CONF_debug` | `string` | | Debug option (true/false) - optional! |
| `TZ` | `string` | | Container timezone eg "Europe/Berlin" |
+24
View File
@@ -0,0 +1,24 @@
version: '3'
services:
app:
build: .
env_file:
- .env
# environment:
# CONF_updateInterval: 300
# CONF_babelLocale: 'de'
# CONF_mqtt: '@json {"broker": "mqtt", "username": "", "password": ""}'
# CONF_volvoData: '@json {"username": "", "password": "", "vin": "", "vccapikey": ""}'
# TZ: 'Europe/Berlin'
links:
- mqtt
depends_on:
- mqtt
mqtt:
container_name: mqtt
image: eclipse-mosquitto
expose:
- 1883
command: [ "/usr/sbin/mosquitto", "-c", "/mosquitto-no-auth.conf" ]
+5 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.3.1"
version: "1.4.2"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
@@ -11,6 +11,7 @@ options:
debug: false
mqtt:
broker: null
port: 1883
username: ""
password: ""
volvoData:
@@ -18,12 +19,14 @@ options:
password: null
vin: ""
vccapikey: null
odometerMultiplier: 1
schema:
updateInterval: int(10,)
babelLocale: str
debug: bool
mqtt:
broker: str
port: int(1,)
username: str?
password: str?
volvoData:
@@ -31,6 +34,7 @@ schema:
password: str
vin: str?
vccapikey: str
odometerMultiplier: int(1,)
arch:
- aarch64
- amd64
+1 -1
View File
@@ -1,6 +1,6 @@
from config import settings
VERSION = "v1.3.1"
VERSION = "v1.4.2"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles"
+1
View File
@@ -2,6 +2,7 @@ from volvo import authorize
from mqtt import update_loop, connect
from const import VERSION
if __name__ == '__main__':
print("Starting volvo2mqtt version " + VERSION)
authorize()
+27 -10
View File
@@ -2,6 +2,7 @@ import time
import paho.mqtt.client as mqtt
import json
import volvo
from util import keys_exists
from threading import Thread, Timer
from datetime import datetime
from babel.dates import format_datetime
@@ -14,14 +15,20 @@ mqtt_client: mqtt.Client
subscribed_topics = []
assumed_climate_state = {}
last_data_update = None
climate_timer: Timer
climate_timer = {}
def connect():
client = mqtt.Client("volvoAAOS2mqtt")
if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
client.username_pw_set(settings["mqtt"]["username"], settings["mqtt"]["password"])
client.connect(settings["mqtt"]["broker"])
port = 1883
if keys_exists(settings["mqtt"], "port"):
conf_port = settings["mqtt"]["port"]
if isinstance(conf_port, int):
if conf_port > 0:
port = settings["mqtt"]["port"]
client.connect(settings["mqtt"]["broker"], port)
client.loop_start()
client.on_message = on_message
client.on_disconnect = on_disconnect
@@ -56,24 +63,24 @@ def on_message(client, userdata, msg):
api_thread.start()
assumed_climate_state[vin] = "ON"
# Starting timer to disable climate after 30 mins
climate_timer = Timer(1 * 60, volvo.disable_climate, (vin, ))
climate_timer.start()
climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, ))
climate_timer[vin].start()
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
if climate_timer.is_alive():
climate_timer.cancel()
if climate_timer[vin].is_alive():
climate_timer[vin].cancel()
update_car_data()
elif "lock_status" in msg.topic:
if payload == "LOCK":
volvo.api_call(CAR_LOCK_URL, "POST", vin)
update_car_data()
update_car_data(True, {"entity_id": "lock_status", "state": "LOCKED"})
elif payload == "UNLOCK":
volvo.api_call(CAR_UNLOCK_URL, "POST", vin)
update_car_data()
update_car_data(True, {"entity_id": "lock_status", "state": "UNLOCKED"})
elif "update_data" in msg.topic:
if payload == "PRESS":
update_car_data(True)
@@ -88,7 +95,7 @@ def update_loop():
time.sleep(settings["updateInterval"])
def update_car_data(force_update=False):
def update_car_data(force_update=False, overwrite={}):
global last_data_update
last_data_update = format_datetime(datetime.now(), format="medium", locale=settings["babelLocale"])
for vin in volvo.vins:
@@ -96,12 +103,21 @@ def update_car_data(force_update=False):
if entity["domain"] == "button":
continue
ov_entity_id = ""
ov_state = ""
if bool(overwrite):
ov_entity_id = overwrite["entity_id"]
ov_state = overwrite["state"]
if entity["id"] == "climate_status":
state = assumed_climate_state[vin]
elif entity["id"] == "last_data_update":
state = last_data_update
else:
state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update)
if entity["id"] == ov_entity_id:
state = ov_state
else:
state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update)
if entity["domain"] == "device_tracker":
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes"
@@ -143,5 +159,6 @@ def create_ha_devices():
mqtt_client.publish(
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
json.dumps(config),
retain=True
)
time.sleep(2)
+1
View File
@@ -3,6 +3,7 @@
"babelLocale": "de",
"mqtt": {
"broker": "",
"port": 1883,
"username": "",
"password": ""
},
+2 -2
View File
@@ -10,7 +10,7 @@ configuration:
description: Enable Volvo API debug, normaly this can stay off
mqtt:
name: MQTT Broker settings
description: Broker is your mqtt broker IP eg "192.168.0.1". Mqtt username and password are optional.
description: Broker is your mqtt broker IP eg "192.168.0.1". Mqtt username and password are optional. Broker port can be changed. If no value is given, port 1883 will be used.
volvoData:
name: Volvo APP credentials
description: You have to enter your Volvo app credentials. The username is normally your email, and the password is the password you use for your Volvo app. The vin can stay empty. The Add-On will use any vin inside your Volvo account. VCCAPI key is required and comes from your Volvo developer account. Take a look at the GitHub repo for more information!
description: You have to enter your Volvo app credentials. The username is normally your email, and the password is the password you use for your Volvo app. The vin can stay empty. The Add-On will use any vin inside your Volvo account. VCCAPI key is required and comes from your Volvo developer account. Odometer multiplier is sometimes 10, sometimes 1. Leave it empty if you don't know what's right. Take a look at the GitHub repo for more information!
+63 -94
View File
@@ -18,15 +18,8 @@ session.headers = {
token_expires_at: datetime
refresh_token = None
vins = []
recharge_cached_api_response = {}
recharge_api_last_update = {}
window_cached_api_response = {}
window_api_last_update = {}
door_cached_api_response = {}
door_api_last_update = {}
tyre_cached_api_response = {}
tyre_api_last_update = {}
supported_endpoints = {}
cached_requests = {}
def authorize():
@@ -177,22 +170,42 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
if url == RECHARGE_STATE_URL:
# Minimize API calls for recharge state
response = pull_recharge_api(url, method, vin, force_update)
response = cached_request(url, method, vin, force_update)
if response is None:
# Exception caught while getting data from volvo api, doing nothing
return None
elif url == WINDOWS_STATE_URL:
# Minimize API calls for window state
response = pull_window_api(url, method, vin, force_update)
response = cached_request(url, method, vin, force_update)
if response is None:
# Exception caught while getting data from volvo api, doing nothing
return ""
elif url == LOCK_STATE_URL:
# Minimize API calls for door state
response = pull_door_api(url, method, vin, force_update)
response = cached_request(url, method, vin, force_update)
if response is None:
# Exception caught while getting data from volvo api, doing nothing
return ""
elif url == TYRE_STATE_URL:
# Minimize API calls for tyre state
response = pull_tyre_api(url, method, vin, force_update)
response = cached_request(url, method, vin, force_update)
if response is None:
# Exception caught while getting data from volvo api, doing nothing
return ""
elif method == "GET":
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
try:
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return ""
elif method == "POST":
print("Starting " + method + " call against " + url)
response = session.post(url.format(vin), timeout=20)
try:
response = session.post(url.format(vin), timeout=20)
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return ""
else:
print("Unkown method posted: " + method + ". Returning nothing")
return ""
@@ -213,87 +226,34 @@ def api_call(url, method, vin, sensor_id=None, force_update=False):
return parse_api_data(data, sensor_id)
def pull_tyre_api(url, method, vin, force_update=False):
global tyre_cached_api_response, tyre_api_last_update
if not vin in tyre_cached_api_response or force_update:
# No API Data for vin cached, get fresh data from API
def cached_request(url, method, vin, force_update=False):
global cached_requests
if not keys_exists(cached_requests, vin + "_" + url):
# No API Data cached, get fresh data from API
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
tyre_cached_api_response[vin] = response
tyre_api_last_update[vin] = datetime.now()
else:
if (datetime.now() - tyre_api_last_update[vin]).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updating
print("Starting " + method + " call against " + url)
try:
response = session.get(url.format(vin), timeout=15)
tyre_cached_api_response[vin] = response
tyre_api_last_update[vin] = datetime.now()
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return None
data = {"response": response, "last_update": datetime.now()}
cached_requests[vin + "_" + url] = data
else:
if (datetime.now() - cached_requests[vin + "_" + url]["last_update"]).total_seconds() >= settings["updateInterval"] \
or (force_update and (datetime.now() - 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:
response = session.get(url.format(vin), timeout=15)
except requests.exceptions.RequestException as e:
print("Error getting data: " + str(e))
return None
data = {"response": response, "last_update": datetime.now()}
cached_requests[vin + "_" + url] = data
else:
# Data is up do date, returning cached data
response = tyre_cached_api_response[vin]
return response
def pull_door_api(url, method, vin, force_update=False):
global door_cached_api_response, door_api_last_update
if not vin in door_cached_api_response or force_update:
# No API Data for vin cached, get fresh data from API
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
door_cached_api_response[vin] = response
door_api_last_update[vin] = datetime.now()
else:
if (datetime.now() - door_api_last_update[vin]).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updating
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
door_cached_api_response[vin] = response
door_api_last_update[vin] = datetime.now()
else:
# Data is up do date, returning cached data
response = door_cached_api_response[vin]
return response
def pull_window_api(url, method, vin, force_update=False):
global window_cached_api_response, window_api_last_update
if not vin in window_cached_api_response or force_update:
# No API Data for vin cached, get fresh data from API
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
window_cached_api_response[vin] = response
window_api_last_update[vin] = datetime.now()
else:
if (datetime.now() - window_api_last_update[vin]).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updating
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
window_cached_api_response[vin] = response
window_api_last_update[vin] = datetime.now()
else:
# Data is up do date, returning cached data
response = window_cached_api_response[vin]
return response
def pull_recharge_api(url, method, vin, force_update=False):
global recharge_cached_api_response, recharge_api_last_update
if not vin in recharge_cached_api_response or force_update:
# No API Data for vin cached, get fresh data from API
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
recharge_cached_api_response[vin] = response
recharge_api_last_update[vin] = datetime.now()
else:
if (datetime.now() - recharge_api_last_update[vin]).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updating
print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15)
recharge_cached_api_response[vin] = response
recharge_api_last_update[vin] = datetime.now()
else:
# Data is up do date, returning cached data
response = recharge_cached_api_response[vin]
response = cached_requests[vin + "_" + url]["response"]
return response
@@ -304,9 +264,11 @@ def parse_api_data(data, sensor_id=None):
elif sensor_id == "electric_range":
return data["electricRange"]["value"] if keys_exists(data, "electricRange") else None
elif sensor_id == "charging_system_status":
return charging_system_states[data["chargingSystemStatus"]["value"]] if keys_exists(data, "chargingSystemStatus") else None
return charging_system_states[data["chargingSystemStatus"]["value"]] if keys_exists(data,
"chargingSystemStatus") else None
elif sensor_id == "charging_connection_status":
return charging_connection_states[data["chargingConnectionStatus"]["value"]] if keys_exists(data, "chargingConnectionStatus") else None
return charging_connection_states[data["chargingConnectionStatus"]["value"]] if keys_exists(data,
"chargingConnectionStatus") else None
elif sensor_id == "estimated_charging_time":
if keys_exists(data, "chargingSystemStatus"):
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]]
@@ -331,7 +293,14 @@ def parse_api_data(data, sensor_id=None):
elif sensor_id == "lock_status":
return data["carLocked"]["value"] if keys_exists(data, "carLocked") else None
elif sensor_id == "odometer":
return int(data["odometer"]["value"]) * 10 if keys_exists(data, "odometer") else None
multiplier = 1
if keys_exists(settings["volvoData"], "odometerMultiplier"):
multiplier = settings["volvoData"]["odometerMultiplier"]
if isinstance(multiplier, str):
multiplier = 1
elif multiplier < 1:
multiplier = 1
return int(data["odometer"]["value"]) * multiplier if keys_exists(data, "odometer") else None
elif sensor_id == "window_front_left":
return data["frontLeftWindowOpen"]["value"] if keys_exists(data, "frontLeftWindowOpen") else None
elif sensor_id == "window_front_right":