mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
room-library: Theben-benchmark corrections (dogfood on 5 official ETS examples)
Ran KNX Association-ecosystem vendor (Theben) 5 official ETS example .knxproj through our QA. Findings folded back into the product: - repair.py _infer_dpt: relative dimming (Brighter/darker, heller/dunkler, светлее/темнее) now infers 3.007 DPT_Control_Dimming instead of falling to 1.001; explicit '% / value' stays 5.001; scene checked before the broad value branch. DPTs verified against XKNX/xknx via deepwiki. - room_library.py: new reusable blocks distilled from vendor patterns — shutter_venetian (slat + slat-status + wind/frost safety-lock), climate_floor modulating actuating-value + status, ventilation (airing stage + CO2 + humidity), lighting_dali, multisensor_air, central scene/all-off/all-on. - project.py classifier: airing/Lüftung/проветривание -> hvac (was unknown). - analyze.py: safety_input_no_status INFO exemption for write-only wind/frost lock inputs (Theben central wind-safety pattern). - SCHEMA.md: new function rows; automation_intents implementation: knx|external with safety_related => knx rule; DPT-tolerance note (missing-DPT hard error applies to generated output; ETS4 object-derived DPT on import is legitimate). - templates: ventilation slot (bathroom/kitchen), opt-in venetian + wind/frost safety intent (living/bedroom), new house-level central.yaml. - tests: +test_theben_corrections.py (10); golden bedroom 26->28 GA (climate actuating-value). pytest 20 passed (pre-existing unrelated test_protocol collection error untouched). senior-code-audit: APPROVED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f0d1afdce2
commit
bf7eba8898
@@ -188,6 +188,22 @@ def _is_scene(ga: GARecord) -> bool:
|
||||
return ga.dpt_main in (17, 18) or ga.category == "scene"
|
||||
|
||||
|
||||
# A wind/frost safety-lock is a WRITE-ONLY 1-bit input the shutter actuator listens
|
||||
# to (the KNX safety logic drives it); it has no feedback object, so a missing
|
||||
# status is expected, not a defect. Gated to 1-bit so a "frost protection setpoint"
|
||||
# (9.x) never matches. This is the Theben-benchmark safety_related lesson: the lock
|
||||
# stays in KNX, never in network-dependent Home Assistant.
|
||||
_SAFETY_INPUT_TOKENS = (
|
||||
"safety lock", "блокировк", "sperre", "verriegel", "wind lock",
|
||||
"wind/frost", "ветров", "мороз", "frost lock", "windsperre", "frostsperre",
|
||||
)
|
||||
|
||||
|
||||
def _is_safety_input(ga: GARecord) -> bool:
|
||||
low = (ga.name or "").lower()
|
||||
return ga.dpt_main == 1 and any(t in low for t in _SAFETY_INPUT_TOKENS)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Sub-DPT sanity (A1): a name implies a specific DPT sub-type. Flag when the
|
||||
# main matches but the sub is wrong (9.001 temp vs 9.004 lux), or — for strong
|
||||
@@ -255,6 +271,19 @@ def detect_missing_status(project: LoadedProject) -> list[dict[str, Any]]:
|
||||
continue
|
||||
if ga.dpt_main is None:
|
||||
continue # DPT issue handled elsewhere
|
||||
# A wind/frost safety-lock is a write-only INPUT — recognise it up front so
|
||||
# it is never (spuriously) paired to a position status, and never demands a
|
||||
# feedback object of its own (safety_related logic stays in KNX).
|
||||
if _is_safety_input(ga):
|
||||
findings.append(_finding(
|
||||
SEVERITY_INFO, "safety_input_no_status", addr,
|
||||
f"Safety/lock input '{ga.name}' has no status GA — expected: a "
|
||||
"wind/frost safety-lock is a write-only input to the actuator, it has "
|
||||
"no state to read back. Keep this protection in KNX (safety_related), "
|
||||
"never in network-dependent Home Assistant.",
|
||||
name=ga.name, dpt=ga.dpt, category=ga.category,
|
||||
))
|
||||
continue
|
||||
# prefer same-main-group candidates, fall back to whole project
|
||||
same_main = [s for s in status_gas if s.main == ga.main]
|
||||
match = find_status(ga, same_main) or find_status(ga, status_gas)
|
||||
|
||||
@@ -58,7 +58,10 @@ _DOMAIN_TERMS: dict[str, list[str]] = {
|
||||
"hvac": ["hvac", "climate", "thermostat", "heat", "cool", "ac", "a/c", "aircon",
|
||||
"air con", "air-con", "conditioner", "ventilation", "radiator", "boiler",
|
||||
"underfloor", "fancoil", "fan coil", "fan-coil", "fan", "valve", "setpoint",
|
||||
"климат", "отопл", "тёпл", "тепл", "конвектор", "вентил",
|
||||
# ventilation / airing (F5 fix): "Airing step" was 'unknown' — an airing
|
||||
# stage is HVAC. lüftung/lüften carry the DE umlaut and an ASCII fallback.
|
||||
"airing", "lüftung", "lüften", "luftung", "luften",
|
||||
"климат", "отопл", "тёпл", "тепл", "конвектор", "вентил", "проветр",
|
||||
"клапан", "кондиц", "котёл", "радиатор", "фанкойл", "уставк",
|
||||
"а/с", "сплит", "вытяжк"],
|
||||
# NOTE: no bare "температур"/"temperature" term — a temperature GA takes its
|
||||
|
||||
@@ -46,10 +46,28 @@ def _infer_dpt(ga: Any) -> str:
|
||||
return "1.010"
|
||||
if "позиц" in low or "position" in low or "stellung" in low:
|
||||
return "5.001"
|
||||
if any(k in low for k in ("диммир", "dimming", "яркост", "brightness")):
|
||||
return "5.001"
|
||||
# Scene BEFORE the broad "value" branch so a "Scene value"/"Szene Wert" GA
|
||||
# maps to 18.001, not 5.001 (a Theben "Dimming value %" carries no scene token,
|
||||
# so it still falls through to 5.001 below).
|
||||
if "сцен" in low or "scene" in low or "szene" in low:
|
||||
return "18.001"
|
||||
# Explicit ABSOLUTE brightness/dimming VALUE (percent / "value" / "dimming
|
||||
# value" / vendor Dimmwert) -> 5.001 scaling. Checked BEFORE relative dimming
|
||||
# so a Theben "Dimming value % …" GA stays 5.001 and is not flipped to 3.007.
|
||||
if any(k in low for k in ("%", "значение", "value", "dimmwert", "helligkeitswert")):
|
||||
return "5.001"
|
||||
# RELATIVE dimming (brighter/darker step control) -> 3.007 DPT_Control_Dimming
|
||||
# (4-bit relative dimming). Verified against the KNX DPT catalogue
|
||||
# (XKNX/xknx via deepwiki: 3.007 == control bit + 3-bit step_code). This is the
|
||||
# dogfood bug fix: "Brighter/ darker …" used to fall through to the 1.001
|
||||
# default, contradicting the object-derived family "3" xknxproject reports.
|
||||
if any(k in low for k in ("brighter", "darker", "heller", "dunkler",
|
||||
"светлее", "темнее", "ярче", "тусклее")):
|
||||
return "3.007"
|
||||
# Generic dimming/brightness keyword with no explicit value and no relative
|
||||
# token -> absolute brightness scaling is the safer default.
|
||||
if any(k in low for k in ("диммир", "dimming", "яркост", "brightness")):
|
||||
return "5.001"
|
||||
if ga.category == "shutter":
|
||||
return "1.008"
|
||||
# switch-like boolean is the safest default
|
||||
|
||||
@@ -79,9 +79,11 @@ MIDDLE_ROLE: dict[str, dict[str, int]] = {
|
||||
"lighting": {"cmd_onoff": 0, "dimming": 1, "brightness_value": 2,
|
||||
"status_onoff": 3, "brightness_status": 4},
|
||||
"shutter": {"move": 0, "stop": 1, "position": 2, "slat": 3,
|
||||
"position_status": 4},
|
||||
"position_status": 4, "slat_status": 5, "safety_lock": 6},
|
||||
# fan(3) is a reserved role slot (no template function uses it yet); airing(6)
|
||||
# carries the ventilation stage, valve(7) the modulating actuating-value.
|
||||
"hvac": {"onoff": 0, "mode": 1, "setpoint": 2, "fan": 3, "status": 4,
|
||||
"actual_temp": 5},
|
||||
"actual_temp": 5, "airing": 6, "valve": 7},
|
||||
"sensor": {"presence": 0, "illuminance": 1, "climate": 2},
|
||||
"central": {"scene": 0, "command": 1},
|
||||
}
|
||||
@@ -96,13 +98,17 @@ MIDDLE_LABELS: dict[str, dict[int, dict[str, str]]] = {
|
||||
1: {"ru": "Стоп", "en": "Stop"},
|
||||
2: {"ru": "Позиция", "en": "Position"},
|
||||
3: {"ru": "Ламели", "en": "Slats"},
|
||||
4: {"ru": "Статус позиции", "en": "Position status"}},
|
||||
4: {"ru": "Статус позиции", "en": "Position status"},
|
||||
5: {"ru": "Статус ламелей", "en": "Slat status"},
|
||||
6: {"ru": "Блокировка (защита)", "en": "Safety lock"}},
|
||||
"hvac": {0: {"ru": "Вкл/Выкл", "en": "On/Off"},
|
||||
1: {"ru": "Режим", "en": "Mode"},
|
||||
2: {"ru": "Уставка", "en": "Setpoint"},
|
||||
3: {"ru": "Вентилятор", "en": "Fan"},
|
||||
4: {"ru": "Статусы", "en": "Statuses"},
|
||||
5: {"ru": "Температура факт", "en": "Actual temperature"}},
|
||||
5: {"ru": "Температура факт", "en": "Actual temperature"},
|
||||
6: {"ru": "Проветривание", "en": "Ventilation"},
|
||||
7: {"ru": "Клапан", "en": "Valve"}},
|
||||
"sensor": {0: {"ru": "Присутствие/Движение", "en": "Presence/Motion"},
|
||||
1: {"ru": "Освещённость", "en": "Illuminance"},
|
||||
2: {"ru": "Климат в комнатах", "en": "Room climate"}},
|
||||
@@ -150,12 +156,35 @@ FUNCTION_OBJECTS: dict[str, list[ObjSpec]] = {
|
||||
ObjSpec("onoff_status", "статус", "status", 1, 11, "status", "lighting", "status_onoff"),
|
||||
ObjSpec("brightness_status", "яркость статус", "brightness status", 5, 1, "status", "lighting", "brightness_status"),
|
||||
],
|
||||
"lighting_dali": [
|
||||
# DALI is a downstream bus behind a gateway; from the KNX side a DALI group
|
||||
# exposes the SAME objects as a dimmer channel (the DALI specifics live
|
||||
# inside the gateway). No invented DPTs — a 1:1 clone of lighting_dimmer.
|
||||
ObjSpec("onoff", "вкл/выкл", "on/off", 1, 1, "command", "lighting", "cmd_onoff"),
|
||||
ObjSpec("dimming", "диммирование", "dimming", 3, 7, "command", "lighting", "dimming"),
|
||||
ObjSpec("brightness", "яркость", "brightness", 5, 1, "command", "lighting", "brightness_value"),
|
||||
ObjSpec("onoff_status", "статус", "status", 1, 11, "status", "lighting", "status_onoff"),
|
||||
ObjSpec("brightness_status", "яркость статус", "brightness status", 5, 1, "status", "lighting", "brightness_status"),
|
||||
],
|
||||
"shutter": [
|
||||
ObjSpec("updown", "вверх/вниз", "up/down", 1, 8, "command", "shutter", "move"),
|
||||
ObjSpec("stop", "стоп", "stop", 1, 10, "command", "shutter", "stop"),
|
||||
ObjSpec("position", "позиция", "position", 5, 1, "command", "shutter", "position"),
|
||||
ObjSpec("position_status", "позиция статус", "position status", 5, 1, "status", "shutter", "position_status"),
|
||||
],
|
||||
# Venetian / façade blind: a roller shutter PLUS slat-angle control (the roller
|
||||
# `shutter` models only up/down/position). Slat command + slat status close the
|
||||
# feedback gap for the angle; the wind/frost safety-lock is a WRITE-ONLY input
|
||||
# to the actuator (no status object exists for it — see analyze._is_safety_input).
|
||||
"shutter_venetian": [
|
||||
ObjSpec("updown", "вверх/вниз", "up/down", 1, 8, "command", "shutter", "move"),
|
||||
ObjSpec("stop", "стоп", "stop", 1, 10, "command", "shutter", "stop"),
|
||||
ObjSpec("position", "позиция", "position", 5, 1, "command", "shutter", "position"),
|
||||
ObjSpec("position_status", "позиция статус", "position status", 5, 1, "status", "shutter", "position_status"),
|
||||
ObjSpec("slat", "ламели", "slat", 5, 1, "command", "shutter", "slat"),
|
||||
ObjSpec("slat_status", "ламели статус", "slat status", 5, 1, "status", "shutter", "slat_status"),
|
||||
ObjSpec("safety_lock", "блокировка защита", "safety lock", 1, 1, "command", "shutter", "safety_lock"),
|
||||
],
|
||||
"climate_floor": [
|
||||
ObjSpec("onoff", "вкл/выкл", "on/off", 1, 1, "command", "hvac", "onoff"),
|
||||
ObjSpec("setpoint", "уставка", "setpoint", 9, 1, "command", "hvac", "setpoint"),
|
||||
@@ -164,18 +193,53 @@ FUNCTION_OBJECTS: dict[str, list[ObjSpec]] = {
|
||||
ObjSpec("setpoint_status", "уставка статус", "setpoint status", 9, 1, "status", "hvac", "status"),
|
||||
ObjSpec("mode_status", "режим статус", "mode status", 20, 102, "status", "hvac", "status"),
|
||||
ObjSpec("actual_temp", "температура факт", "actual temperature", 9, 1, "sensor", "hvac", "actual_temp"),
|
||||
# Modulating valve: a continuous actuating-value (%) command + its status.
|
||||
# A real underfloor/radiator loop is driven continuously, not just on/off,
|
||||
# and without a status HA cannot show the real valve opening.
|
||||
ObjSpec("actuating_value", "уровень клапана", "actuating value", 5, 1, "command", "hvac", "valve"),
|
||||
ObjSpec("actuating_value_status", "уровень клапана статус", "actuating value status", 5, 1, "status", "hvac", "status"),
|
||||
],
|
||||
# Ventilation / airing: a discrete airing STAGE (1-byte counter, DPT 5.010 —
|
||||
# a device/integrator choice, see 01-dpt-derivation.md) plus its feedback, and
|
||||
# the air-quality sensors that drive it (CO₂ 9.008, humidity 9.007).
|
||||
"ventilation": [
|
||||
ObjSpec("airing_stage", "ступень проветривания", "airing stage", 5, 10, "command", "hvac", "airing"),
|
||||
ObjSpec("airing_stage_status", "ступень проветривания статус", "airing stage status", 5, 10, "status", "hvac", "status"),
|
||||
ObjSpec("co2", "CO2", "CO2", 9, 8, "sensor", "sensor", "climate"),
|
||||
ObjSpec("humidity", "влажность", "humidity", 9, 7, "sensor", "sensor", "climate"),
|
||||
],
|
||||
"presence": [
|
||||
ObjSpec("occupancy", "присутствие", "occupancy", 1, 18, "sensor", "sensor", "presence"),
|
||||
ObjSpec("illuminance", "освещённость", "illuminance", 9, 4, "sensor", "sensor", "illuminance"),
|
||||
],
|
||||
# Air-quality multisensor: self-reporting temperature + humidity + CO₂ (no
|
||||
# command, so no status pair — these are read-only measurements).
|
||||
"multisensor_air": [
|
||||
ObjSpec("temperature", "температура", "temperature", 9, 1, "sensor", "sensor", "climate"),
|
||||
ObjSpec("humidity", "влажность", "humidity", 9, 7, "sensor", "sensor", "climate"),
|
||||
ObjSpec("co2", "CO2", "CO2", 9, 8, "sensor", "sensor", "climate"),
|
||||
],
|
||||
# House-level central functions (main 0). A scene control (18.001, no status —
|
||||
# a scene has no single state) and per-domain all-off / all-on broadcasts
|
||||
# (1.001, central macros — no single state to read back).
|
||||
"central_scene": [
|
||||
ObjSpec("scene", "сцена", "scene", 18, 1, "command", "central", "scene"),
|
||||
],
|
||||
"central_all_off": [
|
||||
ObjSpec("all_off", "всё выкл", "all off", 1, 1, "command", "central", "command"),
|
||||
],
|
||||
"central_all_on": [
|
||||
ObjSpec("all_on", "всё вкл", "all on", 1, 1, "command", "central", "command"),
|
||||
],
|
||||
}
|
||||
|
||||
# Function type -> candidate device recipe (device_library) for the BOM proposal.
|
||||
BOM_RECIPE: dict[str, str] = {
|
||||
"lighting_switch": "switch_output",
|
||||
"lighting_dimmer": "dimmer_channel",
|
||||
"lighting_dali": "dali_gateway_group",
|
||||
"shutter": "shutter_channel",
|
||||
"shutter_venetian": "shutter_channel",
|
||||
"climate_floor": "floor_heating_zone",
|
||||
"presence": "presence_detector",
|
||||
}
|
||||
|
||||
@@ -66,13 +66,50 @@ with a canonical DPT and a command/status role:
|
||||
|----------|----------------------|
|
||||
| `lighting_switch` | on/off `1.001` · status `1.011` |
|
||||
| `lighting_dimmer` | on/off `1.001` · dimming `3.007` · brightness `5.001` · status `1.011` · brightness-status `5.001` |
|
||||
| `lighting_dali` | 1:1 clone of `lighting_dimmer` (DALI specifics live inside the gateway; BOM recipe `dali_gateway_group`) |
|
||||
| `shutter` | up/down `1.008` · stop `1.010` · position `5.001` · position-status `5.001` |
|
||||
| `climate_floor` | on/off `1.001` · setpoint `9.001` · mode `20.102` · +3 statuses · actual-temp `9.001` |
|
||||
| `shutter_venetian` | `shutter` + slat `5.001` · slat-status `5.001` + safety-lock `1.001` (write-only input — see below) |
|
||||
| `climate_floor` | on/off `1.001` · setpoint `9.001` · mode `20.102` · +3 statuses · actual-temp `9.001` · actuating-value `5.001` · actuating-value-status `5.001` |
|
||||
| `ventilation` | airing-stage `5.010` · airing-stage-status `5.010` · CO₂ `9.008` · humidity `9.007` |
|
||||
| `multisensor_air` | temperature `9.001` · humidity `9.007` · CO₂ `9.008` (self-reporting, no command → no status pair) |
|
||||
| `presence` | occupancy `1.018` · illuminance `9.004` |
|
||||
| `central_scene` | scene `18.001` (no status — a scene has no single state) |
|
||||
| `central_all_off` / `central_all_on` | all-off / all-on `1.001` (central macro — no single state to read back) |
|
||||
|
||||
Every controllable command gets its status object — the generated project passes
|
||||
`check_missing_status`, `check_dpt`, `check_naming` and `check_policy` cleanly.
|
||||
|
||||
Three object classes legitimately have **no status pair**, and the linters treat
|
||||
them as expected (INFO, not a warning), so a house using them is still clean:
|
||||
|
||||
* **safety-lock** input (`shutter_venetian`): a wind/frost lock is a *write-only*
|
||||
1-bit input the actuator listens to — there is no feedback object
|
||||
(`analyze._is_safety_input` → `safety_input_no_status`);
|
||||
* **scene control** (`central_scene`, `17.x`/`18.x`): a scene recalls a preset,
|
||||
it has no single state (`scene_no_status`);
|
||||
* **central macros** (`central_all_off`/`_all_on`, "all off"/"all on"/"всё"): a
|
||||
broadcast fans out to many actuators (`central_macro_no_status`).
|
||||
|
||||
### Status/feedback tokens (pairing)
|
||||
|
||||
The pairing engine treats **`Feedback` / `Rückmeldung` / `статус` / `state` /
|
||||
`status`** (and `fb`, `rueck`, `rück`) as first-class status tokens — a vendor
|
||||
that names its only status object just "Feedback" (e.g. Theben) still pairs. The
|
||||
token list lives in `project.STATUS_KEYWORDS` and `pairing._FN_STATUS_TOKENS`.
|
||||
|
||||
### DPT tolerance (generation vs import)
|
||||
|
||||
"**Missing DPT = hard error**" (`check_dpt`) is a rule about **our generated
|
||||
output**: the Room Library always emits fully DPT-typed GAs, so a missing DPT in
|
||||
our own output is a real bug. It is *not* a judgement on a third-party **ETS4
|
||||
import**: in ETS4-era projects the DPT legitimately lives on the device's
|
||||
communication object, not on the GA, so `xknxproject` derives it (or reports
|
||||
`missing_dpt` where the object link is unresolved). Auto-completing that DPT is a
|
||||
feature, not a defect flag on the vendor — see
|
||||
[`docs/roadmap/room-library/theben-benchmark/01-dpt-derivation.md`](../../../docs/roadmap/room-library/theben-benchmark/01-dpt-derivation.md).
|
||||
Reserve GAs are intentionally left **without a DPT** — that is a convention (a
|
||||
spare slot), not a KNX-required value.
|
||||
|
||||
## Address allocation (default taxonomy)
|
||||
|
||||
`main` = function domain, `middle` = role/sub-function, `sub` = sequential:
|
||||
@@ -96,9 +133,16 @@ automation_intents:
|
||||
- intent: presence_lights_off
|
||||
description: "Turn lights off when unoccupied."
|
||||
criticality: convenience # or safety_related
|
||||
implementation: external
|
||||
implementation: external # external (HA) | knx
|
||||
```
|
||||
|
||||
`implementation` is `external` (Home Assistant) or `knx` (the autonomous KNX
|
||||
program). **Rule: `criticality: safety_related` ⇒ `implementation: knx`.**
|
||||
Safety protection (wind/frost blind retract, leak→water-shutoff, fire) must run
|
||||
autonomously in KNX and must never depend on a network-reachable HA. The template
|
||||
lays the GA **hook** (e.g. the `shutter_venetian` safety-lock input); the
|
||||
integrator tunes the **thresholds** (wind m/s, frost °C, timers) in KNX.
|
||||
|
||||
## Not in R1
|
||||
|
||||
Docking into an existing project (allocation lockfile, drift detection), exact
|
||||
|
||||
@@ -42,6 +42,12 @@ slots:
|
||||
basic: {enabled: false}
|
||||
comfort: {enabled: true, function: climate_floor, multiplicity: {fixed: 1}}
|
||||
|
||||
- slot_id: ventilation
|
||||
labels: {ru: вентиляция, en: ventilation}
|
||||
presets:
|
||||
basic: {enabled: false}
|
||||
comfort: {enabled: true, function: ventilation, multiplicity: {fixed: 1}}
|
||||
|
||||
automation_intents:
|
||||
- intent: humidity_ventilation
|
||||
description: "Run ventilation above a humidity threshold."
|
||||
|
||||
@@ -15,6 +15,9 @@ parameters:
|
||||
windows:
|
||||
default: 2
|
||||
provenance: {source: preset_rule, note: "typical master bedroom has 2 windows"}
|
||||
venetian_windows:
|
||||
default: 0
|
||||
provenance: {source: preset_rule, note: "opt-in: set >0 where blinds have slats (façade/venetian)"}
|
||||
area_m2:
|
||||
default: 14
|
||||
role: hint
|
||||
@@ -42,6 +45,15 @@ slots:
|
||||
basic: {enabled: true, function: shutter, multiplicity: {param: windows}}
|
||||
comfort: {enabled: true, function: shutter, multiplicity: {param: windows}}
|
||||
|
||||
# Venetian / façade blinds (slat control + wind/frost safety-lock). Opt-in:
|
||||
# emits nothing until venetian_windows > 0, so a plain roller-shutter house is
|
||||
# unchanged. Where windows carry slats, set venetian_windows.
|
||||
- slot_id: venetian_blind
|
||||
labels: {ru: венецианские жалюзи, en: venetian blind}
|
||||
presets:
|
||||
basic: {enabled: true, function: shutter_venetian, multiplicity: {param: venetian_windows}}
|
||||
comfort: {enabled: true, function: shutter_venetian, multiplicity: {param: venetian_windows}}
|
||||
|
||||
- slot_id: presence
|
||||
labels: {ru: датчик, en: sensor}
|
||||
presets:
|
||||
@@ -55,9 +67,15 @@ slots:
|
||||
comfort: {enabled: true, function: climate_floor, multiplicity: {fixed: 1}}
|
||||
|
||||
# Non-executable declarations only (metadata). The engine does NOT run these —
|
||||
# safety-related logic stays external (Home Assistant / the KNX program).
|
||||
# safety-related logic stays in KNX (autonomous), convenience logic external.
|
||||
automation_intents:
|
||||
- intent: presence_lights_off
|
||||
description: "Turn bedroom lights off when unoccupied."
|
||||
criticality: convenience
|
||||
implementation: external
|
||||
# Safety-related: wind/frost protection MUST stay in KNX, never in HA — the
|
||||
# actuator listens on the shutter_venetian safety-lock input.
|
||||
- intent: wind_frost_blind_protection
|
||||
description: "Retract/lock blinds on high wind or frost (via the safety-lock input)."
|
||||
criticality: safety_related
|
||||
implementation: knx
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Room Template — house-level Central functions (Центральные функции)
|
||||
# NOT per-room: composed once for the whole house. A small set of scene controls
|
||||
# (18.001) plus per-domain all-off / all-on broadcasts (1.001). SCHEMA reserves
|
||||
# main group 0 Central for exactly this. Keep the scene set small — panels and the
|
||||
# KNX program assemble the detailed logic; the template only lays the GA hooks.
|
||||
schema_version: 1
|
||||
slot_id: central
|
||||
labels:
|
||||
ru: Центральные функции
|
||||
en: Central
|
||||
|
||||
parameters:
|
||||
scenes:
|
||||
default: 4
|
||||
provenance: {source: preset_rule, note: "keep small — a few house scenes; the rest live on panels"}
|
||||
area_m2:
|
||||
default: 0
|
||||
role: hint
|
||||
provenance: {source: preset_default, user_overridden: false, note: "house-level, not an area"}
|
||||
|
||||
slots:
|
||||
- slot_id: house_scenes
|
||||
labels: {ru: сцены, en: scenes}
|
||||
presets:
|
||||
basic: {enabled: true, function: central_scene, multiplicity: {param: scenes}}
|
||||
comfort: {enabled: true, function: central_scene, multiplicity: {param: scenes}}
|
||||
|
||||
- slot_id: lighting_all_off
|
||||
labels: {ru: освещение всё выкл, en: lighting all off}
|
||||
presets:
|
||||
basic: {enabled: true, function: central_all_off, multiplicity: {fixed: 1}}
|
||||
comfort: {enabled: true, function: central_all_off, multiplicity: {fixed: 1}}
|
||||
|
||||
- slot_id: lighting_all_on
|
||||
labels: {ru: освещение всё вкл, en: lighting all on}
|
||||
presets:
|
||||
basic: {enabled: false}
|
||||
comfort: {enabled: true, function: central_all_on, multiplicity: {fixed: 1}}
|
||||
|
||||
- slot_id: shutters_all_off
|
||||
labels: {ru: шторы всё выкл, en: shutters all off}
|
||||
presets:
|
||||
basic: {enabled: true, function: central_all_off, multiplicity: {fixed: 1}}
|
||||
comfort: {enabled: true, function: central_all_off, multiplicity: {fixed: 1}}
|
||||
|
||||
# Metadata only. Central all-off / scene recall is convenience logic assembled on
|
||||
# panels / in the KNX program; safety-related protection lives per-actuator in KNX.
|
||||
automation_intents:
|
||||
- intent: house_all_off
|
||||
description: "One-touch all-off across lighting and shutters."
|
||||
criticality: convenience
|
||||
implementation: knx
|
||||
@@ -42,6 +42,12 @@ slots:
|
||||
basic: {enabled: false}
|
||||
comfort: {enabled: true, function: presence, multiplicity: {fixed: 1}}
|
||||
|
||||
- slot_id: ventilation
|
||||
labels: {ru: вентиляция, en: ventilation}
|
||||
presets:
|
||||
basic: {enabled: false}
|
||||
comfort: {enabled: true, function: ventilation, multiplicity: {fixed: 1}}
|
||||
|
||||
automation_intents:
|
||||
- intent: presence_lights_off
|
||||
description: "Turn kitchen lights off when unoccupied."
|
||||
|
||||
@@ -12,6 +12,9 @@ parameters:
|
||||
windows:
|
||||
default: 3
|
||||
provenance: {source: preset_rule}
|
||||
venetian_windows:
|
||||
default: 0
|
||||
provenance: {source: preset_rule, note: "opt-in: set >0 where blinds have slats (façade/venetian)"}
|
||||
area_m2:
|
||||
default: 28
|
||||
role: hint
|
||||
@@ -37,6 +40,16 @@ slots:
|
||||
basic: {enabled: true, function: shutter, multiplicity: {param: windows}}
|
||||
comfort: {enabled: true, function: shutter, multiplicity: {param: windows}}
|
||||
|
||||
# Venetian / façade blinds (slat control + wind/frost safety-lock). Opt-in:
|
||||
# emits nothing until venetian_windows > 0, so a plain roller-shutter house is
|
||||
# unchanged. Where windows carry slats, set venetian_windows and drop the roller
|
||||
# count in `windows` accordingly.
|
||||
- slot_id: venetian_blind
|
||||
labels: {ru: венецианские жалюзи, en: venetian blind}
|
||||
presets:
|
||||
basic: {enabled: true, function: shutter_venetian, multiplicity: {param: venetian_windows}}
|
||||
comfort: {enabled: true, function: shutter_venetian, multiplicity: {param: venetian_windows}}
|
||||
|
||||
- slot_id: presence
|
||||
labels: {ru: датчик, en: sensor}
|
||||
presets:
|
||||
@@ -54,3 +67,10 @@ automation_intents:
|
||||
description: "Recall living-room lighting scenes (movie/evening/bright)."
|
||||
criticality: convenience
|
||||
implementation: external
|
||||
# Safety-related: wind/frost retracts the blinds. This protection MUST live in
|
||||
# KNX (autonomous, network-independent), never in Home Assistant — the actuator
|
||||
# listens on the shutter_venetian safety-lock input.
|
||||
- intent: wind_frost_blind_protection
|
||||
description: "Retract/lock blinds on high wind or frost (via the safety-lock input)."
|
||||
criticality: safety_related
|
||||
implementation: knx
|
||||
|
||||
Reference in New Issue
Block a user