mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
A 1-bit switch is domain-agnostic, yet DPT 1.001 defaulted to 'lighting', so an "AC on/off" was classified lighting and tripped downstream checks. The domain is now a combination: explicit name > strong domain-encoding DPT > main-group name; a bare 1-bit GA with no signal is honestly 'unknown', never a guessed lighting. An explicit name contradicting a STRONG DPT yields 'unknown' (a genuine conflict, shown by explain_ga as contested), not a silent pick. Word-boundary matching so "ac" no longer fires inside "terrace"; name terms broadened (EN+RU HVAC incl. ac/a-c/air-conditioner, RGB/colour lighting). check_policy taxonomy-outlier now flags only misplaced ACTUATORS (lighting/ shutter/hvac); sensors, scene links, thresholds and timeout params are the cross-cutting GAs they are. HA generator still emits a switch for a now-HVAC simple on/off, so no device is dropped. Verified on 3 real projects: every AC on/off is HVAC; outlier noise fell demo 11->2, Minsk 31->6, Razdory 200->57. tests/test_domain_classifier.py (13/13 green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.2 KiB
Python
67 lines
3.2 KiB
Python
"""explain_ga — provenance / confidence for one GA (evidence + conflict + pairing tier)."""
|
|
from nickol_knx_mcp.project import build_loaded_from_raw
|
|
from nickol_knx_mcp.explain import explain_ga
|
|
|
|
|
|
def _ga(addr, name, dmain, dsub):
|
|
return {"name": name, "identifier": f"GA-{addr}", "raw_address": 0, "address": addr,
|
|
"project_uid": None, "dpt": {"main": dmain, "sub": dsub}, "data_secure": False,
|
|
"communication_object_ids": [], "description": "", "comment": ""}
|
|
|
|
|
|
def _proj(gas):
|
|
raw = {"info": {"group_address_style": "ThreeLevel", "schema_version": "21"},
|
|
"group_addresses": gas, "communication_objects": {}, "devices": {},
|
|
"functions": {}, "topology": {}, "group_ranges": {}}
|
|
return build_loaded_from_raw(raw, "t.knxproj")
|
|
|
|
|
|
def main():
|
|
gas = {
|
|
# 1-bit switch (domain-agnostic DPT) named "AC" -> correctly HVAC, NOT a
|
|
# conflict: the DPT does not encode a domain, the name legitimately does.
|
|
"3/2/1": _ga("3/2/1", "Living room AC on/off", 1, 1),
|
|
# strong, domain-encoding DPT (3.007 dimming = lighting) named for another
|
|
# domain ("fan" = hvac) -> a GENUINE conflict -> 'unknown' / contested.
|
|
"3/3/1": _ga("3/3/1", "Bathroom fan", 3, 7),
|
|
"1/0/1": _ga("1/0/1", "Kitchen light switch", 1, 1), # name-derived lighting command
|
|
"1/4/1": _ga("1/4/1", "Kitchen light status", 1, 11), # its status
|
|
}
|
|
p = _proj(gas)
|
|
|
|
# 1. AC on/off is now correctly HVAC (name over an agnostic DPT), no conflict
|
|
ac = explain_ga(p, "3/2/1")
|
|
assert ac["category"]["value"] == "hvac", ac["category"]
|
|
assert ac["conflicts"] == "none", ac["conflicts"]
|
|
assert ac["confidence"] == "heuristic", ac["confidence"] # name decided it
|
|
assert any(e["tier"] == "structural" for e in ac["category"]["evidence"]) # DPT still shown
|
|
assert any("hvac" in e["signal"] for e in ac["category"]["evidence"])
|
|
|
|
# 2. strong DPT vs name -> genuine conflict, contested, resolved to unknown
|
|
fan = explain_ga(p, "3/3/1")
|
|
assert fan["category"]["value"] == "unknown", fan["category"]
|
|
assert fan["conflicts"] != "none" and any("hvac" in c for c in fan["conflicts"]), fan["conflicts"]
|
|
assert fan["confidence"] == "contested", fan["confidence"]
|
|
|
|
# 3. name-derived lighting switch: no conflict, heuristic (name, not the agnostic
|
|
# DPT), paired to its status by name token
|
|
sw = explain_ga(p, "1/0/1")
|
|
assert sw["category"]["value"] == "lighting", sw["category"]
|
|
assert sw["conflicts"] == "none", sw["conflicts"]
|
|
assert sw["confidence"] == "heuristic", sw["confidence"]
|
|
assert sw["status_pairing"]["paired"] is True
|
|
assert sw["status_pairing"]["method"] == "name_token"
|
|
assert sw["status_pairing"]["status"] == "1/4/1"
|
|
assert sw["status_pairing"]["tier"] == "heuristic"
|
|
|
|
# 4. missing address -> error
|
|
assert "error" in explain_ga(p, "9/9/9")
|
|
|
|
print("test_explain: OK — AC on/off is HVAC via name over an agnostic DPT (no conflict); "
|
|
"a strong DPT contradicting the name is contested/unknown; name-derived lighting "
|
|
"switch is heuristic + name-token paired; missing address errors.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|