From 5be245735f44d2e7434cc64a4d379f9c0b5da4cd Mon Sep 17 00:00:00 2001 From: moryoav Date: Sat, 29 Aug 2026 15:26:41 +0300 Subject: [PATCH] Add Besen sensor platform (#180585) --- homeassistant/components/besen/const.py | 2 +- homeassistant/components/besen/manifest.json | 2 +- homeassistant/components/besen/sensor.py | 197 ++++ homeassistant/components/besen/strings.json | 13 + requirements_all.txt | 2 +- tests/components/besen/conftest.py | 40 +- .../besen/snapshots/test_sensor.ambr | 1045 +++++++++++++++++ tests/components/besen/test_sensor.py | 180 +++ tests/components/besen/test_switch.py | 13 +- 9 files changed, 1474 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/besen/sensor.py create mode 100644 tests/components/besen/snapshots/test_sensor.ambr create mode 100644 tests/components/besen/test_sensor.py diff --git a/homeassistant/components/besen/const.py b/homeassistant/components/besen/const.py index f9f2f853fb77..052084ab68f1 100644 --- a/homeassistant/components/besen/const.py +++ b/homeassistant/components/besen/const.py @@ -7,4 +7,4 @@ from homeassistant.const import Platform DOMAIN: Final = "besen" NAME: Final = "Besen" -PLATFORMS: Final = [Platform.SWITCH] +PLATFORMS: Final = [Platform.SENSOR, Platform.SWITCH] diff --git a/homeassistant/components/besen/manifest.json b/homeassistant/components/besen/manifest.json index 0be7d16a437b..531b1d7e5408 100644 --- a/homeassistant/components/besen/manifest.json +++ b/homeassistant/components/besen/manifest.json @@ -14,5 +14,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["besen==0.3.4"] + "requirements": ["besen==0.4.0"] } diff --git a/homeassistant/components/besen/sensor.py b/homeassistant/components/besen/sensor.py new file mode 100644 index 000000000000..04de805d343e --- /dev/null +++ b/homeassistant/components/besen/sensor.py @@ -0,0 +1,197 @@ +"""Sensor platform for Besen.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import override + +from besen.models import BesenData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, + StateType, +) +from homeassistant.const import ( + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import BesenConfigEntry +from .coordinator import BesenCoordinator +from .entity import BesenEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class BesenSensorEntityDescription(SensorEntityDescription): + """Describe a Besen sensor entity.""" + + value_fn: Callable[[BesenData], float | int | None] + three_phase_only: bool = False + + +SENSOR_DESCRIPTIONS: tuple[BesenSensorEntityDescription, ...] = ( + BesenSensorEntityDescription( + key="charging_power", + translation_key="charging_power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.charge.power, + ), + BesenSensorEntityDescription( + key="total_energy", + translation_key="total_energy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + value_fn=lambda data: data.charge.total_energy, + ), + BesenSensorEntityDescription( + key="session_energy", + translation_key="session_energy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + value_fn=lambda data: data.charge.session_energy, + ), + BesenSensorEntityDescription( + key="internal_temperature", + translation_key="internal_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.charge.inner_temp_c, + ), + BesenSensorEntityDescription( + key="external_temperature", + translation_key="external_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.charge.outer_temp, + ), + BesenSensorEntityDescription( + key="l1_voltage", + translation_key="l1_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.charge.l1_voltage, + ), + BesenSensorEntityDescription( + key="l1_current", + translation_key="l1_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + value_fn=lambda data: data.charge.l1_amperage, + ), + BesenSensorEntityDescription( + key="l2_voltage", + translation_key="l2_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + three_phase_only=True, + value_fn=lambda data: data.charge.l2_voltage, + ), + BesenSensorEntityDescription( + key="l2_current", + translation_key="l2_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + three_phase_only=True, + value_fn=lambda data: data.charge.l2_amperage, + ), + BesenSensorEntityDescription( + key="l3_voltage", + translation_key="l3_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + three_phase_only=True, + value_fn=lambda data: data.charge.l3_voltage, + ), + BesenSensorEntityDescription( + key="l3_current", + translation_key="l3_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + three_phase_only=True, + value_fn=lambda data: data.charge.l3_amperage, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: BesenConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Besen sensors.""" + + coordinator = entry.runtime_data + async_add_entities( + BesenSensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + if not description.three_phase_only or coordinator.data.info.phases == 3 + ) + + +class BesenSensor(BesenEntity, SensorEntity): + """Representation of a Besen sensor.""" + + entity_description: BesenSensorEntityDescription + + def __init__( + self, + coordinator: BesenCoordinator, + description: BesenSensorEntityDescription, + ) -> None: + """Initialize a Besen sensor.""" + + super().__init__(coordinator, description.key) + self.entity_description = description + + @property + @override + def native_value(self) -> StateType: + """Return the sensor value.""" + + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/besen/strings.json b/homeassistant/components/besen/strings.json index 4fb426652c0c..b7345780d098 100644 --- a/homeassistant/components/besen/strings.json +++ b/homeassistant/components/besen/strings.json @@ -38,6 +38,19 @@ } }, "entity": { + "sensor": { + "charging_power": { "name": "Charging power" }, + "external_temperature": { "name": "External temperature" }, + "internal_temperature": { "name": "Internal temperature" }, + "l1_current": { "name": "L1 current" }, + "l1_voltage": { "name": "L1 voltage" }, + "l2_current": { "name": "L2 current" }, + "l2_voltage": { "name": "L2 voltage" }, + "l3_current": { "name": "L3 current" }, + "l3_voltage": { "name": "L3 voltage" }, + "session_energy": { "name": "Session energy" }, + "total_energy": { "name": "Total energy" } + }, "switch": { "charging": { "name": "Charge" } } diff --git a/requirements_all.txt b/requirements_all.txt index 3ffad0728a2d..64b98edfa1b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -661,7 +661,7 @@ batinfo==0.4.2 beautifulsoup4==4.13.3 # homeassistant.components.besen -besen==0.3.4 +besen==0.4.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/tests/components/besen/conftest.py b/tests/components/besen/conftest.py index ee4d5fb17077..9cc0a1ffa34d 100644 --- a/tests/components/besen/conftest.py +++ b/tests/components/besen/conftest.py @@ -6,9 +6,9 @@ from unittest.mock import AsyncMock, Mock, patch from besen.models import BesenData, ChargerConfig, ChargerInfo, ChargeStatus import pytest -from homeassistant.components.besen.const import DOMAIN +from homeassistant.components.besen.const import DOMAIN, PLATFORMS from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN, Platform from homeassistant.core import HomeAssistant from . import publish_besen_state @@ -52,6 +52,8 @@ def charger_state( charger_status: bool | None = True, available: bool = True, authenticated: bool = True, + phases: int = 1, + charge: ChargeStatus | None = None, ) -> BesenData: """Return a populated charger state.""" @@ -59,19 +61,30 @@ def charger_state( info=ChargerInfo( address=FIXTURE_ADDRESS, serial="SERIAL", - phases=1, + phases=phases, manufacturer="Besen", model="BS20", hardware_version="HW1", software_version="SW1", ), config=ChargerConfig(device_name="Garage", rssi=-55), - charge=ChargeStatus( - charger_status=charger_status, - current_energy=3500, - total_energy=1.2, - current_amount=12.3, - inner_temp_c=24.5, + charge=( + charge + if charge is not None + else ChargeStatus( + charger_status=charger_status, + power=3500, + total_energy=12.3, + session_energy=1.2, + inner_temp_c=24.5, + outer_temp=22.5, + l1_voltage=230.0, + l1_amperage=15.2, + l2_voltage=231.0, + l2_amperage=15.1, + l3_voltage=232.0, + l3_amperage=15.0, + ) ), available=available, authenticated=authenticated, @@ -179,9 +192,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: async def setup_integration( hass: HomeAssistant, entry: MockConfigEntry, + platforms: list[Platform] | None = None, ) -> None: """Set up the Besen integration.""" entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch( + "homeassistant.components.besen.PLATFORMS", + platforms if platforms is not None else PLATFORMS, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/besen/snapshots/test_sensor.ambr b/tests/components/besen/snapshots/test_sensor.ambr new file mode 100644 index 000000000000..ef3c9a4baf5c --- /dev/null +++ b/tests/components/besen/snapshots/test_sensor.ambr @@ -0,0 +1,1045 @@ +# serializer version: 1 +# name: test_sensor_state[single_phase][sensor.garage_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'AA:BB_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'Garage Charging power', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3500', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_external_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_external_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'External temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'External temperature', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'external_temperature', + 'unique_id': 'AA:BB_external_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_external_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Garage External temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_external_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_internal_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_internal_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Internal temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Internal temperature', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'internal_temperature', + 'unique_id': 'AA:BB_internal_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_internal_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Garage Internal temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_internal_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.5', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_l1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 current', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_current', + 'unique_id': 'AA:BB_l1_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_l1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage L1 current', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.2', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_l1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 voltage', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_voltage', + 'unique_id': 'AA:BB_l1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_l1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'Garage L1 voltage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230.0', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session energy', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_energy', + 'unique_id': 'AA:BB_session_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Garage Session energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'AA:BB_total_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Garage Total energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'AA:BB_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'Garage Charging power', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3500', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_external_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_external_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'External temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'External temperature', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'external_temperature', + 'unique_id': 'AA:BB_external_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_external_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Garage External temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_external_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_internal_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_internal_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Internal temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Internal temperature', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'internal_temperature', + 'unique_id': 'AA:BB_internal_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_internal_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Garage Internal temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_internal_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.5', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 current', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_current', + 'unique_id': 'AA:BB_l1_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage L1 current', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.2', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 voltage', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_voltage', + 'unique_id': 'AA:BB_l1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'Garage L1 voltage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230.0', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l2_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2 current', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_current', + 'unique_id': 'AA:BB_l2_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage L2 current', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.1', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l2_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2 voltage', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_voltage', + 'unique_id': 'AA:BB_l2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'Garage L2 voltage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '231.0', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l3_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l3_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3 current', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_current', + 'unique_id': 'AA:BB_l3_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l3_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage L3 current', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l3_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.0', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_l3_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3 voltage', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_voltage', + 'unique_id': 'AA:BB_l3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_l3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'Garage L3 voltage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_l3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '232.0', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session energy', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_energy', + 'unique_id': 'AA:BB_session_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Garage Session energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'AA:BB_total_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Garage Total energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.garage_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- diff --git a/tests/components/besen/test_sensor.py b/tests/components/besen/test_sensor.py new file mode 100644 index 000000000000..27495e588b2b --- /dev/null +++ b/tests/components/besen/test_sensor.py @@ -0,0 +1,180 @@ +"""Tests for the Besen sensor platform.""" + +from unittest.mock import Mock + +from besen.models import ChargeStatus +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + EntityCategory, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import publish_besen_state +from .conftest import charger_state, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +POWER_ENTITY_ID = "sensor.garage_charging_power" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("phases", [1, 3], ids=["single_phase", "three_phase"]) +async def test_sensor_state( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, + phases: int, +) -> None: + """Test sensor states and registry data.""" + + mock_besen_client.state = charger_state(phases=phases) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + mock_besen_client.async_start.assert_awaited_once() + + +async def test_sensor_updates_from_client( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, +) -> None: + """Test sensor states update from client push data.""" + + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + publish_besen_state( + mock_besen_client, + charger_state( + charge=ChargeStatus( + power=7200, + total_energy=123.45, + session_energy=4.56, + inner_temp_c=26.5, + ) + ), + ) + await hass.async_block_till_done() + + assert (state := hass.states.get(POWER_ENTITY_ID)) is not None + assert state.state == "7200" + assert (state := hass.states.get("sensor.garage_total_energy")) is not None + assert state.state == "123.45" + assert (state := hass.states.get("sensor.garage_session_energy")) is not None + assert state.state == "4.56" + assert (state := hass.states.get("sensor.garage_internal_temperature")) is not None + assert state.state == "26.5" + + +async def test_sensor_unknown_value( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, +) -> None: + """Test a missing measurement is unknown while the charger is available.""" + + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + publish_besen_state(mock_besen_client, charger_state(charge=ChargeStatus())) + await hass.async_block_till_done() + + assert (state := hass.states.get(POWER_ENTITY_ID)) is not None + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + ("available", "authenticated"), + [ + (False, True), + (True, False), + ], +) +async def test_sensor_unavailable_from_client_state( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, + available: bool, + authenticated: bool, +) -> None: + """Test sensor availability follows client connection and authentication.""" + + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + publish_besen_state( + mock_besen_client, + charger_state(available=available, authenticated=authenticated), + ) + await hass.async_block_till_done() + + assert (state := hass.states.get(POWER_ENTITY_ID)) is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_diagnostic_sensors_disabled_by_default( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, +) -> None: + """Test diagnostic sensors are disabled by default.""" + + mock_besen_client.state = charger_state(phases=3) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + diagnostic_entries = { + entry.unique_id: entry + for entry in entries + if entry.entity_category is EntityCategory.DIAGNOSTIC + } + assert set(diagnostic_entries) == { + f"{mock_besen_client.address}_external_temperature", + f"{mock_besen_client.address}_l1_current", + f"{mock_besen_client.address}_l1_voltage", + f"{mock_besen_client.address}_l2_current", + f"{mock_besen_client.address}_l2_voltage", + f"{mock_besen_client.address}_l3_current", + f"{mock_besen_client.address}_l3_voltage", + } + for entry in diagnostic_entries.values(): + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entry.entity_id) is None + + +@pytest.mark.parametrize(("phases", "expected"), [(1, False), (3, True)]) +async def test_three_phase_sensor_filtering( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, + phases: int, + expected: bool, +) -> None: + """Test L2 and L3 sensors are added only for three-phase chargers.""" + + mock_besen_client.state = charger_state(phases=phases) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + unique_ids = { + entry.unique_id + for entry in er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + } + three_phase_unique_ids = { + f"{mock_besen_client.address}_l2_voltage", + f"{mock_besen_client.address}_l2_current", + f"{mock_besen_client.address}_l3_voltage", + f"{mock_besen_client.address}_l3_current", + } + assert three_phase_unique_ids.issubset(unique_ids) is expected diff --git a/tests/components/besen/test_switch.py b/tests/components/besen/test_switch.py index 5156f1df9f1d..27b59cbd9a7d 100644 --- a/tests/components/besen/test_switch.py +++ b/tests/components/besen/test_switch.py @@ -15,6 +15,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -38,7 +39,7 @@ async def test_switch_state( ) -> None: """Test switch entity state and registry data.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) mock_besen_client.async_start.assert_awaited_once() @@ -51,7 +52,7 @@ async def test_switch_updates_from_client( ) -> None: """Test switch state updates from client push data.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) publish_besen_state(mock_besen_client, charger_state(charger_status=False)) await hass.async_block_till_done() @@ -68,7 +69,7 @@ async def test_switch_updates_on_refresh( ) -> None: """Test switch state updates when the coordinator refreshes.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) mock_besen_client.state = charger_state(charger_status=False) await async_update_entity(hass, ENTITY_ID) @@ -95,7 +96,7 @@ async def test_switch_unavailable_from_client_state( ) -> None: """Test switch availability follows client availability and authentication.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) publish_besen_state( mock_besen_client, @@ -115,7 +116,7 @@ async def test_switch_services( ) -> None: """Test switch turn on and turn off services.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) await hass.services.async_call( SWITCH_DOMAIN, @@ -155,7 +156,7 @@ async def test_switch_command_failure( side_effect=CommandFailed("failed") ) - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) with pytest.raises(HomeAssistantError) as err: await hass.services.async_call(