diff --git a/homeassistant/components/besen/sensor.py b/homeassistant/components/besen/sensor.py index b70ca1a090f5..50a290596a94 100644 --- a/homeassistant/components/besen/sensor.py +++ b/homeassistant/components/besen/sensor.py @@ -2,6 +2,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass +from datetime import datetime from typing import Final, override from besen.models import BesenData @@ -20,6 +21,7 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfPower, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -113,7 +115,7 @@ def _enum_state(value: str | None, states: Mapping[str, str]) -> str | None: class BesenSensorEntityDescription(SensorEntityDescription): """Describe a Besen sensor entity.""" - value_fn: Callable[[BesenData], StateType] + value_fn: Callable[[BesenData], StateType | datetime] three_phase_only: bool = False @@ -196,6 +198,45 @@ SENSOR_DESCRIPTIONS: tuple[BesenSensorEntityDescription, ...] = ( suggested_display_precision=2, value_fn=lambda data: data.charge.session_energy, ), + BesenSensorEntityDescription( + key="session_start", + translation_key="session_start", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.charge.session_start, + ), + BesenSensorEntityDescription( + key="session_duration", + translation_key="session_duration", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_display_precision=0, + value_fn=lambda data: data.charge.session_duration, + ), + BesenSensorEntityDescription( + key="session_current_limit", + translation_key="session_current_limit", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + suggested_display_precision=0, + value_fn=lambda data: data.charge.session_current_limit, + ), + BesenSensorEntityDescription( + key="scheduled_start", + translation_key="scheduled_start", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + value_fn=lambda data: data.charge.scheduled_start, + ), + BesenSensorEntityDescription( + key="charging_time_limit", + translation_key="charging_time_limit", + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=0, + value_fn=lambda data: data.charge.charging_time_limit, + ), BesenSensorEntityDescription( key="internal_temperature", translation_key="internal_temperature", @@ -321,7 +362,7 @@ class BesenSensor(BesenEntity, SensorEntity): @property @override - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """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 51e205ec5acb..4c7e5e251e5b 100644 --- a/homeassistant/components/besen/strings.json +++ b/homeassistant/components/besen/strings.json @@ -88,6 +88,7 @@ "waiting": "Waiting" } }, + "charging_time_limit": { "name": "Charging time limit" }, "current_state": { "name": "Current state", "state": { @@ -152,7 +153,11 @@ "disconnected": "[%key:common::state::disconnected%]" } }, + "scheduled_start": { "name": "Scheduled start" }, + "session_current_limit": { "name": "Session current limit" }, + "session_duration": { "name": "Session duration" }, "session_energy": { "name": "Session energy" }, + "session_start": { "name": "Session start" }, "total_energy": { "name": "Total energy" } }, "switch": { diff --git a/tests/components/besen/conftest.py b/tests/components/besen/conftest.py index fd2bab82fffe..51478c50e707 100644 --- a/tests/components/besen/conftest.py +++ b/tests/components/besen/conftest.py @@ -1,6 +1,7 @@ """Fixtures for the Besen integration tests.""" from collections.abc import Generator +from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock, patch from besen.models import BesenData, ChargerConfig, ChargerInfo, ChargeStatus @@ -91,6 +92,11 @@ def charger_state( power=3500, total_energy=12.3, session_energy=1.2, + session_start=datetime(2026, 9, 30, 20, 0, tzinfo=UTC), + session_duration=1200, + session_current_limit=10, + scheduled_start=datetime(2026, 9, 30, 19, 30, tzinfo=UTC), + charging_time_limit=240, inner_temp_c=24.5, outer_temp=22.5, l1_voltage=230.0, diff --git a/tests/components/besen/snapshots/test_diagnostics.ambr b/tests/components/besen/snapshots/test_diagnostics.ambr index 783ec5f456a7..dc4229213b3c 100644 --- a/tests/components/besen/snapshots/test_diagnostics.ambr +++ b/tests/components/besen/snapshots/test_diagnostics.ambr @@ -9,7 +9,7 @@ 'charger_status': True, 'charging_status': 'Start', 'charging_status_description': 'EV is connected, please press start', - 'charging_time_limit': None, + 'charging_time_limit': 240, 'current_state': 'Charging', 'emergency_btn_state': None, 'error_details': 'No Error', @@ -28,11 +28,11 @@ 'output_state': 'Charging', 'plug_state': 'Connected Locked', 'power': 3500, - 'scheduled_start': None, - 'session_current_limit': None, - 'session_duration': None, + 'scheduled_start': '2026-09-30T19:30:00+00:00', + 'session_current_limit': 10, + 'session_duration': 1200, 'session_energy': 1.2, - 'session_start': None, + 'session_start': '2026-09-30T20:00:00+00:00', 'total_energy': 12.3, }), 'config': dict({ diff --git a/tests/components/besen/snapshots/test_sensor.ambr b/tests/components/besen/snapshots/test_sensor.ambr index 423723bf38c8..4660e4e3ad04 100644 --- a/tests/components/besen/snapshots/test_sensor.ambr +++ b/tests/components/besen/snapshots/test_sensor.ambr @@ -201,6 +201,61 @@ 'state': 'start', }) # --- +# name: test_sensor_state[single_phase][sensor.garage_charging_time_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_time_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging time limit', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging time limit', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_time_limit', + 'unique_id': 'AA:BB_charging_time_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_charging_time_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'Garage Charging time limit', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_charging_time_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_current_state-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -729,6 +784,167 @@ 'state': 'connected_locked', }) # --- +# name: test_sensor_state[single_phase][sensor.garage_scheduled_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_scheduled_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Scheduled start', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Scheduled start', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'scheduled_start', + 'unique_id': 'AA:BB_scheduled_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_scheduled_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Garage Scheduled start', + }), + 'context': , + 'entity_id': 'sensor.garage_scheduled_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-09-30T19:30:00+00:00', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_current_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_current_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session current limit', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session current limit', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_current_limit', + 'unique_id': 'AA:BB_session_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_current_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage Session current limit', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_current_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session duration', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session duration', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_duration', + 'unique_id': 'AA:BB_session_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'Garage Session duration', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1200', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_session_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -787,6 +1003,57 @@ 'state': '1.2', }) # --- +# name: test_sensor_state[single_phase][sensor.garage_session_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session start', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session start', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_start', + 'unique_id': 'AA:BB_session_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_session_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Garage Session start', + }), + 'context': , + 'entity_id': 'sensor.garage_session_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-09-30T20:00:00+00:00', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_total_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1047,6 +1314,61 @@ 'state': 'start', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_charging_time_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_time_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging time limit', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging time limit', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_time_limit', + 'unique_id': 'AA:BB_charging_time_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_charging_time_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'Garage Charging time limit', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_charging_time_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_current_state-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1807,6 +2129,167 @@ 'state': 'connected_locked', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_scheduled_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_scheduled_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Scheduled start', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Scheduled start', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'scheduled_start', + 'unique_id': 'AA:BB_scheduled_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_scheduled_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Garage Scheduled start', + }), + 'context': , + 'entity_id': 'sensor.garage_scheduled_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-09-30T19:30:00+00:00', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_current_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_current_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session current limit', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session current limit', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_current_limit', + 'unique_id': 'AA:BB_session_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_current_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Garage Session current limit', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_current_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session duration', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session duration', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_duration', + 'unique_id': 'AA:BB_session_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'Garage Session duration', + : , + }), + 'context': , + 'entity_id': 'sensor.garage_session_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1200', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_session_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1865,6 +2348,57 @@ 'state': '1.2', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_session_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_session_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Session start', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session start', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'session_start', + 'unique_id': 'AA:BB_session_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_session_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'timestamp', + : 'Garage Session start', + }), + 'context': , + 'entity_id': 'sensor.garage_session_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-09-30T20:00:00+00:00', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_total_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/besen/test_sensor.py b/tests/components/besen/test_sensor.py index 12576814fefb..e82e0e7fc615 100644 --- a/tests/components/besen/test_sensor.py +++ b/tests/components/besen/test_sensor.py @@ -246,6 +246,30 @@ async def test_diagnostic_sensors_disabled_by_default( assert hass.states.get(entry.entity_id) is None +@pytest.mark.parametrize( + "entity_id", + [ + "sensor.garage_session_current_limit", + "sensor.garage_scheduled_start", + "sensor.garage_charging_time_limit", + ], +) +async def test_optional_session_sensors_disabled_by_default( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, + entity_id: str, +) -> None: + """Test rarely used session sensors are disabled by default.""" + + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + assert (entry := entity_registry.async_get(entity_id)) is not None + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get(entity_id) is None + + @pytest.mark.parametrize(("phases", "expected"), [(1, False), (3, True)]) async def test_three_phase_sensor_filtering( hass: HomeAssistant,