From ffb9ab21c15f2a6b1b8b96e0895f66733f6f12ac Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Wed, 26 May 2021 09:25:47 -0700 Subject: [PATCH] Add binary sensor for smarttub errors (#49364) --- .../components/smarttub/binary_sensor.py | 63 ++++++++++++++++++- homeassistant/components/smarttub/const.py | 1 + .../components/smarttub/controller.py | 5 +- tests/components/smarttub/conftest.py | 2 + .../components/smarttub/test_binary_sensor.py | 44 ++++++++++++- 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index d1019f7f432..7ab343d2015 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,7 +1,7 @@ """Platform for binary sensor integration.""" import logging -from smarttub import SpaReminder +from smarttub import SpaError, SpaReminder import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.helpers import entity_platform -from .const import ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER +from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,13 @@ _LOGGER = logging.getLogger(__name__) # whether the reminder has been snoozed (bool) ATTR_REMINDER_SNOOZED = "snoozed" +ATTR_ERROR_CODE = "error_code" +ATTR_ERROR_TITLE = "error_title" +ATTR_ERROR_DESCRIPTION = "error_description" +ATTR_ERROR_TYPE = "error_type" +ATTR_CREATED_AT = "created_at" +ATTR_UPDATED_AT = "updated_at" + # how many days to snooze the reminder for ATTR_SNOOZE_DAYS = "days" SNOOZE_REMINDER_SCHEMA = { @@ -34,6 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [] for spa in controller.spas: entities.append(SmartTubOnline(controller.coordinator, spa)) + entities.append(SmartTubError(controller.coordinator, spa)) entities.extend( SmartTubReminder(controller.coordinator, spa, reminder) for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values() @@ -119,3 +127,54 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Snooze this reminder for the specified number of days.""" await self.reminder.snooze(days) await self.coordinator.async_request_refresh() + + +class SmartTubError(SmartTubEntity, BinarySensorEntity): + """Indicates whether an error code is present. + + There may be 0 or more errors. If there are >0, we show the first one. + """ + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__( + coordinator, + spa, + "Error", + ) + + @property + def error(self) -> SpaError: + """Return the underlying SpaError object for this entity.""" + errors = self.coordinator.data[self.spa.id][ATTR_ERRORS] + if len(errors) == 0: + return None + return errors[0] + + @property + def is_on(self) -> bool: + """Return true if an error is signaled.""" + return self.error is not None + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + + error = self.error + + if error is None: + return {} + + return { + ATTR_ERROR_CODE: error.code, + ATTR_ERROR_TITLE: error.title, + ATTR_ERROR_DESCRIPTION: error.description, + ATTR_ERROR_TYPE: error.error_type, + ATTR_CREATED_AT: error.created_at.isoformat(), + ATTR_UPDATED_AT: error.updated_at.isoformat(), + } + + @property + def device_class(self) -> str: + """Return the device class for this entity.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 23bd8bd8ec0..f97ef65a54c 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -21,6 +21,7 @@ DEFAULT_LIGHT_EFFECT = "purple" # default to 50% brightness DEFAULT_LIGHT_BRIGHTNESS = 128 +ATTR_ERRORS = "errors" ATTR_LIGHTS = "lights" ATTR_PUMPS = "pumps" ATTR_REMINDERS = "reminders" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 06b0989233c..48b1d603c5c 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -16,6 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + ATTR_ERRORS, ATTR_LIGHTS, ATTR_PUMPS, ATTR_REMINDERS, @@ -92,15 +93,17 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - full_status, reminders = await asyncio.gather( + full_status, reminders, errors = await asyncio.gather( spa.get_status_full(), spa.get_reminders(), + spa.get_errors(), ) return { ATTR_STATUS: full_status, ATTR_PUMPS: {pump.id: pump for pump in full_status.pumps}, ATTR_LIGHTS: {light.zone: light for light in full_status.lights}, ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders}, + ATTR_ERRORS: errors, } async def async_register_devices(self, entry): diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 2b6991fbbe0..c05762a903d 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -87,6 +87,8 @@ def mock_spa(spa_state): mock_spa.get_reminders.return_value = [mock_filter_reminder] + mock_spa.get_errors.return_value = [] + return mock_spa diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index b39986ef394..16b4f60d3e4 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -1,5 +1,11 @@ """Test the SmartTub binary sensor platform.""" -from homeassistant.components.binary_sensor import STATE_OFF +from datetime import datetime +from unittest.mock import create_autospec + +import pytest +import smarttub + +from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON async def test_binary_sensors(spa, setup_entry, hass): @@ -10,6 +16,11 @@ async def test_binary_sensors(spa, setup_entry, hass): # disabled by default assert state is None + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + async def test_reminders(spa, setup_entry, hass): """Test the reminder sensor.""" @@ -21,6 +32,37 @@ async def test_reminders(spa, setup_entry, hass): assert state.attributes["snoozed"] is False +@pytest.fixture +def mock_error(spa): + """Mock error.""" + error = create_autospec(smarttub.SpaError, instance=True) + error.code = 11 + error.title = "Flow Switch Stuck Open" + error.description = None + error.active = True + error.created_at = datetime.now() + error.updated_at = datetime.now() + error.error_type = "TUB_ERROR" + return error + + +async def test_error(spa, hass, config_entry, mock_error): + """Test the error sensor.""" + + spa.get_errors.return_value = [mock_error] + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + + assert state.state == STATE_ON + assert state.attributes["error_code"] == 11 + + async def test_snooze(spa, setup_entry, hass): """Test snoozing a reminder."""