diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index 1bde8c85ea..bdb40e7ace 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -120,6 +120,7 @@ def collect_data(chargepoint): log_data.ev = get_value_or_default(lambda: chargepoint.data.set.charging_ev_data.num, 0) log_data.prio = get_value_or_default(lambda: chargepoint.data.control_parameter.prio, False) log_data.rfid = get_value_or_default(lambda: chargepoint.data.set.rfid) + log_data.odometer = get_value_or_default(lambda: charging_ev.data.get.odometer) log_data.imported_since_mode_switch = get_value_or_default( lambda: chargepoint.data.get.imported - log_data.imported_at_mode_switch, 0) log_data.exported_since_mode_switch = get_value_or_default( @@ -270,6 +271,7 @@ def _create_entry(chargepoint, charging_ev): "chargemode": get_value_or_default(lambda: log_data.chargemode_log_entry), "prio": get_value_or_default(lambda: log_data.prio), "rfid": get_value_or_default(lambda: log_data.rfid), + "odometer": get_value_or_default(lambda: log_data.odometer), "soc_at_start": get_value_or_default(lambda: log_data.soc_at_start), "soc_at_end": get_value_or_default(lambda: charging_ev.data.get.soc), "range_at_start": get_value_or_default(lambda: log_data.range_at_start), diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index b346e336aa..e112386f0d 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -96,6 +96,7 @@ class Log: soc_at_end: Optional[int] = None range_at_start: Optional[float] = None range_at_end: Optional[float] = None + odometer: Optional[float] = None def connected_vehicle_factory() -> ConnectedVehicle: diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 47005c40b3..c548f5b25b 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -63,6 +63,7 @@ class Get: force_soc_update: bool = field(default=False, metadata={ "topic": "get/force_soc_update"}) range: Optional[float] = field(default=None, metadata={"topic": "get/range"}) + odometer: Optional[float] = field(default=None, metadata={"topic": "get/odometer"}) fault_state: int = field(default=0, metadata={"topic": "get/fault_state"}) fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 25ba9d314c..d54c4da024 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -34,6 +34,7 @@ def get_default_charge_log_columns() -> Dict: "vehicle_chargemode": True, "vehicle_prio": True, "vehicle_rfid": True, + "vehicle_odometer": False, "vehicle_soc_at_start": False, "vehicle_soc_at_end": False, "chargepoint_name": True, diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index d54eb93c38..455a8e4ad7 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -393,6 +393,8 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float, [(0, 100)]) elif "/get/range" in msg.topic: self._validate_value(msg, float, [(0, 1000)]) + elif "/get/odometer" in msg.topic: + self._validate_value(msg, float, [(0, 9999999)]) elif "/get/force_soc_update" in msg.topic: self._validate_value(msg, bool) else: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index f79bff292e..7fe4257a04 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 113 + DATASTORE_VERSION = 114 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2955,6 +2955,15 @@ def upgrade(topic: str, payload) -> None: f"'{device_name}' auf Typ '{new_type}'."), MessageType.INFO, ) - self._loop_all_received_topics(upgrade) self._append_datastore_version(113) + + def upgrade_datastore_114(self) -> None: + def upgrade(topic: str, payload) -> None: + if "openWB/general/charge_log_data_config" == topic: + config = decode_payload(payload) + if config.get("vehicle_odometer") is None: + config["vehicle_odometer"] = False + return {topic: config} + self._loop_all_received_topics(upgrade) + self._append_datastore_version(114) diff --git a/packages/modules/common/component_state.py b/packages/modules/common/component_state.py index d483111ed8..be27fda208 100644 --- a/packages/modules/common/component_state.py +++ b/packages/modules/common/component_state.py @@ -155,11 +155,15 @@ def __init__( @auto_str class CarState: - def __init__(self, soc: float, range: Optional[float] = None, soc_timestamp: Optional[float] = None): + def __init__(self, soc: float, + range: Optional[float] = None, + soc_timestamp: Optional[float] = None, + odometer: Optional[float] = None): """Args: soc: actual state of charge in percent range: actual range in km soc_timestamp: timestamp of last request as unix timestamp + odometer: actual odometer of vehicle in km """ self.soc = soc self.range = range @@ -170,6 +174,7 @@ def __init__(self, soc: float, range: Optional[float] = None, soc_timestamp: Opt log.debug(f'Zeitstempel {soc_timestamp} ist in ms, wird in s gewandelt. Modul sollte angepasst werden.') soc_timestamp /= 1000 self.soc_timestamp = soc_timestamp + self.odometer = odometer @auto_str diff --git a/packages/modules/common/configurable_vehicle.py b/packages/modules/common/configurable_vehicle.py index 9ff600c015..1456f9cb36 100644 --- a/packages/modules/common/configurable_vehicle.py +++ b/packages/modules/common/configurable_vehicle.py @@ -129,6 +129,7 @@ def _get_carstate_by_source(self, vehicle_update_data: VehicleUpdateData, source if source == SocSource.API: try: _carState = self.__component_updater(vehicle_update_data) + _odometer = _carState.odometer _now = int(time.time()) _diff = 0 if _carState.soc_timestamp: @@ -137,7 +138,8 @@ def _get_carstate_by_source(self, vehicle_update_data: VehicleUpdateData, source vehicle_update_data.plug_state and\ vehicle_update_data.last_soc and\ vehicle_update_data.last_soc_timestamp >= vehicle_update_data.plug_time and\ - (self.calculated_soc_state.last_imported or vehicle_update_data.imported): + vehicle_update_data.imported -\ + (self.calculated_soc_state.last_imported or vehicle_update_data.imported) > 0: _age = int(self.general_config.request_interval_charging / 60) _txt1 = "Ladestand und Reichweite sind berechnet, da der von der Online-Abfrage " _txt1 = _txt1 + f"gelieferte Zeitstempel mehr als {_age} min alt ist." @@ -146,6 +148,7 @@ def _get_carstate_by_source(self, vehicle_update_data: VehicleUpdateData, source _carState = calc_vehicle_data.calc_vehicle_data(vehicle_update_data, self.calculated_soc_state.last_imported or vehicle_update_data.imported) + _carState.odometer = _odometer except Exception as e: if vehicle_update_data.plug_state and\ vehicle_update_data.last_soc and\ diff --git a/packages/modules/common/store/_car.py b/packages/modules/common/store/_car.py index 37c7d64a30..499e052323 100644 --- a/packages/modules/common/store/_car.py +++ b/packages/modules/common/store/_car.py @@ -25,6 +25,7 @@ def update(self): pub_to_broker("openWB/set/vehicle/"+str(self.vehicle_id)+"/get/soc", self.state.soc, 2) pub_to_broker("openWB/set/vehicle/"+str(self.vehicle_id)+"/get/range", self.state.range, 2) pub_to_broker("openWB/set/vehicle/"+str(self.vehicle_id)+"/get/soc_timestamp", self.state.soc_timestamp) + pub_to_broker("openWB/set/vehicle/"+str(self.vehicle_id)+"/get/odometer", self.state.odometer, 2) def get_car_value_store(id: int) -> ValueStore[CarState]: diff --git a/packages/modules/update_soc.py b/packages/modules/update_soc.py index 10221c6517..10c5c31cfb 100644 --- a/packages/modules/update_soc.py +++ b/packages/modules/update_soc.py @@ -63,6 +63,7 @@ def _get_threads(self) -> Tuple[List[Thread], List[Thread]]: f"EV{ev.num}: Nach dreimaliger erfolgloser SoC-Abfrage wird ein SoC von 0% angenommen.") Pub().pub(f"openWB/set/vehicle/{ev.num}/get/soc", 0) Pub().pub(f"openWB/set/vehicle/{ev.num}/get/range", None) + Pub().pub(f"openWB/set/vehicle/{ev.num}/get/odometer", None) # Es wird ein Zeitstempel gesetzt, unabhängig ob die Abfrage erfolgreich war, da einige # Hersteller bei zu häufigen Abfragen Accounts sperren. Pub().pub(f"openWB/set/vehicle/{ev.num}/get/soc_request_timestamp", @@ -86,6 +87,8 @@ def _get_threads(self) -> Tuple[List[Thread], List[Thread]]: Pub().pub(f"openWB/set/vehicle/{ev.num}/get/soc_request_timestamp", None) if ev.data.get.range is not None: Pub().pub(f"openWB/set/vehicle/{ev.num}/get/range", None) + if ev.data.get.odometer is not None: + Pub().pub(f"openWB/set/vehicle/{ev.num}/get/odometer", None) except Exception: log.exception("Fehler im update_soc-Modul") return threads_update, threads_store diff --git a/packages/modules/vehicles/mqtt/soc.py b/packages/modules/vehicles/mqtt/soc.py index b5b49524e6..fbfd56bb96 100644 --- a/packages/modules/vehicles/mqtt/soc.py +++ b/packages/modules/vehicles/mqtt/soc.py @@ -29,7 +29,8 @@ def on_message(client, userdata, message): topic_prefix = f"openWB/mqtt/vehicle/{vehicle}/get/" return CarState(soc=received_topics.get(f"{topic_prefix}soc"), range=received_topics.get(f"{topic_prefix}range"), - soc_timestamp=received_topics.get(f"{topic_prefix}soc_timestamp")) + soc_timestamp=received_topics.get(f"{topic_prefix}soc_timestamp"), + odometer=received_topics.get(f"{topic_prefix}odometer")) else: configurable_vehicle.fault_state.warning( f"Keine MQTT-Daten für Fahrzeug {vehicle_config.name} empfangen oder es werden " diff --git a/packages/modules/vehicles/ovms/api.py b/packages/modules/vehicles/ovms/api.py index 2e0b7e0caa..cdd530d5c0 100755 --- a/packages/modules/vehicles/ovms/api.py +++ b/packages/modules/vehicles/ovms/api.py @@ -187,17 +187,18 @@ async def _fetch_soc(self, if float(self.range) > 1000.0: self.range = str(float(self.range) / 10) - self.kms = float(statusDict['odometer']) / 10 + self.odometer = float(statusDict['odometer']) / 10 self.vehicle12v = statusDict['vehicle12v'] self.soc_ts = statusDict['m_msgtime_s'] self.soc_tsdt = datetime.strptime(self.soc_ts, date_fmt) self.soc_tsdtL = utc2local(self.soc_tsdt) self.soc_tsX = datetime.timestamp(self.soc_tsdtL) - log.info("soc=" + self.soc + ", range=" + self.range + ", soc_ts=" + str(self.soc_tsdtL)) + log.info("OVMS: soc=" + self.soc + ", range=" + self.range + ", soc_ts=" + str(self.soc_tsdtL) + + ", odometer=" + str(self.odometer)) log.debug("statusDict=\n" + dumps(statusDict, indent=4)) - return int(float(self.soc)), float(self.range), self.soc_tsX + return int(float(self.soc)), float(self.range), self.soc_tsX, float(self.odometer) # sync function @@ -209,6 +210,6 @@ def fetch_soc(conf: OVMS, vehicle: int) -> Union[int, float, str]: # get soc, range from server a = api() - soc, range, soc_ts = loop.run_until_complete(a._fetch_soc(conf, vehicle)) + soc, range, soc_ts, odometer = loop.run_until_complete(a._fetch_soc(conf, vehicle)) - return soc, range, soc_ts + return soc, range, soc_ts, odometer diff --git a/packages/modules/vehicles/ovms/soc.py b/packages/modules/vehicles/ovms/soc.py index efd95708b3..2c2e8fa058 100755 --- a/packages/modules/vehicles/ovms/soc.py +++ b/packages/modules/vehicles/ovms/soc.py @@ -15,8 +15,8 @@ def fetch(vehicle_update_data: VehicleUpdateData, config: OVMS, vehicle: int) -> CarState: - soc, range, soc_ts = api.fetch_soc(config, vehicle) - return CarState(soc=soc, range=range, soc_timestamp=soc_ts) + soc, range, soc_ts, odometer = api.fetch_soc(config, vehicle) + return CarState(soc=soc, range=range, soc_timestamp=soc_ts, odometer=odometer) def create_vehicle(vehicle_config: OVMS, vehicle: int): diff --git a/packages/modules/vehicles/vwgroup/vwgroup.py b/packages/modules/vehicles/vwgroup/vwgroup.py index 758cb8e0e8..35a92d0d94 100644 --- a/packages/modules/vehicles/vwgroup/vwgroup.py +++ b/packages/modules/vehicles/vwgroup/vwgroup.py @@ -86,6 +86,10 @@ async def request_data(self, library) -> Union[int, float, str]: soc_tsdtL = self.utc2local(soc_tsdtZ) self.soc_tsX = datetime.timestamp(soc_tsdtL) self.soc_ts = datetime.strftime(soc_tsdtL, ts_fmt) + if self.su.keys_exist(self.data, 'charging', 'batteryStatus', 'value', 'odometer'): + self.odometer = float(self.data['charging']['batteryStatus']['value']['odometer']) + else: + self.odometer = None except Exception as e: raise Exception("soc/range/soc_ts field missing exception: e=" + str(e)) @@ -131,4 +135,4 @@ async def request_data(self, library) -> Union[int, float, str]: if (library.tokens['accessToken'] != self.accessTokenOld): # modified accessToken? self.su.write_token_file(self.accessTokenFile, library.tokens['accessToken']) - return self.soc, self.range, self.soc_ts, self.soc_tsX + return self.soc, self.range, self.soc_ts, self.soc_tsX, self.odometer diff --git a/packages/modules/vehicles/vwid/libvwid.py b/packages/modules/vehicles/vwid/libvwid.py index 0209d1b761..2018906f56 100755 --- a/packages/modules/vehicles/vwid/libvwid.py +++ b/packages/modules/vehicles/vwid/libvwid.py @@ -62,6 +62,7 @@ class Services: CHARGING = "charging" PARAMETERS = "parameters" SERVICE_STATUS = "service_status" + MEASUREMENTS = "measurements" def find_path_in_dict(src, path) -> object: @@ -310,6 +311,7 @@ async def update(self): self.get_selectivestatus( [ Services.CHARGING, + Services.MEASUREMENTS, ] ) ) @@ -419,7 +421,7 @@ async def expired(self, service): expiration = datetime.now(UTC) + timedelta(days=1) expiration = expiration.replace(tzinfo=None) if now >= expiration: - _LOGGER.warning("Access to %s has expired!", service) + _LOGGER.info("Access to %s has expired!", service) self._discovered = False return True except Exception: @@ -1425,12 +1427,13 @@ async def get_status(self): data['charging']['batteryStatus']['value']['currentSOC_pct'] = str(0) data['charging']['batteryStatus']['value']['cruisingRangeElectric_km'] = str(0) data['charging']['batteryStatus']['value']['carCapturedTimestamp'] = _now + data['charging']['batteryStatus']['value']['odometer'] = None _k = str(vwid.connection.keys()) - _LOGGER.info(f"libvwid.get_status connections at entry: vwid.connections.keys={_k}") + _LOGGER.debug(f"libvwid.get_status connections at entry: vwid.connections.keys={_k}") _update_result = False if self.username not in vwid.connection: - _LOGGER.info(f"create new connection, key={self.username}") + _LOGGER.debug(f"create new connection, key={self.username}") vwid.connection[self.username] = Connection(session, self.username, self.password) self._connection = vwid.connection[self.username] vwid.connection[self.username]._session_tokens['identity'] = {} @@ -1440,7 +1443,7 @@ async def get_status(self): vwid.connection[self.username]._session_tokens['Legacy'][token] = self.tokens[token] _conn_reuse = False else: - _LOGGER.info(f"reuse existing connection, key={self.username}") + _LOGGER.debug(f"reuse existing connection, key={self.username}") vwid.connection[self.username]._session = session _conn_reuse = True if not _conn_reuse: @@ -1472,6 +1475,7 @@ async def get_status(self): range =\ vehicle._states['charging']['batteryStatus']['value']['cruisingRangeElectric_km'] ts = vehicle._states['charging']['batteryStatus']['value']['carCapturedTimestamp'] + odometer = vehicle._states['measurements']['odometerStatus']['value']['odometer'] _LOGGER.debug("vehicle =" + str(vehicle)) _LOGGER.debug("soc =" + str(soc)) _LOGGER.debug("range =" + str(range)) @@ -1481,10 +1485,13 @@ async def get_status(self): data['charging']['batteryStatus']['value']['currentSOC_pct'] = str(soc) data['charging']['batteryStatus']['value']['cruisingRangeElectric_km'] = str(range) data['charging']['batteryStatus']['value']['carCapturedTimestamp'] = str(tsxx) + data['charging']['batteryStatus']['value']['odometer'] = str(odometer) _LOGGER.debug("return data =" + to_json(data, indent=4)) for token in vwid.connection[self.username]._session_tokens['identity']: self.tokens[token] =\ vwid.connection[self.username]._session_tokens['identity'][token] + _LOGGER.info("VWID: soc=" + str(soc)+", range=" + str(range) + "@" + str(tsxx) + + ', odometer=' + str(odometer)) return data else: _LOGGER.error(f"SOCERR-02: Fahrzeug mit VIN {self.vin} nicht gefunden") diff --git a/packages/modules/vehicles/vwid/soc.py b/packages/modules/vehicles/vwid/soc.py index 57e31d4c2f..8fdb236392 100755 --- a/packages/modules/vehicles/vwid/soc.py +++ b/packages/modules/vehicles/vwid/soc.py @@ -25,8 +25,8 @@ async def _fetch_soc() -> Union[int, float, str]: loop = new_event_loop() set_event_loop(loop) - soc, range, soc_ts, soc_tsX = loop.run_until_complete(_fetch_soc()) - return CarState(soc=soc, range=range, soc_timestamp=soc_tsX) + soc, range, soc_ts, soc_tsX, odometer = loop.run_until_complete(_fetch_soc()) + return CarState(soc=soc, range=range, soc_timestamp=soc_tsX, odometer=odometer) vw_group = VwGroup(vehicle_config, vehicle) diff --git a/web/settings/downloadChargeLog.php b/web/settings/downloadChargeLog.php index 4f20635058..2cb85199bd 100644 --- a/web/settings/downloadChargeLog.php +++ b/web/settings/downloadChargeLog.php @@ -17,6 +17,7 @@ "vehicle chargemode" => ["header" => "Lademodus", "type" => "chargemode"], "vehicle prio" => ["header" => "Priorität", "type" => "bool"], "vehicle rfid" => ["header" => "ID-Tag", "type" => "string"], + "vehicle odometer" => ["header" => "Kilometerstand", "type" => "int"], "vehicle soc_at_start" => ["header" => "SoC Beginn", "type" => "int"], "vehicle soc_at_end" => ["header" => "SoC Ende", "type" => "int"], "vehicle range_at_start" => ["header" => "Reichweite Beginn", "type" => "range"],