mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-30 03:41:58 +02:00
Completes the roadmap. The tool now validates, repairs, generates (HA/ETS/handover/IoT), diffs, grades and drafts acceptance protocols — all design-time & read-only. B-tier: - B2 climate-correctness review note (controller/operation modes, setpoint shift cmd+state, mode-without-state flag) in generate_ha.py - B3 semantic project diff (diffproj.py; diff_projects / diff_loaded) - B4 per-function acceptance test protocol (advanced.py; generate_test_protocol) - B5 Matter readiness (advanced.py; check_matter) - B6 metering/energy DPT check + PV/battery/EVSE scaffold (advanced.py; check_energy) C-tier: - C1 KNX IoT Turtle/RDF export (iot.py; generate_knx_iot) - C2 naming-hygiene suggestions (advanced.py; suggest_names) - C3 as-built completeness grader (advanced.py; grade_completeness) A-tier quick wins: - A5 Areas/voice UI-only note; A6 time/date DPT-19.001 expose block (generate_ha.py) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
"""B3 — semantic diff between two .knxproj versions.
|
||
|
||
Turns two Git snapshots of a project into a reviewable change set: which group
|
||
addresses were added / removed, which changed DPT, name or security flag. No
|
||
mainstream tool does this (xknxproject explicitly can't); our Git-tracked, read-only
|
||
workflow is its natural home. Read-only, no bus.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Optional
|
||
|
||
from .project import load_project
|
||
|
||
|
||
def diff_projects(path_a: str, path_b: str,
|
||
password_a: Optional[str] = None,
|
||
password_b: Optional[str] = None) -> dict[str, Any]:
|
||
"""Compare two .knxproj files. `path_a` = old/base, `path_b` = new."""
|
||
a = load_project(path_a, password=password_a)
|
||
b = load_project(path_b, password=password_b)
|
||
return diff_loaded(a, b)
|
||
|
||
|
||
def diff_loaded(a, b) -> dict[str, Any]:
|
||
"""Diff two already-loaded projects (testable core of diff_projects)."""
|
||
ga_a, ga_b = a.gas, b.gas
|
||
keys_a, keys_b = set(ga_a), set(ga_b)
|
||
|
||
added = [{"address": k, "name": ga_b[k].name, "dpt": ga_b[k].dpt}
|
||
for k in sorted(keys_b - keys_a)]
|
||
removed = [{"address": k, "name": ga_a[k].name, "dpt": ga_a[k].dpt}
|
||
for k in sorted(keys_a - keys_b)]
|
||
dpt_changed, renamed, secure_changed = [], [], []
|
||
for k in sorted(keys_a & keys_b):
|
||
x, y = ga_a[k], ga_b[k]
|
||
if (x.dpt or None) != (y.dpt or None):
|
||
dpt_changed.append({"address": k, "name": y.name,
|
||
"from": x.dpt, "to": y.dpt})
|
||
if x.name.strip() != y.name.strip():
|
||
renamed.append({"address": k, "from": x.name, "to": y.name})
|
||
if x.data_secure != y.data_secure:
|
||
secure_changed.append({"address": k, "name": y.name,
|
||
"from": x.data_secure, "to": y.data_secure})
|
||
|
||
def _section(title: str, rows: list, cols) -> list[str]:
|
||
out = [f"\n## {title} ({len(rows)})\n"]
|
||
if not rows:
|
||
out.append("_none_")
|
||
return out
|
||
out.append("| " + " | ".join(cols) + " |")
|
||
out.append("|" + "|".join("---" for _ in cols) + "|")
|
||
for r in rows[:500]:
|
||
out.append("| " + " | ".join(f"`{r.get(c.lower().replace(' ','_'),'')}`"
|
||
if c in ("Address",) else str(r.get(c.lower().replace(' ','_'), ''))
|
||
for c in cols) + " |")
|
||
if len(rows) > 500:
|
||
out.append(f"| … and {len(rows)-500} more |")
|
||
return out
|
||
|
||
md = [f"# Project diff — {a.info.get('name','A')} → {b.info.get('name','B')}\n",
|
||
f"- Base: **{len(ga_a)}** GA · New: **{len(ga_b)}** GA "
|
||
f"(+{len(added)} / −{len(removed)})\n"]
|
||
md += _section("Added", added, ["Address", "Name", "Dpt"])
|
||
md += _section("Removed", removed, ["Address", "Name", "Dpt"])
|
||
md += _section("DPT changed", dpt_changed, ["Address", "From", "To"])
|
||
md += _section("Renamed", renamed, ["Address", "From", "To"])
|
||
md += _section("Security flag changed", secure_changed, ["Address", "From", "To"])
|
||
|
||
return {
|
||
"base_ga": len(ga_a), "new_ga": len(ga_b),
|
||
"added": len(added), "removed": len(removed),
|
||
"dpt_changed": len(dpt_changed), "renamed": len(renamed),
|
||
"secure_changed": len(secure_changed),
|
||
"markdown": "\n".join(md),
|
||
"detail": {"added": added, "removed": removed, "dpt_changed": dpt_changed,
|
||
"renamed": renamed, "secure_changed": secure_changed},
|
||
}
|