Files
Enelix-EMS/tests/check_documentation.py
T
dh 74a2d6c685
Tests / test (push) Successful in 1m1s
feat(workflow): publish verified forecast controls and consolidated docs
Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
2026-10-08 10:02:58 +00:00

89 lines
4.1 KiB
Python

"""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())