From 72a3860361c4c678fa272a4b0186a39f725b942f Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Sat, 24 Jul 2021 03:43:10 -0700 Subject: [PATCH] Add transition to LiteJet (#47657) --- .../components/litejet/config_flow.py | 39 ++++++++++++- homeassistant/components/litejet/const.py | 2 + homeassistant/components/litejet/light.py | 41 ++++++++++---- homeassistant/components/litejet/strings.json | 10 ++++ .../components/litejet/translations/en.json | 10 ++++ tests/components/litejet/test_config_flow.py | 23 +++++++- tests/components/litejet/test_light.py | 56 +++++++++++++++++-- 7 files changed, 163 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index 2e63c150e41..4f8128bd6dc 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -10,13 +10,44 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import CONF_DEFAULT_TRANSITION, DOMAIN _LOGGER = logging.getLogger(__name__) +class LiteJetOptionsFlow(config_entries.OptionsFlow): + """Handle LiteJet options.""" + + def __init__(self, config_entry): + """Initialize LiteJet options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Manage LiteJet options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_DEFAULT_TRANSITION, + default=self.config_entry.options.get( + CONF_DEFAULT_TRANSITION, 0 + ), + ): cv.positive_int, + } + ), + ) + + class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """LiteJet config flow.""" @@ -54,3 +85,9 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import litejet config from configuration.yaml.""" return self.async_create_entry(title=import_data[CONF_PORT], data=import_data) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return LiteJetOptionsFlow(config_entry) diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py index 8e27aa3a0a7..82521092106 100644 --- a/homeassistant/components/litejet/const.py +++ b/homeassistant/components/litejet/const.py @@ -6,3 +6,5 @@ CONF_EXCLUDE_NAMES = "exclude_names" CONF_INCLUDE_SWITCHES = "include_switches" PLATFORMS = ["light", "switch", "scene"] + +CONF_DEFAULT_TRANSITION = "default_transition" diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 5248afb4dbd..172e46c441a 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, LightEntity, ) -from .const import DOMAIN +from .const import CONF_DEFAULT_TRANSITION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,7 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for i in system.loads(): name = system.get_load_name(i) - entities.append(LiteJetLight(config_entry.entry_id, system, i, name)) + entities.append(LiteJetLight(config_entry, system, i, name)) return entities async_add_entities(await hass.async_add_executor_job(get_entities, system), True) @@ -32,9 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" - def __init__(self, entry_id, lj, i, name): + def __init__(self, config_entry, lj, i, name): """Initialize a LiteJet light.""" - self._entry_id = entry_id + self._config_entry = config_entry self._lj = lj self._index = i self._brightness = 0 @@ -57,7 +59,7 @@ class LiteJetLight(LightEntity): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property def name(self): @@ -67,7 +69,7 @@ class LiteJetLight(LightEntity): @property def unique_id(self): """Return a unique identifier for this light.""" - return f"{self._entry_id}_{self._index}" + return f"{self._config_entry.entry_id}_{self._index}" @property def brightness(self): @@ -91,16 +93,33 @@ class LiteJetLight(LightEntity): def turn_on(self, **kwargs): """Turn on the light.""" - if ATTR_BRIGHTNESS in kwargs: - brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 99) - self._lj.activate_load_at(self._index, brightness, 0) - else: + + # If neither attribute is specified then the simple activate load + # LiteJet API will use the per-light default brightness and + # transition values programmed in the LiteJet system. + if ATTR_BRIGHTNESS not in kwargs and ATTR_TRANSITION not in kwargs: self._lj.activate_load(self._index) + return + + # If either attribute is specified then Home Assistant must + # control both values. + default_transition = self._config_entry.options.get(CONF_DEFAULT_TRANSITION, 0) + transition = kwargs.get(ATTR_TRANSITION, default_transition) + brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255) / 255 * 99) + + self._lj.activate_load_at(self._index, brightness, int(transition)) def turn_off(self, **kwargs): """Turn off the light.""" + if ATTR_TRANSITION in kwargs: + self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + return + + # If transition attribute is not specified then the simple + # deactivate load LiteJet API will use the per-light default + # transition value programmed in the LiteJet system. self._lj.deactivate_load(self._index) def update(self): """Retrieve the light's brightness from the LiteJet system.""" - self._brightness = self._lj.get_load_level(self._index) / 99 * 255 + self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json index 79c4ed5f329..426dcd374af 100644 --- a/homeassistant/components/litejet/strings.json +++ b/homeassistant/components/litejet/strings.json @@ -15,5 +15,15 @@ "error": { "open_failed": "Cannot open the specified serial port." } + }, + "options": { + "step": { + "init": { + "title": "Configure LiteJet", + "data": { + "default_transition": "Default Transition (seconds)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/en.json b/homeassistant/components/litejet/translations/en.json index e09b20dc9f2..146aad276c4 100644 --- a/homeassistant/components/litejet/translations/en.json +++ b/homeassistant/components/litejet/translations/en.json @@ -15,5 +15,15 @@ "title": "Connect To LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Default Transition (seconds)" + }, + "title": "Configure LiteJet" + } + } } } \ No newline at end of file diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index 1d72324f484..cfae178f792 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries -from homeassistant.components.litejet.const import DOMAIN +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION, DOMAIN from homeassistant.const import CONF_PORT from tests.common import MockConfigEntry @@ -76,3 +76,22 @@ async def test_import_step(hass): assert result["type"] == "create_entry" assert result["title"] == test_data[CONF_PORT] assert result["data"] == test_data + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_PORT: "/dev/test"}) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_DEFAULT_TRANSITION: 12}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index c455d3a960e..1961843c8b0 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -2,7 +2,8 @@ import logging from homeassistant.components import light -from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_TRANSITION +from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from . import async_init_integration @@ -33,6 +34,55 @@ async def test_on_brightness(hass, mock_litejet): mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0) +async def test_default_transition(hass, mock_litejet): + """Test turning the light on with the default transition option.""" + entry = await async_init_integration(hass) + + hass.config_entries.async_update_entry(entry, options={CONF_DEFAULT_TRANSITION: 12}) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" + + assert not light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 102}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 12) + + +async def test_transition(hass, mock_litejet): + """Test turning the light on with transition.""" + await async_init_integration(hass) + + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" + + assert not light.is_on(hass, ENTITY_LIGHT) + + # On + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: 5}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 99, 5) + + # Off + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: 5}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 0, 5) + + async def test_on_off(hass, mock_litejet): """Test turning the light on and off.""" await async_init_integration(hass) @@ -91,9 +141,7 @@ async def test_activated_event(hass, mock_litejet): assert light.is_on(hass, ENTITY_OTHER_LIGHT) assert hass.states.get(ENTITY_LIGHT).state == "on" assert hass.states.get(ENTITY_OTHER_LIGHT).state == "on" - assert ( - int(hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS)) == 103 - ) + assert hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS) == 103 async def test_deactivated_event(hass, mock_litejet):