Add new sensors of Kostal Plenticore integration (#103802)

This commit is contained in:
stegm 2023-11-29 14:24:09 +01:00 committed by GitHub
parent 0a13968209
commit 09d7679818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 221 additions and 84 deletions

View file

@ -3,13 +3,18 @@ from __future__ import annotations
import asyncio import asyncio
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable, Mapping
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any, TypeVar, cast from typing import Any, TypeVar, cast
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from pykoplenti import ApiClient, ApiException, AuthenticationException from pykoplenti import (
ApiClient,
ApiException,
AuthenticationException,
ExtendedApiClient,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.core import CALLBACK_TYPE, HomeAssistant
@ -51,7 +56,9 @@ class Plenticore:
async def async_setup(self) -> bool: async def async_setup(self) -> bool:
"""Set up Plenticore API client.""" """Set up Plenticore API client."""
self._client = ApiClient(async_get_clientsession(self.hass), host=self.host) self._client = ExtendedApiClient(
async_get_clientsession(self.hass), host=self.host
)
try: try:
await self._client.login(self.config_entry.data[CONF_PASSWORD]) await self._client.login(self.config_entry.data[CONF_PASSWORD])
except AuthenticationException as err: except AuthenticationException as err:
@ -124,7 +131,7 @@ class DataUpdateCoordinatorMixin:
async def async_read_data( async def async_read_data(
self, module_id: str, data_id: str self, module_id: str, data_id: str
) -> dict[str, dict[str, str]] | None: ) -> Mapping[str, Mapping[str, str]] | None:
"""Read data from Plenticore.""" """Read data from Plenticore."""
if (client := self._plenticore.client) is None: if (client := self._plenticore.client) is None:
return None return None
@ -190,7 +197,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]):
class ProcessDataUpdateCoordinator( class ProcessDataUpdateCoordinator(
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]] PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]]
): ):
"""Implementation of PlenticoreUpdateCoordinator for process data.""" """Implementation of PlenticoreUpdateCoordinator for process data."""
@ -206,18 +213,19 @@ class ProcessDataUpdateCoordinator(
return { return {
module_id: { module_id: {
process_data.id: process_data.value process_data.id: process_data.value
for process_data in fetched_data[module_id] for process_data in fetched_data[module_id].values()
} }
for module_id in fetched_data for module_id in fetched_data
} }
class SettingDataUpdateCoordinator( class SettingDataUpdateCoordinator(
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]], DataUpdateCoordinatorMixin PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]],
DataUpdateCoordinatorMixin,
): ):
"""Implementation of PlenticoreUpdateCoordinator for settings data.""" """Implementation of PlenticoreUpdateCoordinator for settings data."""
async def _async_update_data(self) -> dict[str, dict[str, str]]: async def _async_update_data(self) -> Mapping[str, Mapping[str, str]]:
client = self._plenticore.client client = self._plenticore.client
if not self._fetch or client is None: if not self._fetch or client is None:

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["kostal"], "loggers": ["kostal"],
"requirements": ["pykoplenti==1.0.0"] "requirements": ["pykoplenti==1.2.2"]
} }

View file

@ -649,6 +649,39 @@ SENSOR_PROCESS_DATA = [
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy", formatter="format_energy",
), ),
PlenticoreSensorEntityDescription(
module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischarge:Day",
name="Battery Discharge Day",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischarge:Month",
name="Battery Discharge Month",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischarge:Year",
name="Battery Discharge Year",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischarge:Total",
name="Battery Discharge Total",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
PlenticoreSensorEntityDescription( PlenticoreSensorEntityDescription(
module_id="scb:statistic:EnergyFlow", module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischargeGrid:Day", key="Statistic:EnergyDischargeGrid:Day",
@ -682,6 +715,52 @@ SENSOR_PROCESS_DATA = [
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy", formatter="format_energy",
), ),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="pv_P",
name="Sum power of all PV DC inputs",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=True,
state_class=SensorStateClass.MEASUREMENT,
formatter="format_round",
),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="Statistic:EnergyGrid:Total",
name="Energy to Grid Total",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="Statistic:EnergyGrid:Year",
name="Energy to Grid Year",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="Statistic:EnergyGrid:Month",
name="Energy to Grid Month",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="Statistic:EnergyGrid:Day",
name="Energy to Grid Day",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
] ]

View file

@ -1835,7 +1835,7 @@ pykmtronic==0.3.0
pykodi==0.2.7 pykodi==0.2.7
# homeassistant.components.kostal_plenticore # homeassistant.components.kostal_plenticore
pykoplenti==1.0.0 pykoplenti==1.2.2
# homeassistant.components.kraken # homeassistant.components.kraken
pykrakenapi==0.1.8 pykrakenapi==0.1.8

