Add new settings option to kostal plenticore (#153162)

This commit is contained in:
stegm
2025-11-17 15:45:50 +01:00
committed by GitHub
parent b431bb197a
commit 2fe20553b3
8 changed files with 595 additions and 198 deletions

View File

@@ -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 not self._fetch or client is None:
if (client := self._plenticore.client) is None:
return {}
_LOGGER.debug("Fetching %s for %s", self.name, self._fetch)
fetch = defaultdict(set)
return await client.get_setting_values(self._fetch)
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, fetch)
return await client.get_setting_values(fetch)
class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):

View File

@@ -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

View File

@@ -5,12 +5,13 @@ from __future__ import annotations
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
@@ -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(
@@ -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)
@@ -136,7 +188,6 @@ class PlenticoreDataSwitch(
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
@@ -189,3 +240,98 @@ class PlenticoreDataSwitch(
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

View File

@@ -2,18 +2,67 @@
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from collections.abc import Generator, Iterable
import copy
from unittest.mock import patch
from pykoplenti import MeData, VersionData
from pykoplenti import ExtendedApiClient, MeData, SettingsData, VersionData
import pytest
from homeassistant.components.kostal_plenticore.coordinator import Plenticore
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from tests.common import MockConfigEntry
DEFAULT_SETTING_VALUES = {
"devices:local": {
"Properties:StringCnt": "2",
"Properties:String0Features": "1",
"Properties:String1Features": "1",
"Properties:SerialNo": "42",
"Branding:ProductName1": "PLENTICORE",
"Branding:ProductName2": "plus 10",
"Properties:VersionIOC": "01.45",
"Properties:VersionMC": "01.46",
"Battery:MinSoc": "5",
"Battery:MinHomeComsumption": "50",
},
"scb:network": {"Hostname": "scb"},
}
DEFAULT_SETTINGS = {
"devices:local": [
SettingsData(
min="5",
max="100",
default=None,
access="readwrite",
unit="%",
id="Battery:MinSoc",
type="byte",
),
SettingsData(
min="50",
max="38000",
default=None,
access="readwrite",
unit="W",
id="Battery:MinHomeComsumption",
type="byte",
),
],
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Hostname",
type="string",
)
],
}
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
@@ -42,37 +91,67 @@ def mock_installer_config_entry() -> MockConfigEntry:
@pytest.fixture
def mock_plenticore() -> Generator[Plenticore]:
"""Set up a Plenticore mock with some default values."""
def mock_get_settings() -> dict[str, list[SettingsData]]:
"""Add setting data to mock_plenticore_client.
Returns a dictionary with setting data which can be mutated by test cases.
"""
return copy.deepcopy(DEFAULT_SETTINGS)
@pytest.fixture
def mock_get_setting_values() -> dict[str, dict[str, str]]:
"""Add setting values to mock_plenticore_client.
Returns a dictionary with setting values which can be mutated by test cases.
"""
# Add default settings values - this values are always retrieved by the integration on startup
return copy.deepcopy(DEFAULT_SETTING_VALUES)
@pytest.fixture
def mock_plenticore_client(
mock_get_settings: dict[str, list[SettingsData]],
mock_get_setting_values: dict[str, dict[str, str]],
) -> Generator[ExtendedApiClient]:
"""Return a patched ExtendedApiClient."""
with patch(
"homeassistant.components.kostal_plenticore.Plenticore", autospec=True
) as mock_api_class:
# setup
plenticore = mock_api_class.return_value
plenticore.async_setup = AsyncMock()
plenticore.async_setup.return_value = True
"homeassistant.components.kostal_plenticore.coordinator.ExtendedApiClient",
autospec=True,
) as plenticore_client_class:
plenticore.device_info = DeviceInfo(
configuration_url="http://192.168.1.2",
identifiers={("kostal_plenticore", "12345")},
manufacturer="Kostal",
model="PLENTICORE plus 10",
name="scb",
sw_version="IOC: 01.45 MC: 01.46",
def default_settings_data(*args):
# the get_setting_values method can be called with different argument types and numbers
match args:
case (str() as module_id, str() as data_id):
request = {module_id: [data_id]}
case (str() as module_id, Iterable() as data_ids):
request = {module_id: data_ids}
case ({},):
request = args[0]
case _:
raise NotImplementedError
result = {}
for module_id, data_ids in request.items():
if (values := mock_get_setting_values.get(module_id)) is not None:
result[module_id] = {}
for data_id in data_ids:
if data_id in values:
result[module_id][data_id] = values[data_id]
else:
raise ValueError(
f"Missing data_id {data_id} in module {module_id}"
)
else:
raise ValueError(f"Missing module_id {module_id}")
plenticore.client = MagicMock()
return result
plenticore.client.get_version = AsyncMock()
plenticore.client.get_version.return_value = VersionData(
api_version="0.2.0",
hostname="scb",
name="PUCK RESTful API",
sw_version="01.16.05025",
)
plenticore.client.get_me = AsyncMock()
plenticore.client.get_me.return_value = MeData(
client = plenticore_client_class.return_value
client.get_setting_values.side_effect = default_settings_data
client.get_settings.return_value = mock_get_settings
client.get_me.return_value = MeData(
locked=False,
active=True,
authenticated=True,
@@ -80,11 +159,14 @@ def mock_plenticore() -> Generator[Plenticore]:
anonymous=False,
role="USER",
)
client.get_version.return_value = VersionData(
api_version="0.2.0",
hostname="scb",
name="PUCK RESTful API",
sw_version="01.16.05025",
)
plenticore.client.get_process_data = AsyncMock()
plenticore.client.get_settings = AsyncMock()
yield plenticore
yield client
@pytest.fixture

View File

@@ -1,9 +1,8 @@
"""Test Kostal Plenticore diagnostics."""
from pykoplenti import SettingsData
from unittest.mock import Mock
from homeassistant.components.diagnostics import REDACTED
from homeassistant.components.kostal_plenticore.coordinator import Plenticore
from homeassistant.core import HomeAssistant
from tests.common import ANY, MockConfigEntry
@@ -14,30 +13,16 @@ from tests.typing import ClientSessionGenerator
async def test_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_plenticore: Plenticore,
mock_plenticore_client: Mock,
init_integration: MockConfigEntry,
) -> None:
"""Test config entry diagnostics."""
# set some test process and settings data for the diagnostics output
mock_plenticore.client.get_process_data.return_value = {
# set some test process data for the diagnostics output
mock_plenticore_client.get_process_data.return_value = {
"devices:local": ["HomeGrid_P", "HomePv_P"]
}
mock_plenticore.client.get_settings.return_value = {
"devices:local": [
SettingsData(
min="5",
max="100",
default=None,
access="readwrite",
unit="%",
id="Battery:MinSoc",
type="byte",
)
]
}
assert await get_diagnostics_for_config_entry(
hass, hass_client, init_integration
) == {
@@ -65,8 +50,19 @@ async def test_entry_diagnostics(
"available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]},
"available_settings_data": {
"devices:local": [
"min='5' max='100' default=None access='readwrite' unit='%' id='Battery:MinSoc' type='byte'"
]
"min='5' max='100' default=None access='readwrite' unit='%' id='Battery:MinSoc' type='byte'",
"min='50' max='38000' default=None access='readwrite' unit='W' id='Battery:MinHomeComsumption' type='byte'",
],
"scb:network": [
"min='1' max='63' default=None access='readwrite' unit=None id='Hostname' type='string'"
],
},
},
"configuration": {
"devices:local": {
"Properties:StringCnt": "2",
"Properties:String0Features": "1",
"Properties:String1Features": "1",
},
},
"device": {
@@ -78,3 +74,28 @@ async def test_entry_diagnostics(
"sw_version": "IOC: 01.45 MC: 01.46",
},
}
async def test_entry_diagnostics_invalid_string_count(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_plenticore_client: Mock,
mock_get_setting_values: Mock,
init_integration: MockConfigEntry,
) -> None:
"""Test config entry diagnostics if string count is invalid."""
# set some test process data for the diagnostics output
mock_plenticore_client.get_process_data.return_value = {
"devices:local": ["HomeGrid_P", "HomePv_P"]
}
mock_get_setting_values["devices:local"]["Properties:StringCnt"] = "invalid"
diagnostic_data = await get_diagnostics_for_config_entry(
hass, hass_client, init_integration
)
assert diagnostic_data["configuration"] == {
"devices:local": {"Properties:StringCnt": "invalid"}
}

View File

@@ -1,8 +1,6 @@
"""Test Kostal Plenticore number."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import patch
from pykoplenti import ApiClient, SettingsData
import pytest
@@ -21,75 +19,9 @@ from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture
def mock_plenticore_client() -> Generator[ApiClient]:
"""Return a patched ExtendedApiClient."""
with patch(
"homeassistant.components.kostal_plenticore.coordinator.ExtendedApiClient",
autospec=True,
) as plenticore_client_class:
yield plenticore_client_class.return_value
@pytest.fixture
def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list:
"""Add a setting value to the given Plenticore client.
Returns a list with setting values which can be extended by test cases.
"""
mock_plenticore_client.get_settings.return_value = {
"devices:local": [
SettingsData(
min="5",
max="100",
default=None,
access="readwrite",
unit="%",
id="Battery:MinSoc",
type="byte",
),
SettingsData(
min="50",
max="38000",
default=None,
access="readwrite",
unit="W",
id="Battery:MinHomeComsumption",
type="byte",
),
],
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Hostname",
type="string",
)
],
}
# this values are always retrieved by the integration on startup
setting_values = [
{
"devices:local": {
"Properties:SerialNo": "42",
"Branding:ProductName1": "PLENTICORE",
"Branding:ProductName2": "plus 10",
"Properties:VersionIOC": "01.45",
"Properties:VersionMC": " 01.46",
},
"scb:network": {"Hostname": "scb"},
}
]
mock_plenticore_client.get_setting_values.side_effect = setting_values
return setting_values
pytestmark = [
pytest.mark.usefixtures("mock_plenticore_client"),
]
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@@ -97,8 +29,6 @@ async def test_setup_all_entries(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_plenticore_client: ApiClient,
mock_get_setting_values: list,
) -> None:
"""Test if all available entries are setup."""
@@ -118,13 +48,14 @@ async def test_setup_no_entries(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_plenticore_client: ApiClient,
mock_get_setting_values: list,
mock_get_settings: dict[str, list[SettingsData]],
) -> None:
"""Test that no entries are setup if Plenticore does not provide data."""
# remove all settings except hostname which is used during setup
mock_plenticore_client.get_settings.return_value = {
mock_get_settings.clear()
mock_get_settings.update(
{
"scb:network": [
SettingsData(
min="1",
@@ -135,8 +66,9 @@ async def test_setup_no_entries(
id="Hostname",
type="string",
)
],
]
}
)
mock_config_entry.add_to_hass(hass)
@@ -151,12 +83,11 @@ async def test_setup_no_entries(
async def test_number_has_value(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_plenticore_client: ApiClient,
mock_get_setting_values: list,
mock_get_setting_values: dict[str, dict[str, str]],
) -> None:
"""Test if number has a value if data is provided on update."""
mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}})
mock_get_setting_values["devices:local"]["Battery:MinSoc"] = "42"
mock_config_entry.add_to_hass(hass)
@@ -176,11 +107,12 @@ async def test_number_has_value(
async def test_number_is_unavailable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_plenticore_client: ApiClient,
mock_get_setting_values: list,
mock_get_setting_values: dict[str, dict[str, str]],
) -> None:
"""Test if number is unavailable if no data is provided on update."""
del mock_get_setting_values["devices:local"]["Battery:MinSoc"]
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
@@ -198,11 +130,11 @@ async def test_set_value(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_plenticore_client: ApiClient,
mock_get_setting_values: list,
mock_get_setting_values: dict[str, dict[str, str]],
) -> None:
"""Test if a new value could be set."""
mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}})
mock_get_setting_values["devices:local"]["Battery:MinSoc"] = "42"
mock_config_entry.add_to_hass(hass)

View File

@@ -1,24 +1,28 @@
"""Test the Kostal Plenticore Solar Inverter select platform."""
from pykoplenti import SettingsData
import pytest
from homeassistant.components.kostal_plenticore.coordinator import Plenticore
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
pytestmark = [
pytest.mark.usefixtures("mock_plenticore_client"),
]
async def test_select_battery_charging_usage_available(
hass: HomeAssistant,
mock_plenticore: Plenticore,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_get_settings: dict[str, list[SettingsData]],
) -> None:
"""Test that the battery charging usage select entity is added if the settings are available."""
mock_plenticore.client.get_settings.return_value = {
"devices:local": [
mock_get_settings["devices:local"].extend(
[
SettingsData(
min=None,
max=None,
@@ -38,7 +42,7 @@ async def test_select_battery_charging_usage_available(
type="string",
),
]
}
)
mock_config_entry.add_to_hass(hass)
@@ -47,10 +51,63 @@ async def test_select_battery_charging_usage_available(
assert entity_registry.async_is_registered("select.battery_charging_usage_mode")
entity = entity_registry.async_get("select.battery_charging_usage_mode")
assert entity.capabilities.get("options") == [
"None",
"Battery:SmartBatteryControl:Enable",
"Battery:TimeControl:Enable",
]
async def test_select_battery_charging_usage_excess_energy_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_get_settings: dict[str, list[SettingsData]],
mock_get_setting_values: dict[str, dict[str, str]],
) -> None:
"""Test that the battery charging usage select entity contains the option for excess AC energy."""
mock_get_settings["devices:local"].extend(
[
SettingsData(
min=None,
max=None,
default=None,
access="readwrite",
unit=None,
id="Battery:SmartBatteryControl:Enable",
type="string",
),
SettingsData(
min=None,
max=None,
default=None,
access="readwrite",
unit=None,
id="Battery:TimeControl:Enable",
type="string",
),
]
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert entity_registry.async_is_registered("select.battery_charging_usage_mode")
entity = entity_registry.async_get("select.battery_charging_usage_mode")
assert entity.capabilities.get("options") == [
"None",
"Battery:SmartBatteryControl:Enable",
"Battery:TimeControl:Enable",
]
async def test_select_battery_charging_usage_not_available(
hass: HomeAssistant,
mock_plenticore: Plenticore,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:

View File

@@ -1,23 +1,39 @@
"""Test the Kostal Plenticore Solar Inverter switch platform."""
from pykoplenti import SettingsData
from datetime import timedelta
from unittest.mock import Mock
from homeassistant.components.kostal_plenticore.coordinator import Plenticore
from pykoplenti import SettingsData
import pytest
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
pytestmark = [
pytest.mark.usefixtures("mock_plenticore_client"),
]
async def test_installer_setting_not_available(
hass: HomeAssistant,
mock_plenticore: Plenticore,
mock_get_settings: dict[str, list[SettingsData]],
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that the manual charge setting is not available when not using the installer login."""
mock_plenticore.client.get_settings.return_value = {
mock_get_settings.update(
{
"devices:local": [
SettingsData(
min=None,
@@ -30,6 +46,7 @@ async def test_installer_setting_not_available(
)
]
}
)
mock_config_entry.add_to_hass(hass)
@@ -41,13 +58,13 @@ async def test_installer_setting_not_available(
async def test_installer_setting_available(
hass: HomeAssistant,
mock_plenticore: Plenticore,
mock_get_settings: dict[str, list[SettingsData]],
mock_installer_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that the manual charge setting is available when using the installer login."""
mock_plenticore.client.get_settings.return_value = {
mock_get_settings.update(
{
"devices:local": [
SettingsData(
min=None,
@@ -60,6 +77,7 @@ async def test_installer_setting_available(
)
]
}
)
mock_installer_config_entry.add_to_hass(hass)
@@ -67,3 +85,112 @@ async def test_installer_setting_available(
await hass.async_block_till_done()
assert entity_registry.async_is_registered("switch.scb_battery_manual_charge")
async def test_invalid_string_count_value(
hass: HomeAssistant,
mock_get_setting_values: dict[str, dict[str, str]],
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that an invalid string count value is handled correctly."""
mock_get_setting_values["devices:local"].update({"Properties:StringCnt": "invalid"})
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# ensure no shadow management switch entities were registered
assert [
name
for name, _ in entity_registry.entities.items()
if name.startswith("switch.scb_shadow_management_dc_string_")
] == []
@pytest.mark.parametrize(
("shadow_mgmt", "string"),
[
("0", (STATE_OFF, STATE_OFF)),
("1", (STATE_ON, STATE_OFF)),
("2", (STATE_OFF, STATE_ON)),
("3", (STATE_ON, STATE_ON)),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_shadow_management_switch_state(
hass: HomeAssistant,
mock_get_setting_values: dict[str, dict[str, str]],
mock_config_entry: MockConfigEntry,
shadow_mgmt: str,
string: tuple[str, str],
) -> None:
"""Test that the state of the shadow management switch is correct."""
mock_get_setting_values["devices:local"].update(
{"Properties:StringCnt": "2", "Generator:ShadowMgmt:Enable": shadow_mgmt}
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300))
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("switch.scb_shadow_management_dc_string_1")
assert state is not None
assert state.state == string[0]
state = hass.states.get("switch.scb_shadow_management_dc_string_2")
assert state is not None
assert state.state == string[1]
@pytest.mark.parametrize(
("initial_shadow_mgmt", "dc_string", "service", "shadow_mgmt"),
[
("0", 1, SERVICE_TURN_ON, "1"),
("0", 2, SERVICE_TURN_ON, "2"),
("2", 1, SERVICE_TURN_ON, "3"),
("1", 2, SERVICE_TURN_ON, "3"),
("1", 1, SERVICE_TURN_OFF, "0"),
("2", 2, SERVICE_TURN_OFF, "0"),
("3", 1, SERVICE_TURN_OFF, "2"),
("3", 2, SERVICE_TURN_OFF, "1"),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_shadow_management_switch_action(
hass: HomeAssistant,
mock_get_setting_values: dict[str, dict[str, str]],
mock_plenticore_client: Mock,
mock_config_entry: MockConfigEntry,
initial_shadow_mgmt: str,
dc_string: int,
service: str,
shadow_mgmt: str,
) -> None:
"""Test that the shadow management can be switch on/off."""
mock_get_setting_values["devices:local"].update(
{
"Properties:StringCnt": "2",
"Generator:ShadowMgmt:Enable": initial_shadow_mgmt,
}
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300))
await hass.async_block_till_done(wait_background_tasks=True)
await hass.services.async_call(
SWITCH_DOMAIN,
service,
target={ATTR_ENTITY_ID: f"switch.scb_shadow_management_dc_string_{dc_string}"},
blocking=True,
)
mock_plenticore_client.set_setting_values.assert_called_with(
"devices:local", {"Generator:ShadowMgmt:Enable": shadow_mgmt}
)