Add select platform to Peblar Rocksolid EV Chargers integration (#133720)

This commit is contained in:
Franck Nijhof 2024-12-21 14:23:16 +01:00 committed by GitHub
parent 5abc03c21e
commit a3febc4449
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 389 additions and 4 deletions

View File

@ -24,10 +24,12 @@ from .coordinator import (
PeblarConfigEntry,
PeblarMeterDataUpdateCoordinator,
PeblarRuntimeData,
PeblarUserConfigurationDataUpdateCoordinator,
PeblarVersionDataUpdateCoordinator,
)
PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
Platform.UPDATE,
]
@ -56,16 +58,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bo
# Setup the data coordinators
meter_coordinator = PeblarMeterDataUpdateCoordinator(hass, entry, api)
user_configuration_coordinator = PeblarUserConfigurationDataUpdateCoordinator(
hass, entry, peblar
)
version_coordinator = PeblarVersionDataUpdateCoordinator(hass, entry, peblar)
await asyncio.gather(
meter_coordinator.async_config_entry_first_refresh(),
user_configuration_coordinator.async_config_entry_first_refresh(),
version_coordinator.async_config_entry_first_refresh(),
)
# Store the runtime data
entry.runtime_data = PeblarRuntimeData(
system_information=system_information,
meter_coordinator=meter_coordinator,
system_information=system_information,
user_configuraton_coordinator=user_configuration_coordinator,
version_coordinator=version_coordinator,
)

View File

