Compare commits

...
25 Commits
Author SHA1 Message Date
Linus Dietz 71f03e505b Fix recharge caching while using multiple cars 2023-06-20 08:56:45 +02:00
Linus DietzandGitHub 2b6a2a4f4c Update README.md 2023-06-20 08:35:29 +02:00
Linus DietzandGitHub 38cca04473 Add working with section 2023-06-20 08:32:35 +02:00
Linus Dietz fbb98481f4 Undo changes // Bump version 2023-06-20 07:52:08 +02:00
Linus Dietz dad5f9aaaa Remove unused requirements // Bump version 2023-06-20 07:35:30 +02:00
Linus DietzandGitHub c404356ee0 Run workflow on release update 2023-06-20 07:28:37 +02:00
Linus Dietz 0516c29563 Fix type error #3 // Add addon version 2023-06-20 07:12:40 +02:00
Linus DietzandGitHub 9b3f9b4c0b Add pull always to docker run 2023-06-19 19:58:55 +02:00
Linus DietzandGitHub 65d0214664 Fix typo 2023-06-19 19:53:29 +02:00
Linus DietzandGitHub 39f457d01a Add info for multiple vins 2023-06-19 19:49:17 +02:00
Linus Dietz 21cb2999f7 Add support for multiple vehicles 2023-06-19 19:46:06 +02:00
Linus DietzandGitHub 298e8b503a Update README.md 2023-06-19 12:56:52 +02:00
Linus DietzandGitHub 2d50129ec4 Update README.md 2023-06-19 12:49:30 +02:00
Linus DietzandGitHub 5ef1aa528b Update README.md 2023-06-19 12:41:30 +02:00
Linus Dietz 3440dadeab Move vcc api key to settings 2023-06-19 12:39:20 +02:00
Linus DietzandGitHub 1dc38e6925 change image tagging 2023-06-19 12:33:53 +02:00
Linus DietzandGitHub 81b0bb9b85 remove unused step 2023-06-19 12:25:06 +02:00
Linus DietzandGitHub 97fb95799a add armv6 and v8 2023-06-19 12:21:51 +02:00
Linus DietzandGitHub ab73f9afb2 Update build_image.yml 2023-06-19 12:17:48 +02:00
Linus DietzandGitHub 285f9dc79b Update build_image.yml 2023-06-19 12:00:00 +02:00
Linus DietzandGitHub 6dad197ed6 Update build_image.yml 2023-06-19 11:53:53 +02:00
Linus DietzandGitHub 1ed6035a54 Update build_image.yml 2023-06-19 11:52:25 +02:00
Linus DietzandGitHub 666eda99d0 Update build_image.yml 2023-06-19 11:50:01 +02:00
Linus DietzandGitHub 02e64e5555 Add manual action dispatch 2023-06-19 11:48:18 +02:00
Linus DietzandGitHub c298b8972a Change build behaviour 2023-06-19 11:41:46 +02:00
8 changed files with 179 additions and 146 deletions
+14 -6
View File
@@ -11,7 +11,8 @@ name: Create and publish a Docker image
on: on:
release: release:
types: [created] types: [created, edited]
workflow_dispatch:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -29,8 +30,6 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry - name: Log in to the Container registry
@@ -44,10 +43,19 @@ jobs:
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image tags: |
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push
uses: docker/build-push-action@v2
with: with:
context: . context: .
push: true platforms: linux/amd64,linux/arm/v7,linux/arm/v6,linux/arm/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+11 -2
View File
@@ -3,12 +3,17 @@
This component establishes a connection between the newer AAOS Volvo cars and Home Assistant via MQTT.<br> This component establishes a connection between the newer AAOS Volvo cars and Home Assistant via MQTT.<br>
Maybe this component works also with other Volvo cars. Please try out the native Volvo [integration](https://www.home-assistant.io/integrations/volvooncall/) before using this component! If the native component doesn't work for your car, try this mqtt bridge. Maybe this component works also with other Volvo cars. Please try out the native Volvo [integration](https://www.home-assistant.io/integrations/volvooncall/) before using this component! If the native component doesn't work for your car, try this mqtt bridge.
## Confirmed working with
- Volvo XC40 BEV (2023)
Please let me know if your car works with this addon so I can expand the list!
## Setup ## Setup
Just install this addon with the following command. Just install this addon with the following command.
Please note to fill in your settings inside the environment variables. Please note to fill in your settings inside the environment variables.
`docker run -e CONF_updateInterval=300 -e CONF_babelLocale='de' -e CONF_mqtt='@json {"broker": "", "username": "", "password": ""}' -e CONF_volvoData='@json {"username": "", "password": "", "vin": ""}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:main` `docker run --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`
Here is what every option means: Here is what every option means:
@@ -17,5 +22,9 @@ Here is what every option means:
| `updateInterval` | `int` | **required** | Update intervall in seconds. | | `updateInterval` | `int` | **required** | Update intervall in seconds. |
| `babelLocale` | `string` | **required** | Locale for date Format | | `babelLocale` | `string` | **required** | Locale for date Format |
| `mqtt` | `json` | **required** | Broker = Mqtt Broker IP / Username and Passwort are optional! | | `mqtt` | `json` | **required** | Broker = Mqtt Broker IP / Username and Passwort are optional! |
| `volvoData` | `json` | **required** | Username and password are required. Car vin is optional. If no vin is provided, the system will always use the first vehicle in API. | | `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 your api key from [here](https://developer.volvocars.com/account/). |
| `TZ` | `string` | | Container timezone eg "Europe/Berlin" | | `TZ` | `string` | | Container timezone eg "Europe/Berlin" |
If you like my work:
<a href="https://www.buymeacoffee.com/dielee" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
+2
View File
@@ -1,3 +1,5 @@
VERSION = "v1.0.5"
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"
VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}" VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v1/vehicles/{0}"
+2
View File
@@ -1,7 +1,9 @@
from volvo import authorize from volvo import authorize
from mqtt import update_loop, connect from mqtt import update_loop, connect
from const import VERSION
if __name__ == '__main__': if __name__ == '__main__':
print("Starting volvo2mqtt version " + VERSION)
authorize() authorize()
connect() connect()
update_loop() update_loop()
+32 -29
View File
@@ -74,16 +74,18 @@ def update_loop():
while True: while True:
print("Sending mqtt update...") print("Sending mqtt update...")
update_car_data() update_car_data()
print("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.")
time.sleep(settings["updateInterval"]) time.sleep(settings["updateInterval"])
def update_car_data(): def update_car_data():
global last_data_update global last_data_update
last_data_update = format_datetime(datetime.now(), format="medium", locale=settings["babelLocale"]) last_data_update = format_datetime(datetime.now(), format="medium", locale=settings["babelLocale"])
for vin in volvo.vins:
for lock in supported_locks: for lock in supported_locks:
state = volvo.api_call(lock["url"], "GET", lock["id"]) state = volvo.api_call(lock["url"], "GET", vin, lock["id"])
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/lock/{volvo.vin}_{lock['id']}/state", f"homeassistant/lock/{vin}_{lock['id']}/state",
state state
) )
@@ -94,7 +96,7 @@ def update_car_data():
state = "OFF" state = "OFF"
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/switch/{volvo.vin}_{switch['id']}/state", f"homeassistant/switch/{vin}_{switch['id']}/state",
state state
) )
@@ -102,85 +104,86 @@ def update_car_data():
if sensor["id"] == "last_data_update": if sensor["id"] == "last_data_update":
state = last_data_update state = last_data_update
else: else:
state = volvo.api_call(sensor["url"], "GET", sensor["id"]) state = volvo.api_call(sensor["url"], "GET", vin, sensor["id"])
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/sensor/{volvo.vin}_{sensor['id']}/state", f"homeassistant/sensor/{vin}_{sensor['id']}/state",
state state
) )
def create_ha_devices(): def create_ha_devices():
car_details = volvo.api_call(VEHICLE_DETAILS_URL, "GET") for vin in volvo.vins:
car_details = volvo.api_call(VEHICLE_DETAILS_URL, "GET", vin)
for button in supported_buttons: for button in supported_buttons:
command_topic = f"homeassistant/button/{volvo.vin}_{button['id']}/command" command_topic = f"homeassistant/button/{vin}_{button['id']}/command"
config = { config = {
"name": button['name'], "name": button['name'],
"object_id": button['id'], "object_id": button['id'],
"schema": "state", "schema": "state",
"icon": f"mdi:{button['icon']}", "icon": f"mdi:{button['icon']}",
"state_topic": f"homeassistant/button/{volvo.vin}_{button['id']}/state", "state_topic": f"homeassistant/button/{vin}_{button['id']}/state",
"command_topic": command_topic, "command_topic": command_topic,
"device": { "device": {
"identifiers": ["volvoAAOS2mqtt"], "identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo", "manufacturer": "Volvo",
"model": car_details['descriptions']['model'], "model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {volvo.vin}", "name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
}, },
"unique_id": f"volvoAAOS2mqtt_{volvo.vin}_{button['id']}", "unique_id": f"volvoAAOS2mqtt_{vin}_{button['id']}",
} }
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/button/volvoAAOS2mqtt/{volvo.vin}_{button['id']}/config", f"homeassistant/button/volvoAAOS2mqtt/{vin}_{button['id']}/config",
json.dumps(config), json.dumps(config),
) )
subscribed_topics.append(command_topic) subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic) mqtt_client.subscribe(command_topic)
for lock in supported_locks: for lock in supported_locks:
command_topic = f"homeassistant/lock/{volvo.vin}_{lock['id']}/command" command_topic = f"homeassistant/lock/{vin}_{lock['id']}/command"
config = { config = {
"name": lock['name'], "name": lock['name'],
"object_id": lock['id'], "object_id": lock['id'],
"schema": "state", "schema": "state",
"icon": f"mdi:{lock['icon']}", "icon": f"mdi:{lock['icon']}",
"state_topic": f"homeassistant/lock/{volvo.vin}_{lock['id']}/state", "state_topic": f"homeassistant/lock/{vin}_{lock['id']}/state",
"command_topic": command_topic, "command_topic": command_topic,
"optimistic": False, "optimistic": False,
"device": { "device": {
"identifiers": ["volvoAAOS2mqtt"], "identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo", "manufacturer": "Volvo",
"model": car_details['descriptions']['model'], "model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {volvo.vin}", "name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
}, },
"unique_id": f"volvoAAOS2mqtt_{volvo.vin}_{lock['id']}", "unique_id": f"volvoAAOS2mqtt_{vin}_{lock['id']}",
} }
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/lock/volvoAAOS2mqtt/{volvo.vin}_{lock['id']}/config", f"homeassistant/lock/volvoAAOS2mqtt/{vin}_{lock['id']}/config",
json.dumps(config), json.dumps(config),
) )
subscribed_topics.append(command_topic) subscribed_topics.append(command_topic)
mqtt_client.subscribe(command_topic) mqtt_client.subscribe(command_topic)
for switch in supported_switches: for switch in supported_switches:
command_topic = f"homeassistant/switch/{volvo.vin}_{switch['id']}/command" command_topic = f"homeassistant/switch/{vin}_{switch['id']}/command"
config = { config = {
"name": switch['name'], "name": switch['name'],
"object_id": switch['id'], "object_id": switch['id'],
"schema": "state", "schema": "state",
"icon": f"mdi:{switch['icon']}", "icon": f"mdi:{switch['icon']}",
"state_topic": f"homeassistant/switch/{volvo.vin}_{switch['id']}/state", "state_topic": f"homeassistant/switch/{vin}_{switch['id']}/state",
"command_topic": command_topic, "command_topic": command_topic,
"optimistic": False, "optimistic": False,
"device": { "device": {
"identifiers": ["volvoAAOS2mqtt"], "identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo", "manufacturer": "Volvo",
"model": car_details['descriptions']['model'], "model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {volvo.vin}", "name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
}, },
"unique_id": f"volvoAAOS2mqtt_{volvo.vin}_{switch['id']}", "unique_id": f"volvoAAOS2mqtt_{vin}_{switch['id']}",
} }
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/switch/volvoAAOS2mqtt/{volvo.vin}_{switch['id']}/config", f"homeassistant/switch/volvoAAOS2mqtt/{vin}_{switch['id']}/config",
json.dumps(config), json.dumps(config),
) )
subscribed_topics.append(command_topic) subscribed_topics.append(command_topic)
@@ -192,20 +195,20 @@ def create_ha_devices():
"object_id": sensor['id'], "object_id": sensor['id'],
"schema": "state", "schema": "state",
"icon": f"mdi:{sensor['icon']}", "icon": f"mdi:{sensor['icon']}",
"state_topic": f"homeassistant/sensor/{volvo.vin}_{sensor['id']}/state", "state_topic": f"homeassistant/sensor/{vin}_{sensor['id']}/state",
"device": { "device": {
"identifiers": ["volvoAAOS2mqtt"], "identifiers": [f"volvoAAOS2mqtt_{vin}"],
"manufacturer": "Volvo", "manufacturer": "Volvo",
"model": car_details['descriptions']['model'], "model": car_details['descriptions']['model'],
"name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {volvo.vin}", "name": f"{car_details['descriptions']['model']} ({car_details['modelYear']}) - {vin}",
}, },
"unique_id": f"volvoAAOS2mqtt_{volvo.vin}_{sensor['id']}", "unique_id": f"volvoAAOS2mqtt_{vin}_{sensor['id']}",
} }
if "unit" in sensor: if "unit" in sensor:
config["unit_of_measurement"] = sensor["unit"] config["unit_of_measurement"] = sensor["unit"]
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/sensor/volvoAAOS2mqtt/{volvo.vin}_{sensor['id']}/config", f"homeassistant/sensor/volvoAAOS2mqtt/{vin}_{sensor['id']}/config",
json.dumps(config), json.dumps(config),
) )
time.sleep(2) time.sleep(2)
-1
View File
@@ -1,5 +1,4 @@
requests~=2.29.0 requests~=2.29.0
dynaconf~=3.1.12 dynaconf~=3.1.12
paho-mqtt~=1.6.1 paho-mqtt~=1.6.1
pytz~=2023.3
Babel~=2.12.1 Babel~=2.12.1
+2 -1
View File
@@ -9,6 +9,7 @@
"volvoData": { "volvoData": {
"username": "", "username": "",
"password": "", "password": "",
"vin": "" "vin": "",
"vccapikey": ""
} }
} }
+25 -16
View File
@@ -8,14 +8,14 @@ from const import charging_system_states, CLIMATE_START_URL, \
session = requests.Session() session = requests.Session()
session.headers = { session.headers = {
"vcc-api-key": "f0d0419bf51d420c8efb21cf9a127227", "vcc-api-key": settings["volvoData"]["vccapikey"],
"content-type": "application/json", "content-type": "application/json",
"accept": "*/*" "accept": "*/*"
} }
token_expires_at: datetime token_expires_at: datetime
refresh_token = None refresh_token = None
vin = "" vins = []
recharge_response = {} recharge_response = {}
recharge_last_update = None recharge_last_update = None
@@ -42,7 +42,7 @@ def authorize():
token_expires_at = datetime.now() + timedelta(seconds=(data["expires_in"] - 30)) token_expires_at = datetime.now() + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"] refresh_token = data["refresh_token"]
get_vehicle() get_vehicles()
else: else:
message = auth.json() message = auth.json()
raise Exception(message["error_description"]) raise Exception(message["error_description"])
@@ -71,25 +71,31 @@ def refresh_auth():
refresh_token = data["refresh_token"] refresh_token = data["refresh_token"]
def get_vehicle(): def get_vehicles():
global vin global vins
if not settings.volvoData["vin"]: if not settings.volvoData["vin"]:
vehicles = session.get(VEHICLES_URL) vehicles = session.get(VEHICLES_URL)
if vehicles.status_code == 200: if vehicles.status_code == 200:
data = vehicles.json() data = vehicles.json()
if len(data["data"]) > 0: if len(data["data"]) > 0:
vin = data["data"][0]["vin"] for vehicle in data["data"]:
vins.append(vehicle["vin"])
else: else:
print("No vehicle in account " + settings.volvoData["username"] + " found.") raise Exception("No vehicle in account " + settings.volvoData["username"] + " found.")
else: else:
print("Error getting Vehicles " + str(vehicles.status_code)) raise Exception("Error getting vehicles: " + str(vehicles.status_code))
else: else:
vin = settings.volvoData["vin"] if isinstance(settings.volvoData["vin"], list):
# If setting is a list, copy
vins = settings.volvoData["vin"]
else:
# If setting is a string, append to list
vins.append(settings.volvoData["vin"])
if not vin: if len(vins) == 0:
raise Exception("No vehicle found, exiting application!") raise Exception("No vehicle found, exiting application!")
else: else:
print("Vin: " + vin + " found!") print("Vin: " + str(vins) + " found!")
def disable_climate(): def disable_climate():
@@ -97,30 +103,30 @@ def disable_climate():
mqtt.update_car_data() mqtt.update_car_data()
def api_call(url, method, sensor_id=None): def api_call(url, method, vin, sensor_id=None):
global token_expires_at global token_expires_at
if datetime.now() >= token_expires_at: if datetime.now() >= token_expires_at:
refresh_auth() refresh_auth()
global vin, recharge_response, recharge_last_update global recharge_response, recharge_last_update
if url == RECHARGE_STATUS_URL: if url == RECHARGE_STATUS_URL:
# Minimize API calls for recharge API # Minimize API calls for recharge API
if not bool(recharge_response): if not bool(recharge_response):
# No API Data cached, get fresh data from API # No API Data cached, get fresh data from API
print("Starting " + method + " call against " + url) print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15) response = session.get(url.format(vin), timeout=15)
recharge_response = response recharge_response[vin] = response
recharge_last_update = datetime.now() recharge_last_update = datetime.now()
else: else:
if (datetime.now() - recharge_last_update).total_seconds() >= settings["updateInterval"]: if (datetime.now() - recharge_last_update).total_seconds() >= settings["updateInterval"]:
# Old Data in Cache, updateing # Old Data in Cache, updateing
print("Starting " + method + " call against " + url) print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15) response = session.get(url.format(vin), timeout=15)
recharge_response = response recharge_response[vin] = response
recharge_last_update = datetime.now() recharge_last_update = datetime.now()
else: else:
# Data is up do date, returning cached data # Data is up do date, returning cached data
response = recharge_response response = recharge_response[vin]
elif method == "GET": elif method == "GET":
print("Starting " + method + " call against " + url) print("Starting " + method + " call against " + url)
response = session.get(url.format(vin), timeout=15) response = session.get(url.format(vin), timeout=15)
@@ -141,7 +147,10 @@ def api_call(url, method, sensor_id=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 ""
return parse_api_data(url, data, sensor_id)
def parse_api_data(url, data, sensor_id=None):
if url == VEHICLE_DETAILS_URL: if url == VEHICLE_DETAILS_URL:
return data["data"] return data["data"]
elif sensor_id == "battery_charge_level": elif sensor_id == "battery_charge_level":