Add support for SmartTub filtration cycles (#46868)

This commit is contained in:
Matt Zimmerman 2021-02-21 21:36:50 -08:00 committed by GitHub
parent b1a24c8bbb
commit 5d8390fd9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 59 deletions

View File

@ -1,6 +1,8 @@
"""Platform for climate integration.""" """Platform for climate integration."""
import logging import logging
from smarttub import Spa
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT, CURRENT_HVAC_HEAT,
@ -38,9 +40,9 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity):
"""The target water temperature for the spa.""" """The target water temperature for the spa."""
PRESET_MODES = { PRESET_MODES = {
"AUTO": PRESET_NONE, Spa.HeatMode.AUTO: PRESET_NONE,
"ECO": PRESET_ECO, Spa.HeatMode.ECONOMY: PRESET_ECO,
"DAY": PRESET_DAY, Spa.HeatMode.DAY: PRESET_DAY,
} }
HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} HEAT_MODES = {v: k for k, v in PRESET_MODES.items()}
@ -62,7 +64,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity):
@property @property
def hvac_action(self): def hvac_action(self):
"""Return the current running hvac operation.""" """Return the current running hvac operation."""
return self.HVAC_ACTIONS.get(self.get_spa_status("heater")) return self.HVAC_ACTIONS.get(self.spa_status.heater)
@property @property
def hvac_modes(self): def hvac_modes(self):
@ -110,7 +112,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity):
@property @property
def preset_mode(self): def preset_mode(self):
"""Return the current preset mode.""" """Return the current preset mode."""
return self.PRESET_MODES[self.get_spa_status("heatMode")] return self.PRESET_MODES[self.spa_status.heat_mode]
@property @property
def preset_modes(self): def preset_modes(self):
@ -120,12 +122,12 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current water temperature.""" """Return the current water temperature."""
return self.get_spa_status("water.temperature") return self.spa_status.water.temperature
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the target water temperature.""" """Return the target water temperature."""
return self.get_spa_status("setTemperature") return self.spa_status.set_temperature
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""

View File

@ -52,18 +52,8 @@ class SmartTubEntity(CoordinatorEntity):
spa_name = get_spa_name(self.spa) spa_name = get_spa_name(self.spa)
return f"{spa_name} {self._entity_type}" return f"{spa_name} {self._entity_type}"
def get_spa_status(self, path): @property
"""Retrieve a value from the data returned by Spa.get_status(). def spa_status(self) -> smarttub.SpaState:
"""Retrieve the result of Spa.get_status()."""
Nested keys can be specified by a dotted path, e.g. return self.coordinator.data[self.spa.id].get("status")
status['foo']['bar'] is 'foo.bar'.
"""
status = self.coordinator.data[self.spa.id].get("status")
if status is None:
return None
for key in path.split("."):
status = status[key]
return status

View File

@ -6,7 +6,7 @@
"dependencies": [], "dependencies": [],
"codeowners": ["@mdz"], "codeowners": ["@mdz"],
"requirements": [ "requirements": [
"python-smarttub==0.0.12" "python-smarttub==0.0.17"
], ],
"quality_scale": "platinum" "quality_scale": "platinum"
} }

View File

