From 5cd022a683bb3b081a17b14d483fa1cd355df2e7 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 22:09:21 -0800 Subject: [PATCH] Add light platform to SmartTub (#46886) Co-authored-by: J. Nick Koston --- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smarttub/const.py | 7 + .../components/smarttub/controller.py | 4 +- homeassistant/components/smarttub/light.py | 141 ++++++++++++++++++ tests/components/smarttub/conftest.py | 14 ++ tests/components/smarttub/test_light.py | 38 +++++ 6 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/smarttub/light.py create mode 100644 tests/components/smarttub/test_light.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index e8fc9989d38..8467c208076 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -7,7 +7,7 @@ from .controller import SmartTubController _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor", "switch"] +PLATFORMS = ["climate", "light", "sensor", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 8292c5ef826..0b97926cc43 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -13,3 +13,10 @@ API_TIMEOUT = 5 DEFAULT_MIN_TEMP = 18.5 DEFAULT_MAX_TEMP = 40 + +# the device doesn't remember any state for the light, so we have to choose a +# mode (smarttub.SpaLight.LightMode) when turning it on. There is no white +# mode. +DEFAULT_LIGHT_EFFECT = "purple" +# default to 50% brightness +DEFAULT_LIGHT_BRIGHTNESS = 128 diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index feb13066b44..bf8de2f4e2e 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -86,13 +86,15 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - status, pumps = await asyncio.gather( + status, pumps, lights = await asyncio.gather( spa.get_status(), spa.get_pumps(), + spa.get_lights(), ) return { "status": status, "pumps": {pump.id: pump for pump in pumps}, + "lights": {light.zone: light for light in lights}, } async def async_register_devices(self, entry): diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py new file mode 100644 index 00000000000..a4ada7c3024 --- /dev/null +++ b/homeassistant/components/smarttub/light.py @@ -0,0 +1,141 @@ +"""Platform for light integration.""" +import logging + +from smarttub import SpaLight + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + EFFECT_COLORLOOP, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + LightEntity, +) + +from .const import ( + DEFAULT_LIGHT_BRIGHTNESS, + DEFAULT_LIGHT_EFFECT, + DOMAIN, + SMARTTUB_CONTROLLER, +) +from .entity import SmartTubEntity +from .helpers import get_spa_name + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up entities for any lights in the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [ + SmartTubLight(controller.coordinator, light) + for spa in controller.spas + for light in await spa.get_lights() + ] + + async_add_entities(entities) + + +class SmartTubLight(SmartTubEntity, LightEntity): + """A light on a spa.""" + + def __init__(self, coordinator, light): + """Initialize the entity.""" + super().__init__(coordinator, light.spa, "light") + self.light_zone = light.zone + + @property + def light(self) -> SpaLight: + """Return the underlying SpaLight object for this entity.""" + return self.coordinator.data[self.spa.id]["lights"][self.light_zone] + + @property + def unique_id(self) -> str: + """Return a unique ID for this light entity.""" + return f"{super().unique_id}-{self.light_zone}" + + @property + def name(self) -> str: + """Return a name for this light entity.""" + spa_name = get_spa_name(self.spa) + return f"{spa_name} Light {self.light_zone}" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + + # SmartTub intensity is 0..100 + return self._smarttub_to_hass_brightness(self.light.intensity) + + @staticmethod + def _smarttub_to_hass_brightness(intensity): + if intensity in (0, 1): + return 0 + return round(intensity * 255 / 100) + + @staticmethod + def _hass_to_smarttub_brightness(brightness): + return round(brightness * 100 / 255) + + @property + def is_on(self): + """Return true if the light is on.""" + return self.light.mode != SpaLight.LightMode.OFF + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_EFFECT + + @property + def effect(self): + """Return the current effect.""" + mode = self.light.mode.name.lower() + if mode in self.effect_list: + return mode + return None + + @property + def effect_list(self): + """Return the list of supported effects.""" + effects = [ + effect + for effect in map(self._light_mode_to_effect, SpaLight.LightMode) + if effect is not None + ] + + return effects + + @staticmethod + def _light_mode_to_effect(light_mode: SpaLight.LightMode): + if light_mode == SpaLight.LightMode.OFF: + return None + if light_mode == SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL: + return EFFECT_COLORLOOP + + return light_mode.name.lower() + + @staticmethod + def _effect_to_light_mode(effect): + if effect == EFFECT_COLORLOOP: + return SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL + + return SpaLight.LightMode[effect.upper()] + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + + mode = self._effect_to_light_mode(kwargs.get(ATTR_EFFECT, DEFAULT_LIGHT_EFFECT)) + intensity = self._hass_to_smarttub_brightness( + kwargs.get(ATTR_BRIGHTNESS, DEFAULT_LIGHT_BRIGHTNESS) + ) + + await self.light.set_mode(mode, intensity) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self.light.set_mode(self.light.LightMode.OFF, 0) + await self.coordinator.async_request_refresh() diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index ad962ba0474..79e5d06d3b3 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -90,6 +90,20 @@ def mock_spa(): mock_spa.get_pumps.return_value = [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 + + mock_spa.get_lights.return_value = [mock_light_off, mock_light_on] + return mock_spa diff --git a/tests/components/smarttub/test_light.py b/tests/components/smarttub/test_light.py new file mode 100644 index 00000000000..5e9d9459eab --- /dev/null +++ b/tests/components/smarttub/test_light.py @@ -0,0 +1,38 @@ +"""Test the SmartTub light platform.""" + +from smarttub import SpaLight + + +async def test_light(spa, setup_entry, hass): + """Test light entity.""" + + for light in spa.get_lights.return_value: + entity_id = f"light.{spa.brand}_{spa.model}_light_{light.zone}" + state = hass.states.get(entity_id) + assert state is not None + if light.mode == SpaLight.LightMode.OFF: + assert state.state == "off" + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id}, + blocking=True, + ) + light.set_mode.assert_called() + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": 255}, + blocking=True, + ) + light.set_mode.assert_called_with(SpaLight.LightMode.PURPLE, 100) + + else: + assert state.state == "on" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_id}, + blocking=True, + )