From f29bcf7ff76fc164061b74957a3b76cfc5af0369 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Mon, 21 Jun 2021 03:09:41 -0500 Subject: [PATCH] Modern Forms light platform (#51857) * Add light platform to Modern Forms integration * cleanup setup * Code review cleanup --- .../components/modern_forms/__init__.py | 2 + .../components/modern_forms/const.py | 3 + .../components/modern_forms/light.py | 144 +++++++++++++++++ .../components/modern_forms/services.yaml | 25 +++ tests/components/modern_forms/test_light.py | 145 ++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 homeassistant/components/modern_forms/light.py create mode 100644 tests/components/modern_forms/test_light.py diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 4e80c85cd52..7eee8ea9ad8 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -12,6 +12,7 @@ from aiomodernforms import ( from aiomodernforms.models import Device as ModernFormsDeviceState from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST from homeassistant.core import HomeAssistant @@ -27,6 +28,7 @@ from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ + LIGHT_DOMAIN, FAN_DOMAIN, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modern_forms/const.py b/homeassistant/components/modern_forms/const.py index b151a637d75..9dbefcfc570 100644 --- a/homeassistant/components/modern_forms/const.py +++ b/homeassistant/components/modern_forms/const.py @@ -7,8 +7,11 @@ ATTR_MANUFACTURER = "manufacturer" OPT_ON = "on" OPT_SPEED = "speed" +OPT_BRIGHTNESS = "brightness" # Services +SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer" +SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer" SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer" SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer" diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py new file mode 100644 index 00000000000..4b3b675c9e0 --- /dev/null +++ b/homeassistant/components/modern_forms/light.py @@ -0,0 +1,144 @@ +"""Support for Modern Forms Fan lights.""" +from __future__ import annotations + +from typing import Any + +from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from . import ( + ModernFormsDataUpdateCoordinator, + ModernFormsDeviceEntity, + modernforms_exception_handler, +) +from .const import ( + ATTR_SLEEP_TIME, + CLEAR_TIMER, + DOMAIN, + OPT_BRIGHTNESS, + OPT_ON, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + SERVICE_SET_LIGHT_SLEEP_TIMER, +) + +BRIGHTNESS_RANGE = (1, 255) + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Modern Forms platform from config entry.""" + + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + # if no light unit installed no light entity + if not coordinator.data.info.light_type: + return + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_LIGHT_SLEEP_TIMER, + { + vol.Required(ATTR_SLEEP_TIME): vol.All( + vol.Coerce(int), vol.Range(min=1, max=1440) + ), + }, + "async_set_light_sleep_timer", + ) + + platform.async_register_entity_service( + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + {}, + "async_clear_light_sleep_timer", + ) + + async_add_entities( + [ + ModernFormsLightEntity( + entry_id=config_entry.entry_id, coordinator=coordinator + ) + ] + ) + + +class ModernFormsLightEntity(LightEntity, ModernFormsDeviceEntity): + """Defines a Modern Forms light.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms light.""" + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=f"{coordinator.data.info.device_name} Light", + icon=None, + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + + @property + def brightness(self) -> int | None: + """Return the brightness of this light between 1..255.""" + return round( + percentage_to_ranged_value( + BRIGHTNESS_RANGE, self.coordinator.data.state.light_brightness + ) + ) + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self.coordinator.data.state.light_on) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {OPT_ON: LIGHT_POWER_ON} + + if ATTR_BRIGHTNESS in kwargs: + data[OPT_BRIGHTNESS] = ranged_value_to_percentage( + BRIGHTNESS_RANGE, kwargs[ATTR_BRIGHTNESS] + ) + + await self.coordinator.modern_forms.light(**data) + + @modernforms_exception_handler + async def async_set_light_sleep_timer( + self, + sleep_time: int, + ) -> None: + """Set a Modern Forms light sleep timer.""" + await self.coordinator.modern_forms.light(sleep=sleep_time * 60) + + @modernforms_exception_handler + async def async_clear_light_sleep_timer( + self, + ) -> None: + """Clear a Modern Forms light sleep timer.""" + await self.coordinator.modern_forms.light(sleep=CLEAR_TIMER) diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml index b90fec11bf1..d4f2f0e7997 100644 --- a/homeassistant/components/modern_forms/services.yaml +++ b/homeassistant/components/modern_forms/services.yaml @@ -1,3 +1,28 @@ +set_light_sleep_timer: + name: Set light sleep timer + description: Set a sleep timer on a Modern Forms light. + target: + entity: + integration: modern_forms + domain: light + fields: + sleep_time: + name: Sleep Time + description: Number of seconds to set the timer. + required: true + example: "900" + selector: + number: + min: 1 + max: 1440 + unit_of_measurement: minutes +clear_light_sleep_timer: + name: Clear light sleep timer + description: Clear the sleep timer on a Modern Forms light. + target: + entity: + integration: modern_forms + domain: light set_fan_sleep_timer: name: Set fan sleep timer description: Set a sleep timer on a Modern Forms fan. diff --git a/tests/components/modern_forms/test_light.py b/tests/components/modern_forms/test_light.py new file mode 100644 index 00000000000..29725ab4bcd --- /dev/null +++ b/tests/components/modern_forms/test_light.py @@ -0,0 +1,145 @@ +"""Tests for the Modern Forms light platform.""" +from unittest.mock import patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN +from homeassistant.components.modern_forms.const import ( + ATTR_SLEEP_TIME, + DOMAIN, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + SERVICE_SET_LIGHT_SLEEP_TIMER, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms lights.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("light.modernformsfan_light") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 128 + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan Light" + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.modernformsfan_light") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF" + + +async def test_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=False, + ) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(on=True, brightness=100) + + +async def test_sleep_timer_services( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_LIGHT_SLEEP_TIMER, + {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_SLEEP_TIME: 1}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(sleep=60) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(sleep=0) + + +async def test_light_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the Modern Forms lights.""" + + await init_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() + + aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.modernformsfan_light") + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + + +async def test_light_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the Moder Forms lights.""" + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( + "homeassistant.components.modern_forms.ModernFormsDevice.light", + side_effect=ModernFormsConnectionError, + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.modernformsfan_light") + assert state.state == STATE_UNAVAILABLE