Skip to content

Commit 8b0ff3c

Browse files
committed
Add UI to create KNX Cover entities
1 parent 1978e94 commit 8b0ff3c

File tree

8 files changed

+504
-80
lines changed

8 files changed

+504
-80
lines changed

homeassistant/components/knx/const.py

Lines changed: 1 addition & 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
}

homeassistant/components/knx/cover.py

Lines changed: 155 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,81 @@
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
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+
CONF_INVERT_ANGLE,
47+
CONF_INVERT_POSITION,
48+
CONF_INVERT_UPDOWN,
49+
CONF_TRAVELLING_TIME_DOWN,
50+
CONF_TRAVELLING_TIME_UP,
51+
)
3252

3353

3454
async def async_setup_entry(
3555
hass: HomeAssistant,
3656
config_entry: config_entries.ConfigEntry,
3757
async_add_entities: AddConfigEntryEntitiesCallback,
3858
) -> None:
39-
"""Set up cover(s) for KNX platform."""
59+
"""Set up the KNX cover platform."""
4060
knx_module = hass.data[KNX_MODULE_KEY]
41-
config: list[ConfigType] = knx_module.config_yaml[Platform.COVER]
61+
platform = async_get_current_platform()
62+
knx_module.config_store.add_platform(
63+
platform=Platform.COVER,
64+
controller=KnxUiEntityPlatformController(
65+
knx_module=knx_module,
66+
entity_platform=platform,
67+
entity_class=KnxUiCover,
68+
),
69+
)
4270

43-
async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config)
71+
entities: list[KnxYamlEntity | KnxUiEntity] = []
72+
if yaml_platform_config := knx_module.config_yaml.get(Platform.COVER):
73+
entities.extend(
74+
KnxYamlCover(knx_module, entity_config)
75+
for entity_config in yaml_platform_config
76+
)
77+
if ui_config := knx_module.config_store.data["entities"].get(Platform.COVER):
78+
entities.extend(
79+
KnxUiCover(knx_module, unique_id, config)
80+
for unique_id, config in ui_config.items()
81+
)
82+
if entities:
83+
async_add_entities(entities)
4484

4585

46-
class KNXCover(KnxYamlEntity, CoverEntity):
86+
class _KnxCover(CoverEntity):
4787
"""Representation of a KNX cover."""
4888

4989
_device: XknxCover
5090

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)
91+
def init_base(self) -> None:
92+
"""Initialize common attributes - may be based on xknx device instance."""
7993
_supports_tilt = False
8094
self._attr_supported_features = (
81-
CoverEntityFeature.CLOSE
82-
| CoverEntityFeature.OPEN
83-
| CoverEntityFeature.SET_POSITION
95+
CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN
8496
)
97+
if self._device.supports_position or self._device.supports_stop:
98+
# when stop is supported, xknx travelcalculator can set position
99+
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
85100
if self._device.step.writable:
86101
_supports_tilt = True
87102
self._attr_supported_features |= (
@@ -97,13 +112,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
97112
if _supports_tilt:
98113
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
99114

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-
)
115+
self._attr_device_class = CoverDeviceClass.BLIND if _supports_tilt else None
107116

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

homeassistant/components/knx/storage/const.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,14 @@
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_INVERT_UPDOWN: Final = "invert_updown"
34+
CONF_INVERT_POSITION: Final = "invert_position"
35+
CONF_INVERT_ANGLE: Final = "invert_angle"
36+
CONF_GA_POSITION_SET: Final = "ga_position_set"
37+
CONF_GA_POSITION_STATE: Final = "ga_position_state"
38+
CONF_GA_ANGLE: Final = "ga_angle"
39+
CONF_TRAVELLING_TIME_DOWN: Final = "travelling_time_down"
40+
CONF_TRAVELLING_TIME_UP: Final = "travelling_time_up"

0 commit comments

Comments
 (0)