Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(quality_scale)\.yaml|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(conditions|quality_scale|services|triggers)\.yaml|homeassistant/brands/.*\.json|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ homeassistant.components.wiz.*
homeassistant.components.wled.*
homeassistant.components.workday.*
homeassistant.components.worldclock.*
homeassistant.components.xbox.*
homeassistant.components.xiaomi_ble.*
homeassistant.components.yale_smart_alarm.*
homeassistant.components.yalexs_ble.*
Expand Down
11 changes: 9 additions & 2 deletions homeassistant/components/icloud/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
PyiCloudFailedLoginException,
PyiCloudNoDevicesException,
PyiCloudServiceNotActivatedException,
PyiCloudServiceUnavailable,
)
from pyicloud.services.findmyiphone import AppleDevice

Expand Down Expand Up @@ -130,15 +131,21 @@ def setup(self) -> None:
except (
PyiCloudServiceNotActivatedException,
PyiCloudNoDevicesException,
PyiCloudServiceUnavailable,
) as err:
_LOGGER.error("No iCloud device found")
raise ConfigEntryNotReady from err

self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
if user_info is None:
raise ConfigEntryNotReady("No user info found in iCloud devices response")

self._owner_fullname = (
f"{user_info.get('firstName')} {user_info.get('lastName')}"
)

self._family_members_fullname = {}
if user_info.get("membersInfo") is not None:
for prs_id, member in user_info["membersInfo"].items():
for prs_id, member in user_info.get("membersInfo").items():
self._family_members_fullname[prs_id] = (
f"{member['firstName']} {member['lastName']}"
)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/icloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/icloud",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.1.0"]
"requirements": ["pyicloud==2.2.0"]
}
17 changes: 13 additions & 4 deletions homeassistant/components/kostal_plenticore/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,23 @@ class SettingDataUpdateCoordinator(
"""Implementation of PlenticoreUpdateCoordinator for settings data."""

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

if not self._fetch or client is None:
fetch = defaultdict(set)

for module_id, data_ids in self._fetch.items():
fetch[module_id].update(data_ids)

for module_id, data_id in self.async_contexts():
fetch[module_id].add(data_id)

if not fetch:
return {}

_LOGGER.debug("Fetching %s for %s", self.name, self._fetch)
_LOGGER.debug("Fetching %s for %s", self.name, fetch)

return await client.get_setting_values(self._fetch)
return await client.get_setting_values(fetch)


class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/kostal_plenticore/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ async def async_get_config_entry_diagnostics(
},
}

# Add important information how the inverter is configured
string_count_setting = await plenticore.client.get_setting_values(
"devices:local", "Properties:StringCnt"
)
try:
string_count = int(
string_count_setting["devices:local"]["Properties:StringCnt"]
)
except ValueError:
string_count = 0

configuration_settings = await plenticore.client.get_setting_values(
"devices:local",
(
"Properties:StringCnt",
*(f"Properties:String{idx}Features" for idx in range(string_count)),
),
)

data["configuration"] = {
**configuration_settings,
}

device_info = {**plenticore.device_info}
device_info[ATTR_IDENTIFIERS] = REDACTED # contains serial number
data["device"] = device_info
Expand Down
152 changes: 149 additions & 3 deletions homeassistant/components/kostal_plenticore/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from typing import Any, Final

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

