Loading...
E.G.

Move KNX Switches From the Home Assistant UI to knx.yaml

October 14, 2026
Part 6 of 6Series: KNX in Home AssistantAll parts
  1. 01KNX Motion-Sensor Automations in Home Assistant
  2. 02KNX Thermostat Preset Modes in Home Assistant
  3. 03KNX Thermostats Drop to 'Standby' After Every Home Assistant Restart — A Startup Automation Fix
  4. 04Window Open, Heating Off: Auto-Switch Each KNX Thermostat to Frost Protection in Home Assistant
  5. 05Decoupling a KNX Alarm from the JUNG Panel in Home Assistant
  6. 06Move KNX Switches From the Home Assistant UI to knx.yaml
Table of Contents

For about a year I had 33 KNX switches in Home Assistant that existed only in the UI. I had clicked them together in the KNX integration's entity editor when the lighting on two floors, the garage door and two auxiliary switches were wired up, and it was quick. Home Assistant keeps those switches in a JSON file under .storage, which it rewrites itself and which is not in my Git repository. The thermostats, covers and sensors of the same bus have lived in knx.yaml since the beginning, next to the startup automation and the window logic. In September I moved 31 of them over and dropped two that I no longer wanted as switches. I spent a week preparing it, and running it took about ten minutes.

Why I deleted the UI entries before loading the YAML

The KNX integration reads both the UI store and knx.yaml at load time and creates an entity for each definition. A switch defined in both places exists twice, with two entity ids, and nothing warns you. Adding the YAML first and deleting the UI entries afterwards would therefore have created 31 duplicates. I deleted the UI entities first and loaded the YAML after that. In my run the config check of the new file also came after the deletion, as step 2 below shows. A YAML error would have surfaced with the switches already gone, so I would run that check before step 1 next time. Between the deletion and the reload the switches do not exist. During that time they cannot be operated from the dashboard, and automation actions that target them fail.

Until the migration I kept a deploy variant of knx.yaml on the machine, identical to the repository version but without the block for the 31 switches that were still managed in the UI, and a comment at the top saying so. With that arrangement, deploying the full file by accident would have created every switch twice, which is the second reason I wanted it gone.

What a switch looks like in YAML

The YAML holds largely the same settings as the UI editor's JSON file, in a file I can edit and keep in Git. Two details in it were deliberate changes from what I had clicked together a year earlier:

# knx.yaml — two switch definitions, with and without a feedback address
switch:
  - name: "Light 1"
    address: "1/2/3"          # group address the actuator listens on
    state_address: "1/3/3"    # feedback object, if the actuator has one
    sync_state: true
  - name: "Ceiling light"
    address: "1/2/4"
    # no state_address: the actuator has no feedback object.
    # I removed the central status address because reads to it were never answered.

Nine switches had a central status address as their state address, an address that the actuator never answered on. Home Assistant queried it, got nothing, and showed whatever state it had last assumed. In the YAML I removed that address from all nine. Two of them now use their real feedback object, and the other seven have no state address, so Home Assistant shows what it last sent for those.

The tool: four commands over the websocket API

I used a local Node script with four subcommands for this. The commands below are the ones I ran. The script itself and the saved mapping from group address to the old entity id are not part of this post, only the core of the rename step further down. The script uses the same websocket API as the frontend and authenticates with a long-lived access token. Before the first delete I backed up the KNX store, the entity registry and the deployed knx.yaml together. The distinguishing mark between the two kinds of entity is the unique id: UI entities carry an id starting with knx_es_, YAML entities carry the group address itself.

# 0. inventory: 33 UI entities, 2 YAML switches expected
node tools/knx_migrate_ui_switches.mjs list

# 1. delete the UI entities from the KNX config store (websocket knx/delete_entity)
node tools/knx_migrate_ui_switches.mjs delete-ui

# 2. put the full knx.yaml on the machine and validate it
scp knx.yaml ha-host:/tmp/knx.yaml
ssh ha-host 'sudo cp /tmp/knx.yaml /home/ha/config/knx.yaml \
  && sudo docker exec homeassistant python -m homeassistant --script check_config -c /config'

# 3. reload the integration (no restart), wait, restore the old entity ids
node tools/knx_migrate_ui_switches.mjs reload     # calls knx.reload
sleep 20
node tools/knx_migrate_ui_switches.mjs rename     # sets the previous entity_id per switch
node tools/knx_migrate_ui_switches.mjs list       # every line must say OK

