From 859993e443f4956845dd03846f9e5673887724ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Dec 2024 10:55:00 +0100 Subject: [PATCH] Add update platform to Peblar Rocksolid EV Chargers integration (#133570) * Add update platform to Peblar Rocksolid EV Chargers integration * Use device class translations --- homeassistant/components/peblar/__init__.py | 55 ++++++++++- .../components/peblar/coordinator.py | 55 ++++++++++- homeassistant/components/peblar/entity.py | 26 ------ homeassistant/components/peblar/icons.json | 9 ++ homeassistant/components/peblar/sensor.py | 23 +++-- homeassistant/components/peblar/strings.json | 7 ++ homeassistant/components/peblar/update.py | 93 +++++++++++++++++++ 7 files changed, 229 insertions(+), 39 deletions(-) delete mode 100644 homeassistant/components/peblar/entity.py create mode 100644 homeassistant/components/peblar/icons.json create mode 100644 homeassistant/components/peblar/update.py diff --git a/homeassistant/components/peblar/__init__.py b/homeassistant/components/peblar/__init__.py index 559b124c772..d1da6ce83b7 100644 --- a/homeassistant/components/peblar/__init__.py +++ b/homeassistant/components/peblar/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +import asyncio + from aiohttp import CookieJar from peblar import ( AccessMode, @@ -14,22 +16,34 @@ from peblar import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .coordinator import PeblarConfigEntry, PeblarMeterDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import ( + PeblarConfigEntry, + PeblarMeterDataUpdateCoordinator, + PeblarRuntimeData, + PeblarVersionDataUpdateCoordinator, +) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [ + Platform.SENSOR, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool: """Set up Peblar from a config entry.""" + # Set up connection to the Peblar charger peblar = Peblar( host=entry.data[CONF_HOST], session=async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)), ) try: await peblar.login(password=entry.data[CONF_PASSWORD]) + system_information = await peblar.system_information() api = await peblar.rest_api(enable=True, access_mode=AccessMode.READ_WRITE) except PeblarConnectionError as err: raise ConfigEntryNotReady("Could not connect to Peblar charger") from err @@ -40,10 +54,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bo "Unknown error occurred while connecting to Peblar charger" ) from err - coordinator = PeblarMeterDataUpdateCoordinator(hass, entry, api) - await coordinator.async_config_entry_first_refresh() + # Setup the data coordinators + meter_coordinator = PeblarMeterDataUpdateCoordinator(hass, entry, api) + version_coordinator = PeblarVersionDataUpdateCoordinator(hass, entry, peblar) + await asyncio.gather( + meter_coordinator.async_config_entry_first_refresh(), + version_coordinator.async_config_entry_first_refresh(), + ) - entry.runtime_data = coordinator + # Store the runtime data + entry.runtime_data = PeblarRuntimeData( + system_information=system_information, + meter_coordinator=meter_coordinator, + version_coordinator=version_coordinator, + ) + + # Peblar is a single device integration. Setting up the device directly + # during setup. This way we only have to reference it in all entities. + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + configuration_url=f"http://{entry.data[CONF_HOST]}", + connections={ + (dr.CONNECTION_NETWORK_MAC, system_information.ethernet_mac_address), + (dr.CONNECTION_NETWORK_MAC, system_information.wlan_mac_address), + }, + identifiers={(DOMAIN, system_information.product_serial_number)}, + manufacturer=system_information.product_vendor_name, + model_id=system_information.product_number, + model=system_information.product_model_name, + name="Peblar EV Charger", + serial_number=system_information.product_serial_number, + sw_version=version_coordinator.data.current.firmware, + ) + + # Forward the setup to the platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/peblar/coordinator.py b/homeassistant/components/peblar/coordinator.py index 8270905648f..f83ed8f4dda 100644 --- a/homeassistant/components/peblar/coordinator.py +++ b/homeassistant/components/peblar/coordinator.py @@ -1,16 +1,67 @@ """Data update coordinator for Peblar EV chargers.""" +from __future__ import annotations + +from dataclasses import dataclass from datetime import timedelta -from peblar import PeblarApi, PeblarError, PeblarMeter +from peblar import Peblar, PeblarApi, PeblarError, PeblarMeter, PeblarVersions from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from tests.components.peblar.conftest import PeblarSystemInformation from .const import LOGGER -type PeblarConfigEntry = ConfigEntry[PeblarMeterDataUpdateCoordinator] + +@dataclass(kw_only=True) +class PeblarRuntimeData: + """Class to hold runtime data.""" + + system_information: PeblarSystemInformation + meter_coordinator: PeblarMeterDataUpdateCoordinator + version_coordinator: PeblarVersionDataUpdateCoordinator + + +type PeblarConfigEntry = ConfigEntry[PeblarRuntimeData] + + +@dataclass(kw_only=True, frozen=True) +class PeblarVersionInformation: + """Class to hold version information.""" + + current: PeblarVersions + available: PeblarVersions + + +class PeblarVersionDataUpdateCoordinator( + DataUpdateCoordinator[PeblarVersionInformation] +): + """Class to manage fetching Peblar version information.""" + + 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} version", + update_interval=timedelta(hours=2), + ) + + async def _async_update_data(self) -> PeblarVersionInformation: + """Fetch data from the Peblar device.""" + try: + return PeblarVersionInformation( + current=await self.peblar.current_versions(), + available=await self.peblar.available_versions(), + ) + except PeblarError as err: + raise UpdateFailed(err) from err class PeblarMeterDataUpdateCoordinator(DataUpdateCoordinator[PeblarMeter]): diff --git a/homeassistant/components/peblar/entity.py b/homeassistant/components/peblar/entity.py deleted file mode 100644 index 6951cf6c21f..00000000000 --- a/homeassistant/components/peblar/entity.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Base entity for the Peblar integration.""" - -from __future__ import annotations - -from homeassistant.const import CONF_HOST -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import PeblarConfigEntry, PeblarMeterDataUpdateCoordinator - - -class PeblarEntity(CoordinatorEntity[PeblarMeterDataUpdateCoordinator]): - """Defines a Peblar entity.""" - - _attr_has_entity_name = True - - def __init__(self, entry: PeblarConfigEntry) -> None: - """Initialize the Peblar entity.""" - super().__init__(coordinator=entry.runtime_data) - self._attr_device_info = DeviceInfo( - configuration_url=f"http://{entry.data[CONF_HOST]}", - identifiers={(DOMAIN, str(entry.unique_id))}, - manufacturer="Peblar", - name="Peblar EV charger", - ) diff --git a/homeassistant/components/peblar/icons.json b/homeassistant/components/peblar/icons.json new file mode 100644 index 00000000000..073cd08a2c7 --- /dev/null +++ b/homeassistant/components/peblar/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "update": { + "customization": { + "default": "mdi:palette" + } + } + } +} diff --git a/homeassistant/components/peblar/sensor.py b/homeassistant/components/peblar/sensor.py index eafca23e125..d31d929fcab 100644 --- a/homeassistant/components/peblar/sensor.py +++ b/homeassistant/components/peblar/sensor.py @@ -15,10 +15,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import UnitOfEnergy 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 .coordinator import PeblarConfigEntry -from .entity import PeblarEntity +from .const import DOMAIN +from .coordinator import PeblarConfigEntry, PeblarMeterDataUpdateCoordinator @dataclass(frozen=True, kw_only=True) @@ -28,7 +30,7 @@ class PeblarSensorDescription(SensorEntityDescription): value_fn: Callable[[PeblarMeter], int | None] -SENSORS: tuple[PeblarSensorDescription, ...] = ( +DESCRIPTIONS: tuple[PeblarSensorDescription, ...] = ( PeblarSensorDescription( key="energy_total", device_class=SensorDeviceClass.ENERGY, @@ -48,24 +50,33 @@ async def async_setup_entry( ) -> None: """Set up Peblar sensors based on a config entry.""" async_add_entities( - PeblarSensorEntity(entry, description) for description in SENSORS + PeblarSensorEntity(entry, description) for description in DESCRIPTIONS ) -class PeblarSensorEntity(PeblarEntity, SensorEntity): +class PeblarSensorEntity( + CoordinatorEntity[PeblarMeterDataUpdateCoordinator], SensorEntity +): """Defines a Peblar sensor.""" entity_description: PeblarSensorDescription + _attr_has_entity_name = True + def __init__( self, entry: PeblarConfigEntry, description: PeblarSensorDescription, ) -> None: """Initialize the Peblar entity.""" - super().__init__(entry) + super().__init__(entry.runtime_data.meter_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 native_value(self) -> int | None: diff --git a/homeassistant/components/peblar/strings.json b/homeassistant/components/peblar/strings.json index e5fa1e85a6a..2e23fcfcdcd 100644 --- a/homeassistant/components/peblar/strings.json +++ b/homeassistant/components/peblar/strings.json @@ -31,5 +31,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_serial_number": "The discovered Peblar device did not provide a serial number." } + }, + "entity": { + "update": { + "customization": { + "name": "Customization" + } + } } } diff --git a/homeassistant/components/peblar/update.py b/homeassistant/components/peblar/update.py new file mode 100644 index 00000000000..cc0f1ee0c79 --- /dev/null +++ b/homeassistant/components/peblar/update.py @@ -0,0 +1,93 @@ +"""Support for Peblar updates.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +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, + PeblarVersionDataUpdateCoordinator, + PeblarVersionInformation, +) + + +@dataclass(frozen=True, kw_only=True) +class PeblarUpdateEntityDescription(UpdateEntityDescription): + """Describe an Peblar update entity.""" + + installed_fn: Callable[[PeblarVersionInformation], str | None] + available_fn: Callable[[PeblarVersionInformation], str | None] + + +DESCRIPTIONS: tuple[PeblarUpdateEntityDescription, ...] = ( + PeblarUpdateEntityDescription( + key="firmware", + device_class=UpdateDeviceClass.FIRMWARE, + installed_fn=lambda x: x.current.firmware, + available_fn=lambda x: x.available.firmware, + ), + PeblarUpdateEntityDescription( + key="customization", + translation_key="customization", + installed_fn=lambda x: x.current.customization, + available_fn=lambda x: x.available.customization, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PeblarConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Peblar update based on a config entry.""" + async_add_entities( + PeblarUpdateEntity(entry, description) for description in DESCRIPTIONS + ) + + +class PeblarUpdateEntity( + CoordinatorEntity[PeblarVersionDataUpdateCoordinator], UpdateEntity +): + """Defines a Peblar update entity.""" + + entity_description: PeblarUpdateEntityDescription + + _attr_has_entity_name = True + + def __init__( + self, + entry: PeblarConfigEntry, + description: PeblarUpdateEntityDescription, + ) -> None: + """Initialize the update entity.""" + super().__init__(entry.runtime_data.version_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 installed_version(self) -> str | None: + """Version currently installed and in use.""" + return self.entity_description.installed_fn(self.coordinator.data) + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self.entity_description.available_fn(self.coordinator.data)