Files
Nikolay MiroshnichenkoandClaude Opus 5 a3f436fc74 generate_ha: one climate entity per physical device, fail closed when devices cannot be told apart
Found on a real 1312-GA house: a room with floor heating, a convector and an AC unit became
ONE climate (convector setpoint, AC controller mode, AC fan speed as the valve). Room 1.09
matched 2.09 and a central "09." GA, "Kids room 1" took "Kids room 2 temperature", and valves
came from light brightness and blind position statuses. The zone was the room alone: device
words were stripped as qualifiers, "А/С" vanishes in the tokenizer, the floor digit of "1.09"
is dropped.

Now a climate is assembled per room AND device type (floor, wall, radiator/convector, fan
coil, AC; RU/EN/DE words, removed from the zone as whole words):
- room code "N.NN" only at the start of a name, and it must match on both sides;
- standalone numbers of the anchor must be in a device member; a shared room sensor's
  numbers must be among the anchor's; digits in dotted codes and values do not count;
- an untyped GA is shared room data: any role if the room has one device, otherwise only
  the current temperature, which several devices may reuse; with a room code the room's
  untyped sensor qualifies even without the device's extra words;
- a word that sets another device of the room apart keeps its GAs away;
- control roles must be unambiguous: more than one candidate -> review climate_ambiguous;
- two mode GAs that cannot be told apart -> review climate_duplicate_anchor, not dropped;
- only 20.102 / 20.105 anchor; valve needs a valve word; AC never gets a valve;
- stable address order, so results never depend on parse order.

Two gates. Own audit on seven real projects, every changed entity inspected: no control GA
used by two climates, no address lost; house 16 -> 33 climates (one per device), manual
review 33 -> 12; flat 7 -> 9; HDL 3 -> 5; demo 6 -> 13; villa 23 -> 21 with the 3 it cannot
disambiguate sent to review instead of guessed. LLM council (three models + devil's
advocate, sanitised packet): accept with hardening; its P0s (same-type devices, silent
first pick) and P1s (mid-name codes, dotted digits, non-102/105 anchors, generic valve
words) are all in this commit, with tests E and F covering its failure catalogue.
Out of scope, recorded: identically named rooms of different flats without room codes, and
lexical identity in general -> device-channel identity ("lever 0").

Also works around a mypy 2.3.1 parse error on a comment right after a compound if.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:20:49 +02:00