View file

@ -1388,7 +1388,7 @@ pykmtronic==0.3.0
pykodi==0.2.7 pykodi==0.2.7
# homeassistant.components.kostal_plenticore # homeassistant.components.kostal_plenticore
pykoplenti==1.0.0 pykoplenti==1.2.2
# homeassistant.components.kraken # homeassistant.components.kraken
pykrakenapi==0.1.8 pykrakenapi==0.1.8

View file

@ -49,24 +49,20 @@ def mock_plenticore() -> Generator[Plenticore, None, None]:
plenticore.client.get_version = AsyncMock() plenticore.client.get_version = AsyncMock()
plenticore.client.get_version.return_value = VersionData( plenticore.client.get_version.return_value = VersionData(
{ api_version="0.2.0",
"api_version": "0.2.0", hostname="scb",
"hostname": "scb", name="PUCK RESTful API",
"name": "PUCK RESTful API", sw_version="01.16.05025",
"sw_version": "01.16.05025",
}
) )
plenticore.client.get_me = AsyncMock() plenticore.client.get_me = AsyncMock()
plenticore.client.get_me.return_value = MeData( plenticore.client.get_me.return_value = MeData(
{ locked=False,
"locked": False, active=True,
"active": True, authenticated=True,
"authenticated": True, permissions=[],
"permissions": [], anonymous=False,
"anonymous": False, role="USER",
"role": "USER",
}
) )
plenticore.client.get_process_data = AsyncMock() plenticore.client.get_process_data = AsyncMock()

View file

