Skip to content

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/blindblind, Rolllade/shuttershutter, Markise/awningawning;
  • binary_sensor: DPT 1.005 (alarm) → problem, 1.019window, and name hints for motion / window / door / moisture / safety.

Sensor type per DPT sub: 9.001temperature, 9.002temperature_difference_2byte, 9.004illuminance, 9.005wind_speed_ms, 9.007humidity, 9.008ppm, 9.024power_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 (plus operation_mode_state_address where the room has a Vorgabe 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.001 Stellgröß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. The Sollwertverschiebung and Soll-Temperatur aktuell GAs 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

  1. Exclusions win over everything: an excluded GA never produces an entity.
  2. An explicit per-entity platform overrides the heuristic (only the switchlight promotion is offered; cover/sensor/binary_sensor/climate are structural).
  3. An explicit name / device_class overrides the derived value. For a climate entity, key the override by its operation-mode command GA; platform and device_class do not apply, but name and merge do.
  4. merge adds the listed GAs to the entity's extra addresses. On a climate entity a merged GA fills the first free state slot in the order temperature_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.