|
5 | 5 | from dataclasses import dataclass |
6 | 6 | from datetime import timedelta |
7 | 7 | import logging |
8 | | -from typing import Any |
| 8 | +from typing import Any, Final |
9 | 9 |
|
10 | 10 | from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription |
11 | 11 | from homeassistant.const import EntityCategory |
12 | 12 | from homeassistant.core import HomeAssistant |
13 | 13 | from homeassistant.helpers.device_registry import DeviceInfo |
| 14 | +from homeassistant.helpers.entity import Entity |
14 | 15 | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback |
15 | 16 | from homeassistant.helpers.update_coordinator import CoordinatorEntity |
16 | 17 |
|
@@ -66,7 +67,7 @@ async def async_setup_entry( |
66 | 67 | """Add kostal plenticore Switch.""" |
67 | 68 | plenticore = entry.runtime_data |
68 | 69 |
|
69 | | - entities = [] |
| 70 | + entities: list[Entity] = [] |
70 | 71 |
|
71 | 72 | available_settings_data = await plenticore.client.get_settings() |
72 | 73 | settings_data_update_coordinator = SettingDataUpdateCoordinator( |
@@ -103,6 +104,57 @@ async def async_setup_entry( |
103 | 104 | ) |
104 | 105 | ) |
105 | 106 |
|
| 107 | + # add shadow management switches for strings which support it |
| 108 | + string_count_setting = await plenticore.client.get_setting_values( |
| 109 | + "devices:local", "Properties:StringCnt" |
| 110 | + ) |
| 111 | + try: |
| 112 | + string_count = int( |
| 113 | + string_count_setting["devices:local"]["Properties:StringCnt"] |
| 114 | + ) |
| 115 | + except ValueError: |
| 116 | + string_count = 0 |
| 117 | + |
| 118 | + dc_strings = tuple(range(string_count)) |
| 119 | + dc_string_feature_ids = tuple( |
| 120 | + PlenticoreShadowMgmtSwitch.DC_STRING_FEATURE_DATA_ID % dc_string |
| 121 | + for dc_string in dc_strings |
| 122 | + ) |
| 123 | + |
| 124 | + dc_string_features = await plenticore.client.get_setting_values( |
| 125 | + PlenticoreShadowMgmtSwitch.MODULE_ID, |
| 126 | + dc_string_feature_ids, |
| 127 | + ) |
| 128 | + |
| 129 | + for dc_string, dc_string_feature_id in zip( |
| 130 | + dc_strings, dc_string_feature_ids, strict=True |
| 131 | + ): |
| 132 | + try: |
| 133 | + dc_string_feature = int( |
| 134 | + dc_string_features[PlenticoreShadowMgmtSwitch.MODULE_ID][ |
| 135 | + dc_string_feature_id |
| 136 | + ] |
| 137 | + ) |
| 138 | + except ValueError: |
| 139 | + dc_string_feature = 0 |
| 140 | + |
| 141 | + if dc_string_feature == PlenticoreShadowMgmtSwitch.SHADOW_MANAGEMENT_SUPPORT: |
| 142 | + entities.append( |
| 143 | + PlenticoreShadowMgmtSwitch( |
| 144 | + settings_data_update_coordinator, |
| 145 | + dc_string, |
| 146 | + entry.entry_id, |
| 147 | + entry.title, |
| 148 | + plenticore.device_info, |
| 149 | + ) |
| 150 | + ) |
| 151 | + else: |
| 152 | + _LOGGER.debug( |
| 153 | + "Skipping shadow management for DC string %d, not supported (Feature: %d)", |
| 154 | + dc_string + 1, |
| 155 | + dc_string_feature, |
| 156 | + ) |
| 157 | + |
106 | 158 | async_add_entities(entities) |
107 | 159 |
|
108 | 160 |
|
@@ -136,7 +188,6 @@ def __init__( |
136 | 188 | self.off_value = description.off_value |
137 | 189 | self.off_label = description.off_label |
138 | 190 | self._attr_unique_id = f"{entry_id}_{description.module_id}_{description.key}" |
139 | | - |
140 | 191 | self._attr_device_info = device_info |
141 | 192 |
|
142 | 193 | @property |
@@ -189,3 +240,98 @@ def is_on(self) -> bool: |
189 | 240 | f"{self.platform_name} {self._name} {self.off_label}" |
190 | 241 | ) |
191 | 242 | return bool(self.coordinator.data[self.module_id][self.data_id] == self._is_on) |
| 243 | + |
| 244 | + |
| 245 | +class PlenticoreShadowMgmtSwitch( |
| 246 | + CoordinatorEntity[SettingDataUpdateCoordinator], SwitchEntity |
| 247 | +): |
| 248 | + """Representation of a Plenticore Switch for shadow management. |
| 249 | +
|
| 250 | + The shadow management switch can be controlled for each DC string separately. The DC string is |
| 251 | + coded as bit in a single settings value, bit 0 for DC string 1, bit 1 for DC string 2, etc. |
| 252 | +
|
| 253 | + Not all DC strings are available for shadown management, for example if one of them is used |
| 254 | + for a battery. |
| 255 | + """ |
| 256 | + |
| 257 | + _attr_entity_category = EntityCategory.CONFIG |
| 258 | + entity_description: SwitchEntityDescription |
| 259 | + |
| 260 | + MODULE_ID: Final = "devices:local" |
| 261 | + |
| 262 | + SHADOW_DATA_ID: Final = "Generator:ShadowMgmt:Enable" |
| 263 | + """Settings id for the bit coded shadow management.""" |
| 264 | + |
| 265 | + DC_STRING_FEATURE_DATA_ID: Final = "Properties:String%dFeatures" |
| 266 | + """Settings id pattern for the DC string features.""" |
| 267 | + |
| 268 | + SHADOW_MANAGEMENT_SUPPORT: Final = 1 |
| 269 | + """Feature value for shadow management support in the DC string features.""" |
| 270 | + |
| 271 | + def __init__( |
| 272 | + self, |
| 273 | + coordinator: SettingDataUpdateCoordinator, |
| 274 | + dc_string: int, |
| 275 | + entry_id: str, |
| 276 | + platform_name: str, |
| 277 | + device_info: DeviceInfo, |
| 278 | + ) -> None: |
| 279 | + """Create a new Switch Entity for Plenticore shadow management.""" |
| 280 | + super().__init__(coordinator, context=(self.MODULE_ID, self.SHADOW_DATA_ID)) |
| 281 | + |
| 282 | + self._mask: Final = 1 << dc_string |
| 283 | + |
| 284 | + self.entity_description = SwitchEntityDescription( |
| 285 | + key=f"ShadowMgmt{dc_string}", |
| 286 | + name=f"Shadow Management DC string {dc_string + 1}", |
| 287 | + entity_registry_enabled_default=False, |
| 288 | + ) |
| 289 | + |
| 290 | + self.platform_name = platform_name |
| 291 | + self._attr_name = f"{platform_name} {self.entity_description.name}" |
| 292 | + self._attr_unique_id = ( |
| 293 | + f"{entry_id}_{self.MODULE_ID}_{self.SHADOW_DATA_ID}_{dc_string}" |
| 294 | + ) |
| 295 | + self._attr_device_info = device_info |
| 296 | + |
| 297 | + @property |
| 298 | + def available(self) -> bool: |
| 299 | + """Return if entity is available.""" |
| 300 | + return ( |
| 301 | + super().available |
| 302 | + and self.coordinator.data is not None |
| 303 | + and self.MODULE_ID in self.coordinator.data |
| 304 | + and self.SHADOW_DATA_ID in self.coordinator.data[self.MODULE_ID] |
| 305 | + ) |
| 306 | + |
| 307 | + def _get_shadow_mgmt_value(self) -> int: |
| 308 | + """Return the current shadow management value for all strings as integer.""" |
| 309 | + try: |
| 310 | + return int(self.coordinator.data[self.MODULE_ID][self.SHADOW_DATA_ID]) |
| 311 | + except ValueError: |
| 312 | + return 0 |
| 313 | + |
| 314 | + async def async_turn_on(self, **kwargs: Any) -> None: |
| 315 | + """Turn shadow management on.""" |
| 316 | + shadow_mgmt_value = self._get_shadow_mgmt_value() |
| 317 | + shadow_mgmt_value |= self._mask |
| 318 | + |
| 319 | + if await self.coordinator.async_write_data( |
| 320 | + self.MODULE_ID, {self.SHADOW_DATA_ID: str(shadow_mgmt_value)} |
| 321 | + ): |
| 322 | + await self.coordinator.async_request_refresh() |
| 323 | + |
| 324 | + async def async_turn_off(self, **kwargs: Any) -> None: |
| 325 | + """Turn shadow management off.""" |
| 326 | + shadow_mgmt_value = self._get_shadow_mgmt_value() |
| 327 | + shadow_mgmt_value &= ~self._mask |
| 328 | + |
| 329 | + if await self.coordinator.async_write_data( |
| 330 | + self.MODULE_ID, {self.SHADOW_DATA_ID: str(shadow_mgmt_value)} |
| 331 | + ): |
| 332 | + await self.coordinator.async_request_refresh() |
| 333 | + |
| 334 | + @property |
| 335 | + def is_on(self) -> bool: |
| 336 | + """Return true if shadow management is on.""" |
| 337 | + return (self._get_shadow_mgmt_value() & self._mask) != 0 |
0 commit comments