From 2f2b8b1f913faed4a0bf56e56e7c0a6b6a016cfa Mon Sep 17 00:00:00 2001 From: Nikolay Miroshnichenko Date: Wed, 1 Jul 2026 14:54:44 +0200 Subject: [PATCH] fix(handover): read domain names from GroupRanges, not mislabeled GARecord.main_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main/middle range names now resolve correctly (e.g. [1] Освещение 1 этаж, [5] Климат) instead of a middle-group name leaking into the main. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 6 ++++++ nickol_knx_mcp/handover.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d3db0..44c6cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ Zennio project (a 3646-GA multi-vendor villa, 5× larger, no ETS Functions). `Все шторы - Стоп`) 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. +### Fixed +- **Handover domain names** — the group-address map now reads main/middle range names straight + from the project's `GroupRanges` instead of `GARecord.main_name`, which the parser could + mislabel (a middle range's name leaking into the main). Domains now render correctly + (e.g. `[1] Освещение 1 этаж`, `[5] Климат`) instead of a middle-group name. + ## [0.2.0] — 2026-06-30 **Colour/climate entity assembly and GA-intent noise reduction.** Two feature tracks diff --git a/nickol_knx_mcp/handover.py b/nickol_knx_mcp/handover.py index 4b4f69c..c30b19a 100644 --- a/nickol_knx_mcp/handover.py +++ b/nickol_knx_mcp/handover.py @@ -25,23 +25,41 @@ from .intent import INTENT_FUNCTIONAL # --------------------------------------------------------------------------- # # Structure helpers # --------------------------------------------------------------------------- # +def _range_names(project: LoadedProject) -> tuple[dict[int, str], dict[tuple[int, int], str]]: + """Authoritative main/middle names from the project's GroupRanges. + + ``GARecord.main_name`` from the parser can mislabel (it may carry a middle + range's name), so the domain map reads names straight from ``group_ranges``. + """ + main_names: dict[int, str] = {} + mid_names: dict[tuple[int, int], str] = {} + for mkey, mrange in (project.raw.get("group_ranges") or {}).items(): + head = str(mkey).split("/")[0] + if not head.isdigit(): + continue + mi = int(head) + main_names[mi] = mrange.get("name") or "" + for skey, srange in (mrange.get("group_ranges") or {}).items(): + parts = str(skey).split("/") + if len(parts) >= 2 and parts[1].isdigit(): + mid_names[(mi, int(parts[1]))] = srange.get("name") or "" + return main_names, mid_names + + def _domain_map(project: LoadedProject) -> dict[int, dict[str, Any]]: """main -> {name, count, middles: {middle -> {name, count}}}, sorted-ready.""" + main_names, mid_names = _range_names(project) mains: dict[int, dict[str, Any]] = {} for ga in project.gas.values(): if ga.main is None: continue - m = mains.setdefault(ga.main, {"name": ga.main_name or "", "count": 0, "middles": {}}) + m = mains.setdefault( + ga.main, {"name": main_names.get(ga.main, ""), "count": 0, "middles": {}}) m["count"] += 1 - if not m["name"] and ga.main_name: - m["name"] = ga.main_name + midkey = ga.middle if ga.middle is not None else -1 mid = m["middles"].setdefault( - ga.middle if ga.middle is not None else -1, - {"name": ga.middle_name or "", "count": 0}, - ) + midkey, {"name": mid_names.get((ga.main, midkey), ""), "count": 0}) mid["count"] += 1 - if not mid["name"] and ga.middle_name: - mid["name"] = ga.middle_name return mains