mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add services to SmartTub for changing filtration settings (#46980)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
e1b6385b4d
commit
87438dd401
@ -1,4 +1,4 @@
|
|||||||
"""SmartTub integration."""
|
"""Base classes for SmartTub entities."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import smarttub
|
import smarttub
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import smarttub
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
|
|
||||||
from .const import DOMAIN, SMARTTUB_CONTROLLER
|
from .const import DOMAIN, SMARTTUB_CONTROLLER
|
||||||
from .entity import SmartTubSensorBase
|
from .entity import SmartTubSensorBase
|
||||||
@ -16,6 +20,25 @@ ATTR_MODE = "mode"
|
|||||||
# the hour of the day at which to start the cycle (0-23)
|
# the hour of the day at which to start the cycle (0-23)
|
||||||
ATTR_START_HOUR = "start_hour"
|
ATTR_START_HOUR = "start_hour"
|
||||||
|
|
||||||
|
SET_PRIMARY_FILTRATION_SCHEMA = vol.All(
|
||||||
|
cv.has_at_least_one_key(ATTR_DURATION, ATTR_START_HOUR),
|
||||||
|
cv.make_entity_service_schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_DURATION): vol.All(int, vol.Range(min=1, max=24)),
|
||||||
|
vol.Optional(ATTR_START_HOUR): vol.All(int, vol.Range(min=0, max=23)),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SET_SECONDARY_FILTRATION_SCHEMA = {
|
||||||
|
vol.Required(ATTR_MODE): vol.In(
|
||||||
|
{
|
||||||
|
mode.name.lower()
|
||||||
|
for mode in smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
@ -45,6 +68,20 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
platform = entity_platform.current_platform.get()
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
"set_primary_filtration",
|
||||||
|
SET_PRIMARY_FILTRATION_SCHEMA,
|
||||||
|
"async_set_primary_filtration",
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
"set_secondary_filtration",
|
||||||
|
SET_SECONDARY_FILTRATION_SCHEMA,
|
||||||
|
"async_set_secondary_filtration",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SmartTubSensor(SmartTubSensorBase, SensorEntity):
|
class SmartTubSensor(SmartTubSensorBase, SensorEntity):
|
||||||
"""Generic class for SmartTub status sensors."""
|
"""Generic class for SmartTub status sensors."""
|
||||||
@ -66,22 +103,33 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
|
|||||||
coordinator, spa, "Primary Filtration Cycle", "primary_filtration"
|
coordinator, spa, "Primary Filtration Cycle", "primary_filtration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cycle(self) -> smarttub.SpaPrimaryFiltrationCycle:
|
||||||
|
"""Return the underlying smarttub.SpaPrimaryFiltrationCycle object."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
"""Return the current state of the sensor."""
|
"""Return the current state of the sensor."""
|
||||||
return self._state.status.name.lower()
|
return self.cycle.status.name.lower()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
state = self._state
|
|
||||||
return {
|
return {
|
||||||
ATTR_DURATION: state.duration,
|
ATTR_DURATION: self.cycle.duration,
|
||||||
ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(),
|
ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(),
|
||||||
ATTR_MODE: state.mode.name.lower(),
|
ATTR_MODE: self.cycle.mode.name.lower(),
|
||||||
ATTR_START_HOUR: state.start_hour,
|
ATTR_START_HOUR: self.cycle.start_hour,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_set_primary_filtration(self, **kwargs):
|
||||||
|
"""Update primary filtration settings."""
|
||||||
|
await self.cycle.set(
|
||||||
|
duration=kwargs.get(ATTR_DURATION),
|
||||||
|
start_hour=kwargs.get(ATTR_START_HOUR),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
|
class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
|
||||||
"""The secondary filtration cycle."""
|
"""The secondary filtration cycle."""
|
||||||
@ -92,16 +140,27 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
|
|||||||
coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration"
|
coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cycle(self) -> smarttub.SpaSecondaryFiltrationCycle:
|
||||||
|
"""Return the underlying smarttub.SpaSecondaryFiltrationCycle object."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
"""Return the current state of the sensor."""
|
"""Return the current state of the sensor."""
|
||||||
return self._state.status.name.lower()
|
return self.cycle.status.name.lower()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
state = self._state
|
|
||||||
return {
|
return {
|
||||||
ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(),
|
ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(),
|
||||||
ATTR_MODE: state.mode.name.lower(),
|
ATTR_MODE: self.cycle.mode.name.lower(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_set_secondary_filtration(self, **kwargs):
|
||||||
|
"""Update primary filtration settings."""
|
||||||
|
mode = smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode[
|
||||||
|
kwargs[ATTR_MODE].upper()
|
||||||
|
]
|
||||||
|
await self.cycle.set_mode(mode)
|
||||||
|
47
homeassistant/components/smarttub/services.yaml
Normal file
47
homeassistant/components/smarttub/services.yaml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
set_primary_filtration:
|
||||||
|
name: Update primary filtration settings
|
||||||
|
description: Updates the primary filtration settings
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: smarttub
|
||||||
|
domain: sensor
|
||||||
|
fields:
|
||||||
|
duration:
|
||||||
|
name: Duration
|
||||||
|
description: The desired duration of the primary filtration cycle
|
||||||
|
default: 8
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 24
|
||||||
|
unit_of_measurement: "hours"
|
||||||
|
mode: slider
|
||||||
|
example: 8
|
||||||
|
start_hour:
|
||||||
|
description: The hour of the day at which to begin the primary filtration cycle
|
||||||
|
default: 0
|
||||||
|
example: 2
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 23
|
||||||
|
unit_of_measurement: "hour"
|
||||||
|
|
||||||
|
set_secondary_filtration:
|
||||||
|
name: Update secondary filtration settings
|
||||||
|
description: Updates the secondary filtration settings
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: smarttub
|
||||||
|
domain: sensor
|
||||||
|
fields:
|
||||||
|
mode:
|
||||||
|
description: The secondary filtration mode.
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "frequent"
|
||||||
|
- "infrequent"
|
||||||
|
- "away"
|
||||||
|
required: true
|
||||||
|
example: "frequent"
|
@ -35,13 +35,65 @@ async def setup_component(hass):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="spa")
|
@pytest.fixture(name="spa")
|
||||||
def mock_spa():
|
def mock_spa(spa_state):
|
||||||
"""Mock a smarttub.Spa."""
|
"""Mock a smarttub.Spa."""
|
||||||
|
|
||||||
mock_spa = create_autospec(smarttub.Spa, instance=True)
|
mock_spa = create_autospec(smarttub.Spa, instance=True)
|
||||||
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_full.return_value = spa_state
|
||||||
|
|
||||||
|
mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True)
|
||||||
|
mock_circulation_pump.id = "CP"
|
||||||
|
mock_circulation_pump.spa = mock_spa
|
||||||
|
mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF
|
||||||
|
mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION
|
||||||
|
|
||||||
|
mock_jet_off = create_autospec(smarttub.SpaPump, instance=True)
|
||||||
|
mock_jet_off.id = "P1"
|
||||||
|
mock_jet_off.spa = mock_spa
|
||||||
|
mock_jet_off.state = smarttub.SpaPump.PumpState.OFF
|
||||||
|
mock_jet_off.type = smarttub.SpaPump.PumpType.JET
|
||||||
|
|
||||||
|
mock_jet_on = create_autospec(smarttub.SpaPump, instance=True)
|
||||||
|
mock_jet_on.id = "P2"
|
||||||
|
mock_jet_on.spa = mock_spa
|
||||||
|
mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH
|
||||||
|
mock_jet_on.type = smarttub.SpaPump.PumpType.JET
|
||||||
|
|
||||||
|
spa_state.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on]
|
||||||
|
|
||||||
|
mock_light_off = create_autospec(smarttub.SpaLight, instance=True)
|
||||||
|
mock_light_off.spa = mock_spa
|
||||||
|
mock_light_off.zone = 1
|
||||||
|
mock_light_off.intensity = 0
|
||||||
|
mock_light_off.mode = smarttub.SpaLight.LightMode.OFF
|
||||||
|
|
||||||
|
mock_light_on = create_autospec(smarttub.SpaLight, instance=True)
|
||||||
|
mock_light_on.spa = mock_spa
|
||||||
|
mock_light_on.zone = 2
|
||||||
|
mock_light_on.intensity = 50
|
||||||
|
mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE
|
||||||
|
|
||||||
|
spa_state.lights = [mock_light_off, mock_light_on]
|
||||||
|
|
||||||
|
mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True)
|
||||||
|
mock_filter_reminder.id = "FILTER01"
|
||||||
|
mock_filter_reminder.name = "MyFilter"
|
||||||
|
mock_filter_reminder.remaining_days = 2
|
||||||
|
mock_filter_reminder.snoozed = False
|
||||||
|
|
||||||
|
mock_spa.get_reminders.return_value = [mock_filter_reminder]
|
||||||
|
|
||||||
|
return mock_spa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="spa_state")
|
||||||
|
def mock_spa_state():
|
||||||
|
"""Create a smarttub.SpaStateFull with mocks."""
|
||||||
|
|
||||||
full_status = smarttub.SpaStateFull(
|
full_status = smarttub.SpaStateFull(
|
||||||
mock_spa,
|
mock_spa,
|
||||||
{
|
{
|
||||||
@ -73,51 +125,15 @@ def mock_spa():
|
|||||||
"pumps": [],
|
"pumps": [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
mock_spa.get_status_full.return_value = full_status
|
|
||||||
|
|
||||||
mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True)
|
full_status.primary_filtration.set = create_autospec(
|
||||||
mock_circulation_pump.id = "CP"
|
smarttub.SpaPrimaryFiltrationCycle, instance=True
|
||||||
mock_circulation_pump.spa = mock_spa
|
).set
|
||||||
mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF
|
full_status.secondary_filtration.set_mode = create_autospec(
|
||||||
mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION
|
smarttub.SpaSecondaryFiltrationCycle, instance=True
|
||||||
|
).set_mode
|
||||||
|
|
||||||
mock_jet_off = create_autospec(smarttub.SpaPump, instance=True)
|
return full_status
|
||||||
mock_jet_off.id = "P1"
|
|
||||||
mock_jet_off.spa = mock_spa
|
|
||||||
mock_jet_off.state = smarttub.SpaPump.PumpState.OFF
|
|
||||||
mock_jet_off.type = smarttub.SpaPump.PumpType.JET
|
|
||||||
|
|
||||||
mock_jet_on = create_autospec(smarttub.SpaPump, instance=True)
|
|
||||||
mock_jet_on.id = "P2"
|
|
||||||
mock_jet_on.spa = mock_spa
|
|
||||||
mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH
|
|
||||||
mock_jet_on.type = smarttub.SpaPump.PumpType.JET
|
|
||||||
|
|
||||||
full_status.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on]
|
|
||||||
|
|
||||||
mock_light_off = create_autospec(smarttub.SpaLight, instance=True)
|
|
||||||
mock_light_off.spa = mock_spa
|
|
||||||
mock_light_off.zone = 1
|
|
||||||
mock_light_off.intensity = 0
|
|
||||||
mock_light_off.mode = smarttub.SpaLight.LightMode.OFF
|
|
||||||
|
|
||||||
mock_light_on = create_autospec(smarttub.SpaLight, instance=True)
|
|
||||||
mock_light_on.spa = mock_spa
|
|
||||||
mock_light_on.zone = 2
|
|
||||||
mock_light_on.intensity = 50
|
|
||||||
mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE
|
|
||||||
|
|
||||||
full_status.lights = [mock_light_off, mock_light_on]
|
|
||||||
|
|
||||||
mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True)
|
|
||||||
mock_filter_reminder.id = "FILTER01"
|
|
||||||
mock_filter_reminder.name = "MyFilter"
|
|
||||||
mock_filter_reminder.remaining_days = 2
|
|
||||||
mock_filter_reminder.snoozed = False
|
|
||||||
|
|
||||||
mock_spa.get_reminders.return_value = [mock_filter_reminder]
|
|
||||||
|
|
||||||
return mock_spa
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="account")
|
@pytest.fixture(name="account")
|
||||||
|
@ -33,7 +33,7 @@ from homeassistant.const import (
|
|||||||
from . import trigger_update
|
from . import trigger_update
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_update(spa, setup_entry, hass):
|
async def test_thermostat_update(spa, spa_state, setup_entry, hass):
|
||||||
"""Test the thermostat entity."""
|
"""Test the thermostat entity."""
|
||||||
|
|
||||||
entity_id = f"climate.{spa.brand}_{spa.model}_thermostat"
|
entity_id = f"climate.{spa.brand}_{spa.model}_thermostat"
|
||||||
@ -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_full.return_value.heater = "OFF"
|
spa_state.heater = "OFF"
|
||||||
await trigger_update(hass)
|
await trigger_update(hass)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ async def test_thermostat_update(spa, setup_entry, hass):
|
|||||||
)
|
)
|
||||||
spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY)
|
spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY)
|
||||||
|
|
||||||
spa.get_status_full.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY
|
spa_state.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
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test the SmartTub sensor platform."""
|
"""Test the SmartTub sensor platform."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import smarttub
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -23,7 +24,7 @@ async def test_sensor(spa, setup_entry, hass, entity_suffix, expected_state):
|
|||||||
assert state.state == expected_state
|
assert state.state == expected_state
|
||||||
|
|
||||||
|
|
||||||
async def test_primary_filtration(spa, setup_entry, hass):
|
async def test_primary_filtration(spa, spa_state, setup_entry, hass):
|
||||||
"""Test the primary filtration cycle sensor."""
|
"""Test the primary filtration cycle sensor."""
|
||||||
|
|
||||||
entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle"
|
entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle"
|
||||||
@ -35,8 +36,16 @@ async def test_primary_filtration(spa, setup_entry, hass):
|
|||||||
assert state.attributes["mode"] == "normal"
|
assert state.attributes["mode"] == "normal"
|
||||||
assert state.attributes["start_hour"] == 2
|
assert state.attributes["start_hour"] == 2
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"smarttub",
|
||||||
|
"set_primary_filtration",
|
||||||
|
{"entity_id": entity_id, "duration": 8, "start_hour": 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
spa_state.primary_filtration.set.assert_called_with(duration=8, start_hour=1)
|
||||||
|
|
||||||
async def test_secondary_filtration(spa, setup_entry, hass):
|
|
||||||
|
async def test_secondary_filtration(spa, spa_state, setup_entry, hass):
|
||||||
"""Test the secondary filtration cycle sensor."""
|
"""Test the secondary filtration cycle sensor."""
|
||||||
|
|
||||||
entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle"
|
entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle"
|
||||||
@ -45,3 +54,16 @@ async def test_secondary_filtration(spa, setup_entry, hass):
|
|||||||
assert state.state == "inactive"
|
assert state.state == "inactive"
|
||||||
assert state.attributes["cycle_last_updated"] is not None
|
assert state.attributes["cycle_last_updated"] is not None
|
||||||
assert state.attributes["mode"] == "away"
|
assert state.attributes["mode"] == "away"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"smarttub",
|
||||||
|
"set_secondary_filtration",
|
||||||
|
{
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"mode": "frequent",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
spa_state.secondary_filtration.set_mode.assert_called_with(
|
||||||
|
mode=smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode.FREQUENT
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user