diff --git a/packages/modules/vehicles/vwid/libeuda.py b/packages/modules/vehicles/vwid/libeuda.py index 41fd837cb3..818a71163e 100755 --- a/packages/modules/vehicles/vwid/libeuda.py +++ b/packages/modules/vehicles/vwid/libeuda.py @@ -144,6 +144,7 @@ def get_oidc_state(brand: str = DEFAULT_BRAND) -> str: POLL_INTERVAL = 60 # polling interval in seconds CYCLE_INTERVAL = 600 # cycle interval in seconds +INITIAL_RESULT_WAIT = 30 # seconds to wait for the first background result EUDA_THREADNAME = "soc_bt_ev" UTC = None KEEP_JSON = 5 @@ -668,6 +669,42 @@ def utc_to_timestamp(d: str) -> float: _ts = (_dt - _epoch).total_seconds() return _ts +def parse_vehicle_data(payload: dict) -> dict: + """Extract normalized SoC fields from an EUDA JSON payload.""" + data = payload.get('Data', []) + soc = get_field_value_by_key(data, 'ae0294b4-1286-3e98-a818-1485b8d88430') + soc_timestamp_str = None + if soc is not None: + _LOGGER.info(f"soc {soc} found in state_of_charge") + _ts = get_field_timestamp_by_key(data, 'ae0294b4-1286-3e98-a818-1485b8d88430') + soc_timestamp_str = re.sub(r'\....Z', 'Z', _ts) + if soc_timestamp_str is None: + soc_timestamp_str = get_max_value_by_fieldname(data, CAR_TIMESTAMP) + + if soc is None: + soc = get_field_value_by_key(data, 'f89ed652-d104-3fa6-b7e2-ab7543309e7b') + if soc is None: + soc = get_field_value_by_key(data, '506cb83e-f99f-3af3-bbeb-0429b69a78d9') + if soc is None: + soc = get_field_value_by_key(data, 'ac1108b1-b8cc-3db9-a663-03d387e42223') + range = get_field_value_by_key(data, '153e8c40-4c6c-3c17-a11b-0ecc35d55b81') + if range is None: + range = get_field_value_by_key(data, '0ca40e18-0564-3eda-bcc0-7aee9ef44f04') + odometer = get_field_value_by_key(data, '41c0805c-43e5-313e-9dfb-356cb8d20f7c') + if odometer is None: + odometer = get_field_value_by_key(data, '30cc36fd-71ca-3c09-9296-e94ebd47bd2b') + soc_timestamp = utc_to_timestamp(soc_timestamp_str) + if soc_timestamp > 1e10: + soc_timestamp = soc_timestamp / 1000 + + return { + 'soc': soc, + 'range': range, + 'soc_timestamp': soc_timestamp, + 'soc_timestamp_str': soc_timestamp_str, + 'odometer': odometer, + } + class euda(): @@ -726,32 +763,31 @@ def save_json_file(self, _name: str, vin: str, _data: dict) -> bool: return status - def extract_data(self, _Data: dict) -> Union[float, float, float, str, float]: - soc = get_field_value_by_key(_Data, 'ae0294b4-1286-3e98-a818-1485b8d88430') - soc_timestampxx = None - if soc is not None: - _LOGGER.info(f"soc {soc} found in state_of_charge") - _ts = get_field_timestamp_by_key(_Data, 'ae0294b4-1286-3e98-a818-1485b8d88430') - soc_timestampxx = re.sub(r'\....Z', 'Z', _ts) - if soc_timestampxx is None: - soc_timestampxx = get_max_value_by_fieldname(_Data, CAR_TIMESTAMP) - - if soc is None: - soc = get_field_value_by_key(_Data, 'f89ed652-d104-3fa6-b7e2-ab7543309e7b') - if soc is None: - soc = get_field_value_by_key(_Data, '506cb83e-f99f-3af3-bbeb-0429b69a78d9') - if soc is None: - soc = get_field_value_by_key(_Data, 'ac1108b1-b8cc-3db9-a663-03d387e42223') - range = get_field_value_by_key(_Data, '153e8c40-4c6c-3c17-a11b-0ecc35d55b81') - if range is None: - range = get_field_value_by_key(_Data, '0ca40e18-0564-3eda-bcc0-7aee9ef44f04') - odometer = get_field_value_by_key(_Data, '41c0805c-43e5-313e-9dfb-356cb8d20f7c') - if odometer is None: - odometer = get_field_value_by_key(_Data, '30cc36fd-71ca-3c09-9296-e94ebd47bd2b') - soc_timestamp = utc_to_timestamp(soc_timestampxx) - if soc_timestamp > 1e10: - soc_timestamp = soc_timestamp / 1000 - return soc, range, soc_timestamp, soc_timestampxx, odometer + def update_result_from_payload(self, payload: dict, source: str) -> dict: + """Parse an EUDA payload and publish it in the in-memory result cache.""" + vin = payload['vin'] + result = parse_vehicle_data(payload) + euda.result[vin] = result + _ano_j = { ano_vin(vin): euda.result[vin] } + _LOGGER.info(f"thread result:\n{json.dumps(_ano_j, indent=4)}") + + return result + + def load_latest_json_result(self, vin: str) -> bool: + """Load and parse the newest already downloaded EUDA JSON for a VIN.""" + files = glob.glob(str(JSON_PATH) + '/*_' + vin + '.json') + files.sort() + if not files: + return False + latest = files[-1] + try: + with open(latest) as f: + payload = json.load(f) + self.update_result_from_payload(payload, latest) + return True + except Exception as err: + _LOGGER.exception(f"failed to load latest EUDA JSON {latest}: {err}") + return False # eudaThread async def async_eudaThread(self, username: str, password: str, vin: str): @@ -809,43 +845,8 @@ async def async_eudaThread(self, username: str, password: str, vin: str): await asyncio.sleep(POLL_INTERVAL) vin = _data['vin'] - status = self.save_json_file(_name, vin, _data) - - if status: - _Data = _data['Data'] - # soc = get_field_value_by_key(_Data, 'ae0294b4-1286-3e98-a818-1485b8d88430') - # soc_timestamp = None - # if soc is not None: - # _LOGGER.info(f"soc {soc} found in state_of_charge") - # soc_timestamp = get_field_timestamp_by_key(_Data, - # 'ae0294b4-1286-3e98-a818-1485b8d88430') - # if soc_timestamp is None: - # soc_timestamp = get_max_value_by_fieldname(_Data, CAR_TIMESTAMP) - # if soc is None: - # soc = get_field_value_by_key(_Data, 'f89ed652-d104-3fa6-b7e2-ab7543309e7b') - # if soc is None: - # soc = get_field_value_by_key(_Data, '506cb83e-f99f-3af3-bbeb-0429b69a78d9') - # if soc is None: - # soc = get_field_value_by_key(_Data, 'ac1108b1-b8cc-3db9-a663-03d387e42223') - # range = get_field_value_by_key(_Data, '153e8c40-4c6c-3c17-a11b-0ecc35d55b81') - # if range is None: - # range = get_field_value_by_key(_Data, '0ca40e18-0564-3eda-bcc0-7aee9ef44f04') - # odometer = get_field_value_by_key(_Data, '41c0805c-43e5-313e-9dfb-356cb8d20f7c') - # if odometer is None: - # odometer = get_field_value_by_key(_Data, '30cc36fd-71ca-3c09-9296-e94ebd47bd2b') - # soc_timestampxx = utc_to_timestamp(soc_timestamp) - - soc, range, soc_timestamp, soc_timestampxx, odometer = self.extract_data(_Data) - - euda.result[vin] = { - 'soc': soc, - 'range': range, - 'soc_timestamp': soc_timestamp, - 'soc_timestampxx': soc_timestampxx, - 'odometer': odometer, - } - _ano_j = json.dumps(euda.result, indent=4).replace(vin, ano_vin(vin)) - _LOGGER.info(f"thread result:\n{_ano_j}") + self.save_json_file(_name, vin, _data) + self.update_result_from_payload(_data, _name) _LOGGER.info(f"sleep {CYCLE_INTERVAL} seconds") await asyncio.sleep(CYCLE_INTERVAL) @@ -868,14 +869,14 @@ def check_tests(self): with open(_t) as f: _data = json.load(f) _Data = _data['Data'] - soc, range, soc_timestamp, soc_timestampxx, odometer = self.extract_data(_Data) + soc, range, soc_timestamp, soc_timestamp_str, odometer = self.extract_data(_Data) test_result = {} test_result[_vin] = { 'soc': soc, 'range': range, 'soc_timestamp': soc_timestamp, - 'soc_timestampxx': soc_timestampxx, + 'soc_timestamp_str': soc_timestamp_str, 'odometer': odometer, } _ano_j = json.dumps(test_result, indent=4).replace(_vin, ano_vin(_vin)) @@ -930,9 +931,20 @@ async def get_status(self, daemon=True) euda.thread[self.username]['thread'].start() + if self.vin not in euda.result: + self.load_latest_json_result(self.vin) + + wait_until = time.time() + INITIAL_RESULT_WAIT + while self.vin not in euda.result and time.time() < wait_until: + _LOGGER.info(f"wait for first EUDA result for VIN {self.vin}") + time.sleep(1) + if self.vin in euda.result: _LOGGER.debug(f"vehicle match: {self.vin}") - _LOGGER.info(f"result from thread:\n{json.dumps(euda.result, indent=4)}") + _ano_j = {} + for vin in euda.result: + _ano_j[ano_vin(vin)] = euda.result[vin] + _LOGGER.info(f"result from thread:\n{json.dumps(_ano_j, indent=4)}") soc = euda.result[self.vin]['soc'] range = euda.result[self.vin]['range'] try: @@ -944,14 +956,14 @@ async def get_status(self, _LOGGER.warning(f"no range delivered, calculate range = {range}km") ts = euda.result[self.vin]['soc_timestamp'] - tsxx = euda.result[self.vin]['soc_timestampxx'] + ts_str = euda.result[self.vin]['soc_timestamp_str'] odometer = euda.result[self.vin]['odometer'] _LOGGER.debug(f"vin = {self.vin}") _LOGGER.debug(f"soc = {soc}") _LOGGER.debug(f"range = {range}") _LOGGER.debug(f"soc_timestamp = {ts}") - _LOGGER.debug(f"soc_timestampxx = {tsxx}") + _LOGGER.debug(f"soc_timestamp_str = {ts_str}") _LOGGER.debug(f"odometer = {odometer}") data_modified = False @@ -964,8 +976,8 @@ async def get_status(self, if ts and str(ts) != data['soc_timestamp']: data['soc_timestamp'] = str(ts) data_modified = True - if tsxx and str(tsxx) != data['carCapturedTimestamp']: - data['carCapturedTimestamp'] = str(tsxx) + if ts_str and str(ts_str) != data['carCapturedTimestamp']: + data['carCapturedTimestamp'] = str(ts_str) data_modified = True if odometer and str(odometer) != data['odometer']: data['odometer'] = str(odometer) @@ -984,10 +996,10 @@ async def get_status(self, _LOGGER.info(f"return data:\n{json.dumps(data, indent=4)}") soc = data['currentSOC_pct'] range = data['cruisingRangeElectric_km'] - tsxx = data['carCapturedTimestamp'] ts = data['soc_timestamp'] + ts_str = data['carCapturedTimestamp'] odometer = data['odometer'] - _LOGGER.info(f"get_status: soc={soc}, range={range}, ts={ts}, tsxx={tsxx}, odometer={odometer}") + _LOGGER.info(f"get_status: soc={soc}, range={range}, ts={ts}, ts_str={ts_str}, odometer={odometer}") # for test only: # set soc_timestamp to 0 to avoid computed state being later than this reported state @@ -996,7 +1008,7 @@ async def get_status(self, # _LOGGER.info(f"get_status: publish soc_timestamp as 0: topic: {topic}, message: {ep0}") # Pub().pub(topic, ep0) - return float(soc), float(range), float(ts), tsxx, float(odometer) + return float(soc), float(range), float(ts), ts_str, float(odometer) except Exception as e: _LOGGER.exception(f"get_status failed 0, exception={e}") # if exception is a SOCERR reraise it, otherwise raise general SOCERR-00