Add fault event to balboa (#138623)
* Add fault sensor to balboa * Use an event instead of sensor for faults * Don't set fault initially in conftest * Use event type per fault message code * Set fault to None in conftestpull/138685/head^2
parent
e63b17cd58
commit
f76e295204
|
@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
|
@ -28,7 +29,6 @@ PLATFORMS = [
|
|||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
||||
SYNC_TIME_INTERVAL = timedelta(hours=1)
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
"""Support for Balboa events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pybalboa import EVENT_UPDATE, SpaClient
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
FAULT = "fault"
|
||||
FAULT_DATE = "fault_date"
|
||||
REQUEST_FAULT_LOG_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
FAULT_MESSAGE_CODE_MAP: dict[int, str] = {
|
||||
15: "sensor_out_of_sync",
|
||||
16: "low_flow",
|
||||
17: "flow_failed",
|
||||
18: "settings_reset",
|
||||
19: "priming_mode",
|
||||
20: "clock_failed",
|
||||
21: "settings_reset",
|
||||
22: "memory_failure",
|
||||
26: "service_sensor_sync",
|
||||
27: "heater_dry",
|
||||
28: "heater_may_be_dry",
|
||||
29: "water_too_hot",
|
||||
30: "heater_too_hot",
|
||||
31: "sensor_a_fault",
|
||||
32: "sensor_b_fault",
|
||||
34: "pump_stuck",
|
||||
35: "hot_fault",
|
||||
36: "gfci_test_failed",
|
||||
37: "standby_mode",
|
||||
}
|
||||
FAULT_EVENT_TYPES = sorted(set(FAULT_MESSAGE_CODE_MAP.values()))
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's events."""
|
||||
async_add_entities([BalboaEventEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
class BalboaEventEntity(BalboaEntity, EventEntity):
|
||||
"""Representation of a Balboa event entity."""
|
||||
|
||||
_attr_event_types = FAULT_EVENT_TYPES
|
||||
_attr_translation_key = FAULT
|
||||
|
||||
def __init__(self, spa: SpaClient) -> None:
|
||||
"""Initialize a Balboa event entity."""
|
||||
super().__init__(spa, FAULT)
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self) -> None:
|
||||
"""Handle the fault event."""
|
||||
if not (fault := self._client.fault):
|
||||
return
|
||||
fault_date = fault.fault_datetime.isoformat()
|
||||
if self.state_attributes.get(FAULT_DATE) != fault_date:
|
||||
self._trigger_event(
|
||||
FAULT_MESSAGE_CODE_MAP.get(fault.message_code, fault.message),
|
||||
{FAULT_DATE: fault_date, "code": fault.message_code},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(self._client.on(EVENT_UPDATE, self._async_handle_event))
|
||||
|
||||
async def request_fault_log(now: datetime | None = None) -> None:
|
||||
"""Request the most recent fault log."""
|
||||
await self._client.request_fault_log()
|
||||
|
||||
await request_fault_log()
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass, request_fault_log, REQUEST_FAULT_LOG_INTERVAL
|
||||
)
|
||||
)
|
|
@ -57,6 +57,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"fault": {
|
||||
"name": "Fault",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"sensor_out_of_sync": "Sensors are out of sync",
|
||||
"low_flow": "The water flow is low",
|
||||
"flow_failed": "The water flow has failed",
|
||||
"settings_reset": "The settings have been reset",
|
||||
"priming_mode": "Priming mode",
|
||||
"clock_failed": "The clock has failed",
|
||||
"memory_failure": "Program memory failure",
|
||||
"service_sensor_sync": "Sensors are out of sync -- call for service",
|
||||
"heater_dry": "The heater is dry",
|
||||
"heater_may_be_dry": "The heater may be dry",
|
||||
"water_too_hot": "The water is too hot",
|
||||
"heater_too_hot": "The heater is too hot",
|
||||
"sensor_a_fault": "Sensor A fault",
|
||||
"sensor_b_fault": "Sensor B fault",
|
||||
"pump_stuck": "A pump may be stuck on",
|
||||
"hot_fault": "Hot fault",
|
||||
"gfci_test_failed": "The GFCI test failed",
|
||||
"standby_mode": "Standby mode (hold mode)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"pump": {
|
||||
"name": "Pump {index}"
|
||||
|
|
|
@ -68,4 +68,6 @@ def client_fixture() -> Generator[MagicMock]:
|
|||
client.pumps = []
|
||||
client.temperature_range.state = LowHighRange.LOW
|
||||
|
||||
client.fault = None
|
||||
|
||||
yield client
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# serializer version: 1
|
||||
# name: test_events[event.fakespa_fault-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'clock_failed',
|
||||
'flow_failed',
|
||||
'gfci_test_failed',
|
||||
'heater_dry',
|
||||
'heater_may_be_dry',
|
||||
'heater_too_hot',
|
||||
'hot_fault',
|
||||
'low_flow',
|
||||
'memory_failure',
|
||||
'priming_mode',
|
||||
'pump_stuck',
|
||||
'sensor_a_fault',
|
||||
'sensor_b_fault',
|
||||
'sensor_out_of_sync',
|
||||
'service_sensor_sync',
|
||||
'settings_reset',
|
||||
'standby_mode',
|
||||
'water_too_hot',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.fakespa_fault',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fault',
|
||||
'platform': 'balboa',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fault',
|
||||
'unique_id': 'FakeSpa-fault-c0ffee',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_events[event.fakespa_fault-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'clock_failed',
|
||||
'flow_failed',
|
||||
'gfci_test_failed',
|
||||
'heater_dry',
|
||||
'heater_may_be_dry',
|
||||
'heater_too_hot',
|
||||
'hot_fault',
|
||||
'low_flow',
|
||||
'memory_failure',
|
||||
'priming_mode',
|
||||
'pump_stuck',
|
||||
'sensor_a_fault',
|
||||
'sensor_b_fault',
|
||||
'sensor_out_of_sync',
|
||||
'service_sensor_sync',
|
||||
'settings_reset',
|
||||
'standby_mode',
|
||||
'water_too_hot',
|
||||
]),
|
||||
'friendly_name': 'FakeSpa Fault',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.fakespa_fault',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
|
@ -0,0 +1,82 @@
|
|||
"""Tests of the events of the balboa integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import snapshot_platform
|
||||
|
||||
ENTITY_EVENT = "event.fakespa_fault"
|
||||
FAULT_DATE = "fault_date"
|
||||
|
||||
|
||||
async def test_events(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test spa events."""
|
||||
with patch("homeassistant.components.balboa.PLATFORMS", [Platform.EVENT]):
|
||||
entry = await init_integration(hass)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
async def test_event(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
"""Test spa fault event."""
|
||||
await init_integration(hass)
|
||||
|
||||
# check the state is unknown
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# set a fault
|
||||
client.fault = MagicMock(
|
||||
fault_datetime=datetime(2025, 2, 15, 13, 0), message_code=16
|
||||
)
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# check new state is what we expect
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
||||
|
||||
# set fault to None
|
||||
client.fault = None
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# validate state remains unchanged
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
||||
|
||||
# set fault to an unknown one
|
||||
client.fault = MagicMock(
|
||||
fault_datetime=datetime(2025, 2, 15, 14, 0), message_code=-1
|
||||
)
|
||||
# validate a ValueError is raises
|
||||
with pytest.raises(ValueError):
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# validate state remains unchanged
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
Loading…
Reference in New Issue