mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-10-18 04:42:07 +02:00
generate_ha: a command GA the actuator reports on is its own state address
Found on a real 1312-GA house (43 of 159 "switch without status" had a Read+Transmit object on the command GA), measured on all seven corpus projects. detect_missing_status already counted these as satisfied through pairing.self_reporting(); the generator did not and put them on the review list without a state_address. It now uses the same function: no separate status found and the command GA has an R+T object -> state_address = the command GA. Same for the on/off sibling of a dimmable light and for brightness_state_address on a self-reporting brightness GA. Write-only objects still go to review. Corpus: no entity count changed, no address lost; review items 598 -> 555 on the house, -36, -40, -12, -1, -32 on the others. Test C in test_real_house_fixes.py covers R+T, write-only and a non-lighting switch. Not verified on a live Home Assistant: state_address equal to address is accepted by the KNX schema and xknx reads from it, but no running instance was used here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3968ce2cde
commit
76249469bb
@@ -20,6 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
and 14.031, and power factor has its own rule (14.057). Checked against xknx 3.20. On a real
|
||||
1312-GA house: 17 findings -> 2.
|
||||
|
||||
- **Self-reporting commands get their state address.** When the actuator's status object (Read +
|
||||
Transmit flags) is linked to the command GA itself, there is no separate status GA to find. The
|
||||
generator now uses the command GA as `state_address` (and the brightness GA as
|
||||
`brightness_state_address`) instead of reporting a missing status. `check_missing_status` already
|
||||
treated these as satisfied; the two now agree. On a real house 43 fewer review items, 40 to 44
|
||||
entities per large project gain a state address, and no entity count changes.
|
||||
|
||||
### Changed
|
||||
|
||||
- **On/off lighting is generated as a Home Assistant `light`, not a `switch`.** The KNX light platform
|
||||
|
||||
@@ -15,7 +15,7 @@ import yaml
|
||||
|
||||
from .project import LoadedProject, GARecord
|
||||
from .analyze import _is_status_ga
|
||||
from .pairing import find_status, base_tokens, function_status_pairs
|
||||
from .pairing import find_status, base_tokens, function_status_pairs, self_reporting
|
||||
|
||||
# Venetian-blind slat (tilt) detection: a slat GA is the tilt of its parent
|
||||
# blind, not a standalone cover.
|
||||
@@ -440,12 +440,16 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
|
||||
if st5:
|
||||
entity["brightness_state_address"] = st5.address
|
||||
consumed.add(st5.address)
|
||||
elif self_reporting(ga, project):
|
||||
entity["brightness_state_address"] = ga.address
|
||||
if sib is not None:
|
||||
entity["address"] = sib.address
|
||||
s1 = status_for_dpt(sib, 1) # on/off status (1.x)
|
||||
if s1:
|
||||
entity["state_address"] = s1.address
|
||||
consumed.add(s1.address)
|
||||
elif self_reporting(sib, project):
|
||||
entity["state_address"] = sib.address
|
||||
consumed.add(sib.address)
|
||||
attach_colour(entity, ga) # RGB/RGBW/xyY/colour-temp of this zone
|
||||
lights.append(entity)
|
||||
@@ -498,6 +502,11 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
|
||||
if st:
|
||||
entity["state_address"] = st.address
|
||||
consumed.add(st.address)
|
||||
elif self_reporting(ga, project):
|
||||
# The actuator's status object (Read + Transmit) is linked to the command GA
|
||||
# itself, so the command GA is its own state. detect_missing_status already
|
||||
# treats these as satisfied; the generator now agrees.
|
||||
entity["state_address"] = ga.address
|
||||
else:
|
||||
review.append({"reason": "light_without_status" if is_light else "switch_without_status",
|
||||
"address": ga.address, "name": ga.name})
|
||||
|
||||
@@ -76,4 +76,34 @@ assert "1/0/1" in lightsB and lightsB["1/0/1"].get("state_address") == "1/1/1",
|
||||
assert not any(s["address"] == "1/0/1" for s in knxB.get("switch", [])), knxB
|
||||
print("OK: B — on/off lighting is a light with address + state_address, not a switch")
|
||||
|
||||
# ---------------------------------------------------------------- C. self-reporting status
|
||||
def _proj_with_cos(rows, flags_by_addr):
|
||||
gas, cos = {}, {}
|
||||
for a, n, m, s in rows:
|
||||
g = _ga(a, n, m, s)
|
||||
if a in flags_by_addr:
|
||||
cid = f"CO-{a}"
|
||||
g["communication_object_ids"] = [cid]
|
||||
cos[cid] = {"flags": flags_by_addr[a], "group_address_links": [a], "dpts": [], "text": "", "function_text": ""}
|
||||
gas[a] = g
|
||||
return build_loaded_from_raw({"group_addresses": gas, "communication_objects": cos,
|
||||
"info": {"group_address_style": "ThreeLevel"}}, "mem")
|
||||
|
||||
pC = _proj_with_cos(
|
||||
[("1/0/5", "1.02 Hall - Ceiling light - on/off", 1, 1),
|
||||
("1/0/6", "1.03 Living - Floor lamp light - on/off", 1, 1),
|
||||
("5/0/5", "Workshop socket - on/off", 1, 1)],
|
||||
{"1/0/5": {"write": True, "read": True, "transmit": True},
|
||||
"1/0/6": {"write": True, "read": False, "transmit": False},
|
||||
"5/0/5": {"write": True, "read": True, "transmit": True}})
|
||||
resC = generate_ha_yaml(pC)
|
||||
knxC = yaml.safe_load(resC["yaml"].split("\n\n", 1)[1])["knx"]
|
||||
ents = {e["address"]: e for plat in ("light", "switch") for e in knxC.get(plat, [])}
|
||||
assert ents["1/0/5"].get("state_address") == "1/0/5", ents
|
||||
assert ents["5/0/5"].get("state_address") == "5/0/5", ents
|
||||
assert "state_address" not in ents["1/0/6"], ents
|
||||
no_status = {r["address"] for r in resC["review"] if r["reason"] in ("light_without_status", "switch_without_status")}
|
||||
assert no_status == {"1/0/6"}, no_status
|
||||
print("OK: C — Read+Transmit object on the command GA is its own state; write-only stays in review")
|
||||
|
||||
print("\nALL REAL-HOUSE REGRESSION TESTS PASSED")
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"sensor": 55,
|
||||
"switch": 42
|
||||
},
|
||||
"ha_review_items": 413,
|
||||
"ha_review_items": 401,
|
||||
"suggestion_hints": {
|
||||
"channels": 229,
|
||||
"diagnostics_skipped": 2,
|
||||
@@ -141,7 +141,7 @@
|
||||
"sensor": 19,
|
||||
"switch": 3
|
||||
},
|
||||
"ha_review_items": 69,
|
||||
"ha_review_items": 68,
|
||||
"suggestion_hints": {
|
||||
"channels": 52,
|
||||
"diagnostics_skipped": 0,
|
||||
@@ -198,7 +198,7 @@
|
||||
"sensor": 87,
|
||||
"switch": 48
|
||||
},
|
||||
"ha_review_items": 234,
|
||||
"ha_review_items": 202,
|
||||
"suggestion_hints": {
|
||||
"channels": 135,
|
||||
"diagnostics_skipped": 8,
|
||||
@@ -263,7 +263,7 @@
|
||||
"sensor": 111,
|
||||
"switch": 79
|
||||
},
|
||||
"ha_review_items": 326,
|
||||
"ha_review_items": 286,
|
||||
"suggestion_hints": {
|
||||
"channels": 179,
|
||||
"diagnostics_skipped": 21,
|
||||
@@ -328,7 +328,7 @@
|
||||
"sensor": 159,
|
||||
"switch": 95
|
||||
},
|
||||
"ha_review_items": 598,
|
||||
"ha_review_items": 555,
|
||||
"suggestion_hints": {
|
||||
"channels": 305,
|
||||
"diagnostics_skipped": 40,
|
||||
@@ -394,7 +394,7 @@
|
||||
"sensor": 343,
|
||||
"switch": 319
|
||||
},
|
||||
"ha_review_items": 1589,
|
||||
"ha_review_items": 1553,
|
||||
"suggestion_hints": {
|
||||
"channels": 983,
|
||||
"diagnostics_skipped": 53,
|
||||
|
||||
Reference in New Issue
Block a user