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>
Dependabot opened the same mcp <3 widening a third time (PR #17) with
update-types: semver-major in place. The docs do not say whether update-types
covers requirement widening; a versions range is the documented, direct form.
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>
mcp 2.x drops mcp.server.fastmcp; the import fails outright on 2.2.0, which is
what Dependabot's first PR would have allowed. Minor and patch updates still flow.
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)
- 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.
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>
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>
Design-time MCP server that reads .knxproj (read-only), validates naming/DPT/status,
and generates Home Assistant KNX YAML + ETS-importable group addresses (XML/CSV).
No live bus access — confined-workspace writes only.
Includes: 12 MCP tools, end-to-end smoke test, MIT license, English-first README
(+ Russian), CONTRIBUTING with a real-project test call, SECURITY policy, CHANGELOG,
GitHub Actions CI (Python 3.10–3.12), and issue/PR templates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>