@ -5,7 +5,14 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from peblar import Peblar, PeblarApi, PeblarError, PeblarMeter, PeblarVersions
from peblar import (
Peblar,
PeblarApi,
PeblarError,
PeblarMeter,
PeblarUserConfiguration,
PeblarVersions,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@ -19,8 +26,9 @@ from .const import LOGGER
class PeblarRuntimeData:
"""Class to hold runtime data."""
system_information: PeblarSystemInformation
meter_coordinator: PeblarMeterDataUpdateCoordinator
system_information: PeblarSystemInformation
user_configuraton_coordinator: PeblarUserConfigurationDataUpdateCoordinator
version_coordinator: PeblarVersionDataUpdateCoordinator
@ -86,3 +94,29 @@ class PeblarMeterDataUpdateCoordinator(DataUpdateCoordinator[PeblarMeter]):
return await self.api.meter()
except PeblarError as err:
raise UpdateFailed(err) from err
class PeblarUserConfigurationDataUpdateCoordinator(
DataUpdateCoordinator[PeblarUserConfiguration]
):
"""Class to manage fetching Peblar user configuration data."""
def __init__(
self, hass: HomeAssistant, entry: PeblarConfigEntry, peblar: Peblar
) -> None:
"""Initialize the coordinator."""
self.peblar = peblar
super().__init__(
hass,
LOGGER,
config_entry=entry,
name=f"Peblar {entry.title} user configuration",
update_interval=timedelta(minutes=5),
)
async def _async_update_data(self) -> PeblarUserConfiguration:
"""Fetch data from the Peblar device."""
try:
return await self.peblar.user_configuration()
except PeblarError as err:
raise UpdateFailed(err) from err

View File

@ -15,6 +15,7 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
return {
"system_information": entry.runtime_data.system_information.to_dict(),
"user_configuration": entry.runtime_data.user_configuraton_coordinator.data.to_dict(),
"meter": entry.runtime_data.meter_coordinator.data.to_dict(),
"versions": {
"available": entry.runtime_data.version_coordinator.data.available.to_dict(),

View File

@ -1,5 +1,16 @@
{
"entity": {
"select": {
"smart_charging": {
"default": "mdi:lightning-bolt",
"state": {
"fast_solar": "mdi:solar-power",
"pure_solar": "mdi:solar-power-variant",
"scheduled": "mdi:calendar-clock",
"smart_solar": "mdi:solar-power"
}
}
},
"update": {
"customization": {
"default": "mdi:palette"

View File

@ -0,0 +1,95 @@
"""Support for Peblar selects."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from peblar import Peblar, PeblarUserConfiguration, SmartChargingMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import PeblarConfigEntry, PeblarUserConfigurationDataUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
class PeblarSelectEntityDescription(SelectEntityDescription):
"""Class describing Peblar select entities."""
current_fn: Callable[[PeblarUserConfiguration], str | None]
select_fn: Callable[[Peblar, str], Awaitable[Any]]
DESCRIPTIONS = [
PeblarSelectEntityDescription(
key="smart_charging",
translation_key="smart_charging",
entity_category=EntityCategory.CONFIG,
options=[
"default",
"fast_solar",
"pure_solar",
"scheduled",
"smart_solar",
],
current_fn=lambda x: x.smart_charging.value if x.smart_charging else None,
select_fn=lambda x, mode: x.smart_charging(SmartChargingMode(mode)),
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: PeblarConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Peblar select based on a config entry."""
async_add_entities(
PeblarSelectEntity(
entry=entry,
description=description,
)
for description in DESCRIPTIONS
)
class PeblarSelectEntity(
CoordinatorEntity[PeblarUserConfigurationDataUpdateCoordinator], SelectEntity
):
"""Defines a peblar select entity."""
entity_description: PeblarSelectEntityDescription
_attr_has_entity_name = True
def __init__(
self,
entry: PeblarConfigEntry,
description: PeblarSelectEntityDescription,
) -> None:
"""Initialize the select entity."""
super().__init__(entry.runtime_data.user_configuraton_coordinator)
self.entity_description = description
self._attr_unique_id = f"{entry.unique_id}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, entry.runtime_data.system_information.product_serial_number)
},
)
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
return self.entity_description.current_fn(self.coordinator.data)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.select_fn(self.coordinator.peblar, option)
await self.coordinator.async_request_refresh()

View File

@ -33,6 +33,18 @@
}
},
"entity": {
"select": {
"smart_charging": {
"name": "Smart charging",
"state": {
"default": "Default",
"fast_solar": "Fast solar",
"pure_solar": "Pure solar",
"scheduled": "Scheduled",
"smart_solar": "Smart solar"
}
}
},
"update": {
"customization": {
"name": "Customization"

View File

@ -6,7 +6,12 @@ from collections.abc import Generator
from contextlib import nullcontext
from unittest.mock import MagicMock, patch
from peblar import PeblarMeter, PeblarSystemInformation, PeblarVersions
from peblar import (
PeblarMeter,
PeblarSystemInformation,
PeblarUserConfiguration,
PeblarVersions,
)
import pytest
from homeassistant.components.peblar.const import DOMAIN
@ -51,6 +56,9 @@ def mock_peblar() -> Generator[MagicMock]:
peblar.current_versions.return_value = PeblarVersions.from_json(
load_fixture("current_versions.json", DOMAIN)
)
peblar.user_configuration.return_value = PeblarUserConfiguration.from_json(
load_fixture("user_configuration.json", DOMAIN)
)
peblar.system_information.return_value = PeblarSystemInformation.from_json(
load_fixture("system_information.json", DOMAIN)
)

View File

@ -0,0 +1,59 @@
{
"BopFallbackCurrent": 6000,
"BopHomeWizardAddress": "p1meter-093586",
"BopSource": "homewizard",
"BopSourceParameters": "{}",
"ConnectedPhases": 1,
"CurrentCtrlBopCtType": "CTK05-14",
"CurrentCtrlBopEnable": true,
"CurrentCtrlBopFuseRating": 35,
"CurrentCtrlFixedChargeCurrentLimit": 16,
"GroundMonitoring": true,
"GroupLoadBalancingEnable": false,
"GroupLoadBalancingFallbackCurrent": 6,
"GroupLoadBalancingGroupId": 1,
"GroupLoadBalancingInterface": "RS485",
"GroupLoadBalancingMaxCurrent": 0,
"GroupLoadBalancingRole": "",
"HmiBuzzerVolume": 1,
"HmiLedIntensityManual": 0,
"HmiLedIntensityMax": 100,
"HmiLedIntensityMin": 1,
"HmiLedIntensityMode": "Fixed",
"LocalRestApiAccessMode": "ReadWrite",
"LocalRestApiAllowed": true,
"LocalRestApiEnable": true,
"LocalSmartChargingAllowed": true,
"ModbusServerAccessMode": "ReadOnly",
"ModbusServerAllowed": true,
"ModbusServerEnable": true,
"PhaseRotation": "RST",
"PowerLimitInputDi1Inverse": false,
"PowerLimitInputDi1Limit": 6,
"PowerLimitInputDi2Inverse": false,
"PowerLimitInputDi2Limit": 0,
"PowerLimitInputEnable": false,
"PredefinedCpoName": "",
"ScheduledChargingAllowed": true,
"ScheduledChargingEnable": false,
"SeccOcppActive": false,
"SeccOcppUri": "",
"SessionManagerChargeWithoutAuth": false,
"SolarChargingAllowed": true,
"SolarChargingEnable": true,
"SolarChargingMode": "PureSolar",
"SolarChargingSource": "homewizard",
"SolarChargingSourceParameters": "{\"address\":\"p1meter-093586\"}",
"TimeZone": "Europe/Amsterdam",
"UserDefinedChargeLimitCurrent": 16,
"UserDefinedChargeLimitCurrentAllowed": true,
"UserDefinedHouseholdPowerLimit": 20000,
"UserDefinedHouseholdPowerLimitAllowed": true,
"UserDefinedHouseholdPowerLimitEnable": false,
"UserDefinedHouseholdPowerLimitSource": "homewizard",
"UserDefinedHouseholdPowerLimitSourceParameters": "{\"address\":\"p1meter-093586\"}",
"UserKeepSocketLocked": false,
"VDEPhaseImbalanceEnable": false,
"VDEPhaseImbalanceLimit": 20,
"WebIfUpdateHelper": true
}

View File

@ -75,6 +75,67 @@
'WlanApMacAddr': '00:0F:11:58:86:98',
'WlanStaMacAddr': '00:0F:11:58:86:99',
}),
'user_configuration': dict({
'BopFallbackCurrent': 6000,
'BopHomeWizardAddress': 'p1meter-093586',
'BopSource': 'homewizard',
'BopSourceParameters': '{}',
'ConnectedPhases': 1,
'CurrentCtrlBopCtType': 'CTK05-14',
'CurrentCtrlBopEnable': True,
'CurrentCtrlBopFuseRating': 35,
'CurrentCtrlFixedChargeCurrentLimit': 16,
'GroundMonitoring': True,
'GroupLoadBalancingEnable': False,
'GroupLoadBalancingFallbackCurrent': 6,
'GroupLoadBalancingGroupId': 1,
'GroupLoadBalancingInterface': 'RS485',
'GroupLoadBalancingMaxCurrent': 0,
'GroupLoadBalancingRole': '',
'HmiBuzzerVolume': 1,
'HmiLedIntensityManual': 0,
'HmiLedIntensityMax': 100,
'HmiLedIntensityMin': 1,
'HmiLedIntensityMode': 'Fixed',
'LocalRestApiAccessMode': 'ReadWrite',
'LocalRestApiAllowed': True,
'LocalRestApiEnable': True,
'LocalSmartChargingAllowed': True,
'ModbusServerAccessMode': 'ReadOnly',
'ModbusServerAllowed': True,
'ModbusServerEnable': True,
'PhaseRotation': 'RST',
'PowerLimitInputDi1Inverse': False,
'PowerLimitInputDi1Limit': 6,
'PowerLimitInputDi2Inverse': False,
'PowerLimitInputDi2Limit': 0,
'PowerLimitInputEnable': False,
'PredefinedCpoName': '',
'ScheduledChargingAllowed': True,
'ScheduledChargingEnable': False,
'SeccOcppActive': False,
'SeccOcppUri': '',
'SessionManagerChargeWithoutAuth': False,
'SolarChargingAllowed': True,
'SolarChargingEnable': True,
'SolarChargingMode': 'PureSolar',
'SolarChargingSource': 'homewizard',
'SolarChargingSourceParameters': dict({
'address': 'p1meter-093586',
}),
'TimeZone': 'Europe/Amsterdam',
'UserDefinedChargeLimitCurrent': 16,
'UserDefinedChargeLimitCurrentAllowed': True,
'UserDefinedHouseholdPowerLimit': 20000,
'UserDefinedHouseholdPowerLimitAllowed': True,
'UserDefinedHouseholdPowerLimitEnable': False,
'UserDefinedHouseholdPowerLimitSource': 'homewizard',
'UserKeepSocketLocked': False,
'VDEPhaseImbalanceEnable': False,
'VDEPhaseImbalanceLimit': 20,
'WebIfUpdateHelper': True,
'smart_charging': 'pure_solar',
}),
'versions': dict({
'available': dict({
'Customization': 'Peblar-1.9',

View File

@ -0,0 +1,62 @@
# serializer version: 1
# name: test_entities[select][select.peblar_ev_charger_smart_charging-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'default',
'fast_solar',
'pure_solar',
'scheduled',
'smart_solar',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.peblar_ev_charger_smart_charging',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Smart charging',
'platform': 'peblar',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'smart_charging',
'unique_id': '23-45-A4O-MOF-smart_charging',
'unit_of_measurement': None,
})
# ---
# name: test_entities[select][select.peblar_ev_charger_smart_charging-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Peblar EV Charger Smart charging',
'options': list([
'default',
'fast_solar',
'pure_solar',
'scheduled',
'smart_solar',
]),
}),
'context': <ANY>,
'entity_id': 'select.peblar_ev_charger_smart_charging',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'pure_solar',
})
# ---

View File

@ -0,0 +1,35 @@
"""Tests for the Peblar select platform."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.peblar.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.parametrize("init_integration", [Platform.SELECT], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the select entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure all entities are correctly assigned to the Peblar device
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "23-45-A4O-MOF")}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
for entity_entry in entity_entries:
assert entity_entry.device_id == device_entry.id