Skip to content

Commit 307bb05

Browse files
authored
Add support to create KNX Cover entities from UI (#141944)
* Add UI to create KNX Cover entities * Use common constants source for UI and YAML config keys
1 parent b4ae08f commit 307bb05

File tree

9 files changed

+506
-90
lines changed

9 files changed

+506
-90
lines changed

homeassistant/components/knx/const.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class FanZeroMode(StrEnum):
160160

161161
SUPPORTED_PLATFORMS_UI: Final = {
162162
Platform.BINARY_SENSOR,
163+
Platform.COVER,
163164
Platform.LIGHT,
164165
Platform.SWITCH,
165166
}
@@ -182,3 +183,13 @@ class FanZeroMode(StrEnum):
182183
HVACMode.FAN_ONLY: HVACAction.FAN,
183184
HVACMode.DRY: HVACAction.DRYING,
184185
}
186+
187+
188+
class CoverConf:
189+
"""Common config keys for cover."""
190+
191+
TRAVELLING_TIME_DOWN: Final = "travelling_time_down"
192+
TRAVELLING_TIME_UP: Final = "travelling_time_up"
193+
INVERT_UPDOWN: Final = "invert_updown"
194+
INVERT_POSITION: Final = "invert_position"
195+
INVERT_ANGLE: Final = "invert_angle"

homeassistant/components/knx/cover.py

Lines changed: 150 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from __future__ import annotations
44

5-
from collections.abc import Callable
6-
from typing import Any
5+
from typing import Any, Literal
76

7+
from xknx import XKNX
88
from xknx.devices import Cover as XknxCover
99

1010
from homeassistant import config_entries
@@ -22,66 +22,76 @@
2222
Platform,
2323
)
2424
from homeassistant.core import HomeAssistant
25-
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
25+
from homeassistant.helpers.entity_platform import (
26+
AddConfigEntryEntitiesCallback,
27+
async_get_current_platform,
28+
)
2629
from homeassistant.helpers.typing import ConfigType
2730

2831
from . import KNXModule
29-
from .const import KNX_MODULE_KEY
30-
from .entity import KnxYamlEntity
32+
from .const import CONF_SYNC_STATE, DOMAIN, KNX_MODULE_KEY, CoverConf
33+
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
3134
from .schema import CoverSchema
35+
from .storage.const import (
36+
CONF_ENTITY,
37+
CONF_GA_ANGLE,
38+
CONF_GA_PASSIVE,
39+
CONF_GA_POSITION_SET,
40+
CONF_GA_POSITION_STATE,
41+
CONF_GA_STATE,
42+
CONF_GA_STEP,
43+
CONF_GA_STOP,
44+
CONF_GA_UP_DOWN,
45+
CONF_GA_WRITE,
46+
)
3247

3348

3449
async def async_setup_entry(
3550
hass: HomeAssistant,
3651
config_entry: config_entries.ConfigEntry,
3752
async_add_entities: AddConfigEntryEntitiesCallback,
3853
) -> None:
39-
"""Set up cover(s) for KNX platform."""
54+
"""Set up the KNX cover platform."""
4055
knx_module = hass.data[KNX_MODULE_KEY]
41-
config: list[ConfigType] = knx_module.config_yaml[Platform.COVER]
56+
platform = async_get_current_platform()
57+
knx_module.config_store.add_platform(
58+
platform=Platform.COVER,
59+
controller=KnxUiEntityPlatformController(
60+
knx_module=knx_module,
61+
entity_platform=platform,
62+
entity_class=KnxUiCover,
63+
),
64+
)
4265

43-
async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config)
66+
entities: list[KnxYamlEntity | KnxUiEntity] = []
67+
if yaml_platform_config := knx_module.config_yaml.get(Platform.COVER):
68+
entities.extend(
69+
KnxYamlCover(knx_module, entity_config)
70+
for entity_config in yaml_platform_config
71+
)
72+
if ui_config := knx_module.config_store.data["entities"].get(Platform.COVER):
73+
entities.extend(
74+
KnxUiCover(knx_module, unique_id, config)
75+
for unique_id, config in ui_config.items()
76+
)
77+
if entities:
78+
async_add_entities(entities)
4479

