Add options flow fallback for ports

This commit is contained in:
2026-02-13 19:33:55 +01:00
parent 8b20cf4744
commit 502a33dbac
7 changed files with 202 additions and 13 deletions

View File

@@ -1,5 +1,8 @@
# Changelog # Changelog
## 0.0.9 - 2026-02-13
- Add options flow fallback to add/remove ports when subentries are unavailable.
## 0.0.8 - 2026-02-13 ## 0.0.8 - 2026-02-13
- Improve subentry type detection for HA versions exposing different class names. - Improve subentry type detection for HA versions exposing different class names.

View File

@@ -21,6 +21,8 @@ Current minimal scope:
## Configure ## Configure
1. During setup, select the existing Home Assistant KNX integration entry. 1. During setup, select the existing Home Assistant KNX integration entry.
2. Add Ports from the integration's configuration page. 2. Add Ports from the integration's configuration page.
If the "Add Port" UI is missing, open the integration options and manage ports
there (fallback for HA versions without subentry support).
### Binary Sensor Port ### Binary Sensor Port
- `entity_id`: the HA binary_sensor to mirror. - `entity_id`: the HA binary_sensor to mirror.
@@ -65,6 +67,6 @@ Each group address has `invert incoming` and `invert outgoing` toggles to flip K
- Advanced DPT mapping options and inversion settings. - Advanced DPT mapping options and inversion settings.
## Versioning and Releases ## Versioning and Releases
- Current version: 0.0.8 - Current version: 0.0.9
- `CHANGELOG.md` lists versions with the newest entries at the top. - `CHANGELOG.md` lists versions with the newest entries at the top.
- Release creation is manual and only done when explicitly requested. - Release creation is manual and only done when explicitly requested.

View File