645 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- **Climate entities no longer merge different devices of one room.** A room with floor heating, a
convector or radiator and an AC unit used to become a single Home Assistant climate: the convector's
setpoint, the AC's controller mode and the AC fan speed as the "valve". Valves were also taken from
unrelated 5.x statuses (light brightness, blind position), room "1.09" matched "2.09", and "Kids
room 1" took "Kids room 2 temperature". A climate is now assembled per room **and** device type
(floor / wall / radiator-convector / AC, detected in RU/EN/DE names): the room code ("1.09") and the
numbers in the anchor name must match, a shared untyped room sensor may serve every device in the
room as current temperature but never as a device's setpoint or mode, a valve needs a valve word and
an AC never gets one. Hardened after an LLM-council review: a word that sets another device of the
room apart ("shower", "hall") keeps its GAs away; if a control role still has more than one candidate
the device goes to review as `climate_ambiguous` instead of taking the first; two mode GAs that cannot
be told apart go to review as `climate_duplicate_anchor` instead of being dropped silently; room codes
only count at the start of a name ("21.5 °C", "16.10", "ДД 34.1" are not rooms); digits inside dotted
codes are not room numbers; only 20.102 and 20.105 start a climate; fan coils and VRV/VRF are their
own device types; valve words narrowed to valve / клапан / Stellwert. On seven real projects: no
control GA used by two climates, no address lost. A 1312-GA house goes from 16 climates (several
wrong) to 33 (one per device), manual climate review 33 -> 12; a 3646-GA villa keeps 21 of 23 and
sends the 3 it cannot disambiguate (three thermostats in one corridor, two identical AC mode GAs) to
review instead of guessing.
- **Home Assistant lights without `address` are no longer generated.** A 5.001 lighting GA with no
on/off GA in its zone used to become a light with only `brightness_address`, which Home Assistant
rejects (`address` is required on a KNX light). On six real projects 51 such lights were generated,
mostly motion-detector parameters on 5.001 ("sensitivity", "daytime command"). They now go to the
review list as `light_without_switch`; no address is dropped silently.
- **`subdpt_suspect` false positives.** A 1-bit, date/time or text GA named after a quantity
("on by motion detector by illuminance", "CO2 threshold", "meter value recorded, date") is a flag or
timestamp, not the value, and is no longer checked (12 of 12 such hits in the corpus were false).
Legitimate DPTs of the same quantity are accepted: power 9.024 and 14.080, energy 13.010 to 13.016
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
takes a plain on/off light with `address` + `state_address`, and light entities are what Assist and
"all lights" act on. Lighting without a status GA is reported as `light_without_status`. Regenerated
packages move these entities from `switch:` to `light:` (220 on a real house); review before replacing
a deployed package.
## [0.8.2] - 2026-09-14
### Added
- **`load_ga_export(path)`: audit a group-address list without a `.knxproj`.** Reads an ETS
ga-export/01 XML (ETS "Export Group Addresses", or the import file a planning tool such as TapPlan
writes) into a project without devices: names, addresses, DPTs (`DPST-x-y`, `DPT-x`), descriptions,
the `Security` flag and the range tree. GA-level checks and both generators work on it; device,
Function and topology tools have nothing to read and say so. Parsed through the same hardened XML
reader as `.knxproj` members, size-capped at 50 MB. Invalid or duplicate addresses and unknown DPT
tokens are skipped with an entry in `import_warnings`, never silently.
- **Climate: setpoint shift.** A DPT 9.002 or 6.010 group address whose name says shift
(shift / Sollwertverschiebung / смещение / сдвиг) in a climate zone now maps to
`setpoint_shift_address` / `setpoint_shift_state_address` with `setpoint_shift_mode` `DPT9002` or
`DPT6010` taken from the DPT (keys per the Home Assistant KNX climate docs). It used to fall through
to a plain sensor.
### Changed
- **Multi-address entities are named after what their addresses share.** A light built from
"Kitchen Spots On-Off", "Kitchen Spots B.Value" and their feedbacks is now "Kitchen Spots", not
"Kitchen Spots B.Value". Lights, covers and climates take the longest common word prefix of their
member names, but only when it still carries an identity token and does not drop a word containing a
digit; two entities that would collide keep their original names. Found by auditing a TapPlan export.
Only function words are ever cut (value, brightness, up/down, mode, Яркость, Абсолютное диммирование,
Движение, Вкл…); a room, device type, channel or anything else stays. On the maintainer's six real
projects 214 of 1189 entities got a shorter name and no address mapping changed. **A regenerated
package will therefore show different entity names than one generated before** — review the diff
before replacing a package that is already deployed.
### Fixed
- **`suggest_repairs` crashed on any project with a missing status GA** (`UnboundLocalError` in
`repair.py`). Introduced by a local-variable rename in the 12.09 typing cleanup; the test that covers
it was not part of CI, which is why it shipped. CI now runs every test file in `tests/` (11 were
missing), not a hand-picked subset.
- **`suggest.py`: multi-output actuators are split by their vendor object marker.** Zennio-style
devices (Lumento DX4, MAXinBOX, KLIC-DI…) put every output of a device into ONE ETS channel and
separate them only in the object text (`[1] Switch On/Off`, `[2] On/Off (Status)`). The channel
classifier used to see several switch commands in one channel, give up, and let every status object
fall through to a sensor suggestion. Channels are now split into vendor sub-units first. On a real
3646-GA project: entities 341 → 513, sensor noise 752 → 367, agreement with the name/Function engine
92 % → 95 % (platform) and 94 % → 95 % (address keys).
- **Device diagnostics are no longer suggested as sensors** (error flags, communication failures,
firmware/version, bus voltage, reset — matched in the vendor object text or the GA name, EN/DE/RU).
They are counted in `hints["diagnostics_skipped"]` and kept out of the name-based fallback as well.
53 such objects on the same project.
## [0.8.1] - 2026-09-12
### Added
- **Published to PyPI and prepared for the official MCP Registry.** `server.json` (schema 2025-12-11,
`registryType: pypi`, server name `io.github.NickoScope/nickol-knx-mcp`) plus the `mcp-name` ownership
marker in the README, which the registry reads from the published PyPI description. 0.8.0 carried the
marker in the wrong case, which the registry rejects, hence this release.
### Added
- **Official MCP Registry preparation**: `server.json` (schema 2025-12-11, `registryType: pypi`,
server name `io.github.nickoscope/nickol-knx-mcp`, stdio transport, the two optional env vars) and the
ownership marker `<!-- mcp-name: … -->` in the README, which the registry checks inside the published
PyPI description. Package builds clean (`uv build`, `twine check` PASSED). Publishing itself still needs
a PyPI token (owner action), then `mcp-publisher login github && mcp-publisher publish`.
- **`suggest.py` — structure-first entity suggestions (experimental, library-only, no MCP tool yet).**
Prototype of a second Home Assistant KNX `SuggestionProvider` (core PR #180891 contract): device
channel → communication-object flags (write = command sink, transmit = status source) → DPT pattern
→ HA UI entity config (`ga_switch`, `ga_brightness`, `ga_up_down`, `ga_position_state`,
`ga_temperature_current`, …). Names are used only as tie-breaks and always recorded in
`metadata.review`. Channels the FB provider covers are skipped; channel-less devices get pseudo-channels
from vendor object texts; shared status GAs and duplicate writers are handled. `tests/test_suggest.py`
reproduces the FB provider's own nameless fixture from structure alone; `tools/eval_suggest.py`
measures agreement with the name/Function engine on real projects.
### Fixed
- **`check_policy(write_example_to=…)` writes an example profile seeded from the loaded project's own
main groups** instead of a static template (issue #13, avataru). The template listed mains the project
did not have (sensor/energy/diagnostics/reserve), contradicting the real layout and sending the model off
to "correct" a fine taxonomy. Now: each existing main is written with its inferred domain, range name and
category mix; mains with no clear majority are written commented-out with their mix so the integrator
decides; the default taxonomy stays only as a labelled comment; `reserve.expect_range` follows the
project. `_infer_taxonomy` and the example now share one implementation (`taxonomy_seed`). Regression
cases in `tests/test_policy.py` (now in CI).
### Added
- **`check_topology()` — topology & individual-address sanity, grounded in the KNX standard** (tool
count **30 → 31**; `analyze.py`, `server.py`). Flags devices-per-line over the TP1 limits (info at
>64 per segment, warning at >256 per line — KNX Handbook p.36/40/55), individual addresses that
don't parse as a valid `A.L.D` (area 0-15, line 0-15, device 0-255 — confirmed against the xknx
`IndividualAddress` class), duplicate individual addresses (error), and multi-line TP projects with
no coupler (device `.0`) on a line (info). Empty topology / device-less synthetic projects return
no findings. Folded into `analyze_all` under a `topology` key and its severity totals.
`tests/test_topology.py`.
### Security
- **Hardened parsing of untrusted `.knxproj` / `.knxprod`** (`safexml.py`). A project file is a
user-supplied ZIP-of-XML, so both XML and ZIP attack classes apply; every place we open an archive
or parse XML ourselves now goes through one hardened module. **XML:** a dependency-free reject of any
`<!DOCTYPE`/`<!ENTITY` (the billion-laughs / XXE vector — legitimate ETS XML never carries one), plus
`defusedxml` parser-level blocking when installed (added as a dependency). **ZIP:** a pre-flight
against absolute caps (archive size, entry count, per-member and total decompressed size, compression
ratio) rejects zip-bombs, member names are checked for path-traversal / absolute paths, and each
member is read through a streaming cap so a header that under-declares its size still can't exhaust
memory. Wired into `load_project`, `check_device_parameters` and the app-program parser; on a
violation callers return a normal `{"error": ...}` rather than a traceback. Still strictly read-only.
Raised by external security review. `tests/test_safexml.py`.
### Changed
- **Contextual domain classification — a DPT alone no longer decides the domain** (`project.py`,
`explain.py`, `generate_ha.py`, `policy.py`). A 1-bit switch is domain-agnostic (a light just as
easily as a pump or an AC), yet the classifier defaulted DPT 1.001 → *lighting*, so "Living room AC
on/off" came out as lighting and then tripped downstream checks. The domain is now a **combination of
signals**: an explicit name domain wins; a strong, domain-encoding DPT (shutter 1.008, HVAC-mode
20.102, temperature 9.001, dimming 3.007) keeps its domain; for a domain-soft DPT (1.001/1.011/5.001)
the **main group name** disambiguates; with no signal at all a bare 1-bit GA is honestly **unknown**
rather than a guessed lighting. When an explicit name contradicts a *strong* DPT the result is
`unknown` (a genuine conflict, surfaced by `explain_ga` as `contested`) — not a silent pick. Matching
is word-boundary, not substring, so "ac" no longer fires inside "terrace". Name terms broadened
(English + Russian HVAC incl. `ac`/`a/c`/`air conditioner`, RGB/colour lighting). Verified on three
real projects: every "AC on/off" is now HVAC; policy taxonomy-outlier **noise fell sharply** (demo
11→2, one real project 31→6, another 200→57) because `check_policy` now flags only misplaced
**actuators** (lighting/shutter/HVAC) and treats sensors, scene links, thresholds and timeout
parameters as the cross-cutting GAs they are. `explain_ga` shows the winning signal and tier; the HA
generator still emits a switch for a now-HVAC simple on/off, so no device is dropped.
Dogfooding the classifier against a deliberately messy synthetic house then hardened it further:
the **main-range name map no longer lets a "middle 0" clobber its main group's name** (both start at
the same raw address, so the old modulo heuristic overwrote "Освещение" with "Вкл/Выкл" and the
range rescue silently died — depth in the range tree decides now); a category that comes from a
**whole-main-group DPT fallback is soft** (a guess by construction — only an exact `(main, sub)`
table entry is strong), so a vendor enum like 20.609 no longer poses as authoritative HVAC;
**DPT 1.010 start/stop is soft** (ventilation timers use it too, not just shutters); terms learned:
Cyrillic **а/с**, **сплит**, **вытяжк**, **яркост** (RU only — EN "brightness" also means weather
lux), meteo/weather (sensor now outranks hvac so "Метеостанция - Температура" is a sensor, not an
HVAC actuator). Round 2: presence **1.018 is a sensor** (was diagnostics — its name always says so
and the table fought it); **9.001 temperature is soft** (room temp defaults to hvac, a weather-named
temp re-domains to sensor without a fake conflict); **illuminance** terms ("освещённость" ≠
"освещение"); and a **passive-range rule** — a measurement inside a Sensors/Energy/Diagnostics main
takes that domain, while a command is never retyped by a passive range. Real-project noise after
both rounds: demo 2 outliers / 239 GAs (the two central macros), a 685-GA flat 10 (all defensible —
a genuinely mixed water/energy main), a 3646-GA house 75 (~2%).
### Added
- **Room Template Library — R1 (vertical slice)** (`room_library.py`, `room_templates/`, two new MCP
tools; tool count **28 → 30**). Compose a **new** KNX project from a list of parametrised room
templates ("constructor"). The template format is a deliberate public contract (`room_templates/SCHEMA.md`):
`schema_version`, a **locale-neutral semantic `slot_id`** (identity never comes from a human name),
`labels.{ru,en}` for presentation, **per-slot** `basic`/`comfort` presets (not one monolithic room
level — a house can mix comfort climate with basic lighting), and parameters where `area_m2` is an
explicit **hint** with provenance, never a normative fact. Six built-in rooms ship: bedroom, children,
living, kitchen, bathroom, corridor. Templates describe **functions** (function-first); logic
(presence→light) is declared as non-executable `automation_intents` metadata only.
A separate resolved IR (its own dataclasses — **not** the public `GARecord`) drives allocation
(main = domain, middle = role, sub sequential) with a hard error on sub-address exhaustion (never a
silent overflow). Critically, the composer writes a **real `.knxproj` (ZIP of ETS XML) and re-reads it
through the standard `load_project`** — the same path used for third-party projects — so generation
never touches the classifier and is validated by the real reader. New tools:
`validate_room_template(template?, path?)` (schema check) and
`compose_rooms(rooms, language="ru", project_name?, output_dir?, dry_run=true)` — outputs an allocation
`manifest`, ETS GA **XML/CSV** via the existing generators, and a device **BOM proposal** from the
device library. New projects only, dry-run by default; docking into an existing project and exact
device selection are R2. The generated house passes all four linters (naming / missing-status / DPT /
policy) with **0 errors and 0 warnings**. `tests/test_room_library.py` (golden RU/EN, idempotency,
permutation invariance, address-exhaustion error, round-trip 0-error lint, negative-oracle manifest).
- **Explainable aggregate scores** (`advanced.py`, `handover.py`). Every headline percentage now ships
the numbers behind it instead of a bare figure: Matter readiness, the completeness grade and
command/status coverage each carry a `math` block with the numerator, denominator, the exact formula
(that reproduces the percentage) and, for Matter, the functions **excluded** from the denominator
because their category has no Matter cluster (previously a silent skip — the biggest source of "why is
this number what it is?"). The completeness grade also states its band thresholds. A reviewer can now
audit or reproduce any score. Report-only, additive. Raised by external review. `tests/test_explainable_aggregates.py`.
- **Provenance / confidence** (`explain.py`, new MCP tool `explain_ga`). The enriched model mixes ETS
facts, DPT-derived structure and name heuristics; downstream tools then treat the result almost like a
fact. `explain_ga(address)` makes the reasoning auditable for one GA: per decision (category / kind /
status pairing) it reports the signals that fired with a confidence tier — **authoritative** (an ETS
Function role) > **structural** (the KNX DPT) > **heuristic** (a name keyword) — and flags **conflicts**
(e.g. a GA the DPT calls `lighting` while the name says "AC" → `contested`), the hotspot for silent
misclassification. Additive and read-only (no change to the core model). Asked for by three independent
reviewers (two external councils + a field integrator). `tests/test_explain.py`. Tool count 27 → 28.
- **Role-aware feedback completeness** (`detect_role_completeness` in `analyze.py`, surfaced through
`check_missing_status`). "Does the function have *a* status?" was not enough — a dimmer with an on/off
status but no brightness status silently passed and inflated coverage/Matter scores. The new check
flags a brightness/position **command** (DPT 5.001) whose device has no matching value **status**
(`missing_value_status`), using a device-identity match so it does not borrow a sibling's status.
Closes the demo's own documented limitation — recall on the demo house is now 5/5. Raised by an
external expert review. `tests/test_role_completeness.py`.
- **Project Policy Profile** (`policy.py`, new MCP tool `check_policy`). Validate a project
against *its own* agreed rules (main-group taxonomy, naming regex, command/status exemptions)
instead of one universal "professional standard" — a direct answer to integrator feedback that
conventions differ per school. With a YAML profile the declared taxonomy is authoritative; with
**no** profile the taxonomy is **inferred from the project itself** and GAs that deviate from
their main group's own majority domain are flagged (`policy_taxonomy_outlier`) — never against an
alien standard. Ships an example profile (`examples/policy-profile.example.yaml`) and
`tests/test_policy.py`. Tool count 26 → 27.
- **Cross-device parameter QA** (`param_check.py`, new MCP tool `check_device_parameters`).
Reads per-device `ParameterInstanceRef` values straight from the `.knxproj` project part
(data xknxproject does not expose), groups identical devices by application program, and
flags the odd one out: `clear_outliers` (a strong majority with a small minority — e.g. one
thermostat with a different setpoint/hysteresis, one presence detector with a different
detection time) and `split_configs` (balanced 2+ variants — review, often two zones).
Parameter names resolved from the app-program. Findings are ranked by **significance**: a
`focus` list of *config-value* outliers (setpoint/hysteresis/time/threshold — where an odd device
is usually a real mistake), separated from mode/type flags and per-room text labels — on a real
275-device project this turns 422 raw outliers into a 9-item focus (incl. the thermostats whose
init-setpoint differs from their siblings). Read-only; no ETS/bus; encrypted projects are skipped
honestly. Validated on real 42–275-device projects; synthetic `tests/test_param_check.py`.
Community-driven (asked for in Discussions). Tool count 25 → 26.
- **`skills/ha-git-backup`** — an ops-companion skill for the engineer package: a two-circuit
Home Assistant backup system (real git in `/config` with a deploy key and a pre-commit secret
scanner + age-encrypted full backups in GitHub Releases), with install/sync/scan/offsite/restore
scripts, HA automations, a restore runbook and a mandatory monthly drill. Field-tested;
the secret scanner ships canary-tested (BusyBox `grep -e` lesson recorded).
- **Manufacturer names from the archive itself** (`appprog_parser.py`): `parse_devices_from_project`
now reads vendor names from the `knx_master.xml` shipped inside every `.knxproj`/`.knxprod`
(e.g. M-0073 → HDL, M-00B6 → Ekinex S.p.A.) instead of relying on a hardcoded id map that can
never be complete; the static map remains as fallback.
### Fixed
- **Root-cause suppression (external-review noise).** A GA with an **empty name** has an unreliable
classification, so it no longer cascades into a `missing_status_address` warning and a
`policy_taxonomy_outlier` — the single root cause (`empty_name` from `check_naming`) is kept and the
dependent findings are suppressed. On the demo house the empty `2/5/2` now yields one finding instead
of three. `tests/test_root_cause.py`.
- **Noise & unsafe repairs surfaced by an external expert review on the demo house.** (1) Scene-control
GAs (DPT 17/18) are no longer flagged as `missing_status` — they recall a preset and have no single
state to read back (now `scene_no_status`, INFO), and `suggest_repairs` no longer synthesises a bogus
boolean status GA for them. (2) Generic group commands (`All blinds down`, `All shutters up`) are now
recognised as central macros (INFO) like `All lights off`, not missing-status warnings. (3) Synthesised
repair names match the GA's language — an English project no longer gets a Russian `(статус)`/`Значение
яркости` suffix. On the demo house this cut `missing_status_address` warnings from ~7 (mostly scenes) to
the one genuine case. `tests/test_council_fixes.py`.
- **`skills/ha-git-backup`**: `install.sh` now pins `core.sshCommand` + a repo-local `known_hosts`
so the nightly sync pushes from HA's Core container (was failing `Host key verification failed`).
## [0.8.0] — 2026-07-07
**Lessons from a second field project (a different integrator naming school):** positional
command/status pairing, ref-level application programs, and two detector blind spots. All four
changes were validated against a real 859-GA as-built project.
### Added
- **Positional command/status pairing** (#4, `pairing.py` / `analyze.py`). A widespread naming
school keeps feedback in a *parallel middle group* (command `1/0/s` ↔ status `1/1/s`) with the
GA name duplicated 1:1 and no status keyword at all — invisible to lexical pairing.
`detect_missing_status` now also pairs positionally (same main, same sub, different middle,
identical name, DPT-compatible) and recognises **self-reporting commands** (the actuator's
R+T communication object linked to the command GA itself). On the field project this cut
missing-status warnings from 339 to 89 — the ~90 that are actually real — and reports a
`status_pairing_summary` info with both counters.
- **Application-program parser v2** (#6, `appprog_parser.py`). Vendors like HDL, Ekinex and
Creatrol publish object names/DPTs/flags on `<ComObjectRef>` while the base `<ComObject>` is a
near-empty stub. The parser now merges ref-level data over the base (conservatively: refs only
fill empty fields; a DPT is taken only when every declaring ref agrees, disagreeing variants are
recorded as `dpt_variants`, never guessed). Unverified DPTs on the field project's 15 device
models dropped 4341 → 1167 (−73%). The app-program version is now picked by the **highest
ApplicationVersion** instead of list position — a stale v16 was being parsed where v18 existed.
### Fixed
- **`main_group_unnamed` read the wrong names** (#3, `analyze.py`): main-group names are now taken
from the authoritative `group_ranges` (a GA record's `main_name` can carry a middle range's
name), eliminating both false "unnamed" findings and masked truly-unnamed mains.
- **Completeness grader missed pattern-named ranges** (#5, `advanced.py`): a middle range literally
called «Сцены» full of plain-named scene GAs now counts as scene evidence — the grader reads
main/middle range names in addition to GA names (and the scene token matches Russian plural
forms).
- **Pinned `mcp>=1.10,<2`** — the MCP Python SDK v2 (in alpha) renames
`mcp.server.fastmcp.FastMCP` to `mcp.server.MCPServer`; without the upper bound a future
`pip install` would pull v2 and break the server import. Verified against the SDK docs.
## [0.7.0] — 2026-07-02
### Added
- **Exact device decomposition from a local catalog** (`device_library.py`). When the
`NICKOL_KNX_CATALOG` env var points at a device-library YAML file or directory (schema:
`library-schema.md`), `decompose_device` now returns the **exact vendor object model**
(`source: catalog-exact`) — real per-channel blocks, object counts, app-program version and
first-instance objects with their true DPTs — instead of the generic recipe. Falls back to the
built-in recipes (`source: recipe-approximate`) for any device not in the catalog, so behaviour
is unchanged when the env is unset. The catalog itself is vendor-catalog data kept **local** and
is not shipped with the package. Objects the vendor app-program leaves without a DatapointType
stay `dpt: null` — never guessed.
- **`parse_devices_from_project` (new MCP tool + `appprog_parser.py`)** — deterministic parser that
extracts the exact vendor comm-object model from the `M-*` application programs embedded in a
`.knxproj` / `.knxprod`: per device it reports order number, app-program version, object counts and
detected per-channel blocks (unit · objects-per-instance · stride), converting `DPST-x-y` → `x.00y`.
Read-only and PII-safe (reads only vendor catalog data, never the client `P-*/0.xml`). With
`output_path` it writes a device-library YAML into the workspace — feeding the local catalog above,
so `parse → catalog → decompose_device (catalog-exact)` is a closed loop. Now **25 MCP tools**.
## [0.6.0] — 2026-07-01
**Completes the roadmap** — the tool now validates, repairs, generates (HA / ETS / handover / IoT),
diffs, grades and drafts acceptance protocols, all design-time & read-only.
### Added — B-tier
- **B2 — climate-correctness review** (`generate_ha.py`). Climate generation now emits an explicit
review note: controller/operation modes made explicit, setpoint-shift command **and** state paired,
and a flag raised for any mode object that lacks a corresponding state address.
- **B3 — semantic project diff** (`diffproj.py`, new module; new MCP tools `diff_projects` /
`diff_loaded`). Compares two `.knxproj` versions and reports added / removed / DPT-changed /
renamed / security-changed group addresses — a reviewable delta between as-designed revisions.
- **B4 — acceptance test protocol** (`advanced.py`; new MCP tool `generate_test_protocol`).
Produces a per-function acceptance checklist in Markdown for commissioning sign-off.
- **B5 — Matter readiness** (`advanced.py`; new MCP tool `check_matter`). Reports which functions
round-trip cleanly to a Matter cluster and which do not.
- **B6 — energy scaffold** (`advanced.py`; new MCP tool `check_energy`). Checks metering / energy
DPT coverage and scaffolds PV / battery / EVSE structure.
### Added — C-tier
- **C1 — KNX IoT semantic export** (`iot.py`, new module; new MCP tool `generate_knx_iot`).
Emits a KNX IoT Turtle / RDF skeleton for the project.
- **C2 — naming suggestions** (`advanced.py`; new MCP tool `suggest_names`). Naming-hygiene
proposals for group addresses that drift from the zone + function convention.
- **C3 — as-built completeness grader** (`advanced.py`; new MCP tool `grade_completeness`).
Grades a project from bare functional skeleton to as-built, by presence of professional patterns
(central macros, motion tuning, astro/meteo, monitoring, deep metering, scenes, reserves, debug).
### Added — A-tier quick wins
- **A5 — Areas / voice note** (`generate_ha.py`). Documents that HA Areas and voice assignment are
UI-only concerns, not derivable from the `.knxproj`.
- **A6 — time/date expose** (`generate_ha.py`). Emits a KNX `expose` block for DPT-19.001 clock
broadcast so HA can serve time/date to the bus.
## [0.5.0] — 2026-07-01
**From validator to repairer.** Previous releases *flagged* problems in a `.knxproj`; this one
starts *proposing the fix*. The headline is a repair-suggestion engine that turns each finding
into a concrete, reviewable change, alongside two new detectors for gaps that silently break the
Home Assistant layer: relative-only dimmers and actuator-dependent cover behaviour.
### Added
- **B1 — repair-suggestion engine** (`repair.py`, new module; new MCP tool `suggest_repairs`).
For each finding it proposes a concrete fix rather than only naming the problem: infer a DPT for
a group address that has none, correct a suspect sub-DPT, synthesise a status/feedback GA in a
free address slot, or add an absolute-brightness GA for a relative-only dimmer. Suggestions only
— a human reviews them, and accepted new GAs feed `generate_ets_group_addresses`; the server
never writes to ETS or the bus. On a real 3646-GA project it produced **145 proposals**: 32
set-DPT, 1 change-DPT, and 112 synthesised status GAs.
- **A2 — relative-only-dimming detector** (`analyze.py`, `relative_only_dimming` finding). A
`3.007` relative dimmer with no `5.001` absolute-brightness GA in its zone is flagged, because
Home Assistant cannot set a brightness level from relative dimming alone.
- **A3 — cover invert / travel-time surfacing** (`generate_ha.py`). The cover review reason is now
`verify_cover_invert` and carries a note listing the actuator-dependent flags that are *not* in
the `.knxproj` (`invert_position` / `invert_updown` / `invert_angle`, `travelling_time_up` /
`travelling_time_down`), plus a warning when a cover's position lacks a state address.
## [0.4.0] — 2026-07-01
**Two new lint dimensions: does the DPT sub-type match what the name promises, and is the
project's KNX Secure posture consistent.** This release adds a conservative sub-DPT sanity
linter and a report-only KNX Data Secure posture summary (no key material ever touched),
plus the itemised QA findings in the handover pack.
### Added
- **A1 — sub-DPT sanity linter** (`analyze.py`, surfaced by `check_dpt` and `analyze_all` as the
`subdpt_suspect` finding). When a group-address name implies a specific DPT sub-type
(temperature → `9.001`, power → `14.056`, brightness/position → `5.001`, and similar), the
linter flags a wrong sub-type or a wrong main type. Multilingual keyword matching, deliberately
conservative — it only fires when the name is unambiguous, so it does not second-guess correct
or generic DPTs.
- **A4 — KNX Data Secure posture** (`analyze.py` `secure_posture()`, new MCP tool `check_secure`).
A report-only summary of the project's security posture: secured vs plaintext GA counts, middle
groups that mix secure and plaintext objects, and a keyring (`.knxkeys`) handover checklist. It
reads only the per-GA `Security` flag — no key material is read, derived, or emitted.
- **KNX Secure posture section in the handover pack** — section 5 of `handover.md` is rewritten
from a flat secure-GA count into the full posture section (counts, mixed-group flag, keyring
handover checklist), so the as-built deliverable states the security posture explicitly.
- **Itemised QA findings in the handover pack** — section 6 of `handover.md` now lists the
actual 🔴 errors and 🟡 warnings (address + name, grouped by check), not just totals, so the
handover doubles as a review checklist. Info-level items (intentional reserves / logic / macros)
stay collapsed.
## [0.3.0] — 2026-07-01
**Spec→structure: a device library and a design methodology, plus the as-built handover pack.**
This release turns the tool from a `.knxproj` *validator* into a *design* aid: from a project
spec you can now expand each device into its group-address recipe and reason about the whole
structure. Shipped alongside the Track B project handover pack and a set of noise-reduction
refinements, the latter two driven by a second real signed Zennio project (a 3646-GA
multi-vendor villa, 5× larger, no ETS Functions).
### Added
- **Device library + `decompose_device` — one device is not one GA** (`device_library.py`, new
MCP tools `decompose_device` + `list_device_recipes`). Each actuator channel expands into a
set of communication objects — command, status, dimming (3.007), absolute value/position
(5.001), HVAC mode (20.102/20.105), colour (232.600/251.600) — each with its own DPT. Given a
manufacturer order number, device type or alias (e.g. `ZDIDBDX4`, `dimmer`, `JRA/S`,
`presence detector`) and a channel count, the tool returns the objects a professional
typically wires per channel and the total GA count, so a spec/ТЗ device list can be turned
into a group-address structure. Recipes cover switch, dimmer, RGBW LED, shutter/blind,
floor-heating, AC gateway, DALI, presence, metering, leak and touch-panel families across
Zennio + ABB. Recipes are **generic vendor facts** compiled from KNX manufacturer ETS product
databases and public documentation — the *typical-wired* subset, not the full selectable
master menu. New regression tests cover the dimmer/shutter/panel/unknown paths.
- **`docs/spec-to-structure.md` — the spec→structure methodology.** Documents the reverse of
validation (design a group-address structure from a spec), the `decompose_device` pipeline,
and an honest, measured account of what is reproducible from a spec (~90 %: taxonomy, domains,
logic structure, command/status pairing, DPT discipline) versus what is not (the exact
per-device object count — an integrator parameterisation choice that varies 2–9× between
projects; predict a range, never a false-precise number).
- **`generate_handover_pack` — the as-built commissioning deliverable** (Track B). From a
read-only `.knxproj` it assembles `handover.md` (equipment inventory by manufacturer,
group-address map by domain, command/status feedback coverage %, KNX Secure scope, QA state),
a standalone **`topology.svg`** area/line/device diagram, the full `group-addresses.csv` and
the `ha-package.yaml`. On the villa: 275 devices across 8 manufacturers, 14 domains, 93 %
feedback coverage — one command produces the whole handover bundle.
- **Divider/separator scratch detection** — commissioning placeholder names made only of
punctuation (`---`, `=====`) or a marker wrapped in it (`-----addition------`) now classify
as `scratch` intent, so a missing DPT on them is an INFO note, not a 🔴 error.
- **Logic-function object awareness** — a typed GA (e.g. 9.x temperature) wired into a Zennio
`[LF] … Data Entry` container (intentionally type-agnostic, raw 2-byte) surfaces as INFO
`dpt_on_logic_object` instead of a false `dpt_mismatch_co` warning.
- **Central-macro status tolerance** — all-groups broadcast commands (`Общее …`, `Все группы`,
`Все шторы - Стоп`) 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
shipped together, both driven by real ETS projects: full colour-light (RGBW/RGB/CCT) and
KNX `climate` entity generation with a status-pairing (B1) correctness fix, and a GA-intent
classifier that stops intentional non-functional addresses from raising false errors. On a
real 685-GA Zennio project the intent work cut false errors **29 → 6** and missing-status
noise **79 → 45** while preserving all 12 genuine `dpt_mismatch_co` catches.
### Added
- **Colour lights, climate entities and a status-pairing fix** (Track A), driven by the real
signed demo and a 685-GA Zennio project.
- **Colour control** is assembled into the `light` entity: RGB (DPT 232.600 →
`color_address`), RGBW (251.600 → `rgbw_address`), xyY (242.600 → `xyy_address`) and
absolute colour-temperature (7.600 → `color_temperature_address` + `color_temperature_mode`),
each with its status, matched to the zone's light by identity. A colour GA with no on/off
in its zone is routed to review instead of dropped.
- **Climate** entities are now generated: anchored on an HVAC mode (DPT 20.102/20.105), the
zone's current temperature (9.001), target-temperature status, operation/controller modes
and valve `command_value_state` are gathered by location and emitted **only** when the HA-
required minimum (`temperature_address` + `target_temperature_state_address`) is present —
otherwise the zone goes to review, so an invalid climate entity is never written. Keys
verified against the Home Assistant KNX docs. (Real files: demo 6, Zennio 7 climate zones.)
- **B1 fix — no borrowed status.** A command now only takes a status whose identity nests
with its own (a shared zone token like "kitchen" is not enough), and a status maps to
exactly one entity. This stopped "worktop LED" inheriting "island pendants" brightness and
removed all shared-status addresses on the real files.
- New DPTs: 232.600 / 251.600 / 242.600 / 7.600 (colour), 20.105 (controller mode), 1.100
(heat/cool). New regression tests cover colour assembly, the B1 borrow guard and climate
(valid zone emitted, mode-only zone reviewed).
- **GA-intent classification — noise reduction on real projects** (`intent.py`). Every group
address is now classified as `functional` / `reserve` / `logic` / `scratch`. Intentional
non-functional GAs no longer "cry wolf": reserve spares with no DPT become an INFO note
(`reserve_without_dpt`) instead of a 🔴 error; reserve names repeated across different DPTs
are not flagged `duplicate_name` / `inconsistent_dpt`; internal logic / virtual signals and
scratch leftovers are excluded from missing-status warnings. The `dpt_mismatch_co` check and
every real functional finding are untouched. Driven by a real 685-GA Zennio project where
this cut false errors **29 → 6** and missing-status noise **79 → 45** while preserving all
12 real `dpt_mismatch_co` catches. `list_group_addresses`, the report inventory and the
`analyze_all` summary now expose the intent breakdown. New regression test covers the
reserve / logic / scratch patterns (and proves real DPT + status problems still surface).
- `docs/` — a self-contained GitHub Pages landing site (project overview, two-layer
architecture, demo-house stats, an interactive 5-tab dashboard preview, the HA "brain",
and a call for testers).
- `examples/demo-home/` — a complete synthetic worked example: a 239-GA / 47-Function demo
`.knxproj`, the tool's generated report + Home Assistant entities + ETS export, and a
`ha-brain/` Home Assistant smart layer (circadian lighting, multi-factor climate, presence/
season/time logic, statistics). Includes 5 deliberate mistakes to show the checks (and an
honest note on the one the missing-status check doesn't yet catch).
## [0.1.2] — 2026-06-28
**Home Assistant mapping quality** — a second hardening pass driven by running the tool
against more public ETS **4.2 / 5.0 / 5.5 / 6** projects (`yene/knxproj` DemoCase,
`tuxedo0801/KnxProjParser`, `dataheld/knxray`, `whaeuser/open-knxviewer`). Three new
regression tests; zero crashes across the whole corpus.
### Added
- **Dimmable lights are assembled completely.** A light now collects BOTH its on/off
status (1.x) and brightness status (5.x) via identity-based pairing, folds the on/off
command into the same `light` entity (no more duplicate `switch`), and pairs correctly
even when the device identity is a single name token (e.g. `HaloSpotLeft.A.VALUE` ↔
`HaloSpotLeft.A.STATE%`). Switches gained the same identity-based status pairing.
- **Wider shutter detection.** A cover's up/down is recognised on canonical DPT 1.008 *or*
any 1.x command named up/down (`UP/DOWN`, `auf/ab`); stop is recognised on DPT 1.007 /
1.010 / 1.017 *or* a "stop"/"stopp"/"стоп" name. Position and stop siblings attach only to
the same shutter (zone-identity guard), so multiple blinds in one main group no longer
cross-wire. Real ETS4/5 projects that use 1.001+1.017 now map to full covers.
- **Date / time / text DPTs recognised** (10.001 time, 11.001 date, 19.001 datetime,
16.000/16.001 string): routed to `review` as `manual_datetime` / `manual_text` (HA KNX has
dedicated date/time/text platforms) instead of an opaque `unmapped_dpt`.
## [0.1.1] — 2026-06-28
**Hardening release.** Every change below was surfaced by running the tool end-to-end
against **real ETS5/ETS6 project files** (the `XKNX/xknxproject` test fixtures), not the
synthetic smoke test — which alone never exercised the real parser or the MCP server path.
Five new regression tests now guard these cases.
### Fixed
- **Critical:** the `load_project` MCP tool recursed infinitely (`RecursionError`) on
**every** real `.knxproj`. The server tool function `load_project` shadowed the imported
project loader of the same name, so it called itself instead of parsing. It now delegates
to `load_project_file`. This made the tool's primary entry point unusable over MCP; the
synthetic smoke test missed it because it calls the parser directly, bypassing the server.
### Added
- **ETS Function role pairing — the headline feature — is now actually implemented.**
command↔status pairs are taken from ETS Function roles (e.g. `SwitchOnOff` ↔ `InfoOnOff`)
via `pairing.function_status_pairs()`, consumed by both `check_missing_status` and the HA
generator. Previously Functions were ignored entirely despite the README claiming they
were the primary signal. Consequences fixed: a feedback GA named only "Status" now pairs
correctly (name tokens no longer required), HA entities get the right `state_address`, and
function-paired commands are no longer false-flagged as missing a status.
- **No silent drops in HA generation ("no silent caps").** Every group address is now either
emitted as an entity or listed in the `review` output; the YAML header reports how many
need manual review. Previously, GAs the generator could not classify simply vanished.
- **Smarter shutter classification.** German/directional names are recognised (`Behang`,
`Lamelle`, `auf/ab`, `Raffstore`, `Markise`, plus RU equivalents). A shutter-looking
command whose DPT lacks a sub-type now gets an actionable `shutter_incomplete_dpt` review
hint (set 1.008 / 1.010 / 5.001 in ETS) instead of being dropped as "unknown".
- **Venetian slats handled as tilt.** A slat GA (Lamelle / slat / ламель / tilt) is attached
to its parent blind cover as `move_short_address` instead of becoming a standalone cover;
an unmatched slat is flagged `shutter_slat_unattached` for manual attachment.
- **Diagnostics alarms are inputs.** A 1-bit diagnostics GA (wind/frost/rain/smoke/leak
alarm, fault) is now a read-only `binary_sensor` instead of a phantom command switch.
## [0.1.0] — 2026-06-28
Initial public beta.
### Added
- Design-time MCP server with **12 tools**: `load_project`, `list_group_addresses`,
`get_devices`, `get_topology`, `check_naming`, `check_missing_status`, `check_dpt`,
`analyze_all`, `generate_ha_package`, `generate_ets_group_addresses`, `project_report`,
`workspace_info`.
- Read-only `.knxproj` parsing via `xknxproject` (ETS5/ETS6, password-protected supported).
- GA classification (category + kind) from DPT and multilingual (EN/DE/RU) name keywords.
- Naming, missing-status, and DPT validation checks.
- Home Assistant KNX YAML generation with a conservative `review` list for ambiguous items.
- ETS-importable group-address export in XML (`knx.org/xml/ga-export/01`) and CSV.
- Markdown project report.
- Confined-workspace write guarantee (`NICKOL_KNX_WORKSPACE`); no bus access by design.
- `CLAUDE.md` design playbook, end-to-end smoke test (synthetic 16-GA project), example
Claude Desktop config.
### Known limitations
- Tested end-to-end on a synthetic project only; real-world `.knxproj` testing is ongoing
(see the call for testers in the README).
[Unreleased]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.8.2...HEAD
[0.8.2]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.1.2...v0.2.0
[0.1.2]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/NickoScope/nickol-knx-mcp/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/NickoScope/nickol-knx-mcp/releases/tag/v0.1.0