Add icons, state translations and enum mapping for OpenEVSE status sensor (#180329)

Co-authored-by: Colin L Rice <colin@daedrum.net>
This commit is contained in:
Chris
2026-08-27 15:51:16 +02:00
committed by GitHub
co-authored by Colin L Rice
parent d1cd298141
commit dc6b5711b4
5 changed files with 167 additions and 8 deletions
@@ -0,0 +1,46 @@
{
"entity": {
"binary_sensor": {
"divert_active": {
"default": "mdi:call-split"
},
"has_limit": {
"default": "mdi:car-speed-limiter"
},
"shaper_active": {
"default": "mdi:tune-variant"
}
},
"sensor": {
"gfi_trip_count": {
"default": "mdi:counter"
},
"no_gnd_trip_count": {
"default": "mdi:counter"
},
"service_level": {
"default": "mdi:home-lightning-bolt-outline"
},
"status": {
"default": "mdi:ev-station",
"state": {
"charging": "mdi:battery-charging",
"connected": "mdi:power-plug",
"diode_check_failed": "mdi:alert-circle",
"disabled": "mdi:stop-circle-outline",
"gfci_fault": "mdi:alert-circle",
"gfci_self_test_failure": "mdi:alert-circle",
"no_ground": "mdi:alert",
"not_connected": "mdi:power-plug-off",
"over_temperature": "mdi:thermometer-alert",
"sleeping": "mdi:sleep",
"stuck_relay": "mdi:alert",
"vent_required": "mdi:fan-alert"
}
},
"stuck_relay_trip_count": {
"default": "mdi:counter"
}
}
}
}
+27 -1
View File
@@ -44,6 +44,7 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import DOMAIN, INTEGRATION_TITLE
from .coordinator import OpenEVSEConfigEntry, OpenEVSEDataUpdateCoordinator
@@ -53,6 +54,29 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
STATUS_OPTIONS: list[str] = [
"charging",
"connected",
"diode_check_failed",
"disabled",
"gfci_fault",
"gfci_self_test_failure",
"no_ground",
"not_connected",
"over_temperature",
"sleeping",
"stuck_relay",
"vent_required",
]
def _map_status(status: str | None) -> str | None:
"""Map raw status string to enum option."""
if status is not None and (slug := slugify(status)) in STATUS_OPTIONS:
return slug
return None
@dataclass(frozen=True, kw_only=True)
class OpenEVSESensorDescription(SensorEntityDescription):
"""Describes an OpenEVSE sensor entity."""
@@ -65,7 +89,9 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
OpenEVSESensorDescription(
key="status",
translation_key="status",
value_fn=lambda ev: ev.status,
device_class=SensorDeviceClass.ENUM,
options=STATUS_OPTIONS,
value_fn=lambda ev: _map_status(ev.status),
),
OpenEVSESensorDescription(
key="service_level",
+15 -1
View File
@@ -172,7 +172,21 @@
"name": "Smoothed available current"
},
"status": {
"name": "Charging status"
"name": "Charging status",
"state": {
"charging": "Charging",
"connected": "Connected",
"diode_check_failed": "Diode check failed",
"disabled": "Disabled",
"gfci_fault": "GFCI fault",
"gfci_self_test_failure": "GFCI self-test failure",
"no_ground": "No ground",
"not_connected": "Not connected",
"over_temperature": "Over temperature",
"sleeping": "Sleeping",
"stuck_relay": "Stuck relay",
"vent_required": "Vent required"
}
},
"stuck_relay_trip_count": {
"name": "Stuck relay trip count"
@@ -246,7 +246,22 @@
None,
]),
'area_id': None,
'capabilities': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'connected',
'diode_check_failed',
'disabled',
'gfci_fault',
'gfci_self_test_failure',
'no_ground',
'not_connected',
'over_temperature',
'sleeping',
'stuck_relay',
'vent_required',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -265,7 +280,7 @@
'object_id_base': 'Charging status',
'options': dict({
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Charging status',
'platform': 'openevse',
@@ -280,14 +295,29 @@
# name: test_entities[sensor.openevse_mock_config_charging_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'openevse_mock_config Charging status',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'charging',
'connected',
'diode_check_failed',
'disabled',
'gfci_fault',
'gfci_self_test_failure',
'no_ground',
'not_connected',
'over_temperature',
'sleeping',
'stuck_relay',
'vent_required',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.openevse_mock_config_charging_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Charging',
'state': 'charging',
})
# ---
# name: test_entities[sensor.openevse_mock_config_charging_voltage-entry]
+46 -3
View File
@@ -77,7 +77,7 @@ async def test_missing_sensor_graceful_handling(
# Other sensors should still work
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state is not None
assert state.state == "Charging"
assert state.state == "charging"
async def test_websocket_callback_updates_entities(
@@ -92,7 +92,7 @@ async def test_websocket_callback_updates_entities(
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state
assert state.state == "Charging"
assert state.state == "charging"
mock_charger.status = "Sleeping"
await mock_charger.callback()
@@ -100,7 +100,7 @@ async def test_websocket_callback_updates_entities(
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state
assert state.state == "Sleeping"
assert state.state == "sleeping"
async def test_sensor_unavailable_on_coordinator_timeout(
@@ -187,3 +187,46 @@ async def test_yaml_import_already_configured(
issue = issue_registry.async_get_issue("homeassistant", "deprecated_yaml")
assert issue is not None
assert issue.issue_domain == DOMAIN
@pytest.mark.parametrize(
("raw_status", "expected_state"),
[
pytest.param("not connected", "not_connected", id="not_connected"),
pytest.param("connected", "connected", id="connected"),
pytest.param("charging", "charging", id="charging"),
pytest.param("vent required", "vent_required", id="vent_required"),
pytest.param(
"diode check failed", "diode_check_failed", id="diode_check_failed"
),
pytest.param("gfci fault", "gfci_fault", id="gfci_fault"),
pytest.param("no ground", "no_ground", id="no_ground"),
pytest.param("stuck relay", "stuck_relay", id="stuck_relay"),
pytest.param(
"gfci self-test failure",
"gfci_self_test_failure",
id="gfci_self_test_failure",
),
pytest.param("over temperature", "over_temperature", id="over_temperature"),
pytest.param("sleeping", "sleeping", id="sleeping"),
pytest.param("disabled", "disabled", id="disabled"),
pytest.param("unknown", STATE_UNKNOWN, id="unknown"),
pytest.param("unrecognized_raw_status", STATE_UNKNOWN, id="fallback_unknown"),
pytest.param(None, STATE_UNKNOWN, id="none_status"),
],
)
async def test_status_sensor_mapping(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_charger: MagicMock,
raw_status: str | None,
expected_state: str,
) -> None:
"""Test status sensor mapping to enum options."""
mock_charger.status = raw_status
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state is not None
assert state.state == expected_state