#!/usr/bin/env python3 """ RTL_433 Device Protocol Parser Parses RTL_433 C source code to extract device protocol definitions and convert them to a structured JSON format for GigLez matching engine. Usage: python3 scripts/parse_rtl433_devices.py /path/to/rtl_433/ Output: data/rtl_433_protocols.json """ import re import json import os import sys from pathlib import Path from typing import Dict, List, Optional from dataclasses import dataclass, asdict @dataclass class RTL433Device: """Represents an RTL_433 device protocol definition""" device_id: str # e.g., "acurite_rain_896" name: str # Human-readable name modulation: Optional[str] = None # OOK_PULSE_PPM, FSK_PULSE_PWM, etc. short_width: Optional[int] = None # Microseconds long_width: Optional[int] = None gap_limit: Optional[int] = None reset_limit: Optional[int] = None sync_width: Optional[int] = None tolerance: Optional[int] = None frequency: Optional[int] = None # Hz category: Optional[str] = None # Inferred from device type manufacturer: Optional[str] = None # Extracted from name source_file: Optional[str] = None class RTL433Parser: """Parses RTL_433 device definitions from C source""" # Modulation types mapping MODULATION_TYPES = { 'OOK_PULSE_PPM': 'OOK', 'OOK_PULSE_PWM': 'OOK', 'OOK_PULSE_Manchester_Zerobit': 'OOK', 'OOK_PULSE_PCM': 'OOK', 'OOK_PULSE_DMC': 'OOK', 'OOK_PULSE_PIWM_RAW': 'OOK', 'OOK_PULSE_PIWM_DC': 'OOK', 'FSK_PULSE_PCM': 'FSK', 'FSK_PULSE_PWM': 'FSK', 'FSK_PULSE_Manchester_Zerobit': 'FSK', } # Device category patterns (from device name) CATEGORY_PATTERNS = { 'weather': ['weather', 'rain', 'wind', 'humidity', 'temperature', 'barometer', 'therm'], 'tpms': ['tpms', 'tire', 'pressure'], 'automotive': ['car', 'vehicle', 'key', 'fob', 'honda', 'toyota', 'ford', 'bmw', 'audi'], 'security': ['alarm', 'security', 'motion', 'door', 'window', 'smoke', 'pir'], 'home_automation': ['remote', 'switch', 'socket', 'plug', 'doorbell', 'garage', 'gate'], 'sensors': ['sensor', 'detector', 'meter', 'monitor'], 'energy': ['power', 'energy', 'electric', 'gas', 'water', 'current', 'voltage'], 'lighting': ['light', 'lamp', 'dimmer', 'bulb'], } def __init__(self, rtl433_path: str): self.rtl433_path = Path(rtl433_path) self.devices_dir = self.rtl433_path / 'src' / 'devices' self.devices: List[RTL433Device] = [] def parse_all(self) -> List[RTL433Device]: """Parse all device definitions from RTL_433""" print(f"Parsing RTL_433 devices from: {self.devices_dir}") if not self.devices_dir.exists(): raise FileNotFoundError(f"Devices directory not found: {self.devices_dir}") # Get list of device files device_files = sorted(self.devices_dir.glob('*.c')) print(f"Found {len(device_files)} device files") for device_file in device_files: devices_in_file = self.parse_file(device_file) self.devices.extend(devices_in_file) if devices_in_file: print(f" {device_file.name}: {len(devices_in_file)} devices") print(f"\nTotal devices parsed: {len(self.devices)}") return self.devices def parse_file(self, file_path: Path) -> List[RTL433Device]: """Parse device definitions from a single C file""" devices = [] try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Find all r_device struct definitions # Pattern: r_device const device_name = { ... }; device_pattern = r'r_device\s+const\s+(\w+)\s*=\s*\{([^}]+)\}' matches = re.finditer(device_pattern, content, re.MULTILINE | re.DOTALL) for match in matches: device_id = match.group(1) struct_body = match.group(2) device = self.parse_device_struct(device_id, struct_body, file_path.name) if device: devices.append(device) except Exception as e: print(f" Error parsing {file_path.name}: {e}") return devices def parse_device_struct(self, device_id: str, struct_body: str, source_file: str) -> Optional[RTL433Device]: """Parse individual r_device struct""" # Extract .name field name_match = re.search(r'\.name\s*=\s*"([^"]+)"', struct_body) if not name_match: return None name = name_match.group(1) # Extract other fields device = RTL433Device( device_id=device_id, name=name, source_file=source_file ) # Modulation mod_match = re.search(r'\.modulation\s*=\s*(\w+)', struct_body) if mod_match: modulation_raw = mod_match.group(1) device.modulation = self.MODULATION_TYPES.get(modulation_raw, modulation_raw) # Timing parameters for field_name in ['short_width', 'long_width', 'gap_limit', 'reset_limit', 'sync_width', 'tolerance']: field_match = re.search(rf'\.{field_name}\s*=\s*(\d+)', struct_body) if field_match: setattr(device, field_name, int(field_match.group(1))) # Infer category from device name device.category = self.infer_category(name.lower()) # Extract manufacturer from name (first word usually) device.manufacturer = self.extract_manufacturer(name) return device def infer_category(self, name_lower: str) -> str: """Infer device category from name""" for category, keywords in self.CATEGORY_PATTERNS.items(): for keyword in keywords: if keyword in name_lower: return category return 'other' def extract_manufacturer(self, name: str) -> Optional[str]: """Extract manufacturer from device name""" # First word is usually manufacturer words = name.split() if words: manufacturer = words[0] # Filter out generic words generic = ['wireless', 'generic', 'remote', 'sensor', 'temperature', 'humidity'] if manufacturer.lower() not in generic: return manufacturer return None def to_json(self, output_path: str): """Export devices to JSON""" devices_dict = [asdict(d) for d in self.devices] with open(output_path, 'w') as f: json.dump({ 'source': 'RTL_433', 'total_devices': len(self.devices), 'devices': devices_dict }, f, indent=2) print(f"\nExported to: {output_path}") def print_summary(self): """Print summary statistics""" print("\n" + "="*60) print("RTL_433 DEVICE SUMMARY") print("="*60) # By category categories = {} for device in self.devices: cat = device.category or 'unknown' categories[cat] = categories.get(cat, 0) + 1 print("\nDevices by Category:") for cat, count in sorted(categories.items(), key=lambda x: x[1], reverse=True): print(f" {cat:20s}: {count:3d}") # By modulation modulations = {} for device in self.devices: mod = device.modulation or 'unknown' modulations[mod] = modulations.get(mod, 0) + 1 print("\nDevices by Modulation:") for mod, count in sorted(modulations.items(), key=lambda x: x[1], reverse=True): print(f" {mod:20s}: {count:3d}") # Top manufacturers manufacturers = {} for device in self.devices: mfg = device.manufacturer or 'unknown' manufacturers[mfg] = manufacturers.get(mfg, 0) + 1 print("\nTop 10 Manufacturers:") for mfg, count in sorted(manufacturers.items(), key=lambda x: x[1], reverse=True)[:10]: print(f" {mfg:20s}: {count:3d}") def main(): if len(sys.argv) < 2: print("Usage: python3 parse_rtl433_devices.py /path/to/rtl_433/") print("\nExample:") print(" python3 scripts/parse_rtl433_devices.py /tmp/rtl_433/") sys.exit(1) rtl433_path = sys.argv[1] parser = RTL433Parser(rtl433_path) devices = parser.parse_all() # Create data directory if needed data_dir = Path('data') data_dir.mkdir(exist_ok=True) # Export to JSON output_file = data_dir / 'rtl_433_protocols.json' parser.to_json(str(output_file)) # Print summary parser.print_summary() print("\n" + "="*60) print(f"SUCCESS: Parsed {len(devices)} RTL_433 device protocols") print("="*60) if __name__ == '__main__': main()