Compare commits

..
1 Commits
Author SHA1 Message Date
Linus Dietz e6861ab872 Rebuild app OTP authentication 2024-03-14 09:06:17 +01:00
9 changed files with 39 additions and 167 deletions
+6 -6
View File
@@ -27,20 +27,20 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
uses: docker/login-action@v3
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -52,7 +52,7 @@ jobs:
type=semver,pattern={{major}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm/v6,linux/arm/v8
+1 -45
View File
@@ -50,7 +50,6 @@ Please let me know if your car works with this addon so I can expand the list!<b
- Lock/unlock car
- Start/stop climate
- Sensor "Battery Charge Level"
- Sensor "Battery Capacity"
- Sensor "Electric Range"
- Sensor "Charging System Status"
- Sensor "Charging Connection Status" (partly broken since 2.10 car update)
@@ -74,50 +73,6 @@ 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)
## 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
<b>Docker:</b>
@@ -151,6 +106,7 @@ Here is what every option means:
| `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
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg
+2 -3
View File
@@ -1,6 +1,5 @@
requests~=2.32.0
requests~=2.29.0
dynaconf~=3.1.12
paho-mqtt~=1.6.1
Babel~=2.12.1
pytz~=2023.3
google_play_scraper~=1.2.6
pytz~=2023.3
-41
View File
@@ -1,44 +1,3 @@
## 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:
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge"
version: "1.9.3"
version: "1.8.27"
slug: "volvo2mqtt"
init: false
url: "https://github.com/Dielee/volvo2mqtt"
+3 -4
View File
@@ -1,6 +1,6 @@
from config import settings
VERSION = "v1.9.4"
VERSION = "v1.9.0b1"
OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
OAUTH_AUTH_URL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
@@ -22,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"
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"
API_BACKEND_STATUS = "https://public-developer-portal-bff.weu-prod.ecpaz.volvocars.biz/api/v1/backend-status"
API_BACKEND_STATUS = "https://oip-dev-bff.euwest1.production.volvo.care/api/v1/backend-status"
LENGTH_KILOMETERS = "km"
SPEED_KILOMETERS_PER_HOUR = "km/h"
@@ -92,7 +92,6 @@ icon_states = {
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": 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": "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},
@@ -139,5 +138,5 @@ supported_entities = [
]
old_entity_ids = ["months_to_service", "service_warning_trigger", "distance_to_empty"]
otp_max_loops = 24
otp_max_loops = 15
otp_mqtt_topic = "volvoAAOS2mqtt/otp_code"
+9 -25
View File
@@ -47,37 +47,25 @@ def connect():
def create_otp_input():
state_topic = otp_mqtt_topic + "/state"
config = {
"name": "Volvo OTP",
"object_id": f"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"
"unique_id": f"volvoAAOS2mqtt_otp",
"pattern": "\d{6}",
"icon": "mdi:two-factor-authentication"
}
mqtt_client.publish(
"homeassistant/text/volvoAAOS2mqtt/volvo_otp/config",
f"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 delete_otp_input():
topic = "homeassistant/text/volvoAAOS2mqtt/volvo_otp/config"
mqtt_client.publish(topic, payload="", retain=True)
def send_car_images(vin, data, device):
@@ -131,12 +119,8 @@ def on_disconnect(client, userdata, rc):
def on_message(client, userdata, msg):
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!")
global otp_code
otp_code = payload
return None
else:
try:
+5 -25
View File
@@ -5,7 +5,6 @@ import re
import sys
import config
import json
from google_play_scraper import app
from logging import handlers
from datetime import datetime
from const import units
@@ -30,28 +29,9 @@ class SensitiveDataFilter(logging.Filter):
record.msg = re.sub(pattern, "<REDACTED>", record.msg)
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:
def save_to_json(data):
with open('.token', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def get_icon_between(icon_list, state):
@@ -65,7 +45,7 @@ def get_icon_between(icon_list, state):
def setup_logging():
log_location = "volvo2mqtt.log"
if os.environ.get("IS_HA_ADDON"):
check_existing_folder("/addons/volvo2mqtt/log/")
check_existing_folder()
log_location = "/addons/volvo2mqtt/log/volvo2mqtt.log"
logging.Formatter.converter = lambda *args: datetime.now(tz=TZ).timetuple()
@@ -98,8 +78,8 @@ def setup_logging():
logger.setLevel(logging.ERROR)
def check_existing_folder(path):
Path(path).mkdir(parents=True, exist_ok=True)
def check_existing_folder():
Path("/addons/volvo2mqtt/log/").mkdir(parents=True, exist_ok=True)
def keys_exists(element, *keys):
+12 -17
View File
@@ -35,11 +35,9 @@ backend_status = ""
def authorize(renew_tokenfile=False):
global refresh_token
token_path = util.get_token_path()
if os.path.isfile(token_path) and not renew_tokenfile:
if os.path.isfile(".token") and not renew_tokenfile:
logging.info("Using login from token file")
f = open(token_path)
f = open('.token')
data = json.load(f)
refresh_token = data["refresh_token"]
refresh_auth()
@@ -48,7 +46,7 @@ def authorize(renew_tokenfile=False):
auth_session = requests.session()
auth_session.headers = {
"authorization": "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
"User-Agent": "vca-android/" + util.get_volvo_app_version(),
"User-Agent": "vca-android/5.37.0",
"Accept-Encoding": "gzip",
"Content-Type": "application/json; charset=utf-8"
}
@@ -57,7 +55,7 @@ def authorize(renew_tokenfile=False):
"&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")
"&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 conve:engine_status")
auth = auth_session.get(OAUTH_AUTH_URL + url_params)
if auth.status_code == 200:
@@ -93,15 +91,14 @@ def authorize(renew_tokenfile=False):
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)
util.save_to_json(token_data)
get_vcc_api_keys()
get_vehicles()
check_supported_endpoints()
Thread(target=backend_status_loop).start()
else:
message = auth.json()
raise Exception(message["details"][0]["userMessage"])
get_vcc_api_keys()
get_vehicles()
check_supported_endpoints()
Thread(target=backend_status_loop).start()
raise Exception(message["error_description"])
def continue_auth(auth_session, data):
next_url = data["_links"]["continueAuthentication"]["href"] + "?action=continueAuthentication"
@@ -156,6 +153,7 @@ def send_otp(auth_session, data):
if not mqtt.otp_code:
raise Exception ("No OTP found, exting...")
mqtt.delete_otp_input()
auth = auth_session.post(next_url, data=json.dumps(body))
if auth.status_code == 200:
return auth.json()
@@ -185,9 +183,8 @@ def refresh_auth():
return None
if auth.status_code == 200:
token_path = util.get_token_path()
data = auth.json()
util.save_to_json(data, token_path)
util.save_to_json(data)
session.headers.update({"authorization": "Bearer " + data["access_token"]})
global token_expires_at
@@ -550,8 +547,6 @@ def parse_api_data(data, sensor_id=None):
if sensor_id == "battery_charge_level":
return data["batteryChargeLevel"]["value"] if util.keys_exists(data, "batteryChargeLevel") else None
elif sensor_id == "battery_capacity":
return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") else None
elif sensor_id == "electric_range":
return util.convert_metric_values(data["electricRange"]["value"]) \
if util.keys_exists(data, "electricRange") else None