4580

46-
class KNXCover(KnxYamlEntity, CoverEntity):
81+
class _KnxCover(CoverEntity):
4782
"""Representation of a KNX cover."""
4883

4984
_device: XknxCover
5085

51-
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
52-
"""Initialize the cover."""
53-
super().__init__(
54-
knx_module=knx_module,
55-
device=XknxCover(
56-
xknx=knx_module.xknx,
57-
name=config[CONF_NAME],
58-
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
59-
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
60-
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
61-
group_address_position_state=config.get(
62-
CoverSchema.CONF_POSITION_STATE_ADDRESS
63-
),
64-
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
65-
group_address_angle_state=config.get(
66-
CoverSchema.CONF_ANGLE_STATE_ADDRESS
67-
),
68-
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
69-
travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN],
70-
travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP],
71-
invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN],
72-
invert_position=config[CoverSchema.CONF_INVERT_POSITION],
73-
invert_angle=config[CoverSchema.CONF_INVERT_ANGLE],
74-
),
75-
)
76-
self._unsubscribe_auto_updater: Callable[[], None] | None = None
77-
78-
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
86+
def init_base(self) -> None:
87+
"""Initialize common attributes - may be based on xknx device instance."""
7988
_supports_tilt = False
8089
self._attr_supported_features = (
81-
CoverEntityFeature.CLOSE
82-
| CoverEntityFeature.OPEN
83-
| CoverEntityFeature.SET_POSITION
90+
CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN
8491
)
92+
if self._device.supports_position or self._device.supports_stop:
93+
# when stop is supported, xknx travelcalculator can set position
94+
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
8595
if self._device.step.writable:
8696
_supports_tilt = True
8797
self._attr_supported_features |= (
@@ -97,13 +107,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
97107
if _supports_tilt:
98108
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
99109

100-
self._attr_device_class = config.get(CONF_DEVICE_CLASS) or (
101-
CoverDeviceClass.BLIND if _supports_tilt else None
102-
)
103-
self._attr_unique_id = (
104-
f"{self._device.updown.group_address}_"
105-
f"{self._device.position_target.group_address}"
106-
)
110+
self._attr_device_class = CoverDeviceClass.BLIND if _supports_tilt else None
107111

108112
@property
109113
def current_cover_position(self) -> int | None:
@@ -180,3 +184,102 @@ async def async_close_cover_tilt(self, **kwargs: Any) -> None:
180184
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
181185
"""Stop the cover tilt."""
182186
await self._device.stop()
187+
188+
189+
class KnxYamlCover(_KnxCover, KnxYamlEntity):
190+
"""Representation of a KNX cover configured from YAML."""
191+
192+
_device: XknxCover
193+
194+
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
195+
"""Initialize the cover."""
196+
super().__init__(
197+
knx_module=knx_module,
198+
device=XknxCover(
199+
xknx=knx_module.xknx,
200+
name=config[CONF_NAME],
201+
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
202+
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
203+
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
204+
group_address_position_state=config.get(
205+
CoverSchema.CONF_POSITION_STATE_ADDRESS
206+
),
207+
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
208+
group_address_angle_state=config.get(
209+
CoverSchema.CONF_ANGLE_STATE_ADDRESS
210+
),
211+
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
212+
travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN],
213+
travel_time_up=config[CoverConf.TRAVELLING_TIME_UP],
214+
invert_updown=config[CoverConf.INVERT_UPDOWN],
215+
invert_position=config[CoverConf.INVERT_POSITION],
216+
invert_angle=config[CoverConf.INVERT_ANGLE],
217+
),
218+
)
219+
self.init_base()
220+
221+
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
222+
self._attr_unique_id = (
223+
f"{self._device.updown.group_address}_"
224+
f"{self._device.position_target.group_address}"
225+
)
226+
if custom_device_class := config.get(CONF_DEVICE_CLASS):
227+
self._attr_device_class = custom_device_class
228+
229+
230+
def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover:
231+
"""Return a KNX Light device to be used within XKNX."""
232+
233+
def get_address(
234+
key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE
235+
) -> str | None:
236+
"""Get a single group address for given key."""
237+
return knx_config[key][address_type] if key in knx_config else None
238+
239+
def get_addresses(
240+
key: str, address_type: Literal["write", "state"] = CONF_GA_STATE
241+
) -> list[Any] | None:
242+
"""Get group address including passive addresses as list."""
243+
return (
244+
[knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]]
245+
if key in knx_config
246+
else None
247+
)
248+
249+
return XknxCover(
250+
xknx=xknx,
251+
name=name,
252+
group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE),
253+
group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE),
254+
group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE),
255+
group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE),
256+
group_address_position_state=get_addresses(CONF_GA_POSITION_STATE),
257+
group_address_angle=get_address(CONF_GA_ANGLE),
258+
group_address_angle_state=get_addresses(CONF_GA_ANGLE),
259+
travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN],
260+
travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP],
261+
invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False),
262+
invert_position=knx_config.get(CoverConf.INVERT_POSITION, False),
263+
invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False),
264+
sync_state=knx_config[CONF_SYNC_STATE],
265+
)
266+
267+
268+
class KnxUiCover(_KnxCover, KnxUiEntity):
269+
"""Representation of a KNX cover configured from the UI."""
270+
271+
_device: XknxCover
272+
273+
def __init__(
274+
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
275+
) -> None:
276+
"""Initialize KNX cover."""
277+
super().__init__(
278+
knx_module=knx_module,
279+
unique_id=unique_id,
280+
entity_config=config[CONF_ENTITY],
281+
)
282+
self._device = _create_ui_cover(
283+
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
284+
)
285+
self.init_base()

