From 9dc20b5709b7bfe2c11e1e518a9d86eb81d8f143 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Dec 2024 22:40:15 +0100 Subject: [PATCH] Add more sensors to Peblar Rocksolid EV Chargers integration (#133754) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/peblar/const.py | 32 +++ homeassistant/components/peblar/icons.json | 11 + homeassistant/components/peblar/sensor.py | 44 ++- homeassistant/components/peblar/strings.json | 61 ++++- .../peblar/snapshots/test_sensor.ambr | 256 ++++++++++++++++++ tests/components/peblar/test_sensor.py | 1 + 6 files changed, 386 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/peblar/const.py b/homeassistant/components/peblar/const.py index b986c866d16..d7d7c2fa5b5 100644 --- a/homeassistant/components/peblar/const.py +++ b/homeassistant/components/peblar/const.py @@ -5,6 +5,38 @@ from __future__ import annotations import logging from typing import Final +from peblar import ChargeLimiter, CPState + DOMAIN: Final = "peblar" LOGGER = logging.getLogger(__package__) + +PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT = { + ChargeLimiter.CHARGING_CABLE: "charging_cable", + ChargeLimiter.CURRENT_LIMITER: "current_limiter", + ChargeLimiter.DYNAMIC_LOAD_BALANCING: "dynamic_load_balancing", + ChargeLimiter.EXTERNAL_POWER_LIMIT: "external_power_limit", + ChargeLimiter.GROUP_LOAD_BALANCING: "group_load_balancing", + ChargeLimiter.HARDWARE_LIMITATION: "hardware_limitation", + ChargeLimiter.HIGH_TEMPERATURE: "high_temperature", + ChargeLimiter.HOUSEHOLD_POWER_LIMIT: "household_power_limit", + ChargeLimiter.INSTALLATION_LIMIT: "installation_limit", + ChargeLimiter.LOCAL_MODBUS_API: "local_modbus_api", + ChargeLimiter.LOCAL_REST_API: "local_rest_api", + ChargeLimiter.LOCAL_SCHEDULED: "local_scheduled", + ChargeLimiter.OCPP_SMART_CHARGING: "ocpp_smart_charging", + ChargeLimiter.OVERCURRENT_PROTECTION: "overcurrent_protection", + ChargeLimiter.PHASE_IMBALANCE: "phase_imbalance", + ChargeLimiter.POWER_FACTOR: "power_factor", + ChargeLimiter.SOLAR_CHARGING: "solar_charging", +} + +PEBLAR_CP_STATE_TO_HOME_ASSISTANT = { + CPState.CHARGING_SUSPENDED: "suspended", + CPState.CHARGING_VENTILATION: "charging", + CPState.CHARGING: "charging", + CPState.ERROR: "error", + CPState.FAULT: "fault", + CPState.INVALID: "invalid", + CPState.NO_EV_CONNECTED: "no_ev_connected", +} diff --git a/homeassistant/components/peblar/icons.json b/homeassistant/components/peblar/icons.json index 2b24bf71ebc..6244945077b 100644 --- a/homeassistant/components/peblar/icons.json +++ b/homeassistant/components/peblar/icons.json @@ -24,6 +24,17 @@ } } }, + "sensor": { + "cp_state": { + "default": "mdi:ev-plug-type2" + }, + "charge_current_limit_source": { + "default": "mdi:arrow-collapse-up" + }, + "uptime": { + "default": "mdi:timer" + } + }, "switch": { "force_single_phase": { "default": "mdi:power-cycle" diff --git a/homeassistant/components/peblar/sensor.py b/homeassistant/components/peblar/sensor.py index 285a8dd5ea0..233417051cb 100644 --- a/homeassistant/components/peblar/sensor.py +++ b/homeassistant/components/peblar/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime, timedelta from peblar import PeblarUserConfiguration @@ -24,8 +25,13 @@ 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 homeassistant.util.dt import utcnow -from .const import DOMAIN +from .const import ( + DOMAIN, + PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT, + PEBLAR_CP_STATE_TO_HOME_ASSISTANT, +) from .coordinator import PeblarConfigEntry, PeblarData, PeblarDataUpdateCoordinator @@ -34,21 +40,37 @@ class PeblarSensorDescription(SensorEntityDescription): """Describe a Peblar sensor.""" has_fn: Callable[[PeblarUserConfiguration], bool] = lambda _: True - value_fn: Callable[[PeblarData], int | None] + value_fn: Callable[[PeblarData], datetime | int | str | None] DESCRIPTIONS: tuple[PeblarSensorDescription, ...] = ( PeblarSensorDescription( - key="current", + key="cp_state", + translation_key="cp_state", + device_class=SensorDeviceClass.ENUM, + options=list(PEBLAR_CP_STATE_TO_HOME_ASSISTANT.values()), + value_fn=lambda x: PEBLAR_CP_STATE_TO_HOME_ASSISTANT[x.ev.cp_state], + ), + PeblarSensorDescription( + key="charge_current_limit_source", + translation_key="charge_current_limit_source", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=list(PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT.values()), + value_fn=lambda x: PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT[ + x.ev.charge_current_limit_source + ], + ), + PeblarSensorDescription( + key="current_total", device_class=SensorDeviceClass.CURRENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - has_fn=lambda x: x.connected_phases == 1, native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, - value_fn=lambda x: x.meter.current_phase_1, + value_fn=lambda x: x.meter.current_total, ), PeblarSensorDescription( key="current_phase_1", @@ -193,6 +215,16 @@ DESCRIPTIONS: tuple[PeblarSensorDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.meter.voltage_phase_3, ), + PeblarSensorDescription( + key="uptime", + translation_key="uptime", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda x: ( + utcnow().replace(microsecond=0) - timedelta(seconds=x.system.uptime) + ), + ), ) @@ -232,6 +264,6 @@ class PeblarSensorEntity(CoordinatorEntity[PeblarDataUpdateCoordinator], SensorE ) @property - def native_value(self) -> int | None: + def native_value(self) -> datetime | int | str | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/peblar/strings.json b/homeassistant/components/peblar/strings.json index 0632fa31dd0..01022a19c38 100644 --- a/homeassistant/components/peblar/strings.json +++ b/homeassistant/components/peblar/strings.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_serial_number": "The discovered Peblar device did not provide a serial number." + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, "step": { "user": { - "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant.", "data": { "host": "[%key:common::config_flow::data::host%]", "password": "[%key:common::config_flow::data::password%]" @@ -10,26 +18,18 @@ "data_description": { "host": "The hostname or IP address of your Peblar charger on your home network.", "password": "The same password as you use to log in to the Peblar device' local web interface." - } + }, + "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant." }, "zeroconf_confirm": { - "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant.", "data": { "password": "[%key:common::config_flow::data::password%]" }, "data_description": { "password": "[%key:component::peblar::config::step::user::data_description::password%]" - } + }, + "description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant." } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_serial_number": "The discovered Peblar device did not provide a serial number." } }, "entity": { @@ -59,6 +59,38 @@ } }, "sensor": { + "charge_current_limit_source": { + "name": "Limit source", + "state": { + "charging_cable": "Charging cable", + "current_limiter": "Current limiter", + "dynamic_load_balancing": "Dynamic load balancing", + "external_power_limit": "External power limit", + "group_load_balancing": "Group load balancing", + "hardware_limitation": "Hardware limitation", + "high_temperature": "High temperature", + "household_power_limit": "Household power limit", + "installation_limit": "Installation limit", + "local_modbus_api": "Modbus API", + "local_rest_api": "REST API", + "ocpp_smart_charging": "OCPP smart charging", + "overcurrent_protection": "Overcurrent protection", + "phase_imbalance": "Phase imbalance", + "power_factor": "Power factor", + "solar_charging": "Solar charging" + } + }, + "cp_state": { + "name": "State", + "state": { + "charging": "Charging", + "error": "Error", + "fault": "Fault", + "invalid": "Invalid", + "no_ev_connected": "No EV connected", + "suspended": "Suspended" + } + }, "current_phase_1": { "name": "Current phase 1" }, @@ -83,6 +115,9 @@ "power_phase_3": { "name": "Power phase 3" }, + "uptime": { + "name": "Uptime" + }, "voltage_phase_1": { "name": "Voltage phase 1" }, diff --git a/tests/components/peblar/snapshots/test_sensor.ambr b/tests/components/peblar/snapshots/test_sensor.ambr index c3020b60078..da17a4661ee 100644 --- a/tests/components/peblar/snapshots/test_sensor.ambr +++ b/tests/components/peblar/snapshots/test_sensor.ambr @@ -1,4 +1,61 @@ # serializer version: 1 +# name: test_entities[sensor][sensor.peblar_ev_charger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_current_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.242', + }) +# --- # name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -227,6 +284,92 @@ 'state': '880.703', }) # --- +# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging_cable', + 'current_limiter', + 'dynamic_load_balancing', + 'external_power_limit', + 'group_load_balancing', + 'hardware_limitation', + 'high_temperature', + 'household_power_limit', + 'installation_limit', + 'local_modbus_api', + 'local_rest_api', + 'local_scheduled', + 'ocpp_smart_charging', + 'overcurrent_protection', + 'phase_imbalance', + 'power_factor', + 'solar_charging', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_limit_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Limit source', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_current_limit_source', + 'unique_id': '23-45-A4O-MOF_charge_current_limit_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Peblar EV Charger Limit source', + 'options': list([ + 'charging_cable', + 'current_limiter', + 'dynamic_load_balancing', + 'external_power_limit', + 'group_load_balancing', + 'hardware_limitation', + 'high_temperature', + 'household_power_limit', + 'installation_limit', + 'local_modbus_api', + 'local_rest_api', + 'local_scheduled', + 'ocpp_smart_charging', + 'overcurrent_protection', + 'phase_imbalance', + 'power_factor', + 'solar_charging', + ]), + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_limit_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'current_limiter', + }) +# --- # name: test_entities[sensor][sensor.peblar_ev_charger_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -488,6 +631,119 @@ 'state': '0.381', }) # --- +# name: test_entities[sensor][sensor.peblar_ev_charger_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'suspended', + 'charging', + 'charging', + 'error', + 'fault', + 'invalid', + 'no_ev_connected', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.peblar_ev_charger_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cp_state', + 'unique_id': '23-45-A4O-MOF_cp_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Peblar EV Charger State', + 'options': list([ + 'suspended', + 'charging', + 'charging', + 'error', + 'fault', + 'invalid', + 'no_ev_connected', + ]), + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uptime', + 'unique_id': '23-45-A4O-MOF_uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Peblar EV Charger Uptime', + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-12-18T04:16:46+00:00', + }) +# --- # name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/peblar/test_sensor.py b/tests/components/peblar/test_sensor.py index 97402206d33..bad81486838 100644 --- a/tests/components/peblar/test_sensor.py +++ b/tests/components/peblar/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry, snapshot_platform +@pytest.mark.freeze_time("2024-12-21 21:45:00") @pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") async def test_entities(