@@ -18,6 +18,7 @@ from .const import (
CONF_INVERT_OUTGOING, CONF_INVERT_OUTGOING,
CONF_MOVE_LONG_ADDRESS, CONF_MOVE_LONG_ADDRESS,
CONF_MOVE_SHORT_ADDRESS, CONF_MOVE_SHORT_ADDRESS,
CONF_PORTS,
CONF_POSITION_ADDRESS, CONF_POSITION_ADDRESS,
CONF_POSITION_STATE_ADDRESS, CONF_POSITION_STATE_ADDRESS,
CONF_STATE_ADDRESS, CONF_STATE_ADDRESS,
@@ -117,10 +118,8 @@ class BridgeManager:
switch_ports: list[SwitchPort] = [] switch_ports: list[SwitchPort] = []
cover_ports: list[CoverPort] = [] cover_ports: list[CoverPort] = []
subentries = getattr(self.entry, "subentries", []) for port_type, data in _iter_port_configs(self.entry):
for subentry in subentries: if port_type == "binary_sensor":
data = subentry.data
if subentry.type == "binary_sensor":
binary_ports.append( binary_ports.append(
BinarySensorPort( BinarySensorPort(
entity_id=data["entity_id"], entity_id=data["entity_id"],
@@ -133,7 +132,7 @@ class BridgeManager:
), ),
) )
) )
elif subentry.type == "switch": elif port_type == "switch":
switch_ports.append( switch_ports.append(
SwitchPort( SwitchPort(
entity_id=data["entity_id"], entity_id=data["entity_id"],
@@ -155,7 +154,7 @@ class BridgeManager:
), ),
) )
) )
elif subentry.type == "cover": elif port_type == "cover":
cover_ports.append( cover_ports.append(
CoverPort( CoverPort(
entity_id=data["entity_id"], entity_id=data["entity_id"],
@@ -632,3 +631,17 @@ def _invert_out_key(address_key: str) -> str:
def _get_value_type(address_key: str) -> str | None: def _get_value_type(address_key: str) -> str | None:
return ADDRESS_VALUE_TYPE.get(address_key) return ADDRESS_VALUE_TYPE.get(address_key)
def _iter_port_configs(entry: ConfigEntry) -> list[tuple[str, dict[str, Any]]]:
ports: list[tuple[str, dict[str, Any]]] = []
subentries = getattr(entry, "subentries", [])
for subentry in subentries:
ports.append((subentry.type, subentry.data))
option_ports = entry.options.get(CONF_PORTS, [])
for port in option_ports:
port_type = port.get("type")
data = port.get("data", {})
if port_type and isinstance(data, dict):
ports.append((port_type, data))
return ports

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import uuid
import voluptuous as vol import voluptuous as vol
@@ -18,6 +19,8 @@ from .const import (
CONF_KNX_ENTRY_ID, CONF_KNX_ENTRY_ID,
CONF_MOVE_LONG_ADDRESS, CONF_MOVE_LONG_ADDRESS,
CONF_MOVE_SHORT_ADDRESS, CONF_MOVE_SHORT_ADDRESS,
CONF_PORTS,
CONF_PORT_ID,
CONF_POSITION_ADDRESS, CONF_POSITION_ADDRESS,
CONF_POSITION_STATE_ADDRESS, CONF_POSITION_STATE_ADDRESS,
CONF_STATE_ADDRESS, CONF_STATE_ADDRESS,
@@ -89,11 +92,114 @@ class HAKnxBridgeOptionsFlow(config_entries.OptionsFlow):
self._config_entry = config_entry self._config_entry = config_entry
async def async_step_init(self, user_input: dict | None = None): async def async_step_init(self, user_input: dict | None = None):
if user_input is not None: return self.async_show_menu(
return self.async_create_entry(title="", data=user_input) step_id="init",
menu_options=[
"add_binary_sensor",
"add_switch",
"add_cover",
"remove_port",
],
)
schema = vol.Schema({}) async def async_step_add_binary_sensor(self, user_input: dict | None = None):
return self.async_show_form(step_id="init", data_schema=schema) if user_input is not None:
user_input, errors = _validate_knx_addresses(
user_input,
[
CONF_STATE_ADDRESS,
],
)
if errors:
return self.async_show_form(
step_id="add_binary_sensor",
data_schema=_binary_sensor_schema(),
errors=errors,
)
return await self._async_store_port("binary_sensor", user_input)
return self.async_show_form(
step_id="add_binary_sensor", data_schema=_binary_sensor_schema()
)
async def async_step_add_switch(self, user_input: dict | None = None):
if user_input is not None:
user_input, errors = _validate_knx_addresses(
user_input, [CONF_COMMAND_ADDRESS, CONF_STATE_ADDRESS]
)
if errors:
return self.async_show_form(
step_id="add_switch",
data_schema=_switch_schema(),
errors=errors,
)
return await self._async_store_port("switch", user_input)
return self.async_show_form(
step_id="add_switch", data_schema=_switch_schema()
)
async def async_step_add_cover(self, user_input: dict | None = None):
if user_input is not None:
user_input, errors = _validate_knx_addresses(
user_input,
[
CONF_MOVE_LONG_ADDRESS,
CONF_MOVE_SHORT_ADDRESS,
CONF_STOP_ADDRESS,
CONF_POSITION_ADDRESS,
CONF_POSITION_STATE_ADDRESS,
CONF_ANGLE_ADDRESS,
CONF_ANGLE_STATE_ADDRESS,
],
)
if errors:
return self.async_show_form(
step_id="add_cover", data_schema=_cover_schema(), errors=errors
)
return await self._async_store_port("cover", user_input)
return self.async_show_form(step_id="add_cover", data_schema=_cover_schema())
async def async_step_remove_port(self, user_input: dict | None = None):
ports = list(self._config_entry.options.get(CONF_PORTS, []))
if not ports:
return self.async_abort(reason="no_ports_to_remove")
if user_input is not None:
port_id = user_input[CONF_PORT_ID]
ports = [port for port in ports if port.get("id") != port_id]
return self.async_create_entry(title="", data={CONF_PORTS: ports})
options = [
{
"value": port.get("id"),
"label": port.get("title", port.get("id")),
}
for port in ports
if port.get("id")
]
schema = vol.Schema(
{
vol.Required(CONF_PORT_ID): selector.SelectSelector(
selector.SelectSelectorConfig(options=options, mode="dropdown")
)
}
)
return self.async_show_form(step_id="remove_port", data_schema=schema)
async def _async_store_port(self, port_type: str, user_input: dict):
ports = list(self._config_entry.options.get(CONF_PORTS, []))
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
ports.append(
{
"id": uuid.uuid4().hex,
"type": port_type,
"title": title,
"data": user_input,
}
)
return self.async_create_entry(title="", data={CONF_PORTS: ports})
class BinarySensorPortSubentryFlow(config_entries.ConfigSubentryFlow): class BinarySensorPortSubentryFlow(config_entries.ConfigSubentryFlow):

View File

@@ -6,6 +6,8 @@ CONF_STATE_ADDRESS = "state_address"
CONF_COMMAND_ADDRESS = "command_address" CONF_COMMAND_ADDRESS = "command_address"
CONF_INVERT_INCOMING = "invert_incoming" CONF_INVERT_INCOMING = "invert_incoming"
CONF_INVERT_OUTGOING = "invert_outgoing" CONF_INVERT_OUTGOING = "invert_outgoing"
CONF_PORTS = "ports"
CONF_PORT_ID = "port_id"
CONF_MOVE_LONG_ADDRESS = "move_long_address" CONF_MOVE_LONG_ADDRESS = "move_long_address"
CONF_MOVE_SHORT_ADDRESS = "move_short_address" CONF_MOVE_SHORT_ADDRESS = "move_short_address"

View File

@@ -1,7 +1,7 @@
{ {
"domain": "ha_knx_bridge", "domain": "ha_knx_bridge",
"name": "HA KNX Bridge", "name": "HA KNX Bridge",
"version": "0.0.8", "version": "0.0.9",
"config_flow": true, "config_flow": true,
"documentation": "https://github.com/bahmcloud/HA-KNX-Bridge", "documentation": "https://github.com/bahmcloud/HA-KNX-Bridge",
"issue_tracker": "https://github.com/bahmcloud/HA-KNX-Bridge/issues", "issue_tracker": "https://github.com/bahmcloud/HA-KNX-Bridge/issues",

View File

@@ -17,10 +17,73 @@
} }
}, },
"options": { "options": {
"abort": {
"no_ports_to_remove": "There are no ports to remove yet."
},
"step": { "step": {
"init": { "init": {
"title": "HA KNX Bridge Options", "title": "HA KNX Bridge Options",
"description": "No options available yet." "description": "Manage ports when subentries are unavailable.",
"menu_options": {
"add_binary_sensor": "Add binary sensor port",
"add_switch": "Add switch port",
"add_cover": "Add cover port",
"remove_port": "Remove port"
}
},
"add_binary_sensor": {
"title": "Add Binary Sensor Port",
"data": {
"entity_id": "Binary sensor entity",
"state_address": "State group address (DPT 1)",
"state_address_invert_incoming": "Invert incoming",
"state_address_invert_outgoing": "Invert outgoing"
}
},
"add_switch": {
"title": "Add Switch Port",
"data": {
"entity_id": "Switch entity",
"command_address": "Command group address (DPT 1)",
"command_address_invert_incoming": "Invert incoming",
"command_address_invert_outgoing": "Invert outgoing",
"state_address": "State group address (DPT 1)",
"state_address_invert_incoming": "Invert incoming",
"state_address_invert_outgoing": "Invert outgoing"
}
},
"add_cover": {
"title": "Add Cover Port",
"data": {
"entity_id": "Cover entity",
"move_long_address": "Move long (DPT 1.008 Up/Down)",
"move_long_address_invert_incoming": "Invert incoming",
"move_long_address_invert_outgoing": "Invert outgoing",
"move_short_address": "Move short (DPT 1.007 Step)",
"move_short_address_invert_incoming": "Invert incoming",
"move_short_address_invert_outgoing": "Invert outgoing",
"stop_address": "Stop (DPT 1)",
"stop_address_invert_incoming": "Invert incoming",
"stop_address_invert_outgoing": "Invert outgoing",
"position_address": "Set position (DPT 5.001)",
"position_address_invert_incoming": "Invert incoming",
"position_address_invert_outgoing": "Invert outgoing",
"position_state_address": "State position (DPT 5.001)",
"position_state_address_invert_incoming": "Invert incoming",
"position_state_address_invert_outgoing": "Invert outgoing",
"angle_address": "Set tilt (DPT 5.001)",
"angle_address_invert_incoming": "Invert incoming",
"angle_address_invert_outgoing": "Invert outgoing",
"angle_state_address": "State tilt (DPT 5.001)",
"angle_state_address_invert_incoming": "Invert incoming",
"angle_state_address_invert_outgoing": "Invert outgoing"
}
},
"remove_port": {
"title": "Remove Port",
"data": {
"port_id": "Port to remove"
}
} }
} }
}, },