Add support for KNX UI to create BinarySensor entities (#136703)

pull/136051/head
Matthias Alphart 2025-01-28 15:16:58 +01:00 committed by GitHub
parent 139061afa3
commit 658d3cf06e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 191 additions and 47 deletions

View File

@ -18,14 +18,28 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import KNXModule from . import KNXModule
from .const import ATTR_COUNTER, ATTR_SOURCE, KNX_MODULE_KEY from .const import (
from .entity import KnxYamlEntity ATTR_COUNTER,
from .schema import BinarySensorSchema ATTR_SOURCE,
CONF_CONTEXT_TIMEOUT,
CONF_IGNORE_INTERNAL_STATE,
CONF_INVERT,
CONF_RESET_AFTER,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
DOMAIN,
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
async def async_setup_entry( async def async_setup_entry(
@ -35,40 +49,38 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the KNX binary sensor platform.""" """Set up the KNX binary sensor platform."""
knx_module = hass.data[KNX_MODULE_KEY] knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.BINARY_SENSOR] platform = async_get_current_platform()
knx_module.config_store.add_platform(
async_add_entities( platform=Platform.BINARY_SENSOR,
KNXBinarySensor(knx_module, entity_config) for entity_config in config controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiBinarySensor,
),
) )
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.BINARY_SENSOR):
entities.extend(
KnxYamlBinarySensor(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(
Platform.BINARY_SENSOR
):
entities.extend(
KnxUiBinarySensor(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)
class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity):
class _KnxBinarySensor(BinarySensorEntity, RestoreEntity):
"""Representation of a KNX binary sensor.""" """Representation of a KNX binary sensor."""
_device: XknxBinarySensor _device: XknxBinarySensor
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX binary sensor."""
super().__init__(
knx_module=knx_module,
device=XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS],
invert=config[BinarySensorSchema.CONF_INVERT],
sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
ignore_internal_state=config[
BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE
],
context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT),
reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER),
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_force_update = self._device.ignore_internal_state
self._attr_unique_id = str(self._device.remote_value.group_address_state)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Restore last state.""" """Restore last state."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -92,3 +104,59 @@ class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity):
if self._device.last_telegram is not None: if self._device.last_telegram is not None:
attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address)
return attr return attr
class KnxYamlBinarySensor(_KnxBinarySensor, KnxYamlEntity):
"""Representation of a KNX binary sensor configured from YAML."""
_device: XknxBinarySensor
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX binary sensor."""
super().__init__(
knx_module=knx_module,
device=XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_state=config[CONF_STATE_ADDRESS],
invert=config[CONF_INVERT],
sync_state=config[CONF_SYNC_STATE],
ignore_internal_state=config[CONF_IGNORE_INTERNAL_STATE],
context_timeout=config.get(CONF_CONTEXT_TIMEOUT),
reset_after=config.get(CONF_RESET_AFTER),
),
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_force_update = self._device.ignore_internal_state
self._attr_unique_id = str(self._device.remote_value.group_address_state)
class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
"""Representation of a KNX binary sensor configured from UI."""
_device: XknxBinarySensor
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize KNX binary sensor."""
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
self._device = XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address_state=[
config[DOMAIN][CONF_GA_SENSOR][CONF_GA_STATE],
*config[DOMAIN][CONF_GA_SENSOR][CONF_GA_PASSIVE],
],
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN].get(CONF_INVERT, False),
ignore_internal_state=config[DOMAIN].get(CONF_IGNORE_INTERNAL_STATE, False),
context_timeout=config[DOMAIN].get(CONF_CONTEXT_TIMEOUT),
reset_after=config[DOMAIN].get(CONF_RESET_AFTER),
)
self._attr_force_update = self._device.ignore_internal_state

View File

