diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 2846b1db46..3988426ab9 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -20,6 +20,8 @@ log = logging.getLogger(__name__) mqtt_log = logging.getLogger("mqtt") +TIMESTAMP_2100 = 4102441200 # 01.01.2100 00:00:00 + class SetData: def __init__(self, @@ -385,7 +387,7 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, int, [(0, float("inf"))]) elif ("/get/soc_request_timestamp" in msg.topic or "/get/soc_timestamp" in msg.topic): - self._validate_value(msg, float) + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) elif "/get/soc" in msg.topic: self._validate_value(msg, float, [(0, 100)]) elif "/get/range" in msg.topic: @@ -557,7 +559,7 @@ def process_chargepoint_get_topics(self, msg): self._validate_value(msg, str) elif ("/get/error_timestamp" in msg.topic or "/get/rfid_timestamp" in msg.topic): - self._validate_value(msg, float) + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) elif ("/get/fault_str" in msg.topic or "/get/state_str" in msg.topic or "/get/heartbeat" in msg.topic or @@ -735,7 +737,7 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): "openWB/set/general/mqtt_bridge" in msg.topic): self._validate_value(msg, bool) elif "openWB/set/general/grid_protection_timestamp" in msg.topic: - self._validate_value(msg, float) + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) elif "openWB/set/general/grid_protection_random_stop" in msg.topic: self._validate_value(msg, int, [(0, 90)]) elif "openWB/set/general/notifications/selected" in msg.topic: @@ -786,7 +788,7 @@ def process_io_topic(self, msg: mqtt.MQTTMessage): elif "get/fault_str" in msg.topic: self._validate_value(msg, str) elif "/timestamp" in msg.topic: - self._validate_value(msg, float) + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) else: self.__unknown_topic(msg) except Exception: @@ -1026,7 +1028,7 @@ def process_system_topic(self, msg: mqtt.MQTTMessage): elif "/config" in msg.topic: self._validate_value(msg, "json") elif "/error_timestamp" in msg.topic: - self._validate_value(msg, float, [(0, float("inf"))]) + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) elif "/get/fault_state" in msg.topic: self._validate_value(msg, int, [(0, 2)]) elif "/get/fault_str" in msg.topic: diff --git a/packages/modules/common/abstract_vehicle.py b/packages/modules/common/abstract_vehicle.py index 3772b4c63d..02e4b36122 100644 --- a/packages/modules/common/abstract_vehicle.py +++ b/packages/modules/common/abstract_vehicle.py @@ -11,7 +11,8 @@ class VehicleUpdateData: efficiency: float = 90 soc_from_cp: Optional[float] = None timestamp_soc_from_cp: Optional[int] = None - soc_timestamp: Optional[int] = None + last_soc_timestamp: Optional[int] = None + last_soc: float = None @dataclass @@ -24,6 +25,5 @@ class GeneralVehicleConfig: @dataclass class CalculatedSocState: - imported_start: Optional[float] = 0 # don't show in UI + last_imported: Optional[float] = 0 # don't show in UI manual_soc: Optional[int] = None # don't show in UI - soc_start: float = 0 # don't show in UI diff --git a/packages/modules/common/configurable_vehicle.py b/packages/modules/common/configurable_vehicle.py index f829b7e40f..e3f608c760 100644 --- a/packages/modules/common/configurable_vehicle.py +++ b/packages/modules/common/configurable_vehicle.py @@ -69,6 +69,10 @@ def update(self, vehicle_update_data: VehicleUpdateData): log.debug(f"General Config {self.general_config}") with SingleComponentUpdateContext(self.fault_state, self.__initializer): + if vehicle_update_data.imported is None: + self.calculated_soc_state.last_imported = None + Pub().pub(f"openWB/set/vehicle/{self.vehicle}/soc_module/calculated_soc_state", + asdict(self.calculated_soc_state)) source = self._get_carstate_source(vehicle_update_data) if source == SocSource.NO_UPDATE: log.debug("No soc update necessary.") @@ -78,26 +82,18 @@ def update(self, vehicle_update_data: VehicleUpdateData): log.debug("Mqtt uses legacy topics.") return log.debug(f"Requested start soc from {source.value}: {car_state.soc}%") - - if (source != SocSource.CALCULATION or - (vehicle_update_data.imported and self.calculated_soc_state.imported_start is None)): - # Wenn nicht berechnet wurde, SoC als Start merken. - self.calculated_soc_state.imported_start = vehicle_update_data.imported - self.calculated_soc_state.soc_start = car_state.soc - Pub().pub(f"openWB/set/vehicle/{self.vehicle}/soc_module/calculated_soc_state", - asdict(self.calculated_soc_state)) - if (vehicle_update_data.soc_timestamp is None or - vehicle_update_data.soc_timestamp <= car_state.soc_timestamp + 60): + if (vehicle_update_data.last_soc_timestamp is None or + vehicle_update_data.last_soc_timestamp <= car_state.soc_timestamp + 60): # Nur wenn der SoC neuer ist als der bisherige, diesen setzen. # Manche Fahrzeuge liefern in Ladepausen zwar einen SoC, aber manchmal einen alten. # Die Pro liefert manchmal den SoC nicht, bis nach dem Anstecken das SoC-Update getriggert wird. # Wenn Sie dann doch noch den alten SoC liefert, darf dieser nicht verworfen werden. self.store.set(car_state) - elif vehicle_update_data.soc_timestamp > 1e10: - # car_state ist in ms geschrieben, dieser kann überschrieben werden - self.store.set(car_state) else: log.debug("Not updating SoC, because timestamp is older.") + self.calculated_soc_state.last_imported = vehicle_update_data.imported + Pub().pub(f"openWB/set/vehicle/{self.vehicle}/soc_module/calculated_soc_state", + asdict(self.calculated_soc_state)) def _get_carstate_source(self, vehicle_update_data: VehicleUpdateData) -> SocSource: # Kein SoC vom LP vorhanden oder erwünscht @@ -106,7 +102,7 @@ def _get_carstate_source(self, vehicle_update_data: VehicleUpdateData) -> SocSou self.calculated_soc_state.manual_soc is not None): if isinstance(self.vehicle_config, ManualSoc): # Wenn ein manueller SoC gesetzt wurde, diesen als neuen Start merken. - if self.calculated_soc_state.manual_soc is not None or self.calculated_soc_state.imported_start is None: + if self.calculated_soc_state.manual_soc is not None: return SocSource.MANUAL else: if vehicle_update_data.plug_state: @@ -134,8 +130,7 @@ def _get_carstate_by_source(self, vehicle_update_data: VehicleUpdateData, source return CarState(soc=calc_soc.calc_soc( vehicle_update_data, vehicle_update_data.efficiency, - self.calculated_soc_state.imported_start or vehicle_update_data.imported, - self.calculated_soc_state.soc_start, + self.calculated_soc_state.last_imported or vehicle_update_data.imported, vehicle_update_data.battery_capacity)) elif source == SocSource.CP: return CarState(soc=vehicle_update_data.soc_from_cp, @@ -144,7 +139,7 @@ def _get_carstate_by_source(self, vehicle_update_data: VehicleUpdateData, source if self.calculated_soc_state.manual_soc is not None: soc = self.calculated_soc_state.manual_soc else: - soc = self.calculated_soc_state.soc_start + raise ValueError("Manual soc source selected, but no manual soc set.") self.calculated_soc_state.manual_soc = None return CarState(soc) diff --git a/packages/modules/common/configurable_vehicle_test.py b/packages/modules/common/configurable_vehicle_test.py index f31bcf6603..90fed2a5a3 100644 --- a/packages/modules/common/configurable_vehicle_test.py +++ b/packages/modules/common/configurable_vehicle_test.py @@ -91,19 +91,19 @@ def conf_vehicle_mqtt(): [ pytest.param(conf_vehicle_manual(), False, VehicleUpdateData(), CalculatedSocState( manual_soc=34), SocSource.MANUAL, id="Manuell, neuer Start-SoC"), - pytest.param(conf_vehicle_manual(), False, VehicleUpdateData(plug_state=True), CalculatedSocState( - soc_start=34), SocSource.CALCULATION, id="Manuell berechnen"), - pytest.param(conf_vehicle_manual(), False, VehicleUpdateData(), CalculatedSocState( - soc_start=34), SocSource.NO_UPDATE, id="Manuell nicht aktualisieren, da nicht angesteckt"), + pytest.param(conf_vehicle_manual(), False, VehicleUpdateData(plug_state=True, last_soc=34), CalculatedSocState( + ), SocSource.CALCULATION, id="Manuell berechnen"), + pytest.param(conf_vehicle_manual(), False, VehicleUpdateData(last_soc=34), CalculatedSocState(), + SocSource.NO_UPDATE, id="Manuell nicht aktualisieren, da nicht angesteckt"), pytest.param(conf_vehicle_manual_from_cp(), True, VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID), CalculatedSocState(manual_soc=34), SocSource.MANUAL, id="Manuell mit SoC vom LP, neuer Start-SoC"), pytest.param(conf_vehicle_manual_from_cp(), True, - VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_VALID), - CalculatedSocState(soc_start=34), SocSource.CP, id="Manuell mit SoC vom LP, neuer LP-SoC"), + VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_VALID, last_soc=34), + CalculatedSocState(), SocSource.CP, id="Manuell mit SoC vom LP, neuer LP-SoC"), pytest.param(conf_vehicle_manual_from_cp(), True, - VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID), - CalculatedSocState(soc_start=34), SocSource.CALCULATION, + VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID, last_soc=34), + CalculatedSocState(), SocSource.CALCULATION, id="Manuell mit SoC vom LP, LP-SoC berechnen"), pytest.param(conf_vehicle_api(), True, VehicleUpdateData(), CalculatedSocState(), SocSource.API, id="API"), pytest.param(conf_vehicle_api_from_cp(), True, VehicleUpdateData( @@ -112,8 +112,8 @@ def conf_vehicle_mqtt(): pytest.param(conf_vehicle_api_from_cp(), True, VehicleUpdateData(soc_from_cp=None), CalculatedSocState(), SocSource.API, id="API mit SoC vom LP, kein LP-SoC"), pytest.param(conf_vehicle_api_from_cp(), True, - VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID), - CalculatedSocState(soc_start=34), SocSource.CALCULATION, + VehicleUpdateData(soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID, last_soc=34), + CalculatedSocState(), SocSource.CALCULATION, id="API mit SoC vom LP, LP-SoC berechnen"), pytest.param(conf_vehicle_api_while_charging(), False, VehicleUpdateData(), CalculatedSocState(), SocSource.API, id="API mit Berechnung, keine Ladung"), @@ -141,11 +141,11 @@ def test_get_carstate_source(conf_vehicle: ConfigurableVehicle, @pytest.mark.parametrize( "vehicle_update_data, use_soc_from_cp, expected_calculated_soc_state, expected_call_count", [ - pytest.param(VehicleUpdateData(), False, CalculatedSocState(soc_start=42), 1, id="request only from api"), - pytest.param(VehicleUpdateData(imported=150), True, CalculatedSocState( - soc_start=42, imported_start=150), 1, id="request from api, not plugged"), - pytest.param(VehicleUpdateData(imported=200, plug_state=True), True, CalculatedSocState( - soc_start=42, imported_start=200), 1, id="request from api, recently plugged"), + pytest.param(VehicleUpdateData(last_soc=42), False, CalculatedSocState(), 1, id="request only from api"), + pytest.param(VehicleUpdateData(imported=150, last_soc=42), True, CalculatedSocState( + last_imported=150), 1, id="request from api, not plugged"), + pytest.param(VehicleUpdateData(imported=200, plug_state=True, last_soc=42), True, + CalculatedSocState(last_imported=200), 1, id="request from api, recently plugged"), ]) def test_update_api(vehicle_update_data, use_soc_from_cp, @@ -191,7 +191,7 @@ def test_1(monkeypatch): # evaluation assert mock_value_store.set.call_args[0][0].soc == 45 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=45) + assert c.calculated_soc_state == CalculatedSocState(manual_soc=None) def test_2(monkeypatch): @@ -213,8 +213,6 @@ def test_2(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[3][0][0].soc == 47 - assert mock_calc_soc.call_args[0][3] == 45 # soc - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=45) def test_3(monkeypatch): @@ -237,7 +235,6 @@ def test_3(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[3][0][0].soc == 44 assert mock_calc_soc.call_count == 2 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_4(monkeypatch): @@ -259,7 +256,6 @@ def test_4(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[2][0][0].soc == 44 assert mock_calc_soc.call_count == 2 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_5(monkeypatch): @@ -281,7 +277,6 @@ def test_5(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[2][0][0].soc == 44 assert mock_calc_soc.call_count == 1 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_6(monkeypatch): @@ -292,7 +287,6 @@ def test_6(monkeypatch): mock_value_store = Mock(name="value_store") monkeypatch.setattr(store, "get_car_value_store", Mock(return_value=mock_value_store)) c = conf_vehicle_manual_from_cp() - c.calculated_soc_state = CalculatedSocState(manual_soc=None, soc_start=42) # execution c.update(VehicleUpdateData(plug_state=True, soc_from_cp=45, timestamp_soc_from_cp=TIMESTAMP_SOC_INVALID)) @@ -300,7 +294,6 @@ def test_6(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[0][0][0].soc == 44 assert mock_calc_soc.call_count == 1 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_7(monkeypatch): @@ -318,7 +311,6 @@ def test_7(monkeypatch): # evaluation assert mock_value_store.set.call_args[0][0].soc == 42 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_8(monkeypatch): @@ -336,7 +328,6 @@ def test_8(monkeypatch): # evaluation assert mock_value_store.set.call_args[0][0].soc == 42 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) def test_9(monkeypatch): @@ -355,7 +346,6 @@ def test_9(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[1][0][0].soc == 46 assert mock_calc_soc.call_count == 1 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=45) def test_10(monkeypatch): @@ -370,7 +360,6 @@ def test_10(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[0][0][0].soc == 42 - assert c.calculated_soc_state == CalculatedSocState(soc_start=42) def test_11(monkeypatch): @@ -390,4 +379,3 @@ def test_11(monkeypatch): # evaluation assert mock_value_store.set.call_args_list[1][0][0].soc == 44 assert mock_calc_soc.call_count == 1 - assert c.calculated_soc_state == CalculatedSocState(manual_soc=None, soc_start=42) diff --git a/packages/modules/update_soc.py b/packages/modules/update_soc.py index 998f4caab7..cbf3974b17 100644 --- a/packages/modules/update_soc.py +++ b/packages/modules/update_soc.py @@ -127,7 +127,8 @@ def _get_vehicle_update_data(self, ev_num: int) -> VehicleUpdateData: battery_capacity=battery_capacity, soc_from_cp=soc_from_cp, timestamp_soc_from_cp=timestamp_soc_from_cp, - soc_timestamp=soc_timestamp) + last_soc_timestamp=soc_timestamp, + last_soc=ev.data.get.soc) def _filter_failed_store_threads(self, threads_store: List[Thread]) -> List[Thread]: ev_data = copy.deepcopy(subdata.SubData.ev_data) diff --git a/packages/modules/vehicles/common/calc_soc/calc_soc.py b/packages/modules/vehicles/common/calc_soc/calc_soc.py index 8a4871f223..be0c032be3 100644 --- a/packages/modules/vehicles/common/calc_soc/calc_soc.py +++ b/packages/modules/vehicles/common/calc_soc/calc_soc.py @@ -7,18 +7,17 @@ def calc_soc(vehicle_update_data: VehicleUpdateData, efficiency: int, - imported_start: float, - soc_start: float, + last_imported: float, battery_capacity: float) -> float: - imported_since_start = vehicle_update_data.imported - imported_start - energy_battery_gain = imported_since_start * efficiency / 100 + imported_since_last_soc = vehicle_update_data.imported - last_imported + energy_battery_gain = imported_since_last_soc * efficiency / 100 battery_soc_gain = (energy_battery_gain / battery_capacity) * 100 - soc = soc_start + battery_soc_gain + soc = vehicle_update_data.last_soc + battery_soc_gain log.debug( - f"SoC-Gain: (({imported_since_start/1000}kWh charged * {efficiency}% efficiency) / " + f"SoC-Gain: (({imported_since_last_soc/1000}kWh charged * {efficiency}% efficiency) / " f"{battery_capacity/1000}kWh battery-size) * 100 = {battery_soc_gain}%", ) - log.debug(f"{soc_start}% + {energy_battery_gain / 1000}kWh = {soc}%") + log.debug(f"{vehicle_update_data.last_soc}% + {energy_battery_gain / 1000}kWh = {soc}%") if soc > 100: log.warning(f"Calculated SoC of {soc}% exceeds maximum and is limited to 100%! Check your settings!") soc = 100 diff --git a/packages/modules/vehicles/common/calc_soc/calc_soc_test.py b/packages/modules/vehicles/common/calc_soc/calc_soc_test.py index e4bba669ab..e5b20d2cf8 100644 --- a/packages/modules/vehicles/common/calc_soc/calc_soc_test.py +++ b/packages/modules/vehicles/common/calc_soc/calc_soc_test.py @@ -4,7 +4,8 @@ def test_calc_soc(): # setup & execution - soc = calc_soc(VehicleUpdateData(imported=10000), 90, 0, 12.6, 100000) + soc = calc_soc(VehicleUpdateData(imported=10000, last_soc=12.6), + efficiency=90, last_imported=0, battery_capacity=100000) # evaluation assert soc == 21.6