11 Commits

10 changed files with 1100 additions and 65 deletions

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
## 0.0.10 - 2026-02-13
- Add translations for options flow menu labels.
## 0.0.9 - 2026-02-13
- Add options flow fallback to add/remove ports when subentries are unavailable.
## 0.0.8 - 2026-02-13
- Improve subentry type detection for HA versions exposing different class names.
## 0.0.7 - 2026-02-13
- Avoid crashing config entries when subentries are unsupported.
## 0.0.6 - 2026-02-13
- Fix config flow subentry type compatibility with older Home Assistant versions.
## 0.0.5 - 2026-02-13
- Centralize DPT auto-selection for KNX event registration per address.
## 0.0.4 - 2026-02-13
- Add per-group-address invert toggles for incoming and outgoing KNX payloads.
## 0.0.3 - 2026-02-13
- Add switch port support with KNX command/state mapping (DPT 1).
## 0.0.2 - 2026-02-13
- Validate and normalize KNX group addresses in subentry config flows.
- Add options flow skeleton for future settings.
- Add `bcs.yaml` metadata for BCS store listing.
## 0.0.1 - 2026-02-13 ## 0.0.1 - 2026-02-13
- Initial HACS-ready scaffold with config flow and subentries for binary sensor and cover. - Initial HACS-ready scaffold with config flow and subentries for binary sensor and cover.
- KNX bridge logic for basic send/receive mappings. - KNX bridge logic for basic send/receive mappings.

View File

