fix: council-review noise cuts — scenes/central-macros exempt, language-matched repairs

External expert review (real run on the demo house) flagged: scenes (18.001)
wrongly marked missing_status + an unsafe DPT-1.011 status repair for them;
'All blinds down' warned while 'All lights off' was correctly INFO; and a Russian
'(статус)' suffix synthesised on an English project. Fixes: scene_no_status INFO
+ no synth status for scenes; broadened central-macro detection (generic 'all
blinds/shutters/...'); language-matched repair suffixes. Demo missing_status
warnings ~7 -> 1 genuine. tests/test_council_fixes.py; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nikolay1
2026-07-15 09:09:57 +02:00
co-authored by Claude Opus 4.8
parent 1e924ede6f
commit c53f388dcb
4 changed files with 101 additions and 3 deletions
+18 -1
View File
@@ -171,6 +171,9 @@ def _is_status_ga(ga: GARecord) -> bool:
# rather than a defect — surfaced as INFO, not a 🟡 warning.
_CENTRAL_MACRO_TOKENS = (
"общее", "групповое", "все ", "всё", "central", "all groups", "all lights",
# generic group/broadcast commands (multi-word so they don't false-match "wall")
"all blinds", "all shutters", "all covers", "all windows", "all sockets",
"all off", "all on", "master off", "всех ",
)
@@ -179,6 +182,12 @@ def _is_central_macro(name: str) -> bool:
return any(t in low for t in _CENTRAL_MACRO_TOKENS)
def _is_scene(ga: GARecord) -> bool:
"""Scene-control / scene-number GAs (17.x / 18.x) have no single real state to
read back — a missing status is expected, not a defect."""
return ga.dpt_main in (17, 18) or ga.category == "scene"
# --------------------------------------------------------------------------- #
# Sub-DPT sanity (A1): a name implies a specific DPT sub-type. Flag when the
# main matches but the sub is wrong (9.001 temp vs 9.004 lux), or — for strong
@@ -257,7 +266,15 @@ def detect_missing_status(project: LoadedProject) -> list[dict[str, Any]]:
if self_reporting(ga, project):
n_self_report += 1
continue
if _is_central_macro(ga.name):
if _is_scene(ga):
findings.append(_finding(
SEVERITY_INFO, "scene_no_status", addr,
f"Scene control '{ga.name}' (DPT {ga.dpt or '?'}) has no status GA — "
"expected: a scene recalls/stores a preset, it has no single state to "
"read back. No status is applicable.",
name=ga.name, dpt=ga.dpt, category=ga.category,
))
elif _is_central_macro(ga.name):
findings.append(_finding(
SEVERITY_INFO, "central_macro_no_status", addr,
f"Central/group macro '{ga.name}' has no status GA — expected "
+13 -2
View File
@@ -22,6 +22,16 @@ _STATUS_DPT = {1: "1.011", 3: "5.001", 5: "5.001", 9: "9.001",
13: "13.013", 14: "14.056", 20: "20.102"}
def _has_cyrillic(s: str) -> bool:
return any("Ѐ" <= c <= "ӿ" for c in (s or ""))
def _suffix(name: str, ru: str, en: str) -> str:
"""Match the suffix language to the GA name so an English project doesn't get a
Russian '(статус)' and vice-versa (council feedback: mixed-language names)."""
return ru if _has_cyrillic(name) else en
def _infer_dpt(ga: Any) -> str:
"""Best-effort DPT for a GA that has none, from its name/category/kind."""
exp = _expected_subdpt(ga.name)
@@ -88,7 +98,8 @@ def suggest_repairs(project: LoadedProject) -> dict[str, Any]:
new = _next_free(used, ga.main if ga.main is not None else 1)
proposals.append({
"code": code, "action": "add_ga", "address": new, "for": addr,
"name": f"{ga.name} - Значение яркости", "dpt": "5.001",
"name": f"{ga.name}{_suffix(ga.name, ' - Значение яркости', ' - Brightness value')}",
"dpt": "5.001",
"rationale": "absolute-brightness GA so Home Assistant can set a level",
})
@@ -103,7 +114,7 @@ def suggest_repairs(project: LoadedProject) -> dict[str, Any]:
new = _next_free(used, ga.main if ga.main is not None else 1, prefer_middle=4)
proposals.append({
"code": "missing_status", "action": "add_ga", "address": new, "for": addr,
"name": f"{ga.name} (статус)", "dpt": sdpt,
"name": f"{ga.name}{_suffix(ga.name, ' (статус)', ' (status)')}", "dpt": sdpt,
"rationale": "status/feedback GA so Home Assistant reads real state",
})