The delete step uses the knx/delete_entity websocket command per entity, which is what the UI editor calls when you press the trash icon. Home Assistant removes the entity from the KNX store and from the entity registry. That second part is the reason the rename step exists.

Keeping the entity ids

When the YAML entities come up after the reload, Home Assistant assigns them fresh entity ids derived from their names. Where a new entity id differed from the old one, I would have had to edit every dashboard card and automation that used it. The rename step avoids that: it holds a mapping from group address to the entity id the UI entity used to have, finds the new YAML entity by its unique id, and renames it through the registry API.

// the core of "rename": entity_registry/update over the websocket API
// MAPPING: group address (write) -> the entity_id the UI entity used to have
for (const [ga, oldId] of Object.entries(MAPPING)) {
  const yamlEntity = registry.find((e) => e.platform === "knx" && e.unique_id === ga);
  if (!yamlEntity || yamlEntity.entity_id === oldId) continue;
  await ws.send({ type: "config/entity_registry/update",
                  entity_id: yamlEntity.entity_id, new_entity_id: oldId });
}

I waited 20 seconds between reload and rename. The KNX integration registers its entities asynchronously after the reload call returns, and my script skips any entity it does not find in the registry yet, without an error. I had run list too early once and half the entities were missing. 20 seconds was enough here, and I have not measured how short the wait could be.

The final list showed 0 UI entities and 33 YAML switches, which are the 31 I carried over plus two that had been in the YAML before. All 31 migrated switches had their previous entity ids again. After the migration all 31 entities were assigned to their previous areas again, and the two labels that a time-control automation filters on were set as before. The rename excerpt above does not show that part.

The five automation actions that broke anyway

The entity ids were restored, and five actions in three automations still broke. They had been created in the automation editor as device actions, which bind to the registry's internal device id. Deleting the UI entities deleted those devices, and the actions pointed at nothing. Home Assistant did not fail the three automations for that. It logged a warning when the action ran and skipped it.

# before: a device action bound to a registry device id that no longer exists
- device_id: 3f1c…a97e
  domain: switch
  type: turn_on
  entity_id: 9b2d…c410

# after: the same action on the stable entity id
- action: switch.turn_on
  target:
    entity_id: switch.light_1

I replaced all five with plain switch.turn_on and switch.turn_off actions on the entity ids and reloaded automations. Actions that had been disabled stayed disabled. Afterwards a full config check and a scan of the log for unknown entity messages came back clean. Since then I write new automation actions as entity actions with switch.turn_on and switch.turn_off, the way the split YAML files already do.

What I tested, and what I could not

I switched four switches on and off from Home Assistant, checked the outgoing bus telegrams and restored the original off state afterwards. For the lamps that have no feedback object that is not an independent confirmation, because Home Assistant only knows what it sent. I tested the garage door on site with two pulses and checked its feedback: 0 while opening, 1 after closing. The doc now says that turn_on sends a pulse and does not fix the direction of travel.

The recorder logged 28 warnings during the renames, because the old target ids already had history and the short history under the interim names is not merged. I left the database alone. I have never executed the rollback, which would restore the KNX store, the entity registry and the old knx.yaml together from the backup I took before the deletion. For such a restore Home Assistant has to be stopped first, otherwise it overwrites the restored .storage files again. I did not want to try that on the production bus for a migration that had already succeeded.

Frequently asked questions

Can I keep some KNX entities in the UI and others in YAML?

Yes, as long as each switch is defined in only one of the two places. Otherwise it exists twice.

Why not just export the UI configuration?

There is no export. The store is a JSON file under .storage/knx that you can read, and its structure maps onto the YAML options closely enough that a script can translate it. The tool's list command is essentially that translation, printed side by side with what the YAML defines.

Does knx.reload really avoid a restart?

For the KNX integration itself, yes. It tears down and re-creates its entities and re-reads knx.yaml. Automations that reference the entities keep running during that time, so one can trigger while its target entity is missing. A later change to thermostat state addresses went in with a full restart, so I can only vouch for reload on this switch migration.

Questions or additions?

Comments run on GitHub Discussions, so you need a GitHub account to write one.

Related articles