mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-30 03:41:58 +02:00
fix(generate_ha): Russian function words in identity + exact-identity outranks subset
Dogfooding the voice layer exposed two pairing defects: (1) _FUNC_WORDS had no
Russian entries, so 'вкл/выкл/цвет/яркость' polluted device identity and an RGBW
colour GA could not find its own switch; (2) subset identity matching let
'RGBW подсветка яркость' {zone, подсветка} grab 'камин подсветка' by subset and
its status by tie-break. Sibling pick and status_for_dpt now prefer EXACT
identity equality before subset fallback. 13/13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6cabea08af
commit
4a9dd5ce4a
@@ -69,6 +69,9 @@ _FUNC_WORDS = {
|
||||
# colour function words: ignored so a colour GA pairs to its zone's light
|
||||
"colour", "color", "rgb", "rgbw", "rgbww", "hue", "saturation", "sat",
|
||||
"white", "warm", "cct", "kelvin", "temp", "temperature", "xyy", "farbe",
|
||||
# Russian function words — identity is the zone/device, not the verb
|
||||
"вкл", "выкл", "статус", "состояние", "яркость", "значение", "диммирование",
|
||||
"цвет", "позиция", "движение", "стоп", "вверх", "вниз",
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +159,12 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
|
||||
# so a command never borrows a sibling's status.
|
||||
if not _identity_match(cmd.name, s.name):
|
||||
continue
|
||||
score = len(cid & _pair_ident(s.name)) + (2 if s.main == cmd.main else 0)
|
||||
sid = _pair_ident(s.name)
|
||||
# EXACT identity outranks any subset overlap: "RGBW подсветка"
|
||||
# must take "RGBW подсветка статус" over "камин подсветка статус"
|
||||
# even though both share {zone, подсветка}.
|
||||
score = len(cid & sid) + (2 if s.main == cmd.main else 0) \
|
||||
+ (3 if sid == cid else 0)
|
||||
if score > best_score:
|
||||
best, best_score = s, score
|
||||
return best
|
||||
@@ -263,18 +271,23 @@ def generate_ha_yaml(project: LoadedProject) -> dict[str, Any]:
|
||||
if st5:
|
||||
entity["brightness_state_address"] = st5.address
|
||||
consumed.add(st5.address)
|
||||
for sib in same_main_gas(ga):
|
||||
if sib.address in consumed:
|
||||
continue
|
||||
if sib.category in ("lighting", "unknown") and sib.dpt_main == 1 \
|
||||
and sib.kind == "command" and _identity_match(ga.name, sib.name):
|
||||
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)
|
||||
consumed.add(sib.address)
|
||||
break
|
||||
# two-pass sibling pick: EXACT identity first, subset only as a
|
||||
# fallback — else "RGBW подсветка яркость" ({living, подсветка})
|
||||
# grabs "камин подсветка" ({living, камин, подсветка}) by subset
|
||||
# while its true on/off sits one address further.
|
||||
sibs = [s for s in same_main_gas(ga)
|
||||
if s.address not in consumed and s.dpt_main == 1
|
||||
and s.kind == "command" and s.category in ("lighting", "unknown")]
|
||||
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 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)
|
||||
consumed.add(sib.address)
|
||||
attach_colour(entity, ga) # RGB/RGBW/xyY/colour-temp of this zone
|
||||
lights.append(entity)
|
||||
consumed.add(ga.address)
|
||||
|
||||
@@ -186,13 +186,21 @@ def check_policy(project: LoadedProject, policy: dict[str, Any]) -> dict[str, An
|
||||
"populated — leave spare address space per this project's convention.",
|
||||
})
|
||||
elif not reserve_mains and source != "profile":
|
||||
# inferred taxonomy: no main group is reserve-like at all — the
|
||||
# project has no spare address space anywhere
|
||||
findings.append({
|
||||
"severity": "info", "code": "policy_no_reserve", "address": "-",
|
||||
"message": "No reserve main group exists in the project — every main is "
|
||||
"populated. Leave spare address space for future extensions.",
|
||||
})
|
||||
# inferred taxonomy can't see reserves (reserve GAs are non-functional
|
||||
# by intent) — before complaining, look for reserve-intent GAs and
|
||||
# reserve-named group ranges directly.
|
||||
has_reserve = any(g.intent == "reserve" for g in project.gas.values())
|
||||
if not has_reserve:
|
||||
rtok = ("резерв", "reserve", "spare")
|
||||
names = [g.main_name for g in project.gas.values()] + \
|
||||
[g.middle_name for g in project.gas.values()]
|
||||
has_reserve = any(t in (n or "").lower() for n in names for t in rtok)
|
||||
if not has_reserve:
|
||||
findings.append({
|
||||
"severity": "info", "code": "policy_no_reserve", "address": "-",
|
||||
"message": "No reserve main group exists in the project — every main is "
|
||||
"populated. Leave spare address space for future extensions.",
|
||||
})
|
||||
|
||||
errors = sum(1 for f in findings if f["severity"] == "error")
|
||||
warns = sum(1 for f in findings if f["severity"] == "warning")
|
||||
|
||||
Reference in New Issue
Block a user