From c530b4bbaf30993a8d3e537de9c63f421ffa7b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Apr 2020 19:26:34 -0500 Subject: [PATCH 1/2] Restore isy light brightness after off (#34320) --- custom_components/isy994/light.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/custom_components/isy994/light.py b/custom_components/isy994/light.py index 63c2750..6e0eea9 100644 --- a/custom_components/isy994/light.py +++ b/custom_components/isy994/light.py @@ -33,6 +33,11 @@ async def async_setup_entry( class ISYLightDevice(ISYDevice, Light): """Representation of an ISY994 light device.""" + def __init__(self, node) -> None: + """Initialize the ISY994 light device.""" + super().__init__(node) + self._last_brightness = self.brightness + @property def is_on(self) -> bool: """Get whether the ISY994 light is on.""" @@ -47,12 +52,21 @@ def brightness(self) -> float: def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" + self._last_brightness = self.brightness if not self._node.turn_off(): _LOGGER.debug("Unable to turn off light") + def on_update(self, event: object) -> None: + """Save brightness in the update event from the ISY994 Node.""" + if self.value not in (0, ISY_VALUE_UNKNOWN): + self._last_brightness = self.value + super().on_update(event) + # pylint: disable=arguments-differ def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" + if brightness is None and self._last_brightness is not None: + brightness = self._last_brightness if not self._node.turn_on(val=brightness): _LOGGER.debug("Unable to turn on light") From cef6707d214391276e52975e28df7d06a72c083d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Apr 2020 15:15:37 +0000 Subject: [PATCH 2/2] Add options flow for isy994 Move sensor_string and sensor_string into the options flow as the config flow is only supposed to collect the minimal amount of information to get the integration up and running Add a Restore Light Brightness option to the config flow Add a last_brightness attribute to lights --- custom_components/isy994/__init__.py | 45 ++++++++++++++++-- custom_components/isy994/config_flow.py | 46 +++++++++++++++++-- custom_components/isy994/const.py | 7 +++ custom_components/isy994/light.py | 43 ++++++++++++++--- custom_components/isy994/strings.json | 19 ++++++-- custom_components/isy994/translations/en.json | 19 ++++++-- 6 files changed, 159 insertions(+), 20 deletions(-) diff --git a/custom_components/isy994/__init__.py b/custom_components/isy994/__init__.py index 3fb3527..984de61 100644 --- a/custom_components/isy994/__init__.py +++ b/custom_components/isy994/__init__.py @@ -71,6 +71,7 @@ SUPPORTED_DOMAINS, SUPPORTED_PROGRAM_DOMAINS, SUPPORTED_VARIABLE_DOMAINS, + UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -459,8 +460,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Set up the ISY 994 platform.""" + # As there currently is no way to import options from yaml + # when setting up a config entry, we fallback to adding + # the options to the config entry and pull them out here if + # they are missing from the options + _async_import_options_from_data_if_missing(hass, entry) + hass.data[DOMAIN][entry.entry_id] = {} hass_isy_data = hass.data[DOMAIN][entry.entry_id] + hass_isy_data[ISY994_NODES] = {} for domain in SUPPORTED_DOMAINS: hass_isy_data[ISY994_NODES][domain] = [] @@ -474,6 +482,7 @@ async def async_setup_entry( hass_isy_data[ISY994_VARIABLES][domain] = [] isy_config = entry.data + isy_options = entry.options # Required user = isy_config[CONF_USERNAME] @@ -482,8 +491,8 @@ async def async_setup_entry( # Optional tls_version = isy_config.get(CONF_TLS_VER) - ignore_identifier = isy_config.get(CONF_IGNORE_STRING) - sensor_identifier = isy_config.get(CONF_SENSOR_STRING) + ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING) + sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING) isy_variables = isy_config.get(CONF_ISY_VARIABLES, {}) if host.scheme == "http": @@ -547,9 +556,35 @@ def _start_auto_update() -> None: await hass.async_add_executor_job(_start_auto_update) + undo_listener = entry.add_update_listener(_async_update_listener) + + hass_isy_data[UNDO_UPDATE_LISTENER] = undo_listener + return True +async def _async_update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry +): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: config_entries.ConfigEntry +): + options = dict(entry.options) + modified = False + for importable_option in [CONF_IGNORE_STRING, CONF_SENSOR_STRING]: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, options=options) + + async def _async_get_or_create_isy_device_in_registry( hass: HomeAssistant, entry: config_entries.ConfigEntry, isy ) -> None: @@ -579,7 +614,9 @@ async def async_unload_entry( ) ) - isy = hass.data[DOMAIN][entry.entry_id][ISY994_ISY] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + + isy = hass_isy_data[ISY994_ISY] def _stop_auto_update() -> None: """Start isy auto update.""" @@ -588,6 +625,8 @@ def _stop_auto_update() -> None: await hass.async_add_executor_job(_stop_auto_update) + hass_isy_data[UNDO_UPDATE_LISTENER]() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/custom_components/isy994/config_flow.py b/custom_components/isy994/config_flow.py index 5096c28..9f0e356 100644 --- a/custom_components/isy994/config_flow.py +++ b/custom_components/isy994/config_flow.py @@ -9,13 +9,17 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from .const import ( CONF_IGNORE_STRING, + CONF_RESTORE_LIGHT_STATE, CONF_SENSOR_STRING, CONF_TLS_VER, DEFAULT_IGNORE_STRING, + DEFAULT_RESTORE_LIGHT_STATE, DEFAULT_SENSOR_STRING, + DEFAULT_TLS_VERSION, ) from .const import DOMAIN # pylint:disable=unused-import @@ -27,9 +31,7 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_TLS_VER): vol.Coerce(float), - vol.Optional(CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING): str, - vol.Optional(CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING): str + vol.Optional(CONF_TLS_VER, default=DEFAULT_TLS_VERSION): vol.In([1.1, 1.2]), # Variables require yaml }, extra=vol.ALLOW_EXTRA, @@ -120,6 +122,44 @@ async def async_step_import(self, user_input): """Handle import.""" return await self.async_step_user(user_input) + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for isy994.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self.config_entry.options + restore_light_state = options.get( + CONF_RESTORE_LIGHT_STATE, DEFAULT_RESTORE_LIGHT_STATE + ) + ignore_string = options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING) + sensor_string = options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING) + + options_schema = vol.Schema( + { + vol.Required( + CONF_RESTORE_LIGHT_STATE, default=restore_light_state + ): bool, + vol.Optional(CONF_IGNORE_STRING, default=ignore_string): str, + vol.Optional(CONF_SENSOR_STRING, default=sensor_string): str, + } + ) + + return self.async_show_form(step_id="init", data_schema=options_schema,) + class InvalidHost(exceptions.HomeAssistantError): """Error to indicate the host value is invalid.""" diff --git a/custom_components/isy994/const.py b/custom_components/isy994/const.py index 20af5ac..4574912 100644 --- a/custom_components/isy994/const.py +++ b/custom_components/isy994/const.py @@ -68,13 +68,18 @@ MANUFACTURER = "Universal Devices, Inc" +ATTR_LAST_BRIGHTNESS = "last_brightness" + CONF_IGNORE_STRING = "ignore_string" CONF_SENSOR_STRING = "sensor_string" CONF_ISY_VARIABLES = "isy_variables" CONF_TLS_VER = "tls" +CONF_RESTORE_LIGHT_STATE = "restore_light_state" DEFAULT_IGNORE_STRING = "{IGNORE ME}" DEFAULT_SENSOR_STRING = "sensor" +DEFAULT_RESTORE_LIGHT_STATE = False +DEFAULT_TLS_VERSION = 1.1 DEFAULT_ON_VALUE = 1 DEFAULT_OFF_VALUE = 0 @@ -216,6 +221,8 @@ ISY994_PROGRAMS = "isy994_programs" ISY994_VARIABLES = "isy994_variables" +UNDO_UPDATE_LISTENER = "undo_update_listener" + ISY_HVAC_MODES = [ HVAC_MODE_OFF, HVAC_MODE_HEAT, diff --git a/custom_components/isy994/light.py b/custom_components/isy994/light.py index 6e0eea9..03e9e71 100644 --- a/custom_components/isy994/light.py +++ b/custom_components/isy994/light.py @@ -1,16 +1,22 @@ """Support for ISY994 lights.""" import logging -from typing import Callable +from typing import Callable, Dict from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType from . import ISYDevice, migrate_old_unique_ids -from .const import DOMAIN as ISY994_DOMAIN, ISY994_NODES +from .const import ( + ATTR_LAST_BRIGHTNESS, + CONF_RESTORE_LIGHT_STATE, + DOMAIN as ISY994_DOMAIN, + ISY994_NODES, +) _LOGGER = logging.getLogger(__name__) @@ -22,21 +28,25 @@ async def async_setup_entry( ) -> bool: """Set up the ISY994 light platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + isy_options = entry.options + restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) + devices = [] for node in hass_isy_data[ISY994_NODES][DOMAIN]: - devices.append(ISYLightDevice(node)) + devices.append(ISYLightDevice(node, restore_light_state)) await migrate_old_unique_ids(hass, DOMAIN, devices) async_add_entities(devices) -class ISYLightDevice(ISYDevice, Light): +class ISYLightDevice(ISYDevice, Light, RestoreEntity): """Representation of an ISY994 light device.""" - def __init__(self, node) -> None: + def __init__(self, node, restore_light_state) -> None: """Initialize the ISY994 light device.""" super().__init__(node) self._last_brightness = self.brightness + self._restore_light_state = restore_light_state @property def is_on(self) -> bool: @@ -50,6 +60,13 @@ def brightness(self) -> float: """Get the brightness of the ISY994 light.""" return STATE_UNKNOWN if self.value == ISY_VALUE_UNKNOWN else int(self.value) + @property + def device_state_attributes(self) -> Dict: + """Return the light attributes.""" + attribs = super().device_state_attributes + attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness + return attribs + def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" self._last_brightness = self.brightness @@ -65,7 +82,7 @@ def on_update(self, event: object) -> None: # pylint: disable=arguments-differ def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" - if brightness is None and self._last_brightness is not None: + if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness if not self._node.turn_on(val=brightness): _LOGGER.debug("Unable to turn on light") @@ -74,3 +91,17 @@ def turn_on(self, brightness=None, **kwargs) -> None: def supported_features(self): """Flag supported features.""" return SUPPORT_BRIGHTNESS + + async def async_added_to_hass(self) -> None: + """Restore last_brightness on restart.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state: + return + + if ( + ATTR_LAST_BRIGHTNESS in last_state.attributes + and last_state.attributes[ATTR_LAST_BRIGHTNESS] + ): + self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] diff --git a/custom_components/isy994/strings.json b/custom_components/isy994/strings.json index cee03f2..45b11e1 100644 --- a/custom_components/isy994/strings.json +++ b/custom_components/isy994/strings.json @@ -3,12 +3,10 @@ "step" : { "user" : { "data" : { - "sensor_string" : "Sensor String: If this string is found in the device name or folder, Home Assistant will assume it is as a sensor or binary sensor.", "username" : "Username", "host" : "URL", - "ignore_string" : "Ignore String: Any devices that contain this string in their name will be ignored.", "password" : "Password", - "tls" : "The TLS version of the ISY controller. This value can be either 1.1 or 1.2" + "tls" : "The TLS version of the ISY controller." }, "description" : "The host entry must be in full URL format, e.g., http://192.168.10.100:80", "title" : "Connect to your ISY994" @@ -24,5 +22,18 @@ "abort" : { "already_configured" : "Device is already configured" } - } + }, + "options": { + "step": { + "init": { + "title" : "ISY994 Options", + "description": "Any device or folder that contains 'Sensor String' in the name will be treated as a sensor or binary sensor. Any device with 'Ignore String' in the name will be ignored. If 'Restore Light Brightness' is enabled, the previous brightness will be restored when turning on a light instead of the On-Level.", + "data": { + "sensor_string" : "Sensor String", + "ignore_string" : "Ignore String", + "restore_light_state" : "Restore Light Brightness" + } + } + } + } } diff --git a/custom_components/isy994/translations/en.json b/custom_components/isy994/translations/en.json index cee03f2..45b11e1 100644 --- a/custom_components/isy994/translations/en.json +++ b/custom_components/isy994/translations/en.json @@ -3,12 +3,10 @@ "step" : { "user" : { "data" : { - "sensor_string" : "Sensor String: If this string is found in the device name or folder, Home Assistant will assume it is as a sensor or binary sensor.", "username" : "Username", "host" : "URL", - "ignore_string" : "Ignore String: Any devices that contain this string in their name will be ignored.", "password" : "Password", - "tls" : "The TLS version of the ISY controller. This value can be either 1.1 or 1.2" + "tls" : "The TLS version of the ISY controller." }, "description" : "The host entry must be in full URL format, e.g., http://192.168.10.100:80", "title" : "Connect to your ISY994" @@ -24,5 +22,18 @@ "abort" : { "already_configured" : "Device is already configured" } - } + }, + "options": { + "step": { + "init": { + "title" : "ISY994 Options", + "description": "Any device or folder that contains 'Sensor String' in the name will be treated as a sensor or binary sensor. Any device with 'Ignore String' in the name will be ignored. If 'Restore Light Brightness' is enabled, the previous brightness will be restored when turning on a light instead of the On-Level.", + "data": { + "sensor_string" : "Sensor String", + "ignore_string" : "Ignore String", + "restore_light_state" : "Restore Light Brightness" + } + } + } + } }