mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
Generate JSON->event adapter via the events codemod
Extend build_events.py (the codemod) so events.json stays the source of
truth: each generated event class gains build_from_json(event), mapping a
pump-logs JSON event's eventProperties onto its {field}[Raw] attrs
(matched by normalized field name), plus RawEvent.build_from_json() which
derives timestampRaw from pumpDateTime so eventTimestamp/seqNum/eventId
keep working. Add generic.Event_from_json()/Events_from_json() dispatchers
mirroring Event()/Events(); unknown eventCode -> bare RawEvent.
Enum/dictionary/ratio fields are raw ints (map directly onto {field}Raw so
existing properties resolve); bitmask fields arrive as arrays of set-bit
indices, converted to the raw int via _bitmask_arr_to_int. events.py is
regenerated; the byte-path build() is unchanged.
Tests: tests/eventparser/test_from_json.py.
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
def _norm(s):
|
||||||
|
return re.sub(r'[^a-z0-9]', '', s.lower())
|
||||||
|
|
||||||
header = '''# THIS FILE IS AUTOGENERATED. DO NOT EDIT.
|
header = '''# THIS FILE IS AUTOGENERATED. DO NOT EDIT.
|
||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, IntFlag
|
from enum import Enum, IntFlag
|
||||||
from .raw_event import RawEvent, BaseEvent
|
from .raw_event import RawEvent, BaseEvent
|
||||||
@@ -9,6 +15,19 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
EVENT_LEN = 26
|
EVENT_LEN = 26
|
||||||
|
|
||||||
|
def _norm(s):
|
||||||
|
return re.sub(r'[^a-z0-9]', '', s.lower())
|
||||||
|
|
||||||
|
def _bitmask_arr_to_int(v):
|
||||||
|
# pump-logs bitmask fields arrive as arrays of set-bit indices; convert to the
|
||||||
|
# int the generated IntFlag / bitmask_to_list expects. Tolerate an int too.
|
||||||
|
if isinstance(v, (list, tuple)):
|
||||||
|
r = 0
|
||||||
|
for i in v:
|
||||||
|
r |= (1 << int(i))
|
||||||
|
return r
|
||||||
|
return int(v) if v is not None else 0
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
TYPE_TO_STRUCT = {
|
TYPE_TO_STRUCT = {
|
||||||
@@ -56,6 +75,14 @@ class {name}(BaseEvent):
|
|||||||
{build_p2}
|
{build_p2}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_from_json(event):
|
||||||
|
props = {{_norm(k): v for k, v in event.get("eventProperties", {{}}).items()}}
|
||||||
|
return {name}(
|
||||||
|
raw = RawEvent.build_from_json(event),
|
||||||
|
{build_json}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def eventTimestamp(self):
|
def eventTimestamp(self):
|
||||||
return self.raw.timestamp
|
return self.raw.timestamp
|
||||||
@@ -130,6 +157,20 @@ def build_decode(event_def):
|
|||||||
|
|
||||||
return '\n'.join([f'{" "*8}{f}' for f in p1s]), '\n'.join([f'{" "*12}{f}' for f in p2s])
|
return '\n'.join([f'{" "*8}{f}' for f in p1s]), '\n'.join([f'{" "*12}{f}' for f in p2s])
|
||||||
|
|
||||||
|
def build_json_kwargs(event_def):
|
||||||
|
lines = []
|
||||||
|
for name, field in event_def["data"].items():
|
||||||
|
suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else ''
|
||||||
|
attr = f'{fieldNameFormat(name)}{suffix}'
|
||||||
|
key = _norm(name)
|
||||||
|
is_bitmask = "transform" in field and any(tx[0] == 'bitmask' for tx in field["transform"])
|
||||||
|
if is_bitmask:
|
||||||
|
lines.append(f'{attr} = _bitmask_arr_to_int(props.get("{key}", 0)),')
|
||||||
|
else:
|
||||||
|
lines.append(f'{attr} = props.get("{key}", None),')
|
||||||
|
return '\n'.join([f'{" "*12}{l}' for l in lines])
|
||||||
|
|
||||||
|
|
||||||
def build_transform_funcs(event_def):
|
def build_transform_funcs(event_def):
|
||||||
try:
|
try:
|
||||||
from transforms import TRANSFORMS
|
from transforms import TRANSFORMS
|
||||||
@@ -154,6 +195,7 @@ def build_event(event_id, event_def):
|
|||||||
fields_dict = build_fields_dict(event_def),
|
fields_dict = build_fields_dict(event_def),
|
||||||
build_p1 = build_decode(event_def)[0],
|
build_p1 = build_decode(event_def)[0],
|
||||||
build_p2 = build_decode(event_def)[1],
|
build_p2 = build_decode(event_def)[1],
|
||||||
|
build_json = build_json_kwargs(event_def),
|
||||||
transform_funcs = build_transform_funcs(event_def),
|
transform_funcs = build_transform_funcs(event_def),
|
||||||
id = event_id,
|
id = event_id,
|
||||||
raw_name = event_def["name"]
|
raw_name = event_def["name"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,4 +25,19 @@ def Event(x):
|
|||||||
Events = lambda x: (Event(bytearray(e)) for e in batched(x, EVENT_LEN))
|
Events = lambda x: (Event(bytearray(e)) for e in batched(x, EVENT_LEN))
|
||||||
|
|
||||||
def decode_raw_events(raw):
|
def decode_raw_events(raw):
|
||||||
return base64.b64decode(raw)
|
return base64.b64decode(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def Event_from_json(event):
|
||||||
|
raw_event = RawEvent.build_from_json(event)
|
||||||
|
if not raw_event.id in EVENT_IDS:
|
||||||
|
# Log unknown events with their property keys for reverse-engineering
|
||||||
|
props = ' '.join(event['eventProperties'].keys())
|
||||||
|
# Also log seqNum and timestamp for correlation
|
||||||
|
logger.debug(f"UNKNOWN_JSON_EVENT | id={raw_event.id} | seqNum={raw_event.seqNum} | timestamp={raw_event.timestamp.isoformat()} | props={props}")
|
||||||
|
return raw_event
|
||||||
|
|
||||||
|
|
||||||
|
return EVENT_IDS[raw_event.id].build_from_json(event)
|
||||||
|
|
||||||
|
Events_from_json = lambda events: (Event_from_json(e) for e in events)
|
||||||
@@ -48,6 +48,22 @@ class RawEvent:
|
|||||||
raw = raw
|
raw = raw
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_from_json(event):
|
||||||
|
# pump-logs JSON events carry pumpDateTime (naive local wall-clock,
|
||||||
|
# no tz). Reproduce the byte path: store timestampRaw as seconds since
|
||||||
|
# TANDEM_EPOCH parsed AS IF UTC, so the .timestamp property re-forces
|
||||||
|
# the same wall-clock into TIMEZONE_NAME. source is unused; raw bytes
|
||||||
|
# are absent on the JSON path.
|
||||||
|
timestampRaw = arrow.get(event["pumpDateTime"]).int_timestamp - TANDEM_EPOCH
|
||||||
|
return RawEvent(
|
||||||
|
source = 0,
|
||||||
|
id = event["eventCode"],
|
||||||
|
timestampRaw = timestampRaw,
|
||||||
|
seqNum = event["sequenceNumber"],
|
||||||
|
raw = b''
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
# Event timestamps do not have TZ data attached to them when parsed,
|
# Event timestamps do not have TZ data attached to them when parsed,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from tconnectsync.eventparser.generic import Event_from_json, Events_from_json
|
||||||
|
from tconnectsync.eventparser import events as eventtypes
|
||||||
|
from tconnectsync.eventparser.raw_event import RawEvent
|
||||||
|
|
||||||
|
# Trimmed real pump-log events (values from a captured account response).
|
||||||
|
BASAL_279 = {
|
||||||
|
"deviceAssignmentId": "4ff6bebc-d4d6-4423-b123-eecfcf5a4238",
|
||||||
|
"eventCode": 279,
|
||||||
|
"sequenceGroup": 0,
|
||||||
|
"sequenceNumber": 393131,
|
||||||
|
"pumpDateTime": "2026-04-30T00:03:29",
|
||||||
|
"eventProperties": {
|
||||||
|
"commandedRateSource": 3, "reservedA2": 0, "spareA3": 0,
|
||||||
|
"commandedRate": 0, "profileBasalRate": 1000, "algorithmRate": 0,
|
||||||
|
"tempRate": 65535,
|
||||||
|
},
|
||||||
|
"estimatedDateTime": "2026-04-30T00:03:29Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
ALARM_5 = {
|
||||||
|
"deviceAssignmentId": "4ff6bebc-d4d6-4423-b123-eecfcf5a4238",
|
||||||
|
"eventCode": 5,
|
||||||
|
"sequenceGroup": 0,
|
||||||
|
"sequenceNumber": 500001,
|
||||||
|
"pumpDateTime": "2026-04-30T01:00:00",
|
||||||
|
"eventProperties": {"alarmId": 18, "faultLocatorData": 8311, "param1": 3993668, "param2": 0},
|
||||||
|
"estimatedDateTime": "2026-04-30T01:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildFromJson(unittest.TestCase):
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
def test_dispatches_to_correct_class(self):
|
||||||
|
self.assertIsInstance(Event_from_json(BASAL_279), eventtypes.LidBasalDelivery)
|
||||||
|
self.assertIsInstance(Event_from_json(ALARM_5), eventtypes.LidAlarmActivated)
|
||||||
|
|
||||||
|
def test_plain_fields(self):
|
||||||
|
ev = Event_from_json(BASAL_279)
|
||||||
|
self.assertEqual(ev.commandedRate, 0)
|
||||||
|
self.assertEqual(ev.profileBasalRate, 1000)
|
||||||
|
self.assertEqual(ev.tempRate, 65535)
|
||||||
|
|
||||||
|
def test_envelope_fields(self):
|
||||||
|
ev = Event_from_json(BASAL_279)
|
||||||
|
self.assertEqual(ev.seqNum, 393131)
|
||||||
|
self.assertEqual(ev.eventId, 279)
|
||||||
|
|
||||||
|
def test_timestamp_preserves_wall_clock(self):
|
||||||
|
# eventTimestamp keeps pumpDateTime's wall-clock (tz forced to the
|
||||||
|
# configured TIMEZONE_NAME), so the naive portion round-trips exactly.
|
||||||
|
ev = Event_from_json(BASAL_279)
|
||||||
|
self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:03:29")
|
||||||
|
|
||||||
|
def test_missing_plain_field_defaults_to_none(self):
|
||||||
|
event = dict(BASAL_279)
|
||||||
|
event["eventProperties"] = {k: v for k, v in BASAL_279["eventProperties"].items() if k != "tempRate"}
|
||||||
|
ev = Event_from_json(event)
|
||||||
|
self.assertIsNone(ev.tempRate)
|
||||||
|
self.assertEqual(ev.commandedRate, 0) # others still parse
|
||||||
|
|
||||||
|
def test_extra_keys_are_ignored(self):
|
||||||
|
event = dict(BASAL_279)
|
||||||
|
event["eventProperties"] = dict(BASAL_279["eventProperties"], someFutureField=42)
|
||||||
|
ev = Event_from_json(event) # must not raise
|
||||||
|
self.assertFalse(hasattr(ev, "someFutureField"))
|
||||||
|
|
||||||
|
def test_events_from_json_yields_in_order(self):
|
||||||
|
out = list(Events_from_json([BASAL_279, ALARM_5]))
|
||||||
|
self.assertEqual([type(e).__name__ for e in out],
|
||||||
|
["LidBasalDelivery", "LidAlarmActivated"])
|
||||||
|
|
||||||
|
def test_unknown_eventcode_falls_back_to_rawevent(self):
|
||||||
|
ev = Event_from_json({
|
||||||
|
"eventCode": 99999,
|
||||||
|
"sequenceNumber": 7,
|
||||||
|
"pumpDateTime": "2026-04-30T00:00:00",
|
||||||
|
"eventProperties": {},
|
||||||
|
})
|
||||||
|
self.assertIs(type(ev), RawEvent)
|
||||||
|
self.assertEqual(ev.eventId, 99999)
|
||||||
|
self.assertEqual(ev.seqNum, 7)
|
||||||
|
self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:00:00")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user