mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
Add new settings option to kostal plenticore (#153162)
This commit is contained in:
@@ -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]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user