diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 58f400597b3..4bf20868ee0 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,4 +1,4 @@ -"""SmartTub integration.""" +"""Base classes for SmartTub entities.""" import logging import smarttub diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index ea803c9862b..07866d0b7a4 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -2,7 +2,11 @@ from enum import Enum import logging +import smarttub +import voluptuous as vol + from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers import config_validation as cv, entity_platform from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase @@ -16,6 +20,25 @@ ATTR_MODE = "mode" # the hour of the day at which to start the cycle (0-23) 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): """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) + 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): """Generic class for SmartTub status sensors.""" @@ -66,22 +103,33 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): coordinator, spa, "Primary Filtration Cycle", "primary_filtration" ) + @property + def cycle(self) -> smarttub.SpaPrimaryFiltrationCycle: + """Return the underlying smarttub.SpaPrimaryFiltrationCycle object.""" + return self._state + @property def state(self) -> str: """Return the current state of the sensor.""" - return self._state.status.name.lower() + return self.cycle.status.name.lower() @property def extra_state_attributes(self): """Return the state attributes.""" - state = self._state return { - ATTR_DURATION: state.duration, - ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), - ATTR_MODE: state.mode.name.lower(), - ATTR_START_HOUR: state.start_hour, + ATTR_DURATION: self.cycle.duration, + ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(), + ATTR_MODE: self.cycle.mode.name.lower(), + 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): """The secondary filtration cycle.""" @@ -92,16 +140,27 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration" ) + @property + def cycle(self) -> smarttub.SpaSecondaryFiltrationCycle: + """Return the underlying smarttub.SpaSecondaryFiltrationCycle object.""" + return self._state + @property def state(self) -> str: """Return the current state of the sensor.""" - return self._state.status.name.lower() + return self.cycle.status.name.lower() @property def extra_state_attributes(self): """Return the state attributes.""" - state = self._state return { - ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), - ATTR_MODE: state.mode.name.lower(), + ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(), + 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) diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml new file mode 100644 index 00000000000..30bd225113e --- /dev/null +++ b/homeassistant/components/smarttub/services.yaml @@ -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" diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 84566fcccc5..2b6991fbbe0 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -35,13 +35,65 @@ async def setup_component(hass): @pytest.fixture(name="spa") -def mock_spa(): +def mock_spa(spa_state): """Mock a smarttub.Spa.""" mock_spa = create_autospec(smarttub.Spa, instance=True) mock_spa.id = "mockspa1" mock_spa.brand = "mockbrand1" 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( mock_spa, { @@ -73,51 +125,15 @@ def mock_spa(): "pumps": [], }, ) - mock_spa.get_status_full.return_value = full_status - 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 + full_status.primary_filtration.set = create_autospec( + smarttub.SpaPrimaryFiltrationCycle, instance=True + ).set + full_status.secondary_filtration.set_mode = create_autospec( + smarttub.SpaSecondaryFiltrationCycle, instance=True + ).set_mode - 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 - - 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 + return full_status @pytest.fixture(name="account") diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index a9c2de4e6e2..83a223cee98 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( 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.""" 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 - spa.get_status_full.return_value.heater = "OFF" + spa_state.heater = "OFF" await trigger_update(hass) 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.get_status_full.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY + spa_state.heat_mode = smarttub.Spa.HeatMode.ECONOMY await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 4f179b24910..025e0b6e13e 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -1,6 +1,7 @@ """Test the SmartTub sensor platform.""" import pytest +import smarttub @pytest.mark.parametrize( @@ -23,7 +24,7 @@ async def test_sensor(spa, setup_entry, hass, entity_suffix, 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.""" 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["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.""" 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.attributes["cycle_last_updated"] is not None 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 + )