Add Besen status sensors (#180887)

This commit is contained in:
Yoav Mor
2026-09-12 10:16:36 +02:00
committed by GitHub
parent 626d48147c
commit 2486d57519
5 changed files with 1213 additions and 3 deletions
+133 -3
View File
@@ -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",
@@ -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" }
},
+6
View File
@@ -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,
@@ -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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.garage_charging_message',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charging message',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Charging message',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_charging_message',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'start',
'finish_charging',
'waiting',
'finished',
'canceled',
'connect',
'fault',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.garage_charging_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charging status',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Charging status',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'start',
'finish_charging',
'waiting',
'finished',
'canceled',
'connect',
'fault',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_charging_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'start',
})
# ---
# name: test_sensor_state[single_phase][sensor.garage_current_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_current_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Current state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_current_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'charging',
})
# ---
# name: test_sensor_state[single_phase][sensor.garage_error_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_error_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Error state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Error state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_error_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'idle',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_output_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Output state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Output state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'idle',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_output_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'charging',
})
# ---
# name: test_sensor_state[single_phase][sensor.garage_plug_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disconnected',
'connected_unlocked',
'connected_locked',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_plug_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Plug state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Plug state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disconnected',
'connected_unlocked',
'connected_locked',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_plug_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.garage_charging_message',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charging message',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Charging message',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_charging_message',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'start',
'finish_charging',
'waiting',
'finished',
'canceled',
'connect',
'fault',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.garage_charging_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charging status',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Charging status',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'start',
'finish_charging',
'waiting',
'finished',
'canceled',
'connect',
'fault',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_charging_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'start',
})
# ---
# name: test_sensor_state[three_phase][sensor.garage_current_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_current_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Current state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_current_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'charging',
})
# ---
# name: test_sensor_state[three_phase][sensor.garage_error_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_error_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Error state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Error state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: 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': <ANY>,
'entity_id': 'sensor.garage_error_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'idle',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_output_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Output state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Output state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'idle',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_output_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'charging',
})
# ---
# name: test_sensor_state[three_phase][sensor.garage_plug_state-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disconnected',
'connected_unlocked',
'connected_locked',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.garage_plug_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Plug state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Garage Plug state',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'disconnected',
'connected_unlocked',
'connected_locked',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.garage_plug_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'connected_locked',
})
# ---
# name: test_sensor_state[three_phase][sensor.garage_session_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+112
View File
@@ -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)
}