Compare commits

...
73 Commits
Author SHA1 Message Date
jarvis bf6568d945 fix: full patch - reconnect_delay, keepalive, clean_session, QoS1
Patches for paho-mqtt rc=7 at startup (MQTT_ERR_CONN_LOST):

1. reconnect_delay_set(1, 30) - auto-reconnect with backoff
2. keepalive=120 - prevents timeout during long OTP startup
3. subscribe(otp_topic) in on_connect - no race condition
4. time.sleep(0.05) - prevents TCP socket buffer overflow
5. clean_session=False - queued messages survive reconnect
6. qos=1 for discovery configs - PUBACK confirms delivery
2026-06-24 17:05:49 +02:00
linus f8b6f9cf53 Move to base python 2026-05-15 09:30:54 +02:00
linus 7c32ca59be Add build.yaml for homeassistant version > 2026.04.X 2026-05-15 09:29:01 +02:00
linus 67948b0900 Bump version 2026-05-15 09:10:56 +02:00
48663d2e0d Fix AttributeError for paho-mqtt < 2.0 compatibility (#321)
When paho-mqtt < 2.0 is installed (e.g. in cached Docker layers),
mqtt.CallbackAPIVersion does not exist, causing an immediate crash on
startup with:

  AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'

Add try/except fallback so the addon works with both paho-mqtt 1.x and
2.x, eliminating the crash and allowing the addon to start correctly.

Co-authored-by: Barry Jarman <bazza@Barrys-Mac-mini.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 09:09:26 +02:00
linus 76cfa21db7 Bump pypi packages 2026-03-23 07:57:37 +01:00
linus 9d11dff24b Fix #318 2026-03-23 07:47:15 +01:00
bazza2000andGitHub 3a4160451c Fix: use POST for continueAuthentication endpoint (#317)
Volvo's PingFederate auth API now requires a POST request for the
continueAuthentication step after OTP verification. Using GET returns
the same OTP_VERIFIED state in a loop, never progressing to COMPLETED,
which causes a KeyError on 'authorizeResponse'.

Changing auth_session.get() to auth_session.post() with an empty JSON
body causes the API to return COMPLETED with the expected authorizeResponse.
2026-03-23 07:43:53 +01:00
linus 515e1a8d27 Bump addon version 2026-01-02 18:17:31 +01:00
linus 4961f7bfad Move readme to 2026 maintenance 2026-01-02 18:16:30 +01:00
linus a68216bdd5 Move object_id to default_entity_id for mqtt entities according to #312 2026-01-02 18:12:47 +01:00
linus 98d8dd7e65 Fix missing charging system state 2026-01-02 18:06:30 +01:00
linus 7acfdea1d8 Move energy api to v2 2025-11-16 08:36:30 +01:00
linus f95348769b Fix missing local refresh file while loading from filesystem 2025-11-12 14:04:52 +01:00
linus 4673f14a1c Add more logging for refresh process // renew refresh token if returned from api 2025-08-23 11:25:38 +02:00
linus 1a2f35f1b5 Add more logging for refresh process // renew refresh token if returned from api 2025-08-23 11:22:37 +02:00
Gert-Andry KäärameesandGitHub 33ee258f3f handle missing refresh_token key in token file (#306) 2025-07-28 07:39:44 +02:00
Russell StephensandGitHub 54db72df94 Update README.md (#304)
Improve readability and copy-paste functionality of docker command
2025-07-16 10:58:03 +02:00
linus 70b027fb70 Fix and unit for and 2025-07-13 20:03:27 +02:00
linus 1c07ec1b46 Make 1.12.0 ready.. version chaos 2025-07-12 17:51:17 +02:00
linus 2fa154e023 Make 1.2.0 ready 2025-07-12 17:49:51 +02:00
linus ae5e3074ed Add logging options for plugin 2025-07-12 17:45:25 +02:00
Luka GraandGitHub f5a1e48e26 TA trip statistics (#283)
Add separate sensors for TA statistics
averageFuelConsumptionAutomatic.value
averageEnergyConsumptionAutomatic.value
averageSpeedAutomatic.value
tripMeterAutomatic.value
2025-07-12 17:38:00 +02:00
ee9d3cdef9 Some minor corrections and more checking for exceptions (#267)
* better logging, indicate URL used on API errors, check exceptions in more places to dont kill threads, add option for mqtt logging

* dont force token renew on 5xx errors

* indicate payload on error message to understand better issue #270

* better checking on received update_interval #270

---------

Co-authored-by: Linus Dietz <45101649+Dielee@users.noreply.github.com>
2025-07-12 17:37:46 +02:00
G JohanssonandGitHub f1914e99c3 Remove not used addon settings (#266) 2025-07-11 21:57:26 +02:00
wivakuandGitHub 7a63f4d00f add long term statistics for fuel/energy consumption (#243)
Similar to #68
2025-07-11 21:56:05 +02:00
Brian WongandGitHub b644fe32fb Update README.md (#286)
added V60 CC
2025-07-11 21:55:20 +02:00
linus d9e4aada80 Bump Version 2025-07-08 13:21:52 +02:00
Linus DietzandGitHub 178f6a88af Update README.md 2025-07-08 13:18:00 +02:00
Linus DietzandGitHub 5ed7ef97ba Update README.md 2025-07-08 13:17:09 +02:00
linus db87aec408 Fix missing refresh token from reauth 2025-07-08 13:15:39 +02:00
234f48ff5c Update volvo.py (#295)
Replaced HTTP with HTTPS in responses

Co-authored-by: esusxunil <61476952+esusxunil@users.noreply.github.com>
2025-07-08 12:57:20 +02:00
Linus DietzandGitHub c2e873d7fe Update README.md 2025-01-02 11:08:30 +01:00
linus d3d511477b Rework image creation 2024-10-28 07:26:02 +01:00
linus 75db5da324 Fix re-auth with OTP while app runtime #251 2024-10-27 10:06:47 +01:00
Linus DietzandGitHub fd4fc9e173 Add EX30 2024-10-27 09:43:39 +01:00
linus 75013509f4 Fix re-auth with OTP while app runtime #251 2024-10-26 18:27:29 +02:00
linus 1d8132d93a Bump version 2024-10-16 07:41:57 +02:00
linus fc529f313f Remove unused multiplier options #239 2024-10-16 07:40:37 +02:00
linus 3f36368773 Optimize vcc api key check 2024-09-25 07:39:23 +02:00
linus 0cfb1a9e3f Support new statistic endpoints // Remove unused multiplier options 2024-09-20 09:43:59 +02:00
linus e179c76d39 Bump version 2024-09-16 19:27:51 +02:00
linus 391ae952d9 Bump version 2024-09-16 17:19:33 +02:00
Nick CampbellandGitHub f9f5e218f5 Handle boundary condition when selecting battery icon (#235)
We need to be able to handle floating point numbers right on the
boundary between two icons. This means our from..to configurations must
perfectly align at each end, and the algorithm to select those must not
be both less-than-or-equal (<=) AND more-than-or-equal (>=).

After this change:

  >>> get_icon_between(icon_states["battery_charge_level"], 0)
  'battery-alert-variant-outline'
  >>> get_icon_between(icon_states["battery_charge_level"], 9.9)
  'battery-alert-variant-outline'
  >>> get_icon_between(icon_states["battery_charge_level"], 10)
  'battery-10'
  >>> get_icon_between(icon_states["battery_charge_level"], 39)
  'battery-30'
  >>> get_icon_between(icon_states["battery_charge_level"], 39.9)
  'battery-30'
  >>> get_icon_between(icon_states["battery_charge_level"], 39.9999)
  'battery-30'
  >>> get_icon_between(icon_states["battery_charge_level"], 40)
  'battery-40'
  >>> get_icon_between(icon_states["battery_charge_level"], 99.99)
  'battery-90'
  >>> get_icon_between(icon_states["battery_charge_level"], 100)
  'battery'
  >>> get_icon_between(icon_states["battery_charge_level"], 101)
  'battery'
2024-09-16 17:17:22 +02:00
Linus DietzandGitHub 12744b6397 Add EX40 BEV 2024-09-06 09:56:35 +02:00
Linus DietzandGitHub 065c10c5bc Add alpine comunity packages 2024-08-30 12:02:55 +02:00
Linus Dietz 4b83aa149e Update readme 2024-08-09 09:50:51 +02:00
Linus Dietz 4a8caaf436 Add supported cars 2024-08-09 09:46:16 +02:00
Linus Dietz 710ca2fcbc Bump version 2024-08-09 09:32:21 +02:00
Linus Dietz c1998f8938 Rework image function 2024-08-09 09:30:04 +02:00
Linus Dietz 693e0b0048 Fix typo 2024-08-09 08:26:22 +02:00
Linus Dietz f23f56e45e Take care of corrupted token files #220 2024-08-09 08:25:17 +02:00
Linus Dietz 17eaa8dde2 Added retain status to survive HA reboots #210 2024-07-29 10:10:59 +02:00
Linus DietzandGitHub 8f3bf5f3b1 add addon changelog 2024-07-08 11:22:52 +02:00
Linus DietzandGitHub 9d4f8fab0c Bump addon version 2024-07-08 11:21:24 +02:00
Linus Dietz 414ac503b1 Fix backend status URL #209 #208 2024-07-08 10:52:54 +02:00
Linus Dietz 18926b67e4 Fix typo 2024-05-29 20:11:36 +02:00
Linus Dietz f650ea9247 Fix retained otp messages bug 2024-05-29 20:08:19 +02:00
Dielee a7a2e1182c Add missing addon changelog #199 2024-05-29 15:49:23 +02:00
Linus DietzandGitHub 3d3227de0d Update README.md 2024-05-29 07:46:08 +02:00
Dielee 44509529ff Set otp state right after HA input 2024-05-28 08:38:15 +02:00
Linus DietzandGitHub 0dd5852d81 Update README.md 2024-05-28 08:34:45 +02:00
Dielee c6ddd71021 Bump addon version 2024-05-28 08:26:38 +02:00
Dielee 1f0612ca4a Try to fix not saveable OTP Code 2024-05-28 08:26:18 +02:00
Dielee 6306c9a4b3 Let token survive addon updates 2024-05-27 14:16:07 +02:00
Dielee 8bd0388690 Extend waiting time for otp tp 120 seconds 2024-05-27 14:05:19 +02:00
Linus DietzandGitHub 245dd82c82 Update README.md 2024-05-27 14:02:39 +02:00
Dielee 8e471d8eb0 Fix security alert 2024-05-27 08:27:25 +02:00
Gert-janandGitHub cb1377005c Add "Battery Capacity" sensor (#185) 2024-05-27 08:18:25 +02:00
Linus DietzandGitHub 89bd76e981 Move to OTP auth (#190)
* Rebuild app OTP authentication

* Adding OTP readme

* Fix error message // Remove duplicated scopes

* Add dynamic header building including latest app version

* Fix missing startupcheck if using token from file

* Change version
2024-05-27 08:17:52 +02:00
Linus DietzandGitHub 19aafa3b21 Move actions to node.js 20 2024-03-14 09:14:12 +01:00
Linus DietzandGitHub 03755ca034 add C40 2024 2024-03-05 18:32:10 +01:00
Linus Dietz 907440b81e Fix changelog typo 2024-01-26 08:29:26 +01:00
13 changed files with 975 additions and 356 deletions
+6 -6
View File
@@ -27,20 +27,20 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
@@ -52,7 +52,7 @@ jobs:
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=sha type=sha
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm/v7,linux/arm/v6,linux/arm/v8 platforms: linux/amd64,linux/arm/v7,linux/arm/v6,linux/arm/v8
+72 -14
View File
@@ -1,4 +1,5 @@
# Volvo2Mqtt # Volvo2Mqtt
![Supports aarch64 Architecture][aarch64-shield] ![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield] ![Supports amd64 Architecture][amd64-shield]
![Supports armhf Architecture][armhf-shield] ![Supports armhf Architecture][armhf-shield]
@@ -21,13 +22,17 @@ If you like my work:<br>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/U7U8MFXCF) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/U7U8MFXCF)
## Confirmed working with ## Confirmed working with
- EX30 BEV (2024)
- XC40 BEV (2024) - XC40 BEV (2024)
- XC40 BEV (2023) - XC40 BEV (2023)
- XC40 BEV (2022) - XC40 BEV (2022)
- XC40 PHEV (2021) - XC40 PHEV (2021)
- V60 T8 PHEV (2023) - V60 T8 PHEV (2023)
- V60 T6 PHEV (2025)
- V60 CC B5 Mildhybrid (2024)
- C40 BEV (2023) - C40 BEV (2023)
- C40 BEV (2022) - C40 BEV (2022)
- C40 BEV (2024)
- XC90 T8 PHEV (2023) - XC90 T8 PHEV (2023)
- XC60 PHEV (2024) - XC60 PHEV (2024)
- XC60 PHEV (2023) - XC60 PHEV (2023)
@@ -39,6 +44,7 @@ If you like my work:<br>
- V90 PHEV T8 (2019)* - V90 PHEV T8 (2019)*
- V90 PHEV T6 (2024) - V90 PHEV T6 (2024)
- V90 B5 Mildhybrid (2023) - V90 B5 Mildhybrid (2023)
- EX40 BEV (2025)
*only partly working *only partly working
@@ -49,9 +55,10 @@ Please let me know if your car works with this addon so I can expand the list!<b
- Lock/unlock car - Lock/unlock car
- Start/stop climate - Start/stop climate
- Sensor "Battery Charge Level" - Sensor "Battery Charge Level"
- Sensor "Battery Capacity"
- Sensor "Electric Range" - Sensor "Electric Range"
- Sensor "Charging System Status" - Sensor "Charging System Status"
- Sensor "Charging Connection Status" (partly broken since 2.10 car update) - Sensor "Charging Connection Status"
- Sensor "Estimated Charging Finish Time" - Sensor "Estimated Charging Finish Time"
- Sensor "Door Lock Status (All doors, tank lid, and engine hood)" - Sensor "Door Lock Status (All doors, tank lid, and engine hood)"
- Sensor "Window Lock Status (All windows and sunroof - Thanks to @navet)" - Sensor "Window Lock Status (All windows and sunroof - Thanks to @navet)"
@@ -72,13 +79,67 @@ Please let me know if your car works with this addon so I can expand the list!<b
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) 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)
## OTP Authentication
As of version <b>v1.9.0</b>, this addon uses the same OTP authentication as the Volvo app.
The following steps are required for authentication in exactly this order:
1. Setup volvo2Mqtt, either via Docker, or via HA addon (take a look at the "Setup" section below)
2. Fill in your settings and start volvo2Mqtt
3. Your log will show the following lines <br>
```Waiting for otp code... Please check your mailbox and post your otp code to the following mqtt topic "volvoAAOS2mqtt/otp_code". Retry 0/15```<br>
```Waiting for otp code... Please check your mailbox and post your otp code to the following mqtt topic "volvoAAOS2mqtt/otp_code". Retry 1/15```<br>
```etc ...```
4. Now, open your mailbox and copy your OTP Code
5. Open HomeAssistant and search for the entity ID ```text.volvo_otp```
6. Paste your OTP into the text entity and **<ins>press Enter</ins>**<br>
6a. (optional) If your OTP cannot be safed, try to paste your OTP code via MQTT to the following topic: `volvoAAOS2mqtt/otp_code` <p>
<details>
<summary>OTP Post Script</summary>
<div>
<p>
1. Safe the following script in your script section <br>
2. Go to https://yourHAUrl/developer-tools/service<br>
3. Search for "Post Volvo OTP"<br>
4. Tick "OTP" and fill in your OTP<br>
5. Run this service<br>
</div>
alias: Post Volvo OTP
sequence:
- service: mqtt.publish
metadata: {}
data:
topic: volvoAAOS2mqtt/otp_code
payload: "{{ otp }}"
mode: single
fields:
otp:
selector:
text: null
name: OTP
description: Volvo OTP
</details>
8. If everything has worked, your addon is now authenticated. In the future, OTP authentication only needs to be done when updating, not when restarting the container.
## Setup ## Setup
<b>Docker:</b> <b>Docker:</b>
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 -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": ["key1", "key2"], "odometerMultiplier": 1, "averageSpeedDivider": 1, "averageFuelConsumptionMultiplier": 1}' -e TZ='Europe/Berlin' --name volvo2mqtt ghcr.io/dielee/volvo2mqtt:latest` <pre>
<code>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": ["key1", "key2"]}' \
-e TZ='Europe/Berlin' \
--name volvo2mqtt \
ghcr.io/dielee/volvo2mqtt:latest
</code>
</pre>
<b>HA Add-On:</b><br> <b>HA Add-On:</b><br>
@@ -90,22 +151,19 @@ Here is what every option means:
| ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- | | ------------------------- | :-------: | :-----------------------------------: | :----------: | --------------------------------------------------------------- |
| `CONF_updateInterval` | `int` | | **required** | Update intervall in seconds. | | `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_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` | `broker` | **required** | Your MQTT Broker IP. Eg. 192.168.0.5. | `CONF_mqtt` | `json` | `broker` | **required** | Your MQTT Broker IP. Eg. 192.168.0.5. |
| `CONF_mqtt` | `json` | `port` | 1883 | Your MQTT Broker Port. If no value is given, port 1883 will be used. | | `CONF_mqtt` | `json` | `port` | 1883 | Your MQTT Broker Port. If no value is given, port 1883 will be used. |
| `CONF_mqtt` | `json` | `username` | optional | MQTT Username for your broker. | `CONF_mqtt` | `json` | `username` | optional | MQTT Username for your broker. |
| `CONF_mqtt` | `json` | `password` | optional | MQTT Password for your broker. | `CONF_mqtt` | `json` | `password` | optional | MQTT Password for your broker. |
| `CONF_volvoData` | `json` | `username` | **required** | Normally your email address to login into the Volvo App. | `CONF_mqtt` | `json` | `logging` | optional | Enable MQTT logging (true/false). Default don't enable MQTT log. |
| `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App. | `CONF_volvoData` | `json` | `username` | **required** | Normally your email address to login into the Volvo App. |
| `CONF_volvoData` | `json` | `vin` | optional | A single VIN like "VIN1" or a list of VINs like "["VIN1", "VIN2"]". Leave this empty if you don't know your VIN. The addon will use every car that is tied to your account. | `CONF_volvoData` | `json` | `password` | **required** | Your password to login into the Volvo App. |
| `CONF_volvoData` | `json` | `vccapikey` | **required** | VCCAPIKEY linked with your volvo developer account. Get your Vccapi key from [here](https://developer.volvocars.com/account/). <b>Starting version 1.8.0, it is possible to define multiple keys, like this: `["vccapikey1", "vccapikey2", "vccapikey3", "etc..."]`</b> | `CONF_volvoData` | `json` | `vin` | optional | A single VIN like "VIN1" or a list of VINs like "["VIN1", "VIN2"]". Leave this empty if you don't know your VIN. The addon will use every car that is tied to your account. |
| `CONF_volvoData` | `json` | `odometerMultiplier` | optional | The multiplier value for the odometer value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the multiplier will be 1. | `CONF_volvoData` | `json` | `vccapikey` | **required** | VCCAPIKEY linked with your volvo developer account. Get your Vccapi key from [here](https://developer.volvocars.com/account/). <b>Starting version 1.8.0, it is possible to define multiple keys, like this: `["vccapikey1", "vccapikey2", "vccapikey3", "etc..."]`</b> |
| `CONF_volvoData` | `json` | `averageSpeedDivider` | optional | The divider value for the average speed value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the divider will be 1.
| `CONF_volvoData` | `json` | `averageFuelConsumptionMultiplier` | optional | The multiplier value for the average fuel consumption value, as the volvo api delivers inconsistent data. For some cars this setting is 10, for some 1. Try what's right for your car. If you leave it empty, the multiplier will be 1.
| `CONF_debug` | `string` | | optional | Debug option (true/false). Normally you don't need this. | | `CONF_debug` | `string` | | optional | Debug option (true/false). Normally you don't need this. |
| `TZ` | `string` | | **required** | Container timezone eg "Europe/Berlin" from [here](https://docs.diladele.com/docker/timezones.html)| | `TZ` | `string` | | **required** | Container timezone eg "Europe/Berlin" from [here](https://docs.diladele.com/docker/timezones.html)|
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg [amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg [armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg
@@ -113,6 +171,6 @@ Here is what every option means:
[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg [i386-shield]: https://img.shields.io/badge/i386-yes-green.svg
[releases]: https://github.com/Dielee/volvo2mqtt/releases [releases]: https://github.com/Dielee/volvo2mqtt/releases
[releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg [releases-shield]: https://img.shields.io/github/release/Dielee/volvo2mqtt.svg
[maintenance-shield]: https://img.shields.io/maintenance/yes/2024.svg [maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
[commits-shield]: https://img.shields.io/github/commit-activity/y/Dielee/volvo2mqtt.svg [commits-shield]: https://img.shields.io/github/commit-activity/y/Dielee/volvo2mqtt.svg
[commits]: https://github.com/Dielee/volvo2mqtt/commits/main [commits]: https://github.com/Dielee/volvo2mqtt/commits/main
+6 -5
View File
@@ -1,5 +1,6 @@
requests~=2.29.0 requests~=2.32.0
dynaconf~=3.1.12 dynaconf~=3.2.13
paho-mqtt~=1.6.1 paho-mqtt~=2.1.0
Babel~=2.12.1 Babel~=2.18.0
pytz~=2023.3 pytz~=2026.1.post1
google_play_scraper~=1.2.6
+242 -15
View File
@@ -1,57 +1,247 @@
## v1.8.27 ## v1.13.5
### 🐛 Bug Fixes:
- Add build.yaml for homeassistant version > 2026.04.X
## v1.13.4
### 🐛 Bug Fixes:
- Fix `AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'` when paho-mqtt < 2.0 is installed
## v1.13.3
### 🐛 Bug Fixes:
- Bump pypi packages
## v1.13.2
### 🐛 Bug Fixes:
- Fix KeyError: 'authorizeResponse' in auth flow #318
## v1.13.1
### 🐛 Bug Fixes:
- Fix missing charging system state `error` #314
- Move `object_id` to `default_entity_id` for mqtt entities according to #312
## v1.13.0
### 🚀 Features: ### 🚀 Features:
- Add state cacheing for "diagnostics" endpoint - Move energy api to v2 (https://developer.volvocars.com/apis/energy/v2/release-notes/)
## v1.12.2
### 🚀 Features:
- Add more logging for refresh process
- Update refresh token, if volvo API returns a new one
## v1.12.1
### 🐛 Bug Fixes:
- Fix `trip_speed` and `trip_distance` unit for `en_US` and `en_GB` #301
## v1.12.0
### 🚀 Features:
- Some minor corrections and more checking for exceptions #267
- Better logging, indicate URL used on API errors, check exceptions in more places to dont kill threads, add option for mqtt logging
- Dont force token renew on 5xx errors
- Indicate payload on error message to understand better issue #270
- Better checking on received update_interval #270
- Add TA trip statistics #283
Thanks to @luka6000 and @ivanfmartinez for your PRs!
## v1.10.6
### 🐛 Bug Fixes:
- Fix missing refresh_token and API changes by volvo. Thanks to @esusxunil.
## v1.10.5
### 🐛 Bug Fixes:
- Fix interior and exterior images if more than one car is in use
## v1.10.4
### 🐛 Bug Fixes:
- re-fix bug if app needs to be re-authenticated with OTP while runtime
## v1.10.3
### 🐛 Bug Fixes:
- Add more info logging for credential refresh process
- Fix bug if app needs to be re-authenticated with OTP while runtime
## v1.10.2
### 🐛 Bug Fixes:
- Remove unused multiplier options #239
## v1.10.1
### 🚀 Features:
- Optimize logging for vcc-api-key check #238
## v1.10.0
### 🚀 Features:
- Remove unused multiplier options
### 🐛 Bug Fixes:
- Support new statistic keys #237
## v1.9.7
### 🐛 Bug Fixes:
- Fix EX30 battery state #230
## v1.9.6
### 🐛 Bug Fixes:
- Fixed interior and exterior Images #219
- Take care of broken token files #220
## v1.9.5
### 🚀 Features:
- Added "retain" states to survive HA reboot
## v1.9.4
### 🐛 Bug Fixes:
- Fix backend state API url
## v1.9.3
### 🐛 Bug Fixes:
- Ignore retained OTP messages #193
## v1.9.2
### 🐛 Bug Fixes:
- Try to fix OTP Input
- Fix `SyntaxWarning: invalid escape sequence '\d'` #196
## v1.9.1
### 🚀 Features:
- The token file now survives an HA addon update/restart
### 🐛 Bug Fixes:
- Extend OTP retry time to 120 seconds
## v1.9.0
### ⚠️ Breaking change
### 🚀 Features:
- Add "Battery Capacity" sensor. Thanks to @gurtjun!
- OTP Auth
This addon uses the same OTP authentication as the Volvo app, now.
The following steps are required for authentication in exactly this order:
1. Setup volvo2Mqtt, either via Docker, or via HA addon (take a look at the "Setup" section)
2. Fill in your settings and start volvo2Mqtt
3. Your log will show the following lines
Waiting for otp code... Please check your mailbox and post your otp code to the following mqtt topic "volvoAAOS2mqtt/otp_code". Retry 0/15
Waiting for otp code... Please check your mailbox and post your otp code to the following mqtt topic "volvoAAOS2mqtt/otp_code". Retry 1/15
etc ...
4. Now, open your mailbox and copy your OTP Code
5. Open HomeAssistant and search for the entity ID text.volvo_otp
6. Paste your OTP into the text entity and press Enter
7. If everything has worked, your addon is now authenticated. In the future, OTP authentication only needs to be done when updating, not when restarting the container.
## v1.8.27
### 🚀 Features:
- Add state caching for "diagnostics" endpoint
## v1.8.26 ## v1.8.26
### 🚀 Features: ### 🚀 Features:
- Add option to disable updates #160 (Set the update interval to -1) - Add option to disable updates #160 (Set the update interval to -1)
## v1.8.25 ## v1.8.25
### 🚀 Features: ### 🚀 Features:
- Fix INTERNAL_SERVER_ERROR #165 - Fix INTERNAL_SERVER_ERROR #165
## v1.8.24 ## v1.8.24
### 🚀 Features: ### 🚀 Features:
- Add option for dynamic interval settings #160 - Add option for dynamic interval settings #160
- Add warnings endpoint #151 - Add warnings endpoint #151
## v1.8.23 ## v1.8.23
### 🚀 Features: ### 🚀 Features:
- Add new charging system states #152 - Add new charging system states #152
## v1.8.22 ## v1.8.22
### 🚀 Features: ### 🚀 Features:
- Change units to HA default #148 - Change units to HA default #148
## v1.8.21 ## v1.8.21
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Try to fix addon build #146 - Try to fix addon build #146
## v1.8.20 ## v1.8.20
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix Addon crash if icon state is `UNKOWN` - Fix Addon crash if icon state is `UNKOWN`
- Add `UNSPECIFIED` state for windows - Add `UNSPECIFIED` state for windows
## v1.8.19 ## v1.8.19
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix Estimated Charging Time not changing to zero #133 - Fix Estimated Charging Time not changing to zero #133
## v1.8.18 ## v1.8.18
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix Backend State Sensor returning `null` - Fix Backend State Sensor returning `null`
## v1.8.17 ## v1.8.17
### 🚀 Features: ### 🚀 Features:
- Move vehicle and vehicle details to v2 endpoint - Move vehicle and vehicle details to v2 endpoint
@@ -61,6 +251,7 @@
- Fix Battery Charge Level icon update - Fix Battery Charge Level icon update
## v1.8.16 ## v1.8.16
### 🚀 Features: ### 🚀 Features:
- Add `Average energy consumption`, `Distance to empty battery` and `Washer fluid level` sensors - Add `Average energy consumption`, `Distance to empty battery` and `Washer fluid level` sensors
@@ -68,18 +259,21 @@
- Use cached requests for fuel endpoint - Use cached requests for fuel endpoint
## v1.8.15 ## v1.8.15
### 🚀 Features: ### 🚀 Features:
- Move `batteryChargeLevel` to the new v2 fuel endpoint - Move `batteryChargeLevel` to the new v2 fuel endpoint
- Add api backend status sensor - Add api backend status sensor
## v1.8.14 ## v1.8.14
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix distance to empty sensor #127 - Fix distance to empty sensor #127
- Remove unused service warning trigger sensor - Remove unused service warning trigger sensor
## v1.8.13 ## v1.8.13
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Add "Time to Service" sensor #125<p> - Add "Time to Service" sensor #125<p>
@@ -87,62 +281,73 @@
"Time to Service" contains this information now. "Time to Service" contains this information now.
## v1.8.12 ## v1.8.12
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix `AJAR` state for windows - Fix `AJAR` state for windows
- Fix deprecation from `currentThread` to `current_thread` - Fix deprecation from `currentThread` to `current_thread`
## v1.8.11 ## v1.8.11
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Remove old code from checks for running engine sensor - Remove old code from checks for running engine sensor
- Move battery charge level sensor from v2 to v1, as v2 returns 404 - Move battery charge level sensor from v2 to v1, as v2 returns 404
## v1.8.10 ## v1.8.10
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Add `AJAR` state for sunroof #123 - Add `AJAR` state for sunroof #123
## v1.8.9 ## v1.8.9
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix api changes from volvo #121 - Fix api changes from volvo #121
## v1.8.8 ## v1.8.8
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix json decode error, if volvo API returns a simple string #104 - Fix json decode error, if volvo API returns a simple string #104
## v1.8.7 ## v1.8.7
### 🚀 Features: ### 🚀 Features:
- Add option to disable log completely #96 - Add option to disable log completely #96
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix regex error for some mailaddresses #97 - Fix regex error for some mailaddresses #97
## v1.8.6 ## v1.8.6
### 🚀 Features: ### 🚀 Features:
- Add option to use multiple docker containers (with different logins) #93 - Add option to use multiple docker containers (with different logins) #93
## v1.8.5 ## v1.8.5
### 🚀 Features: ### 🚀 Features:
- Allow phone number as username #91 - Allow phone number as username #91
## v1.8.4 ## v1.8.4
### 🚀 Features: ### 🚀 Features:
- Optimize Addon configuration #90 - Optimize Addon configuration #90
## v1.8.3 ## v1.8.3
### 🚀 Features: ### 🚀 Features:
- Add `device_class: battery` for battery state sensors from BEV and PHEV - Add `device_class: battery` for battery state sensors from BEV and PHEV
## v1.8.2 ## v1.8.2
### 🚀 Features: ### 🚀 Features:
- Add `updateInterval` limit to prevent abuse (60 Seconds) - Add `updateInterval` limit to prevent abuse (60 Seconds)
@@ -150,17 +355,19 @@
- Optimize vcc api key change behaviour - Optimize vcc api key change behaviour
## v1.8.1 ## v1.8.1
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix app crash if `debug` is not set #86 - Fix app crash if `debug` is not set #86
## v1.8.0 ## v1.8.0
### 🚀 Features: ### 🚀 Features:
- <b>Breaking change: The `vccapikey` setting is now a list. It is possible to - <b>Breaking change: The `vccapikey` setting is now a list. It is possible to
add multiple VCCAPIKEYs. This will be helpful if you are extending your 10.000 call limit sometimes. add multiple VCCAPIKEYs. This will be helpful if you are extending your 10.000 call limit sometimes.
Change your vccapikey config like this: Change your vccapikey config like this:
Old: Old:
``` ```
"vccapikey": "vccapikey1" "vccapikey": "vccapikey1"
@@ -175,24 +382,28 @@
- etc. - etc.
``` ```
More information about this feature [here](https://github.com/Dielee/volvo2mqtt/issues/84). More information about this feature [here](https://github.com/Dielee/volvo2mqtt/issues/84).
</b> </b>
## v1.7.10 ## v1.7.10
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix application startup if using `en_US` or `en_GB` as locale - Fix application startup if using `en_US` or `en_GB` as locale
## v1.7.9 ## v1.7.9
### 🚀 Features: ### 🚀 Features:
- Added vehicle diagnostics sensors #75. Thanks to @aerrow5! - Added vehicle diagnostics sensors #75. Thanks to @aerrow5!
## v1.7.8 ## v1.7.8
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fix "Estimated Charging Finish Time" Sensor - Fix "Estimated Charging Finish Time" Sensor
## v1.7.7 ## v1.7.7
### 🚀 Features: ### 🚀 Features:
- Added long term statistics for some sensors #68. Thanks to @bogdanbujdea! - Added long term statistics for some sensors #68. Thanks to @bogdanbujdea!
@@ -200,44 +411,54 @@
- Perpare for scheduler option (not yet ready) - Perpare for scheduler option (not yet ready)
## v1.7.6 ## v1.7.6
### 🚀 Features: ### 🚀 Features:
- Added support for interior and exterior images via the new [MQTT Image](https://www.home-assistant.io/integrations/image.mqtt/) entity. #62 - Added support for interior and exterior images via the
new [MQTT Image](https://www.home-assistant.io/integrations/image.mqtt/) entity. #62
<b>NOTE: This feature is only available using homeassistant version 2023.7.1 or greater.</b> <b>NOTE: This feature is only available using homeassistant version 2023.7.1 or greater.</b>
## v1.7.5 ## v1.7.5
### 🚀 Features: ### 🚀 Features:
- Added support for `LOCKING` and `UNLOCKING` state (Lock entity) - Added support for `LOCKING` and `UNLOCKING` state (Lock entity)
## v1.7.4 ## v1.7.4
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fixed undocumented charging connection state `CONNECTION_STATUS_FAULT` '#57 - Fixed undocumented charging connection state `CONNECTION_STATUS_FAULT` '#57
## v1.7.3 ## v1.7.3
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fixed API changes from Volvo #53 - Fixed API changes from Volvo #53
<b>ATTENTION: Volvo optimized their API. Maybe you have to set your odometer multiplier to 1. <b>ATTENTION: Volvo optimized their API. Maybe you have to set your odometer multiplier to 1.
Also check all the other multipliers!</b> Also check all the other multipliers!</b>
## v1.7.2 ## v1.7.2
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fixed "Engine State" can be "true" or "false", not "RUNNING" or "STOPPED" #47 - Fixed "Engine State" can be "true" or "false", not "RUNNING" or "STOPPED" #47
## v1.7.1 ## v1.7.1
### 🚀 Features: ### 🚀 Features:
- Moved "Engine State" from "sensor" to the "binary_sensor" domain - Moved "Engine State" from "sensor" to the "binary_sensor" domain
- Improved HA Add-On security level to 7 - Improved HA Add-On security level to 7
<b>NOTE: The old "sensor" entity will be not deleted automatically. If you want to clean up your mqtt device, just delete it and restart the volvo2mqtt addon. <b>NOTE: The old "sensor" entity will be not deleted automatically. If you want to clean up your mqtt device, just
delete it and restart the volvo2mqtt addon.
</b> </b>
## v1.7.0 ## v1.7.0
### 🚀 Features: ### 🚀 Features:
- Added MQTT autodiscovery for HA Add-On users - Added MQTT autodiscovery for HA Add-On users
@@ -250,21 +471,25 @@ Also check all the other multipliers!</b>
![image](https://i.imgur.com/GBamhFn.png)</b> ![image](https://i.imgur.com/GBamhFn.png)</b>
## v1.6.5 ## v1.6.5
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fixed missing key in unit mappings for `en_US` and `en_GB` mapping - Fixed missing key in unit mappings for `en_US` and `en_GB` mapping
## v1.6.4 ## v1.6.4
### 🚀 Features: ### 🚀 Features:
- Added dynamic icons for lock, battery and window state #43 - Added dynamic icons for lock, battery and window state #43
## v1.6.3 ## v1.6.3
### 🚀 Features: ### 🚀 Features:
- Moved data source for "Distance to empty" from extended api to connected api - Moved data source for "Distance to empty" from extended api to connected api
## v1.6.2 ## v1.6.2
### 🚀 Features: ### 🚀 Features:
- Moved print statements to logger - Moved print statements to logger
@@ -272,17 +497,19 @@ Also check all the other multipliers!</b>
- The full HA Addon log can now be found at your addon smb share `\\<Your HA IP>\addons\volvo2mqtt\log\volvo2mqtt.log` - The full HA Addon log can now be found at your addon smb share `\\<Your HA IP>\addons\volvo2mqtt\log\volvo2mqtt.log`
## v1.6.1 ## v1.6.1
### 🚀 Features: ### 🚀 Features:
- Added "Washer Fluid Level" and "Distance to Empty" sensors #37 - Added "Washer Fluid Level" and "Distance to Empty" sensors #37
- Expanded climate control for better state sync with HA - Expanded climate control for better state sync with HA
### 🐛 Bug Fixes: ### 🐛 Bug Fixes:
- Fixed wrong return values for not working endpoints - Fixed wrong return values for not working endpoints
- Fixed app crash on credential renewal timeout #39 - Fixed app crash on credential renewal timeout #39
## v1.6.0 ## v1.6.0
### 🚀 Features: ### 🚀 Features:
- Added conversion from metric to imperial system for `en_US` and `en_GB` locale - Added conversion from metric to imperial system for `en_US` and `en_GB` locale
+4 -1
View File
@@ -1,6 +1,9 @@
ARG BUILD_FROM ARG BUILD_FROM
FROM $BUILD_FROM FROM $BUILD_FROM
# Add community packets
RUN echo -e "https://nl.alpinelinux.org/alpine/latest-stable/main\nhttps://nl.alpinelinux.org/alpine/latest-stable/community" > /etc/apk/repositories
# Install requirements for add-on # Install requirements for add-on
RUN apk add --no-cache python3 bash py3-pip RUN apk add --no-cache python3 bash py3-pip
@@ -18,4 +21,4 @@ COPY / .
RUN chmod a+x /volvoAAOS2mqtt/run.sh RUN chmod a+x /volvoAAOS2mqtt/run.sh
# command to run on container start # command to run on container start
CMD [ "/volvoAAOS2mqtt/run.sh" ] CMD [ "/volvoAAOS2mqtt/run.sh" ]
+6
View File
@@ -0,0 +1,6 @@
build_from:
amd64: ghcr.io/home-assistant/base-python:latest
aarch64: ghcr.io/home-assistant/base-python:latest
armv7: ghcr.io/home-assistant/base-python:latest
armhf: ghcr.io/home-assistant/base-python:latest
i386: ghcr.io/home-assistant/base-python:latest
+3 -7
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt" name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge" description: "Volvo AAOS MQTT bridge"
version: "1.8.27" version: "1.13.5"
slug: "volvo2mqtt" slug: "volvo2mqtt"
init: false init: false
url: "https://github.com/Dielee/volvo2mqtt" url: "https://github.com/Dielee/volvo2mqtt"
@@ -21,15 +21,13 @@ options:
port: "auto_port" port: "auto_port"
username: "auto_user" username: "auto_user"
password: "auto_password" password: "auto_password"
logging: false
volvoData: volvoData:
username: null username: null
password: null password: null
vin: "" vin: ""
vccapikey: vccapikey:
- null - null
odometerMultiplier: 1
averageSpeedDivider: 1
averageFuelConsumptionMultiplier: 1
schema: schema:
updateInterval: int(60,) updateInterval: int(60,)
babelLocale: str babelLocale: str
@@ -41,15 +39,13 @@ schema:
port: str port: str
username: str? username: str?
password: str? password: str?
logging: bool
volvoData: volvoData:
username: match(^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)|(\+\d{5,20})$) username: match(^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)|(\+\d{5,20})$)
password: str password: str
vin: str? vin: str?
vccapikey: vccapikey:
- match(^\b\w{32}\b$) - match(^\b\w{32}\b$)
odometerMultiplier: int(1,)
averageSpeedDivider: int(1,)
averageFuelConsumptionMultiplier: int(1,)
arch: arch:
- aarch64 - aarch64
- amd64 - amd64
+37 -25
View File
@@ -1,8 +1,9 @@
from config import settings from config import settings
VERSION = "v1.8.27" VERSION = "v1.13.5"
OAUTH_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2" OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
OAUTH_AUTH_URL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles" VEHICLES_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles"
VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}" VEHICLE_DETAILS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}"
WINDOWS_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/windows" WINDOWS_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/windows"
@@ -11,7 +12,7 @@ CLIMATE_STOP_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/
LOCK_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/doors" LOCK_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/doors"
CAR_LOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/lock" CAR_LOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/lock"
CAR_UNLOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/unlock" CAR_UNLOCK_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/commands/unlock"
RECHARGE_STATE_URL = "https://api.volvocars.com/energy/v1/vehicles/{0}/recharge-status" RECHARGE_STATE_URL = "https://api.volvocars.com/energy/v2/vehicles/{0}/state"
ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/odometer" ODOMETER_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/odometer"
LOCATION_STATE_URL = "https://api.volvocars.com/location/v1/vehicles/{0}/location" LOCATION_STATE_URL = "https://api.volvocars.com/location/v1/vehicles/{0}/location"
TYRE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/tyres" TYRE_STATE_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/tyres"
@@ -21,7 +22,7 @@ STATISTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/st
ENGINE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/engine" 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" VEHICLE_DIAGNOSTICS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/diagnostics"
WARNINGS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/warnings" WARNINGS_URL = "https://api.volvocars.com/connected-vehicle/v2/vehicles/{0}/warnings"
API_BACKEND_STATUS = "https://oip-dev-bff.euwest1.production.volvo.care/api/v1/backend-status" API_BACKEND_STATUS = "https://public-developer-portal-bff.weu-prod.ecpaz.volvocars.biz/api/v1/backend-status"
LENGTH_KILOMETERS = "km" LENGTH_KILOMETERS = "km"
SPEED_KILOMETERS_PER_HOUR = "km/h" SPEED_KILOMETERS_PER_HOUR = "km/h"
@@ -40,7 +41,9 @@ units = {
"odometer": {"unit": LENGTH_MILES}, "odometer": {"unit": LENGTH_MILES},
"average_speed": {"unit": SPEED_MILES_PER_HOUR}, "average_speed": {"unit": SPEED_MILES_PER_HOUR},
"distance_to_empty": {"unit": LENGTH_MILES}, "distance_to_empty": {"unit": LENGTH_MILES},
"distance_to_service": {"unit": LENGTH_MILES} "distance_to_service": {"unit": LENGTH_MILES},
"trip_distance": {"unit": LENGTH_MILES},
"trip_speed": {"unit": SPEED_MILES_PER_HOUR}
}, },
"en_US": { "en_US": {
"divider": 1.60934, "divider": 1.60934,
@@ -48,19 +51,21 @@ units = {
"odometer": {"unit": LENGTH_MILES}, "odometer": {"unit": LENGTH_MILES},
"average_speed": {"unit": SPEED_MILES_PER_HOUR}, "average_speed": {"unit": SPEED_MILES_PER_HOUR},
"distance_to_empty": {"unit": LENGTH_MILES}, "distance_to_empty": {"unit": LENGTH_MILES},
"distance_to_service": {"unit": LENGTH_MILES} "distance_to_service": {"unit": LENGTH_MILES},
"trip_distance": {"unit": LENGTH_MILES},
"trip_speed": {"unit": SPEED_MILES_PER_HOUR}
} }
} }
availability_topic = "volvoAAOS2mqtt/availability" availability_topic = "volvoAAOS2mqtt/availability"
charging_system_states = {"CHARGING_SYSTEM_CHARGING": "Charging", "CHARGING_SYSTEM_IDLE": "Idle", charging_system_states = {"CHARGING": "Charging", "IDLE": "Idle",
"CHARGING_SYSTEM_FAULT": "Fault", "CHARGING_SYSTEM_UNSPECIFIED": "Unspecified", "FAULT": "Fault", "UNSPECIFIED": "Unspecified",
"CHARGING_SYSTEM_DONE": "Done", "CHARGING_SYSTEM_SCHEDULED": "Scheduled"} "DONE": "Done", "SCHEDULED": "Scheduled", "ERROR": "Error",
"DISCHARGING": "Discharging"}
charging_connection_states = {"CONNECTION_STATUS_DISCONNECTED": "Disconnected", "CONNECTION_STATUS_UNSPECIFIED": "Unspecified", charging_connection_states = {"DISCONNECTED": "Disconnected", "UNSPECIFIED": "Unspecified",
"CONNECTION_STATUS_CONNECTED_DC": "Connected DC", "CONNECTION_STATUS_CONNECTED_AC": "Connected AC", "FAULT": "Fault", "CONNECTED": "Connected"}
"CONNECTION_STATUS_FAULT": "Fault"}
window_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"} window_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"} door_states = {"CLOSED": "OFF", "OPEN": "ON", "UNSPECIFIED": "UNKNOWN", "AJAR": "ON"}
@@ -74,23 +79,24 @@ icon_states = {
"door_rear_right": {"ON": "car-door", "OFF": "car-door-lock"}, "door_rear_right": {"ON": "car-door", "OFF": "car-door-lock"},
"engine_state": {"ON": "engine-outline", "OFF": "engine-off-outline"}, "engine_state": {"ON": "engine-outline", "OFF": "engine-off-outline"},
"battery_charge_level": [ "battery_charge_level": [
{"from": 100, "to": 100, "icon": "battery"}, {"from": float('inf'), "to": 100, "icon": "battery"},
{"from": 99, "to": 90, "icon": "battery-90"}, {"from": 100, "to": 90, "icon": "battery-90"},
{"from": 89, "to": 80, "icon": "battery-80"}, {"from": 90, "to": 80, "icon": "battery-80"},
{"from": 79, "to": 70, "icon": "battery-70"}, {"from": 80, "to": 70, "icon": "battery-70"},
{"from": 69, "to": 60, "icon": "battery-60"}, {"from": 70, "to": 60, "icon": "battery-60"},
{"from": 59, "to": 50, "icon": "battery-50"}, {"from": 60, "to": 50, "icon": "battery-50"},
{"from": 49, "to": 40, "icon": "battery-40"}, {"from": 50, "to": 40, "icon": "battery-40"},
{"from": 39, "to": 30, "icon": "battery-30"}, {"from": 40, "to": 30, "icon": "battery-30"},
{"from": 29, "to": 20, "icon": "battery-20"}, {"from": 30, "to": 20, "icon": "battery-20"},
{"from": 19, "to": 10, "icon": "battery-10"}, {"from": 20, "to": 10, "icon": "battery-10"},
{"from": 9, "to": 0, "icon": "battery-alert-variant-outline"}, {"from": 10, "to": 0, "icon": "battery-alert-variant-outline"},
] ]
} }
supported_entities = [ supported_entities = [
{"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL, "state_class": "measurement"}, {"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"}, {"name": "Battery Charge Level", "domain": "sensor", "device_class": "battery", "id": "battery_charge_level", "unit": "%", "icon": "car-battery", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
{"name": "Battery Capacity", "domain": "sensor", "device_class": "energy_storage", "id": "battery_capacity", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-battery", "url": VEHICLE_DETAILS_URL, "state_class": "measurement"},
{"name": "Electric Range", "domain": "sensor", "id": "electric_range", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["electric_range"]["unit"], "icon": "map-marker-distance", "url": RECHARGE_STATE_URL, "state_class": "measurement"}, {"name": "Electric Range", "domain": "sensor", "id": "electric_range", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["electric_range"]["unit"], "icon": "map-marker-distance", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"name": "Estimated Charging Time", "domain": "sensor", "id": "estimated_charging_time", "unit": TIME_MINUTES, "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL, "state_class": "measurement"}, {"name": "Estimated Charging Time", "domain": "sensor", "id": "estimated_charging_time", "unit": TIME_MINUTES, "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL, "state_class": "measurement"},
{"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},
@@ -121,8 +127,8 @@ supported_entities = [
{"name": "Tire Rear Right", "domain": "sensor", "id": "tyre_rear_right", "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": "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": VOLUME_LITERS, "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"}, {"name": "Fuel Level", "domain": "sensor", "id": "fuel_level", "unit": VOLUME_LITERS, "icon": "fuel", "url": FUEL_BATTERY_STATE_URL, "state_class": "measurement"},
{"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL}, {"name": "Average Fuel Consumption", "domain": "sensor", "id": "average_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL}, {"name": "Average Energy Consumption", "domain": "sensor", "id": "average_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Distance to Empty Tank", "domain": "sensor", "id": "distance_to_empty_tank", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"}, {"name": "Distance to Empty Tank", "domain": "sensor", "id": "distance_to_empty_tank", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Distance to Empty Battery", "domain": "sensor", "id": "distance_to_empty_battery", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"}, {"name": "Distance to Empty Battery", "domain": "sensor", "id": "distance_to_empty_battery", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["distance_to_empty"]["unit"], "icon": "map-marker-distance", "url": STATISTICS_URL, "state_class": "measurement"},
{"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"}, {"name": "Average Speed", "domain": "sensor", "id": "average_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["average_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"},
@@ -134,6 +140,12 @@ supported_entities = [
{"name": "API Backend status", "domain": "sensor", "id": "api_backend_status", "icon": "alert"}, {"name": "API Backend status", "domain": "sensor", "id": "api_backend_status", "icon": "alert"},
{"name": "Update Interval", "domain": "number", "id": "update_interval", "unit": "seconds", "icon": "timer", "min": -1, "max": 600, "mode": "box"}, {"name": "Update Interval", "domain": "number", "id": "update_interval", "unit": "seconds", "icon": "timer", "min": -1, "max": 600, "mode": "box"},
{"name": "Warnings", "domain": "sensor", "id": "warnings", "icon": "alert", "url": WARNINGS_URL} {"name": "Warnings", "domain": "sensor", "id": "warnings", "icon": "alert", "url": WARNINGS_URL}
,{"name": "Trip Fuel Consumption", "domain": "sensor", "id": "trip_fuel_consumption", "unit": VOLUME_LITERS, "icon": "fuel", "url": STATISTICS_URL}
,{"name": "Trip distance", "domain": "sensor", "id": "trip_distance", "unit": LENGTH_KILOMETERS if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["trip_distance"]["unit"], "icon": "counter", "url": STATISTICS_URL, "state_class":"measurement"}
,{"name": "Trip Energy Consumption", "domain": "sensor", "id": "trip_energy_consumption", "unit": ENERGY_KILO_WATT_HOUR, "icon": "car-electric", "url": STATISTICS_URL}
,{"name": "Trip Speed", "domain": "sensor", "id": "trip_speed", "unit": SPEED_KILOMETERS_PER_HOUR if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["trip_speed"]["unit"], "icon": "speedometer", "url": STATISTICS_URL, "state_class": "measurement"}
] ]
old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"] old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"]
otp_max_loops = 24
otp_mqtt_topic = "volvoAAOS2mqtt/otp_code"
+154 -51
View File
@@ -5,12 +5,14 @@ import json
import volvo import volvo
import util import util
import os import os
import requests
import threading
from threading import Thread, Timer from threading import Thread, Timer
from datetime import datetime from datetime import datetime
from babel.dates import format_datetime from babel.dates import format_datetime
from config import settings from config import settings
from const import CLIMATE_START_URL, CLIMATE_STOP_URL, CAR_LOCK_URL, \ 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, availability_topic, icon_states, old_entity_ids, otp_mqtt_topic
mqtt_client: mqtt.Client mqtt_client: mqtt.Client
subscribed_topics = [] subscribed_topics = []
@@ -20,11 +22,25 @@ climate_timer = {}
engine_status = {} engine_status = {}
devices = {} devices = {}
active_schedules = {} active_schedules = {}
otp_code = None
def connect(): def connect():
client = mqtt.Client("volvoAAOS2mqtt") if os.environ.get("IS_HA_ADDON") \ client_id = "volvoAAOS2mqtt" if os.environ.get("IS_HA_ADDON") \
else mqtt.Client("volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", "")) else "volvoAAOS2mqtt_" + settings.volvoData["username"].replace("+", "")
try:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, client_id, clean_session=False)
except AttributeError:
# paho-mqtt < 2.0 does not have CallbackAPIVersion
client = mqtt.Client(client_id)
if "logging" in settings["mqtt"] and settings["mqtt"]["logging"]:
mqtt_logger = logging.getLogger("mqtt")
client.enable_logger(mqtt_logger)
client.on_message = safe_on_message
client.on_disconnect = on_disconnect
client.on_connect = on_connect
client.reconnect_delay_set(min_delay=1, max_delay=30)
client.will_set(availability_topic, "offline", 0, False) client.will_set(availability_topic, "offline", 0, False)
if settings["mqtt"]["username"] and settings["mqtt"]["password"]: if settings["mqtt"]["username"] and settings["mqtt"]["password"]:
@@ -35,27 +51,60 @@ def connect():
if isinstance(conf_port, int): if isinstance(conf_port, int):
if conf_port > 0: if conf_port > 0:
port = settings["mqtt"]["port"] port = settings["mqtt"]["port"]
client.connect(settings["mqtt"]["broker"], port) client.connect(settings["mqtt"]["broker"], port, 120)
client.loop_start() client.loop_start()
client.on_message = on_message
client.on_disconnect = on_disconnect
client.on_connect = on_connect
global mqtt_client global mqtt_client
mqtt_client = client mqtt_client = client
def create_otp_input():
state_topic = otp_mqtt_topic + "/state"
config = {
"name": "Volvo OTP",
"default_entity_id": "volvo_otp",
"schema": "state",
"command_topic": otp_mqtt_topic,
"state_topic": state_topic,
"unique_id": "volvoAAOS2mqtt_otp",
"pattern": r"\d{6}",
"icon": "mdi:two-factor-authentication",
"mode": "text"
}
mqtt_client.publish(
"homeassistant/text/volvoAAOS2mqtt/volvo_otp/config",
json.dumps(config),
retain=True
)
mqtt_client.publish(
state_topic,
"000000",
retain=True
)
def set_otp_state():
mqtt_client.publish(
otp_mqtt_topic + "/state",
otp_code,
retain=True
)
def send_car_images(vin, data, device): def send_car_images(vin, data, device):
if util.keys_exists(data, "images"): if util.keys_exists(data, "images"):
for entity in [{"name": "Exterior Image", "id": "exterior_image"}, for entity in [{"name": "Exterior Image", "id": "exterior_image"},
{"name": "Interior Image", "id": "interior_image"}]: {"name": "Interior Image", "id": "interior_image"}]:
url_topic = f"homeassistant/image/{vin}_{entity['id']}/image_url" image_topic = f"homeassistant/image/{vin}_{entity['id']}/image_topic"
config = { config = {
"name": entity["name"], "name": entity["name"],
"object_id": f"volvo_{vin}_{entity['id']}", "default_entity_id": f"volvo_{vin}_{entity['id']}",
"schema": "state", "schema": "state",
"icon": "mdi:image-area", "icon": "mdi:image-area",
"url_topic": url_topic, "content_type": "image/png",
"image_topic": image_topic,
"device": device, "device": device,
"unique_id": f"volvoAAOS2mqtt_{vin}_{entity['id']}", "unique_id": f"volvoAAOS2mqtt_{vin}_{entity['id']}",
"availability_topic": availability_topic "availability_topic": availability_topic
@@ -67,71 +116,116 @@ def send_car_images(vin, data, device):
retain=True retain=True
) )
if entity["id"] == "exterior_image": headers = {"User-Agent": "(Windows NT 10.0; Win64; x64)",
mqtt_client.publish( "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "document", "Accept-Encoding": "gzip, deflate, br, zstd",
url_topic, "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1"}
data["images"]["exteriorImageUrl"],
retain=True
)
if entity["id"] == "interior_image": # Post Images
if not os.path.exists(f'{entity["id"]}_{vin}.png'):
data_path = "exteriorImageUrl" if entity["id"] == "exterior_image" else "internalImageUrl"
response = requests.get(data["images"][data_path], headers=headers)
if response.status_code == 200:
with open(f'{entity["id"]}_{vin}.png', 'wb') as image:
image.write(response.content)
else:
logging.warning("Error getting car images: " + str(response.status_code) + " Message: " + response.text)
if os.path.exists(f'{entity["id"]}_{vin}.png'):
ext_image = open(f'{entity["id"]}_{vin}.png', mode="rb").read()
mqtt_client.publish( mqtt_client.publish(
url_topic, image_topic,
data["images"]["internalImageUrl"], ext_image,
retain=True retain=True
) )
def on_connect(client, userdata, flags, rc): def on_connect(client, userdata, flags, rc):
# set a better name for the mqtt_loop Thread
threading.current_thread().name = 'mqtt_thread'
logging.info("MQTT connected")
send_heartbeat() send_heartbeat()
mqtt_client.subscribe("volvoAAOS2mqtt/otp_code")
if len(subscribed_topics) > 0: if len(subscribed_topics) > 0:
for topic in subscribed_topics: for topic in subscribed_topics:
mqtt_client.subscribe(topic) mqtt_client.subscribe(topic)
def on_disconnect(client, userdata, rc): def on_disconnect(client, userdata, rc):
logging.warning("MQTT disconnected, reconnecting automatically") logging.warning(f"MQTT disconnected, reconnecting automatically rc={rc}")
def on_message(client, userdata, msg): def safe_on_message(client, userdata, msg):
try: try:
vin = msg.topic.split('/')[2].split('_')[0] on_message(client, userdata, msg)
except IndexError: except Exception as error:
logging.error("Error - Cannot get vin from MQTT topic!") logging.error(f"Exception {error} processing received message on topic {msg.topic}")
return None return None
def on_message(client, userdata, msg):
payload = msg.payload.decode("UTF-8") payload = msg.payload.decode("UTF-8")
if msg.topic == otp_mqtt_topic:
if msg.retain == 0:
global otp_code
otp_code = payload
set_otp_state()
else:
logging.warning("Found retained OTP, this can't work! Please clean retained messages!")
return None
else:
try:
vin = msg.topic.split('/')[2].split('_')[0]
except IndexError:
logging.error(f"Error - Cannot get vin from MQTT topic : {msg.topic}!")
return None
if "climate_status" in msg.topic: if "climate_status" in msg.topic:
if payload == "ON": if payload == "ON":
start_climate(vin) start_climate(vin)
return None
elif payload == "OFF": elif payload == "OFF":
stop_climate(vin) stop_climate(vin)
return None
return None
elif "lock_status" in msg.topic: elif "lock_status" in msg.topic:
if payload == "LOCK": if payload == "LOCK":
lock_car(vin) lock_car(vin)
return None
elif payload == "UNLOCK": elif payload == "UNLOCK":
unlock_car(vin) unlock_car(vin)
return None
return None
elif "update_data" in msg.topic: elif "update_data" in msg.topic:
if payload == "PRESS": if payload == "PRESS":
update_car_data(True) update_car_data(True)
return None
return None
elif "update_interval" in msg.topic: elif "update_interval" in msg.topic:
update_interval = int(payload) try:
if update_interval >= 60 or update_interval == -1: update_interval = int(payload)
settings.update({"updateInterval": int(update_interval)}) if update_interval >= 60 or update_interval == -1:
else: settings.update({"updateInterval": int(update_interval)})
logging.warning("Interval " + str(update_interval) + " seconds is to low. Doing nothing!") else:
update_car_data() logging.warning(f"Interval {update_interval} seconds is to low. Doing nothing!")
update_car_data()
return None
except ValueError as error:
logging.error(f"Unable to change update_interval {error} payload={payload}")
return None
elif "schedule" in msg.topic: elif "schedule" in msg.topic:
try: try:
d = json.loads(payload) d = json.loads(payload)
except ValueError as e: except ValueError as e:
logging.error("Can't set timer. Error: " + str(e)) logging.error(f"Can't set timer. Error: {e}, payload={payload}")
return None return None
if d["mode"] == "timer": if d["mode"] == "timer":
start_climate_timer(d, vin) start_climate_timer(d, vin)
return None
else: else:
logging.warning("No schedule mode found, doing nothing") logging.warning("No schedule mode found, doing nothing")
return None
return None
def start_climate_timer(d, vin): def start_climate_timer(d, vin):
@@ -143,13 +237,13 @@ def start_climate_timer(d, vin):
start_datetime = local_datetime.replace(hour=hour, minute=minute, second=0) start_datetime = local_datetime.replace(hour=hour, minute=minute, second=0)
timer_seconds = (start_datetime - local_datetime).total_seconds() timer_seconds = (start_datetime - local_datetime).total_seconds()
except Exception as e: except Exception as e:
logging.error("Error creating climate timer: " + str(e)) logging.error(f"Error creating climate timer: {e}")
return None return None
if timer_seconds > 0: if timer_seconds > 0:
Timer(timer_seconds, activate_climate_timer, (vin, start_datetime.isoformat(),)).start() Timer(timer_seconds, activate_climate_timer, (vin, start_datetime.isoformat(),)).start()
active_schedules[vin]["timers"].append(start_datetime.isoformat()) active_schedules[vin]["timers"].append(start_datetime.isoformat())
logging.debug("Climate timer set to " + str(start_datetime)) logging.debug(f"Climate timer set to {start_datetime}")
update_car_data() update_car_data()
else: else:
logging.warning("Timer can not be set. Unusable start time entered") logging.warning("Timer can not be set. Unusable start time entered")
@@ -157,28 +251,28 @@ def start_climate_timer(d, vin):
def unlock_car(vin): def unlock_car(vin):
# Start the api call in another thread for HA performance # Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin)).start() Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin), name="unlock_car_thread").start()
# Force set unlocking state # Force set unlocking state
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"}) update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"})
# Fetch API lock state until unlocking finished # Fetch API lock state until unlocking finished
Thread(target=volvo.check_lock_status, args=(vin, "LOCKED")).start() Thread(target=volvo.check_lock_status, args=(vin, "LOCKED"), name="check_lock_status_thread").start()
def lock_car(vin): def lock_car(vin):
# Start the api call in another thread for HA performance # Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin)).start() Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin), name="lock_car_thread").start()
# Force set locking state # Force set locking state
update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"}) update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"})
# Fetch API lock state until locking finished # Fetch API lock state until locking finished
Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED")).start() Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED"), name="check_lock_status_thread").start()
def stop_climate(vin): def stop_climate(vin):
global assumed_climate_state, climate_timer, engine_status global assumed_climate_state, climate_timer, engine_status
# Start the api call in another thread for HA performance # Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start() Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin), name="stop_climate_thread").start()
# Stop door check thread if running # Stop door check thread if running
if engine_status[vin].is_alive(): if engine_status[vin].is_alive():
@@ -202,10 +296,10 @@ def activate_climate_timer(vin, start_time):
def start_climate(vin): def start_climate(vin):
global assumed_climate_state, climate_timer, engine_status global assumed_climate_state, climate_timer, engine_status
# Start the api call in another thread for HA performance # Start the api call in another thread for HA performance
Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start() Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin), name="start_climate_thread").start()
# Start door check thread to turn off climate if driver door is opened # Start door check thread to turn off climate if driver door is opened
check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin,)) check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin,), name="check_engine_status_thread")
check_engine_thread.start() check_engine_thread.start()
engine_status[vin] = check_engine_thread engine_status[vin] = check_engine_thread
@@ -223,9 +317,12 @@ def update_loop():
while True: while True:
if settings["updateInterval"] > 0: if settings["updateInterval"] > 0:
logging.info("Sending mqtt update...") logging.info("Sending mqtt update...")
send_heartbeat() try:
update_car_data() send_heartbeat()
logging.info("Mqtt update done. Next run in " + str(settings["updateInterval"]) + " seconds.") update_car_data()
logging.info(f"Mqtt update done. Next run in {settings['updateInterval']} seconds.")
except Exception as error:
logging.info(f"Exception {error} in Mqtt update. Next run in {settings['updateInterval']} seconds.")
time.sleep(settings["updateInterval"]) time.sleep(settings["updateInterval"])
else: else:
logging.info("Data update is disabled, doing nothing for 30 seconds") logging.info("Data update is disabled, doing nothing for 30 seconds")
@@ -272,7 +369,8 @@ def update_car_data(force_update=False, overwrite={}):
elif entity["id"] == "warnings": elif entity["id"] == "warnings":
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes", f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes",
json.dumps(state) json.dumps(state),
retain=True
) )
if state: if state:
state = sum(value == "FAILURE" for value in state.values()) state = sum(value == "FAILURE" for value in state.values())
@@ -283,10 +381,12 @@ def update_car_data(force_update=False, overwrite={}):
else: else:
topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state" topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state"
logging.info(f"update_car_data {topic} {state}")
if state or state == 0: if state or state == 0:
mqtt_client.publish( mqtt_client.publish(
topic, topic,
json.dumps(state) if isinstance(state, dict) or isinstance(state, list) else state json.dumps(state) if isinstance(state, dict) or isinstance(state, list) else state,
retain=True
) )
update_ha_device(entity, vin, state) update_ha_device(entity, vin, state)
@@ -311,7 +411,7 @@ def update_ha_device(entity, vin, state):
logging.debug("Updating icon to " + icon + " for " + entity["id"]) logging.debug("Updating icon to " + icon + " for " + entity["id"])
config = { config = {
"name": entity['name'], "name": entity['name'],
"object_id": f"volvo_{vin}_{entity['id']}", "default_entity_id": f"volvo_{vin}_{entity['id']}",
"schema": "state", "schema": "state",
"icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}", "icon": f"mdi:{icon}" if icon else f"mdi:{entity['icon']}",
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state", "state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
@@ -340,6 +440,7 @@ def update_ha_device(entity, vin, state):
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config", f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
json.dumps(config), json.dumps(config),
qos=1,
retain=True retain=True
) )
@@ -352,7 +453,7 @@ def create_ha_devices():
for entity in volvo.supported_endpoints[vin]: for entity in volvo.supported_endpoints[vin]:
config = { config = {
"name": entity['name'], "name": entity['name'],
"object_id": f"volvo_{vin}_{entity['id']}", "default_entity_id": f"volvo_{vin}_{entity['id']}",
"schema": "state", "schema": "state",
"icon": f"mdi:{entity['icon']}", "icon": f"mdi:{entity['icon']}",
"state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state", "state_topic": f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state",
@@ -390,18 +491,20 @@ def create_ha_devices():
mqtt_client.publish( mqtt_client.publish(
f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config", f"homeassistant/{entity['domain']}/volvoAAOS2mqtt/{vin}_{entity['id']}/config",
json.dumps(config), json.dumps(config),
qos=1,
retain=True retain=True
) )
time.sleep(0.05)
time.sleep(2) time.sleep(2)
send_heartbeat() send_heartbeat()
def send_heartbeat(): def send_heartbeat():
mqtt_client.publish(availability_topic, "online") mqtt_client.publish(availability_topic, "online", retain=True)
def send_offline(): def send_offline():
mqtt_client.publish(availability_topic, "offline") mqtt_client.publish(availability_topic, "offline", retain=True)
def delete_old_entities(): def delete_old_entities():
+3 -5
View File
@@ -8,15 +8,13 @@
"broker": "", "broker": "",
"port": 1883, "port": 1883,
"username": "", "username": "",
"password": "" "password": "",
"logging": false
}, },
"volvoData": { "volvoData": {
"username": "", "username": "",
"password": "", "password": "",
"vin": "", "vin": "",
"vccapikey": [], "vccapikey": []
"odometerMultiplier": "",
"averageSpeedDivider": "",
"averageFuelConsumptionMultiplier": ""
} }
} }
+1 -1
View File
@@ -19,4 +19,4 @@ configuration:
description: Leave the settings as they are if you are using the MQTT Mosquitto Addon. If not, take a look at the readme from volvo2mqtt. description: Leave the settings as they are if you are using the MQTT Mosquitto Addon. If not, take a look at the readme from volvo2mqtt.
volvoData: volvoData:
name: Volvo configuration options name: Volvo configuration options
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. This setting works as list, so it is possible to define multiple VCCAPI keys. Odometer multiplier is sometimes 10, sometimes 1. The same is applicable for average speed divider and average fuel consumption. Leave it as it is if you don't know what's right. 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. This setting works as list, so it is possible to define multiple VCCAPI keys. Take a look at the GitHub repo for more information!
+30 -5
View File
@@ -4,6 +4,8 @@ import os
import re import re
import sys import sys
import config import config
import json
from google_play_scraper import app
from logging import handlers from logging import handlers
from datetime import datetime from datetime import datetime
from const import units from const import units
@@ -28,11 +30,34 @@ class SensitiveDataFilter(logging.Filter):
record.msg = re.sub(pattern, "<REDACTED>", record.msg) record.msg = re.sub(pattern, "<REDACTED>", record.msg)
return True return True
def get_volvo_app_version():
result = app(
'se.volvo.vcc',
lang='en',
country='us'
)
if "version" in result:
return result["version"]
else:
return "5.37.0"
def get_token_path():
token_path = ".token"
if os.environ.get("IS_HA_ADDON"):
check_existing_folder("/addons/volvo2mqtt/token/")
token_path = "/addons/volvo2mqtt/token/.token"
return token_path
def save_to_json(data, token_path):
with open(token_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def get_icon_between(icon_list, state): def get_icon_between(icon_list, state):
icon = None icon = None
for s in icon_list: for s in icon_list:
if s["to"] <= state <= s["from"]: if s["to"] <= state < s["from"]:
icon = s["icon"] icon = s["icon"]
return icon return icon
@@ -40,13 +65,13 @@ def get_icon_between(icon_list, state):
def setup_logging(): def setup_logging():
log_location = "volvo2mqtt.log" log_location = "volvo2mqtt.log"
if os.environ.get("IS_HA_ADDON"): if os.environ.get("IS_HA_ADDON"):
check_existing_folder() check_existing_folder("/addons/volvo2mqtt/log/")
log_location = "/addons/volvo2mqtt/log/volvo2mqtt.log" log_location = "/addons/volvo2mqtt/log/volvo2mqtt.log"
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple() logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1) file_log_handler = logging.handlers.RotatingFileHandler(log_location, maxBytes=1000000, backupCount=1)
formatter = logging.Formatter( formatter = logging.Formatter(
'%(asctime)s volvo2mqtt [%(process)d] - %(levelname)s: %(message)s', '%(asctime)s volvo2mqtt [%(threadName)s] [%(process)d] - %(levelname)s: %(message)s',
'%b %d %H:%M:%S') '%b %d %H:%M:%S')
file_log_handler.setFormatter(formatter) file_log_handler.setFormatter(formatter)
@@ -73,8 +98,8 @@ def setup_logging():
logger.setLevel(logging.ERROR) logger.setLevel(logging.ERROR)
def check_existing_folder(): def check_existing_folder(path):
Path("/addons/volvo2mqtt/log/").mkdir(parents=True, exist_ok=True) Path(path).mkdir(parents=True, exist_ok=True)
def keys_exists(element, *keys): def keys_exists(element, *keys):
+411 -221
View File
@@ -1,18 +1,21 @@
import json
import logging import logging
import requests import requests
import mqtt import mqtt
import util import util
import time import time
import re import re
import os.path
from threading import current_thread, Thread from threading import current_thread, Thread
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 json import JSONDecodeError from json import JSONDecodeError
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_AUTH_URL, OAUTH_TOKEN_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, \ WINDOWS_STATE_URL, LOCK_STATE_URL, TYRE_STATE_URL, supported_entities, FUEL_BATTERY_STATE_URL, \
STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, VEHICLE_DIAGNOSTICS_URL, API_BACKEND_STATUS, WARNINGS_URL, engine_states STATISTICS_URL, ENGINE_DIAGNOSTICS_URL, VEHICLE_DIAGNOSTICS_URL, API_BACKEND_STATUS, WARNINGS_URL, engine_states, \
otp_max_loops
session = requests.Session() session = requests.Session()
session.headers = { session.headers = {
@@ -30,40 +33,151 @@ vcc_api_keys = []
backend_status = "" backend_status = ""
def authorize(): def authorize(renew_tokenfile=False):
headers = { global refresh_token
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded", token_path = util.get_token_path()
"accept": "application/json" if os.path.isfile(token_path) and not renew_tokenfile:
} logging.info("Using login from token file")
f = open(token_path)
try:
data = json.load(f)
refresh_token = data["refresh_token"]
refresh_auth()
except ValueError:
logging.warning("Detected corrupted token file, restarting auth process")
authorize(True)
return
except KeyError:
logging.warning("Token file missing refresh_token, restarting auth process")
authorize(True)
return
else:
logging.info("Starting login with OTP")
auth_session = requests.session()
auth_session.headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"User-Agent": "vca-android/" + util.get_volvo_app_version(),
"Accept-Encoding": "gzip",
"Content-Type": "application/json; charset=utf-8"
}
url_params = ("?client_id=h4Yf0b"
"&response_type=code"
"&acr_values=urn:volvoid:aal:bronze:2sv"
"&response_mode=pi.flow"
"&scope=openid email profile care_by_volvo:financial_information:invoice:read care_by_volvo:financial_information:payment_method care_by_volvo:subscription:read customer:attributes customer:attributes:write order:attributes vehicle:attributes tsp_customer_api:all conve:brake_status conve:climatization_start_stop conve:command_accessibility conve:commands conve:diagnostics_engine_status conve:diagnostics_workshop conve:doors_status conve:engine_status conve:environment conve:fuel_status conve:honk_flash conve:lock conve:lock_status conve:navigation conve:odometer_status conve:trip_statistics conve:tyre_status conve:unlock conve:vehicle_relation conve:warnings conve:windows_status energy:battery_charge_level energy:charging_connection_status energy:charging_system_status energy:electric_range energy:estimated_charging_time energy:recharge_status vehicle:attributes")
auth = auth_session.get(OAUTH_AUTH_URL + url_params)
if auth.status_code == 200:
response = auth.json()
auth_state = response["status"]
if auth_state == "USERNAME_PASSWORD_REQUIRED":
auth_session.headers.update({"x-xsrf-header": "PingFederate"})
response = check_username_password(auth_session, response)
auth_state = response["status"]
if auth_state == "OTP_REQUIRED":
response = send_otp(auth_session, response)
response = continue_auth(auth_session, response)
token_data = get_token(auth_session, response)
else:
raise Exception("Unkown auth state " + auth_state)
elif auth_state == "OTP_REQUIRED":
response = send_otp(auth_session, response)
response = continue_auth(auth_session, response)
token_data = get_token(auth_session, response)
elif auth_state == "OTP_VERIFIED":
response = continue_auth(auth_session, response)
token_data = get_token(auth_session, response)
elif auth_state == "COMPLETED":
token_data = get_token(auth_session, response)
else:
raise Exception("Unkown auth state " + auth_state)
session.headers.update({"authorization": "Bearer " + token_data["access_token"]})
global token_expires_at
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(token_data["expires_in"] - 30))
refresh_token = token_data["refresh_token"]
util.save_to_json(token_data, token_path)
else:
message = auth.json()
raise Exception(message["details"][0]["userMessage"])
get_vcc_api_keys()
get_vehicles()
check_supported_endpoints()
Thread(target=backend_status_loop,name="backend_status_thread").start()
def continue_auth(auth_session, data):
next_url = data["_links"]["continueAuthentication"]["href"].replace("http://", "https://") + "?action=continueAuthentication"
auth = auth_session.post(next_url, data="{}")
body = {
"username": settings.volvoData["username"],
"password": settings.volvoData["password"],
"grant_type": "password",
"scope": "openid email profile care_by_volvo:financial_information:invoice:read care_by_volvo:financial_information:payment_method care_by_volvo:subscription:read customer:attributes customer:attributes:write order:attributes vehicle:attributes tsp_customer_api:all conve:brake_status conve:climatization_start_stop conve:command_accessibility conve:commands conve:diagnostics_engine_status conve:diagnostics_workshop conve:doors_status conve:engine_status conve:environment conve:fuel_status conve:honk_flash conve:lock conve:lock_status conve:navigation conve:odometer_status conve:trip_statistics conve:tyre_status conve:unlock conve:vehicle_relation conve:warnings conve:windows_status energy:battery_charge_level energy:charging_connection_status energy:charging_system_status energy:electric_range energy:estimated_charging_time energy:recharge_status vehicle:attributes"
}
auth = requests.post(OAUTH_URL, data=body, headers=headers)
if auth.status_code == 200: if auth.status_code == 200:
data = auth.json() return auth.json()
session.headers.update({"authorization": "Bearer " + data["access_token"]})
global token_expires_at, refresh_token
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"]
get_vcc_api_keys()
get_vehicles()
check_supported_endpoints()
Thread(target=backend_status_loop).start()
else: else:
message = auth.json() message = auth.json()
raise Exception(message["details"][0]["userMessage"])
def get_token(auth_session, data):
auth_session.headers.update({"content-type": "application/x-www-form-urlencoded"})
body = {"code": data["authorizeResponse"]["code"], "grant_type": "authorization_code"}
token_auth = auth_session.post(OAUTH_TOKEN_URL, data=body)
if token_auth.status_code == 200:
return token_auth.json()
else:
message = token_auth.json()
raise Exception(message["error_description"]) raise Exception(message["error_description"])
def check_username_password(auth_session, data):
next_url = data["_links"]["checkUsernamePassword"]["href"].replace("http://", "https://") + "?action=checkUsernamePassword"
body = {"username": settings.volvoData["username"],
"password": settings.volvoData["password"]}
auth = auth_session.post(next_url, data=json.dumps(body))
if auth.status_code == 200:
return auth.json()
else:
message = auth.json()
raise Exception(message["details"][0]["userMessage"])
def send_otp(auth_session, data):
mqtt.create_otp_input()
next_url = data["_links"]["checkOtp"]["href"].replace("http://", "https://") + "?action=checkOtp"
body = {"otp": ""}
for i in range(otp_max_loops):
if mqtt.otp_code:
body["otp"] = mqtt.otp_code
break
logging.info("Waiting for otp code... Please check your mailbox and post your otp code to the following "
"mqtt topic \"volvoAAOS2mqtt/otp_code\". Retry " + str(i) + "/" + str(otp_max_loops))
time.sleep(5)
if not mqtt.otp_code:
raise Exception ("No OTP found, exting...")
auth = auth_session.post(next_url, data=json.dumps(body))
mqtt.otp_code = None
if auth.status_code == 200:
return auth.json()
else:
message = auth.json()
raise Exception(message["details"][0]["userMessage"])
def refresh_auth(): def refresh_auth():
logging.info("Refreshing credentials")
global refresh_token global refresh_token
logging.info("Refreshing credentials with token " + refresh_token)
headers = { headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=", "authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"content-type": "application/x-www-form-urlencoded", "content-type": "application/x-www-form-urlencoded",
@@ -76,18 +190,45 @@ def refresh_auth():
} }
try: try:
auth = requests.post(OAUTH_URL, data=body, headers=headers) auth = requests.post(OAUTH_TOKEN_URL, data=body, headers=headers)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logging.error("Error refreshing credentials data: " + str(e)) logging.error("Error refreshing credentials data: " + str(e))
return None return None
if auth.status_code == 200: if auth.status_code == 200:
data = auth.json() token_path = util.get_token_path()
session.headers.update({"authorization": "Bearer " + data["access_token"]}) if os.path.exists(token_path):
with open(token_path) as f:
data = json.load(f)
refresh_data = auth.json()
if not "refresh_token" in refresh_data:
f = open(token_path)
try:
data = json.load(f)
refresh_data["refresh_token"] = data["refresh_token"]
except ValueError:
raise Exception("Cannot refresh token!")
else:
logging.info("New refresh token found, updating!")
util.save_to_json(refresh_data, token_path)
session.headers.update({"authorization": "Bearer " + refresh_data["access_token"]})
global token_expires_at global token_expires_at
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30)) token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(data["expires_in"] - 30))
refresh_token = data["refresh_token"] refresh_token = data["refresh_token"]
return None
elif int(auth.status_code / 100) == 5:
# Server error try again later
logging.warning("Refreshing credentials failed!: " + str(auth.status_code) + " Message: " + auth.text)
return None
else:
# TODO check other codes that dont have to force the renew_tokenfile
logging.warning("Refreshing credentials failed!: " + str(auth.status_code) + " Message: " + auth.text)
authorize(renew_tokenfile=True)
return None
def get_vehicles(): def get_vehicles():
@@ -200,9 +341,17 @@ def check_vcc_api_key(test_key, extended_until=None):
logging.warning("VCCAPIKEY " + test_key + " is extended and will be reusable at: " logging.warning("VCCAPIKEY " + test_key + " is extended and will be reusable at: "
+ format_datetime(extended_until, format="medium", locale=settings["babelLocale"])) + format_datetime(extended_until, format="medium", locale=settings["babelLocale"]))
else: else:
logging.warning("VCCAPIKEY " + test_key + " isn't working! " + data["error"]["message"]) if "error" in data:
logging.warning("VCCAPIKEY " + test_key + " isn't working! " + data["error"]["message"])
else:
logging.warning("VCCAPIKEY " + test_key + " isn't working! Statuscode " + str(response.status_code) +
" Message: " + response.text)
else: else:
logging.warning("VCCAPIKEY " + test_key + " isn't working! " + data["error"]["message"]) if "error" in data:
logging.warning("VCCAPIKEY " + test_key + " isn't working! " + data["error"]["message"])
else:
logging.warning("VCCAPIKEY " + test_key + " isn't working! Statuscode " + str(response.status_code) +
" Message: " + response.text)
return True, extended_until return True, extended_until
@@ -313,31 +462,44 @@ def check_engine_status(vin):
mqtt.assumed_climate_state[vin] = "OFF" mqtt.assumed_climate_state[vin] = "OFF"
mqtt.update_car_data() mqtt.update_car_data()
break break
time.sleep(5) time.sleep(5)
return None
def backend_status_loop(): def backend_status_loop():
logging.debug("backend_status_loop started")
while True: while True:
get_backend_status() last_status = get_backend_status()
# Update every hour # Update every hour when not error, 10 minutes when got server error
time.sleep(3600) time.sleep(3600 if not last_status.startswith("APIERR_") else 600)
def get_backend_status(): def get_backend_status():
global backend_status global backend_status
response = session.get(API_BACKEND_STATUS, timeout=15)
try: try:
data = response.json() response = session.get(API_BACKEND_STATUS, timeout=15)
if util.keys_exists(data, "message"): try:
if data["message"]: data = response.json()
backend_status = data["message"] if util.keys_exists(data, "message"):
if data["message"]:
backend_status = data["message"]
else:
backend_status = "NO_WARNING"
else: else:
backend_status = "NO_WARNING" backend_status = "NO_WARNING"
else:
backend_status = "NO_WARNING"
except JSONDecodeError as e: except JSONDecodeError as e:
backend_status = "NO_WARNING" logging.error(f"Invalid json response from API Backend Status - {response} - {e}")
backend_status = "APIERR_INVALID_JSON_RESPONSE"
except requests.exceptions.ReadTimeout as e:
logging.error(f"API Backend Status - timeout - {e}")
backend_status = "APIERR_TIMEOUT"
except requests.exceptions.RequestException as e:
logging.error(f"API Backend Status - Error - {e}")
backend_status = "APIERR_REQUEST_ERROR"
return backend_status return backend_status
@@ -353,28 +515,28 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
# 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":
logging.debug("Starting " + method + " call against " + url) logging.debug(f"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:
logging.error("Error getting data: " + str(e)) logging.error(f"Error getting data: {e} from {url}")
return None return None
elif method == "POST": elif method == "POST":
logging.debug("Starting " + method + " call against " + url) logging.debug(f"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:
logging.error("Error getting data: " + str(e)) logging.error(f"Error getting data from {url} : {e}")
return None return None
else: else:
logging.error("Unkown method posted: " + method + ". Returning nothing") logging.error(f"Unkown method posted: {method}. Returning nothing")
return None return None
logging.debug("Response status code: " + str(response.status_code)) logging.debug(f"Response status code: {response.status_code}")
try: try:
data = response.json() data = response.json()
except JSONDecodeError as e: except JSONDecodeError as e:
logging.error("Fetched json decode error, Volvo API seems to return garbage. Skipping update. Error: " + str(e)) logging.error(f"Fetched json decode error, Volvo API seems to return garbage. Skipping update. Error: {e}")
return None return None
if response.status_code == 200: if response.status_code == 200:
@@ -397,9 +559,9 @@ def api_call(url, method, vin, sensor_id=None, force_update=False, key_change=Fa
api_call(url, method, vin, sensor_id, force_update, True) api_call(url, method, vin, sensor_id, force_update, True)
else: else:
logging.error( logging.error(
"API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text) f"API Call failed to {url}. Status Code: {response.status_code}. Error: {response.text}")
else: else:
logging.error("API Call failed. Status Code: " + str(response.status_code) + ". Error: " + response.text) logging.error(f"API Call failed to {url}. Status Code: {response.status_code}. Error: {response.text}")
return None return None
@@ -407,11 +569,11 @@ def cached_request(url, method, vin, force_update=False, key_change=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
logging.debug("Starting " + method + " call against " + url) logging.debug(f"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:
logging.error("Error getting data: " + str(e)) logging.error(f"Error getting data from {url}: {e}")
return None return None
data = {"response": response, "last_update": datetime.now(util.TZ)} data = {"response": response, "last_update": datetime.now(util.TZ)}
@@ -423,11 +585,11 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
"last_update"]).total_seconds() >= 2) \ "last_update"]).total_seconds() >= 2) \
or key_change: or key_change:
# Old Data in Cache, or force mode active, updating # Old Data in Cache, or force mode active, updating
logging.debug("Starting " + method + " call against " + url) logging.debug(f"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:
logging.error("Error getting data: " + str(e)) logging.error(f"Error getting data from {url} : {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
@@ -438,174 +600,202 @@ def cached_request(url, method, vin, force_update=False, key_change=False):
def parse_api_data(data, sensor_id=None): def parse_api_data(data, sensor_id=None):
if sensor_id != "api_backend_status": if sensor_id in ["battery_charge_level", "electric_range", "charging_system_status",
data = data["data"] "charging_connection_status", "estimated_charging_time", "estimated_charging_finish_time"]:
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 util.convert_metric_values(data["electricRange"]["value"]) \ return util.convert_metric_values(data["electricRange"]["value"]) \
if util.keys_exists(data, "electricRange") else None 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"]] \ return charging_system_states[data["chargingStatus"]["value"]] \
if util.keys_exists(data, "chargingSystemStatus") else None if util.keys_exists(data, "chargingStatus") else None
elif sensor_id == "charging_connection_status": elif sensor_id == "charging_connection_status":
return charging_connection_states[data["chargingConnectionStatus"]["value"]] \ return charging_connection_states[data["chargerConnectionStatus"]["value"]] \
if util.keys_exists(data, "chargingConnectionStatus") else None if util.keys_exists(data, "chargerConnectionStatus") 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, "chargingStatus"):
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]] charging_system_state = charging_system_states[data["chargingStatus"]["value"]]
if charging_system_state == "Charging": if charging_system_state == "Charging":
return data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") else "" return data["estimatedChargingTimeToTargetBatteryChargeLevel"]["value"] if util.keys_exists(data,
else: "estimatedChargingTimeToTargetBatteryChargeLevel") else ""
return 0 else:
return 0
return None
elif sensor_id == "estimated_charging_finish_time":
if util.keys_exists(data, "estimatedChargingTimeToTargetBatteryChargeLevel"):
charging_system_state = charging_system_states[data["chargingStatus"]["value"]]
if charging_system_state == "Charging":
charging_time = int(
data["estimatedChargingTimeToTargetBatteryChargeLevel"]["value"] if util.keys_exists(data, "estimatedChargingTimeToTargetBatteryChargeLevel")
else 0)
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time)
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"])
else:
return ""
return None
return None return None
elif sensor_id == "estimated_charging_finish_time": else:
if util.keys_exists(data, "chargingSystemStatus"): if sensor_id != "api_backend_status":
charging_system_state = charging_system_states[data["chargingSystemStatus"]["value"]] data = data["data"]
if charging_system_state == "Charging":
charging_time = int( if sensor_id == "lock_status":
data["estimatedChargingTime"]["value"] if util.keys_exists(data, "estimatedChargingTime") return data["centralLock"]["value"] if util.keys_exists(data, "centralLock") else None
else 0) elif sensor_id == "battery_capacity":
charging_finished = datetime.now(util.TZ) + timedelta(minutes=charging_time) return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") else None
return format_datetime(charging_finished, format="medium", locale=settings["babelLocale"]) elif sensor_id == "odometer":
else: return util.convert_metric_values(int(data["odometer"]["value"])) \
return "" if util.keys_exists(data, "odometer") else None
return None elif sensor_id == "window_front_left":
elif sensor_id == "lock_status": return window_states[data["frontLeftWindow"]["value"]] if util.keys_exists(data, "frontLeftWindow") \
return data["centralLock"]["value"] if util.keys_exists(data, "centralLock") else None else None
elif sensor_id == "odometer": elif sensor_id == "window_front_right":
multiplier = 1 return window_states[data["frontRightWindow"]["value"]] if util.keys_exists(data, "frontRightWindow") \
if util.keys_exists(settings["volvoData"], "odometerMultiplier"): else None
multiplier = settings["volvoData"]["odometerMultiplier"] elif sensor_id == "window_rear_left":
if isinstance(multiplier, str): return window_states[data["rearLeftWindow"]["value"]] if util.keys_exists(data, "rearLeftWindow") \
multiplier = 1 else None
elif multiplier < 1: elif sensor_id == "window_rear_right":
multiplier = 1 return window_states[data["rearRightWindow"]["value"]] if util.keys_exists(data, "rearRightWindow") \
return util.convert_metric_values(int(data["odometer"]["value"]) * multiplier) \ else None
if util.keys_exists(data, "odometer") else None elif sensor_id == "door_front_left":
elif sensor_id == "window_front_left": return door_states[data["frontLeftDoor"]["value"]] if util.keys_exists(data, "frontLeftDoor") else None
return window_states[data["frontLeftWindow"]["value"]] if util.keys_exists(data, "frontLeftWindow") \ elif sensor_id == "door_front_right":
else None return door_states[data["frontRightDoor"]["value"]] \
elif sensor_id == "window_front_right": if util.keys_exists(data, "frontRightDoor") else None
return window_states[data["frontRightWindow"]["value"]] if util.keys_exists(data, "frontRightWindow") \ elif sensor_id == "door_rear_left":
else None return door_states[data["rearLeftDoor"]["value"]] if util.keys_exists(data, "rearLeftDoor") else None
elif sensor_id == "window_rear_left": elif sensor_id == "door_rear_right":
return window_states[data["rearLeftWindow"]["value"]] if util.keys_exists(data, "rearLeftWindow") \ return door_states[data["rearRightDoor"]["value"]] if util.keys_exists(data, "rearRightDoor") else None
else None elif sensor_id == "tailgate":
elif sensor_id == "window_rear_right": return door_states[data["tailgate"]["value"]] if util.keys_exists(data, "tailgate") else None
return window_states[data["rearRightWindow"]["value"]] if util.keys_exists(data, "rearRightWindow") \ elif sensor_id == "sunroof":
else None return door_states[data["sunroof"]["value"]] if util.keys_exists(data, "sunroof") else None
elif sensor_id == "door_front_left": elif sensor_id == "engine_hood":
return door_states[data["frontLeftDoor"]["value"]] if util.keys_exists(data, "frontLeftDoor") else None return door_states[data["hood"]["value"]] if util.keys_exists(data, "hood") else None
elif sensor_id == "door_front_right": elif sensor_id == "tank_lid":
return door_states[data["frontRightDoor"]["value"]] \ return door_states[data["tankLid"]["value"]] if util.keys_exists(data, "tankLid") else None
if util.keys_exists(data, "frontRightDoor") else None elif sensor_id == "tyre_front_left":
elif sensor_id == "door_rear_left": return data["frontLeft"]["value"] if util.keys_exists(data, "frontLeft") else None
return door_states[data["rearLeftDoor"]["value"]] if util.keys_exists(data, "rearLeftDoor") else None elif sensor_id == "tyre_front_right":
elif sensor_id == "door_rear_right": return data["frontRight"]["value"] if util.keys_exists(data, "frontRight") else None
return door_states[data["rearRightDoor"]["value"]] if util.keys_exists(data, "rearRightDoor") else None elif sensor_id == "tyre_rear_left":
elif sensor_id == "tailgate": return data["rearLeft"]["value"] if util.keys_exists(data, "rearLeft") else None
return door_states[data["tailgate"]["value"]] if util.keys_exists(data, "tailgate") else None elif sensor_id == "tyre_rear_right":
elif sensor_id == "sunroof": return data["rearRight"]["value"] if util.keys_exists(data, "rearRight") else None
return door_states[data["sunroof"]["value"]] if util.keys_exists(data, "sunroof") else None elif sensor_id == "engine_state":
elif sensor_id == "engine_hood": return engine_states[data["engineStatus"]["value"]] if util.keys_exists(data, "engineStatus") else None
return door_states[data["hood"]["value"]] if util.keys_exists(data, "hood") else None elif sensor_id == "fuel_level":
elif sensor_id == "tank_lid": if util.keys_exists(data, "fuelAmount"):
return door_states[data["tankLid"]["value"]] if util.keys_exists(data, "tankLid") else None fuel_amount = float(data["fuelAmount"]["value"])
elif sensor_id == "tyre_front_left": if fuel_amount > 0:
return data["frontLeft"]["value"] if util.keys_exists(data, "frontLeft") else None return fuel_amount
elif sensor_id == "tyre_front_right": return None
return data["frontRight"]["value"] if util.keys_exists(data, "frontRight") else None elif sensor_id == "average_fuel_consumption":
elif sensor_id == "tyre_rear_left": average_fuel_con = 0
return data["rearLeft"]["value"] if util.keys_exists(data, "rearLeft") else None if util.keys_exists(data, "averageFuelConsumption"):
elif sensor_id == "tyre_rear_right": average_fuel_con = float(data["averageFuelConsumption"]["value"])
return data["rearRight"]["value"] if util.keys_exists(data, "rearRight") else None elif util.keys_exists(data, "averageFuelConsumptionAutomatic"):
elif sensor_id == "engine_state": average_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
return engine_states[data["engineStatus"]["value"]] if util.keys_exists(data, "engineStatus") else None
elif sensor_id == "fuel_level":
if util.keys_exists(data, "fuelAmount"):
fuel_amount = float(data["fuelAmount"]["value"])
if fuel_amount > 0:
return fuel_amount
return None
elif sensor_id == "average_fuel_consumption":
if util.keys_exists(data, "averageFuelConsumption"):
average_fuel_con = float(data["averageFuelConsumption"]["value"])
if average_fuel_con > 0: if average_fuel_con > 0:
multiplier = 1 return average_fuel_con
if util.keys_exists(settings["volvoData"], "averageFuelConsumptionMultiplier"): return None
multiplier = settings["volvoData"]["averageFuelConsumptionMultiplier"] elif sensor_id == "average_speed":
if isinstance(multiplier, str): average_speed = 0
multiplier = 1 if util.keys_exists(data, "averageSpeed"):
elif multiplier < 1: average_speed = float(data["averageSpeed"]["value"])
multiplier = 1 elif util.keys_exists(data, "averageSpeedAutomatic"):
return average_fuel_con * multiplier average_speed = float(data["averageSpeedAutomatic"]["value"])
return None
elif sensor_id == "average_speed": if average_speed != 0:
if util.keys_exists(data, "averageSpeed"): return util.convert_metric_values(average_speed)
average_speed = float(data["averageSpeed"]["value"])
if average_speed > 1: return None
divider = 1 elif sensor_id == "location":
if util.keys_exists(settings["volvoData"], "averageSpeedDivider"): coordinates = {}
divider = settings["volvoData"]["averageSpeedDivider"] if util.keys_exists(data, "geometry"):
if isinstance(divider, str): raw_data = data["geometry"]
divider = 1 if util.keys_exists(raw_data, "coordinates"):
elif divider < 1: coordinates = {"longitude": raw_data["coordinates"][0],
divider = 1 "latitude": raw_data["coordinates"][1],
return util.convert_metric_values(average_speed / divider) "gps_accuracy": 1}
return None return coordinates
elif sensor_id == "location": elif sensor_id == "distance_to_empty_tank":
coordinates = {} if util.keys_exists(data, "distanceToEmptyTank"):
if util.keys_exists(data, "geometry"): distance_to_empty = int(data["distanceToEmptyTank"]["value"])
raw_data = data["geometry"] if distance_to_empty > 0:
if util.keys_exists(raw_data, "coordinates"): return util.convert_metric_values(data["distanceToEmptyTank"]["value"])
coordinates = {"longitude": raw_data["coordinates"][0], return None
"latitude": raw_data["coordinates"][1], elif sensor_id == "distance_to_empty_battery":
"gps_accuracy": 1} if util.keys_exists(data, "distanceToEmptyBattery"):
return coordinates distance_to_empty = int(data["distanceToEmptyBattery"]["value"])
elif sensor_id == "distance_to_empty_tank": if distance_to_empty > 0:
if util.keys_exists(data, "distanceToEmptyTank"): return util.convert_metric_values(data["distanceToEmptyBattery"]["value"])
distance_to_empty = int(data["distanceToEmptyTank"]["value"]) return None
if distance_to_empty > 0: elif sensor_id == "hours_to_service":
return util.convert_metric_values(data["distanceToEmptyTank"]["value"]) return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None
return None elif sensor_id == "km_to_service":
elif sensor_id == "distance_to_empty_battery": if util.keys_exists(data, "distanceToService"):
if util.keys_exists(data, "distanceToEmptyBattery"): km_to_service = int(data["distanceToService"]["value"])
distance_to_empty = int(data["distanceToEmptyBattery"]["value"]) if km_to_service > 0:
if distance_to_empty > 0: return util.convert_metric_values(data["distanceToService"]["value"])
return util.convert_metric_values(data["distanceToEmptyBattery"]["value"]) return None
return None elif sensor_id == "time_to_service":
elif sensor_id == "hours_to_service": if util.keys_exists(data, "timeToService"):
return data["engineHoursToService"]["value"] if util.keys_exists(data, "engineHoursToService") else None return str(data["timeToService"]["value"]) + " " + data["timeToService"]["unit"]
elif sensor_id == "km_to_service": else:
if util.keys_exists(data, "distanceToService"): return None
km_to_service = int(data["distanceToService"]["value"]) elif sensor_id == "service_warning_status":
if km_to_service > 0: return data["serviceWarning"]["value"] if util.keys_exists(data, "serviceWarning") else None
return util.convert_metric_values(data["distanceToService"]["value"]) elif sensor_id == "average_energy_consumption":
return None average_energy_con = 0
elif sensor_id == "time_to_service": if util.keys_exists(data, "averageEnergyConsumption"):
if util.keys_exists(data, "timeToService"): average_energy_con = data["averageEnergyConsumption"]["value"]
return str(data["timeToService"]["value"]) + " " + data["timeToService"]["unit"] elif util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
average_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
if average_energy_con != 0:
return average_energy_con
return None
elif sensor_id == "washer_fluid_warning":
return data["washerFluidLevelWarning"]["value"] if util.keys_exists(data, "washerFluidLevelWarning") else None
elif sensor_id == "warnings":
warnings = 0
cleaned_data = {}
for key, dicts in data.items():
contains_data = sum(value == "NO_WARNING" or value == "FAILURE" for value in dicts.values())
if contains_data:
warnings = warnings + 1
cleaned_data[key] = dicts["value"]
return cleaned_data if warnings > 0 else None
elif sensor_id == "trip_fuel_consumption":
trip_fuel_con = 0
if util.keys_exists(data, "averageFuelConsumptionAutomatic"):
trip_fuel_con = float(data["averageFuelConsumptionAutomatic"]["value"])
if trip_fuel_con > 0:
return trip_fuel_con
return None
elif sensor_id == "trip_distance":
return util.convert_metric_values(int(data["tripMeterAutomatic"]["value"])) \
if util.keys_exists(data, "tripMeterAutomatic") else None
elif sensor_id == "trip_energy_consumption":
trip_energy_con = 0
if util.keys_exists(data, "averageEnergyConsumptionAutomatic"):
trip_energy_con = data["averageEnergyConsumptionAutomatic"]["value"]
if trip_energy_con != 0:
return trip_energy_con
return None
elif sensor_id == "trip_speed":
trip_speed = 0
if util.keys_exists(data, "averageSpeedAutomatic"):
trip_speed = float(data["averageSpeedAutomatic"]["value"])
if trip_speed != 0:
return util.convert_metric_values(trip_speed)
return None
else: else:
return None return None
elif sensor_id == "service_warning_status":
return data["serviceWarning"]["value"] if util.keys_exists(data, "serviceWarning") else None
elif sensor_id == "average_energy_consumption":
return data["averageEnergyConsumption"]["value"] if util.keys_exists(data, "averageEnergyConsumption") else None
elif sensor_id == "washer_fluid_warning":
return data["washerFluidLevelWarning"]["value"] if util.keys_exists(data, "washerFluidLevelWarning") else None
elif sensor_id == "warnings":
warnings = 0
cleaned_data = {}
for key, dicts in data.items():
contains_data = sum(value == "NO_WARNING" or value == "FAILURE" for value in dicts.values())
if contains_data:
warnings = warnings + 1
cleaned_data[key] = dicts["value"]
return cleaned_data if warnings > 0 else None
else:
return None