mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
feat(analyze): de-noise [LF] objects, central macros, separator scratch
Three false-alarm downgrades derived from validating on a larger multi-vendor villa, each turning a false positive into an INFO note without hiding any real defect: - intent: divider/separator names (punctuation-only or a marker wrapped in it, e.g. "-----addition------", "---") classify as `scratch`, so a missing DPT on them is INFO, not a red error. - analyze: typed GAs wired into Zennio "[LF] ... Data Entry" type-agnostic logic-function containers surface as INFO `dpt_on_logic_object` instead of a false `dpt_mismatch_co` warning. - analyze: all-groups broadcast/central-macro commands surface as INFO `central_macro_no_status` instead of `missing_status_address`, since a fan-out broadcast has no single state to read back. Adds a regression block covering all three; real DPT mismatches and real missing-status gaps still warn. Unreleased; no version bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bc33273cd4
commit
87f065dde7
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
**Noise-reduction refinements from a 3646-GA multi-vendor villa.** Validated the tool on a
|
||||
second real signed Zennio project (5× larger, no ETS Functions). Three de-noising rules,
|
||||
each downgrading a false alarm to INFO without hiding any real problem — on that project
|
||||
errors fell **36 → 32** and warnings **179 → 139**.
|
||||
|
||||
### Added
|
||||
- **Divider/separator scratch detection** — commissioning placeholder names made only of
|
||||
punctuation (`---`, `=====`) or a marker wrapped in it (`-----addition------`) now classify
|
||||
as `scratch` intent, so a missing DPT on them is an INFO note, not a 🔴 error.
|
||||
- **Logic-function object awareness** — a typed GA (e.g. 9.x temperature) wired into a Zennio
|
||||
`[LF] … Data Entry` container (intentionally type-agnostic, raw 2-byte) surfaces as INFO
|
||||
`dpt_on_logic_object` instead of a false `dpt_mismatch_co` warning.
|
||||
- **Central-macro status tolerance** — all-groups broadcast commands (`Общее …`, `Все группы`,
|
||||
`Все шторы - Стоп`) surface as INFO `central_macro_no_status` instead of a
|
||||
`missing_status_address` warning, since a fan-out broadcast has no single state to read back.
|
||||
|
||||
## [0.2.0] — 2026-06-30
|
||||
|
||||
**Colour/climate entity assembly and GA-intent noise reduction.** Two feature tracks
|
||||
|
||||
@@ -156,6 +156,20 @@ def _is_status_ga(ga: GARecord) -> bool:
|
||||
return any(k in low for k in STATUS_KEYWORDS)
|
||||
|
||||
|
||||
# Central / group-macro command names ("Общее освещение - Все группы", "Все
|
||||
# шторы - стоп", "Групповое включение"). A broadcast that fans out to many
|
||||
# actuators has no single state to read back, so a missing status is expected
|
||||
# rather than a defect — surfaced as INFO, not a 🟡 warning.
|
||||
_CENTRAL_MACRO_TOKENS = (
|
||||
"общее", "групповое", "все ", "всё", "central", "all groups", "all lights",
|
||||
)
|
||||
|
||||
|
||||
def _is_central_macro(name: str) -> bool:
|
||||
low = (name or "").lower()
|
||||
return any(t in low for t in _CENTRAL_MACRO_TOKENS)
|
||||
|
||||
|
||||
# command DPT main -> acceptable status DPT mains
|
||||
_STATUS_COMPAT = {
|
||||
1: {1}, # switch command -> 1.x status
|
||||
@@ -197,12 +211,20 @@ def detect_missing_status(project: LoadedProject) -> list[dict[str, Any]]:
|
||||
same_main = [s for s in status_gas if s.main == ga.main]
|
||||
match = find_status(ga, same_main) or find_status(ga, status_gas)
|
||||
if match is None:
|
||||
findings.append(_finding(
|
||||
SEVERITY_WARN, "missing_status_address", addr,
|
||||
f"Command '{ga.name}' (DPT {ga.dpt or '?'}, {ga.label}) has no "
|
||||
"status/feedback GA. Home Assistant cannot read real state.",
|
||||
name=ga.name, dpt=ga.dpt, category=ga.category,
|
||||
))
|
||||
if _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 "
|
||||
"for an all-groups broadcast; there is no single state to read back.",
|
||||
name=ga.name, dpt=ga.dpt, category=ga.category,
|
||||
))
|
||||
else:
|
||||
findings.append(_finding(
|
||||
SEVERITY_WARN, "missing_status_address", addr,
|
||||
f"Command '{ga.name}' (DPT {ga.dpt or '?'}, {ga.label}) has no "
|
||||
"status/feedback GA. Home Assistant cannot read real state.",
|
||||
name=ga.name, dpt=ga.dpt, category=ga.category,
|
||||
))
|
||||
|
||||
return findings
|
||||
|
||||
@@ -247,6 +269,20 @@ def detect_dpt_issues(project: LoadedProject) -> list[dict[str, Any]]:
|
||||
continue
|
||||
mains = {d.get("main") for d in co_dpts}
|
||||
if ga.dpt_main not in mains:
|
||||
co_name = (co.get("name") or "").strip()
|
||||
# Zennio Logic-Function data-entry objects ("[LF] (2-Byte) Data
|
||||
# Entry N") are intentionally type-agnostic raw containers; wiring
|
||||
# a typed GA (9.x temperature, etc.) into one is expected design,
|
||||
# not a DPT bug. Surface it as INFO instead of a 🟡 warning.
|
||||
if co_name.lower().startswith("[lf]"):
|
||||
findings.append(_finding(
|
||||
SEVERITY_INFO, "dpt_on_logic_object", addr,
|
||||
f"GA DPT main {ga.dpt_main} links a type-agnostic logic-function "
|
||||
f"object '{co_name}' (DPT main {sorted(m for m in mains if m is not None)}) "
|
||||
"— expected for LF data-entry blocks, not a mismatch.",
|
||||
name=ga.name,
|
||||
))
|
||||
break
|
||||
findings.append(_finding(
|
||||
SEVERITY_WARN, "dpt_mismatch_co", addr,
|
||||
f"GA DPT main {ga.dpt_main} differs from linked communication "
|
||||
|
||||
@@ -46,6 +46,14 @@ _SCRATCH_EXACT = (
|
||||
"neue gruppenadresse",
|
||||
)
|
||||
|
||||
# Commissioning divider / marker words that appear wrapped in punctuation
|
||||
# ("-----addition------", "--- разделитель ---"). Checked after stripping the
|
||||
# surrounding separators.
|
||||
_SCRATCH_CORE = ("addition", "разделитель", "separator", "divider")
|
||||
|
||||
# Characters used to draw separators / dividers in GA names.
|
||||
_SEP_CHARS = "-–—_=+*.•·~<>| \t"
|
||||
|
||||
|
||||
def classify_intent(name: str) -> str:
|
||||
"""Return one of functional / reserve / logic / scratch for a GA name."""
|
||||
@@ -62,6 +70,13 @@ def classify_intent(name: str) -> str:
|
||||
# Bare numeric placeholders like "1", "5", "12" — classic leftovers.
|
||||
if re.fullmatch(r"\d{1,3}", raw):
|
||||
return INTENT_SCRATCH
|
||||
# Divider / separator placeholders: a name that is only punctuation ("---",
|
||||
# "=====") or a marker word wrapped in it ("-----addition------"). These are
|
||||
# commissioning scratch, not functional GAs, so a missing DPT on them is
|
||||
# intentional rather than a 🔴 error.
|
||||
core = raw.strip(_SEP_CHARS)
|
||||
if not core or core.lower() in _SCRATCH_CORE:
|
||||
return INTENT_SCRATCH
|
||||
|
||||
if any(t in low for t in _RESERVE_TOKENS):
|
||||
return INTENT_RESERVE
|
||||
|
||||
@@ -271,7 +271,13 @@ assert classify_intent("Метеостанция - Запрос температ
|
||||
assert classify_intent("суммарный сигнал включения групп света") == INTENT_LOGIC
|
||||
assert classify_intent("1") == INTENT_SCRATCH
|
||||
assert classify_intent("Новый групповой адрес") == INTENT_SCRATCH
|
||||
# divider / separator commissioning scratch (large-villa refinement)
|
||||
assert classify_intent("-----addition------") == INTENT_SCRATCH
|
||||
assert classify_intent("---") == INTENT_SCRATCH
|
||||
assert classify_intent("=====") == INTENT_SCRATCH
|
||||
assert classify_intent("Kitchen ceiling light switch") == INTENT_FUNCTIONAL
|
||||
# a real name that merely CONTAINS dashes must stay functional
|
||||
assert classify_intent("01. Кухня - Свет - Вкл/выкл") == INTENT_FUNCTIONAL
|
||||
|
||||
_rawD = {"group_addresses": {
|
||||
# reserve spares: no DPT (intentional) + same name, different DPTs
|
||||
@@ -314,6 +320,55 @@ assert "9/2/0" not in _msD, "scratch GA wrongly flagged missing_status"
|
||||
assert "1/0/0" in _msD, "real functional command lost its missing-status warning!"
|
||||
print("OK: reserve/logic/scratch de-noised; real DPT + status problems preserved")
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Regression (large-villa refinements): [LF] logic-object DPT + central-macro status.
|
||||
# From a real 3646-GA villa where (a) typed GAs wired into Zennio "[LF] Data
|
||||
# Entry" objects produced false DPT-mismatch warnings, and (b) central "Все
|
||||
# группы"/"Общее" broadcast commands produced false missing-status warnings.
|
||||
# Both must be downgraded to INFO while REAL mismatches / gaps still warn.
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n=== REGRESSION: [LF] logic-object + central-macro de-noise (large villa) ===")
|
||||
|
||||
_rawLF = {
|
||||
"communication_objects": {
|
||||
"CO-LF": {"name": "[LF] (2-Byte) Data Entry 1", "number": 1, "text": "",
|
||||
"function_text": "", "description": "", "device_address": "1.1.9",
|
||||
"device_application": None, "module": None, "channel": None,
|
||||
"dpts": [{"main": 7, "sub": 1}], "object_size": "2 bytes",
|
||||
"group_address_links": ["5/0/0"], "flags": {}, "dpas": None},
|
||||
"CO-REAL": {"name": "Operation status", "number": 2, "text": "",
|
||||
"function_text": "", "description": "", "device_address": "1.1.9",
|
||||
"device_application": None, "module": None, "channel": None,
|
||||
"dpts": [{"main": 7, "sub": 1}], "object_size": "2 bytes",
|
||||
"group_address_links": ["6/1/13"], "flags": {}, "dpas": None},
|
||||
},
|
||||
"group_addresses": {
|
||||
# typed temperature GA wired into an [LF] container -> INFO, not warning
|
||||
"5/0/0": ga("5/0/0", "01. Холл - АТ - Воздух", 9, 1, co_ids=["CO-LF"]),
|
||||
# real bitfield-vs-object mismatch on a normal object -> still a warning
|
||||
"6/1/13": ga("6/1/13", "Основная В/У - Статус работы", 237, 600, co_ids=["CO-REAL"]),
|
||||
},
|
||||
}
|
||||
_lf = detect_dpt_issues(build_loaded_from_raw(_rawLF, "mem"))
|
||||
_lfc = {(f["code"], f["address"]) for f in _lf}
|
||||
assert ("dpt_on_logic_object", "5/0/0") in _lfc, _lfc
|
||||
assert ("dpt_mismatch_co", "5/0/0") not in _lfc, "[LF] object wrongly warned as mismatch"
|
||||
assert ("dpt_mismatch_co", "6/1/13") in _lfc, "real DPT mismatch was suppressed!"
|
||||
|
||||
_rawCM = {"group_addresses": {
|
||||
"0/0/1": ga("0/0/1", "Общее освещение - Все группы", 1, 1), # central macro
|
||||
"0/1/0": ga("0/1/0", "Все шторы - Стоп", 1, 10), # central macro
|
||||
"1/0/9": ga("1/0/9", "Гостиная - Бра - Вкл", 1, 1), # normal cmd, no status
|
||||
}}
|
||||
_cm = detect_missing_status(build_loaded_from_raw(_rawCM, "mem"))
|
||||
_cmc = {(f["code"], f["address"]) for f in _cm}
|
||||
assert ("central_macro_no_status", "0/0/1") in _cmc, _cmc
|
||||
assert ("missing_status_address", "0/0/1") not in _cmc, "central macro wrongly warned"
|
||||
assert ("central_macro_no_status", "0/1/0") in _cmc, _cmc
|
||||
# a normal command with no status MUST still warn (no over-suppression)
|
||||
assert ("missing_status_address", "1/0/9") in _cmc, "real missing-status lost!"
|
||||
print("OK: [LF] objects + central macros de-noised; real mismatches/gaps preserved")
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Regression (Track A): colour light assembly + B1 (no borrowed brightness_state)
|
||||
# + climate assembly. Patterns from the real signed demo / a real Zennio project.
|
||||
|
||||
Reference in New Issue
Block a user