Skip to content

Commit 2fe2055

Browse files
authored
Add new settings option to kostal plenticore (home-assistant#153162)
1 parent b431bb1 commit 2fe2055

File tree

8 files changed

+595
-198
lines changed

8 files changed

+595
-198
lines changed

homeassistant/components/kostal_plenticore/coordinator.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,23 @@ class SettingDataUpdateCoordinator(
237237
"""Implementation of PlenticoreUpdateCoordinator for settings data."""
238238

239239
async def _async_update_data(self) -> Mapping[str, Mapping[str, str]]:
240-
client = self._plenticore.client
240+
if (client := self._plenticore.client) is None:
241+
return {}
241242

242-
if not self._fetch or client is None:
243+
fetch = defaultdict(set)
244+
245+
for module_id, data_ids in self._fetch.items():
246+
fetch[module_id].update(data_ids)
247+
248+
for module_id, data_id in self.async_contexts():
249+
fetch[module_id].add(data_id)
250+
251+
if not fetch:
243252
return {}
244253

245-
_LOGGER.debug("Fetching %s for %s", self.name, self._fetch)
254+
_LOGGER.debug("Fetching %s for %s", self.name, fetch)
246255

247-
return await client.get_setting_values(self._fetch)
256+
return await client.get_setting_values(fetch)
248257

249258

250259
class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):

homeassistant/components/kostal_plenticore/diagnostics.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ async def async_get_config_entry_diagnostics(
3434
},
3535
}
3636

37+
# Add important information how the inverter is configured
38+
string_count_setting = await plenticore.client.get_setting_values(
39+
"devices:local", "Properties:StringCnt"
40+
)
41+
try:
42+
string_count = int(
43+
string_count_setting["devices:local"]["Properties:StringCnt"]
44+
)
45+
except ValueError:
46+
string_count = 0
47+
48+
configuration_settings = await plenticore.client.get_setting_values(
49+
"devices:local",
50+
(
51+
"Properties:StringCnt",
52+
*(f"Properties:String{idx}Features" for idx in range(string_count)),
53+
),
54+
)
55+
56+
data["configuration"] = {
57+
**configuration_settings,
58+
}
59+
3760
device_info = {**plenticore.device_info}
3861
device_info[ATTR_IDENTIFIERS] = REDACTED # contains serial number
3962
data["device"] = device_info

homeassistant/components/kostal_plenticore/switch.py

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
from dataclasses import dataclass
66
from datetime import timedelta
77
import logging
8-
from typing import Any
8+
from typing import Any, Final
99

1010
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
1111
from homeassistant.const import EntityCategory
1212
from homeassistant.core import HomeAssistant
1313
from homeassistant.helpers.device_registry import DeviceInfo
14+
from homeassistant.helpers.entity import Entity
1415
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1516
from homeassistant.helpers.update_coordinator import CoordinatorEntity
1617

@@ -66,7 +67,7 @@ async def async_setup_entry(
6667
"""Add kostal plenticore Switch."""
6768
plenticore = entry.runtime_data
6869

69-
entities = []
70+
entities: list[Entity] = []
7071

7172
available_settings_data = await plenticore.client.get_settings()
7273
settings_data_update_coordinator = SettingDataUpdateCoordinator(
@@ -103,6 +104,57 @@ async def async_setup_entry(
103104
)
104105
)
105106

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+
106158
async_add_entities(entities)
107159

108160

@@ -136,7 +188,6 @@ def __init__(
136188
self.off_value = description.off_value
137189
self.off_label = description.off_label
138190
self._attr_unique_id = f"{entry_id}_{description.module_id}_{description.key}"
139-
140191
self._attr_device_info = device_info
141192

142193
@property
@@ -189,3 +240,98 @@ def is_on(self) -> bool:
189240
f"{self.platform_name} {self._name} {self.off_label}"
190241
)
191242
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

Comments
 (0)