Skip to content

Commit 247c265

Browse files
ndrsnhsLKuemmel
andauthored
homeassistant vehicle soc (#2875)
* add homeassistant soc module * add entity id * adjust test * check configuration * Update packages/modules/vehicles/homeassistant/soc.py --------- Co-authored-by: LKuemmel <76958050+LKuemmel@users.noreply.github.com>
1 parent 4b26dcd commit 247c265

4 files changed

Lines changed: 161 additions & 1 deletion

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from helpermodules.auto_str import auto_str
2+
from typing import Optional
3+
4+
5+
@auto_str
6+
class HaVehicleSocConfiguration:
7+
def __init__(
8+
self,
9+
url: Optional[str] = None,
10+
token: Optional[str] = None,
11+
entity_id: Optional[str] = None
12+
):
13+
self.url = url
14+
self.token = token
15+
self.entity_id = entity_id
16+
17+
18+
@auto_str
19+
class HaVehicleSocSetup():
20+
def __init__(self,
21+
name: str = "HomeAssistant",
22+
type: str = "homeassistant",
23+
official: bool = True,
24+
configuration: HaVehicleSocConfiguration = None) -> None:
25+
self.name = name
26+
self.type = type
27+
self.official = official
28+
self.configuration = configuration or HaVehicleSocConfiguration()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import logging
2+
3+
from typing import List, Union
4+
from datetime import datetime
5+
6+
from helpermodules.cli import run_using_positional_cli_args
7+
from modules.common import req
8+
from modules.common import store
9+
from modules.common.abstract_device import DeviceDescriptor
10+
from modules.common.abstract_vehicle import VehicleUpdateData
11+
from modules.common.component_state import CarState
12+
from modules.common.configurable_vehicle import ConfigurableVehicle
13+
from modules.vehicles.homeassistant.config import HaVehicleSocSetup, HaVehicleSocConfiguration
14+
15+
16+
log = logging.getLogger(__name__)
17+
18+
19+
def extract_to_epoch(input_string: Union[str, int, float]) -> float:
20+
# If already an integer, return it
21+
if isinstance(input_string, int) or isinstance(input_string, float):
22+
return int(input_string)
23+
24+
# Try parsing as UTC formatted time
25+
try:
26+
dt = datetime.fromisoformat(input_string)
27+
return int(dt.timestamp())
28+
except ValueError:
29+
log.exception(f'Kein ISO 8601 formatiertes Datum in "{input_string}" gefunden.')
30+
return None
31+
32+
33+
def fetch_soc(config: HaVehicleSocSetup) -> CarState:
34+
url = config.configuration.url
35+
entity_id = config.configuration.entity_id
36+
token = config.configuration.token
37+
if url is None or url == "":
38+
raise ValueError("Keine URL zum Abrufen der Daten definiert. Bitte Konfiguration anpassen.")
39+
if entity_id is None or entity_id == "":
40+
raise ValueError("Keine Entitäts-ID definiert. Bitte Konfiguration anpassen.")
41+
if token is None or token == "":
42+
raise ValueError("Kein Token definiert. Bitte Konfiguration anpassen.")
43+
url = url + "/api/states/" + entity_id
44+
response = req.get_http_session().get(url, timeout=10,
45+
headers={
46+
"authorization": "Bearer " + token,
47+
"content-type": "application/json"}
48+
)
49+
json = response.json()
50+
soc = float(json['state'])
51+
soc_timestamp = extract_to_epoch(json['last_changed'])
52+
return CarState(soc=soc, soc_timestamp=soc_timestamp)
53+
54+
55+
def create_vehicle(vehicle_config: HaVehicleSocSetup, vehicle: int):
56+
def updater(vehicle_update_data: VehicleUpdateData) -> CarState:
57+
return fetch_soc(vehicle_config)
58+
return ConfigurableVehicle(vehicle_config=vehicle_config,
59+
component_updater=updater,
60+
vehicle=vehicle)
61+
62+
63+
def json_update(charge_point: int,
64+
url: str,
65+
token: str,
66+
entity_id: str
67+
):
68+
log.debug(f'homeassistant-soc: charge_point={charge_point} url="{url}" token="{token}" '
69+
f'entity_id="{entity_id}"')
70+
store.get_car_value_store(charge_point).store.set(
71+
fetch_soc(HaVehicleSocSetup(configuration=HaVehicleSocConfiguration(url=url,
72+
token=token,
73+
entity_id=entity_id))))
74+
75+
76+
def main(argv: List[str]):
77+
run_using_positional_cli_args(json_update, argv)
78+
79+
80+
device_descriptor = DeviceDescriptor(configuration_factory=HaVehicleSocSetup)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
from modules.vehicles.homeassistant.soc import fetch_soc, HaVehicleSocSetup, HaVehicleSocConfiguration
4+
5+
6+
class TestSoc(unittest.TestCase):
7+
8+
def setUp(self):
9+
self.test_cases = [{
10+
"sample_data": {
11+
"entity_id": "sensor.ioniq_ev_battery_level",
12+
"state": "84",
13+
"attributes": {
14+
"state_class": "measurement",
15+
"unit_of_measurement": "%",
16+
"device_class": "battery",
17+
"friendly_name": "IONIQ EV Battery Level"
18+
},
19+
"last_changed": "2025-09-29T17:48:02.754865+00:00",
20+
"last_reported": "2025-09-29T17:48:02.754865+00:00",
21+
"last_updated": "2025-09-29T17:48:02.754865+00:00",
22+
"context": {
23+
"id": "05K6B4GTT29YSKIDVJV2R7V8KY",
24+
"parent_id": None,
25+
"user_id": None
26+
}
27+
},
28+
"url": "http://1.1.1.1:4711",
29+
"entity_id": "sensor.ioniq_ev_battery_level",
30+
"token": "testtoken",
31+
"expected_soc": 84,
32+
"expected_range": None,
33+
"expected_timestamp": 1759168082
34+
}]
35+
36+
@patch('soc.req.get_http_session')
37+
def test_fetch_soc(self, mock_get_http_session):
38+
for case in self.test_cases:
39+
mock_response = MagicMock()
40+
mock_response.json.return_value = case['sample_data']
41+
mock_get_http_session.return_value.get.return_value = mock_response
42+
43+
vehicle_config = HaVehicleSocSetup(configuration=HaVehicleSocConfiguration(
44+
url=case['url'],
45+
token=case['token'],
46+
entity_id=case['entity_id']
47+
))
48+
car_state = fetch_soc(vehicle_config)
49+
50+
self.assertEqual(car_state.soc, case['expected_soc'])
51+
self.assertEqual(car_state.range, case['expected_range'])
52+
self.assertEqual(car_state.soc_timestamp, case['expected_timestamp'])

packages/modules/vehicles/json/test_soc.py renamed to packages/modules/vehicles/json/test_json_soc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22
from unittest.mock import patch, MagicMock
3-
from soc import initialize_vehicle, fetch_soc, JsonSocSetup, JsonSocConfiguration
3+
from modules.vehicles.json.soc import initialize_vehicle, fetch_soc, JsonSocSetup, JsonSocConfiguration
44

55

66
class TestSoc(unittest.TestCase):

0 commit comments

Comments
 (0)