diff --git a/.gitignore b/.gitignore index 6977844..15b7129 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ knx-workspace/ *.knxproj *.knxkeys *.keyring +# ...except the curated synthetic demo (no real topology / no secrets) +!examples/demo-home/demo-home.knxproj # OS / editors .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7bdff..c94f063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `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 diff --git a/examples/demo-home/README.md b/examples/demo-home/README.md new file mode 100644 index 0000000..e1039e1 --- /dev/null +++ b/examples/demo-home/README.md @@ -0,0 +1,54 @@ +# Demo House — end-to-end example + +A complete, **synthetic** worked example of the whole stack this project targets: + +``` +demo-home.knxproj → nickol-knx-mcp → generated/ (report · HA entities · ETS export) + (KNX design) (design-time) ↓ + ha-brain/ (Home Assistant smart logic) +``` + +It exists so you can see real input → real output without needing your own project, and as a +showcase of how the KNX (I/O) and Home Assistant (logic) layers fit together. + +## What's here + +| Path | What it is | +|---|---| +| `demo-home.knxproj` | The synthetic project: **239 group addresses, 47 ETS Functions**, 13 zones (living w/ fireplace, kitchen, master + 2 kids bedrooms each with ensuite, guest WC, laundry, 2 corridors, staircase). Lighting (switch/dim/CCT/RGBW), underfloor heating + AC, sensors, scenes. | +| `generated/project_report.md` | `project_report` output — inventory + 🔴🟡🔵 findings + HA mapping preview. | +| `generated/home-assistant-knx.yaml` | `generate_ha_package` output — KNX entities (switch/light/cover/binary_sensor/sensor) + a `review` list. **Nothing is dropped silently.** | +| `generated/group-addresses.xml` / `.csv` | `generate_ets_group_addresses` output — ETS-importable. | +| `ha-brain/` | The Home Assistant **smart layer** on top of these entities — circadian lighting, multi-factor climate, presence/season/time logic, statistics. See its own [README](ha-brain/README.md). | + +## What the tool found (and the deliberate flaws) + +The project **intentionally contains 5 mistakes** so the checks have something to catch — see them +flagged in `generated/project_report.md`: + +| # | Planted mistake | Caught? | +|---|---|---| +| 1 | A group address with **no DPT** (`Living room CO2`, 4/2/1) | ✅ `check_dpt` 🔴 | +| 2 | Two GAs with the **same name, different DPT** (`Kitchen temperature`) | ✅ `check_dpt` | +| 3 | A dimmer with an on/off status but **no brightness status** (`Kitchen worktop LED`) | ⚠️ not flagged* | +| 4 | A switch with **no status** at all (`Guest WC ceiling`) | ✅ `check_missing_status` | +| 5 | A GA with an **empty name** (`2/5/2`) | ✅ `check_naming` 🔴 | + +\* **Honest limitation surfaced by this very demo:** the missing-status check currently asks +"does this control have *a* status?", not "does it have *each expected* status type?". The Kitchen +dimmer has an on/off status, so it isn't flagged for its missing brightness status. Tracked for a +future release. + +Inventory: 239 GAs · 47 Functions · 0 errors that block parsing. The HA generator produced +**19 switches, 13 lights, 6 covers, 13 binary sensors, 63 sensors**, with 18/19 switches getting a +`state_address` paired via ETS Function roles. + +## ⚠️ Caveats (please read) + +- **Synthetic & generated.** This `.knxproj` was authored programmatically (ETS6 schema + `project/22`) — not exported from ETS. It parses cleanly with `xknxproject` (the library this + tool uses) and is intended for **tool validation and demonstration**. It has **not** been + verified to open in ETS6 itself; a real ETS-authored export is the gold standard for that. +- **The flaws are on purpose.** Do not "fix" them — they are the point. +- **`ha-brain/` is a design demo.** Valid Home Assistant YAML built on best practices, but not + deployed against a live bus here; entity IDs assume the names `generate_ha_package` produces. diff --git a/examples/demo-home/demo-home.knxproj b/examples/demo-home/demo-home.knxproj new file mode 100644 index 0000000..4b72f0b Binary files /dev/null and b/examples/demo-home/demo-home.knxproj differ diff --git a/examples/demo-home/generated/group-addresses.csv b/examples/demo-home/generated/group-addresses.csv new file mode 100644 index 0000000..4edbd30 --- /dev/null +++ b/examples/demo-home/generated/group-addresses.csv @@ -0,0 +1,281 @@ +"Group name";"Address";"Central";"Unfiltered";"Description";"DatapointType";"Security" +"Central / Scenes";"0/-/-";"";"";"";"";"" +"Scenes";"0/0/-";"";"";"";"";"" +"Scene All off";"0/0/1";"";"";"";"DPST-18-1";"Auto" +"Scene Movie";"0/0/2";"";"";"";"DPST-18-1";"Auto" +"Scene Night";"0/0/3";"";"";"";"DPST-18-1";"Auto" +"Scene Away";"0/0/4";"";"";"";"DPST-18-1";"Auto" +"Central functions";"0/1/-";"";"";"";"";"" +"All lights off";"0/1/1";"";"";"";"DPST-1-1";"Auto" +"All blinds down";"0/1/2";"";"";"";"DPST-1-8";"Auto" +"Fireplace (cmd)";"0/3/-";"";"";"";"";"" +"Living room fireplace on/off";"0/3/0";"";"";"";"DPST-1-1";"Auto" +"Fireplace (status)";"0/4/-";"";"";"";"";"" +"Living room fireplace status";"0/4/0";"";"";"";"DPST-1-11";"Auto" +"Lighting";"1/-/-";"";"";"";"";"" +"Switch (cmd)";"1/0/-";"";"";"";"";"" +"Living room ceiling spots switch";"1/0/1";"";"";"";"DPST-1-1";"Auto" +"Living room cove switch";"1/0/2";"";"";"";"DPST-1-1";"Auto" +"Living room RGBW accent wall switch";"1/0/3";"";"";"";"DPST-1-1";"Auto" +"Living room fireplace floor lamp switch";"1/0/4";"";"";"";"DPST-1-1";"Auto" +"Kitchen main ceiling switch";"1/0/10";"";"";"";"DPST-1-1";"Auto" +"Kitchen worktop LED switch";"1/0/11";"";"";"";"DPST-1-1";"Auto" +"Kitchen island pendants switch";"1/0/12";"";"";"";"DPST-1-1";"Auto" +"Master bedroom ceiling switch";"1/0/20";"";"";"";"DPST-1-1";"Auto" +"Master bedroom bedside left switch";"1/0/21";"";"";"";"DPST-1-1";"Auto" +"Master bedroom bedside right switch";"1/0/22";"";"";"";"DPST-1-1";"Auto" +"Master bedroom wardrobe LED switch";"1/0/23";"";"";"";"DPST-1-1";"Auto" +"Master bath ceiling switch";"1/0/25";"";"";"";"DPST-1-1";"Auto" +"Master bath mirror CCT switch";"1/0/26";"";"";"";"DPST-1-1";"Auto" +"Kids room 1 ceiling switch";"1/0/30";"";"";"";"DPST-1-1";"Auto" +"Kids room 1 RGB accent switch";"1/0/31";"";"";"";"DPST-1-1";"Auto" +"Kids bath 1 ceiling switch";"1/0/35";"";"";"";"DPST-1-1";"Auto" +"Kids room 2 ceiling switch";"1/0/40";"";"";"";"DPST-1-1";"Auto" +"Kids room 2 desk light switch";"1/0/41";"";"";"";"DPST-1-1";"Auto" +"Kids bath 2 ceiling switch";"1/0/45";"";"";"";"DPST-1-1";"Auto" +"Guest WC ceiling switch";"1/0/50";"";"";"";"DPST-1-1";"Auto" +"Laundry ceiling switch";"1/0/55";"";"";"";"DPST-1-1";"Auto" +"Corridor GF ceiling switch";"1/0/60";"";"";"";"DPST-1-1";"Auto" +"Corridor UF ceiling switch";"1/0/61";"";"";"";"DPST-1-1";"Auto" +"Staircase LED steps switch";"1/0/65";"";"";"";"DPST-1-1";"Auto" +"Dim relative (cmd)";"1/1/-";"";"";"";"";"" +"Living room cove dim";"1/1/2";"";"";"";"DPST-3-7";"Auto" +"Living room RGBW accent wall dim";"1/1/3";"";"";"";"DPST-3-7";"Auto" +"Kitchen worktop LED dim";"1/1/11";"";"";"";"DPST-3-7";"Auto" +"Kitchen island pendants dim";"1/1/12";"";"";"";"DPST-3-7";"Auto" +"Master bedroom ceiling dim";"1/1/20";"";"";"";"DPST-3-7";"Auto" +"Master bedroom bedside left dim";"1/1/21";"";"";"";"DPST-3-7";"Auto" +"Master bedroom bedside right dim";"1/1/22";"";"";"";"DPST-3-7";"Auto" +"Master bath mirror CCT dim";"1/1/26";"";"";"";"DPST-3-7";"Auto" +"Kids room 1 ceiling dim";"1/1/30";"";"";"";"DPST-3-7";"Auto" +"Kids room 1 RGB accent dim";"1/1/31";"";"";"";"DPST-3-7";"Auto" +"Kids room 2 ceiling dim";"1/1/40";"";"";"";"DPST-3-7";"Auto" +"Staircase LED steps dim";"1/1/65";"";"";"";"DPST-3-7";"Auto" +"Brightness (cmd)";"1/2/-";"";"";"";"";"" +"Living room cove brightness";"1/2/2";"";"";"";"DPST-5-1";"Auto" +"Living room RGBW accent wall brightness";"1/2/3";"";"";"";"DPST-5-1";"Auto" +"Kitchen worktop LED brightness";"1/2/11";"";"";"";"DPST-5-1";"Auto" +"Kitchen island pendants brightness";"1/2/12";"";"";"";"DPST-5-1";"Auto" +"Master bedroom ceiling brightness";"1/2/20";"";"";"";"DPST-5-1";"Auto" +"Master bedroom bedside left brightness";"1/2/21";"";"";"";"DPST-5-1";"Auto" +"Master bedroom bedside right brightness";"1/2/22";"";"";"";"DPST-5-1";"Auto" +"Master bath mirror CCT brightness";"1/2/26";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 ceiling brightness";"1/2/30";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 RGB accent brightness";"1/2/31";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 ceiling brightness";"1/2/40";"";"";"";"DPST-5-1";"Auto" +"Staircase LED steps brightness";"1/2/65";"";"";"";"DPST-5-1";"Auto" +"Colour/CCT (cmd)";"1/3/-";"";"";"";"";"" +"Living room RGBW accent wall colour";"1/3/3";"";"";"";"DPST-251-600";"Auto" +"Kitchen worktop LED colour temp";"1/3/11";"";"";"";"DPST-7-600";"Auto" +"Master bath mirror CCT colour temp";"1/3/26";"";"";"";"DPST-7-600";"Auto" +"Kids room 1 RGB accent colour";"1/3/31";"";"";"";"DPST-232-600";"Auto" +"Switch (status)";"1/4/-";"";"";"";"";"" +"Living room ceiling spots status";"1/4/1";"";"";"";"DPST-1-11";"Auto" +"Living room cove status";"1/4/2";"";"";"";"DPST-1-11";"Auto" +"Living room RGBW accent wall status";"1/4/3";"";"";"";"DPST-1-11";"Auto" +"Living room fireplace floor lamp status";"1/4/4";"";"";"";"DPST-1-11";"Auto" +"Kitchen main ceiling status";"1/4/10";"";"";"";"DPST-1-11";"Auto" +"Kitchen worktop LED status";"1/4/11";"";"";"";"DPST-1-11";"Auto" +"Kitchen island pendants status";"1/4/12";"";"";"";"DPST-1-11";"Auto" +"Master bedroom ceiling status";"1/4/20";"";"";"";"DPST-1-11";"Auto" +"Master bedroom bedside left status";"1/4/21";"";"";"";"DPST-1-11";"Auto" +"Master bedroom bedside right status";"1/4/22";"";"";"";"DPST-1-11";"Auto" +"Master bedroom wardrobe LED status";"1/4/23";"";"";"";"DPST-1-11";"Auto" +"Master bath ceiling status";"1/4/25";"";"";"";"DPST-1-11";"Auto" +"Master bath mirror CCT status";"1/4/26";"";"";"";"DPST-1-11";"Auto" +"Kids room 1 ceiling status";"1/4/30";"";"";"";"DPST-1-11";"Auto" +"Kids room 1 RGB accent status";"1/4/31";"";"";"";"DPST-1-11";"Auto" +"Kids bath 1 ceiling status";"1/4/35";"";"";"";"DPST-1-11";"Auto" +"Kids room 2 ceiling status";"1/4/40";"";"";"";"DPST-1-11";"Auto" +"Kids room 2 desk light status";"1/4/41";"";"";"";"DPST-1-11";"Auto" +"Kids bath 2 ceiling status";"1/4/45";"";"";"";"DPST-1-11";"Auto" +"Laundry ceiling status";"1/4/55";"";"";"";"DPST-1-11";"Auto" +"Corridor GF ceiling status";"1/4/60";"";"";"";"DPST-1-11";"Auto" +"Corridor UF ceiling status";"1/4/61";"";"";"";"DPST-1-11";"Auto" +"Staircase LED steps status";"1/4/65";"";"";"";"DPST-1-11";"Auto" +"Brightness (status)";"1/5/-";"";"";"";"";"" +"Living room cove brightness status";"1/5/2";"";"";"";"DPST-5-1";"Auto" +"Living room RGBW accent wall brightness status";"1/5/3";"";"";"";"DPST-5-1";"Auto" +"Kitchen island pendants brightness status";"1/5/12";"";"";"";"DPST-5-1";"Auto" +"Master bedroom ceiling brightness status";"1/5/20";"";"";"";"DPST-5-1";"Auto" +"Master bedroom bedside left brightness status";"1/5/21";"";"";"";"DPST-5-1";"Auto" +"Master bedroom bedside right brightness status";"1/5/22";"";"";"";"DPST-5-1";"Auto" +"Master bath mirror CCT brightness status";"1/5/26";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 ceiling brightness status";"1/5/30";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 RGB accent brightness status";"1/5/31";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 ceiling brightness status";"1/5/40";"";"";"";"DPST-5-1";"Auto" +"Staircase LED steps brightness status";"1/5/65";"";"";"";"DPST-5-1";"Auto" +"Colour/CCT (status)";"1/6/-";"";"";"";"";"" +"Living room RGBW accent wall colour status";"1/6/3";"";"";"";"DPST-251-600";"Auto" +"Kitchen worktop LED colour temp status";"1/6/11";"";"";"";"DPST-7-600";"Auto" +"Master bath mirror CCT colour temp status";"1/6/26";"";"";"";"DPST-7-600";"Auto" +"Kids room 1 RGB accent colour status";"1/6/31";"";"";"";"DPST-232-600";"Auto" +"Shutters / Blinds";"2/-/-";"";"";"";"";"" +"Up/Down";"2/0/-";"";"";"";"";"" +"Living room window blind up/down";"2/0/1";"";"";"";"DPST-1-8";"Auto" +"Living room terrace blind up/down";"2/0/2";"";"";"";"DPST-1-8";"Auto" +"Master bedroom blind up/down";"2/0/10";"";"";"";"DPST-1-8";"Auto" +"Kids room 1 blind up/down";"2/0/20";"";"";"";"DPST-1-8";"Auto" +"Kids room 2 blind up/down";"2/0/30";"";"";"";"DPST-1-8";"Auto" +"Stop/Step";"2/1/-";"";"";"";"";"" +"Living room window blind stop";"2/1/1";"";"";"";"DPST-1-10";"Auto" +"Living room terrace blind stop";"2/1/2";"";"";"";"DPST-1-10";"Auto" +"Master bedroom blind stop";"2/1/10";"";"";"";"DPST-1-10";"Auto" +"Kids room 1 blind stop";"2/1/20";"";"";"";"DPST-1-10";"Auto" +"Kids room 2 blind stop";"2/1/30";"";"";"";"DPST-1-10";"Auto" +"Position (cmd)";"2/2/-";"";"";"";"";"" +"Living room window blind position";"2/2/1";"";"";"";"DPST-5-1";"Auto" +"Living room terrace blind position";"2/2/2";"";"";"";"DPST-5-1";"Auto" +"Master bedroom blind position";"2/2/10";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 blind position";"2/2/20";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 blind position";"2/2/30";"";"";"";"DPST-5-1";"Auto" +"Slat (cmd)";"2/3/-";"";"";"";"";"" +"Living room window blind slat";"2/3/1";"";"";"";"DPST-5-1";"Auto" +"Master bedroom blind slat";"2/3/10";"";"";"";"DPST-5-1";"Auto" +"Position (status)";"2/5/-";"";"";"";"";"" +"Living room window blind position status";"2/5/1";"";"";"";"DPST-5-1";"Auto" +"";"2/5/2";"";"";"";"DPST-5-1";"Auto" +"Master bedroom blind position status";"2/5/10";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 blind position status";"2/5/20";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 blind position status";"2/5/30";"";"";"";"DPST-5-1";"Auto" +"Slat (status)";"2/6/-";"";"";"";"";"" +"Living room window blind slat status";"2/6/1";"";"";"";"DPST-5-1";"Auto" +"Master bedroom blind slat status";"2/6/10";"";"";"";"DPST-5-1";"Auto" +"HVAC (heating + cooling)";"3/-/-";"";"";"";"";"" +"Setpoint (cmd)";"3/0/-";"";"";"";"";"" +"Living room heating setpoint";"3/0/1";"";"";"";"DPST-9-1";"Auto" +"Kitchen heating setpoint";"3/0/2";"";"";"";"DPST-9-1";"Auto" +"Master bedroom heating setpoint";"3/0/3";"";"";"";"DPST-9-1";"Auto" +"Master bath heating setpoint";"3/0/4";"";"";"";"DPST-9-1";"Auto" +"Kids room 1 heating setpoint";"3/0/5";"";"";"";"DPST-9-1";"Auto" +"Kids bath 1 heating setpoint";"3/0/6";"";"";"";"DPST-9-1";"Auto" +"Kids room 2 heating setpoint";"3/0/7";"";"";"";"DPST-9-1";"Auto" +"Kids bath 2 heating setpoint";"3/0/8";"";"";"";"DPST-9-1";"Auto" +"Guest WC heating setpoint";"3/0/9";"";"";"";"DPST-9-1";"Auto" +"Laundry heating setpoint";"3/0/10";"";"";"";"DPST-9-1";"Auto" +"Corridor GF heating setpoint";"3/0/11";"";"";"";"DPST-9-1";"Auto" +"Corridor UF heating setpoint";"3/0/12";"";"";"";"DPST-9-1";"Auto" +"Living room AC setpoint";"3/0/21";"";"";"";"DPST-9-1";"Auto" +"Kitchen AC setpoint";"3/0/22";"";"";"";"DPST-9-1";"Auto" +"Master bedroom AC setpoint";"3/0/23";"";"";"";"DPST-9-1";"Auto" +"Kids room 1 AC setpoint";"3/0/25";"";"";"";"DPST-9-1";"Auto" +"Kids room 2 AC setpoint";"3/0/27";"";"";"";"DPST-9-1";"Auto" +"Mode (cmd)";"3/1/-";"";"";"";"";"" +"Living room HVAC mode";"3/1/1";"";"";"";"DPST-20-102";"Auto" +"Kitchen HVAC mode";"3/1/2";"";"";"";"DPST-20-102";"Auto" +"Master bedroom HVAC mode";"3/1/3";"";"";"";"DPST-20-102";"Auto" +"Master bath HVAC mode";"3/1/4";"";"";"";"DPST-20-102";"Auto" +"Kids room 1 HVAC mode";"3/1/5";"";"";"";"DPST-20-102";"Auto" +"Kids bath 1 HVAC mode";"3/1/6";"";"";"";"DPST-20-102";"Auto" +"Kids room 2 HVAC mode";"3/1/7";"";"";"";"DPST-20-102";"Auto" +"Kids bath 2 HVAC mode";"3/1/8";"";"";"";"DPST-20-102";"Auto" +"Guest WC HVAC mode";"3/1/9";"";"";"";"DPST-20-102";"Auto" +"Laundry HVAC mode";"3/1/10";"";"";"";"DPST-20-102";"Auto" +"Corridor GF HVAC mode";"3/1/11";"";"";"";"DPST-20-102";"Auto" +"Corridor UF HVAC mode";"3/1/12";"";"";"";"DPST-20-102";"Auto" +"Living room AC mode";"3/1/21";"";"";"";"DPST-20-102";"Auto" +"Kitchen AC mode";"3/1/22";"";"";"";"DPST-20-102";"Auto" +"Master bedroom AC mode";"3/1/23";"";"";"";"DPST-20-102";"Auto" +"Kids room 1 AC mode";"3/1/25";"";"";"";"DPST-20-102";"Auto" +"Kids room 2 AC mode";"3/1/27";"";"";"";"DPST-20-102";"Auto" +"AC On/Off (cmd)";"3/2/-";"";"";"";"";"" +"Living room AC on/off";"3/2/21";"";"";"";"DPST-1-1";"Auto" +"Kitchen AC on/off";"3/2/22";"";"";"";"DPST-1-1";"Auto" +"Master bedroom AC on/off";"3/2/23";"";"";"";"DPST-1-1";"Auto" +"Kids room 1 AC on/off";"3/2/25";"";"";"";"DPST-1-1";"Auto" +"Kids room 2 AC on/off";"3/2/27";"";"";"";"DPST-1-1";"Auto" +"AC Fan (cmd)";"3/3/-";"";"";"";"";"" +"Living room AC fan speed";"3/3/21";"";"";"";"DPST-5-1";"Auto" +"Kitchen AC fan speed";"3/3/22";"";"";"";"DPST-5-1";"Auto" +"Master bedroom AC fan speed";"3/3/23";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 AC fan speed";"3/3/25";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 AC fan speed";"3/3/27";"";"";"";"DPST-5-1";"Auto" +"Setpoint (status)";"3/4/-";"";"";"";"";"" +"Living room heating setpoint status";"3/4/1";"";"";"";"DPST-9-1";"Auto" +"Kitchen heating setpoint status";"3/4/2";"";"";"";"DPST-9-1";"Auto" +"Master bedroom heating setpoint status";"3/4/3";"";"";"";"DPST-9-1";"Auto" +"Master bath heating setpoint status";"3/4/4";"";"";"";"DPST-9-1";"Auto" +"Kids room 1 heating setpoint status";"3/4/5";"";"";"";"DPST-9-1";"Auto" +"Kids bath 1 heating setpoint status";"3/4/6";"";"";"";"DPST-9-1";"Auto" +"Kids room 2 heating setpoint status";"3/4/7";"";"";"";"DPST-9-1";"Auto" +"Kids bath 2 heating setpoint status";"3/4/8";"";"";"";"DPST-9-1";"Auto" +"Guest WC heating setpoint status";"3/4/9";"";"";"";"DPST-9-1";"Auto" +"Laundry heating setpoint status";"3/4/10";"";"";"";"DPST-9-1";"Auto" +"Corridor GF heating setpoint status";"3/4/11";"";"";"";"DPST-9-1";"Auto" +"Corridor UF heating setpoint status";"3/4/12";"";"";"";"DPST-9-1";"Auto" +"Living room AC setpoint status";"3/4/21";"";"";"";"DPST-9-1";"Auto" +"Kitchen AC setpoint status";"3/4/22";"";"";"";"DPST-9-1";"Auto" +"Master bedroom AC setpoint status";"3/4/23";"";"";"";"DPST-9-1";"Auto" +"Kids room 1 AC setpoint status";"3/4/25";"";"";"";"DPST-9-1";"Auto" +"Kids room 2 AC setpoint status";"3/4/27";"";"";"";"DPST-9-1";"Auto" +"AC On/Off (status)";"3/5/-";"";"";"";"";"" +"Living room AC on/off status";"3/5/21";"";"";"";"DPST-1-11";"Auto" +"Kitchen AC on/off status";"3/5/22";"";"";"";"DPST-1-11";"Auto" +"Master bedroom AC on/off status";"3/5/23";"";"";"";"DPST-1-11";"Auto" +"Kids room 1 AC on/off status";"3/5/25";"";"";"";"DPST-1-11";"Auto" +"Kids room 2 AC on/off status";"3/5/27";"";"";"";"DPST-1-11";"Auto" +"Floor valve (status)";"3/6/-";"";"";"";"";"" +"Living room floor valve status";"3/6/1";"";"";"";"DPST-5-1";"Auto" +"Kitchen floor valve status";"3/6/2";"";"";"";"DPST-5-1";"Auto" +"Master bedroom floor valve status";"3/6/3";"";"";"";"DPST-5-1";"Auto" +"Master bath floor valve status";"3/6/4";"";"";"";"DPST-5-1";"Auto" +"Kids room 1 floor valve status";"3/6/5";"";"";"";"DPST-5-1";"Auto" +"Kids bath 1 floor valve status";"3/6/6";"";"";"";"DPST-5-1";"Auto" +"Kids room 2 floor valve status";"3/6/7";"";"";"";"DPST-5-1";"Auto" +"Kids bath 2 floor valve status";"3/6/8";"";"";"";"DPST-5-1";"Auto" +"Guest WC floor valve status";"3/6/9";"";"";"";"DPST-5-1";"Auto" +"Laundry floor valve status";"3/6/10";"";"";"";"DPST-5-1";"Auto" +"Corridor GF floor valve status";"3/6/11";"";"";"";"DPST-5-1";"Auto" +"Corridor UF floor valve status";"3/6/12";"";"";"";"DPST-5-1";"Auto" +"Sensors";"4/-/-";"";"";"";"";"" +"Temperature";"4/0/-";"";"";"";"";"" +"Living room temperature";"4/0/1";"";"";"";"DPST-9-1";"Auto" +"Kitchen temperature";"4/0/2";"";"";"";"DPST-9-1";"Auto" +"Master bedroom temperature";"4/0/3";"";"";"";"DPST-9-1";"Auto" +"Master bath temperature";"4/0/4";"";"";"";"DPST-9-1";"Auto" +"Kids room 1 temperature";"4/0/5";"";"";"";"DPST-9-1";"Auto" +"Kids bath 1 temperature";"4/0/6";"";"";"";"DPST-9-1";"Auto" +"Kids room 2 temperature";"4/0/7";"";"";"";"DPST-9-1";"Auto" +"Kids bath 2 temperature";"4/0/8";"";"";"";"DPST-9-1";"Auto" +"Kitchen temperature";"4/0/22";"";"";"";"DPST-9-2";"Auto" +"Humidity";"4/1/-";"";"";"";"";"" +"Master bath humidity";"4/1/4";"";"";"";"DPST-9-7";"Auto" +"Kids bath 1 humidity";"4/1/6";"";"";"";"DPST-9-7";"Auto" +"Kids bath 2 humidity";"4/1/8";"";"";"";"DPST-9-7";"Auto" +"Guest WC humidity";"4/1/9";"";"";"";"DPST-9-7";"Auto" +"Laundry humidity";"4/1/10";"";"";"";"DPST-9-7";"Auto" +"CO2";"4/2/-";"";"";"";"";"" +"Living room CO2";"4/2/1";"";"";"";"";"Auto" +"Master bedroom CO2";"4/2/3";"";"";"";"DPST-9-8";"Auto" +"Kids room 1 CO2";"4/2/5";"";"";"";"DPST-9-8";"Auto" +"Kids room 2 CO2";"4/2/7";"";"";"";"DPST-9-8";"Auto" +"Brightness (lux)";"4/3/-";"";"";"";"";"" +"Living room brightness";"4/3/1";"";"";"";"DPST-9-4";"Auto" +"Corridor GF brightness";"4/3/11";"";"";"";"DPST-9-4";"Auto" +"Corridor UF brightness";"4/3/12";"";"";"";"DPST-9-4";"Auto" +"Staircase brightness";"4/3/13";"";"";"";"DPST-9-4";"Auto" +"Presence";"4/4/-";"";"";"";"";"" +"Master bath presence";"4/4/4";"";"";"";"DPST-1-18";"Auto" +"Kids bath 1 presence";"4/4/6";"";"";"";"DPST-1-18";"Auto" +"Kids bath 2 presence";"4/4/8";"";"";"";"DPST-1-18";"Auto" +"Guest WC presence";"4/4/9";"";"";"";"DPST-1-18";"Auto" +"Laundry presence";"4/4/10";"";"";"";"DPST-1-18";"Auto" +"Corridor GF presence";"4/4/11";"";"";"";"DPST-1-18";"Auto" +"Corridor UF presence";"4/4/12";"";"";"";"DPST-1-18";"Auto" +"Staircase presence";"4/4/13";"";"";"";"DPST-1-18";"Auto" +"Window/Door";"4/5/-";"";"";"";"";"" +"Living room window contact";"4/5/1";"";"";"";"DPST-1-19";"Auto" +"Master bedroom window contact";"4/5/3";"";"";"";"DPST-1-19";"Auto" +"Energy";"5/-/-";"";"";"";"";"" +"Active energy";"5/0/-";"";"";"";"";"" +"House total energy";"5/0/0";"";"";"";"DPST-13-13";"Auto" +"Lighting energy";"5/0/1";"";"";"";"DPST-13-13";"Auto" +"HVAC energy";"5/0/2";"";"";"";"DPST-13-13";"Auto" +"Power";"5/1/-";"";"";"";"";"" +"House total power";"5/1/0";"";"";"";"DPST-14-56";"Auto" +"Diagnostics";"6/-/-";"";"";"";"";"" +"Device online";"6/0/-";"";"";"";"";"" +"Lighting actuator online";"6/0/1";"";"";"";"DPST-1-11";"Auto" +"Shutter actuator online";"6/0/2";"";"";"";"DPST-1-11";"Auto" +"Device fault";"6/1/-";"";"";"";"";"" +"HVAC controller fault";"6/1/1";"";"";"";"DPST-1-5";"Auto" diff --git a/examples/demo-home/generated/group-addresses.xml b/examples/demo-home/generated/group-addresses.xml new file mode 100644 index 0000000..cef994f --- /dev/null +++ b/examples/demo-home/generated/group-addresses.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/demo-home/generated/home-assistant-knx.yaml b/examples/demo-home/generated/home-assistant-knx.yaml new file mode 100644 index 0000000..535df4f --- /dev/null +++ b/examples/demo-home/generated/home-assistant-knx.yaml @@ -0,0 +1,372 @@ +# Home Assistant KNX package generated by nickol-knx-mcp +# Source project: Nikolay Demo House +# REVIEW before use: command/status pairing is inferred heuristically. +# Multi-GA fixtures (light/cover/climate) and scenes need manual verification. +# 65 group address(es) need manual review (see the 'review' list); nothing is dropped silently. + +knx: + switch: + - name: All lights off + address: 0/1/1 + - name: Living room fireplace on/off + address: 0/3/0 + state_address: 0/4/0 + - name: Kitchen island pendants switch + address: 1/0/12 + state_address: 1/4/12 + - name: Master bath mirror CCT switch + address: 1/0/26 + state_address: 1/4/26 + - name: Kids room 1 ceiling switch + address: 1/0/30 + state_address: 1/4/30 + - name: Kids room 1 RGB accent switch + address: 1/0/31 + state_address: 1/4/31 + - name: Kids bath 1 ceiling switch + address: 1/0/35 + state_address: 1/4/35 + - name: Kids room 2 ceiling switch + address: 1/0/40 + state_address: 1/4/40 + - name: Kids room 2 desk light switch + address: 1/0/41 + state_address: 1/4/41 + - name: Kids bath 2 ceiling switch + address: 1/0/45 + state_address: 1/4/45 + - name: Guest WC ceiling switch + address: 1/0/50 + state_address: 1/4/1 + - name: Laundry ceiling switch + address: 1/0/55 + state_address: 1/4/55 + - name: Corridor GF ceiling switch + address: 1/0/60 + state_address: 1/4/60 + - name: Corridor UF ceiling switch + address: 1/0/61 + state_address: 1/4/61 + - name: Living room AC on/off + address: 3/2/21 + state_address: 3/5/21 + - name: Kitchen AC on/off + address: 3/2/22 + state_address: 3/5/22 + - name: Master bedroom AC on/off + address: 3/2/23 + state_address: 3/5/23 + - name: Kids room 1 AC on/off + address: 3/2/25 + state_address: 3/5/25 + - name: Kids room 2 AC on/off + address: 3/2/27 + state_address: 3/5/27 + light: + - name: Living room cove brightness + brightness_address: 1/2/2 + brightness_state_address: 1/5/2 + address: 1/0/1 + state_address: 1/4/1 + - name: Living room RGBW accent wall brightness + brightness_address: 1/2/3 + brightness_state_address: 1/5/3 + address: 1/0/2 + state_address: 1/4/2 + - name: Kitchen worktop LED brightness + brightness_address: 1/2/11 + brightness_state_address: 1/5/12 + address: 1/0/10 + state_address: 1/4/10 + - name: Kitchen island pendants brightness + brightness_address: 1/2/12 + brightness_state_address: 1/5/12 + address: 1/0/11 + state_address: 1/4/11 + - name: Master bedroom ceiling brightness + brightness_address: 1/2/20 + brightness_state_address: 1/5/20 + address: 1/0/20 + state_address: 1/4/20 + - name: Master bedroom bedside left brightness + brightness_address: 1/2/21 + brightness_state_address: 1/5/21 + address: 1/0/21 + state_address: 1/4/21 + - name: Master bedroom bedside right brightness + brightness_address: 1/2/22 + brightness_state_address: 1/5/22 + address: 1/0/22 + state_address: 1/4/22 + - name: Master bath mirror CCT brightness + brightness_address: 1/2/26 + brightness_state_address: 1/5/26 + address: 1/0/23 + state_address: 1/4/23 + - name: Kids room 1 ceiling brightness + brightness_address: 1/2/30 + brightness_state_address: 1/5/30 + address: 1/0/3 + state_address: 1/4/3 + - name: Kids room 1 RGB accent brightness + brightness_address: 1/2/31 + brightness_state_address: 1/5/31 + address: 1/0/4 + state_address: 1/4/4 + - name: Kids room 2 ceiling brightness + brightness_address: 1/2/40 + brightness_state_address: 1/5/40 + address: 1/0/25 + state_address: 1/4/25 + - name: Staircase LED steps brightness + brightness_address: 1/2/65 + brightness_state_address: 1/5/65 + address: 1/0/65 + state_address: 1/4/65 + - name: '' + brightness_address: 2/5/2 + cover: + - name: All blinds down + move_long_address: 0/1/2 + - name: Living room window blind up/down + move_long_address: 2/0/1 + move_short_address: 2/1/1 + position_address: 2/2/1 + position_state_address: 2/5/1 + - name: Living room terrace blind up/down + move_long_address: 2/0/2 + move_short_address: 2/1/2 + position_address: 2/2/2 + position_state_address: 2/5/20 + - name: Master bedroom blind up/down + move_long_address: 2/0/10 + move_short_address: 2/1/10 + position_address: 2/2/10 + position_state_address: 2/5/10 + - name: Kids room 1 blind up/down + move_long_address: 2/0/20 + move_short_address: 2/1/20 + position_address: 2/2/20 + position_state_address: 2/5/30 + - name: Kids room 2 blind up/down + move_long_address: 2/0/30 + move_short_address: 2/1/30 + position_address: 2/2/30 + position_state_address: 2/6/1 + binary_sensor: + - name: Master bath presence + state_address: 4/4/4 + - name: Kids bath 1 presence + state_address: 4/4/6 + - name: Kids bath 2 presence + state_address: 4/4/8 + - name: Guest WC presence + state_address: 4/4/9 + - name: Laundry presence + state_address: 4/4/10 + - name: Corridor GF presence + state_address: 4/4/11 + - name: Corridor UF presence + state_address: 4/4/12 + - name: Staircase presence + state_address: 4/4/13 + - name: Living room window contact + state_address: 4/5/1 + - name: Master bedroom window contact + state_address: 4/5/3 + - name: Lighting actuator online + state_address: 6/0/1 + - name: Shutter actuator online + state_address: 6/0/2 + - name: HVAC controller fault + state_address: 6/1/1 + sensor: + - name: Kitchen worktop LED colour temp + state_address: 1/3/11 + type: 2byte_unsigned + - name: Master bath mirror CCT colour temp + state_address: 1/3/26 + type: 2byte_unsigned + - name: Kitchen worktop LED colour temp status + state_address: 1/6/11 + type: 2byte_unsigned + - name: Master bath mirror CCT colour temp status + state_address: 1/6/26 + type: 2byte_unsigned + - name: Living room heating setpoint + state_address: 3/0/1 + type: temperature + - name: Kitchen heating setpoint + state_address: 3/0/2 + type: temperature + - name: Master bedroom heating setpoint + state_address: 3/0/3 + type: temperature + - name: Master bath heating setpoint + state_address: 3/0/4 + type: temperature + - name: Kids room 1 heating setpoint + state_address: 3/0/5 + type: temperature + - name: Kids bath 1 heating setpoint + state_address: 3/0/6 + type: temperature + - name: Kids room 2 heating setpoint + state_address: 3/0/7 + type: temperature + - name: Kids bath 2 heating setpoint + state_address: 3/0/8 + type: temperature + - name: Guest WC heating setpoint + state_address: 3/0/9 + type: temperature + - name: Laundry heating setpoint + state_address: 3/0/10 + type: temperature + - name: Corridor GF heating setpoint + state_address: 3/0/11 + type: temperature + - name: Corridor UF heating setpoint + state_address: 3/0/12 + type: temperature + - name: Living room AC setpoint + state_address: 3/0/21 + type: temperature + - name: Kitchen AC setpoint + state_address: 3/0/22 + type: temperature + - name: Master bedroom AC setpoint + state_address: 3/0/23 + type: temperature + - name: Kids room 1 AC setpoint + state_address: 3/0/25 + type: temperature + - name: Kids room 2 AC setpoint + state_address: 3/0/27 + type: temperature + - name: Living room heating setpoint status + state_address: 3/4/1 + type: temperature + - name: Kitchen heating setpoint status + state_address: 3/4/2 + type: temperature + - name: Master bedroom heating setpoint status + state_address: 3/4/3 + type: temperature + - name: Master bath heating setpoint status + state_address: 3/4/4 + type: temperature + - name: Kids room 1 heating setpoint status + state_address: 3/4/5 + type: temperature + - name: Kids bath 1 heating setpoint status + state_address: 3/4/6 + type: temperature + - name: Kids room 2 heating setpoint status + state_address: 3/4/7 + type: temperature + - name: Kids bath 2 heating setpoint status + state_address: 3/4/8 + type: temperature + - name: Guest WC heating setpoint status + state_address: 3/4/9 + type: temperature + - name: Laundry heating setpoint status + state_address: 3/4/10 + type: temperature + - name: Corridor GF heating setpoint status + state_address: 3/4/11 + type: temperature + - name: Corridor UF heating setpoint status + state_address: 3/4/12 + type: temperature + - name: Living room AC setpoint status + state_address: 3/4/21 + type: temperature + - name: Kitchen AC setpoint status + state_address: 3/4/22 + type: temperature + - name: Master bedroom AC setpoint status + state_address: 3/4/23 + type: temperature + - name: Kids room 1 AC setpoint status + state_address: 3/4/25 + type: temperature + - name: Kids room 2 AC setpoint status + state_address: 3/4/27 + type: temperature + - name: Living room temperature + state_address: 4/0/1 + type: temperature + - name: Kitchen temperature + state_address: 4/0/2 + type: temperature + - name: Master bedroom temperature + state_address: 4/0/3 + type: temperature + - name: Master bath temperature + state_address: 4/0/4 + type: temperature + - name: Kids room 1 temperature + state_address: 4/0/5 + type: temperature + - name: Kids bath 1 temperature + state_address: 4/0/6 + type: temperature + - name: Kids room 2 temperature + state_address: 4/0/7 + type: temperature + - name: Kids bath 2 temperature + state_address: 4/0/8 + type: temperature + - name: Kitchen temperature + state_address: 4/0/22 + type: 2byte_float + - name: Master bath humidity + state_address: 4/1/4 + type: humidity + - name: Kids bath 1 humidity + state_address: 4/1/6 + type: humidity + - name: Kids bath 2 humidity + state_address: 4/1/8 + type: humidity + - name: Guest WC humidity + state_address: 4/1/9 + type: humidity + - name: Laundry humidity + state_address: 4/1/10 + type: humidity + - name: Master bedroom CO2 + state_address: 4/2/3 + type: ppm + - name: Kids room 1 CO2 + state_address: 4/2/5 + type: ppm + - name: Kids room 2 CO2 + state_address: 4/2/7 + type: ppm + - name: Living room brightness + state_address: 4/3/1 + type: illuminance + - name: Corridor GF brightness + state_address: 4/3/11 + type: illuminance + - name: Corridor UF brightness + state_address: 4/3/12 + type: illuminance + - name: Staircase brightness + state_address: 4/3/13 + type: illuminance + - name: House total energy + state_address: 5/0/0 + type: active_energy_kwh + - name: Lighting energy + state_address: 5/0/1 + type: active_energy_kwh + - name: HVAC energy + state_address: 5/0/2 + type: active_energy_kwh + - name: House total power + state_address: 5/1/0 + type: power diff --git a/examples/demo-home/generated/project_report.md b/examples/demo-home/generated/project_report.md new file mode 100644 index 0000000..3c14919 --- /dev/null +++ b/examples/demo-home/generated/project_report.md @@ -0,0 +1,117 @@ +# KNX Project Report — Nikolay Demo House + +- **Source:** `demo-home.knxproj` +- **GA style:** ThreeLevel +- **ETS tool version:** 6.1.5686.0 (xknxproject 3.9.0) +- **Last modified:** 2026-06-28T10:00:00Z + + +## 1. Inventory + +- Group addresses: **239** +- Devices: **0** +- Functions: **47** +- Without DPT: **1** +- KNX Data Secure GAs: **0** + + +**By category:** diagnostics=10, energy=2, hvac=77, lighting=100, scene=4, sensor=15, shutter=26, unknown=5 + +**By kind:** command=100, sensor=55, status=81, unknown=3 + + +## 2. Findings + +Totals: 🔴 errors **2**, 🟡 warnings **10**, 🔵 info **0** + + +### 2.1 Naming & structure (2) + +- 🔴 `2/5/2` — Group address has no name. +- 🟡 `4/0/2, 4/0/22` — Name used by 2 group addresses: ['4/0/2', '4/0/22']. + + +### 2.2 Missing status addresses (8) + +- 🟡 `1/0/50` — Function 'Guest WC ceiling' (FT-1) has command GAs but no status/feedback GA. +- 🟡 `0/0/1` — Command 'Scene All off' (DPT 18.001, Scene control) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `0/0/2` — Command 'Scene Movie' (DPT 18.001, Scene control) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `0/0/3` — Command 'Scene Night' (DPT 18.001, Scene control) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `0/0/4` — Command 'Scene Away' (DPT 18.001, Scene control) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `0/1/1` — Command 'All lights off' (DPT 1.001, Switch on/off) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `0/1/2` — Command 'All blinds down' (DPT 1.008, Up/Down) has no status/feedback GA. Home Assistant cannot read real state. +- 🟡 `2/5/2` — Command '' (DPT 5.001, Scaling 0-100% (brightness/position)) has no status/feedback GA. Home Assistant cannot read real state. + + +### 2.3 DPT consistency (2) + +- 🔴 `4/2/1` — 'Living room CO2' has no DPT assigned. Home Assistant requires a DPT to decode this group address. +- 🟡 `4/0/2, 4/0/22` — Group addresses sharing name 'Kitchen temperature' use different DPTs: ['9.001', '9.002']. + + +## 3. Home Assistant mapping preview + +Entities that can be generated now: switch **19**, light **13**, cover **6**, binary_sensor **13**, sensor **63**. + + +**Needs manual review (65):** + +- `0/1/2` All blinds down — verify_cover_mapping +- `2/0/1` Living room window blind up/down — verify_cover_mapping +- `2/0/2` Living room terrace blind up/down — verify_cover_mapping +- `2/0/10` Master bedroom blind up/down — verify_cover_mapping +- `2/0/20` Kids room 1 blind up/down — verify_cover_mapping +- `2/0/30` Kids room 2 blind up/down — verify_cover_mapping +- `0/1/1` All lights off — switch_without_status +- `0/0/1` Scene All off — manual_scene +- `0/0/2` Scene Movie — manual_scene +- `0/0/3` Scene Night — manual_scene +- `0/0/4` Scene Away — manual_scene +- `1/3/3` Living room RGBW accent wall colour — unmapped_dpt +- `1/3/31` Kids room 1 RGB accent colour — unmapped_dpt +- `1/6/3` Living room RGBW accent wall colour status — unmapped_dpt +- `1/6/31` Kids room 1 RGB accent colour status — unmapped_dpt +- `3/1/1` Living room HVAC mode — manual_climate +- `3/1/2` Kitchen HVAC mode — manual_climate +- `3/1/3` Master bedroom HVAC mode — manual_climate +- `3/1/4` Master bath HVAC mode — manual_climate +- `3/1/5` Kids room 1 HVAC mode — manual_climate +- `3/1/6` Kids bath 1 HVAC mode — manual_climate +- `3/1/7` Kids room 2 HVAC mode — manual_climate +- `3/1/8` Kids bath 2 HVAC mode — manual_climate +- `3/1/9` Guest WC HVAC mode — manual_climate +- `3/1/10` Laundry HVAC mode — manual_climate +- `3/1/11` Corridor GF HVAC mode — manual_climate +- `3/1/12` Corridor UF HVAC mode — manual_climate +- `3/1/21` Living room AC mode — manual_climate +- `3/1/22` Kitchen AC mode — manual_climate +- `3/1/23` Master bedroom AC mode — manual_climate +- `3/1/25` Kids room 1 AC mode — manual_climate +- `3/1/27` Kids room 2 AC mode — manual_climate +- `1/1/2` Living room cove dim — not_mapped +- `1/1/3` Living room RGBW accent wall dim — not_mapped +- `1/1/11` Kitchen worktop LED dim — not_mapped +- `1/1/12` Kitchen island pendants dim — not_mapped +- `1/1/20` Master bedroom ceiling dim — not_mapped +- `1/1/21` Master bedroom bedside left dim — not_mapped +- `1/1/22` Master bedroom bedside right dim — not_mapped +- `1/1/26` Master bath mirror CCT dim — not_mapped +- `1/1/30` Kids room 1 ceiling dim — not_mapped +- `1/1/31` Kids room 1 RGB accent dim — not_mapped +- `1/1/40` Kids room 2 ceiling dim — not_mapped +- `1/1/65` Staircase LED steps dim — not_mapped +- `2/3/1` Living room window blind slat — shutter_slat_unattached +- `2/3/10` Master bedroom blind slat — shutter_slat_unattached +- `2/6/10` Master bedroom blind slat status — shutter_slat_unattached +- `3/3/21` Living room AC fan speed — not_mapped +- `3/3/22` Kitchen AC fan speed — not_mapped +- `3/3/23` Master bedroom AC fan speed — not_mapped +- … and 15 more + + +## 4. Next steps + +1. Resolve 🔴 errors (missing DPT, empty names) in ETS first. +2. Add status/feedback GAs for every flagged command. +3. Re-run this report until errors are clear. +4. Generate ETS CSV/XML and HA YAML, commit to Git, then import into ETS and deploy to Home Assistant. diff --git a/examples/demo-home/ha-brain/README.md b/examples/demo-home/ha-brain/README.md new file mode 100644 index 0000000..e8ae057 --- /dev/null +++ b/examples/demo-home/ha-brain/README.md @@ -0,0 +1,108 @@ +# Demo House — the "brain" (Home Assistant smart layer) + +Sophisticated control logic for the synthetic [`demo-home.knxproj`](../demo-home.knxproj), +built the way the project's architecture demands: + +> **KNX (ETS)** is the body — group addresses are the muscles (commands) and nerves +> (status/sensors). It runs autonomously. **Home Assistant** is the *brain* — all the +> time/season/presence-aware logic and statistics live here, reading **real** KNX state. + +This is a **design demo** (no live bus). It is valid, deployable Home Assistant YAML built on +[official best practices](https://www.home-assistant.io/docs/) — native helpers +(`utility_meter`, `history_stats`, `statistics`, `derivative`, `trend`, `threshold`, `schedule`, +`integration`, `min_max`, `group`) wherever possible, trigger-based template sensors only for +genuinely multi-factor derived context, correct automation modes, `entity_id` (never +`device_id`), and `color_temp_kelvin` (mireds removed in 2026.3). + +--- + +## Architecture — 5 layers + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ L4 STATISTICS energy meters · runtime · degree-hours · KPIs │ 50_statistics +├────────────────────────────────────────────────────────────────────┤ +│ L3 LIGHTING circadian brightness/CCT · motion+presence · scenes │ 40_lighting_engine +│ L2 CLIMATE multi-factor setpoint · changeover · adaptive preheat│ 30_climate_engine +├────────────────────────────────────────────────────────────────────┤ +│ L1 HOME MODE Home/Away/Night/Eco/Guest/Vacation state machine │ 20_home_mode +├────────────────────────────────────────────────────────────────────┤ +│ L0 SENSES season · daypart · weekend · occupancy · comfort │ 10_context_sensors +│ L0 HELPERS input_*/schedule/timer/utility_meter │ 00_helpers +├────────────────────────────────────────────────────────────────────┤ +│ KNX I/O demo-home.knxproj — 239 group addresses │ (ETS layer) +└────────────────────────────────────────────────────────────────────┘ +``` + +Everything flows through **one source of truth**: `input_select.home_mode`. The senses feed the +state machine; the state machine + senses feed the climate/lighting engines; everything is +metered by the statistics layer. + +--- + +## "Depends on everything" — the input matrix + +| Input (sense) | Climate setpoint | Lighting | Mode machine | Statistics | +|---|:---:|:---:|:---:|:---:| +| **Time of day / daypart** (sun elevation) | ✅ profile | ✅ circadian b/CCT | ✅ night | ✅ tariff | +| **Day of week** (weekday/weekend schedule) | ✅ wake later | — | ✅ wake window | ✅ weekly | +| **Season** (month) + **outdoor temp** | ✅ trim + changeover | ✅ floor at night | ✅ heat/cool | ✅ degree-hours | +| **Presence/occupancy** (per zone, motion+hold) | ✅ setback | ✅ motion lights | ✅ Away/Home | ✅ dwell time | +| **Home mode** (Away/Night/Vacation/Eco) | ✅ deep setback / frost | ✅ all-off / sim | (is the state) | ✅ occupied-h | +| **Window contacts** | ✅ suspend heating | — | — | — | +| **CO₂ / air quality** (trend + threshold) | — | — | ✅ → Eco | ✅ exceedance | +| **Lux / darkness** (threshold) | — | ✅ only when dark | — | — | +| **Temperature slope** (derivative) | ✅ adaptive preheat | — | — | ✅ | + +The climate **target setpoint** (`sensor.living_target_setpoint`) is the centrepiece: a single +trigger-based template that folds **8 inputs** into one number — daypart profile → weekend hold → +occupancy setback → home-mode override → seasonal trim / cooling target → window interlock → +clamp. See [`30_climate_engine.yaml`](packages/30_climate_engine.yaml). + +--- + +## Showcase scenarios (for the demo) + +1. **Winter weekday morning.** `season=winter`, `hvac=Heating`. At ~06:40 the **adaptive pre-heat** + (`sensor.living_preheat_minutes = ΔT / rate × cold-penalty`) starts warming the Living room so + it hits comfort exactly when the work-week schedule opens at 07:00. Lights stay off (sun below + horizon handled by circadian + lux gate). +2. **Summer weekend afternoon.** Changeover flips to **Cooling** (outdoor > 22 °C); setpoint + becomes a cool target; weekend holds comfort later; blinds/scenes available. +3. **Everyone leaves.** No presence 20 min → **Away**: heating drops by the away-setback, every + light/switch turns off, the house meter keeps logging on the off-peak tariff. +4. **Bedtime.** Quiet-hours schedule → **Night**: `script.goodnight` ramps everything down, stair + LED to 8 % @ 2200 K, bedrooms cool for sleep. +5. **Poor air.** CO₂ trend rising + threshold high for 15 min → **Eco** + a ventilation nudge. +6. **Vacation.** Frost-guard setpoints + **presence simulation** randomly toggles rooms in the + evening so the house looks lived-in. + +--- + +## Files + +| File | Layer | Contents | +|---|---|---| +| `packages/00_helpers.yaml` | L0 | input_select/number/boolean, schedules, timers, utility_meter | +| `packages/10_context_sensors.yaml` | L0 | season, daypart, weekend, per-zone occupancy, comfort, statistics/derivative/trend/threshold | +| `packages/20_home_mode.yaml` | L1 | the state machine + seasonal heat/cool changeover | +| `packages/30_climate_engine.yaml` | L2 | KNX climate entities + multi-factor setpoint + pre-heat + interlocks | +| `packages/40_lighting_engine.yaml` | L3 | circadian sensors + adaptive motion/presence + scenes + vacation sim | +| `packages/50_statistics.yaml` | L4 | history_stats, integration→utility_meter degree-hours, KPIs, tariffs | + +## Deploy (in a live HA) + +```yaml +# configuration.yaml +homeassistant: + packages: !include_dir_named demo-home-ha/packages +``` + +Then create the matching KNX entities from the demo project with **nickol-knx-mcp** +(`generate_ha_package`) and reconcile entity names. Two climate zones (Living, Master bedroom) are +shown in full; the same per-zone pattern repeats for all 13 zones. + +> **Notes & honest caveats.** Entity IDs assume the slugified names produced by +> `generate_ha_package`. The two-climate-zone set is illustrative — production would template the +> remaining zones. In a live system, the `input_*`/`schedule`/`utility_meter` helpers are normally +> created via the UI/config-flow; here they are YAML so the whole brain is reviewable in one place. diff --git a/examples/demo-home/ha-brain/packages/00_helpers.yaml b/examples/demo-home/ha-brain/packages/00_helpers.yaml new file mode 100644 index 0000000..da8130d --- /dev/null +++ b/examples/demo-home/ha-brain/packages/00_helpers.yaml @@ -0,0 +1,72 @@ +# ============================================================================= +# Demo House — Layer 0: helpers (native HA constructs, not templates) +# The "knobs and registers" the brain reads/writes. Created here as YAML for the +# showcase; in a live system these would normally be UI/config-flow Helpers. +# ============================================================================= + +# ---- The home state machine (single source of truth for "what mode are we in") +input_select: + home_mode: + name: Home mode + icon: mdi:home-heart + options: [Home, Away, Night, Guest, Eco, Vacation] + initial: Home + + hvac_season_mode: + name: HVAC season changeover + icon: mdi:sun-snowflake-variant + options: [Heating, Cooling, Off, Auto] + initial: Auto + +# ---- Tunables exposed to the user (so logic is not hard-coded) +input_number: + comfort_temp_living: { name: Comfort setpoint · Living, min: 18, max: 25, step: 0.5, unit_of_measurement: "°C", initial: 21.5 } + comfort_temp_bedroom: { name: Comfort setpoint · Bedroom, min: 16, max: 24, step: 0.5, unit_of_measurement: "°C", initial: 20.5 } + eco_setback: { name: Eco setback ΔT, min: 0, max: 5, step: 0.5, unit_of_measurement: "°C", initial: 1.5 } + away_setback: { name: Away setback ΔT, min: 0, max: 6, step: 0.5, unit_of_measurement: "°C", initial: 3.0 } + frost_guard: { name: Frost-guard / vacation, min: 7, max: 16, step: 0.5, unit_of_measurement: "°C", initial: 12.0 } + cooling_changeover: { name: Changeover · cool above, min: 18, max: 28, step: 0.5, unit_of_measurement: "°C", initial: 22.0 } + heating_changeover: { name: Changeover · heat below, min: 8, max: 20, step: 0.5, unit_of_measurement: "°C", initial: 15.0 } + lux_dark_threshold: { name: "Lights: dark below (lux)", min: 5, max: 200, step: 5, unit_of_measurement: "lx", initial: 60 } + co2_high_threshold: { name: "CO2 high (ppm)", min: 600, max: 1600, step: 50, unit_of_measurement: "ppm", initial: 1000 } + preheat_rate_c_per_h: { name: Heating rate model (°C/h), min: 0.5, max: 4, step: 0.1, unit_of_measurement: "°C/h", initial: 1.4 } + outdoor_temp_sim: { name: "Outdoor temp (sim, if no weather)", min: -25, max: 40, step: 0.5, unit_of_measurement: "°C", initial: 8.0 } + +input_boolean: + vacation_presence_sim: { name: Vacation presence simulation, icon: mdi:account-clock } + guest_mode: { name: Guest mode, icon: mdi:account-group } + auto_light_enable: { name: Adaptive lighting enabled, icon: mdi:lightbulb-auto, initial: "on" } + +# ---- Schedules (native) — weekday work hours & quiet hours +schedule: + work_week: + name: Work week (occupied-expected) + monday: [{ from: "07:00:00", to: "22:30:00" }] + tuesday: [{ from: "07:00:00", to: "22:30:00" }] + wednesday: [{ from: "07:00:00", to: "22:30:00" }] + thursday: [{ from: "07:00:00", to: "22:30:00" }] + friday: [{ from: "07:00:00", to: "23:30:00" }] + saturday: [{ from: "08:30:00", to: "23:59:00" }] + sunday: [{ from: "08:30:00", to: "22:30:00" }] + quiet_hours: + name: Quiet / night hours + monday: [{ from: "23:00:00", to: "06:30:00" }] + sunday: [{ from: "23:00:00", to: "06:30:00" }] + +# ---- Per-zone occupancy hold timers (armed by motion, read by occupancy sensor) +timer: + occ_living: { name: Occupancy hold · Living, duration: "00:15:00", restore: true } + occ_bedroom: { name: Occupancy hold · Bedroom, duration: "00:20:00", restore: true } + occ_corridor: { name: Occupancy hold · Corridor, duration: "00:03:00", restore: true } + occ_staircase: { name: Occupancy hold · Staircase, duration: "00:02:00", restore: true } + occ_bath_m: { name: Occupancy hold · Master bath, duration: "00:10:00", restore: true } + +# ---- Energy meters (native utility_meter, multi-cycle + tariffs) +utility_meter: + lighting_daily: { source: sensor.lighting_energy, cycle: daily } + lighting_monthly: { source: sensor.lighting_energy, cycle: monthly } + hvac_daily: { source: sensor.hvac_energy, cycle: daily } + hvac_monthly: { source: sensor.hvac_energy, cycle: monthly } + hvac_seasonal: { source: sensor.hvac_energy, cycle: quarterly } + house_daily: { source: sensor.house_total_energy, cycle: daily, tariffs: [peak, offpeak] } + house_monthly: { source: sensor.house_total_energy, cycle: monthly, tariffs: [peak, offpeak] } diff --git a/examples/demo-home/ha-brain/packages/10_context_sensors.yaml b/examples/demo-home/ha-brain/packages/10_context_sensors.yaml new file mode 100644 index 0000000..1676ecb --- /dev/null +++ b/examples/demo-home/ha-brain/packages/10_context_sensors.yaml @@ -0,0 +1,180 @@ +# ============================================================================= +# Demo House — Layer 0b: context / "senses" +# Trigger-based template sensors for genuinely multi-factor derived context, +# plus native integrations (statistics / derivative / trend / threshold) where +# a built-in does the job better than a template. +# ============================================================================= + +template: + # -------- SEASON (meteorological, by month) -------- + - trigger: + - platform: homeassistant + event: start + - platform: time + at: "00:05:00" + - platform: time_pattern + hours: "/6" + sensor: + - name: Season + unique_id: demo_season + icon: mdi:sun-snowflake + state: > + {% set m = now().month %} + {% if m in [12,1,2] %}winter + {% elif m in [3,4,5] %}spring + {% elif m in [6,7,8] %}summer + {% else %}autumn{% endif %} + + # -------- DAYPART (sun elevation + clock, 8 phases) -------- + - trigger: + - platform: homeassistant + event: start + - platform: state + entity_id: sun.sun + - platform: time_pattern + minutes: "/10" + sensor: + - name: Daypart + unique_id: demo_daypart + icon: mdi:theme-light-dark + state: > + {% set elev = state_attr('sun.sun','elevation') | float(0) %} + {% set rising = is_state('sun.sun','above_horizon') and (state_attr('sun.sun','rising') | default(false)) %} + {% set h = now().hour %} + {% if h < 5 %}deep_night + {% elif elev < -6 and h < 12 %}dawn + {% elif elev < 6 and rising %}morning + {% elif elev >= 30 %}midday + {% elif elev >= 6 %}afternoon + {% elif elev >= -6 and not rising %}dusk + {% elif h < 23 %}evening + {% else %}night{% endif %} + + # -------- DAY TYPE -------- + - trigger: + - platform: homeassistant + event: start + - platform: time + at: "00:01:00" + - platform: time_pattern + hours: "/3" + binary_sensor: + - name: Is weekend + unique_id: demo_is_weekend + state: "{{ now().weekday() >= 5 }}" + + # -------- OUTDOOR TEMPERATURE (real weather if present, else sim knob) -------- + - sensor: + - name: Outdoor temperature + unique_id: demo_outdoor_temp + device_class: temperature + unit_of_measurement: "°C" + state: > + {% set w = state_attr('weather.forecast_home','temperature') %} + {{ w if w is not none else states('input_number.outdoor_temp_sim') }} + availability: "{{ states('input_number.outdoor_temp_sim') not in ['unknown','unavailable'] }}" + + # -------- PER-ZONE OCCUPANCY (presence + motion-hold timer + mode) -------- + # Where the demo has a presence GA we trust it; elsewhere we infer from the + # hold timer (armed by motion automations in 40_lighting_engine). + - binary_sensor: + - name: Living room occupied + unique_id: occ_living + device_class: occupancy + delay_off: "00:10:00" + state: > + {{ is_state('input_select.home_mode','Home') + and (is_state('timer.occ_living','active') + or is_state('binary_sensor.corridor_gf_presence','on') + or states('sensor.daypart') in ['evening','dusk']) }} + - name: Master bedroom occupied + unique_id: occ_bedroom + device_class: occupancy + delay_off: "00:15:00" + state: > + {{ is_state('timer.occ_bedroom','active') + or (is_state('input_select.home_mode','Night') + and is_state('binary_sensor.corridor_uf_presence','off')) }} + - name: Master bath occupied + unique_id: occ_bath_m + device_class: occupancy + delay_off: "00:03:00" + state: "{{ is_state('binary_sensor.master_bath_presence','on') or is_state('timer.occ_bath_m','active') }}" + + # -------- COMFORT INDEX per room (temp + humidity + CO2 → 0..100) -------- + - trigger: + - platform: state + entity_id: + - sensor.living_room_temperature + - sensor.master_bath_humidity + - sensor.master_bedroom_co2 + - platform: time_pattern + minutes: "/5" + sensor: + - name: Living room comfort index + unique_id: comfort_living + unit_of_measurement: "%" + icon: mdi:emoticon-happy-outline + state: > + {% set t = states('sensor.living_room_temperature') | float(21) %} + {% set rh = states('sensor.master_bath_humidity') | float(45) %} + {% set co2 = states('sensor.master_bedroom_co2') | float(600) %} + {# penalise deviation from 22°C / 45% RH / 600 ppm #} + {% set pt = 100 - (((t - 22) | abs) * 9) %} + {% set ph = 100 - (((rh - 45) | abs) * 1.5) %} + {% set pc = 100 - ([co2 - 600, 0] | max) / 12 %} + {{ [[ (pt*0.5 + ph*0.2 + pc*0.3), 0] | max, 100] | min | round(0) }} + attributes: + temperature: "{{ states('sensor.living_room_temperature') }}" + co2: "{{ states('sensor.master_bedroom_co2') }}" + +# -------- NATIVE integrations (better than templates here) -------- +sensor: + # rolling 24h climate statistics + - platform: statistics + name: Living temp 24h + entity_id: sensor.living_room_temperature + state_characteristic: mean + max_age: { hours: 24 } + - platform: statistics + name: Bedroom CO2 peak 24h + entity_id: sensor.master_bedroom_co2 + state_characteristic: value_max + max_age: { hours: 24 } + # temperature rate-of-change → drives adaptive pre-heat + - platform: derivative + name: Living temp slope + source: sensor.living_room_temperature + unit_time: h + time_window: "00:30:00" + # average house temperature across zones + - platform: min_max + name: House mean temperature + type: mean + entity_ids: + - sensor.living_room_temperature + - sensor.master_bedroom_temperature + - sensor.kids_room_1_temperature + - sensor.kids_room_2_temperature + +binary_sensor: + # CO2 rising fast (ventilation hint) + - platform: trend + sensors: + bedroom_co2_rising: + entity_id: sensor.master_bedroom_co2 + sample_duration: 900 + max_samples: 10 + min_gradient: 0.15 + device_class: problem + +# threshold helpers (built-in hysteresis) +threshold: + - name: CO2 high + entity_id: sensor.master_bedroom_co2 + upper: 1000 + hysteresis: 75 + - name: Living too dark + entity_id: sensor.living_room_brightness + lower: 60 + hysteresis: 15 diff --git a/examples/demo-home/ha-brain/packages/20_home_mode.yaml b/examples/demo-home/ha-brain/packages/20_home_mode.yaml new file mode 100644 index 0000000..3870e1b --- /dev/null +++ b/examples/demo-home/ha-brain/packages/20_home_mode.yaml @@ -0,0 +1,165 @@ +# ============================================================================= +# Demo House — Layer 1: the home-mode state machine (the "brain stem") +# Everything downstream (climate, light, stats) reads input_select.home_mode. +# Transitions are driven by presence + schedule + time + CO2 + manual. +# ============================================================================= + +# Aggregate presence: any motion/presence GA in the house (native group) +binary_sensor: + - platform: group + name: Anyone present + device_class: occupancy + entities: + - binary_sensor.corridor_gf_presence + - binary_sensor.corridor_uf_presence + - binary_sensor.staircase_presence + - binary_sensor.master_bath_presence + - binary_sensor.kids_bath_1_presence + - binary_sensor.kids_bath_2_presence + - binary_sensor.guest_wc_presence + - binary_sensor.laundry_presence + +automation: + # ---- HOME → AWAY : no presence for 20 min (unless on vacation) ---- + - id: mode_to_away + alias: "Mode · go Away when empty" + mode: restart + trigger: + - platform: state + entity_id: binary_sensor.anyone_present + to: "off" + for: "00:20:00" + condition: + - condition: not + conditions: + - condition: state + entity_id: input_select.home_mode + state: [Away, Vacation] + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Away } + + # ---- AWAY/ECO → HOME : presence returns ---- + - id: mode_to_home + alias: "Mode · return Home on presence" + mode: single + trigger: + - platform: state + entity_id: binary_sensor.anyone_present + to: "on" + condition: + - condition: state + entity_id: input_select.home_mode + state: [Away, Eco] + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: "{{ 'Guest' if is_state('input_boolean.guest_mode','on') else 'Home' }}" } + + # ---- HOME → NIGHT : quiet hours begin and we are home ---- + - id: mode_to_night + alias: "Mode · go Night at quiet hours" + mode: single + trigger: + - platform: state + entity_id: schedule.quiet_hours + to: "on" + condition: + - condition: state + entity_id: input_select.home_mode + state: [Home, Guest, Eco] + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Night } + + # ---- NIGHT → HOME : morning wake (work-week window opens + movement) ---- + - id: mode_night_to_home + alias: "Mode · wake from Night" + mode: single + trigger: + - platform: state + entity_id: schedule.work_week + to: "on" + - platform: state + entity_id: binary_sensor.corridor_uf_presence + to: "on" + condition: + - condition: state + entity_id: input_select.home_mode + state: Night + - condition: time + after: "05:30:00" + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Home } + + # ---- → ECO : sustained high CO2 while present (air-quality driven) ---- + - id: mode_to_eco_air + alias: "Mode · Eco on poor air" + mode: single + trigger: + - platform: state + entity_id: binary_sensor.co2_high + to: "on" + for: "00:15:00" + condition: + - condition: state + entity_id: input_select.home_mode + state: Home + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Eco } + - service: notify.persistent_notification + data: + title: Air quality + message: "CO₂ high — switched to Eco; consider ventilating." + + # ---- VACATION mirror from boolean ---- + - id: mode_vacation_sync + alias: "Mode · Vacation from toggle" + mode: single + trigger: + - platform: state + entity_id: input_boolean.vacation_presence_sim + to: "on" + action: + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Vacation } + + # ---- SEASON CHANGEOVER : decide heat vs cool by season + outdoor temp ---- + - id: hvac_changeover + alias: "HVAC · seasonal heat/cool changeover" + mode: restart + trigger: + - platform: state + entity_id: [sensor.season, sensor.outdoor_temperature] + - platform: homeassistant + event: start + - platform: time_pattern + minutes: "/30" + action: + - variables: + season: "{{ states('sensor.season') }}" + outdoor: "{{ states('sensor.outdoor_temperature') | float(10) }}" + cool_above: "{{ states('input_number.cooling_changeover') | float(22) }}" + heat_below: "{{ states('input_number.heating_changeover') | float(15) }}" + - choose: + - conditions: "{{ season == 'winter' or outdoor < heat_below }}" + sequence: + - service: input_select.select_option + target: { entity_id: input_select.hvac_season_mode } + data: { option: Heating } + - conditions: "{{ season == 'summer' or outdoor > cool_above }}" + sequence: + - service: input_select.select_option + target: { entity_id: input_select.hvac_season_mode } + data: { option: Cooling } + default: + - service: input_select.select_option + target: { entity_id: input_select.hvac_season_mode } + data: { option: Off } diff --git a/examples/demo-home/ha-brain/packages/30_climate_engine.yaml b/examples/demo-home/ha-brain/packages/30_climate_engine.yaml new file mode 100644 index 0000000..bfee391 --- /dev/null +++ b/examples/demo-home/ha-brain/packages/30_climate_engine.yaml @@ -0,0 +1,204 @@ +# ============================================================================= +# Demo House — Layer 2: CLIMATE ENGINE (the showpiece) +# KNX climate entities (mapped to demo .knxproj GAs) + a multi-factor target +# setpoint that depends on: season · daypart · day-type · per-zone occupancy · +# home-mode · window contacts · outdoor temp · CO2 · comfort. Plus adaptive +# pre-heat (rate model) and window/air interlocks. Two zones shown in full; +# the same pattern repeats for all 13 zones. +# ============================================================================= + +knx: + climate: + - name: "Living room HVAC" + temperature_address: "4/0/1" # current room temp (9.001) + target_temperature_address: "3/0/1" # setpoint command (9.001) + target_temperature_state_address: "3/4/1" # setpoint status + operation_mode_address: "3/1/1" # HVAC mode (20.102) + operation_mode_state_address: "3/1/1" + command_value_state_address: "3/6/1" # floor valve % (5.001) + min_temp: 12 + max_temp: 28 + - name: "Master bedroom HVAC" + temperature_address: "4/0/3" + target_temperature_address: "3/0/3" + target_temperature_state_address: "3/4/3" + operation_mode_address: "3/1/3" + operation_mode_state_address: "3/1/3" + command_value_state_address: "3/6/3" + min_temp: 12 + max_temp: 28 + +template: + # ----------------------------------------------------------------------- # + # THE multi-factor target setpoint. Reads "everything". # + # ----------------------------------------------------------------------- # + - trigger: + - platform: state + entity_id: + - input_select.home_mode + - input_select.hvac_season_mode + - sensor.daypart + - sensor.season + - binary_sensor.is_weekend + - binary_sensor.living_room_occupied + - binary_sensor.living_room_window_contact + - sensor.outdoor_temperature + - platform: time_pattern + minutes: "/5" + - platform: homeassistant + event: start + sensor: + - name: Living target setpoint + unique_id: living_target_setpoint + device_class: temperature + unit_of_measurement: "°C" + state: > + {% set comfort = states('input_number.comfort_temp_living') | float(21.5) %} + {% set mode = states('input_select.home_mode') %} + {% set hvac = states('input_select.hvac_season_mode') %} + {% set season = states('sensor.season') %} + {% set dp = states('sensor.daypart') %} + {% set weekend = is_state('binary_sensor.is_weekend','on') %} + {% set occ = is_state('binary_sensor.living_room_occupied','on') %} + {% set window = is_state('binary_sensor.living_room_window_contact','on') %} + {% set eco = states('input_number.eco_setback') | float(1.5) %} + {% set away = states('input_number.away_setback') | float(3.0) %} + {% set frost = states('input_number.frost_guard') | float(12.0) %} + + {# --- 1. daypart base profile (heating reference) --- #} + {% set prof = { + 'deep_night': comfort - 3.0, 'dawn': comfort - 1.5, 'morning': comfort, + 'midday': comfort - 0.5, 'afternoon': comfort - 0.5, 'evening': comfort + 0.5, + 'dusk': comfort, 'night': comfort - 2.5 } %} + {% set t = prof.get(dp, comfort) %} + + {# --- 2. weekend holds morning comfort longer --- #} + {% if weekend and dp in ['morning','midday'] %}{% set t = comfort + 0.3 %}{% endif %} + + {# --- 3. occupancy setback --- #} + {% if not occ %}{% set t = t - eco %}{% endif %} + + {# --- 4. home-mode overrides --- #} + {% if mode == 'Away' %}{% set t = [t - away, 16.5] | max %} + {% elif mode == 'Vacation' %}{% set t = frost %} + {% elif mode == 'Eco' %}{% set t = t - eco %} + {% elif mode == 'Night' %}{% set t = t - 0.5 %}{% endif %} + + {# --- 5. seasonal trim (winter slightly warmer, summer becomes cool target) --- #} + {% if season == 'winter' %}{% set t = t + 0.3 %}{% endif %} + {% if hvac == 'Cooling' %}{% set t = [comfort + 2.0, 24.0] | max %}{% endif %} + + {# --- 6. window open → suspend (frost guard) --- #} + {% if window %}{% set t = frost %}{% endif %} + + {{ [[t, 12.0] | max, 28.0] | min | round(1) }} + attributes: + basis: "{{ states('sensor.daypart') }} / {{ states('input_select.home_mode') }} / {{ states('sensor.season') }}" + cooling: "{{ is_state('input_select.hvac_season_mode','Cooling') }}" + + - name: Bedroom target setpoint + unique_id: bedroom_target_setpoint + device_class: temperature + unit_of_measurement: "°C" + state: > + {% set comfort = states('input_number.comfort_temp_bedroom') | float(20.5) %} + {% set mode = states('input_select.home_mode') %} + {% set dp = states('sensor.daypart') %} + {% set occ = is_state('binary_sensor.master_bedroom_occupied','on') %} + {% set frost = states('input_number.frost_guard') | float(12.0) %} + {# bedrooms: cool for sleep, warm for wake #} + {% set prof = { 'deep_night': comfort - 2.0, 'dawn': comfort + 0.5, 'morning': comfort + 0.5, + 'night': comfort - 1.5 } %} + {% set t = prof.get(dp, comfort - 1.0) %} + {% if mode == 'Vacation' %}{% set t = frost %} + {% elif mode == 'Away' %}{% set t = comfort - 3.0 %} + {% elif mode == 'Night' and occ %}{% set t = comfort - 1.0 %}{% endif %} + {{ [[t, 12.0] | max, 26.0] | min | round(1) }} + + # ----------------------------------------------------------------------- # + # Adaptive pre-heat lead time = ΔT / heating-rate, stretched when cold. # + # ----------------------------------------------------------------------- # + - sensor: + - name: Living preheat minutes + unique_id: living_preheat_minutes + unit_of_measurement: min + icon: mdi:fan-clock + state: > + {% set cur = states('sensor.living_room_temperature') | float(20) %} + {% set tgt = states('sensor.living_target_setpoint') | float(21) %} + {% set rate = states('input_number.preheat_rate_c_per_h') | float(1.4) %} + {% set outdoor = states('sensor.outdoor_temperature') | float(8) %} + {% set delta = [tgt - cur, 0] | max %} + {# colder outside ⇒ slower warm-up ⇒ longer lead #} + {% set penalty = [1 + (15 - outdoor) / 40, 1] | max %} + {{ (delta / rate * 60 * penalty) | round(0) }} + +automation: + # ---- Push computed setpoint to the KNX climate entity on any change ---- + - id: climate_apply_living + alias: "Climate · apply Living setpoint" + mode: restart + trigger: + - platform: state + entity_id: sensor.living_target_setpoint + condition: + - condition: template + value_template: "{{ states('sensor.living_target_setpoint') | float(0) > 0 }}" + action: + - service: climate.set_temperature + target: { entity_id: climate.living_room_hvac } + data: + temperature: "{{ states('sensor.living_target_setpoint') | float }}" + + - id: climate_apply_bedroom + alias: "Climate · apply Bedroom setpoint" + mode: restart + trigger: + - platform: state + entity_id: sensor.bedroom_target_setpoint + action: + - service: climate.set_temperature + target: { entity_id: climate.master_bedroom_hvac } + data: + temperature: "{{ states('sensor.bedroom_target_setpoint') | float }}" + + # ---- Window interlock: open window pauses heating immediately ---- + - id: climate_window_interlock + alias: "Climate · window-open interlock" + mode: parallel + trigger: + - platform: state + entity_id: binary_sensor.living_room_window_contact + to: "on" + action: + - service: climate.set_temperature + target: { entity_id: climate.living_room_hvac } + data: { temperature: "{{ states('input_number.frost_guard') | float(12) }}" } + - service: notify.persistent_notification + data: { message: "Living window open — heating paused." } + + # ---- Adaptive pre-heat: warm up before the work-week window opens ---- + - id: climate_preheat + alias: "Climate · adaptive pre-heat" + mode: single + trigger: + - platform: time_pattern + minutes: "/10" + condition: + - condition: state + entity_id: input_select.hvac_season_mode + state: Heating + - condition: state + entity_id: input_select.home_mode + state: [Night, Away] + # within the lead window before 07:00 occupancy + - condition: template + value_template: > + {% set lead = states('sensor.living_preheat_minutes') | float(0) %} + {% set wake = today_at('07:00') %} + {% set start = wake - timedelta(minutes=lead) %} + {{ start <= now() < wake and lead > 0 }} + action: + - service: climate.set_temperature + target: { entity_id: climate.living_room_hvac } + data: { temperature: "{{ states('input_number.comfort_temp_living') | float(21.5) }}" } diff --git a/examples/demo-home/ha-brain/packages/40_lighting_engine.yaml b/examples/demo-home/ha-brain/packages/40_lighting_engine.yaml new file mode 100644 index 0000000..08ad245 --- /dev/null +++ b/examples/demo-home/ha-brain/packages/40_lighting_engine.yaml @@ -0,0 +1,255 @@ +# ============================================================================= +# Demo House — Layer 3: LIGHTING ENGINE +# Circadian (human-centric) brightness + colour temperature, adaptive motion/ +# presence lighting (only when dark / allowed by mode), scene orchestration and +# vacation presence simulation. Uses color_temp_kelvin (mireds removed in 2026.3). +# ============================================================================= + +template: + - trigger: + - platform: state + entity_id: [sun.sun, sensor.daypart, input_select.home_mode] + - platform: time_pattern + minutes: "/10" + - platform: homeassistant + event: start + sensor: + # Brightness 1..100 %, follows the sun, floors low at night. + - name: Circadian brightness + unique_id: circadian_brightness + unit_of_measurement: "%" + icon: mdi:brightness-percent + state: > + {% set elev = state_attr('sun.sun','elevation') | float(0) %} + {% set mode = states('input_select.home_mode') %} + {% if mode in ['Night'] %}{{ 8 }} + {% elif mode == 'Vacation' %}{{ 0 }} + {% else %} + {% set b = 100 if elev >= 25 else (40 + elev * 2.4) %} + {{ [[b, 12] | max, 100] | min | round(0) }} + {% endif %} + # Colour temperature: warm at night (2200K) → cool at midday (5000K). + - name: Circadian color temp + unique_id: circadian_color_temp_kelvin + unit_of_measurement: K + icon: mdi:temperature-kelvin + state: > + {% set elev = state_attr('sun.sun','elevation') | float(0) %} + {% set dp = states('sensor.daypart') %} + {% if dp in ['deep_night','night'] %}{{ 2200 }} + {% elif dp in ['evening','dusk'] %}{{ 2700 }} + {% else %}{{ [[2700 + elev * 80, 2700] | max, 5000] | min | round(0) }}{% endif %} + +script: + # Morning ramp: simulate a sunrise on bedroom + corridor over ~10 min. + morning_ramp: + alias: "Scene · Morning ramp" + mode: restart + sequence: + - repeat: + count: 10 + sequence: + - service: light.turn_on + target: { entity_id: light.master_bedroom_ceiling } + data: + brightness_pct: "{{ repeat.index * 10 }}" + color_temp_kelvin: "{{ 2200 + repeat.index * 200 }}" + - delay: "00:01:00" + + goodnight: + alias: "Scene · Goodnight" + mode: single + sequence: + - service: light.turn_off + target: + entity_id: + - light.living_room_cove + - light.living_room_rgbw_accent_wall + - light.kitchen_island_pendants + - service: switch.turn_off + target: { entity_id: switch.living_room_ceiling_spots } + - delay: "00:00:05" + - service: light.turn_on + target: { entity_id: light.staircase_led_steps } + data: { brightness_pct: 10, color_temp_kelvin: 2200 } + - service: input_select.select_option + target: { entity_id: input_select.home_mode } + data: { option: Night } + + movie_mode: + alias: "Scene · Movie (Living)" + mode: single + sequence: + - service: switch.turn_off + target: { entity_id: switch.living_room_ceiling_spots } + - service: light.turn_on + target: { entity_id: light.living_room_cove } + data: { brightness_pct: 15, color_temp_kelvin: 2300 } + - service: light.turn_on + target: { entity_id: light.living_room_rgbw_accent_wall } + data: { brightness_pct: 40, rgbw_color: [60, 10, 120, 0] } + - service: cover.set_cover_position + target: { entity_id: cover.living_room_window_blind } + data: { position: 0 } + +automation: + # ---- Adaptive motion lights (corridors) — only when dark; dim at night ---- + - id: light_corridor_gf + alias: "Light · Corridor GF (motion+adaptive)" + mode: restart + trigger: + - platform: state + entity_id: binary_sensor.corridor_gf_presence + to: "on" + condition: + - condition: state + entity_id: input_boolean.auto_light_enable + state: "on" + - condition: or + conditions: + - condition: state + entity_id: binary_sensor.living_too_dark + state: "on" + - condition: state + entity_id: input_select.home_mode + state: Night + action: + - service: timer.start + target: { entity_id: timer.occ_corridor } + - service: switch.turn_on + target: { entity_id: switch.corridor_gf_ceiling } + - id: light_corridor_gf_off + alias: "Light · Corridor GF off after hold" + mode: single + trigger: + - platform: event + event_type: timer.finished + event_data: { entity_id: timer.occ_corridor } + action: + - service: switch.turn_off + target: { entity_id: switch.corridor_gf_ceiling } + + # ---- Staircase: adaptive dimmable, very dim at night ---- + - id: light_staircase + alias: "Light · Staircase (motion+adaptive)" + mode: restart + trigger: + - platform: state + entity_id: binary_sensor.staircase_presence + to: "on" + condition: + - condition: state + entity_id: input_boolean.auto_light_enable + state: "on" + action: + - service: timer.start + target: { entity_id: timer.occ_staircase } + - service: light.turn_on + target: { entity_id: light.staircase_led_steps } + data: + brightness_pct: > + {{ 8 if is_state('input_select.home_mode','Night') + else states('sensor.circadian_brightness') | int(60) }} + color_temp_kelvin: "{{ states('sensor.circadian_color_temp_kelvin') | int(2700) }}" + - id: light_staircase_off + alias: "Light · Staircase off after hold" + mode: single + trigger: + - platform: event + event_type: timer.finished + event_data: { entity_id: timer.occ_staircase } + action: + - service: light.turn_off + target: { entity_id: light.staircase_led_steps } + + # ---- Master bath: presence light, fan-free, off after hold ---- + - id: light_bath_master + alias: "Light · Master bath (presence)" + mode: restart + trigger: + - platform: state + entity_id: binary_sensor.master_bath_presence + to: "on" + action: + - service: timer.start + target: { entity_id: timer.occ_bath_m } + - service: switch.turn_on + target: { entity_id: switch.master_bath_ceiling } + - id: light_bath_master_off + alias: "Light · Master bath off" + mode: single + trigger: + - platform: event + event_type: timer.finished + event_data: { entity_id: timer.occ_bath_m } + action: + - service: switch.turn_off + target: { entity_id: switch.master_bath_ceiling } + + # ---- Living room: when occupied in the evening, set cove to circadian ---- + - id: light_living_circadian + alias: "Light · Living circadian when occupied" + mode: restart + trigger: + - platform: state + entity_id: binary_sensor.living_room_occupied + to: "on" + - platform: state + entity_id: sensor.circadian_brightness + condition: + - condition: state + entity_id: binary_sensor.living_room_occupied + state: "on" + - condition: state + entity_id: input_select.home_mode + state: [Home, Guest] + - condition: state + entity_id: binary_sensor.living_too_dark + state: "on" + action: + - service: light.turn_on + target: { entity_id: light.living_room_cove } + data: + brightness_pct: "{{ states('sensor.circadian_brightness') | int(60) }}" + + # ---- Whole-home reactions to mode changes ---- + - id: light_mode_reactions + alias: "Light · react to home mode" + mode: queued + trigger: + - platform: state + entity_id: input_select.home_mode + action: + - choose: + - conditions: "{{ trigger.to_state.state == 'Away' }}" + sequence: + - service: light.turn_off + target: { entity_id: all } + - service: switch.turn_off + target: { entity_id: all } + - conditions: "{{ trigger.to_state.state == 'Night' }}" + sequence: + - service: script.goodnight + + # ---- Vacation presence simulation: random rooms on/off in the evening ---- + - id: light_vacation_sim + alias: "Light · Vacation simulation" + mode: single + trigger: + - platform: time_pattern + minutes: "/20" + condition: + - condition: state + entity_id: input_select.home_mode + state: Vacation + - condition: state + entity_id: sensor.daypart + state: [evening, dusk, night] + action: + - service: "{{ ['switch.turn_on','switch.turn_off'] | random }}" + target: + entity_id: > + {{ [ 'switch.living_room_ceiling_spots', + 'switch.kitchen_main_ceiling', + 'light.master_bedroom_ceiling', + 'switch.corridor_gf_ceiling' ] | random }} diff --git a/examples/demo-home/ha-brain/packages/50_statistics.yaml b/examples/demo-home/ha-brain/packages/50_statistics.yaml new file mode 100644 index 0000000..02bf572 --- /dev/null +++ b/examples/demo-home/ha-brain/packages/50_statistics.yaml @@ -0,0 +1,135 @@ +# ============================================================================= +# Demo House — Layer 4: STATISTICS & ANALYTICS +# utility_meter (in 00_helpers) + history_stats runtime counters + derived KPIs. +# "Collect statistics depending on everything": per-room runtime, presence dwell, +# heating load, comfort, energy-per-occupied-hour, seasonal degree-hours. +# ============================================================================= + +# Heating-active flags (valve % > 5) feed runtime stats +template: + - binary_sensor: + - name: Living heating active + unique_id: living_heating_active + device_class: heat + state: "{{ states('sensor.living_room_floor_valve_status') | float(0) > 5 }}" + - name: Bedroom heating active + unique_id: bedroom_heating_active + device_class: heat + state: "{{ states('sensor.master_bedroom_floor_valve_status') | float(0) > 5 }}" + + # Instantaneous heating demand (°C) — integrated natively below into degree-hours + - sensor: + - name: Heating demand + unique_id: heating_demand + unit_of_measurement: "°C" + icon: mdi:thermometer-lines + state: > + {% set outdoor = states('sensor.outdoor_temperature') | float(8) %} + {{ [18 - outdoor, 0] | max if is_state('input_select.hvac_season_mode','Heating') else 0 }} + + # ---- Derived KPIs ---- + - trigger: + - platform: time_pattern + minutes: "/15" + - platform: homeassistant + event: start + sensor: + # Energy efficiency: HVAC kWh today per occupied-hour today + - name: Energy per occupied hour + unique_id: energy_per_occupied_hour + unit_of_measurement: "kWh/h" + icon: mdi:lightning-bolt-outline + state: > + {% set kwh = states('sensor.hvac_daily') | float(0) %} + {% set occh = states('sensor.house_occupied_hours_today') | float(0) %} + {{ (kwh / occh) | round(3) if occh > 0.2 else 0 }} + + # House comfort score = mean of room comfort indices + - name: House comfort score + unique_id: house_comfort_score + unit_of_measurement: "%" + icon: mdi:gauge + state: > + {{ states('sensor.living_room_comfort_index') | float(70) | round(0) }} + +# ---- Native integration of heating demand → degree-hours, reset daily ---- +sensor: + - platform: integration + name: Heating degree hours raw + source: sensor.heating_demand + unit_time: h + method: left + + # ---- Runtime counters (native history_stats) ---- + - platform: history_stats + name: Living lights on today + entity_id: light.living_room_cove + state: "on" + type: time + start: "{{ today_at('00:00') }}" + end: "{{ now() }}" + + - platform: history_stats + name: Living heating hours today + entity_id: binary_sensor.living_heating_active + state: "on" + type: time + start: "{{ today_at('00:00') }}" + end: "{{ now() }}" + + - platform: history_stats + name: Bedroom heating hours today + entity_id: binary_sensor.bedroom_heating_active + state: "on" + type: time + start: "{{ today_at('00:00') }}" + end: "{{ now() }}" + + - platform: history_stats + name: House occupied hours today + entity_id: binary_sensor.anyone_present + state: "on" + type: time + start: "{{ today_at('00:00') }}" + end: "{{ now() }}" + + - platform: history_stats + name: Master bath usage count today + entity_id: binary_sensor.master_bath_presence + state: "on" + type: count + start: "{{ today_at('00:00') }}" + end: "{{ now() }}" + + # weekly heating runtime (trend across the week) + - platform: history_stats + name: Living heating hours this week + entity_id: binary_sensor.living_heating_active + state: "on" + type: time + start: "{{ now().replace(hour=0, minute=0, second=0) - timedelta(days=now().weekday()) }}" + end: "{{ now() }}" + +automation: + # ---- Tariff switching for the house meter (peak 07–23, offpeak otherwise) ---- + - id: stats_tariff_peak + alias: "Stats · tariff → peak" + mode: single + trigger: [{ platform: time, at: "07:00:00" }] + action: + - service: utility_meter.select_tariff + target: { entity_id: [select.house_daily, select.house_monthly] } + data: { tariff: peak } + - id: stats_tariff_offpeak + alias: "Stats · tariff → offpeak" + mode: single + trigger: [{ platform: time, at: "23:00:00" }] + action: + - service: utility_meter.select_tariff + target: { entity_id: [select.house_daily, select.house_monthly] } + data: { tariff: offpeak } + +# utility_meter gives the degree-hours a clean daily/seasonal reset (native) +utility_meter: + heating_degree_hours_daily: { source: sensor.heating_degree_hours_raw, cycle: daily } + heating_degree_hours_seasonal: { source: sensor.heating_degree_hours_raw, cycle: quarterly }