bussard ha-config: Home Assistant KNX config generation¶
bussard ha-config generates the Home Assistant KNX integration
YAML (the knx: platform schema) straight from the bussard model, so the KNX-as-code
repo stays the single source of truth for Home Assistant too. This document explains how
entities are derived and how to tune the result with ha.yaml; the command's flags are
in reference.md.
The output is a complete knx: document with a generated-by header and a summary
footer:
# generated by bussard ha-config — do not hand-edit; use ha.yaml overrides
knx:
switch:
- name: Kitchen Light Switch
address: 0/0/6
state_address: 0/0/7
cover:
- name: Living Room Blind
move_long_address: 0/1/0
move_short_address: 0/1/1
position_address: 0/1/2
position_state_address: 0/1/3
device_class: blind
climate:
- name: Living Room Operating Mode
temperature_address: 0/2/0
command_value_state_address: 0/2/2
operation_mode_address: 0/2/3
operation_mode_state_address: 0/2/4
sensor:
- name: Outside Temperature
state_address: 2/0/0
type: temperature
# summary
# entities: 6 switch, 1 light, 3 cover, 2 climate, 9 sensor, 4 binary_sensor
# group addresses: 30/33 mapped
# unmapped: 3 group addresses (no-dpt: 1, dpt 1: 1, dpt 20: 1)
# note: climate 'Living Room': forced-mode (Zwang, DPT 20.102) recognised but unwired (no HA KNX schema key); left unmapped
Output is deterministic: entities are sorted by platform, then name, then primary group
address, and every optional field is omitted when empty, so re-running produces a
minimal diff. Copy the knx: block into your Home Assistant configuration (or
!include the file).
How entities are derived¶
bussard has devices, com-objects and group addresses (GAs); Home Assistant wants
entities. Derivation reasons per device: it walks each device's com-objects, resolves
each to the GA it sends or listens to (from links.yaml), groups related objects into a
cluster (by channel where the model records one, otherwise one object per cluster), and
emits one entity per cluster.
Roles come from the com-object flags:
- an object with the W flag receives commands, so its listened GA is a command address (the GA Home Assistant sends to);
- an object with the T flag transmits status, so its sent GA is a state address (the GA Home Assistant reads).
Mapping rules¶
| Model shape | Home Assistant entity |
|---|---|
1.008 up/down [+ 1.007 step/stop, 5.001 position ± status] |
cover (move_long / move_short / position / position_state) |
1.001 switch (has a W command object) |
switch (or light) |
1.001 switch + 5.001 brightness on the same channel |
light with brightness_address |
9.xxx value a sensor sends |
sensor with type per DPT sub |
other numeric value (7/12/13/14/5) a sensor sends |
sensor with the matching type |
1.xxx a sensor sends (T flag): presence, contacts, alarms |
binary_sensor |
a per-room heating cluster anchored by a 20.102 operation-mode command |
climate (see below) |
device_class is inferred from the GA/device name and DPT sub where it is unambiguous:
- cover:
Raffstore/Jalousie/blind→blind,Rolllade/shutter→shutter,Markise/awning→awning; - binary_sensor: DPT
1.005(alarm) →problem,1.019→window, and name hints formotion/window/door/moisture/safety.
Sensor type per DPT sub: 9.001 → temperature, 9.002 →
temperature_difference_2byte, 9.004 → illuminance, 9.005 → wind_speed_ms,
9.007 → humidity, 9.008 → ppm, 9.024 → power_2byte, and so on (unknown 9.x
falls back to 2byte_float). The _2byte types matter: HA's plain power and
temperature types decode four bytes, so a 2-byte KNX value mapped to them reads wrong.
Only an object that publishes a value becomes a sensor: one with the Transmit flag, or a
readable one that answers a GroupValueRead. A write-only command input (a setpoint an
actuator receives) is skipped, because a read-only HA sensor on it would never update.
Climate (room heating)¶
A heating cluster is not one device. The operation mode and setpoint come from a room
controller, the valve position from a heating actuator, and the room temperature often
from a third device (a presence detector or a thermostat). Because the GAs are spread
across devices, climate is the one place derivation correlates by room name instead of
by device channel: it reads GA names from groups.yaml, strips a known German heating
suffix, and groups the remainder by the room prefix.
Recognised suffixes (DPT in brackets): Isttemperatur (9.001, room temp),
Betriebsmodus Vorgabe (20.102, operation-mode command), Betriebsmodus Vorgabe Status
(20.102, its status), Betriebsmodus Zwang (20.102, forced mode),
Sollwertverschiebung and its Status (9.002, setpoint shift), Stellgröße
Heizen/Kühlen (5.001, valve position), and Soll-Temperatur aktuell (9.001, current
target). A suffix only matches when the DPT matches too, so a plain 9.001 temperature
elsewhere is never pulled into a climate cluster.
The anchor rule: a room becomes a climate entity only if it has an operation-mode
command (Betriebsmodus Vorgabe, 20.102). A room with just a temperature sensor, or
just a valve, is not controllable and is left to the sensor pass. This keeps stray
9.001 temperatures (and rooms like the utility room that only report a temperature)
from fabricating empty climate entities.
For a centrally heated installation, the flow temperature steers the whole house and a room is heated or not as a unit, so there is no per-room target temperature to set. Derivation reflects that:
- the operation mode is the control:
operation_mode_address(plusoperation_mode_state_addresswhere the room has aVorgabe Status). In Home Assistant this surfaces as preset modes: comfort = room heated, standby / economy / frost protection = off; - the room temperature (
temperature_address) and the valve position (command_value_state_address, from the 5.001Stellgröße) are wired as read-only telemetry; - setpoint shift (
setpoint_shift_address/..._state_address) and target temperature (target_temperature_state_address) are deliberately not wired, because emitting them would invite a temperature change the central system does not honour. TheSollwertverschiebungandSoll-Temperatur aktuellGAs fall through to the sensor pass as read-only values instead.
If your installation does do per-room setpoint control, re-enable setpoint wiring per
room with an ha.yaml merge on the climate entity's primary GA (the operation-mode
command). The mapped keys match the Home Assistant KNX climate schema exactly:
temperature_address, operation_mode_address, operation_mode_state_address,
command_value_state_address, target_temperature_state_address,
setpoint_shift_address, setpoint_shift_state_address, setpoint_shift_mode.
Forced mode (Zwang): the Betriebsmodus Zwang (20.102) GA overrides the operation mode,
but the Home Assistant KNX climate schema has no key for a forced-mode address. Rather
than guess a mapping, bussard leaves it unwired, counts it in the # unmapped: dpt-20
total, and emits a # note: line naming the room, so nothing is silently dropped.
What does not map¶
A GA that fits no rule is never silently dropped: it is counted in the # unmapped:
footer, grouped by DPT main number, so you can see the coverage. Typical unmapped GAs
are forced HVAC-mode datapoints (20.102 Zwang), relative dimming (3.007),
date/time (10.001/11.001), and base setpoints the model does not tie to a
controllable entity. Recognised-but-unwired GAs (such as the climate forced mode) also
get a # note: line so the reason is explicit.
Deduplication¶
The same GA is often touched by several devices: an actuator that switches it and the
push-buttons that command it. Actuators are derived first and claim their GAs; a later
sensor pass only maps GAs no actuator claimed, and any entity whose GAs are already
spoken for is dropped. The result is one entity per GA. Colliding entity names get their
primary GA appended (Name (1/2/3)) so names stay unique per platform, as Home
Assistant requires.
Overrides: ha.yaml¶
Put an optional ha.yaml next to your model (in the --dir) to tune the result. It is
parsed strictly, rejecting unknown fields and duplicate keys, so a typo is an error, not
a silent no-op.
# ha.yaml: overrides for `bussard ha-config`.
global:
# A plain switchable actuator becomes a `switch` (default) or a `light`.
default_platform_for_switches: switch # or: light
# Group addresses (or GA prefixes ending in `/`) to skip entirely. An excluded
# GA produces no entity and is not counted as unmapped.
exclude:
- "0/0/1" # a single GA
- "8/" # every GA under main group 8
- "4/1/" # every GA under 4/1
# Per-entity overrides, keyed by the entity's *primary* group address (the GA
# Home Assistant sends to for switch/light/cover, or reads for sensor/
# binary_sensor).
entities:
"1/0/1":
platform: light # promote a derived switch to a light (or vice-versa)
name: "Kitchen ceiling" # override the derived name
device_class: outlet # set/override the Home Assistant device_class
"3/1/5":
merge: ["3/1/6"] # merge extra GAs onto this entity
"0/3/2": # a climate entity, keyed by its operation-mode GA
name: "Büro heating" # rename it
merge: ["0/3/8"] # e.g. re-enable a setpoint-shift status GA
Precedence¶
- Exclusions win over everything: an excluded GA never produces an entity.
- An explicit per-entity
platformoverrides the heuristic (only theswitch↔lightpromotion is offered; cover/sensor/binary_sensor/climate are structural). - An explicit
name/device_classoverrides the derived value. For aclimateentity, key the override by its operation-mode command GA;platformanddevice_classdo not apply, butnameandmergedo. mergeadds the listed GAs to the entity's extra addresses. On aclimateentity a merged GA fills the first free state slot in the ordertemperature_address,target_temperature_state_address,operation_mode_state_address,setpoint_shift_state_address,command_value_state_address(command slots are never merge targets).
Quality check against your model¶
Run it and read the footer: the entity counts and the # unmapped: line tell you how
much of the model mapped and what remains. Spot-check a few entities; adjust with
ha.yaml. On the reference model (424 GAs), 393 GAs map (39 switch, 5 light, 26 cover,
10 climate, 135 sensor, 60 binary_sensor) with the remaining 31 reported as unmapped.
Ten of those are the 20.102 Zwang forced-mode GAs (one per heating room), each named
in a # note: line; the rest are relative dimming (3.007), date/time, and a few
unhandled one-offs.