@ -1,4 +1,5 @@
"""Platform for sensor integration.""" """Platform for sensor integration."""
from enum import Enum
import logging import logging
from .const import DOMAIN, SMARTTUB_CONTROLLER from .const import DOMAIN, SMARTTUB_CONTROLLER
@ -6,6 +7,11 @@ from .entity import SmartTubEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DURATION = "duration"
ATTR_LAST_UPDATED = "last_updated"
ATTR_MODE = "mode"
ATTR_START_HOUR = "start_hour"
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up sensor entities for the sensors in the tub.""" """Set up sensor entities for the sensors in the tub."""
@ -18,15 +24,17 @@ async def async_setup_entry(hass, entry, async_add_entities):
[ [
SmartTubSensor(controller.coordinator, spa, "State", "state"), SmartTubSensor(controller.coordinator, spa, "State", "state"),
SmartTubSensor( SmartTubSensor(
controller.coordinator, spa, "Flow Switch", "flowSwitch" controller.coordinator, spa, "Flow Switch", "flow_switch"
), ),
SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"), SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"),
SmartTubSensor( SmartTubSensor(
controller.coordinator, spa, "Blowout Cycle", "blowoutCycle" controller.coordinator, spa, "Blowout Cycle", "blowout_cycle"
), ),
SmartTubSensor( SmartTubSensor(
controller.coordinator, spa, "Cleanup Cycle", "cleanupCycle" controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle"
), ),
SmartTubPrimaryFiltrationCycle(controller.coordinator, spa),
SmartTubSecondaryFiltrationCycle(controller.coordinator, spa),
] ]
) )
@ -36,17 +44,69 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SmartTubSensor(SmartTubEntity): class SmartTubSensor(SmartTubEntity):
"""Generic and base class for SmartTub sensors.""" """Generic and base class for SmartTub sensors."""
def __init__(self, coordinator, spa, sensor_name, spa_status_key): def __init__(self, coordinator, spa, sensor_name, attr_name):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator, spa, sensor_name) super().__init__(coordinator, spa, sensor_name)
self._spa_status_key = spa_status_key self._attr_name = attr_name
@property @property
def _state(self): def _state(self):
"""Retrieve the underlying state from the spa.""" """Retrieve the underlying state from the spa."""
return self.get_spa_status(self._spa_status_key) return getattr(self.spa_status, self._attr_name)
@property @property
def state(self) -> str: def state(self) -> str:
"""Return the current state of the sensor.""" """Return the current state of the sensor."""
if isinstance(self._state, Enum):
return self._state.name.lower()
return self._state.lower() return self._state.lower()
class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
"""The primary filtration cycle."""
def __init__(self, coordinator, spa):
"""Initialize the entity."""
super().__init__(
coordinator, spa, "primary filtration cycle", "primary_filtration"
)
@property
def state(self) -> str:
"""Return the current state of the sensor."""
return self._state.status.name.lower()
@property
def device_state_attributes(self):
"""Return the state attributes."""
state = self._state
return {
ATTR_DURATION: state.duration,
ATTR_LAST_UPDATED: state.last_updated.isoformat(),
ATTR_MODE: state.mode.name.lower(),
ATTR_START_HOUR: state.start_hour,
}
class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
"""The secondary filtration cycle."""
def __init__(self, coordinator, spa):
"""Initialize the entity."""
super().__init__(
coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration"
)
@property
def state(self) -> str:
"""Return the current state of the sensor."""
return self._state.status.name.lower()
@property
def device_state_attributes(self):
"""Return the state attributes."""
state = self._state
return {
ATTR_LAST_UPDATED: state.last_updated.isoformat(),
ATTR_MODE: state.mode.name.lower(),
}

View File

@ -1817,7 +1817,7 @@ python-qbittorrent==0.4.2
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.12 python-smarttub==0.0.17
# homeassistant.components.sochain # homeassistant.components.sochain
python-sochain-api==0.0.2 python-sochain-api==0.0.2

View File

@ -942,7 +942,7 @@ python-nest==4.1.0
python-openzwave-mqtt[mqtt-client]==1.4.0 python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.12 python-smarttub==0.0.17
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.12 python-songpal==0.12

View File

@ -42,7 +42,9 @@ def mock_spa():
mock_spa.id = "mockspa1" mock_spa.id = "mockspa1"
mock_spa.brand = "mockbrand1" mock_spa.brand = "mockbrand1"
mock_spa.model = "mockmodel1" mock_spa.model = "mockmodel1"
mock_spa.get_status.return_value = { mock_spa.get_status.return_value = smarttub.SpaState(
mock_spa,
**{
"setTemperature": 39, "setTemperature": 39,
"water": {"temperature": 38}, "water": {"temperature": 38},
"heater": "ON", "heater": "ON",
@ -66,8 +68,8 @@ def mock_spa():
"uv": "OFF", "uv": "OFF",
"blowoutCycle": "INACTIVE", "blowoutCycle": "INACTIVE",
"cleanupCycle": "INACTIVE", "cleanupCycle": "INACTIVE",
} },
)
mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True)
mock_circulation_pump.id = "CP" mock_circulation_pump.id = "CP"
mock_circulation_pump.spa = mock_spa mock_circulation_pump.spa = mock_spa

View File

@ -42,7 +42,7 @@ async def test_thermostat_update(spa, setup_entry, hass):
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
spa.get_status.return_value["heater"] = "OFF" spa.get_status.return_value.heater = "OFF"
await trigger_update(hass) await trigger_update(hass)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -83,9 +83,9 @@ async def test_thermostat_update(spa, setup_entry, hass):
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO},
blocking=True, blocking=True,
) )
spa.set_heat_mode.assert_called_with("ECO") spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY)
spa.get_status.return_value["heatMode"] = "ECO" spa.get_status.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY
await trigger_update(hass) await trigger_update(hass)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO

View File

@ -11,7 +11,7 @@ async def test_sensors(spa, setup_entry, hass):
assert state is not None assert state is not None
assert state.state == "normal" assert state.state == "normal"
spa.get_status.return_value["state"] = "BAD" spa.get_status.return_value.state = "BAD"
await trigger_update(hass) await trigger_update(hass)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state is not None assert state is not None
@ -36,3 +36,19 @@ async def test_sensors(spa, setup_entry, hass):
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state is not None assert state is not None
assert state.state == "inactive" assert state.state == "inactive"
entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "inactive"
assert state.attributes["duration"] == 4
assert state.attributes["last_updated"] is not None
assert state.attributes["mode"] == "normal"
assert state.attributes["start_hour"] == 2
entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "inactive"
assert state.attributes["last_updated"] is not None
assert state.attributes["mode"] == "away"