From 384bb28615edb01ae60930110b626f0b65c3d731 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 29 Oct 2025 11:05:24 +0100 Subject: [PATCH 1/5] add homeassistant soc module --- .../modules/vehicles/homeassistant/config.py | 26 +++++++ .../modules/vehicles/homeassistant/soc.py | 70 +++++++++++++++++++ .../vehicles/homeassistant/test_soc.py | 50 +++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 packages/modules/vehicles/homeassistant/config.py create mode 100644 packages/modules/vehicles/homeassistant/soc.py create mode 100644 packages/modules/vehicles/homeassistant/test_soc.py diff --git a/packages/modules/vehicles/homeassistant/config.py b/packages/modules/vehicles/homeassistant/config.py new file mode 100644 index 0000000000..c28cfcafeb --- /dev/null +++ b/packages/modules/vehicles/homeassistant/config.py @@ -0,0 +1,26 @@ +from helpermodules.auto_str import auto_str +from typing import Optional + + +@auto_str +class HaVehicleSocConfiguration: + def __init__( + self, + url: Optional[str] = None, + token: Optional[str] = None + ): + self.url = url + self.token = token + + +@auto_str +class HaVehicleSocSetup(): + def __init__(self, + name: str = "HomeAssistant", + type: str = "homeassistant", + official: bool = True, + configuration: HaVehicleSocConfiguration = None) -> None: + self.name = name + self.type = type + self.official = official + self.configuration = configuration or HaVehicleSocConfiguration() diff --git a/packages/modules/vehicles/homeassistant/soc.py b/packages/modules/vehicles/homeassistant/soc.py new file mode 100644 index 0000000000..0ba1cb5e09 --- /dev/null +++ b/packages/modules/vehicles/homeassistant/soc.py @@ -0,0 +1,70 @@ +import logging + +from typing import List, Union +from datetime import datetime + +from helpermodules.cli import run_using_positional_cli_args +from modules.common import req +from modules.common import store +from modules.common.abstract_device import DeviceDescriptor +from modules.common.abstract_vehicle import VehicleUpdateData +from modules.common.component_state import CarState +from modules.common.configurable_vehicle import ConfigurableVehicle +from modules.vehicles.homeassistant.config import HaVehicleSocSetup, HaVehicleSocConfiguration + + +log = logging.getLogger(__name__) + + +def extract_to_epoch(input_string: Union[str, int, float]) -> float: + # If already an integer, return it + if isinstance(input_string, int) or isinstance(input_string, float): + return int(input_string) + + # Try parsing as UTC formatted time + try: + dt = datetime.fromisoformat(input_string) + return int(dt.timestamp()) + except ValueError: + log.exception(f'Kein ISO 8601 formatiertes Datum in "{input_string}" gefunden.') + return None + + +def fetch_soc(config: HaVehicleSocSetup) -> CarState: + url = config.configuration.url + if url is None or url == "": + raise ValueError("Keine URL zum Abrufen der Daten definiert. Bitte in der Konfiguration aktualisieren.") + response = req.get_http_session().get(url, timeout=10, + headers={ + "authorization": "Bearer " + config.configuration.token, + "content-type": "application/json"} + ) + response.raise_for_status() + json = response.json() + soc = float(json['state']) + soc_timestamp = extract_to_epoch(json['last_changed']) + return CarState(soc=soc, soc_timestamp=soc_timestamp) + + +def create_vehicle(vehicle_config: HaVehicleSocSetup, vehicle: int): + def updater(vehicle_update_data: VehicleUpdateData) -> CarState: + return fetch_soc(vehicle_config) + return ConfigurableVehicle(vehicle_config=vehicle_config, + component_updater=updater, + vehicle=vehicle) + + +def json_update(charge_point: int, + url: str, + token: str + ): + log.debug(f'homeassistant-soc: charge_point={charge_point} url="{url}" token="{token}"') + store.get_car_value_store(charge_point).store.set( + fetch_soc(HaVehicleSocSetup(configuration=HaVehicleSocConfiguration(url=url, token=token)))) + + +def main(argv: List[str]): + run_using_positional_cli_args(json_update, argv) + + +device_descriptor = DeviceDescriptor(configuration_factory=HaVehicleSocSetup) diff --git a/packages/modules/vehicles/homeassistant/test_soc.py b/packages/modules/vehicles/homeassistant/test_soc.py new file mode 100644 index 0000000000..444376c48c --- /dev/null +++ b/packages/modules/vehicles/homeassistant/test_soc.py @@ -0,0 +1,50 @@ +import unittest +from unittest.mock import patch, MagicMock +from soc import fetch_soc, HaVehicleSocSetup, HaVehicleSocConfiguration + + +class TestSoc(unittest.TestCase): + + def setUp(self): + self.test_cases = [{ + "sample_data": { + "entity_id": "sensor.ioniq_ev_battery_level", + "state": "84", + "attributes": { + "state_class": "measurement", + "unit_of_measurement": "%", + "device_class": "battery", + "friendly_name": "IONIQ EV Battery Level" + }, + "last_changed": "2025-09-29T17:48:02.754865+00:00", + "last_reported": "2025-09-29T17:48:02.754865+00:00", + "last_updated": "2025-09-29T17:48:02.754865+00:00", + "context": { + "id": "05K6B4GTT29YSKIDVJV2R7V8KY", + "parent_id": None, + "user_id": None + } + }, + "url": "http://1.1.1.1:4711/api/states/sensor.ioniq_ev_battery_level", + "token": "testtoken", + "expected_soc": 84, + "expected_range": None, + "expected_timestamp": 1759168082 + }] + + @patch('soc.req.get_http_session') + def test_fetch_soc(self, mock_get_http_session): + for case in self.test_cases: + mock_response = MagicMock() + mock_response.json.return_value = case['sample_data'] + mock_get_http_session.return_value.get.return_value = mock_response + + vehicle_config = HaVehicleSocSetup(configuration=HaVehicleSocConfiguration( + url=case['url'], + token=case['token'] + )) + car_state = fetch_soc(vehicle_config) + + self.assertEqual(car_state.soc, case['expected_soc']) + self.assertEqual(car_state.range, case['expected_range']) + self.assertEqual(car_state.soc_timestamp, case['expected_timestamp']) From 7a6b41ff591912cbfff28e935f67904e68bceb58 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 29 Oct 2025 13:46:49 +0100 Subject: [PATCH 2/5] add entity id --- packages/modules/vehicles/homeassistant/config.py | 4 +++- packages/modules/vehicles/homeassistant/soc.py | 12 ++++++++---- packages/modules/vehicles/homeassistant/test_soc.py | 6 ++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/modules/vehicles/homeassistant/config.py b/packages/modules/vehicles/homeassistant/config.py index c28cfcafeb..9c49500bed 100644 --- a/packages/modules/vehicles/homeassistant/config.py +++ b/packages/modules/vehicles/homeassistant/config.py @@ -7,10 +7,12 @@ class HaVehicleSocConfiguration: def __init__( self, url: Optional[str] = None, - token: Optional[str] = None + token: Optional[str] = None, + entity_id: Optional[str] = None ): self.url = url self.token = token + self.entity_id = entity_id @auto_str diff --git a/packages/modules/vehicles/homeassistant/soc.py b/packages/modules/vehicles/homeassistant/soc.py index 0ba1cb5e09..88779ea903 100644 --- a/packages/modules/vehicles/homeassistant/soc.py +++ b/packages/modules/vehicles/homeassistant/soc.py @@ -31,7 +31,7 @@ def extract_to_epoch(input_string: Union[str, int, float]) -> float: def fetch_soc(config: HaVehicleSocSetup) -> CarState: - url = config.configuration.url + url = config.configuration.url+"/api/states/"+config.configuration.entity_id if url is None or url == "": raise ValueError("Keine URL zum Abrufen der Daten definiert. Bitte in der Konfiguration aktualisieren.") response = req.get_http_session().get(url, timeout=10, @@ -56,11 +56,15 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: def json_update(charge_point: int, url: str, - token: str + token: str, + entity_id: str ): - log.debug(f'homeassistant-soc: charge_point={charge_point} url="{url}" token="{token}"') + log.debug(f'homeassistant-soc: charge_point={charge_point} url="{url}" token="{token}" ' + f'entity_id="{entity_id}"') store.get_car_value_store(charge_point).store.set( - fetch_soc(HaVehicleSocSetup(configuration=HaVehicleSocConfiguration(url=url, token=token)))) + fetch_soc(HaVehicleSocSetup(configuration=HaVehicleSocConfiguration(url=url, + token=token, + entity_id=entity_id)))) def main(argv: List[str]): diff --git a/packages/modules/vehicles/homeassistant/test_soc.py b/packages/modules/vehicles/homeassistant/test_soc.py index 444376c48c..d248a59a95 100644 --- a/packages/modules/vehicles/homeassistant/test_soc.py +++ b/packages/modules/vehicles/homeassistant/test_soc.py @@ -25,7 +25,8 @@ def setUp(self): "user_id": None } }, - "url": "http://1.1.1.1:4711/api/states/sensor.ioniq_ev_battery_level", + "url": "http://1.1.1.1:4711", + "entity_id": "sensor.ioniq_ev_battery_level", "token": "testtoken", "expected_soc": 84, "expected_range": None, @@ -41,7 +42,8 @@ def test_fetch_soc(self, mock_get_http_session): vehicle_config = HaVehicleSocSetup(configuration=HaVehicleSocConfiguration( url=case['url'], - token=case['token'] + token=case['token'], + entity_id=case['entity_id'] )) car_state = fetch_soc(vehicle_config) From 407056f9f9b73b6e7038f5464e59c0af60fe10fe Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 29 Oct 2025 14:08:20 +0100 Subject: [PATCH 3/5] adjust test --- .../vehicles/homeassistant/{test_soc.py => test_ha_soc.py} | 2 +- .../modules/vehicles/json/{test_soc.py => test_json_soc.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/modules/vehicles/homeassistant/{test_soc.py => test_ha_soc.py} (95%) rename packages/modules/vehicles/json/{test_soc.py => test_json_soc.py} (96%) diff --git a/packages/modules/vehicles/homeassistant/test_soc.py b/packages/modules/vehicles/homeassistant/test_ha_soc.py similarity index 95% rename from packages/modules/vehicles/homeassistant/test_soc.py rename to packages/modules/vehicles/homeassistant/test_ha_soc.py index d248a59a95..a81ac3a87c 100644 --- a/packages/modules/vehicles/homeassistant/test_soc.py +++ b/packages/modules/vehicles/homeassistant/test_ha_soc.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import patch, MagicMock -from soc import fetch_soc, HaVehicleSocSetup, HaVehicleSocConfiguration +from modules.vehicles.homeassistant.soc import fetch_soc, HaVehicleSocSetup, HaVehicleSocConfiguration class TestSoc(unittest.TestCase): diff --git a/packages/modules/vehicles/json/test_soc.py b/packages/modules/vehicles/json/test_json_soc.py similarity index 96% rename from packages/modules/vehicles/json/test_soc.py rename to packages/modules/vehicles/json/test_json_soc.py index 08b159ce9b..a5d3611341 100644 --- a/packages/modules/vehicles/json/test_soc.py +++ b/packages/modules/vehicles/json/test_json_soc.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import patch, MagicMock -from soc import initialize_vehicle, fetch_soc, JsonSocSetup, JsonSocConfiguration +from modules.vehicles.json.soc import initialize_vehicle, fetch_soc, JsonSocSetup, JsonSocConfiguration class TestSoc(unittest.TestCase): From c95567ae0d87899243dd28986fb8f1a77da09649 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 30 Oct 2025 07:46:18 +0100 Subject: [PATCH 4/5] check configuration --- packages/modules/vehicles/homeassistant/soc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/modules/vehicles/homeassistant/soc.py b/packages/modules/vehicles/homeassistant/soc.py index 88779ea903..39ac354a3d 100644 --- a/packages/modules/vehicles/homeassistant/soc.py +++ b/packages/modules/vehicles/homeassistant/soc.py @@ -31,12 +31,19 @@ def extract_to_epoch(input_string: Union[str, int, float]) -> float: def fetch_soc(config: HaVehicleSocSetup) -> CarState: - url = config.configuration.url+"/api/states/"+config.configuration.entity_id + url = config.configuration.url + entity_id = config.configuration.entity_id + token = config.configuration.token if url is None or url == "": - raise ValueError("Keine URL zum Abrufen der Daten definiert. Bitte in der Konfiguration aktualisieren.") + raise ValueError("Keine URL zum Abrufen der Daten definiert. Bitte Konfiguration anpassen.") + if entity_id is None or entity_id == "": + raise ValueError("Keine Entitäts-ID definiert. Bitte Konfiguration anpassen.") + if token is None or token == "": + raise ValueError("Kein Token definiert. Bitte Konfiguration anpassen.") + url = url + "/api/states/" + entity_id response = req.get_http_session().get(url, timeout=10, headers={ - "authorization": "Bearer " + config.configuration.token, + "authorization": "Bearer " + token, "content-type": "application/json"} ) response.raise_for_status() From 936cf9b7ba46c0286e6b00799376368aee82b471 Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:46:35 +0100 Subject: [PATCH 5/5] Update packages/modules/vehicles/homeassistant/soc.py --- packages/modules/vehicles/homeassistant/soc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/modules/vehicles/homeassistant/soc.py b/packages/modules/vehicles/homeassistant/soc.py index 39ac354a3d..419c19de21 100644 --- a/packages/modules/vehicles/homeassistant/soc.py +++ b/packages/modules/vehicles/homeassistant/soc.py @@ -46,7 +46,6 @@ def fetch_soc(config: HaVehicleSocSetup) -> CarState: "authorization": "Bearer " + token, "content-type": "application/json"} ) - response.raise_for_status() json = response.json() soc = float(json['state']) soc_timestamp = extract_to_epoch(json['last_changed'])