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:
Nikolay1
2026-07-16 11:21:18 +02:00
co-authored by Claude Opus 4.8
parent f0d1afdce2
commit bf7eba8898
12 changed files with 446 additions and 13 deletions
+29
View File
@@ -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)
+4 -1
View File
@@ -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
+20 -2
View File
@@ -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
+68 -4
View File
@@ -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",
}
+46 -2
View File
@@ -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."
+19 -1
View File
@@ -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."
+20
View File
@@ -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