diff --git a/scripts/sync_tandem_events.py b/scripts/sync_tandem_events.py new file mode 100755 index 0000000..063a7d0 --- /dev/null +++ b/scripts/sync_tandem_events.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Sync event definitions from Tandem's official webapp and regenerate event classes. + +Extracts the complete events.json from the JSON.parse() statement embedded in +Tandem's reports module JavaScript, updates local events.json, and regenerates +events.py with all event class definitions. + +Usage: + python3 scripts/sync_tandem_events.py [--output FILE] + +Example: + python3 scripts/sync_tandem_events.py \\ + https://modules.us.tandemdiabetes.com/webapp/modules/reports-module/v1.8.0/2451.97042bc1.chunk.js +""" + +import sys +import json +import subprocess +import requests +from pathlib import Path + + +DEFAULT_EVENTS_FILE = "tconnectsync/eventparser/events.json" +DEFAULT_GENERATOR = "build_events.py" + + +def fetch_module(url): + """Fetch the minified JavaScript module from Tandem.""" + print(f"Fetching Tandem module...", file=sys.stderr) + response = requests.get(url, timeout=30) + response.raise_for_status() + content = response.text + print(f"Fetched {len(content):,} bytes", file=sys.stderr) + return content + + +def extract_events_json_from_parse(js_content): + """ + Extract the complete events.json from the JSON.parse() statement. + + Finds: JSON.parse('{"events":{...}}') + And returns the parsed events dictionary. + """ + start = js_content.find("JSON.parse('") + if start < 0: + return None + + start += len("JSON.parse('") + + # Find the matching closing brace + brace_count = 0 + end = start + escape_next = False + + for i in range(start, len(js_content)): + char = js_content[i] + + if escape_next: + escape_next = False + continue + + if char == '\\': + escape_next = True + continue + + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0: + end = i + 1 + break + + if brace_count != 0: + return None + + json_str = js_content[start:end] + + # Unescape the string + json_str = json_str.replace('\\"', '"') + + try: + return json.loads(json_str) + except json.JSONDecodeError: + return None + + +def load_existing_events(filepath): + """Load the existing events.json file.""" + if not Path(filepath).exists(): + return {"events": {}} + + try: + with open(filepath, 'r') as f: + data = json.load(f) + print(f"Loaded {len(data.get('events', {}))} existing events", file=sys.stderr) + return data + except Exception as e: + print(f"Warning: Could not load existing events.json: {e}", file=sys.stderr) + return {"events": {}} + + +def merge_events(existing_data, extracted_data): + """ + Merge extracted events with existing events. + + Keeps all existing events and updates/adds with extracted ones. + """ + existing = existing_data.get('events', {}) + extracted = extracted_data.get('events', {}) + + before_count = len(existing) + + # Add/update extracted events + existing.update(extracted) + + after_count = len(existing) + added = after_count - before_count + + if added > 0: + print(f"Added {added} new events from Tandem module", file=sys.stderr) + else: + print(f"Updated {len(extracted)} events from Tandem module", file=sys.stderr) + + return existing_data + + +def write_events_file(filepath, data): + """Write events.json with proper formatting.""" + if 'events' in data: + data['events'] = { + k: data['events'][k] + for k in sorted(data['events'].keys(), key=lambda x: int(x)) + } + + filepath = Path(filepath) + filepath.parent.mkdir(parents=True, exist_ok=True) + + with open(filepath, 'w') as f: + json.dump(data, f, indent=2) + + print(f"Wrote {len(data.get('events', {}))} events to {filepath}", file=sys.stderr) + + +def regenerate_events_py(events_json_path, generator_name): + """Regenerate events.py from updated events.json.""" + events_dir = Path(events_json_path).parent + generator_path = events_dir / generator_name + + if not generator_path.exists(): + print(f"⚠ Generator not found at {generator_path}", file=sys.stderr) + return False + + print(f"Regenerating events.py...", file=sys.stderr) + + try: + result = subprocess.run( + [sys.executable, generator_name], + cwd=str(events_dir), + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + # Write generated code to events.py + events_py = events_dir / 'events.py' + events_py.write_text(result.stdout) + print(f"✓ events.py regenerated ({len(result.stdout):,} bytes)", file=sys.stderr) + return True + else: + print(f"✗ Generator failed: {result.stderr}", file=sys.stderr) + return False + + except subprocess.TimeoutExpired: + print("✗ Generator timed out", file=sys.stderr) + return False + except Exception as e: + print(f"✗ Error running generator: {e}", file=sys.stderr) + return False + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description='Sync event definitions from Tandem and regenerate events.py' + ) + parser.add_argument('url', help='URL to Tandem JavaScript module') + parser.add_argument('--output', default=DEFAULT_EVENTS_FILE, help='Output events.json path') + parser.add_argument('--no-generate', action='store_true', help='Skip events.py regeneration') + + args = parser.parse_args() + + try: + js_content = fetch_module(args.url) + + extracted_data = extract_events_json_from_parse(js_content) + if not extracted_data: + print("✗ Could not extract events.json from Tandem module", file=sys.stderr) + sys.exit(1) + + extracted_events = extracted_data.get('events', {}) + print(f"✓ Extracted {len(extracted_events)} events from Tandem module", file=sys.stderr) + + existing_data = load_existing_events(args.output) + merged_data = merge_events(existing_data, extracted_data) + write_events_file(args.output, merged_data) + + if not args.no_generate: + regenerate_events_py(args.output, DEFAULT_GENERATOR) + + print(f"\n✓ Done", file=sys.stderr) + + except requests.exceptions.RequestException as e: + print(f"✗ Network error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"✗ Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tconnectsync/eventparser/events.json b/tconnectsync/eventparser/events.json index f937768..15e0290 100644 --- a/tconnectsync/eventparser/events.json +++ b/tconnectsync/eventparser/events.json @@ -2263,8 +2263,8 @@ "8": "EGV was successfully added to CGM subsystem array (e.g., not a duplicate)", "9": "OMR reading type", "11": "Sensor Type (see CGMTxType enum)", - "12": "Sensor Type (see CGMTxType enum) 2", - "13": "Sensor Type (see CGMTxType enum) 3" + "12": "Sensor Type (see CGMTxType enum)", + "13": "Sensor Type (see CGMTxType enum)" } ] ] diff --git a/tconnectsync/eventparser/events.py b/tconnectsync/eventparser/events.py index 71348c5..e567d24 100644 --- a/tconnectsync/eventparser/events.py +++ b/tconnectsync/eventparser/events.py @@ -5413,8 +5413,8 @@ class LidCgmDataFsl3(BaseEvent): "8": "EGV was successfully added to CGM subsystem array (e.g., not a duplicate)", "9": "OMR reading type", "11": "Sensor Type (see CGMTxType enum)", - "12": "Sensor Type (see CGMTxType enum) 2", - "13": "Sensor Type (see CGMTxType enum) 3" + "12": "Sensor Type (see CGMTxType enum)", + "13": "Sensor Type (see CGMTxType enum)" } class EgvinfobitmaskBitmask(IntFlag): @@ -5426,9 +5426,9 @@ class LidCgmDataFsl3(BaseEvent): ValidAlgstateAlgstateIs100 = 2**7 EgvWasSuccessfullyAddedToCgmSubsystemArrayE = 2**8 OmrReadingType = 2**9 - SensorTypeSeeCgmtxtypeEnum = 2**11 - SensorTypeSeeCgmtxtypeEnum2 = 2**12 - SensorTypeSeeCgmtxtypeEnum3 = 2**13 + SensorTypeSeeCgmtxtypeEnum_11 = 2**11 + SensorTypeSeeCgmtxtypeEnum_12 = 2**12 + SensorTypeSeeCgmtxtypeEnum_13 = 2**13 @property def egvInfoBitmask(self):