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
from collections import defaultdict
from collections.abc import Callable
from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
import logging
from typing import Any, TypeVar, cast
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.core import CALLBACK_TYPE, HomeAssistant
@ -51,7 +56,9 @@ class Plenticore:
async def async_setup(self) -> bool:
"""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:
await self._client.login(self.config_entry.data[CONF_PASSWORD])
except AuthenticationException as err:
@ -124,7 +131,7 @@ class DataUpdateCoordinatorMixin:
async def async_read_data(
self, module_id: str, data_id: str
) -> dict[str, dict[str, str]] | None:
) -> Mapping[str, Mapping[str, str]] | None:
"""Read data from Plenticore."""
if (client := self._plenticore.client) is None:
return None
@ -190,7 +197,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]):
class ProcessDataUpdateCoordinator(
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]]
PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]]
):
"""Implementation of PlenticoreUpdateCoordinator for process data."""
@ -206,18 +213,19 @@ class ProcessDataUpdateCoordinator(
return {
module_id: {
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
}
class SettingDataUpdateCoordinator(
PlenticoreUpdateCoordinator[dict[str, dict[str, str]]], DataUpdateCoordinatorMixin
PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]],
DataUpdateCoordinatorMixin,
):
"""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
if not self._fetch or client is None:

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/kostal_plenticore",
"iot_class": "local_polling",
"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,
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(
module_id="scb:statistic:EnergyFlow",
key="Statistic:EnergyDischargeGrid:Day",
@ -682,6 +715,52 @@ SENSOR_PROCESS_DATA = [
state_class=SensorStateClass.TOTAL_INCREASING,
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
# homeassistant.components.kostal_plenticore
pykoplenti==1.0.0
pykoplenti==1.2.2
# homeassistant.components.kraken
pykrakenapi==0.1.8

View file

@ -1388,7 +1388,7 @@ pykmtronic==0.3.0
pykodi==0.2.7
# homeassistant.components.kostal_plenticore
pykoplenti==1.0.0
pykoplenti==1.2.2
# homeassistant.components.kraken
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.return_value = VersionData(
{
"api_version": "0.2.0",
"hostname": "scb",
"name": "PUCK RESTful API",
"sw_version": "01.16.05025",
}
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(
{
"locked": False,
"active": True,
"authenticated": True,
"permissions": [],
"anonymous": False,
"role": "USER",
}
locked=False,
active=True,
authenticated=True,
permissions=[],
anonymous=False,
role="USER",
)
plenticore.client.get_process_data = AsyncMock()

View file

@ -54,7 +54,19 @@ async def test_form_g1(
# mock of the context manager instance
mock_apiclient.login = 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(
# G1 model has the entry id "Hostname"
@ -108,7 +120,19 @@ async def test_form_g2(
# mock of the context manager instance
mock_apiclient.login = 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(
# 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 = {
"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",
)
]
}
@ -56,12 +54,12 @@ async def test_entry_diagnostics(
"disabled_by": None,
},
"client": {
"version": "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)",
"version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'",
"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_settings_data": {
"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 unittest.mock import AsyncMock, MagicMock, patch
from pykoplenti import ApiClient, SettingsData
from pykoplenti import ApiClient, ExtendedApiClient, SettingsData
import pytest
from homeassistant.components.kostal_plenticore.const import DOMAIN
@ -17,10 +17,10 @@ from tests.common import MockConfigEntry
def mock_apiclient() -> Generator[ApiClient, None, None]:
"""Return a mocked ApiClient class."""
with patch(
"homeassistant.components.kostal_plenticore.helper.ApiClient",
"homeassistant.components.kostal_plenticore.helper.ExtendedApiClient",
autospec=True,
) as mock_api_class:
apiclient = MagicMock(spec=ApiClient)
apiclient = MagicMock(spec=ExtendedApiClient)
apiclient.__aenter__.return_value = apiclient
apiclient.__aexit__ = AsyncMock()
mock_api_class.return_value = apiclient
@ -34,7 +34,19 @@ async def test_plenticore_async_setup_g1(
) -> None:
"""Tests the async_setup() method of the Plenticore class for G1 models."""
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(
# G1 model has the entry id "Hostname"
@ -74,7 +86,19 @@ async def test_plenticore_async_setup_g2(
) -> None:
"""Tests the async_setup() method of the Plenticore class for G2 models."""
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(
# G1 model has the entry id "Hostname"

View file

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

View file

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