"""Check literal Symcon registrations and reviewed dynamic families against READMEs. This is a documentation-presence check, not a PHP evaluator or hardware test. Only repository sources are read; no credentials, runtime state or device APIs. """ import pathlib import re import sys ROOT = pathlib.Path(__file__).resolve().parents[1] MODULES = { 'Manager': 'Manager', 'Batterie': 'Batterie', 'Warmwassererwaermer': 'Wassererwaermer', 'Pufferspeicher': 'Pufferspeicher', 'VerbraucherEinStufig': 'Verbraucher-1-Stufig', 'Waermepumpe': 'Waermepumpe', 'LadestationStandAlone': 'Ladestation-Stand-Alone', 'LadestationGateway': 'Ladestation-Gateway', 'EaseeGateway': 'Easee-Gateway', 'Energiediagramm': 'Energiediagramm', 'CC100Hardware': 'CC100-Hardware', 'ShellyModul': 'Shelly-Modul', 'Verbrauchskostenreport': 'Verbrauchskostenreport', 'VirtuelleBatterie': 'Virtuelle-Batterie', 'VGTSchnittstelle': 'VGT-Schnittstelle', } DYNAMIC = { 'Energiediagramm': ['FarbeEigenverbrauchRGB', 'FarbeAutarkieRGB', 'FarbeHintergrundRGB', 'FarbeHintergrundAkzent1RGB', 'FarbeHintergrundAkzent2RGB'], 'Manager': ['PVLeistungArchiv', 'HausverbrauchLeistungArchiv', 'NetzleistungArchiv', 'BatterieleistungArchiv', 'SDLLeistungArchiv', 'PVEnergie', 'Hausenergie', 'NetzbezugEnergie', 'EinspeisungEnergie', 'BatterieLadenEnergie', 'BatterieEntladenEnergie', 'SDLLadenEnergie', 'SDLEntladenEnergie', 'EnergieflussPV', 'EnergieflussNetz', 'EnergieflussHaus', 'EnergieflussBatterie', 'EnergieflussSDL', 'EnergieflussIst_', 'EnergieflussSoll_', 'DiagrammHausLeistung', 'DiagrammHausEnergie', 'DiagrammPVLeistung_', 'DiagrammPVEnergie_', 'DiagrammBatterieLeistung_', 'DiagrammBatterieLaden_', 'DiagrammBatterieEntladen_', 'DiagrammVerbraucherLeistung_', 'DiagrammVerbraucherEnergie_'], 'CC100Hardware': ['DI1', 'DI8', 'DO1', 'DO4', 'AI1', 'AI2', 'AO1', 'AO2', 'PT1', 'PT2'], 'ShellyModul': ['Typabbildung', 'Hash', 'switch:0.aenergy.total', 'Online', 'Switch.Set'], } def sources(path, seen=None): seen = set() if seen is None else seen path = path.resolve() if path in seen: return seen.add(path) text = path.read_text(encoding='utf-8') yield text for match in re.finditer(r"require_once\s+__DIR__\s*\.\s*['\"]([^'\"]+)['\"]", text): target = (path.parent / match[1].lstrip('/')).resolve() if ROOT in target.parents and target.is_file() and 'vendor' not in target.parts: yield from sources(target, seen) def main(): modules = sorted(ROOT.glob('*/module.php')) errors, checked, dynamic = [], 0, 0 for source in modules: name = source.parent.name if name not in MODULES: errors.append(name + ': missing documentation mapping') continue document = ROOT / 'docs/module' / MODULES[name] / 'README.md' if not document.is_file() or not (source.parent / 'README.md').is_file(): errors.append(name + ': README missing') continue text = document.read_text(encoding='utf-8') shared = ROOT / 'docs/Schnittstelle.md' if name not in ['Manager', 'EaseeGateway'] and shared.exists(): text += shared.read_text(encoding='utf-8') fields = set() for code in sources(source): fields.update(re.findall(r"->Register(Property|Variable)(?:Boolean|Integer|Float|String)\s*\(\s*['\"]([^'\"]+)['\"]", code)) for kind, ident in sorted(fields): checked += 1 if ident not in text: errors.append(name + ': undocumented ' + kind + ' ' + ident) for ident in DYNAMIC.get(name, []): dynamic += 1 if ident not in text: errors.append(name + ': undocumented dynamic family ' + ident) if not modules: errors.append('No installable module sources found') for error in errors: print(error) print(f'{len(modules)} modules; {checked} literal registrations; {dynamic} dynamic markers; {len(errors)} gaps') return bool(errors) if __name__ == '__main__': sys.exit(main())