@ -67,6 +67,8 @@ CONF_KNX_SECURE_USER_PASSWORD: Final = "user_password"
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: Final = "device_authentication" CONF_KNX_SECURE_DEVICE_AUTHENTICATION: Final = "device_authentication"
CONF_CONTEXT_TIMEOUT: Final = "context_timeout"
CONF_IGNORE_INTERNAL_STATE: Final = "ignore_internal_state"
CONF_PAYLOAD_LENGTH: Final = "payload_length" CONF_PAYLOAD_LENGTH: Final = "payload_length"
CONF_RESET_AFTER: Final = "reset_after" CONF_RESET_AFTER: Final = "reset_after"
CONF_RESPOND_TO_READ: Final = "respond_to_read" CONF_RESPOND_TO_READ: Final = "respond_to_read"
@ -156,7 +158,11 @@ SUPPORTED_PLATFORMS_YAML: Final = {
Platform.WEATHER, Platform.WEATHER,
} }
SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH, Platform.LIGHT} SUPPORTED_PLATFORMS_UI: Final = {
Platform.BINARY_SENSOR,
Platform.LIGHT,
Platform.SWITCH,
}
# Map KNX controller modes to HA modes. This list might not be complete. # Map KNX controller modes to HA modes. This list might not be complete.
CONTROLLER_MODES: Final = { CONTROLLER_MODES: Final = {

View File

@ -45,6 +45,8 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from .const import ( from .const import (
CONF_CONTEXT_TIMEOUT,
CONF_IGNORE_INTERNAL_STATE,
CONF_INVERT, CONF_INVERT,
CONF_KNX_EXPOSE, CONF_KNX_EXPOSE,
CONF_PAYLOAD_LENGTH, CONF_PAYLOAD_LENGTH,
@ -211,14 +213,6 @@ class BinarySensorSchema(KNXPlatformSchema):
"""Voluptuous schema for KNX binary sensors.""" """Voluptuous schema for KNX binary sensors."""
PLATFORM = Platform.BINARY_SENSOR PLATFORM = Platform.BINARY_SENSOR
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_SYNC_STATE = CONF_SYNC_STATE
CONF_INVERT = CONF_INVERT
CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state"
CONF_CONTEXT_TIMEOUT = "context_timeout"
CONF_RESET_AFTER = CONF_RESET_AFTER
DEFAULT_NAME = "KNX Binary Sensor" DEFAULT_NAME = "KNX Binary Sensor"
ENTITY_SCHEMA = vol.All( ENTITY_SCHEMA = vol.All(

View File

@ -10,6 +10,7 @@ CONF_GA_STATE: Final = "state"
CONF_GA_PASSIVE: Final = "passive" CONF_GA_PASSIVE: Final = "passive"
CONF_DPT: Final = "dpt" CONF_DPT: Final = "dpt"
CONF_GA_SENSOR: Final = "ga_sensor"
CONF_GA_SWITCH: Final = "ga_switch" CONF_GA_SWITCH: Final = "ga_switch"
CONF_GA_COLOR_TEMP: Final = "ga_color_temp" CONF_GA_COLOR_TEMP: Final = "ga_color_temp"
CONF_COLOR_TEMP_MIN: Final = "color_temp_min" CONF_COLOR_TEMP_MIN: Final = "color_temp_min"

View File

@ -11,12 +11,15 @@ from homeassistant.const import (
CONF_PLATFORM, CONF_PLATFORM,
Platform, Platform,
) )
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.helpers.typing import VolDictType, VolSchemaType from homeassistant.helpers.typing import VolDictType, VolSchemaType
from ..const import ( from ..const import (
CONF_CONTEXT_TIMEOUT,
CONF_IGNORE_INTERNAL_STATE,
CONF_INVERT, CONF_INVERT,
CONF_RESET_AFTER,
CONF_RESPOND_TO_READ, CONF_RESPOND_TO_READ,
CONF_SYNC_STATE, CONF_SYNC_STATE,
DOMAIN, DOMAIN,
@ -42,6 +45,7 @@ from .const import (
CONF_GA_RED_BRIGHTNESS, CONF_GA_RED_BRIGHTNESS,
CONF_GA_RED_SWITCH, CONF_GA_RED_SWITCH,
CONF_GA_SATURATION, CONF_GA_SATURATION,
CONF_GA_SENSOR,
CONF_GA_STATE, CONF_GA_STATE,
CONF_GA_SWITCH, CONF_GA_SWITCH,
CONF_GA_WHITE_BRIGHTNESS, CONF_GA_WHITE_BRIGHTNESS,
@ -94,6 +98,29 @@ def optional_ga_schema(key: str, ga_selector: GASelector) -> VolDictType:
} }
BINARY_SENSOR_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA,
vol.Required(DOMAIN): {
vol.Required(CONF_GA_SENSOR): GASelector(write=False, state_required=True),
vol.Required(CONF_RESPOND_TO_READ, default=False): bool,
vol.Required(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_INVERT): selector.BooleanSelector(),
vol.Optional(CONF_IGNORE_INTERNAL_STATE): selector.BooleanSelector(),
vol.Optional(CONF_CONTEXT_TIMEOUT): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=0.1, unit_of_measurement="s"
)
),
vol.Optional(CONF_RESET_AFTER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=0.1, unit_of_measurement="s"
)
),
},
}
)
SWITCH_SCHEMA = vol.Schema( SWITCH_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA,
@ -213,6 +240,9 @@ ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All(
cv.key_value_schemas( cv.key_value_schemas(
CONF_PLATFORM, CONF_PLATFORM,
{ {
Platform.BINARY_SENSOR: vol.Schema(
{vol.Required(CONF_DATA): BINARY_SENSOR_SCHEMA}, extra=vol.ALLOW_EXTRA
),
Platform.SWITCH: vol.Schema( Platform.SWITCH: vol.Schema(
{vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA
), ),

View File

@ -1,10 +1,19 @@
"""Test KNX binary sensor.""" """Test KNX binary sensor."""
from datetime import timedelta from datetime import timedelta
from typing import Any
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.const import (
CONF_CONTEXT_TIMEOUT,
CONF_IGNORE_INTERNAL_STATE,
CONF_INVERT,
CONF_RESET_AFTER,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
)
from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.components.knx.schema import BinarySensorSchema
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@ -12,10 +21,12 @@ from homeassistant.const import (
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
EntityCategory, EntityCategory,
Platform,
) )
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import KnxEntityGenerator
from .conftest import KNXTestKit from .conftest import KNXTestKit
from tests.common import ( from tests.common import (
@ -60,7 +71,7 @@ async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit) -> None:
{ {
CONF_NAME: "test_invert", CONF_NAME: "test_invert",
CONF_STATE_ADDRESS: "2/2/2", CONF_STATE_ADDRESS: "2/2/2",
BinarySensorSchema.CONF_INVERT: True, CONF_INVERT: True,
}, },
] ]
} }
@ -113,7 +124,7 @@ async def test_binary_sensor_ignore_internal_state(
{ {
CONF_NAME: "test_ignore", CONF_NAME: "test_ignore",
CONF_STATE_ADDRESS: "2/2/2", CONF_STATE_ADDRESS: "2/2/2",
BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE: True, CONF_IGNORE_INTERNAL_STATE: True,
CONF_SYNC_STATE: False, CONF_SYNC_STATE: False,
}, },
] ]
@ -156,7 +167,7 @@ async def test_binary_sensor_counter(
{ {
CONF_NAME: "test", CONF_NAME: "test",
CONF_STATE_ADDRESS: "2/2/2", CONF_STATE_ADDRESS: "2/2/2",
BinarySensorSchema.CONF_CONTEXT_TIMEOUT: context_timeout, CONF_CONTEXT_TIMEOUT: context_timeout,
CONF_SYNC_STATE: False, CONF_SYNC_STATE: False,
}, },
] ]
@ -220,7 +231,7 @@ async def test_binary_sensor_reset(
{ {
CONF_NAME: "test", CONF_NAME: "test",
CONF_STATE_ADDRESS: "2/2/2", CONF_STATE_ADDRESS: "2/2/2",
BinarySensorSchema.CONF_RESET_AFTER: 1, CONF_RESET_AFTER: 1,
CONF_SYNC_STATE: False, CONF_SYNC_STATE: False,
}, },
] ]
@ -279,7 +290,7 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None:
{ {
CONF_NAME: "test", CONF_NAME: "test",
CONF_STATE_ADDRESS: _ADDRESS, CONF_STATE_ADDRESS: _ADDRESS,
BinarySensorSchema.CONF_INVERT: True, CONF_INVERT: True,
CONF_SYNC_STATE: False, CONF_SYNC_STATE: False,
}, },
] ]
@ -295,3 +306,37 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None:
await knx.receive_write(_ADDRESS, True) await knx.receive_write(_ADDRESS, True)
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF assert state.state is STATE_OFF
@pytest.mark.parametrize(
("knx_data"),
[
{
"ga_sensor": {"state": "2/2/2"},
"sync_state": True,
},
{
"ga_sensor": {"state": "2/2/2"},
"sync_state": True,
"invert": True,
},
],
)
async def test_binary_sensor_ui_create(
hass: HomeAssistant,
knx: KNXTestKit,
create_ui_entity: KnxEntityGenerator,
knx_data: dict[str, Any],
) -> None:
"""Test creating a binary sensor."""
await knx.setup_integration({})
await create_ui_entity(
platform=Platform.BINARY_SENSOR,
entity_data={"name": "test"},
knx_data=knx_data,
)
# created entity sends read-request to KNX bus
await knx.assert_read("2/2/2")
await knx.receive_response("2/2/2", not knx_data.get("invert"))
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON