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>
Found on a real 1312-GA house (43 of 159 "switch without status" had a Read+Transmit
object on the command GA), measured on all seven corpus projects.
detect_missing_status already counted these as satisfied through pairing.self_reporting();
the generator did not and put them on the review list without a state_address. It now
uses the same function: no separate status found and the command GA has an R+T object
-> state_address = the command GA. Same for the on/off sibling of a dimmable light and
for brightness_state_address on a self-reporting brightness GA. Write-only objects still
go to review.
Corpus: no entity count changed, no address lost; review items 598 -> 555 on the house,
-36, -40, -12, -1, -32 on the others. Test C in test_real_house_fixes.py covers R+T,
write-only and a non-lighting switch.
Not verified on a live Home Assistant: state_address equal to address is accepted by the
KNX schema and xknx reads from it, but no running instance was used here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on a real 1312-GA house, measured on all seven corpus projects.
B. Lighting on/off (1.001, lighting category) is emitted as a Home Assistant light with
address + state_address, not a switch. The HA KNX light platform documents exactly
that ("Simple light"). suggest.py already proposed light for these; the two engines
disagreed. Without status it is reported as light_without_status.
D. A 5.001 lighting GA with no on/off GA in its zone produced a light with only
brightness_address. Home Assistant requires address on a KNX light, so that YAML was
invalid. 51 of them across six projects, mostly motion-detector parameters on 5.001.
They now go to review as light_without_switch, and the brightness status is no longer
consumed for an entity that does not exist, so it surfaces as not_mapped.
Corpus, before -> after: lights without address 51 -> 0 everywhere; no address lost from
YAML or review on any project; switch -> light moves of 220 (house), 205, 62, 43, 17, 10,
13. test_pipeline B1 fixture gains its on/off GAs (it encoded an invalid light) plus a
test for the review route. CHANGELOG also records fix A from 481bab0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on a real 1312-GA house, confirmed across all seven corpus projects.
- A quantity word in the name of a 1-bit, date/time or text GA names what the flag,
threshold trigger or timestamp is about ("on by motion detector by illuminance",
"CO2 threshold 1", "meter value recorded, date"), not the value. 12 of 12 hits on
1-bit GAs in the corpus were false positives; mains 1, 10, 11, 16, 19 are skipped.
- Legitimate alternatives are accepted, checked against xknx 3.20: power 9.024 kW and
14.080 VA; energy 13.010 to 13.016 and 14.031 J, plus power DPTs under Russian
"электроэнергия" names that are routinely used for power readings.
- Power factor gets its own rule (14.057) ahead of power, so "Фактор мощности" is no
longer expected to be 14.056.
Real findings still fire: a bare DPT 9 named "lux threshold", a 5.x named "temperature
control", a 5.001 named "power", 9.002 named "temperature". New test file
test_real_house_fixes.py, wired into CI.
Corpus: subdpt_suspect 17 -> 2 on the house, 7 -> 2 and 1 -> 0 on two others; no other
metric moved. Baseline re-recorded in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
load_ga_export (32nd tool), multi-address entity naming, climate setpoint shift,
suggest_repairs crash fix, CI runs the whole test suite. Details in CHANGELOG.
Pre-release checks: corpus guard no drift on six real projects; twine check PASSED
for wheel and sdist; mcp-name marker present in METADATA; wheel carries ga_export
and no tools/tests/corpus files; sdist has no corpus map or project files; a clean
isolated venv from the wheel imports 0.8.2 and exposes load_ga_export. CHANGELOG
compare links added for 0.8.1 and 0.8.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things found by auditing a TapPlan export, plus one regression found on the way.
load_ga_export(path): new tool. Reads an ETS ga-export/01 XML (ETS "Export Group
Addresses", or the import file a planning tool writes) into a project without
devices, through safe_fromstring, capped at 50 MB and 8 levels of range nesting.
Invalid or duplicate addresses and unknown DPT tokens go to import_warnings, never
silently. Round trip with our own generate_ets_xml is covered by a test.
Entity naming: lights, covers and climates are named after the common word prefix
of their member names, cutting only function words. The first version of the rule
turned "01. <room> - All Blinds - Move" into "01" on a real project, so anything not
in the function vocabulary now stays. On six real projects 214 of 1189 entities got
a shorter name, each rename reviewed, no address mapping changed. Regenerated
packages show different names; noted in the changelog.
Setpoint shift: 9.002 / 6.010 with a shift word maps to setpoint_shift_address,
setpoint_shift_state_address and setpoint_shift_mode (keys checked against the HA
KNX climate docs). It used to become a plain sensor.
Fixed: suggest_repairs raised UnboundLocalError on any project with a missing status
GA. My local rename in the 12.09 typing cleanup left two references to the old name.
test_council_fixes covers it but was not in CI, so it shipped. CI now runs all 21
test files instead of a hand-picked 10; the 11 added ones pass from a clean clone.
Verified: all 21 tests, ruff, mypy with the package installed, corpus guard no drift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lint job failed on its first run with three errors my local run had not
shown. Cause: locally mypy had no xknxproject installed, so ignore_missing_imports
hid every mismatch against that library's own TypedDicts. CI installs the package
and therefore sees them.
The two range-walk helpers took dict[str, Any] but are handed a GroupRange
TypedDict, which mypy rejects because dict is invariant; they take Mapping now,
which is what they actually use. suggest passes the raw parsed dict into
build_loaded_from_raw, whose parameter is KNXProject, so the boundary is an
explicit cast rather than a widened public signature.
CONTRIBUTING now gives the command that matches CI, with the reason, so the next
person does not get a clean local run and a red CI.
Suite passes, corpus guard reports no drift.
The CI ran tests only. Added a lint job with two narrow gates:
ruff with select = E9,F — syntax errors and pyflakes (undefined names, unused
imports, broken f-strings). Style rules are deliberately off: the full default
set flagged 202 things, almost all cosmetic, and a gate nobody reads is not a
gate. Fixed the 9 findings it did have (8 unused imports, 1 empty f-string).
mypy, clean at 13 errors fixed. None of the 13 was a live bug, so every fix is
an annotation or a local rename: the two accumulators in advanced, the block map
in appprog_parser, a reused loop name in project and in repair, a TypedDict lost
through dict(), a mixed-type summary dict in handover. Two are worth naming.
_CH_TOKEN_RE now captures the letter prefix directly instead of re-matching its
own capture, which removes an unguarded .group() on a possibly-None match; the
regex accepts exactly the same strings. repair's status-DPT lookup now spells out
the None case, which is what dict.get already did since None is not a key there.
Verified the fixes moved nothing: the full suite passes, and the corpus guard
reports no drift across all six real projects, the 3646-GA one included.
Matrix extended to 3.13 and 3.14; both were smoke-tested locally first.
Dependabot config for pip and github-actions, weekly and grouped so a quiet week
is one PR. Dependabot security updates and CodeQL default setup enabled in the
repository settings.
- sort by the address itself (GA main/middle/sub numerically, device area/line/device), so the
result no longer depends on parser dict order and a retry returns the same page
- return {items, total_matched, returned, next_cursor} instead of a bare list truncated at limit:
truncation was silent and a caller could not distinguish a full answer from a cut one
- tests/test_paging.py (in CI): numeric vs lexical order, full walk at page sizes 1..100 covers every
row exactly once, same page after a reshuffled re-parse, filters reflected in total_matched
- verified on a real 3646-GA project; README EN/RU + CHANGELOG updated
- prompted by feedback on the r/mcp post (2026-09-12)
- _subunit()/_split_subunits(): '[1] Switch On/Off' style markers separate the outputs a vendor
packs into one ETS channel; each sub-unit is classified on its own (ids and secondary_info carry it)
- diagnostics (error/communication/firmware/version/bus voltage/reset, EN+DE+RU) never become sensors
and are excluded from the name-based fallback; counted in hints.diagnostics_skipped
- real-project effect (3646 GAs): entities 341->513, sensors 752->367, platform agreement 92->95 %,
address-key agreement 94->95 %
- tests: multi-output split (two lights, statuses consumed, no sensor fallout) + diagnostics filter
- server.json name fixed to io.github.NickoScope/nickol-knx-mcp (registry permissions are case-sensitive)
- README mcp-name marker in the same case; 0.8.0 had it lowercase, which the registry rejects
- version bumped in pyproject + __init__; CHANGELOG 0.8.1
- registry: io.github.NickoScope/nickol-knx-mcp v0.8.1 active, package pypi/nickol-knx-mcp 0.8.1
- example_policy_yaml(project): mains written with inferred domain, range name and
category mix; no-majority mains commented-out with their mix; defaults only as a
labelled comment; reserve.expect_range follows the project; empty/2-level -> {}.
- taxonomy_seed() is the single source for _infer_taxonomy and the example.
- server.check_policy(write_example_to) passes the loaded project, reports seeded_from.
- tests/test_policy.py: cases 4-6 (no leaked mains, round-trip, mixed main, quotes, no project); added to CI.
- README/README.ru/CHANGELOG.
Cloners install and run the tool but rarely reread the README, so the existing
'call for testers' never reaches them. Two low-noise touchpoints instead:
- project_report: a one-line footer linking the real-project test-report issue
template (the report is the artefact a human reads after a real run);
- load_project: a short 'feedback' field on the result — the first call every
new user makes.
Anonymised addresses explicitly welcome. No logic change; full suite green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kris1166's real-file re-test of f20b2e4: main bug gone (light:0, all 29 RM → status) but
15/29 position feedbacks stayed lighting/not_mapped. Root cause (reproduced 1:1): those
actuator-level Functions carry blank or raw-GUID roles — no role token anywhere — so the
shutter-Function gate skipped them even though they hold a 1.008 up/down move. The
anomaly (2/3/1 promoting while its step/stop stayed unknown) is explained: its move has a
role; its step/stop is DPT 1.009 'Enable' (the reporter's own modeling slip, not a step).
- A shutter Function is now recognised by a shutter ROLE token OR by an UNCONTESTED 1.008
move member (category==shutter; a 1.008 whose name pins another domain must not license
its neutral siblings — gate-1 audit).
- is_step includes DPT 1.017 (trigger), which the cover builder already treats as a stop.
Gate-1: APPROVED; 0 category/kind/platform drift across 5 real .knxproj (1965 GA); the new
fixtures fail on the old code. Gate-2: the LLM council already endorsed the sibling-DPT
(1.008 move) signal as the primary structural evidence — this implements it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field data by Kris1166 (issue #12). A DPT-5.001 shutter position feedback named only '… RM'
was classified lighting/light and emitted as a bogus HA light entity. Two layers:
- classification (project.py): an ETS Function carrying a shutter role promotes a member to
shutter — but only on POSITIVE evidence (member is a step/stop 1.007/1.010 OR a 5.001 position
STATUS, the Function has an up/down move sibling, and the name has no explicit non-shutter
domain), so a scene-recall/dimmer-feedback/lock sharing a mixed Function is left alone
(LLM-council review; a Function is a free grouping, not proof of domain).
- status recognition (project.py name_is_status): a standalone 'RM'/'FB' token is now a status —
the 'rm '/'rm_' substrings and the stopword tokenizer both missed a name ending in bare 'RM'.
- analyze._is_status_ga delegates to name_is_status; generate_ha then wires it as the cover's
position_state_address.
Passed both gates (self-audit + LLM-council). tests/test_rm_status.py pins it (red/green + negatives:
a light's RM stays light, a stray light / a scene-recall in a shutter Function are not promoted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Post-fix hardening from an LLM-council review of 0198ae3, with a gate-1 audit pass:
- _is_shutter_control rejects a bare 1.007/1.010 whose tokens are a foreign domain
(light/dim/socket/HVAC) or a central macro (Alle/Zentral) — a 'stop' is an operation
signal, not a domain one, so a lighting 'Licht Stopp' or a house-wide 'Alle Stopp' can
no longer be mis-paired/stolen as a cover's step/stop.
- _rank gains a stable address tiebreak -> deterministic pick across parses on a full tie.
- gate-1 caught 'master' wrongly excluding Master-zone covers; removed. Vent words kept
admissible (roof-window/flap openers are covers).
- tests groups E/F/G pin the foreign-stop, central-steal, and Master-zone cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second pass on issue #11, grounded in Kris1166's real ETS-6 field dump (groups A/B/C),
after the first fix (dd8ecac) proved partial. Three levers:
- project.py: ETS Function role (MoveUpDown/StopStepUpDown) authoritatively promotes a
GA to category=shutter — rescues bare DPT 1.007 step/stops the name classifier left
'unknown' (group A: 26/33 covers were dropped). Guarded to DPT 1.x/5.x; role strings
from Kris's real dump, not invented.
- generate_ha.py: cover builder now gathers candidates and picks the BEST per role
(same-function > name-token overlap > nearest sub) instead of first-match, so a
function-less roller takes its own step/stop, not an awning's sharing the zone token
(group C). Bare 1.007/1.010 admitted only with a shutter name signal, so a foreign
lighting step can't be stolen (audit finding).
- tests: test_cover_pairing.py rebuilt from the real A/B/C structures + a group-D guard
for the foreign-1.007 case. Fails without the fix, passes with it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
generate_ha_package cross-wired move_short_address across unrelated ETS
Functions sharing a zone token (a window and an awning both 'Room A'), and
could leave a cover with no step/stop. Pair siblings by ETS Function
membership (authoritative); a sibling owned by a different function is never
borrowed. Falls back to the existing name-token logic only when the move GA
has no function, so projects without ETS Functions are unchanged.
Adds tests/test_cover_pairing.py (reproduces the field case from #11, fails
without the fix) and wires it into CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
detect_topology_issues now surfaces in the human-review project_report markdown
(new '2.4 Topology & addressing' section) and its severities count into the
report totals — closes the audit's deferred LOW (per CLAUDE.md the human reviews
project_report before ETS import, so topology issues must be visible there).
Functional check: Poltavskiy report renders the coupler INFO; suite 27 passed
(pre-existing test_protocol collection error untouched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New design-time check grounded in the KNX standard (KNX Handbook + xknx via
deepwiki). detect_topology_issues() + check_topology() MCP tool (30->31 tools),
folded into analyze_all counters:
- devices per line vs TP1 segment 64 / line 256 (KNX Handbook p.36/40/55) — TP-only
- individual address valid A.L.D (area 0-15, line 0-15, device 0-255; xknx-confirmed)
- duplicate individual address -> error; missing .0 coupler on a multi-line TP -> info
Medium-gated on the real xknxproject string ('Twisted Pair (TP)'), so IP/PL/RF
lines are never falsely flagged (fixed a dead 'medium==IP' guard caught in audit;
regression test added). senior-code-audit: APPROVED. Functional smoke on 2 real
projects: IP lines clean, TP coupler/count notes correct. 64/256 cited to the
Handbook, not attributed to xknx. tests/test_topology.py 7/7; suite 27 passed
(pre-existing unrelated test_protocol collection error untouched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two new tools (28 -> 30): validate_room_template checks a built-in or custom
template against the R1 schema; compose_rooms builds a new project from a room
list and returns the allocation manifest, ETS GA XML/CSV (via the existing
generators on the re-read project), and a device BOM proposal. New projects
only, dry-run by default; writes artifacts into the confined workspace when
dry_run=false and output_dir is given. Never touches a bus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the Room Template Library core: the public YAML template contract
(schema_version, locale-neutral semantic slot_id, ru/en labels, per-slot
basic/comfort presets, area_m2 as a provenance-tagged hint) with 6 built-in
rooms (bedroom, children, living, kitchen, bathroom, corridor) + SCHEMA.md.
room_library.py implements a function-first pipeline: templates+params ->
resolved IR dataclasses (NOT GARecord) -> deterministic, permutation-invariant
allocation (main=domain, middle=role, sub sequential; hard error on sub
exhaustion) -> a real .knxproj ZIP that is re-read via load_project (same path
as third-party projects, so generation never touches the classifier). Includes
validate_room_template, compose(), manifest + device BOM proposal builders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dogfooding the voice layer exposed two pairing defects: (1) _FUNC_WORDS had no
Russian entries, so 'вкл/выкл/цвет/яркость' polluted device identity and an RGBW
colour GA could not find its own switch; (2) subset identity matching let
'RGBW подсветка яркость' {zone, подсветка} grab 'камин подсветка' by subset and
its status by tie-break. Sibling pick and status_for_dpt now prefer EXACT
identity equality before subset fallback. 13/13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- location phrases no longer vote a domain: "Boiler room light" is a light in
the boiler room, not an HVAC function (location stoplist stripped first).
- dropped the bare "температур" hvac term: a temperature GA takes its domain
from context (уставк/кондиц/тёпл words or its main group), so "Гостиная
температура" in a Sensors main is a sensor, not an hvac actuator outlier.
- "online/offline/heartbeat/watchdog/связь" are diagnostics terms (checked
first), so actuator-monitoring GAs named after the load they watch stay diag.
- check_policy: central macros ("Весь свет выкл") are never taxonomy outliers;
policy_no_reserve now also fires under an INFERRED taxonomy when no reserve
main exists at all; "весь "/"мастер" added to central-macro tokens.
Messy-house dogfood now reports exactly the planted misplacements (3/3) and the
reserve gap; demo 0 outliers. 13/13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DPT 1.018 occupancy: category sensor (was diagnostics; the name always says
sensor and the table fought it into a fake conflict).
- 9.001 temperature is SOFT: room temperature defaults to hvac, but a weather-
named temperature re-domains to sensor without a fabricated conflict.
- illuminance terms: "освещённость"/"illuminance" are sensor words and must not
be swallowed by the greedy lighting prefix "освещ".
- passive-range rule: a measurement inside a Sensors/Energy/Diagnostics main
takes that main's domain; a COMMAND is never retyped by a passive range.
Deliberately NOT tuned further: remaining real-project findings (Minsk 10/685,
Razdory 75/3646, demo 2/239) are defensible mixed-main observations a reviewer
should see once — zeroing them out would overfit one integrator's idiosyncrasy.
13/13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Building a deliberately messy synthetic house instantly exposed four real gaps:
- _build_range_name_map: a middle group 0 starts at the same raw address as its
main, so the modulo heuristic let "Вкл/Выкл" clobber "Освещение" and the
main-range rescue silently died. Depth in the range tree decides now.
- a category from a whole-main DPT fallback (e.g. 20.609 -> hvac) was treated as
strong; only an exact (main,sub) table entry is strong now (is_exact_dpt).
- DPT 1.010 start/stop marked soft: ventilation timers use it, not just shutters.
- terms learned: Cyrillic а/с, сплит, вытяжк, яркост (RU only); weather/метео in
sensor, and sensor now outranks hvac so weather-station temperature is a
sensor, not an HVAC actuator.
Real-project noise after the round: Minsk 1 outlier/685 GAs (was 31 pre-P5),
demo 2/239, Razdory 46/3646. Regressions in test_domain_classifier. 13/13 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 1-bit switch is domain-agnostic, yet DPT 1.001 defaulted to 'lighting', so an
"AC on/off" was classified lighting and tripped downstream checks. The domain is
now a combination: explicit name > strong domain-encoding DPT > main-group name;
a bare 1-bit GA with no signal is honestly 'unknown', never a guessed lighting.
An explicit name contradicting a STRONG DPT yields 'unknown' (a genuine conflict,
shown by explain_ga as contested), not a silent pick. Word-boundary matching so
"ac" no longer fires inside "terrace"; name terms broadened (EN+RU HVAC incl.
ac/a-c/air-conditioner, RGB/colour lighting).
check_policy taxonomy-outlier now flags only misplaced ACTUATORS (lighting/
shutter/hvac); sensors, scene links, thresholds and timeout params are the
cross-cutting GAs they are. HA generator still emits a switch for a now-HVAC
simple on/off, so no device is dropped.
Verified on 3 real projects: every AC on/off is HVAC; outlier noise fell demo
11->2, Minsk 31->6, Razdory 200->57. tests/test_domain_classifier.py (13/13 green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matter readiness, the completeness grade and command/status coverage each now
carry a `math` block: 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 and the
biggest "why is this number what it is?" gap. Completeness also states its band
thresholds. Report-only, additive; scores unchanged, only now auditable.
Raised by external review. tests/test_explainable_aggregates.py (12/12 green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A .knxproj is a user-supplied ZIP-of-XML — untrusted input. New safexml.py
centralises hardened parsing for every place we open an archive or parse XML
ourselves (load_project, check_device_parameters, app-program parser):
XML - dependency-free reject of DOCTYPE/ENTITY (billion-laughs / XXE;
legitimate ETS XML never carries one) + defusedxml parser-level
blocking when installed (added as a dependency).
ZIP - pre-flight against absolute caps (archive size, entry count, per-member
and total decompressed size, compression ratio) rejects zip-bombs;
member names checked for path-traversal / absolute paths; each member
read through a streaming cap so a lying header can't exhaust memory.
Violations return a normal error dict, not a traceback. Still read-only.
Raised by external security review. tests/test_safexml.py (11/11 suite green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Council #2: the empty GA 2/5/2 spawned missing_status + policy_taxonomy_outlier +
a lighting classification on top of its real defect. An empty name means the
classifier can't be trusted, so detect_missing_status and check_policy now skip
blank-name GAs; the single root cause is empty_name (check_naming). Demo 2/5/2:
3 findings -> 1. tests/test_root_cause.py; full suite green (10 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent reviewers (two external councils + a field integrator) asked for
the same thing: the enriched model mixes ETS facts, DPT structure and name
heuristics, and downstream tools treat it almost as fact. explain_ga makes the
reasoning auditable per GA — evidence per decision with a confidence tier
(authoritative ETS Function > structural DPT > heuristic name), the status-pairing
method, and CONFLICTS (name 'AC' vs DPT 'lighting' -> contested), the silent-
misclassification hotspot. Additive, read-only, no core-model change. Full suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'Does the function have *a* status' missed a dimmer with on/off status but no
brightness status (demo planted error #3), silently inflating coverage/Matter.
detect_role_completeness flags a brightness/position command (5.001) whose device
has no matching value status (missing_value_status), with a device-identity match
so it doesn't borrow a sibling's status. Demo recall 4/5 -> 5/5. Raised by the
external expert review. Test + demo ground-truth updated. Full suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
External expert review (real run on the demo house) flagged: scenes (18.001)
wrongly marked missing_status + an unsafe DPT-1.011 status repair for them;
'All blinds down' warned while 'All lights off' was correctly INFO; and a Russian
'(статус)' suffix synthesised on an English project. Fixes: scene_no_status INFO
+ no synth status for scenes; broadened central-macro detection (generic 'all
blinds/shutters/...'); language-matched repair suffixes. Demo missing_status
warnings ~7 -> 1 genuine. tests/test_council_fixes.py; full suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rank findings by significance: config_value (setpoint/hysteresis/time/threshold —
an odd device is usually a real mistake) vs config_flag (mode/type) vs label
(per-room text, often intentional). New focus list surfaces the config-value clear
outliers first. On a real 275-device project this turns 422 raw outliers into a
9-item focus that includes the thermostats whose init-setpoint differs from their
siblings. Multilingual name hints (EN/DE/RU). Test updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validate a project against ITS OWN rules, not a universal standard. With a YAML
profile the declared main-group taxonomy / naming / pairing 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 — never against
an alien standard (a well-organised real 356-GA project drops from 329 false
mismatches vs the default to 31 genuine self-deviations). Answers the recurring
integrator critique that 'your best practices aren't universal'. Example profile
+ test_policy.py included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New module param_check.py + MCP tool check_device_parameters: reads per-device
ParameterInstanceRef values from the .knxproj project part (xknxproject does not
expose them), 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) and split_configs (balanced
variants → review). Parameter names resolved from the app-program; encrypted
projects skipped honestly. Read-only, no ETS/bus. Validated on real 42-275-device
projects; synthetic test_param_check.py. Answers a community feature request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From validator to repairer: propose concrete fixes, not just flag problems.
- B1 repair-suggestion engine (repair.py) + suggest_repairs MCP tool: infer a
DPT for a missing-DPT GA, correct a suspect sub-DPT, synthesise a status GA in
a free slot, or add an absolute-brightness GA. Suggestions only; accepted new
GAs feed generate_ets_group_addresses; never writes to ETS or the bus.
- A2 relative-only-dimming detector (analyze.py): 3.007 relative dimmer with no
5.001 absolute-brightness GA in its zone -> HA cannot set a level.
- A3 cover invert/travel-time surfacing (generate_ha.py): verify_cover_invert
review note lists actuator-dependent flags absent from the .knxproj and warns
when position lacks a state address.
- A2/A3/B1 regression tests; CHANGELOG + version bump to 0.5.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A1 — sub-DPT sanity linter: when a GA name implies a specific DPT
sub-type (temperature->9.001, power->14.056, brightness/position->5.001),
flag a wrong sub or wrong main. Multilingual, conservative; surfaced as
the subdpt_suspect finding via check_dpt / analyze_all.
A4 — KNX Data Secure posture: secure_posture() + new check_secure MCP
tool. Report-only summary (secured vs plaintext counts, mixed
secure/plaintext middle groups, keyring handover checklist). Reads only
the per-GA Security flag; no key material touched. Handover pack section
5 rewritten to this posture section.
Version bump to 0.4.0; CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the tool from a .knxproj validator into a design aid.
- device_library.py + decompose_device / list_device_recipes MCP tools:
expand a device (order number / type / alias) into its group-address
recipe — command/status/dimming/position/mode objects with DPTs —
across Zennio + ABB families. Generic vendor facts, typical-wired set.
- docs/spec-to-structure.md: the spec→structure methodology, with a
measured account of what a spec reproduces (~90%) vs the per-device
object count it cannot (2–9× per-project parameterisation).
- Ship alongside the Track B generate_handover_pack and the de-noise
refinements accumulated since 0.2.0.
- CHANGELOG 0.3.0; version bump 0.2.0 → 0.3.0; new device-library tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Main/middle range names now resolve correctly (e.g. [1] Освещение 1 этаж, [5] Климат)
instead of a middle-group name leaking into the main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three false-alarm downgrades derived from validating on a larger multi-vendor
villa, each turning a false positive into an INFO note without hiding any real
defect:
- intent: divider/separator names (punctuation-only or a marker wrapped in it,
e.g. "-----addition------", "---") classify as `scratch`, so a missing DPT on
them is INFO, not a red error.
- analyze: typed GAs wired into Zennio "[LF] ... Data Entry" type-agnostic
logic-function containers surface as INFO `dpt_on_logic_object` instead of a
false `dpt_mismatch_co` warning.
- analyze: all-groups broadcast/central-macro commands surface as INFO
`central_macro_no_status` instead of `missing_status_address`, since a fan-out
broadcast has no single state to read back.
Adds a regression block covering all three; real DPT mismatches and real
missing-status gaps still warn. Unreleased; no version bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump version 0.1.2 -> 0.2.0 (pyproject + __init__). Promote the
Unreleased changelog section to [0.2.0] — 2026-06-30. Scrub a client
city label from two test comments (provenance note only, no PII/topology).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>