Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 85 additions & 73 deletions packages/modules/vehicles/vwid/libeuda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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))
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down