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 = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
|
Platform.EVENT,
|
||||||
Platform.FAN,
|
Platform.FAN,
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
|
@ -28,7 +29,6 @@ PLATFORMS = [
|
||||||
Platform.TIME,
|
Platform.TIME,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
||||||
SYNC_TIME_INTERVAL = timedelta(hours=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": {
|
"fan": {
|
||||||
"pump": {
|
"pump": {
|
||||||
"name": "Pump {index}"
|
"name": "Pump {index}"
|
||||||
|
|
|
@ -68,4 +68,6 @@ def client_fixture() -> Generator[MagicMock]:
|
||||||
client.pumps = []
|
client.pumps = []
|
||||||
client.temperature_range.state = LowHighRange.LOW
|
client.temperature_range.state = LowHighRange.LOW
|
||||||
|
|
||||||
|
client.fault = None
|
||||||
|
|
||||||
yield client
|
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