@ -54,7 +54,19 @@ async def test_form_g1(
# mock of the context manager instance # mock of the context manager instance
mock_apiclient.login = AsyncMock() mock_apiclient.login = AsyncMock()
mock_apiclient.get_settings = AsyncMock( mock_apiclient.get_settings = AsyncMock(
return_value={"scb:network": [SettingsData({"id": "Hostname"})]} return_value={
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Hostname",
type="string",
),
]
}
) )
mock_apiclient.get_setting_values = AsyncMock( mock_apiclient.get_setting_values = AsyncMock(
# G1 model has the entry id "Hostname" # G1 model has the entry id "Hostname"
@ -108,7 +120,19 @@ async def test_form_g2(
# mock of the context manager instance # mock of the context manager instance
mock_apiclient.login = AsyncMock() mock_apiclient.login = AsyncMock()
mock_apiclient.get_settings = AsyncMock( mock_apiclient.get_settings = AsyncMock(
return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} return_value={
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Network:Hostname",
type="string",
),
]
}
) )
mock_apiclient.get_setting_values = AsyncMock( mock_apiclient.get_setting_values = AsyncMock(
# G1 model has the entry id "Hostname" # G1 model has the entry id "Hostname"

View file

@ -26,15 +26,13 @@ async def test_entry_diagnostics(
mock_plenticore.client.get_settings.return_value = { mock_plenticore.client.get_settings.return_value = {
"devices:local": [ "devices:local": [
SettingsData( SettingsData(
{ min="5",
"id": "Battery:MinSoc", max="100",
"unit": "%", default=None,
"default": "None", access="readwrite",
"min": 5, unit="%",
"max": 100, id="Battery:MinSoc",
"type": "byte", type="byte",
"access": "readwrite",
}
) )
] ]
} }
@ -56,12 +54,12 @@ async def test_entry_diagnostics(
"disabled_by": None, "disabled_by": None,
}, },
"client": { "client": {
"version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", "version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'",
"me": "Me(locked=False, active=True, authenticated=True, permissions=[], anonymous=False, role=USER)", "me": "is_locked=False is_active=True is_authenticated=True permissions=[] is_anonymous=False role='USER'",
"available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]},
"available_settings_data": { "available_settings_data": {
"devices:local": [ "devices:local": [
"SettingsData(id=Battery:MinSoc, unit=%, default=None, min=5, max=100,type=byte, access=readwrite)" "min='5' max='100' default=None access='readwrite' unit='%' id='Battery:MinSoc' type='byte'"
] ]
}, },
}, },

View file

@ -3,7 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pykoplenti import ApiClient, SettingsData from pykoplenti import ApiClient, ExtendedApiClient, SettingsData
import pytest import pytest
from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.components.kostal_plenticore.const import DOMAIN
@ -17,10 +17,10 @@ from tests.common import MockConfigEntry
def mock_apiclient() -> Generator[ApiClient, None, None]: def mock_apiclient() -> Generator[ApiClient, None, None]:
"""Return a mocked ApiClient class.""" """Return a mocked ApiClient class."""
with patch( with patch(
"homeassistant.components.kostal_plenticore.helper.ApiClient", "homeassistant.components.kostal_plenticore.helper.ExtendedApiClient",
autospec=True, autospec=True,
) as mock_api_class: ) as mock_api_class:
apiclient = MagicMock(spec=ApiClient) apiclient = MagicMock(spec=ExtendedApiClient)
apiclient.__aenter__.return_value = apiclient apiclient.__aenter__.return_value = apiclient
apiclient.__aexit__ = AsyncMock() apiclient.__aexit__ = AsyncMock()
mock_api_class.return_value = apiclient mock_api_class.return_value = apiclient
@ -34,7 +34,19 @@ async def test_plenticore_async_setup_g1(
) -> None: ) -> None:
"""Tests the async_setup() method of the Plenticore class for G1 models.""" """Tests the async_setup() method of the Plenticore class for G1 models."""
mock_apiclient.get_settings = AsyncMock( mock_apiclient.get_settings = AsyncMock(
return_value={"scb:network": [SettingsData({"id": "Hostname"})]} return_value={
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Hostname",
type="string",
)
]
}
) )
mock_apiclient.get_setting_values = AsyncMock( mock_apiclient.get_setting_values = AsyncMock(
# G1 model has the entry id "Hostname" # G1 model has the entry id "Hostname"
@ -74,7 +86,19 @@ async def test_plenticore_async_setup_g2(
) -> None: ) -> None:
"""Tests the async_setup() method of the Plenticore class for G2 models.""" """Tests the async_setup() method of the Plenticore class for G2 models."""
mock_apiclient.get_settings = AsyncMock( mock_apiclient.get_settings = AsyncMock(
return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} return_value={
"scb:network": [
SettingsData(
min="1",
max="63",
default=None,
access="readwrite",
unit=None,
id="Network:Hostname",
type="string",
)
]
}
) )
mock_apiclient.get_setting_values = AsyncMock( mock_apiclient.get_setting_values = AsyncMock(
# G1 model has the entry id "Hostname" # G1 model has the entry id "Hostname"

View file

@ -23,9 +23,9 @@ from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture @pytest.fixture
def mock_plenticore_client() -> Generator[ApiClient, None, None]: def mock_plenticore_client() -> Generator[ApiClient, None, None]:
"""Return a patched ApiClient.""" """Return a patched ExtendedApiClient."""
with patch( with patch(
"homeassistant.components.kostal_plenticore.helper.ApiClient", "homeassistant.components.kostal_plenticore.helper.ExtendedApiClient",
autospec=True, autospec=True,
) as plenticore_client_class: ) as plenticore_client_class:
yield plenticore_client_class.return_value yield plenticore_client_class.return_value
@ -41,39 +41,33 @@ def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list:
mock_plenticore_client.get_settings.return_value = { mock_plenticore_client.get_settings.return_value = {
"devices:local": [ "devices:local": [
SettingsData( SettingsData(
{ min="5",
"default": None, max="100",
"min": 5, default=None,
"max": 100, access="readwrite",
"access": "readwrite", unit="%",
"unit": "%", id="Battery:MinSoc",
"type": "byte", type="byte",
"id": "Battery:MinSoc",
}
), ),
SettingsData( SettingsData(
{ min="50",
"default": None, max="38000",
"min": 50, default=None,
"max": 38000, access="readwrite",
"access": "readwrite", unit="W",
"unit": "W", id="Battery:MinHomeComsumption",
"type": "byte", type="byte",
"id": "Battery:MinHomeComsumption",
}
), ),
], ],
"scb:network": [ "scb:network": [
SettingsData( SettingsData(
{ min="1",
"min": "1", max="63",
"default": None, default=None,
"access": "readwrite", access="readwrite",
"unit": None, unit=None,
"id": "Hostname", id="Hostname",
"type": "string", type="string",
"max": "63",
}
) )
], ],
} }
@ -129,15 +123,13 @@ async def test_setup_no_entries(
mock_plenticore_client.get_settings.return_value = { mock_plenticore_client.get_settings.return_value = {
"scb:network": [ "scb:network": [
SettingsData( SettingsData(
{ min="1",
"min": "1", max="63",
"default": None, default=None,
"access": "readwrite", access="readwrite",
"unit": None, unit=None,
"id": "Hostname", id="Hostname",
"type": "string", type="string",
"max": "63",
}
) )
], ],
} }

View file

@ -18,8 +18,24 @@ async def test_select_battery_charging_usage_available(
mock_plenticore.client.get_settings.return_value = { mock_plenticore.client.get_settings.return_value = {
"devices:local": [ "devices:local": [
SettingsData({"id": "Battery:SmartBatteryControl:Enable"}), SettingsData(
SettingsData({"id": "Battery:TimeControl:Enable"}), 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",
),
] ]
} }