From 2256965cdb168e021b8b5afb57def68aa3ba6986 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 5 Oct 2025 20:04:14 +0200 Subject: [PATCH 01/14] Issue #378 log message fixed --- src/integrations/openwb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index ca84f28..83207cc 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -40,7 +40,7 @@ def update_openwb( vehicle_status, charge_status ) if electric_range is not None and range_topic is not None: - LOG.info("OpenWB Integration published range to %f", range_topic) + LOG.info("OpenWB Integration published range to %s", range_topic) self.__publisher.publish_float( key=range_topic, value=electric_range, From a7fba1de9f7dc8f36253b1f446b9444c479fcd73 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 5 Oct 2025 20:47:54 +0200 Subject: [PATCH 02/14] SoC Timestamp topic added to openWB --- examples/charging-stations.json.sample_openWB_2.0 | 10 ++++++---- src/integrations/openwb/__init__.py | 11 +++++++++++ src/integrations/openwb/charging_station.py | 2 ++ tests/integrations/openwb/test_openwb_integration.py | 6 ++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/examples/charging-stations.json.sample_openWB_2.0 b/examples/charging-stations.json.sample_openWB_2.0 index 544f93b..854c987 100644 --- a/examples/charging-stations.json.sample_openWB_2.0 +++ b/examples/charging-stations.json.sample_openWB_2.0 @@ -2,8 +2,9 @@ { "chargeStateTopic": "openWB/chargepoint/2/get/charge_state", "chargingValue": "true", - "socTopic": "openWB/set/vehicle/2/get/soc", - "rangeTopic": "openWB/set/vehicle/2/get/range", + "socTopic": "openWB/set/mqtt/vehicle/2/get/soc", + "socTsTopic": "openWB/set/mqtt/vehicle/2/get/soc_timestamp", + "rangeTopic": "openWB/set/mqtt/vehicle/2/get/range", "chargerConnectedTopic": "openWB/chargepoint/2/get/plug_state", "chargerConnectedValue": "true", "vin": "vin1" @@ -11,8 +12,9 @@ { "chargeStateTopic": "openWB/chargepoint/3/get/charge_state", "chargingValue": "true", - "socTopic": "openWB/set/vehicle/3/get/soc", - "rangeTopic": "openWB/set/vehicle/3/get/range", + "socTopic": "openWB/set/mqtt/vehicle/3/get/soc", + "socTsTopic": "openWB/set/mqtt/vehicle/3/get/soc_timestamp", + "rangeTopic": "openWB/set/mqtt/vehicle/3/get/range", "chargerConnectedTopic": "openWB/chargepoint/3/get/plug_state", "chargerConnectedValue": "true", "vin": "vin2" diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index 83207cc..4792640 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import logging from typing import TYPE_CHECKING @@ -56,3 +57,13 @@ def update_openwb( value=soc, no_prefix=True, ) + + soc_ts_topic = self.__charging_station.soc_ts_topic + soc_ts = int(datetime.datetime.now().timestamp()) + if soc_ts_topic is not None: + LOG.info("OpenWB Integration publisehd SoC timestamp to %s", soc_ts_topic) + self.__publisher.publish_int( + key=soc_ts_topic, + value=soc_ts, + no_prefix=True, + ) diff --git a/src/integrations/openwb/charging_station.py b/src/integrations/openwb/charging_station.py index b02542c..cede1bd 100644 --- a/src/integrations/openwb/charging_station.py +++ b/src/integrations/openwb/charging_station.py @@ -8,11 +8,13 @@ def __init__( charge_state_topic: str, charging_value: str, soc_topic: str | None = None, + soc_ts_topic: str | None = None ) -> None: self.vin: str = vin self.charge_state_topic: str = charge_state_topic self.charging_value: str = charging_value self.soc_topic: str | None = soc_topic + self.soc_ts_topic: str | None = soc_ts_topic self.range_topic: str | None = None self.connected_topic: str | None = None self.connected_value: str | None = None diff --git a/tests/integrations/openwb/test_openwb_integration.py b/tests/integrations/openwb/test_openwb_integration.py index 99d931e..d4394cf 100644 --- a/tests/integrations/openwb/test_openwb_integration.py +++ b/tests/integrations/openwb/test_openwb_integration.py @@ -25,6 +25,7 @@ RANGE_TOPIC = "/mock/range" CHARGE_STATE_TOPIC = "/mock/charge/state" SOC_TOPIC = "/mock/soc/state" +SOC_TS_TOPIC = "/mock/soc/timestamp" CHARGING_VALUE = "VehicleIsCharging" @@ -46,6 +47,7 @@ def setUp(self) -> None: charge_state_topic=CHARGE_STATE_TOPIC, charging_value=CHARGING_VALUE, soc_topic=SOC_TOPIC, + soc_ts_topic=SOC_TS_TOPIC, ) charging_station.range_topic = RANGE_TOPIC self.openwb_integration = OpenWBIntegration( @@ -65,6 +67,10 @@ async def test_update_soc_with_no_bms_data(self) -> None: SOC_TOPIC, float(DRIVETRAIN_SOC_VEHICLE), ) + self.assert_mqtt_topic( + SOC_TS_TOPIC, + int, # We just check that it's an int, the exact value is time-dependent + ) self.assert_mqtt_topic( RANGE_TOPIC, DRIVETRAIN_RANGE_VEHICLE, From 99b0a696743004d01e64b895af25b7b1cba3e93d Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 5 Oct 2025 22:23:58 +0200 Subject: [PATCH 03/14] socTsTopic parsing and README update --- README.md | 1 + src/configuration/parser.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9787d36..b3bdf42 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ The key-value pairs in the JSON express the following: | chargeStateTopic | topic indicating the charge state - **required** | | chargingValue | payload that indicates the charging - **required** | | socTopic | topic where the gateway publishes the SoC for the charging station - optional | +| socTsTopic | topic where the gateway publishes the SoC timestamp for the charging station - optional | | rangeTopic | topic where the gateway publishes the range for the charging station - optional | | chargerConnectedTopic | topic indicating that the vehicle is connected to the charging station - optional | | chargerConnectedValue | payload that indicates that the charger is connected - optional | diff --git a/src/configuration/parser.py b/src/configuration/parser.py index 6ceffba..cba562b 100644 --- a/src/configuration/parser.py +++ b/src/configuration/parser.py @@ -550,10 +550,15 @@ def __process_charging_stations_file(config: Configuration, json_file: str) -> N for item in data: charge_state_topic = item["chargeStateTopic"] charging_value = item["chargingValue"] + soc_ts_topic = item.get("socTsTopic", None) vin = item["vin"] if "socTopic" in item: charging_station = ChargingStation( - vin, charge_state_topic, charging_value, item["socTopic"] + vin, + charge_state_topic, + charging_value, + item["socTopic"], + soc_ts_topic, ) else: charging_station = ChargingStation( From c8b2569cdc453b6cb90a6a262e14d6a7a9f37ebb Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Mon, 6 Oct 2025 19:58:23 +0200 Subject: [PATCH 04/14] openwb unit tests fixed --- poetry.lock | 46 ++++++++++++++++++- pyproject.toml | 1 + .../openwb/test_openwb_integration.py | 12 ++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2899fdd..61814c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "anyio" @@ -217,6 +217,21 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "freezegun" +version = "1.5.5" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2"}, + {file = "freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "gmqtt" version = "0.7.0" @@ -675,6 +690,21 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.1.1" @@ -737,6 +767,18 @@ httpx = ">=0.27.0,<0.29.0" pycryptodome = ">=3.20.0,<4.0.0" tenacity = ">=9.0.0,<10.0.0" +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -824,4 +866,4 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "f78ad950e86b99428d134a345e435585c43bf2bbef1d53363cb45981c281631c" +content-hash = "4f7392baf839ed8a0b7cc404b94a5e4c29bdf7074521b5c3973965b8e36c7a4a" diff --git a/pyproject.toml b/pyproject.toml index 3c803f3..3b84b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ pytest-asyncio = "^1.2.0" pytest-mock = "^3.14.0" mypy = "^1.15.0" pylint = "^3.3.6" +freezegun = "^1.5.5" [tool.poetry.dependencies] saic-ismart-client-ng = { develop = true } diff --git a/tests/integrations/openwb/test_openwb_integration.py b/tests/integrations/openwb/test_openwb_integration.py index d4394cf..23cdcec 100644 --- a/tests/integrations/openwb/test_openwb_integration.py +++ b/tests/integrations/openwb/test_openwb_integration.py @@ -1,9 +1,11 @@ from __future__ import annotations +from datetime import datetime from typing import Any import unittest from apscheduler.schedulers.blocking import BlockingScheduler +from freezegun import freeze_time import pytest from saic_ismart_client_ng.api.vehicle.schema import VinInfo @@ -55,6 +57,7 @@ def setUp(self) -> None: publisher=self.publisher, ) + @freeze_time("2025-01-01 12:00:00") async def test_update_soc_with_no_bms_data(self) -> None: vehicle_status_resp = get_mock_vehicle_status_resp() result = self.vehicle_state.handle_vehicle_status(vehicle_status_resp) @@ -69,7 +72,7 @@ async def test_update_soc_with_no_bms_data(self) -> None: ) self.assert_mqtt_topic( SOC_TS_TOPIC, - int, # We just check that it's an int, the exact value is time-dependent + int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) ) self.assert_mqtt_topic( RANGE_TOPIC, @@ -77,10 +80,12 @@ async def test_update_soc_with_no_bms_data(self) -> None: ) expected_topics = { SOC_TOPIC, + SOC_TS_TOPIC, RANGE_TOPIC, } assert expected_topics == set(self.publisher.map.keys()) + @freeze_time("2025-01-01 12:00:00") async def test_update_soc_with_bms_data(self) -> None: vehicle_status_resp = get_mock_vehicle_status_resp() chrg_mgmt_data_resp = get_mock_charge_management_data_resp() @@ -104,8 +109,13 @@ async def test_update_soc_with_bms_data(self) -> None: RANGE_TOPIC, DRIVETRAIN_RANGE_BMS, ) + self.assert_mqtt_topic( + SOC_TS_TOPIC, + int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) + ) expected_topics = { SOC_TOPIC, + SOC_TS_TOPIC, RANGE_TOPIC, } assert expected_topics == set(self.publisher.map.keys()) From 86fb347a18a3a93f6d8aec17f8f7a576f0ff5dea Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Mon, 6 Oct 2025 20:26:37 +0200 Subject: [PATCH 05/14] ChargingStation init and logging fixed --- src/configuration/parser.py | 20 ++++++++------------ src/integrations/openwb/__init__.py | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/configuration/parser.py b/src/configuration/parser.py index cba562b..305a833 100644 --- a/src/configuration/parser.py +++ b/src/configuration/parser.py @@ -550,20 +550,16 @@ def __process_charging_stations_file(config: Configuration, json_file: str) -> N for item in data: charge_state_topic = item["chargeStateTopic"] charging_value = item["chargingValue"] + soc_topic = item.get("socTopic", None) soc_ts_topic = item.get("socTsTopic", None) vin = item["vin"] - if "socTopic" in item: - charging_station = ChargingStation( - vin, - charge_state_topic, - charging_value, - item["socTopic"], - soc_ts_topic, - ) - else: - charging_station = ChargingStation( - vin, charge_state_topic, charging_value - ) + charging_station = ChargingStation( + vin, + charge_state_topic, + charging_value, + soc_topic, + soc_ts_topic, + ) if "rangeTopic" in item: charging_station.range_topic = item["rangeTopic"] if "chargerConnectedTopic" in item: diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index 4792640..b76d00e 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -61,7 +61,7 @@ def update_openwb( soc_ts_topic = self.__charging_station.soc_ts_topic soc_ts = int(datetime.datetime.now().timestamp()) if soc_ts_topic is not None: - LOG.info("OpenWB Integration publisehd SoC timestamp to %s", soc_ts_topic) + LOG.info("OpenWB Integration published SoC timestamp to %s", soc_ts_topic) self.__publisher.publish_int( key=soc_ts_topic, value=soc_ts, From dd83d85ecb8dbe059bf8674518608aa70758283f Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 12:22:49 +0100 Subject: [PATCH 06/14] feat: add energy-based refresh logic and unit tests --- .../charging-stations.json.sample_openWB_2.0 | 2 + src/configuration/parser.py | 2 + src/handlers/vehicle.py | 13 +++ src/integrations/openwb/__init__.py | 59 ++++++++++++ src/integrations/openwb/charging_station.py | 13 ++- src/mqtt_gateway.py | 12 ++- src/publisher/core.py | 6 ++ src/publisher/mqtt_publisher.py | 17 ++++ .../openwb/test_openwb_integration.py | 93 ++++++++++++++++++- 9 files changed, 207 insertions(+), 10 deletions(-) diff --git a/examples/charging-stations.json.sample_openWB_2.0 b/examples/charging-stations.json.sample_openWB_2.0 index 854c987..023de94 100644 --- a/examples/charging-stations.json.sample_openWB_2.0 +++ b/examples/charging-stations.json.sample_openWB_2.0 @@ -7,6 +7,7 @@ "rangeTopic": "openWB/set/mqtt/vehicle/2/get/range", "chargerConnectedTopic": "openWB/chargepoint/2/get/plug_state", "chargerConnectedValue": "true", + "importedEnergyTopic": "openWB/chargepoint/2/get/daily_imported", "vin": "vin1" }, { @@ -17,6 +18,7 @@ "rangeTopic": "openWB/set/mqtt/vehicle/3/get/range", "chargerConnectedTopic": "openWB/chargepoint/3/get/plug_state", "chargerConnectedValue": "true", + "importedEnergyTopic": "openWB/chargepoint/2/get/daily_imported", "vin": "vin2" } ] diff --git a/src/configuration/parser.py b/src/configuration/parser.py index 305a833..5e0a410 100644 --- a/src/configuration/parser.py +++ b/src/configuration/parser.py @@ -566,6 +566,8 @@ def __process_charging_stations_file(config: Configuration, json_file: str) -> N charging_station.connected_topic = item["chargerConnectedTopic"] if "chargerConnectedValue" in item: charging_station.connected_value = item["chargerConnectedValue"] + if "importedEnergyTopic" in item: + charging_station.imported_energy_topic = item["importedEnergyTopic"] config.charging_stations_by_vin[vin] = charging_station except FileNotFoundError: LOG.warning(f"File {json_file} does not exist") diff --git a/src/handlers/vehicle.py b/src/handlers/vehicle.py index 58f394e..2951857 100644 --- a/src/handlers/vehicle.py +++ b/src/handlers/vehicle.py @@ -17,6 +17,7 @@ import mqtt_topics from saic_api_listener import MqttGatewayAbrpListener, MqttGatewayOsmAndListener from status_publisher.vehicle_info import VehicleInfoPublisher +from vehicle import RefreshMode if TYPE_CHECKING: from saic_ismart_client_ng import SaicApi @@ -308,6 +309,18 @@ def __setup_ha_discovery( return HomeAssistantDiscovery(vehicle_state, vin_info, config) return None + async def handle_charging_station_energy_imported(self, energy: float) -> None: + if self.openwb_integration is None: + return + if self.openwb_integration.should_refresh_by_imported_energy( + energy, self.vehicle_state.charge_polling_min_percent, self.vin_info.vin + ): + LOG.info( + f"Triggering refresh due to imported energy {energy} Wh for vehicle {self.vin_info.vin}" + ) + self.vehicle_state.set_refresh_mode( + RefreshMode.Force, "imported energy threshold reached" + ) class VehicleHandlerLocator(ABC): def get_vehicle_handler(self, vin: str) -> VehicleHandler | None: diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index b76d00e..8e63f48 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -2,6 +2,7 @@ import datetime import logging +import math from typing import TYPE_CHECKING import extractors @@ -30,12 +31,17 @@ def __init__( ) -> None: self.__charging_station = charging_station self.__publisher = publisher + self.real_total_battery_capacity: float | None = None + self.last_imported_energy_wh: float | None = None + self.computed_refresh_by_imported_energy_wh: float | None = None def update_openwb( self, vehicle_status: VehicleStatusRespProcessingResult, charge_status: ChrgMgmtDataRespProcessingResult | None, ) -> None: + if charge_status: + self.real_total_battery_capacity = charge_status.real_total_battery_capacity range_topic = self.__charging_station.range_topic electric_range = extractors.extract_electric_range( vehicle_status, charge_status @@ -67,3 +73,56 @@ def update_openwb( value=soc_ts, no_prefix=True, ) + + def should_refresh_by_imported_energy( + self, imported_energy_wh: float, charge_polling_min_percent: float, vin: str + ) -> bool: + """Determine whether the vehicle status should be refreshed based on the imported energy since the last refresh. + + Returns True if a refresh should be triggered, False otherwise. + """ + # Return False if battery capacity is not available + if self.real_total_battery_capacity is None: + LOG.warning( + "Battery capacity not available. Cannot calculate energy per percent." + ) + return False + + # Calculate the energy corresponding to 1% of the battery in Wh + energy_per_percent = (self.real_total_battery_capacity * 1000) / 100.0 + + # Minimum energy threshold for triggering a refresh + energy_for_min_pct = math.ceil(charge_polling_min_percent * energy_per_percent) + + # Initialize the refresh threshold if it hasn't been set yet + if not self.computed_refresh_by_imported_energy_wh: + self.computed_refresh_by_imported_energy_wh = ( + imported_energy_wh + energy_for_min_pct + ) + LOG.debug( + f"Initial imported energy threshold for vehicle {vin} set to " + f"{self.computed_refresh_by_imported_energy_wh} Wh" + ) + + # Check if the imported energy exceeds the threshold + refresh_needed = False + if imported_energy_wh >= self.computed_refresh_by_imported_energy_wh: + LOG.info( + f"Imported energy threshold of {self.computed_refresh_by_imported_energy_wh} Wh reached " + f"(current: {imported_energy_wh} Wh). Triggering vehicle refresh." + ) + refresh_needed = True + + # Calculate the next threshold + self.computed_refresh_by_imported_energy_wh = ( + imported_energy_wh + energy_for_min_pct + ) + LOG.debug( + f"Next imported energy threshold for vehicle {vin} set to " + f"{self.computed_refresh_by_imported_energy_wh} Wh" + ) + + # Save the last imported energy value + self.last_imported_energy_wh = imported_energy_wh + + return refresh_needed diff --git a/src/integrations/openwb/charging_station.py b/src/integrations/openwb/charging_station.py index cede1bd..bb42e74 100644 --- a/src/integrations/openwb/charging_station.py +++ b/src/integrations/openwb/charging_station.py @@ -8,13 +8,18 @@ def __init__( charge_state_topic: str, charging_value: str, soc_topic: str | None = None, - soc_ts_topic: str | None = None + soc_ts_topic: str | None = None, + range_topic: str | None = None, + connected_topic: str | None = None, + connected_value: str | None = None, + imported_energy_topic: str | None = None, ) -> None: self.vin: str = vin self.charge_state_topic: str = charge_state_topic self.charging_value: str = charging_value self.soc_topic: str | None = soc_topic self.soc_ts_topic: str | None = soc_ts_topic - self.range_topic: str | None = None - self.connected_topic: str | None = None - self.connected_value: str | None = None + self.range_topic: str | None = range_topic + self.connected_topic: str | None = connected_topic + self.connected_value: str | None = connected_value + self.imported_energy_topic: str | None = imported_energy_topic diff --git a/src/mqtt_gateway.py b/src/mqtt_gateway.py index 844a5ae..7ab8048 100644 --- a/src/mqtt_gateway.py +++ b/src/mqtt_gateway.py @@ -20,7 +20,7 @@ from publisher.log_publisher import ConsolePublisher from publisher.mqtt_publisher import MqttPublisher from saic_api_listener import MqttGatewaySaicApiListener -from vehicle import VehicleState +from vehicle import RefreshMode, VehicleState from vehicle_info import VehicleInfo if TYPE_CHECKING: @@ -207,6 +207,16 @@ async def on_charging_detected(self, vin: str) -> None: else: LOG.debug(f"Charging detected for unknown vin {vin}") + @override + async def on_charging_station_energy_imported( + self, vin: str, energy: float + ) -> None: + vehicle_handler = self.get_vehicle_handler(vin) + if vehicle_handler: + await vehicle_handler.handle_charging_station_energy_imported(energy) + else: + LOG.debug(f"Imported energy for unknown vin {vin} received") + @override async def on_mqtt_global_command_received( self, *, topic: str, payload: str diff --git a/src/publisher/core.py b/src/publisher/core.py index b0ffa66..0c61ddb 100644 --- a/src/publisher/core.py +++ b/src/publisher/core.py @@ -30,6 +30,12 @@ async def on_mqtt_global_command_received( ) -> None: raise NotImplementedError("Should have implemented this") + @abstractmethod + async def on_charging_station_energy_imported( + self, vin: str, energy: float + ) -> None: + raise NotImplementedError("Should have implemented this") + class Publisher(ABC): def __init__(self, config: Configuration) -> None: diff --git a/src/publisher/mqtt_publisher.py b/src/publisher/mqtt_publisher.py index cb2809a..6afab7c 100644 --- a/src/publisher/mqtt_publisher.py +++ b/src/publisher/mqtt_publisher.py @@ -26,6 +26,7 @@ def __init__(self, configuration: Configuration) -> None: self.vin_by_charge_state_topic: dict[str, str] = {} self.last_charge_state_by_vin: dict[str, str] = {} self.vin_by_charger_connected_topic: dict[str, str] = {} + self.vin_by_imported_energy_topic: dict[str, str] = {} self.first_connection = True mqtt_client = gmqtt.Client( @@ -127,6 +128,14 @@ def enable_commands(self) -> None: charging_station.connected_topic ] = charging_station.vin self.client.subscribe(charging_station.connected_topic) + if charging_station.imported_energy_topic: + LOG.debug( + f"Subscribing to MQTT topic {charging_station.daily_imported_topic}" + ) + self.vin_by_imported_energy_topic[ + charging_station.imported_energy_topic + ] = charging_station.vin + self.client.subscribe(charging_station.daily_imported_topic) if self.configuration.ha_discovery_enabled: # enable dynamic discovery pushing in case ha reconnects self.client.subscribe(self.configuration.ha_lwt_topic) @@ -166,6 +175,14 @@ async def __on_message_real(self, *, topic: str, payload: str) -> None: LOG.debug( f"Vehicle with vin {vin} is disconnected from its charging station" ) + elif topic in self.vin_by_imported_energy_topic: + LOG.debug(f"Received message over topic {topic} with payload {payload}") + vin = self.vin_by_imported_energy_topic[topic] + LOG.info(f"Received imported energy message for vehicle with vin {vin}") + if self.command_listener is not None: + await self.command_listener.on_charging_station_energy_imported( + vin, float(payload) + ) elif topic == self.configuration.ha_lwt_topic: if self.command_listener is not None: await self.command_listener.on_mqtt_global_command_received( diff --git a/tests/integrations/openwb/test_openwb_integration.py b/tests/integrations/openwb/test_openwb_integration.py index 23cdcec..a86a866 100644 --- a/tests/integrations/openwb/test_openwb_integration.py +++ b/tests/integrations/openwb/test_openwb_integration.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +import math from typing import Any import unittest @@ -29,6 +30,11 @@ SOC_TOPIC = "/mock/soc/state" SOC_TS_TOPIC = "/mock/soc/timestamp" CHARGING_VALUE = "VehicleIsCharging" +IMPORTED_ENERGY_TOPIC = "/mock/imported_energy" +CAR_CAPACITY_KWH = 50.0 +CHARGE_POLLING_MIN_PERCENT = 5.0 +ENERGY_PER_PERCENT = CAR_CAPACITY_KWH * 1000.0 / 100.0 +ENERGY_FOR_MIN_PCT = math.ceil(CHARGE_POLLING_MIN_PERCENT * ENERGY_PER_PERCENT) class TestOpenWBIntegration(unittest.IsolatedAsyncioTestCase): @@ -50,8 +56,8 @@ def setUp(self) -> None: charging_value=CHARGING_VALUE, soc_topic=SOC_TOPIC, soc_ts_topic=SOC_TS_TOPIC, + range_topic=RANGE_TOPIC, ) - charging_station.range_topic = RANGE_TOPIC self.openwb_integration = OpenWBIntegration( charging_station=charging_station, publisher=self.publisher, @@ -71,8 +77,7 @@ async def test_update_soc_with_no_bms_data(self) -> None: float(DRIVETRAIN_SOC_VEHICLE), ) self.assert_mqtt_topic( - SOC_TS_TOPIC, - int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) + SOC_TS_TOPIC, int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) ) self.assert_mqtt_topic( RANGE_TOPIC, @@ -110,8 +115,7 @@ async def test_update_soc_with_bms_data(self) -> None: DRIVETRAIN_RANGE_BMS, ) self.assert_mqtt_topic( - SOC_TS_TOPIC, - int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) + SOC_TS_TOPIC, int(datetime(2025, 1, 1, 12, 0, 0).timestamp()) ) expected_topics = { SOC_TOPIC, @@ -120,6 +124,85 @@ async def test_update_soc_with_bms_data(self) -> None: } assert expected_topics == set(self.publisher.map.keys()) + async def test_imported_energy_initial_threshold_set(self) -> None: + """Initial call should set the first threshold but not trigger a refresh.""" + imported_energy = 1000.0 # 1 kWh + + self.openwb_integration.real_total_battery_capacity = CAR_CAPACITY_KWH + self.openwb_integration.computed_refresh_by_imported_energy_wh = None + + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy, CHARGE_POLLING_MIN_PERCENT, VIN + ) + + assert not should_refresh, "Initial call should not trigger refresh" + assert ( + self.openwb_integration.computed_refresh_by_imported_energy_wh is not None + ), "Initial threshold should be computed and stored" + + async def test_imported_energy_threshold_reached_triggers_refresh(self) -> None: + """When imported energy increases beyond threshold, refresh should trigger.""" + imported_energy_start = 1000.0 # 1 kWh + threshold_wh = imported_energy_start + ENERGY_FOR_MIN_PCT + + self.openwb_integration.real_total_battery_capacity = CAR_CAPACITY_KWH + self.openwb_integration.computed_refresh_by_imported_energy_wh = threshold_wh + + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + threshold_wh, CHARGE_POLLING_MIN_PERCENT, VIN + ) + + assert should_refresh, "Refresh should trigger when threshold is reached" + + async def test_imported_energy_no_refresh_before_threshold(self) -> None: + """Should not trigger refresh before threshold is reached.""" + imported_energy_wh = 1000.0 # 1 kWh + threshold_wh = imported_energy_wh + ENERGY_FOR_MIN_PCT + + self.openwb_integration.real_total_battery_capacity = CAR_CAPACITY_KWH + self.openwb_integration.computed_refresh_by_imported_energy_wh = threshold_wh + + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh + (ENERGY_FOR_MIN_PCT / 2), + CHARGE_POLLING_MIN_PERCENT, + VIN, + ) + + assert not should_refresh, ( + "Should not refresh before threshold is fully reached" + ) + + async def test_imported_energy_updates_next_threshold(self) -> None: + """After refresh, a new threshold should be calculated.""" + imported_energy_wh = 1000.0 # 1 kWh + + self.openwb_integration.real_total_battery_capacity = CAR_CAPACITY_KWH + self.openwb_integration.computed_refresh_by_imported_energy_wh = imported_energy_wh + + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh + ENERGY_FOR_MIN_PCT, + CHARGE_POLLING_MIN_PERCENT, + VIN, + ) + + assert should_refresh, "Refresh should be triggered at the threshold" + assert ( + self.openwb_integration.computed_refresh_by_imported_energy_wh + == imported_energy_wh + ENERGY_FOR_MIN_PCT * 2 + ), "Next threshold should be correctly updated" + + async def test_imported_energy_missing_battery_capacity(self) -> None: + """If no battery capacity is known, refresh calculation should be skipped.""" + self.openwb_integration.real_total_battery_capacity = None + + result = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh=1000.0, + charge_polling_min_percent=CHARGE_POLLING_MIN_PERCENT, + vin=VIN, + ) + + assert not result, "Should not refresh if capacity is unknown" + def assert_mqtt_topic(self, topic: str, value: Any) -> None: mqtt_map = self.publisher.map if topic in mqtt_map: From 01a915a746d26cf3b66276a28e29403a5986134f Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 12:45:47 +0100 Subject: [PATCH 07/14] feat: handle imported energy reset for refresh calculation --- src/integrations/openwb/__init__.py | 14 ++++--- .../openwb/test_openwb_integration.py | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index 8e63f48..a1124d9 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -77,9 +77,11 @@ def update_openwb( def should_refresh_by_imported_energy( self, imported_energy_wh: float, charge_polling_min_percent: float, vin: str ) -> bool: - """Determine whether the vehicle status should be refreshed based on the imported energy since the last refresh. + """Determine if the vehicle status should be refreshed based on imported energy. - Returns True if a refresh should be triggered, False otherwise. + The method triggers a refresh if the imported energy since the last refresh + exceeds a threshold based on battery capacity and a minimum percentage. + If the imported energy decreases (e.g., daily reset), the threshold is recalculated. """ # Return False if battery capacity is not available if self.real_total_battery_capacity is None: @@ -95,12 +97,15 @@ def should_refresh_by_imported_energy( energy_for_min_pct = math.ceil(charge_polling_min_percent * energy_per_percent) # Initialize the refresh threshold if it hasn't been set yet - if not self.computed_refresh_by_imported_energy_wh: + if ( + not self.computed_refresh_by_imported_energy_wh + or imported_energy_wh < (self.last_imported_energy_wh or 0) + ): self.computed_refresh_by_imported_energy_wh = ( imported_energy_wh + energy_for_min_pct ) LOG.debug( - f"Initial imported energy threshold for vehicle {vin} set to " + f"Initial or reset threshold for vehicle {vin} set to " f"{self.computed_refresh_by_imported_energy_wh} Wh" ) @@ -124,5 +129,4 @@ def should_refresh_by_imported_energy( # Save the last imported energy value self.last_imported_energy_wh = imported_energy_wh - return refresh_needed diff --git a/tests/integrations/openwb/test_openwb_integration.py b/tests/integrations/openwb/test_openwb_integration.py index a86a866..dc637c2 100644 --- a/tests/integrations/openwb/test_openwb_integration.py +++ b/tests/integrations/openwb/test_openwb_integration.py @@ -203,6 +203,43 @@ async def test_imported_energy_missing_battery_capacity(self) -> None: assert not result, "Should not refresh if capacity is unknown" + async def test_imported_energy_calculation_reset_refresh(self) -> None: + """Test that the refresh threshold is recalculated if imported_energy_wh decreases, simulating a daily reset or counter rollover.""" + imported_energy_wh = 1000.0 # 1 kWh + + self.openwb_integration.real_total_battery_capacity = CAR_CAPACITY_KWH + + # Initial energy value + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh, CHARGE_POLLING_MIN_PERCENT, VIN + ) + assert not should_refresh, "Initial call should not trigger refresh" + + # Increase energy above threshold + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh + ENERGY_FOR_MIN_PCT, + CHARGE_POLLING_MIN_PERCENT, + VIN, + ) + assert should_refresh, "Refresh should trigger when threshold is reached" + + # Simulate daily reset to 0 Wh + reset_energy_wh = 0.0 + # Should NOT trigger refresh immediately + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + reset_energy_wh, CHARGE_POLLING_MIN_PERCENT, VIN + ) + assert not should_refresh, "Reset should not trigger refresh" + + # Next increase above new threshold + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + reset_energy_wh + ENERGY_FOR_MIN_PCT, + CHARGE_POLLING_MIN_PERCENT, + VIN, + ) + assert should_refresh, "Refresh should trigger after reset and threshold reached" + + def assert_mqtt_topic(self, topic: str, value: Any) -> None: mqtt_map = self.publisher.map if topic in mqtt_map: From ab872ce2ab562318fed3cb09c24b6be28ea29d22 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 12:57:12 +0100 Subject: [PATCH 08/14] charging station imported_energy_topic attribute corrected --- src/publisher/mqtt_publisher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/publisher/mqtt_publisher.py b/src/publisher/mqtt_publisher.py index 6afab7c..6faf19e 100644 --- a/src/publisher/mqtt_publisher.py +++ b/src/publisher/mqtt_publisher.py @@ -130,12 +130,12 @@ def enable_commands(self) -> None: self.client.subscribe(charging_station.connected_topic) if charging_station.imported_energy_topic: LOG.debug( - f"Subscribing to MQTT topic {charging_station.daily_imported_topic}" + f"Subscribing to MQTT topic {charging_station.imported_energy_topic}" ) self.vin_by_imported_energy_topic[ charging_station.imported_energy_topic ] = charging_station.vin - self.client.subscribe(charging_station.daily_imported_topic) + self.client.subscribe(charging_station.imported_energy_topic) if self.configuration.ha_discovery_enabled: # enable dynamic discovery pushing in case ha reconnects self.client.subscribe(self.configuration.ha_lwt_topic) From 13260b7e10cc3cfc99dc02ac0f23b01c623d1f47 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 14:09:01 +0100 Subject: [PATCH 09/14] RefreshMode enum value corrected --- src/handlers/vehicle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/vehicle.py b/src/handlers/vehicle.py index 2951857..0badff9 100644 --- a/src/handlers/vehicle.py +++ b/src/handlers/vehicle.py @@ -319,7 +319,7 @@ async def handle_charging_station_energy_imported(self, energy: float) -> None: f"Triggering refresh due to imported energy {energy} Wh for vehicle {self.vin_info.vin}" ) self.vehicle_state.set_refresh_mode( - RefreshMode.Force, "imported energy threshold reached" + RefreshMode.FORCE, "imported energy threshold reached" ) class VehicleHandlerLocator(ABC): From 2928edb07c069d7008865f2c29e77168ee7c8863 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 14:29:55 +0100 Subject: [PATCH 10/14] openWB integration uses charger connection state --- src/integrations/openwb/__init__.py | 21 +++++++++++++++---- src/mqtt_gateway.py | 14 +++++++++++++ src/publisher/core.py | 5 +++++ src/publisher/mqtt_publisher.py | 6 ++++++ .../openwb/test_openwb_integration.py | 12 +++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index a1124d9..9f3cd6a 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -31,6 +31,7 @@ def __init__( ) -> None: self.__charging_station = charging_station self.__publisher = publisher + self.charger_connected: bool | None = None self.real_total_battery_capacity: float | None = None self.last_imported_energy_wh: float | None = None self.computed_refresh_by_imported_energy_wh: float | None = None @@ -74,6 +75,13 @@ def update_openwb( no_prefix=True, ) + def set_charger_connection_state(self, connected: bool) -> None: + self.charger_connected = connected + if self.charger_connected: + LOG.info("OpenWB Integration: Charger connected") + else: + LOG.info("OpenWB Integration: Charger disconnected") + def should_refresh_by_imported_energy( self, imported_energy_wh: float, charge_polling_min_percent: float, vin: str ) -> bool: @@ -83,6 +91,12 @@ def should_refresh_by_imported_energy( exceeds a threshold based on battery capacity and a minimum percentage. If the imported energy decreases (e.g., daily reset), the threshold is recalculated. """ + # Charger connection state might not be available. If disconnected, skip the check. + if self.charger_connected is False: + LOG.debug( + f"Charger for vehicle {vin} is disconnected. Skipping imported energy check." + ) + return False # Return False if battery capacity is not available if self.real_total_battery_capacity is None: LOG.warning( @@ -97,9 +111,8 @@ def should_refresh_by_imported_energy( energy_for_min_pct = math.ceil(charge_polling_min_percent * energy_per_percent) # Initialize the refresh threshold if it hasn't been set yet - if ( - not self.computed_refresh_by_imported_energy_wh - or imported_energy_wh < (self.last_imported_energy_wh or 0) + if not self.computed_refresh_by_imported_energy_wh or imported_energy_wh < ( + self.last_imported_energy_wh or 0 ): self.computed_refresh_by_imported_energy_wh = ( imported_energy_wh + energy_for_min_pct @@ -113,7 +126,7 @@ def should_refresh_by_imported_energy( refresh_needed = False if imported_energy_wh >= self.computed_refresh_by_imported_energy_wh: LOG.info( - f"Imported energy threshold of {self.computed_refresh_by_imported_energy_wh} Wh reached " + f"OpenWB Integration: Imported energy threshold of {self.computed_refresh_by_imported_energy_wh} Wh reached " f"(current: {imported_energy_wh} Wh). Triggering vehicle refresh." ) refresh_needed = True diff --git a/src/mqtt_gateway.py b/src/mqtt_gateway.py index 7ab8048..02ca5a4 100644 --- a/src/mqtt_gateway.py +++ b/src/mqtt_gateway.py @@ -217,6 +217,20 @@ async def on_charging_station_energy_imported( else: LOG.debug(f"Imported energy for unknown vin {vin} received") + @override + async def on_charger_connection_state_changed( + self, vin: str, connected: bool + ) -> None: + vehicle_handler = self.get_vehicle_handler(vin) + if not vehicle_handler: + LOG.debug(f"Connected state change for unknown vin {vin} received") + return + + integration = vehicle_handler.openwb_integration + if integration: + integration.set_charger_connection_state(connected) + LOG.debug(f"Updated connected state for vehicle {vin} to {connected}") + @override async def on_mqtt_global_command_received( self, *, topic: str, payload: str diff --git a/src/publisher/core.py b/src/publisher/core.py index 0c61ddb..072b1d9 100644 --- a/src/publisher/core.py +++ b/src/publisher/core.py @@ -35,6 +35,11 @@ async def on_charging_station_energy_imported( self, vin: str, energy: float ) -> None: raise NotImplementedError("Should have implemented this") + @abstractmethod + async def on_charger_connection_state_changed( + self, vin: str, connected: bool + ) -> None: + raise NotImplementedError("Should have implemented this") class Publisher(ABC): diff --git a/src/publisher/mqtt_publisher.py b/src/publisher/mqtt_publisher.py index 6faf19e..5644f63 100644 --- a/src/publisher/mqtt_publisher.py +++ b/src/publisher/mqtt_publisher.py @@ -171,10 +171,16 @@ async def __on_message_real(self, *, topic: str, payload: str) -> None: LOG.debug( f"Vehicle with vin {vin} is connected to its charging station" ) + charger_connected = True else: LOG.debug( f"Vehicle with vin {vin} is disconnected from its charging station" ) + charger_connected = False + if self.command_listener is not None: + await self.command_listener.on_charger_connection_state_changed( + vin, charger_connected + ) elif topic in self.vin_by_imported_energy_topic: LOG.debug(f"Received message over topic {topic} with payload {payload}") vin = self.vin_by_imported_energy_topic[topic] diff --git a/tests/integrations/openwb/test_openwb_integration.py b/tests/integrations/openwb/test_openwb_integration.py index dc637c2..04f1d3a 100644 --- a/tests/integrations/openwb/test_openwb_integration.py +++ b/tests/integrations/openwb/test_openwb_integration.py @@ -203,6 +203,18 @@ async def test_imported_energy_missing_battery_capacity(self) -> None: assert not result, "Should not refresh if capacity is unknown" + async def test_imported_energy_charger_disconnected(self) -> None: + """If charger is disconnected, refresh calculation should be skipped.""" + self.openwb_integration.charger_connected = False + + should_refresh = self.openwb_integration.should_refresh_by_imported_energy( + imported_energy_wh=1000.0, + charge_polling_min_percent=CHARGE_POLLING_MIN_PERCENT, + vin=VIN, + ) + + assert not should_refresh, "Should not refresh if charger is disconnected" + async def test_imported_energy_calculation_reset_refresh(self) -> None: """Test that the refresh threshold is recalculated if imported_energy_wh decreases, simulating a daily reset or counter rollover.""" imported_energy_wh = 1000.0 # 1 kWh From 811f04362c67ab66c839a1ca399c847b059ab940 Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 14:36:23 +0100 Subject: [PATCH 11/14] fix: log charger connection only on state change --- src/integrations/openwb/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index 9f3cd6a..d76129c 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -76,6 +76,10 @@ def update_openwb( ) def set_charger_connection_state(self, connected: bool) -> None: + if self.charger_connected == connected: + # No change, do nothing + return + self.charger_connected = connected if self.charger_connected: LOG.info("OpenWB Integration: Charger connected") From 216dca527ac91c5082189a5603b87ced82f845cc Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sun, 2 Nov 2025 14:52:46 +0100 Subject: [PATCH 12/14] add vin to charger connection status messages --- src/integrations/openwb/__init__.py | 6 +++--- src/mqtt_gateway.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/integrations/openwb/__init__.py b/src/integrations/openwb/__init__.py index d76129c..90e6f43 100644 --- a/src/integrations/openwb/__init__.py +++ b/src/integrations/openwb/__init__.py @@ -75,16 +75,16 @@ def update_openwb( no_prefix=True, ) - def set_charger_connection_state(self, connected: bool) -> None: + def set_charger_connection_state(self, connected: bool, vin: str) -> None: if self.charger_connected == connected: # No change, do nothing return self.charger_connected = connected if self.charger_connected: - LOG.info("OpenWB Integration: Charger connected") + LOG.info("OpenWB Integration: Charger connected to vehicle %s", vin) else: - LOG.info("OpenWB Integration: Charger disconnected") + LOG.info("OpenWB Integration: Charger disconnected from vehicle %s", vin) def should_refresh_by_imported_energy( self, imported_energy_wh: float, charge_polling_min_percent: float, vin: str diff --git a/src/mqtt_gateway.py b/src/mqtt_gateway.py index 02ca5a4..34878a7 100644 --- a/src/mqtt_gateway.py +++ b/src/mqtt_gateway.py @@ -228,7 +228,7 @@ async def on_charger_connection_state_changed( integration = vehicle_handler.openwb_integration if integration: - integration.set_charger_connection_state(connected) + integration.set_charger_connection_state(connected, vin) LOG.debug(f"Updated connected state for vehicle {vin} to {connected}") @override From d1f811c76d0d6640baa62caf458cdc3e585941bc Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sat, 8 Nov 2025 17:47:45 +0100 Subject: [PATCH 13/14] openWB energy-based refresh logic #387 --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b3bdf42..e24489c 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,15 @@ The key-value pairs in the JSON express the following: | chargerConnectedTopic | topic indicating that the vehicle is connected to the charging station - optional | | chargerConnectedValue | payload that indicates that the charger is connected - optional | | vin | vehicle identification number to map the charging station information to a vehicle - **required** | +­| importedEnergyTopic | topic where the charging station publishes the amount of imported energy (in Wh) - optional | + +#### `importedEnergyTopic` + +This topic provides the amount of energy imported by the charging station, measured in watt-hours. + +During the charging process, the system normally calculates the vehicle status refresh interval based on the current charging power and the vehicle’s battery capacity. However, this estimation can become inaccurate during PV surplus charging, where the charging power fluctuates significantly. + +If the charging station can report the total imported energy (for example, the `daily_imported` topic provided by openWB), this value can be used instead to determine when to refresh the vehicle status. This approach ensures more reliable updates even under variable charging conditions. ### Advanced settings From 9e2ce0cc22bdfafcc649868ee054d9f40ef8b67a Mon Sep 17 00:00:00 2001 From: Thomas Salm Date: Sat, 8 Nov 2025 17:55:55 +0100 Subject: [PATCH 14/14] unused vehicle.RefreshMode import removed --- src/mqtt_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mqtt_gateway.py b/src/mqtt_gateway.py index 34878a7..c30ef5d 100644 --- a/src/mqtt_gateway.py +++ b/src/mqtt_gateway.py @@ -20,7 +20,7 @@ from publisher.log_publisher import ConsolePublisher from publisher.mqtt_publisher import MqttPublisher from saic_api_listener import MqttGatewaySaicApiListener -from vehicle import RefreshMode, VehicleState +from vehicle import VehicleState from vehicle_info import VehicleInfo if TYPE_CHECKING: