mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
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>
645 lines
47 KiB
Markdown
645 lines
47 KiB
Markdown
# 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
|