Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions homeassistant/components/openevse/icons.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
28 changes: 27 additions & 1 deletion homeassistant/components/openevse/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)
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
Expand All @@ -53,6 +54,29 @@
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."""
Expand All @@ -65,7 +89,9 @@ class OpenEVSESensorDescription(SensorEntityDescription):
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",
Expand Down
16 changes: 15 additions & 1 deletion homeassistant/components/openevse/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
36 changes: 33 additions & 3 deletions tests/components/openevse/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand All @@ -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]
Expand Down
49 changes: 46 additions & 3 deletions tests/components/openevse/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -92,15 +92,15 @@ 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()
await hass.async_block_till_done()

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(
Expand Down Expand Up @@ -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
Loading