Compare commits

..
7 Commits
9 changed files with 31 additions and 130 deletions
+6 -6
View File
@@ -27,20 +27,20 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
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@v5 uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
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@v5 uses: docker/build-push-action@v2
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
+2 -31
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 - 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" (partly broken since 2.10 car update)
@@ -87,36 +86,8 @@ The following steps are required for authentication in exactly this order:
```etc ...``` ```etc ...```
4. Now, open your mailbox and copy your OTP Code 4. Now, open your mailbox and copy your OTP Code
5. Open HomeAssistant and search for the entity ID ```text.volvo_otp``` 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> 6. Paste your OTP into the text entity and press Enter
6a. (optional) If your OTP cannot be safed, try to paste your OTP code via MQTT to the following topic: `volvoAAOS2mqtt/otp_code` <p> 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.
<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>
+1 -1
View File
@@ -1,4 +1,4 @@
requests~=2.32.0 requests~=2.31.0
dynaconf~=3.1.12 dynaconf~=3.1.12
paho-mqtt~=1.6.1 paho-mqtt~=1.6.1
Babel~=2.12.1 Babel~=2.12.1
-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 ## v1.8.27
### 🚀 Features: ### 🚀 Features:
+1 -1
View File
@@ -1,6 +1,6 @@
name: "Volvo2Mqtt" name: "Volvo2Mqtt"
description: "Volvo AAOS MQTT bridge" description: "Volvo AAOS MQTT bridge"
version: "1.9.3" version: "1.9.0"
slug: "volvo2mqtt" slug: "volvo2mqtt"
init: false init: false
url: "https://github.com/Dielee/volvo2mqtt" url: "https://github.com/Dielee/volvo2mqtt"
+2 -3
View File
@@ -1,6 +1,6 @@
from config import settings from config import settings
VERSION = "v1.9.3" VERSION = "v1.9.0"
OAUTH_TOKEN_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" OAUTH_AUTH_URL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
@@ -92,7 +92,6 @@ icon_states = {
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},
@@ -139,5 +138,5 @@ supported_entities = [
] ]
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_max_loops = 15
otp_mqtt_topic = "volvoAAOS2mqtt/otp_code" otp_mqtt_topic = "volvoAAOS2mqtt/otp_code"
+9 -25
View File
@@ -47,37 +47,25 @@ def connect():
def create_otp_input(): def create_otp_input():
state_topic = otp_mqtt_topic + "/state"
config = { config = {
"name": "Volvo OTP", "name": "Volvo OTP",
"object_id": f"volvo_otp", "object_id": f"volvo_otp",
"schema": "state", "schema": "state",
"command_topic": otp_mqtt_topic, "command_topic": otp_mqtt_topic,
"state_topic": state_topic, "unique_id": f"volvoAAOS2mqtt_otp",
"unique_id": "volvoAAOS2mqtt_otp", "pattern": "\d{6}",
"pattern": r"\d{6}", "icon": "mdi:two-factor-authentication"
"icon": "mdi:two-factor-authentication",
"mode": "text"
} }
mqtt_client.publish( mqtt_client.publish(
"homeassistant/text/volvoAAOS2mqtt/volvo_otp/config", f"homeassistant/text/volvoAAOS2mqtt/volvo_otp/config",
json.dumps(config), json.dumps(config),
retain=True retain=True
) )
mqtt_client.publish( def delete_otp_input():
state_topic, topic = "homeassistant/text/volvoAAOS2mqtt/volvo_otp/config"
"000000", mqtt_client.publish(topic, payload="", retain=True)
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):
@@ -131,12 +119,8 @@ def on_disconnect(client, userdata, rc):
def on_message(client, userdata, msg): 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.topic == otp_mqtt_topic:
if msg.retain == 0: global otp_code
global otp_code otp_code = payload
otp_code = payload
set_otp_state()
else:
logging.warning("Found retained OTP, this can't work! Pleas clean retained messages!")
return None return None
else: else:
try: try:
+5 -13
View File
@@ -42,16 +42,8 @@ def get_volvo_app_version():
else: else:
return "5.37.0" return "5.37.0"
def get_token_path(): def save_to_json(data):
token_path = ".token" with open('.token', 'w', encoding='utf-8') as f:
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) json.dump(data, f, ensure_ascii=False, indent=4)
def get_icon_between(icon_list, state): def get_icon_between(icon_list, state):
@@ -65,7 +57,7 @@ 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("/addons/volvo2mqtt/log/") check_existing_folder()
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()
@@ -98,8 +90,8 @@ def setup_logging():
logger.setLevel(logging.ERROR) logger.setLevel(logging.ERROR)
def check_existing_folder(path): def check_existing_folder():
Path(path).mkdir(parents=True, exist_ok=True) Path("/addons/volvo2mqtt/log/").mkdir(parents=True, exist_ok=True)
def keys_exists(element, *keys): def keys_exists(element, *keys):
+5 -9
View File
@@ -35,11 +35,9 @@ backend_status = ""
def authorize(renew_tokenfile=False): def authorize(renew_tokenfile=False):
global refresh_token global refresh_token
if os.path.isfile(".token") and not renew_tokenfile:
token_path = util.get_token_path()
if os.path.isfile(token_path) and not renew_tokenfile:
logging.info("Using login from token file") logging.info("Using login from token file")
f = open(token_path) f = open('.token')
data = json.load(f) data = json.load(f)
refresh_token = data["refresh_token"] refresh_token = data["refresh_token"]
refresh_auth() refresh_auth()
@@ -93,7 +91,7 @@ def authorize(renew_tokenfile=False):
token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(token_data["expires_in"] - 30)) token_expires_at = datetime.now(util.TZ) + timedelta(seconds=(token_data["expires_in"] - 30))
refresh_token = token_data["refresh_token"] refresh_token = token_data["refresh_token"]
util.save_to_json(token_data, token_path) util.save_to_json(token_data)
else: else:
message = auth.json() message = auth.json()
raise Exception(message["details"][0]["userMessage"]) raise Exception(message["details"][0]["userMessage"])
@@ -156,6 +154,7 @@ def send_otp(auth_session, data):
if not mqtt.otp_code: if not mqtt.otp_code:
raise Exception ("No OTP found, exting...") raise Exception ("No OTP found, exting...")
mqtt.delete_otp_input()
auth = auth_session.post(next_url, data=json.dumps(body)) auth = auth_session.post(next_url, data=json.dumps(body))
if auth.status_code == 200: if auth.status_code == 200:
return auth.json() return auth.json()
@@ -185,9 +184,8 @@ def refresh_auth():
return None return None
if auth.status_code == 200: if auth.status_code == 200:
token_path = util.get_token_path()
data = auth.json() data = auth.json()
util.save_to_json(data, token_path) util.save_to_json(data)
session.headers.update({"authorization": "Bearer " + data["access_token"]}) session.headers.update({"authorization": "Bearer " + data["access_token"]})
global token_expires_at global token_expires_at
@@ -550,8 +548,6 @@ def parse_api_data(data, sensor_id=None):
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 == "battery_capacity":
return data["batteryCapacityKWH"] if util.keys_exists(data, "batteryCapacityKWH") 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