@@ -5,6 +5,7 @@ add "Ports" that bind an HA entity to KNX addresses for state updates and comman
Current minimal scope: Current minimal scope:
- Binary Sensor port (state -> KNX) - Binary Sensor port (state -> KNX)
- Switch port (KNX -> HA commands, HA state -> KNX)
- Cover port (KNX -> HA commands, HA state -> KNX) - Cover port (KNX -> HA commands, HA state -> KNX)
## Requirements ## Requirements
@@ -20,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.
@@ -35,10 +38,28 @@ Current minimal scope:
- `angle_state_address` (DPT 5.001): KNX group address that receives HA tilt updates. - `angle_state_address` (DPT 5.001): KNX group address that receives HA tilt updates.
If a group address is left empty, it is ignored. If a group address is left empty, it is ignored.
Group address format must be `X/Y/Z` (0-31/0-7/0-255) or `X/Y` (0-31/0-2047).
Each group address has `invert incoming` and `invert outgoing` toggles to flip KNX payloads.
### Switch Port
- `entity_id`: the HA switch to control/monitor.
- `command_address` (DPT 1): KNX group address for on/off commands.
- `state_address` (DPT 1): KNX group address that receives HA on/off state.
## Notes ## Notes
- For DPT 1.008 (Up/Down), the bridge treats `0 = Up/Open` and `1 = Down/Close`. - For DPT 1.008 (Up/Down), the bridge treats `0 = Up/Open` and `1 = Down/Close`.
- For DPT 5.001, values are interpreted as 0-100 percent where 0 = closed and 100 = open. - For DPT 5.001, values are interpreted as 0-100 percent where 0 = closed and 100 = open.
- DPTs are auto-selected per address:
- Binary sensor `state_address`: DPT 1
- Switch `command_address`: DPT 1
- Switch `state_address`: DPT 1
- Cover `move_long_address`: DPT 1.008
- Cover `move_short_address`: DPT 1.007
- Cover `stop_address`: DPT 1
- Cover `position_address`: DPT 5.001
- Cover `position_state_address`: DPT 5.001
- Cover `angle_address`: DPT 5.001
- Cover `angle_state_address`: DPT 5.001
## Roadmap ## Roadmap
- Optional standalone KNX connection (without requiring the HA KNX integration). - Optional standalone KNX connection (without requiring the HA KNX integration).
@@ -46,6 +67,6 @@ If a group address is left empty, it is ignored.
- Advanced DPT mapping options and inversion settings. - Advanced DPT mapping options and inversion settings.
## Versioning and Releases ## Versioning and Releases
- Current version: 0.0.1 - Current version: 0.0.10
- `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.

21
bcs.yaml Normal file
View File

@@ -0,0 +1,21 @@
name: HA KNX Bridge
description: >
Home Assistant custom integration that mirrors Home Assistant entities
to KNX group addresses and accepts KNX actions to control Home Assistant
entities. It reuses an existing Home Assistant KNX integration and
provides per-entity "Ports" with automatic DPT selection and UI setup
via config flow.
category: Integrations
author: Bahmcloud
maintainer: Bahmcloud
domains:
- ha_knx_bridge
min_ha_version: "2025.12.0"
homepage: https://github.com/bahmcloud/HA-KNX-Bridge
issues: https://github.com/bahmcloud/HA-KNX-Bridge/issues
source: https://github.com/bahmcloud/HA-KNX-Bridge

View File

@@ -12,8 +12,13 @@ from homeassistant.helpers import event as event_helper
from .const import ( from .const import (
CONF_ANGLE_ADDRESS, CONF_ANGLE_ADDRESS,
CONF_ANGLE_STATE_ADDRESS, CONF_ANGLE_STATE_ADDRESS,
ADDRESS_VALUE_TYPE,
CONF_COMMAND_ADDRESS,
CONF_INVERT_INCOMING,
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,
@@ -29,18 +34,52 @@ KNX_DOMAIN = "knx"
class BinarySensorPort: class BinarySensorPort:
entity_id: str entity_id: str
state_address: str | None state_address: str | None
state_invert_incoming: bool
state_invert_outgoing: bool
@dataclass(frozen=True) @dataclass(frozen=True)
class CoverPort: class CoverPort:
entity_id: str entity_id: str
move_long_address: str | None move_long_address: str | None
move_long_invert_incoming: bool
move_long_invert_outgoing: bool
move_short_address: str | None move_short_address: str | None
move_short_invert_incoming: bool
move_short_invert_outgoing: bool
stop_address: str | None stop_address: str | None
stop_invert_incoming: bool
stop_invert_outgoing: bool
position_address: str | None position_address: str | None
position_invert_incoming: bool
position_invert_outgoing: bool
position_state_address: str | None position_state_address: str | None
position_state_invert_incoming: bool
position_state_invert_outgoing: bool
angle_address: str | None angle_address: str | None
angle_invert_incoming: bool
angle_invert_outgoing: bool
angle_state_address: str | None angle_state_address: str | None
angle_state_invert_incoming: bool
angle_state_invert_outgoing: bool
@dataclass(frozen=True)
class SwitchPort:
entity_id: str
command_address: str | None
state_address: str | None
command_invert_incoming: bool
command_invert_outgoing: bool
state_invert_incoming: bool
state_invert_outgoing: bool
@dataclass(frozen=True)
class AddressOptions:
value_type: str | None
invert_incoming: bool
invert_outgoing: bool
class BridgeManager: class BridgeManager:
@@ -51,14 +90,15 @@ class BridgeManager:
self._knx_event_unsub: callable | None = None self._knx_event_unsub: callable | None = None
self._address_handlers: dict[str, callable[[Event], Any]] = {} self._address_handlers: dict[str, callable[[Event], Any]] = {}
self._registered_addresses: list[tuple[str, str | None]] = [] self._registered_addresses: list[tuple[str, str | None]] = []
self._address_options: dict[str, AddressOptions] = {}
async def async_setup(self) -> None: async def async_setup(self) -> None:
if not self.hass.services.has_service(KNX_DOMAIN, "send"): if not self.hass.services.has_service(KNX_DOMAIN, "send"):
raise ConfigEntryNotReady("KNX integration services not available") raise ConfigEntryNotReady("KNX integration services not available")
binary_ports, cover_ports = self._load_ports() binary_ports, switch_ports, cover_ports = self._load_ports()
self._register_outgoing(binary_ports, cover_ports) self._register_outgoing(binary_ports, switch_ports, cover_ports)
await self._register_incoming(cover_ports) await self._register_incoming(switch_ports, cover_ports)
async def async_unload(self) -> None: async def async_unload(self) -> None:
for unsub in self._unsub_listeners: for unsub in self._unsub_listeners:
@@ -71,71 +111,221 @@ class BridgeManager:
await self._unregister_knx_events() await self._unregister_knx_events()
def _load_ports(self) -> tuple[list[BinarySensorPort], list[CoverPort]]: def _load_ports(
self,
) -> tuple[list[BinarySensorPort], list[SwitchPort], list[CoverPort]]:
binary_ports: list[BinarySensorPort] = [] binary_ports: list[BinarySensorPort] = []
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"],
state_address=_clean_address(data.get(CONF_STATE_ADDRESS)), state_address=_clean_address(data.get(CONF_STATE_ADDRESS)),
state_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_STATE_ADDRESS))
),
state_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_STATE_ADDRESS))
),
) )
) )
elif subentry.type == "cover": elif port_type == "switch":
switch_ports.append(
SwitchPort(
entity_id=data["entity_id"],
command_address=_clean_address(
data.get(CONF_COMMAND_ADDRESS)
),
state_address=_clean_address(data.get(CONF_STATE_ADDRESS)),
command_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_COMMAND_ADDRESS))
),
command_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_COMMAND_ADDRESS))
),
state_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_STATE_ADDRESS))
),
state_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_STATE_ADDRESS))
),
)
)
elif port_type == "cover":
cover_ports.append( cover_ports.append(
CoverPort( CoverPort(
entity_id=data["entity_id"], entity_id=data["entity_id"],
move_long_address=_clean_address(data.get(CONF_MOVE_LONG_ADDRESS)), move_long_address=_clean_address(data.get(CONF_MOVE_LONG_ADDRESS)),
move_long_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_MOVE_LONG_ADDRESS))
),
move_long_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_MOVE_LONG_ADDRESS))
),
move_short_address=_clean_address(data.get(CONF_MOVE_SHORT_ADDRESS)), move_short_address=_clean_address(data.get(CONF_MOVE_SHORT_ADDRESS)),
move_short_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_MOVE_SHORT_ADDRESS))
),
move_short_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_MOVE_SHORT_ADDRESS))
),
stop_address=_clean_address(data.get(CONF_STOP_ADDRESS)), stop_address=_clean_address(data.get(CONF_STOP_ADDRESS)),
stop_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_STOP_ADDRESS))
),
stop_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_STOP_ADDRESS))
),
position_address=_clean_address(data.get(CONF_POSITION_ADDRESS)), position_address=_clean_address(data.get(CONF_POSITION_ADDRESS)),
position_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_POSITION_ADDRESS))
),
position_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_POSITION_ADDRESS))
),
position_state_address=_clean_address( position_state_address=_clean_address(
data.get(CONF_POSITION_STATE_ADDRESS) data.get(CONF_POSITION_STATE_ADDRESS)
), ),
position_state_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_POSITION_STATE_ADDRESS))
),
position_state_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_POSITION_STATE_ADDRESS))
),
angle_address=_clean_address(data.get(CONF_ANGLE_ADDRESS)), angle_address=_clean_address(data.get(CONF_ANGLE_ADDRESS)),
angle_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_ANGLE_ADDRESS))
),
angle_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_ANGLE_ADDRESS))
),
angle_state_address=_clean_address( angle_state_address=_clean_address(
data.get(CONF_ANGLE_STATE_ADDRESS) data.get(CONF_ANGLE_STATE_ADDRESS)
), ),
angle_state_invert_incoming=_clean_bool(
data.get(_invert_in_key(CONF_ANGLE_STATE_ADDRESS))
),
angle_state_invert_outgoing=_clean_bool(
data.get(_invert_out_key(CONF_ANGLE_STATE_ADDRESS))
),
) )
) )
return binary_ports, cover_ports return binary_ports, switch_ports, cover_ports
def _register_outgoing( def _register_outgoing(
self, binary_ports: list[BinarySensorPort], cover_ports: list[CoverPort] self,
binary_ports: list[BinarySensorPort],
switch_ports: list[SwitchPort],
cover_ports: list[CoverPort],
) -> None: ) -> None:
for port in binary_ports: for port in binary_ports:
if not port.state_address: if not port.state_address:
continue continue
self._remember_address_options(
port.state_address,
_get_value_type(CONF_STATE_ADDRESS),
port.state_invert_incoming,
port.state_invert_outgoing,
)
self._unsub_listeners.append( self._unsub_listeners.append(
event_helper.async_track_state_change_event( event_helper.async_track_state_change_event(
self.hass, [port.entity_id], self._binary_sensor_changed(port) self.hass, [port.entity_id], self._binary_sensor_changed(port)
) )
) )
for port in switch_ports:
if not port.state_address:
continue
self._remember_address_options(
port.state_address,
_get_value_type(CONF_STATE_ADDRESS),
port.state_invert_incoming,
port.state_invert_outgoing,
)
self._unsub_listeners.append(
event_helper.async_track_state_change_event(
self.hass, [port.entity_id], self._switch_changed(port)
)
)
for port in cover_ports: for port in cover_ports:
if not (port.position_state_address or port.angle_state_address): if not (port.position_state_address or port.angle_state_address):
continue continue
if port.position_state_address:
self._remember_address_options(
port.position_state_address,
_get_value_type(CONF_POSITION_STATE_ADDRESS),
port.position_state_invert_incoming,
port.position_state_invert_outgoing,
)
if port.angle_state_address:
self._remember_address_options(
port.angle_state_address,
_get_value_type(CONF_ANGLE_STATE_ADDRESS),
port.angle_state_invert_incoming,
port.angle_state_invert_outgoing,
)
self._unsub_listeners.append( self._unsub_listeners.append(
event_helper.async_track_state_change_event( event_helper.async_track_state_change_event(
self.hass, [port.entity_id], self._cover_changed(port) self.hass, [port.entity_id], self._cover_changed(port)
) )
) )
async def _register_incoming(self, cover_ports: list[CoverPort]) -> None: async def _register_incoming(
self, switch_ports: list[SwitchPort], cover_ports: list[CoverPort]
) -> None:
for port in switch_ports:
self._register_knx_switch_address(
port.command_address,
port.command_invert_incoming,
port.command_invert_outgoing,
port,
)
for port in cover_ports: for port in cover_ports:
self._register_knx_address(port.move_long_address, None, port, "move_long")
self._register_knx_address(port.move_short_address, None, port, "move_short")
self._register_knx_address(port.stop_address, None, port, "stop")
self._register_knx_address( self._register_knx_address(
port.position_address, "percent", port, "position" port.move_long_address,
CONF_MOVE_LONG_ADDRESS,
port.move_long_invert_incoming,
port.move_long_invert_outgoing,
port,
"move_long",
) )
self._register_knx_address( self._register_knx_address(
port.angle_address, "percent", port, "angle" port.move_short_address,
CONF_MOVE_SHORT_ADDRESS,
port.move_short_invert_incoming,
port.move_short_invert_outgoing,
port,
"move_short",
)
self._register_knx_address(
port.stop_address,
CONF_STOP_ADDRESS,
port.stop_invert_incoming,
port.stop_invert_outgoing,
port,
"stop",
)
self._register_knx_address(
port.position_address,
CONF_POSITION_ADDRESS,
port.position_invert_incoming,
port.position_invert_outgoing,
port,
"position",
)
self._register_knx_address(
port.angle_address,
CONF_ANGLE_ADDRESS,
port.angle_invert_incoming,
port.angle_invert_outgoing,
port,
"angle",
) )
if self._address_handlers: if self._address_handlers:
@@ -148,7 +338,9 @@ class BridgeManager:
def _register_knx_address( def _register_knx_address(
self, self,
address: str | None, address: str | None,
value_type: str | None, address_key: str,
invert_incoming: bool,
invert_outgoing: bool,
port: CoverPort, port: CoverPort,
action: str, action: str,
) -> None: ) -> None:
@@ -158,8 +350,45 @@ class BridgeManager:
self._address_handlers[address] = lambda event: self._handle_cover_action( self._address_handlers[address] = lambda event: self._handle_cover_action(
port, action, event port, action, event
) )
value_type = _get_value_type(address_key)
self._remember_address_options(
address, value_type, invert_incoming, invert_outgoing
)
self._registered_addresses.append((address, value_type)) self._registered_addresses.append((address, value_type))
def _register_knx_switch_address(
self,
address: str | None,
invert_incoming: bool,
invert_outgoing: bool,
port: SwitchPort,
) -> None:
if not address:
return
self._address_handlers[address] = lambda event: self._handle_switch_action(
port, event
)
self._remember_address_options(
address,
_get_value_type(CONF_COMMAND_ADDRESS),
invert_incoming,
invert_outgoing,
)
self._registered_addresses.append((address, None))
def _remember_address_options(
self,
address: str,
value_type: str | None,
invert_incoming: bool,
invert_outgoing: bool,
) -> None:
self._address_options[address] = AddressOptions(
value_type=value_type,
invert_incoming=invert_incoming,
invert_outgoing=invert_outgoing,
)
async def _register_knx_events(self) -> None: async def _register_knx_events(self) -> None:
for address, value_type in self._registered_addresses: for address, value_type in self._registered_addresses:
data: dict[str, Any] = {"address": address} data: dict[str, Any] = {"address": address}
@@ -187,6 +416,24 @@ class BridgeManager:
if new_state.state not in ("on", "off"): if new_state.state not in ("on", "off"):
return return
payload = 1 if new_state.state == "on" else 0 payload = 1 if new_state.state == "on" else 0
payload = _invert_value(
payload, port.state_address, self._address_options, "outgoing"
)
await self._knx_send_raw(port.state_address, payload)
return _handler
def _switch_changed(self, port: SwitchPort) -> callable[[Event], Any]:
async def _handler(event: Event) -> None:
new_state: State | None = event.data.get("new_state")
if new_state is None:
return
if new_state.state not in ("on", "off"):
return
payload = 1 if new_state.state == "on" else 0
payload = _invert_value(
payload, port.state_address, self._address_options, "outgoing"
)
await self._knx_send_raw(port.state_address, payload) await self._knx_send_raw(port.state_address, payload)
return _handler return _handler
@@ -200,11 +447,25 @@ class BridgeManager:
if port.position_state_address is not None: if port.position_state_address is not None:
position = new_state.attributes.get("current_position") position = new_state.attributes.get("current_position")
if position is not None: if position is not None:
await self._knx_send_percent(port.position_state_address, position) position = _invert_value(
position,
port.position_state_address,
self._address_options,
"outgoing",
)
await self._knx_send_percent(
port.position_state_address, position
)
if port.angle_state_address is not None: if port.angle_state_address is not None:
angle = new_state.attributes.get("current_tilt_position") angle = new_state.attributes.get("current_tilt_position")
if angle is not None: if angle is not None:
angle = _invert_value(
angle,
port.angle_state_address,
self._address_options,
"outgoing",
)
await self._knx_send_percent(port.angle_state_address, angle) await self._knx_send_percent(port.angle_state_address, angle)
return _handler return _handler
@@ -229,6 +490,9 @@ class BridgeManager:
value = _extract_event_value(event) value = _extract_event_value(event)
if value is None: if value is None:
return return
value = _invert_value(
value, event.data.get("destination"), self._address_options, "incoming"
)
if action == "move_long": if action == "move_long":
if value == 0: if value == 0:
@@ -250,6 +514,18 @@ class BridgeManager:
port.entity_id, "set_cover_tilt_position", {"tilt_position": value} port.entity_id, "set_cover_tilt_position", {"tilt_position": value}
) )
async def _handle_switch_action(self, port: SwitchPort, event: Event) -> None:
value = _extract_event_value(event)
if value is None:
return
value = _invert_value(
value, event.data.get("destination"), self._address_options, "incoming"
)
if value == 0:
await self._call_switch_service(port.entity_id, "turn_off")
elif value == 1:
await self._call_switch_service(port.entity_id, "turn_on")
async def _call_cover_service( async def _call_cover_service(
self, entity_id: str, service: str, service_data: dict[str, Any] | None = None self, entity_id: str, service: str, service_data: dict[str, Any] | None = None
) -> None: ) -> None:
@@ -260,6 +536,16 @@ class BridgeManager:
"cover", service, data, blocking=False "cover", service, data, blocking=False
) )
async def _call_switch_service(
self, entity_id: str, service: str, service_data: dict[str, Any] | None = None
) -> None:
data = {"entity_id": entity_id}
if service_data:
data.update(service_data)
await self.hass.services.async_call(
"switch", service, data, blocking=False
)
async def _knx_send_raw(self, address: str | None, payload: int) -> None: async def _knx_send_raw(self, address: str | None, payload: int) -> None:
if not address: if not address:
return return
@@ -305,3 +591,57 @@ def _clean_address(address: Any) -> str | None:
stripped = address.strip() stripped = address.strip()
return stripped or None return stripped or None
return None return None
def _clean_bool(value: Any) -> bool:
return bool(value)
def _invert_value(
value: int,
address: str | None,
address_options: dict[str, AddressOptions],
direction: str,
) -> int:
if address is None:
return value
options = address_options.get(address)
if options is None:
return value
if direction == "incoming":
if not options.invert_incoming:
return value
else:
if not options.invert_outgoing:
return value
if options.value_type == "percent":
return 100 - value
if value not in (0, 1):
return value
return 0 if value == 1 else 1
def _invert_in_key(address_key: str) -> str:
return f"{address_key}_{CONF_INVERT_INCOMING}"
def _invert_out_key(address_key: str) -> str:
return f"{address_key}_{CONF_INVERT_OUTGOING}"
def _get_value_type(address_key: str) -> str | None:
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
@@ -12,9 +13,14 @@ from homeassistant.helpers import selector
from .const import ( from .const import (
CONF_ANGLE_ADDRESS, CONF_ANGLE_ADDRESS,
CONF_ANGLE_STATE_ADDRESS, CONF_ANGLE_STATE_ADDRESS,
CONF_COMMAND_ADDRESS,
CONF_INVERT_INCOMING,
CONF_INVERT_OUTGOING,
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,
@@ -54,38 +60,185 @@ class HAKnxBridgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
return self.async_show_form(step_id="user", data_schema=schema) return self.async_show_form(step_id="user", data_schema=schema)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return HAKnxBridgeOptionsFlow(config_entry)
@staticmethod @staticmethod
@callback @callback
def async_get_supported_subentry_types(config_entry): def async_get_supported_subentry_types(config_entry):
subentry_type = _get_subentry_type()
if subentry_type is None:
_LOGGER.warning(
"Config subentries are not supported in this Home Assistant version"
)
return {}
return { return {
"binary_sensor": config_entries.SubentryType( "binary_sensor": subentry_type(
name="Binary Sensor Port", flow_class=BinarySensorPortSubentryFlow name="Binary Sensor Port", flow_class=BinarySensorPortSubentryFlow
), ),
"cover": config_entries.SubentryType( "switch": subentry_type(
name="Switch Port", flow_class=SwitchPortSubentryFlow
),
"cover": subentry_type(
name="Cover Port", flow_class=CoverPortSubentryFlow name="Cover Port", flow_class=CoverPortSubentryFlow
), ),
} }
class HAKnxBridgeOptionsFlow(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self._config_entry = config_entry
async def async_step_init(self, user_input: dict | None = None):
return self.async_show_menu(
step_id="init",
menu_options=[
"add_binary_sensor",
"add_switch",
"add_cover",
"remove_port",
],
)
async def async_step_add_binary_sensor(self, user_input: dict | None = None):
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):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input: dict | None = None): async def async_step_user(self, user_input: dict | None = None):
if user_input is not None: 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="user", data_schema=_binary_sensor_schema(), errors=errors
)
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID]) title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
return self.async_create_entry(title=title, data=user_input) return self.async_create_entry(title=title, data=user_input)
schema = vol.Schema( return self.async_show_form(step_id="user", data_schema=_binary_sensor_schema())
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=["binary_sensor"]) class SwitchPortSubentryFlow(config_entries.ConfigSubentryFlow):
), VERSION = 1
vol.Optional(CONF_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text") async def async_step_user(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]
return self.async_show_form(step_id="user", data_schema=schema) )
if errors:
return self.async_show_form(
step_id="user", data_schema=_switch_schema(), errors=errors
)
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
return self.async_create_entry(title=title, data=user_input)
return self.async_show_form(step_id="user", data_schema=_switch_schema())
class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow): class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow):
@@ -93,38 +246,26 @@ class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow):
async def async_step_user(self, user_input: dict | None = None): async def async_step_user(self, user_input: dict | None = None):
if user_input is not 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="user", data_schema=_cover_schema(), errors=errors
)
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID]) title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
return self.async_create_entry(title=title, data=user_input) return self.async_create_entry(title=title, data=user_input)
schema = vol.Schema( return self.async_show_form(step_id="user", data_schema=_cover_schema())
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=["cover"])
),
vol.Optional(CONF_MOVE_LONG_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_MOVE_SHORT_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_STOP_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_POSITION_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_POSITION_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_ANGLE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(CONF_ANGLE_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
}
)
return self.async_show_form(step_id="user", data_schema=schema)
def _entity_title(hass, entity_id: str) -> str: def _entity_title(hass, entity_id: str) -> str:
@@ -132,3 +273,207 @@ def _entity_title(hass, entity_id: str) -> str:
if state is None: if state is None:
return entity_id return entity_id
return state.attributes.get("friendly_name", entity_id) return state.attributes.get("friendly_name", entity_id)
def _binary_sensor_schema() -> vol.Schema:
return vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=["binary_sensor"])
),
vol.Optional(CONF_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
}
)
def _cover_schema() -> vol.Schema:
return vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=["cover"])
),
vol.Optional(CONF_MOVE_LONG_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_MOVE_LONG_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_MOVE_LONG_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_MOVE_SHORT_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_MOVE_SHORT_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_MOVE_SHORT_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_STOP_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_STOP_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_STOP_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_POSITION_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_POSITION_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_POSITION_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_POSITION_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_POSITION_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_POSITION_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_ANGLE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_ANGLE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_ANGLE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_ANGLE_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_ANGLE_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_ANGLE_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
}
)
def _switch_schema() -> vol.Schema:
return vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=["switch"])
),
vol.Optional(CONF_COMMAND_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_COMMAND_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_COMMAND_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(CONF_STATE_ADDRESS): selector.TextSelector(
selector.TextSelectorConfig(type="text")
),
vol.Optional(_invert_in_key(CONF_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
vol.Optional(_invert_out_key(CONF_STATE_ADDRESS), default=False): (
selector.BooleanSelector()
),
}
)
def _validate_knx_addresses(
user_input: dict, keys: list[str]
) -> tuple[dict, dict[str, str]]:
errors: dict[str, str] = {}
cleaned = dict(user_input)
for key in keys:
if key not in cleaned:
continue
value = cleaned.get(key)
if value is None:
cleaned.pop(key, None)
cleaned.pop(_invert_in_key(key), None)
cleaned.pop(_invert_out_key(key), None)
continue
try:
normalized = _normalize_group_address(str(value))
except ValueError:
errors[key] = "invalid_ga"
continue
if normalized == "":
cleaned.pop(key, None)
cleaned.pop(_invert_in_key(key), None)
cleaned.pop(_invert_out_key(key), None)
else:
cleaned[key] = normalized
return cleaned, errors
def _normalize_group_address(value: str) -> str:
text = value.strip()
if not text:
return ""
parts = text.split("/")
if len(parts) == 2:
main, sub = _parse_int(parts[0]), _parse_int(parts[1])
if not (0 <= main <= 31 and 0 <= sub <= 2047):
raise ValueError("group address out of range")
return f"{main}/{sub}"
if len(parts) == 3:
main, middle, sub = (
_parse_int(parts[0]),
_parse_int(parts[1]),
_parse_int(parts[2]),
)
if not (0 <= main <= 31 and 0 <= middle <= 7 and 0 <= sub <= 255):
raise ValueError("group address out of range")
return f"{main}/{middle}/{sub}"
raise ValueError("invalid group address format")
def _parse_int(value: str) -> int:
text = value.strip()
if text == "":
raise ValueError("empty group address part")
return int(text)
def _invert_in_key(address_key: str) -> str:
return f"{address_key}_{CONF_INVERT_INCOMING}"
def _invert_out_key(address_key: str) -> str:
return f"{address_key}_{CONF_INVERT_OUTGOING}"
def _get_subentry_type():
candidates = [
"SubentryType",
"ConfigEntrySubentryType",
"ConfigSubentryType",
"SubEntryType",
]
for name in candidates:
subentry_type = getattr(config_entries, name, None)
if subentry_type is not None:
return subentry_type
_LOGGER.debug(
"Subentry type class not found on homeassistant.config_entries. "
"Available attrs: %s",
", ".join(sorted(dir(config_entries))),
)
return None

View File

@@ -3,6 +3,11 @@ DOMAIN = "ha_knx_bridge"
CONF_KNX_ENTRY_ID = "knx_entry_id" CONF_KNX_ENTRY_ID = "knx_entry_id"
CONF_STATE_ADDRESS = "state_address" CONF_STATE_ADDRESS = "state_address"
CONF_COMMAND_ADDRESS = "command_address"
CONF_INVERT_INCOMING = "invert_incoming"
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"
@@ -11,3 +16,22 @@ CONF_POSITION_ADDRESS = "position_address"
CONF_POSITION_STATE_ADDRESS = "position_state_address" CONF_POSITION_STATE_ADDRESS = "position_state_address"
CONF_ANGLE_ADDRESS = "angle_address" CONF_ANGLE_ADDRESS = "angle_address"
CONF_ANGLE_STATE_ADDRESS = "angle_state_address" CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
ADDRESS_DPT_MAP: dict[str, str] = {
CONF_STATE_ADDRESS: "1",
CONF_COMMAND_ADDRESS: "1",
CONF_MOVE_LONG_ADDRESS: "1.008",
CONF_MOVE_SHORT_ADDRESS: "1.007",
CONF_STOP_ADDRESS: "1",
CONF_POSITION_ADDRESS: "5.001",
CONF_POSITION_STATE_ADDRESS: "5.001",
CONF_ANGLE_ADDRESS: "5.001",
CONF_ANGLE_STATE_ADDRESS: "5.001",
}
ADDRESS_VALUE_TYPE: dict[str, str] = {
CONF_POSITION_ADDRESS: "percent",
CONF_POSITION_STATE_ADDRESS: "percent",
CONF_ANGLE_ADDRESS: "percent",
CONF_ANGLE_STATE_ADDRESS: "percent",
}

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.1", "version": "0.0.10",
"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

@@ -3,6 +3,9 @@
"abort": { "abort": {
"knx_not_configured": "Set up the Home Assistant KNX integration first." "knx_not_configured": "Set up the Home Assistant KNX integration first."
}, },
"error": {
"invalid_ga": "Invalid KNX group address. Use X/Y/Z (0-31/0-7/0-255) or X/Y (0-31/0-2047)."
},
"step": { "step": {
"user": { "user": {
"title": "HA KNX Bridge", "title": "HA KNX Bridge",
@@ -13,14 +16,106 @@
} }
} }
}, },
"options": {
"abort": {
"no_ports_to_remove": "There are no ports to remove yet."
},
"step": {
"init": {
"title": "HA KNX Bridge Options",
"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"
}
}
}
},
"config_subentries": { "config_subentries": {
"error": {
"invalid_ga": "Invalid KNX group address. Use X/Y/Z (0-31/0-7/0-255) or X/Y (0-31/0-2047)."
},
"binary_sensor": { "binary_sensor": {
"step": { "step": {
"user": { "user": {
"title": "Binary Sensor Port", "title": "Binary Sensor Port",
"data": { "data": {
"entity_id": "Binary sensor entity", "entity_id": "Binary sensor entity",
"state_address": "State group address (DPT 1)" "state_address": "State group address (DPT 1)",
"state_address_invert_incoming": "Invert incoming",
"state_address_invert_outgoing": "Invert outgoing"
}
}
}
},
"switch": {
"step": {
"user": {
"title": "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"
} }
} }
} }
@@ -32,12 +127,26 @@
"data": { "data": {
"entity_id": "Cover entity", "entity_id": "Cover entity",
"move_long_address": "Move long (DPT 1.008 Up/Down)", "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": "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": "Stop (DPT 1)",
"stop_address_invert_incoming": "Invert incoming",
"stop_address_invert_outgoing": "Invert outgoing",
"position_address": "Set position (DPT 5.001)", "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": "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": "Set tilt (DPT 5.001)",
"angle_state_address": "State 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"
} }
} }
} }

73
translations/de.json Normal file
View File

@@ -0,0 +1,73 @@
{
"options": {
"abort": {
"no_ports_to_remove": "Es gibt noch keine Ports zum Entfernen."
},
"step": {
"init": {
"title": "HA KNX Bridge Optionen",
"description": "Ports verwalten, wenn Subentries nicht verfügbar sind.",
"menu_options": {
"add_binary_sensor": "Binary-Sensor-Port hinzufügen",
"add_switch": "Schalter-Port hinzufügen",
"add_cover": "Cover-Port hinzufügen",
"remove_port": "Port entfernen"
}
},
"add_binary_sensor": {
"title": "Binary-Sensor-Port hinzufügen",
"data": {
"entity_id": "Binary-Sensor-Entity",
"state_address": "State-Gruppenadresse (DPT 1)",
"state_address_invert_incoming": "Eingehend invertieren",
"state_address_invert_outgoing": "Ausgehend invertieren"
}
},
"add_switch": {
"title": "Schalter-Port hinzufügen",
"data": {
"entity_id": "Schalter-Entity",
"command_address": "Command-Gruppenadresse (DPT 1)",
"command_address_invert_incoming": "Eingehend invertieren",
"command_address_invert_outgoing": "Ausgehend invertieren",
"state_address": "State-Gruppenadresse (DPT 1)",
"state_address_invert_incoming": "Eingehend invertieren",
"state_address_invert_outgoing": "Ausgehend invertieren"
}
},
"add_cover": {
"title": "Cover-Port hinzufügen",
"data": {
"entity_id": "Cover-Entity",
"move_long_address": "Move long (DPT 1.008 Auf/Ab)",
"move_long_address_invert_incoming": "Eingehend invertieren",
"move_long_address_invert_outgoing": "Ausgehend invertieren",
"move_short_address": "Move short (DPT 1.007 Schritt)",
"move_short_address_invert_incoming": "Eingehend invertieren",
"move_short_address_invert_outgoing": "Ausgehend invertieren",
"stop_address": "Stop (DPT 1)",
"stop_address_invert_incoming": "Eingehend invertieren",
"stop_address_invert_outgoing": "Ausgehend invertieren",
"position_address": "Position setzen (DPT 5.001)",
"position_address_invert_incoming": "Eingehend invertieren",
"position_address_invert_outgoing": "Ausgehend invertieren",
"position_state_address": "Positionsstatus (DPT 5.001)",
"position_state_address_invert_incoming": "Eingehend invertieren",
"position_state_address_invert_outgoing": "Ausgehend invertieren",
"angle_address": "Tilt setzen (DPT 5.001)",
"angle_address_invert_incoming": "Eingehend invertieren",
"angle_address_invert_outgoing": "Ausgehend invertieren",
"angle_state_address": "Tilt-Status (DPT 5.001)",
"angle_state_address_invert_incoming": "Eingehend invertieren",
"angle_state_address_invert_outgoing": "Ausgehend invertieren"
}
},
"remove_port": {
"title": "Port entfernen",
"data": {
"port_id": "Zu entfernender Port"
}
}
}
}
}

73
translations/en.json Normal file
View File

@@ -0,0 +1,73 @@
{
"options": {
"abort": {
"no_ports_to_remove": "There are no ports to remove yet."
},
"step": {
"init": {
"title": "HA KNX Bridge Options",
"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"
}
}
}
}
}