Expand Down Expand Up @@ -66,7 +67,7 @@ async def async_setup_entry(
"""Add kostal plenticore Switch."""
plenticore = entry.runtime_data

entities = []
entities: list[Entity] = []

available_settings_data = await plenticore.client.get_settings()
settings_data_update_coordinator = SettingDataUpdateCoordinator(
Expand Down Expand Up @@ -103,6 +104,57 @@ async def async_setup_entry(
)
)

# add shadow management switches for strings which support it
string_count_setting = await plenticore.client.get_setting_values(
"devices:local", "Properties:StringCnt"
)
try:
string_count = int(
string_count_setting["devices:local"]["Properties:StringCnt"]
)
except ValueError:
string_count = 0

dc_strings = tuple(range(string_count))
dc_string_feature_ids = tuple(
PlenticoreShadowMgmtSwitch.DC_STRING_FEATURE_DATA_ID % dc_string
for dc_string in dc_strings
)

dc_string_features = await plenticore.client.get_setting_values(
PlenticoreShadowMgmtSwitch.MODULE_ID,
dc_string_feature_ids,
)

for dc_string, dc_string_feature_id in zip(
dc_strings, dc_string_feature_ids, strict=True
):
try:
dc_string_feature = int(
dc_string_features[PlenticoreShadowMgmtSwitch.MODULE_ID][
dc_string_feature_id
]
)
except ValueError:
dc_string_feature = 0

if dc_string_feature == PlenticoreShadowMgmtSwitch.SHADOW_MANAGEMENT_SUPPORT:
entities.append(
PlenticoreShadowMgmtSwitch(
settings_data_update_coordinator,
dc_string,
entry.entry_id,
entry.title,
plenticore.device_info,
)
)
else:
_LOGGER.debug(
"Skipping shadow management for DC string %d, not supported (Feature: %d)",
dc_string + 1,
dc_string_feature,
)

async_add_entities(entities)


Expand Down Expand Up @@ -136,7 +188,6 @@ def __init__(
self.off_value = description.off_value
self.off_label = description.off_label
self._attr_unique_id = f"{entry_id}_{description.module_id}_{description.key}"

self._attr_device_info = device_info

@property
Expand Down Expand Up @@ -189,3 +240,98 @@ def is_on(self) -> bool:
f"{self.platform_name} {self._name} {self.off_label}"
)
return bool(self.coordinator.data[self.module_id][self.data_id] == self._is_on)


class PlenticoreShadowMgmtSwitch(
CoordinatorEntity[SettingDataUpdateCoordinator], SwitchEntity
):
"""Representation of a Plenticore Switch for shadow management.

The shadow management switch can be controlled for each DC string separately. The DC string is
coded as bit in a single settings value, bit 0 for DC string 1, bit 1 for DC string 2, etc.

Not all DC strings are available for shadown management, for example if one of them is used
for a battery.
"""

_attr_entity_category = EntityCategory.CONFIG
entity_description: SwitchEntityDescription

MODULE_ID: Final = "devices:local"

SHADOW_DATA_ID: Final = "Generator:ShadowMgmt:Enable"
"""Settings id for the bit coded shadow management."""

DC_STRING_FEATURE_DATA_ID: Final = "Properties:String%dFeatures"
"""Settings id pattern for the DC string features."""

SHADOW_MANAGEMENT_SUPPORT: Final = 1
"""Feature value for shadow management support in the DC string features."""

def __init__(
self,
coordinator: SettingDataUpdateCoordinator,
dc_string: int,
entry_id: str,
platform_name: str,
device_info: DeviceInfo,
) -> None:
"""Create a new Switch Entity for Plenticore shadow management."""
super().__init__(coordinator, context=(self.MODULE_ID, self.SHADOW_DATA_ID))

self._mask: Final = 1 << dc_string

self.entity_description = SwitchEntityDescription(
key=f"ShadowMgmt{dc_string}",
name=f"Shadow Management DC string {dc_string + 1}",
entity_registry_enabled_default=False,
)

self.platform_name = platform_name
self._attr_name = f"{platform_name} {self.entity_description.name}"
self._attr_unique_id = (
f"{entry_id}_{self.MODULE_ID}_{self.SHADOW_DATA_ID}_{dc_string}"
)
self._attr_device_info = device_info

@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.data is not None
and self.MODULE_ID in self.coordinator.data
and self.SHADOW_DATA_ID in self.coordinator.data[self.MODULE_ID]
)

def _get_shadow_mgmt_value(self) -> int:
"""Return the current shadow management value for all strings as integer."""
try:
return int(self.coordinator.data[self.MODULE_ID][self.SHADOW_DATA_ID])
except ValueError:
return 0

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn shadow management on."""
shadow_mgmt_value = self._get_shadow_mgmt_value()
shadow_mgmt_value |= self._mask

if await self.coordinator.async_write_data(
self.MODULE_ID, {self.SHADOW_DATA_ID: str(shadow_mgmt_value)}
):
await self.coordinator.async_request_refresh()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn shadow management off."""
shadow_mgmt_value = self._get_shadow_mgmt_value()
shadow_mgmt_value &= ~self._mask

if await self.coordinator.async_write_data(
self.MODULE_ID, {self.SHADOW_DATA_ID: str(shadow_mgmt_value)}
):
await self.coordinator.async_request_refresh()

@property
def is_on(self) -> bool:
"""Return true if shadow management is on."""
return (self._get_shadow_mgmt_value() & self._mask) != 0
22 changes: 7 additions & 15 deletions homeassistant/components/lcn/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support for LCN binary sensors."""

from collections.abc import Iterable
from datetime import timedelta
from functools import partial

import pypck
Expand All @@ -19,6 +20,7 @@
from .helpers import InputType, LcnConfigEntry

PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(minutes=1)


def add_lcn_entities(
Expand Down Expand Up @@ -69,21 +71,11 @@ def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
config[CONF_DOMAIN_DATA][CONF_SOURCE]
]

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(
self.bin_sensor_port
)

async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if not self.device_connection.is_group:
await self.device_connection.cancel_status_request_handler(
self.bin_sensor_port
)
async def async_update(self) -> None:
"""Update the state of the entity."""
await self.device_connection.request_status_binary_sensors(
SCAN_INTERVAL.seconds
)

def input_received(self, input_obj: InputType) -> None:
"""Set sensor value when LCN input object (command) is received."""
Expand Down
Loading
Loading