generate_ha: on/off lighting becomes a light; brightness-only "lights" go to review instead of invalid YAML

Found on a real 1312-GA house, measured on all seven corpus projects.

B. Lighting on/off (1.001, lighting category) is emitted as a Home Assistant light with
   address + state_address, not a switch. The HA KNX light platform documents exactly
   that ("Simple light"). suggest.py already proposed light for these; the two engines
   disagreed. Without status it is reported as light_without_status.

D. A 5.001 lighting GA with no on/off GA in its zone produced a light with only
   brightness_address. Home Assistant requires address on a KNX light, so that YAML was
   invalid. 51 of them across six projects, mostly motion-detector parameters on 5.001.
   They now go to review as light_without_switch, and the brightness status is no longer
   consumed for an entity that does not exist, so it surfaces as not_mapped.

Corpus, before -> after: lights without address 51 -> 0 everywhere; no address lost from
YAML or review on any project; switch -> light moves of 220 (house), 205, 62, 43, 17, 10,
13. test_pipeline B1 fixture gains its on/off GAs (it encoded an invalid light) plus a
test for the review route. CHANGELOG also records fix A from 481bab0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nikolay Miroshnichenko
2026-09-16 18:32:42 +02:00
co-authored by Claude Opus 5
parent 481bab0e0c
commit cb226a008f
5 changed files with 100 additions and 28 deletions
+25 -8
View File
@@ -413,11 +413,6 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
if ga.address in consumed:
continue
if ga.category == "lighting" and ga.dpt_main == 5 and ga.kind == "command":
entity = {"name": ga.name, "brightness_address": ga.address}
st5 = status_for_dpt(ga, 5) # brightness status (5.x)
if st5:
entity["brightness_state_address"] = st5.address
consumed.add(st5.address)
# two-pass sibling pick: EXACT identity first, subset only as a
# fallback — else "RGBW подсветка яркость" ({living, подсветка})
# grabs "камин подсветка" ({living, камин, подсветка}) by subset
@@ -428,6 +423,23 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
my_ident = _pair_ident(ga.name)
sib = next((s for s in sibs if _pair_ident(s.name) == my_ident), None) \
or next((s for s in sibs if _identity_match(ga.name, s.name)), None)
if sib is None:
# Home Assistant requires `address` on a KNX light, so a brightness GA
# with no on/off GA in its zone would be invalid YAML. On real projects
# these are mostly device parameters on 5.001 (motion-detector
# sensitivity, "daytime command"), not lights. Fail closed.
review.append({"reason": "light_without_switch", "address": ga.address,
"name": ga.name, "dpt": ga.dpt,
"hint": "5.001 lighting GA with no on/off GA in its zone. A Home "
"Assistant light needs `address`; attach the on/off GA "
"manually, or it is a device parameter, not a light."})
consumed.add(ga.address)
continue
entity = {"name": ga.name, "brightness_address": ga.address}
st5 = status_for_dpt(ga, 5) # brightness status (5.x)
if st5:
entity["brightness_state_address"] = st5.address
consumed.add(st5.address)
if sib is not None:
entity["address"] = sib.address
s1 = status_for_dpt(sib, 1) # on/off status (1.x)
@@ -466,7 +478,7 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
attach_colour(entity, ga)
lights.append(entity)
# ---- 3. SWITCHES (1.001 command) ----
# ---- 3. ON/OFF LIGHTS and SWITCHES (1.001 command) ----
for ga in project.gas.values():
if ga.address in consumed:
continue
@@ -477,14 +489,19 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
consumed.add(ga.address)
continue
entity = {"name": ga.name, "address": ga.address}
# Lighting on/off is a light in Home Assistant, not a switch: the KNX light
# platform takes a plain on/off light with `address` + `state_address`
# ("Simple light" in the HA KNX docs), and light entities are what Assist
# and "all lights" targeting act on.
is_light = ga.category == "lighting"
st = status_for_dpt(ga, 1)
if st:
entity["state_address"] = st.address
consumed.add(st.address)
else:
review.append({"reason": "switch_without_status",
review.append({"reason": "light_without_status" if is_light else "switch_without_status",
"address": ga.address, "name": ga.name})
switches.append(entity)
(lights if is_light else switches).append(entity)
consumed.add(ga.address)
# ---- 3b. CLIMATE — anchored on an HVAC mode (DPT 20.102/20.105). Zone