homeassistant/components/knx/schema.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
CONF_SYNC_STATE,
5757
KNX_ADDRESS,
5858
ColorTempModes,
59+
CoverConf,
5960
FanZeroMode,
6061
)
6162
from .validation import (
@@ -453,11 +454,6 @@ class CoverSchema(KNXPlatformSchema):
453454
CONF_POSITION_STATE_ADDRESS = "position_state_address"
454455
CONF_ANGLE_ADDRESS = "angle_address"
455456
CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
456-
CONF_TRAVELLING_TIME_DOWN = "travelling_time_down"
457-
CONF_TRAVELLING_TIME_UP = "travelling_time_up"
458-
CONF_INVERT_UPDOWN = "invert_updown"
459-
CONF_INVERT_POSITION = "invert_position"
460-
CONF_INVERT_ANGLE = "invert_angle"
461457

462458
DEFAULT_TRAVEL_TIME = 25
463459
DEFAULT_NAME = "KNX Cover"
@@ -474,14 +470,14 @@ class CoverSchema(KNXPlatformSchema):
474470
vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator,
475471
vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator,
476472
vol.Optional(
477-
CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
473+
CoverConf.TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
478474
): cv.positive_float,
479475
vol.Optional(
480-
CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
476+
CoverConf.TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
481477
): cv.positive_float,
482-
vol.Optional(CONF_INVERT_UPDOWN, default=False): cv.boolean,
483-
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
484-
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
478+
vol.Optional(CoverConf.INVERT_UPDOWN, default=False): cv.boolean,
479+
vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean,
480+
vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean,
485481
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
486482
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
487483
}

homeassistant/components/knx/storage/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@
2727
CONF_GA_WHITE_SWITCH: Final = "ga_white_switch"
2828
CONF_GA_HUE: Final = "ga_hue"
2929
CONF_GA_SATURATION: Final = "ga_saturation"
30+
CONF_GA_UP_DOWN: Final = "ga_up_down"
31+
CONF_GA_STOP: Final = "ga_stop"
32+
CONF_GA_STEP: Final = "ga_step"
33+
CONF_GA_POSITION_SET: Final = "ga_position_set"
34+
CONF_GA_POSITION_STATE: Final = "ga_position_state"
35+
CONF_GA_ANGLE: Final = "ga_angle"

0 commit comments

Comments
 (0)