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>
Colour control assembled into light entities (RGB 232.600, RGBW 251.600,
xyY 242.600, colour-temp 7.600) with statuses, matched by zone identity.
Climate entities generated from HVAC mode zones (temp 9.001 + target status
+ operation/controller mode 20.102/20.105 + valve command_value 5.001),
emitted only when HA-required keys are present, else routed to review.
B1 fix: a command no longer borrows a sibling's status (identity must nest,
status maps to exactly one entity) — removed all shared-status addresses.
New DPTs 232.600/251.600/242.600/7.600/20.105/1.100. HA keys verified vs docs.
Real files: signed demo 6 climate + 4 colour lights; Zennio 685-GA 7 climate;
zero shared-status; all 10 regression groups pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Classify every group address as functional/reserve/logic/scratch (intent.py)
and reclassify intentional non-functional GAs instead of crying wolf:
- reserve spare with no DPT -> INFO reserve_without_dpt (not a missing_dpt error)
- reserve name reused across DPTs -> no duplicate_name / inconsistent_dpt
- logic/virtual + scratch GAs -> excluded from missing-status warnings
dpt_mismatch_co and all real functional findings are untouched.
Driven by a real 685-GA Zennio project: false errors 29 -> 6,
missing-status noise 79 -> 45, all 12 real dpt_mismatch_co catches preserved.
Intent breakdown now exposed via list_group_addresses, report inventory and
the analyze_all summary. New regression test covers the reserve/logic/scratch
patterns and proves real DPT + status problems still surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-2 fixes found by running against more public ETS4/5/6 fixtures (yene
DemoCase, KnxProjParser, knxray, open-knxviewer):
- Lights assemble both on/off and brightness status via identity-based pairing
and fold the on/off command into one light entity (no duplicate switch),
pairing even when device identity is a single name token. Switches too.
- Cover up/down recognised on 1.008 or any 1.x named up/down; stop on
1.007/1.010/1.017 or a stop-name. Siblings attach only to the same shutter
(zone-identity guard) so multiple blinds don't cross-wire.
- Date/time/text DPTs (10/11/16/19) route to review (manual_datetime/manual_text).
- Add regression tests for all three.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Venetian slat GAs (Lamelle/slat/tilt) attach to their parent blind cover as
move_short_address instead of becoming standalone covers; unmatched slats are
flagged shutter_slat_unattached for manual attachment.
- 1-bit diagnostics GAs (wind/frost/smoke/leak alarm, fault) become a read-only
binary_sensor instead of a phantom command switch.
- Add regression tests for both.
Backlog item #6 from VALIDATION-FINDINGS, surfaced by real ETS fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- generate_ha now lists every unmapped GA in `review` instead of dropping it
silently ("no silent caps"); YAML header reports the review count.
- Shutter category recognises German/directional names (Behang, Lamelle,
auf/ab, Raffstore, Markise, RU equivalents). Shutter-looking commands with an
incomplete DPT get an actionable `shutter_incomplete_dpt` review hint instead
of vanishing.
- Add regression tests for both.
Backlog items #4 and #5 from VALIDATION-FINDINGS, surfaced by real ETS fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>