From 8de7966104911bca6f855a1755a6d71a07afb9de Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 9 Oct 2021 19:10:43 +0300 Subject: [PATCH] Add Shelly config entry reload on device config change (#57356) --- homeassistant/components/shelly/__init__.py | 74 ++++++++++++++++++++- homeassistant/components/shelly/const.py | 11 +++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b0df4d4cb7f..87896f4380b 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType from .const import ( @@ -39,6 +40,8 @@ from .const import ( DEFAULT_COAP_PORT, DEVICE, DOMAIN, + DUAL_MODE_LIGHT_MODELS, + ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, POLLING_TIMEOUT_SEC, @@ -252,6 +255,18 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device + self._debounced_reload = Debouncer( + hass, + _LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + self._last_cfg_changed: int | None = None + self._last_mode: str | None = None + self._last_effect: int | None = None + entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -261,6 +276,11 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) + async def _async_reload_entry(self) -> None: + """Reload entry.""" + _LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -280,8 +300,24 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): break - # Check for input events + # Check for input events and config change + cfg_changed = 0 for block in self.device.blocks: + if block.type == "device": + cfg_changed = block.cfgChanged + + # For dual mode bulbs ignore change if it is due to mode/effect change + if self.model in DUAL_MODE_LIGHT_MODELS: + if "mode" in block.sensor_ids and self.model != "SHRGBW2": + if self._last_mode != block.mode: + self._last_cfg_changed = None + self._last_mode = block.mode + + if "effect" in block.sensor_ids: + if self._last_effect != block.effect: + self._last_cfg_changed = None + self._last_effect = block.effect + if ( "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids @@ -318,6 +354,15 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.name, ) + if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: + _LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + self._last_cfg_changed = cfg_changed + async def _async_update_data(self) -> None: """Fetch data.""" if sleep_period := self.entry.data.get("sleep_period"): @@ -496,6 +541,15 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device + self._debounced_reload = Debouncer( + hass, + _LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -505,6 +559,11 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) + async def _async_reload_entry(self) -> None: + """Reload entry.""" + _LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -518,7 +577,18 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._last_event = self.device.event for event in self.device.event["events"]: - if event.get("event") not in RPC_INPUTS_EVENTS_TYPES: + event_type = event.get("event") + if event_type is None: + continue + + if event_type == "config_changed": + _LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + elif event_type not in RPC_INPUTS_EVENTS_TYPES: continue self.hass.bus.async_fire( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index a6f3ab12c6b..50f81511062 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -35,6 +35,14 @@ MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( "SHVIN-1", ) +# Bulbs that support white & color modes +DUAL_MODE_LIGHT_MODELS: Final = ( + "SHBDUO-1", + "SHBLB-1", + "SHCB-1", + "SHRGBW2", +) + # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC: Final = 18 @@ -137,3 +145,6 @@ UPTIME_DEVIATION: Final = 5 # Max RPC switch/input key instances MAX_RPC_KEY_INSTANCES = 4 + +# Time to wait before reloading entry upon device config change +ENTRY_RELOAD_COOLDOWN = 60