diff --git a/homeassistant/components/besen/sensor.py b/homeassistant/components/besen/sensor.py index 04de805d343e..b70ca1a090f5 100644 --- a/homeassistant/components/besen/sensor.py +++ b/homeassistant/components/besen/sensor.py @@ -1,8 +1,8 @@ """Sensor platform for Besen.""" -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import override +from typing import Final, override from besen.models import BesenData @@ -30,16 +30,146 @@ from .entity import BesenEntity PARALLEL_UPDATES = 0 +ERROR_STATES: Final = { + "Relay Stick Error": "relay_stick_error", + "OFFLINE": "offline", + "CC Error": "cc_error", + "CP Error": "cp_error", + "Emergency Stop": "emergency_stop", + "Over Temperature": "over_temperature", + "Leakage Protection": "leakage_protection", + "Short Circuit": "short_circuit", + "Over Current": "over_current", + "Ungrounded": "ungrounded", + "Over Voltage": "over_voltage", + "Low Voltage": "low_voltage", + "Input Power Error": "input_power_error", + "DLB Over Current - Mains overload": "dlb_over_current", + "Diode Short Circuit": "diode_short_circuit", + "RTC Failure": "rtc_failure", + "Flash Memory Failure": "flash_memory_failure", + "EEPROM Failure": "eeprom_failure", + "Metering Module Failure": "metering_module_failure", + "No Error": "no_error", +} + +CHARGING_STATES: Final = { + "Start": "start", + "Finish Charging": "finish_charging", + "Waiting": "waiting", + "Finished": "finished", + "Cancel": "canceled", + "Connect": "connect", + "Fault": "fault", +} + +CHARGING_MESSAGES: Final = { + "EV is connected, please press start": "ev_connected_press_start", + "Charging": "charging", + "Charging has started, waiting for EV.": "waiting_for_ev", + "Charging completed": "charging_completed", + "Charging reservation.": "charging_reservation", + "The plug is not connected, please start charging after connecting.": ( + "plug_not_connected" + ), + "See Error State": "see_error_state", + "Wait for the swipe to start": "waiting_for_swipe", + "Wait for the button to activate": "waiting_for_button", +} + +PLUG_STATES: Final = { + "Disconnected": "disconnected", + "Connected Unlocked": "connected_unlocked", + "Connected Locked": "connected_locked", +} + +OUTPUT_STATES: Final = { + "Charging": "charging", + "Idle": "idle", +} + +CURRENT_STATES: Final = { + "Fault": "fault", + "Charging Fault 1": "charging_fault_1", + "Charging Fault 2": "charging_fault_2", + "Waiting for swipe": "waiting_for_swipe", + "Waiting for button": "waiting_for_button", + "Not Connected": "not_connected", + "Ready to charge": "ready_to_charge", + "Charging": "charging", + "Completed": "completed", + "Completed Full Charge": "completed_full_charge", + "Charging Reservation": "charging_reservation", +} + + +def _enum_state(value: str | None, states: Mapping[str, str]) -> str | None: + """Return the stable Home Assistant value for a charger state.""" + + return states.get(value) if value is not None else None + @dataclass(frozen=True, kw_only=True) class BesenSensorEntityDescription(SensorEntityDescription): """Describe a Besen sensor entity.""" - value_fn: Callable[[BesenData], float | int | None] + value_fn: Callable[[BesenData], StateType] three_phase_only: bool = False SENSOR_DESCRIPTIONS: tuple[BesenSensorEntityDescription, ...] = ( + BesenSensorEntityDescription( + key="charging_status", + translation_key="charging_status", + device_class=SensorDeviceClass.ENUM, + options=list(CHARGING_STATES.values()), + value_fn=lambda data: _enum_state(data.charge.charging_status, CHARGING_STATES), + ), + BesenSensorEntityDescription( + key="charging_message", + translation_key="charging_message", + device_class=SensorDeviceClass.ENUM, + options=list(CHARGING_MESSAGES.values()), + value_fn=lambda data: _enum_state( + data.charge.charging_status_description, CHARGING_MESSAGES + ), + ), + BesenSensorEntityDescription( + key="error_state", + translation_key="error_state", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=list(ERROR_STATES.values()), + value_fn=lambda data: _enum_state(data.charge.error_details, ERROR_STATES), + ), + BesenSensorEntityDescription( + key="plug_state", + translation_key="plug_state", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=list(PLUG_STATES.values()), + value_fn=lambda data: _enum_state(data.charge.plug_state, PLUG_STATES), + ), + BesenSensorEntityDescription( + key="output_state", + translation_key="output_state", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=list(OUTPUT_STATES.values()), + value_fn=lambda data: _enum_state(data.charge.output_state, OUTPUT_STATES), + ), + BesenSensorEntityDescription( + key="current_state", + translation_key="current_state", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=list(CURRENT_STATES.values()), + value_fn=lambda data: _enum_state(data.charge.current_state, CURRENT_STATES), + ), BesenSensorEntityDescription( key="charging_power", translation_key="charging_power", diff --git a/homeassistant/components/besen/strings.json b/homeassistant/components/besen/strings.json index a9508a9df882..547a52e1feed 100644 --- a/homeassistant/components/besen/strings.json +++ b/homeassistant/components/besen/strings.json @@ -42,7 +42,74 @@ "charging_current": { "name": "Charging current" } }, "sensor": { + "charging_message": { + "name": "Charging message", + "state": { + "charging": "[%key:common::state::charging%]", + "charging_completed": "Charging completed", + "charging_reservation": "Charging reservation", + "ev_connected_press_start": "EV is connected, press start", + "plug_not_connected": "The plug is not connected. Connect it before starting charging.", + "see_error_state": "See error state", + "waiting_for_button": "Waiting for the button to activate", + "waiting_for_ev": "Charging started, waiting for the EV", + "waiting_for_swipe": "Waiting for a card swipe" + } + }, "charging_power": { "name": "Charging power" }, + "charging_status": { + "name": "Charging status", + "state": { + "canceled": "Canceled", + "connect": "Connect", + "fault": "[%key:common::state::fault%]", + "finish_charging": "Finish charging", + "finished": "Finished", + "start": "Start", + "waiting": "Waiting" + } + }, + "current_state": { + "name": "Current state", + "state": { + "charging": "[%key:common::state::charging%]", + "charging_fault_1": "Charging fault 1", + "charging_fault_2": "Charging fault 2", + "charging_reservation": "Charging reservation", + "completed": "Completed", + "completed_full_charge": "Completed, fully charged", + "fault": "[%key:common::state::fault%]", + "not_connected": "Not connected", + "ready_to_charge": "Ready to charge", + "waiting_for_button": "Waiting for button", + "waiting_for_swipe": "Waiting for card swipe" + } + }, + "error_state": { + "name": "Error state", + "state": { + "cc_error": "CC error", + "cp_error": "CP error", + "diode_short_circuit": "Diode short circuit", + "dlb_over_current": "DLB overcurrent, mains overload", + "eeprom_failure": "EEPROM failure", + "emergency_stop": "Emergency stop", + "flash_memory_failure": "Flash memory failure", + "input_power_error": "Input power error", + "leakage_protection": "Leakage protection", + "low_voltage": "Low voltage", + "metering_module_failure": "Metering module failure", + "no_error": "No error", + "offline": "Offline", + "over_current": "Overcurrent", + "over_temperature": "Overtemperature", + "over_voltage": "Overvoltage", + "relay_stick_error": "Relay stick error", + "rtc_failure": "RTC failure", + "short_circuit": "Short circuit", + "ungrounded": "Ungrounded" + } + }, "external_temperature": { "name": "External temperature" }, "internal_temperature": { "name": "Internal temperature" }, "l1_current": { "name": "L1 current" }, @@ -51,6 +118,21 @@ "l2_voltage": { "name": "L2 voltage" }, "l3_current": { "name": "L3 current" }, "l3_voltage": { "name": "L3 voltage" }, + "output_state": { + "name": "Output state", + "state": { + "charging": "[%key:common::state::charging%]", + "idle": "[%key:common::state::idle%]" + } + }, + "plug_state": { + "name": "Plug state", + "state": { + "connected_locked": "Connected, locked", + "connected_unlocked": "Connected, unlocked", + "disconnected": "[%key:common::state::disconnected%]" + } + }, "session_energy": { "name": "Session energy" }, "total_energy": { "name": "Total energy" } }, diff --git a/tests/components/besen/conftest.py b/tests/components/besen/conftest.py index 9de1e42b0e07..8f79e287e6a7 100644 --- a/tests/components/besen/conftest.py +++ b/tests/components/besen/conftest.py @@ -80,6 +80,12 @@ def charger_state( if charge is not None else ChargeStatus( charger_status=charger_status, + error_details="No Error", + charging_status="Start", + charging_status_description="EV is connected, please press start", + plug_state="Connected Locked", + output_state="Charging", + current_state="Charging", power=3500, total_energy=12.3, session_energy=1.2, diff --git a/tests/components/besen/snapshots/test_sensor.ambr b/tests/components/besen/snapshots/test_sensor.ambr index ef3c9a4baf5c..423723bf38c8 100644 --- a/tests/components/besen/snapshots/test_sensor.ambr +++ b/tests/components/besen/snapshots/test_sensor.ambr @@ -1,4 +1,78 @@ # serializer version: 1 +# name: test_sensor_state[single_phase][sensor.garage_charging_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'ev_connected_press_start', + 'charging', + 'waiting_for_ev', + 'charging_completed', + 'charging_reservation', + 'plug_not_connected', + 'see_error_state', + 'waiting_for_swipe', + 'waiting_for_button', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging message', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging message', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_message', + 'unique_id': 'AA:BB_charging_message', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_charging_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Charging message', + : list([ + 'ev_connected_press_start', + 'charging', + 'waiting_for_ev', + 'charging_completed', + 'charging_reservation', + 'plug_not_connected', + 'see_error_state', + 'waiting_for_swipe', + 'waiting_for_button', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_charging_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ev_connected_press_start', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_charging_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -57,6 +131,250 @@ 'state': '3500', }) # --- +# name: test_sensor_state[single_phase][sensor.garage_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'start', + 'finish_charging', + 'waiting', + 'finished', + 'canceled', + 'connect', + 'fault', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'AA:BB_charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Charging status', + : list([ + 'start', + 'finish_charging', + 'waiting', + 'finished', + 'canceled', + 'connect', + 'fault', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'start', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_current_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'fault', + 'charging_fault_1', + 'charging_fault_2', + 'waiting_for_swipe', + 'waiting_for_button', + 'not_connected', + 'ready_to_charge', + 'charging', + 'completed', + 'completed_full_charge', + 'charging_reservation', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_current_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_state', + 'unique_id': 'AA:BB_current_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_current_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Current state', + : list([ + 'fault', + 'charging_fault_1', + 'charging_fault_2', + 'waiting_for_swipe', + 'waiting_for_button', + 'not_connected', + 'ready_to_charge', + 'charging', + 'completed', + 'completed_full_charge', + 'charging_reservation', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_current_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_error_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'relay_stick_error', + 'offline', + 'cc_error', + 'cp_error', + 'emergency_stop', + 'over_temperature', + 'leakage_protection', + 'short_circuit', + 'over_current', + 'ungrounded', + 'over_voltage', + 'low_voltage', + 'input_power_error', + 'dlb_over_current', + 'diode_short_circuit', + 'rtc_failure', + 'flash_memory_failure', + 'eeprom_failure', + 'metering_module_failure', + 'no_error', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_error_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Error state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Error state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'error_state', + 'unique_id': 'AA:BB_error_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_error_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Error state', + : list([ + 'relay_stick_error', + 'offline', + 'cc_error', + 'cp_error', + 'emergency_stop', + 'over_temperature', + 'leakage_protection', + 'short_circuit', + 'over_current', + 'ungrounded', + 'over_voltage', + 'low_voltage', + 'input_power_error', + 'dlb_over_current', + 'diode_short_circuit', + 'rtc_failure', + 'flash_memory_failure', + 'eeprom_failure', + 'metering_module_failure', + 'no_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_error_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_error', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_external_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -289,6 +607,128 @@ 'state': '230.0', }) # --- +# name: test_sensor_state[single_phase][sensor.garage_output_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'charging', + 'idle', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_output_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Output state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Output state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'output_state', + 'unique_id': 'AA:BB_output_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_output_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Output state', + : list([ + 'charging', + 'idle', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_output_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_plug_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disconnected', + 'connected_unlocked', + 'connected_locked', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_plug_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Plug state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plug state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plug_state', + 'unique_id': 'AA:BB_plug_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[single_phase][sensor.garage_plug_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Plug state', + : list([ + 'disconnected', + 'connected_unlocked', + 'connected_locked', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_plug_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'connected_locked', + }) +# --- # name: test_sensor_state[single_phase][sensor.garage_session_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -405,6 +845,80 @@ 'state': '12.3', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_charging_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'ev_connected_press_start', + 'charging', + 'waiting_for_ev', + 'charging_completed', + 'charging_reservation', + 'plug_not_connected', + 'see_error_state', + 'waiting_for_swipe', + 'waiting_for_button', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging message', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging message', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_message', + 'unique_id': 'AA:BB_charging_message', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_charging_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Charging message', + : list([ + 'ev_connected_press_start', + 'charging', + 'waiting_for_ev', + 'charging_completed', + 'charging_reservation', + 'plug_not_connected', + 'see_error_state', + 'waiting_for_swipe', + 'waiting_for_button', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_charging_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ev_connected_press_start', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_charging_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -463,6 +977,250 @@ 'state': '3500', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'start', + 'finish_charging', + 'waiting', + 'finished', + 'canceled', + 'connect', + 'fault', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'AA:BB_charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Charging status', + : list([ + 'start', + 'finish_charging', + 'waiting', + 'finished', + 'canceled', + 'connect', + 'fault', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'start', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_current_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'fault', + 'charging_fault_1', + 'charging_fault_2', + 'waiting_for_swipe', + 'waiting_for_button', + 'not_connected', + 'ready_to_charge', + 'charging', + 'completed', + 'completed_full_charge', + 'charging_reservation', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_current_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_state', + 'unique_id': 'AA:BB_current_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_current_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Current state', + : list([ + 'fault', + 'charging_fault_1', + 'charging_fault_2', + 'waiting_for_swipe', + 'waiting_for_button', + 'not_connected', + 'ready_to_charge', + 'charging', + 'completed', + 'completed_full_charge', + 'charging_reservation', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_current_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_error_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'relay_stick_error', + 'offline', + 'cc_error', + 'cp_error', + 'emergency_stop', + 'over_temperature', + 'leakage_protection', + 'short_circuit', + 'over_current', + 'ungrounded', + 'over_voltage', + 'low_voltage', + 'input_power_error', + 'dlb_over_current', + 'diode_short_circuit', + 'rtc_failure', + 'flash_memory_failure', + 'eeprom_failure', + 'metering_module_failure', + 'no_error', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_error_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Error state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Error state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'error_state', + 'unique_id': 'AA:BB_error_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_error_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Error state', + : list([ + 'relay_stick_error', + 'offline', + 'cc_error', + 'cp_error', + 'emergency_stop', + 'over_temperature', + 'leakage_protection', + 'short_circuit', + 'over_current', + 'ungrounded', + 'over_voltage', + 'low_voltage', + 'input_power_error', + 'dlb_over_current', + 'diode_short_circuit', + 'rtc_failure', + 'flash_memory_failure', + 'eeprom_failure', + 'metering_module_failure', + 'no_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_error_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_error', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_external_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -927,6 +1685,128 @@ 'state': '232.0', }) # --- +# name: test_sensor_state[three_phase][sensor.garage_output_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'charging', + 'idle', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_output_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Output state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Output state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'output_state', + 'unique_id': 'AA:BB_output_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_output_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Output state', + : list([ + 'charging', + 'idle', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_output_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_plug_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disconnected', + 'connected_unlocked', + 'connected_locked', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_plug_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Plug state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plug state', + 'platform': 'besen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plug_state', + 'unique_id': 'AA:BB_plug_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_state[three_phase][sensor.garage_plug_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'Garage Plug state', + : list([ + 'disconnected', + 'connected_unlocked', + 'connected_locked', + ]), + }), + 'context': , + 'entity_id': 'sensor.garage_plug_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'connected_locked', + }) +# --- # name: test_sensor_state[three_phase][sensor.garage_session_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/besen/test_sensor.py b/tests/components/besen/test_sensor.py index 27495e588b2b..12576814fefb 100644 --- a/tests/components/besen/test_sensor.py +++ b/tests/components/besen/test_sensor.py @@ -2,10 +2,26 @@ from unittest.mock import Mock +from besen.const import ( + CHARGING_STATUS, + CHARGING_STATUS_DESCRIPTIONS, + CURRENT_STATE, + ERRORS, + OUTPUT_STATE, + PLUG_STATE, +) from besen.models import ChargeStatus import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.besen.sensor import ( + CHARGING_MESSAGES, + CHARGING_STATES, + CURRENT_STATES, + ERROR_STATES, + OUTPUT_STATES, + PLUG_STATES, +) from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -21,6 +37,8 @@ from .conftest import charger_state, setup_integration from tests.common import MockConfigEntry, snapshot_platform POWER_ENTITY_ID = "sensor.garage_charging_power" +CHARGING_STATUS_ENTITY_ID = "sensor.garage_charging_status" +CHARGING_MESSAGE_ENTITY_ID = "sensor.garage_charging_message" @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -42,6 +60,7 @@ async def test_sensor_state( mock_besen_client.async_start.assert_awaited_once() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_updates_from_client( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -55,6 +74,12 @@ async def test_sensor_updates_from_client( mock_besen_client, charger_state( charge=ChargeStatus( + error_details="Emergency Stop", + charging_status="Fault", + charging_status_description="See Error State", + plug_state="Disconnected", + output_state="Idle", + current_state="Ready to charge", power=7200, total_energy=123.45, session_energy=4.56, @@ -72,6 +97,18 @@ async def test_sensor_updates_from_client( assert state.state == "4.56" assert (state := hass.states.get("sensor.garage_internal_temperature")) is not None assert state.state == "26.5" + assert (state := hass.states.get(CHARGING_STATUS_ENTITY_ID)) is not None + assert state.state == "fault" + assert (state := hass.states.get(CHARGING_MESSAGE_ENTITY_ID)) is not None + assert state.state == "see_error_state" + assert (state := hass.states.get("sensor.garage_error_state")) is not None + assert state.state == "emergency_stop" + assert (state := hass.states.get("sensor.garage_plug_state")) is not None + assert state.state == "disconnected" + assert (state := hass.states.get("sensor.garage_output_state")) is not None + assert state.state == "idle" + assert (state := hass.states.get("sensor.garage_current_state")) is not None + assert state.state == "ready_to_charge" async def test_sensor_unknown_value( @@ -88,6 +125,60 @@ async def test_sensor_unknown_value( assert (state := hass.states.get(POWER_ENTITY_ID)) is not None assert state.state == STATE_UNKNOWN + assert (state := hass.states.get(CHARGING_STATUS_ENTITY_ID)) is not None + assert state.state == STATE_UNKNOWN + assert (state := hass.states.get(CHARGING_MESSAGE_ENTITY_ID)) is not None + assert state.state == STATE_UNKNOWN + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + "value", + [ + pytest.param(None, id="missing"), + pytest.param("Unexpected", id="unsupported"), + pytest.param("Unknown", id="unknown"), + *( + pytest.param(f"Unknown {index}", id=f"unknown_{index}") + for index in range(11) + ), + ], +) +async def test_enum_sensors_unknown_for_unsupported_values( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_besen_client: Mock, + value: str | None, +) -> None: + """Test missing and undocumented protocol values clear the previous state.""" + + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) + + publish_besen_state( + mock_besen_client, + charger_state( + charge=ChargeStatus( + error_details=value, + charging_status=value, + charging_status_description=value, + plug_state=value, + output_state=value, + current_state=value, + ) + ), + ) + await hass.async_block_till_done() + + for entity_id in ( + CHARGING_STATUS_ENTITY_ID, + CHARGING_MESSAGE_ENTITY_ID, + "sensor.garage_error_state", + "sensor.garage_plug_state", + "sensor.garage_output_state", + "sensor.garage_current_state", + ): + assert (state := hass.states.get(entity_id)) is not None + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize( @@ -139,12 +230,16 @@ async def test_diagnostic_sensors_disabled_by_default( } assert set(diagnostic_entries) == { f"{mock_besen_client.address}_external_temperature", + f"{mock_besen_client.address}_error_state", + f"{mock_besen_client.address}_current_state", 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", + f"{mock_besen_client.address}_output_state", + f"{mock_besen_client.address}_plug_state", } for entry in diagnostic_entries.values(): assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @@ -178,3 +273,20 @@ async def test_three_phase_sensor_filtering( f"{mock_besen_client.address}_l3_current", } assert three_phase_unique_ids.issubset(unique_ids) is expected + + +def test_enum_sensor_options_cover_known_library_states() -> None: + """Test every known library state has a stable Home Assistant option.""" + + assert set(ERROR_STATES) == set(ERRORS.values()) - {"Unknown"} + assert set(CHARGING_STATES) == set(CHARGING_STATUS.values()) + assert set(CHARGING_MESSAGES) == set(CHARGING_STATUS_DESCRIPTIONS.values()) + assert set(PLUG_STATES) == set(PLUG_STATE) - { + f"Unknown {index}" for index in range(6) + } + assert set(OUTPUT_STATES) == set(OUTPUT_STATE) - { + f"Unknown {index}" for index in range(7) + } + assert set(CURRENT_STATES) == set(CURRENT_STATE) - { + f"Unknown {index}" for index in range(1, 11) + }