From ebac7b7aad75962af659cd95e8229c57ecfc7c36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 12:36:45 -0700 Subject: [PATCH 01/13] Speed up TP-Link lights (#33606) * Speed up TP-Link lights * Color temp kan be None * hs as int, force color temp=0 * Fix color temp? * Additional tplink cleanups to reduce api calls * Update test to return state, remove Throttle * Fix state restore on off/on * Fix lights without hue/sat Co-authored-by: J. Nick Koston --- homeassistant/components/tplink/light.py | 286 ++++++++++++++--------- tests/components/tplink/test_light.py | 3 +- 2 files changed, 173 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 7e07f7931f5..fcb05bcdcea 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -15,18 +15,22 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, Light, ) +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.device_registry as dr from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, ) +import homeassistant.util.dt as dt_util from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN from .common import async_add_entities_retry PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=5) +CURRENT_POWER_UPDATE_INTERVAL = timedelta(seconds=60) +HISTORICAL_POWER_UPDATE_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) @@ -34,6 +38,21 @@ ATTR_CURRENT_POWER_W = "current_power_w" ATTR_DAILY_ENERGY_KWH = "daily_energy_kwh" ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh" +LIGHT_STATE_DFT_ON = "dft_on_state" +LIGHT_STATE_ON_OFF = "on_off" +LIGHT_STATE_BRIGHTNESS = "brightness" +LIGHT_STATE_COLOR_TEMP = "color_temp" +LIGHT_STATE_HUE = "hue" +LIGHT_STATE_SATURATION = "saturation" +LIGHT_STATE_ERROR_MSG = "err_msg" + +LIGHT_SYSINFO_MAC = "mac" +LIGHT_SYSINFO_ALIAS = "alias" +LIGHT_SYSINFO_MODEL = "model" +LIGHT_SYSINFO_IS_DIMMABLE = "is_dimmable" +LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP = "is_variable_color_temp" +LIGHT_SYSINFO_IS_COLOR = "is_color" + async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform. @@ -82,7 +101,21 @@ class LightState(NamedTuple): brightness: int color_temp: float hs: Tuple[int, int] - emeter_params: dict + + def to_param(self): + """Return a version that we can send to the bulb.""" + if self.color_temp: + color_temp = mired_to_kelvin(self.color_temp) + else: + color_temp = None + + return { + LIGHT_STATE_ON_OFF: 1 if self.state else 0, + LIGHT_STATE_BRIGHTNESS: brightness_to_percentage(self.brightness), + LIGHT_STATE_COLOR_TEMP: color_temp, + LIGHT_STATE_HUE: self.hs[0] if self.hs else 0, + LIGHT_STATE_SATURATION: self.hs[1] if self.hs else 0, + } class LightFeatures(NamedTuple): @@ -107,6 +140,9 @@ class TPLinkSmartBulb(Light): self._light_state = cast(LightState, None) self._is_available = True self._is_setting_light_state = False + self._last_current_power_update = None + self._last_historical_power_update = None + self._emeter_params = {} @property def unique_id(self): @@ -137,45 +173,42 @@ class TPLinkSmartBulb(Light): @property def device_state_attributes(self): """Return the state attributes of the device.""" - return self._light_state.emeter_params + return self._emeter_params async def async_turn_on(self, **kwargs): """Turn the light on.""" - brightness = ( - int(kwargs[ATTR_BRIGHTNESS]) - if ATTR_BRIGHTNESS in kwargs - else self._light_state.brightness - if self._light_state.brightness is not None - else 255 - ) - color_tmp = ( - int(kwargs[ATTR_COLOR_TEMP]) - if ATTR_COLOR_TEMP in kwargs - else self._light_state.color_temp - ) + if ATTR_BRIGHTNESS in kwargs: + brightness = int(kwargs[ATTR_BRIGHTNESS]) + elif self._light_state.brightness is not None: + brightness = self._light_state.brightness + else: + brightness = 255 - await self.async_set_light_state_retry( + if ATTR_COLOR_TEMP in kwargs: + color_tmp = int(kwargs[ATTR_COLOR_TEMP]) + else: + color_tmp = self._light_state.color_temp + + if ATTR_HS_COLOR in kwargs: + # TP-Link requires integers. + hue_sat = tuple(int(val) for val in kwargs[ATTR_HS_COLOR]) + + # TP-Link cannot have both color temp and hue_sat + color_tmp = 0 + else: + hue_sat = self._light_state.hs + + await self._async_set_light_state_retry( self._light_state, - LightState( - state=True, - brightness=brightness, - color_temp=color_tmp, - hs=tuple(kwargs.get(ATTR_HS_COLOR, self._light_state.hs or ())), - emeter_params=self._light_state.emeter_params, + self._light_state._replace( + state=True, brightness=brightness, color_temp=color_tmp, hs=hue_sat, ), ) async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self.async_set_light_state_retry( - self._light_state, - LightState( - state=False, - brightness=self._light_state.brightness, - color_temp=self._light_state.color_temp, - hs=self._light_state.hs, - emeter_params=self._light_state.emeter_params, - ), + await self._async_set_light_state_retry( + self._light_state, self._light_state._replace(state=False), ) @property @@ -214,21 +247,11 @@ class TPLinkSmartBulb(Light): if self._is_setting_light_state: return - # Initial run, perform call blocking. - if not self._light_features: - self.do_update_retry(False) - # Subsequent runs should not block. - else: - self.hass.add_job(self.do_update_retry, True) - - def do_update_retry(self, update_state: bool) -> None: - """Update state data with retry.""" "" try: # Update light features only once. - self._light_features = ( - self._light_features or self.get_light_features_retry() - ) - self._light_state = self.get_light_state_retry(self._light_features) + if not self._light_features: + self._light_features = self._get_light_features_retry() + self._light_state = self._get_light_state_retry() self._is_available = True except (SmartDeviceException, OSError) as ex: if self._is_available: @@ -237,45 +260,42 @@ class TPLinkSmartBulb(Light): ) self._is_available = False - # The local variables were updates asyncronousally, - # we need the entity registry to poll this object's properties for - # updated information. Calling schedule_update_ha_state will only - # cause a loop. - if update_state: - self.schedule_update_ha_state() - @property def supported_features(self): """Flag supported features.""" return self._light_features.supported_features - def get_light_features_retry(self) -> LightFeatures: + def _get_light_features_retry(self) -> LightFeatures: """Retry the retrieval of the supported features.""" try: - return self.get_light_features() + return self._get_light_features() except (SmartDeviceException, OSError): pass _LOGGER.debug("Retrying getting light features") - return self.get_light_features() + return self._get_light_features() - def get_light_features(self): + def _get_light_features(self): """Determine all supported features in one go.""" sysinfo = self.smartbulb.sys_info supported_features = 0 + # Calling api here as it reformats mac = self.smartbulb.mac - alias = self.smartbulb.alias - model = self.smartbulb.model + alias = sysinfo[LIGHT_SYSINFO_ALIAS] + model = sysinfo[LIGHT_SYSINFO_MODEL] min_mireds = None max_mireds = None - if self.smartbulb.is_dimmable: + if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE): supported_features += SUPPORT_BRIGHTNESS - if getattr(self.smartbulb, "is_variable_color_temp", False): + if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP): supported_features += SUPPORT_COLOR_TEMP - min_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[1]) - max_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[0]) - if getattr(self.smartbulb, "is_color", False): + # Have to make another api request here in + # order to not re-implement pyHS100 here + max_range, min_range = self.smartbulb.valid_temperature_range + min_mireds = kelvin_to_mired(min_range) + max_mireds = kelvin_to_mired(max_range) + if sysinfo.get(LIGHT_SYSINFO_IS_COLOR): supported_features += SUPPORT_COLOR return LightFeatures( @@ -288,110 +308,146 @@ class TPLinkSmartBulb(Light): max_mireds=max_mireds, ) - def get_light_state_retry(self, light_features: LightFeatures) -> LightState: + def _get_light_state_retry(self) -> LightState: """Retry the retrieval of getting light states.""" try: - return self.get_light_state(light_features) + return self._get_light_state() except (SmartDeviceException, OSError): pass _LOGGER.debug("Retrying getting light state") - return self.get_light_state(light_features) + return self._get_light_state() - def get_light_state(self, light_features: LightFeatures) -> LightState: - """Get the light state.""" - emeter_params = {} + def _light_state_from_params(self, light_state_params) -> LightState: brightness = None color_temp = None hue_saturation = None - state = self.smartbulb.state == SmartBulb.BULB_STATE_ON + light_features = self._light_features + + state = bool(light_state_params[LIGHT_STATE_ON_OFF]) + + if not state and LIGHT_STATE_DFT_ON in light_state_params: + light_state_params = light_state_params[LIGHT_STATE_DFT_ON] if light_features.supported_features & SUPPORT_BRIGHTNESS: - brightness = brightness_from_percentage(self.smartbulb.brightness) + brightness = brightness_from_percentage( + light_state_params[LIGHT_STATE_BRIGHTNESS] + ) if light_features.supported_features & SUPPORT_COLOR_TEMP: - if self.smartbulb.color_temp is not None and self.smartbulb.color_temp != 0: - color_temp = kelvin_to_mired(self.smartbulb.color_temp) + if ( + light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None + and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0 + ): + color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP]) if light_features.supported_features & SUPPORT_COLOR: - hue, sat, _ = self.smartbulb.hsv - hue_saturation = (hue, sat) - - if self.smartbulb.has_emeter: - emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( - self.smartbulb.current_consumption() + hue_saturation = ( + light_state_params[LIGHT_STATE_HUE], + light_state_params[LIGHT_STATE_SATURATION], ) - daily_statistics = self.smartbulb.get_emeter_daily() - monthly_statistics = self.smartbulb.get_emeter_monthly() - try: - emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format( - daily_statistics[int(time.strftime("%d"))] - ) - emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format( - monthly_statistics[int(time.strftime("%m"))] - ) - except KeyError: - # device returned no daily/monthly history - pass return LightState( state=state, brightness=brightness, color_temp=color_temp, hs=hue_saturation, - emeter_params=emeter_params, ) - async def async_set_light_state_retry( + def _get_light_state(self) -> LightState: + """Get the light state.""" + self._update_emeter() + return self._light_state_from_params(self.smartbulb.get_light_state()) + + def _update_emeter(self): + if not self.smartbulb.has_emeter: + return + + now = dt_util.utcnow() + if ( + not self._last_current_power_update + or self._last_current_power_update + CURRENT_POWER_UPDATE_INTERVAL < now + ): + self._last_current_power_update = now + self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( + self.smartbulb.current_consumption() + ) + + if ( + not self._last_historical_power_update + or self._last_historical_power_update + HISTORICAL_POWER_UPDATE_INTERVAL + < now + ): + self._last_historical_power_update = now + daily_statistics = self.smartbulb.get_emeter_daily() + monthly_statistics = self.smartbulb.get_emeter_monthly() + try: + self._emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format( + daily_statistics[int(time.strftime("%d"))] + ) + self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format( + monthly_statistics[int(time.strftime("%m"))] + ) + except KeyError: + # device returned no daily/monthly history + pass + + async def _async_set_light_state_retry( self, old_light_state: LightState, new_light_state: LightState ) -> None: """Set the light state with retry.""" - # Optimistically setting the light state. - self._light_state = new_light_state - # Tell the device to set the states. + if not _light_state_diff(old_light_state, new_light_state): + # Nothing to do, avoid the executor + return + self._is_setting_light_state = True try: - await self.hass.async_add_executor_job( - self.set_light_state, old_light_state, new_light_state + light_state_params = await self.hass.async_add_executor_job( + self._set_light_state, old_light_state, new_light_state ) self._is_available = True self._is_setting_light_state = False + if LIGHT_STATE_ERROR_MSG in light_state_params: + raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG]) + self._light_state = self._light_state_from_params(light_state_params) return except (SmartDeviceException, OSError): pass try: _LOGGER.debug("Retrying setting light state") - await self.hass.async_add_executor_job( - self.set_light_state, old_light_state, new_light_state + light_state_params = await self.hass.async_add_executor_job( + self._set_light_state, old_light_state, new_light_state ) self._is_available = True + if LIGHT_STATE_ERROR_MSG in light_state_params: + raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG]) + self._light_state = self._light_state_from_params(light_state_params) except (SmartDeviceException, OSError) as ex: self._is_available = False _LOGGER.warning("Could not set data for %s: %s", self.smartbulb.host, ex) self._is_setting_light_state = False - def set_light_state( + def _set_light_state( self, old_light_state: LightState, new_light_state: LightState ) -> None: """Set the light state.""" - # Calling the API with the new state information. - if new_light_state.state != old_light_state.state: - if new_light_state.state: - self.smartbulb.state = SmartBulb.BULB_STATE_ON - else: - self.smartbulb.state = SmartBulb.BULB_STATE_OFF - return + diff = _light_state_diff(old_light_state, new_light_state) - if new_light_state.color_temp != old_light_state.color_temp: - self.smartbulb.color_temp = mired_to_kelvin(new_light_state.color_temp) + if not diff: + return - brightness_pct = brightness_to_percentage(new_light_state.brightness) - if new_light_state.hs != old_light_state.hs and len(new_light_state.hs) > 1: - hue, sat = new_light_state.hs - hsv = (int(hue), int(sat), brightness_pct) - self.smartbulb.hsv = hsv - elif new_light_state.brightness != old_light_state.brightness: - self.smartbulb.brightness = brightness_pct + return self.smartbulb.set_light_state(diff) + + +def _light_state_diff(old_light_state: LightState, new_light_state: LightState): + old_state_param = old_light_state.to_param() + new_state_param = new_light_state.to_param() + + return { + key: value + for key, value in new_state_param.items() + if new_state_param.get(key) != old_state_param.get(key) + } diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index e13870b8ee2..f6f27a888c5 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -85,6 +85,7 @@ def light_mock_data_fixture() -> None: light_state.update(state) light_state["dft_on_state"] = drt_on_state + return light_state set_light_state_patch = patch( "homeassistant.components.tplink.common.SmartBulb.set_light_state", @@ -310,7 +311,7 @@ async def test_get_light_state_retry( if set_state_call_count == 1: raise SmartDeviceException() - light_mock_data.set_light_state(state_data) + return light_mock_data.set_light_state(state_data) light_mock_data.set_light_state_mock.side_effect = set_light_state_side_effect From 49db0a37200fb2f0f537ac12e2ddaa4e767aae4f Mon Sep 17 00:00:00 2001 From: danbishop Date: Wed, 8 Apr 2020 20:07:43 +0100 Subject: [PATCH 02/13] Update sensor.py (#33788) Add missing semi-colons to html entities on notification message --- homeassistant/components/octoprint/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 83b247c39cb..08ffbc5849e 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "If you do not want to have your printer on
" " at all times, and you would like to monitor
" "temperatures, please add
" - "bed and/or number_of_tools to your configuration
" + "bed and/or number_of_tools to your configuration
" "and restart.", title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, From 442499b452d7e3cc41682221e5e453dfbba54680 Mon Sep 17 00:00:00 2001 From: Jason Swails Date: Wed, 8 Apr 2020 12:39:36 -0400 Subject: [PATCH 03/13] Bump pylutron-caseta version to 0.6.1 (#33815) --- homeassistant/components/lutron_caseta/manifest.json | 3 +-- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 856bf285a16..90c9d4fc9c9 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,6 @@ "domain": "lutron_caseta", "name": "Lutron Caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.6.0"], - "dependencies": [], + "requirements": ["pylutron-caseta==0.6.1"], "codeowners": ["@swails"] } diff --git a/requirements_all.txt b/requirements_all.txt index 15168678a85..59de07c2e16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,7 +1378,7 @@ pylitejet==0.1 pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta -pylutron-caseta==0.6.0 +pylutron-caseta==0.6.1 # homeassistant.components.lutron pylutron==0.2.5 From da8ce072166eddcedadf9f8f84ce30b86ae228fb Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 8 Apr 2020 14:44:52 -0500 Subject: [PATCH 04/13] Update to pyipp==0.9.1 (#33819) --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 9e491a54896..4be57f13fbb 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.9.0"], + "requirements": ["pyipp==0.9.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 59de07c2e16..d615626f998 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.0 +pyipp==0.9.1 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3403ad5a519..74587dc3cf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.0 +pyipp==0.9.1 # homeassistant.components.iqvia pyiqvia==0.2.1 From 7eba08f3857038cb427da85fd2f61a01af4cd55a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 8 Apr 2020 13:49:05 -0600 Subject: [PATCH 05/13] Fix unhandled exception in Recollect Waste (#33823) --- homeassistant/components/recollect_waste/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 17496f3d361..bc1ace5369f 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,4 +1,5 @@ """Support for Recollect Waste curbside collection pickup.""" +from datetime import timedelta import logging import recollect_waste @@ -16,7 +17,7 @@ CONF_PLACE_ID = "place_id" CONF_SERVICE_ID = "service_id" DEFAULT_NAME = "recollect_waste" ICON = "mdi:trash-can-outline" -SCAN_INTERVAL = 86400 +SCAN_INTERVAL = timedelta(days=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( From 663c994dfb68cc8319b517c6b1149f1055d4bd30 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 8 Apr 2020 22:04:47 +0200 Subject: [PATCH 06/13] Fix modbus transaction response (#33824) Sometimes a modbus server do not respond to a transaction, this is a contradiction to the modbus protocol specification, but merely a matter of fact. Use asynio.await_for() to provoke a timeout, and close the transaction. --- homeassistant/components/modbus/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 869d9f7ac67..eb0e1b30d8a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from async_timeout import timeout from pymodbus.client.asynchronous import schedulers from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as ClientSerial from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ClientTCP @@ -246,7 +247,12 @@ class ModbusHub: await self._connect_delay() async with self._lock: kwargs = {"unit": unit} if unit else {} - result = await func(address, count, **kwargs) + try: + async with timeout(self._config_timeout): + result = await func(address, count, **kwargs) + except asyncio.TimeoutError: + result = None + if isinstance(result, (ModbusException, ExceptionResponse)): _LOGGER.error("Hub %s Exception (%s)", self._config_name, result) return result @@ -256,7 +262,11 @@ class ModbusHub: await self._connect_delay() async with self._lock: kwargs = {"unit": unit} if unit else {} - await func(address, value, **kwargs) + try: + async with timeout(self._config_timeout): + func(address, value, **kwargs) + except asyncio.TimeoutError: + return async def read_coils(self, unit, address, count): """Read coils.""" From 885cf20afa8ed39e28ff88e0609cc9bb7719649d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 8 Apr 2020 21:48:46 +0200 Subject: [PATCH 07/13] Fix kef DSP_SCAN_INTERVAL timedelta (#33825) reported on https://community.home-assistant.io/t/kef-ls50-wireless/70269/134 --- homeassistant/components/kef/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 2a227212006..14e6e6b406f 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -67,7 +67,7 @@ SERVICE_LOW_HZ = "set_low_hz" SERVICE_SUB_DB = "set_sub_db" SERVICE_UPDATE_DSP = "update_dsp" -DSP_SCAN_INTERVAL = 3600 +DSP_SCAN_INTERVAL = timedelta(seconds=3600) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From 9b8d1b88c5b4fed65b5ecc190f6b2f101adfed55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2020 20:29:46 -0500 Subject: [PATCH 08/13] Fix Doorbird yaml import aborted if discovery finds it first (#33843) --- .../components/doorbird/config_flow.py | 36 +++++++--- tests/components/doorbird/test_config_flow.py | 67 +++++++++++++++++++ 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index aa712a63ed0..52f94116344 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -68,17 +68,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if "base" not in errors: + info, errors = await self._async_validate_or_error(user_input) + if not errors: await self.async_set_unique_id(info["mac_addr"]) self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) @@ -119,8 +110,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Handle import.""" + if user_input: + info, errors = await self._async_validate_or_error(user_input) + if not errors: + await self.async_set_unique_id( + info["mac_addr"], raise_on_progress=False + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) return await self.async_step_user(user_input) + async def _async_validate_or_error(self, user_input): + """Validate doorbird or error.""" + errors = {} + info = {} + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return info, errors + @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index f911787c1c3..8b49f87bd0b 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -127,6 +127,73 @@ async def test_form_import(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_import_with_zeroconf_already_discovered(hass): + """Test we get the form with import source.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + + await setup.async_setup_component(hass, "persistent_notification", {}) + + # Running the zeroconf init will make the unique id + # in progress + zero_conf = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM + assert zero_conf["step_id"] == "user" + assert zero_conf["errors"] == {} + + import_config = VALID_CONFIG.copy() + import_config[CONF_EVENTS] = ["event1", "event2", "event3"] + import_config[CONF_TOKEN] = "imported_token" + import_config[ + CONF_CUSTOM_URL + ] = "http://legacy.custom.url/should/only/come/in/from/yaml" + + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} + ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.doorbird.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=import_config, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "1.2.3.4" + assert result["data"] == { + "host": "1.2.3.4", + "name": "mydoorbird", + "password": "password", + "username": "friend", + "events": ["event1", "event2", "event3"], + "token": "imported_token", + # This will go away once we convert to cloud hooks + "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", + } + # It is not possible to import options at this time + # so they end up in the config entry data and are + # used a fallback when they are not in options + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" await hass.async_add_executor_job( From df768cab7ddb5663736cdc3a4912e02d72861cc6 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 8 Apr 2020 21:36:11 -0400 Subject: [PATCH 09/13] Bump up ZHA dependencies (#33856) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 66b89724a2f..193a3c12165 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,7 +8,7 @@ "zha-quirks==0.0.38", "zigpy-cc==0.3.1", "zigpy-deconz==0.8.0", - "zigpy-homeassistant==0.18.1", + "zigpy-homeassistant==0.18.2", "zigpy-xbee-homeassistant==0.11.0", "zigpy-zigate==0.5.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index d615626f998..cd5dd932d4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2191,7 +2191,7 @@ zigpy-cc==0.3.1 zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.18.1 +zigpy-homeassistant==0.18.2 # homeassistant.components.zha zigpy-xbee-homeassistant==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74587dc3cf1..3e09e7916f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -807,7 +807,7 @@ zigpy-cc==0.3.1 zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.18.1 +zigpy-homeassistant==0.18.2 # homeassistant.components.zha zigpy-xbee-homeassistant==0.11.0 From dceb0d9bf74b49c2898928177e2f5168828cdd2f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 8 Apr 2020 23:44:33 -0400 Subject: [PATCH 10/13] =?UTF-8?q?Fix=20vizio=20bug=20that=20occurs=20when?= =?UTF-8?q?=20CONF=5FAPPS=20isn't=20in=20config=20entry=E2=80=A6=20(#33857?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix bug when search for string in dict fails when dict is null * another bug fix that I only noticed because of this other bug * add test to cover failure scenario * update docstring * add additional assertions to cover failure scenario that's being fixed --- homeassistant/components/vizio/config_flow.py | 4 +-- tests/components/vizio/test_config_flow.py | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index ba3ac5107bb..51f00ad98bb 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -125,8 +125,8 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): default_include_or_exclude = ( CONF_EXCLUDE if self.config_entry.options - and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS) - else CONF_EXCLUDE + and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS, {}) + else CONF_INCLUDE ) options.update( { diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index a8a760d8ca2..b5b10534759 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_C from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( CONF_APPS, + CONF_APPS_TO_INCLUDE_OR_EXCLUDE, CONF_INCLUDE, CONF_VOLUME_STEP, DEFAULT_NAME, @@ -176,6 +177,39 @@ async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} +async def test_tv_options_flow_start_with_volume(hass: HomeAssistantType) -> None: + """Test options config flow for TV with providing apps option after providing volume step in initial config.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_VALID_TV_CONFIG, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + ) + entry.add_to_hass(hass) + + assert entry.options + assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} + assert CONF_APPS not in entry.options + assert CONF_APPS_TO_INCLUDE_OR_EXCLUDE not in entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + options = {CONF_VOLUME_STEP: VOLUME_STEP} + options.update(MOCK_INCLUDE_APPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=options + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS in result["data"] + assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} + + async def test_user_host_already_configured( hass: HomeAssistantType, vizio_connect: pytest.fixture, From c1814201be27e9f505e48e2d24173de861062a99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 20:46:37 -0700 Subject: [PATCH 11/13] Bumped version to 0.108.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4b829692ea5..f9413cf6e4d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 05289216c40b6f4204d3dcf0a8a48b80e70c69d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Apr 2020 15:11:46 -0700 Subject: [PATCH 12/13] TTS: Wait till files are created in tests (#33760) --- tests/components/tts/test_init.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index ab5d562ffc8..5f0642474e0 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -121,6 +121,7 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir): ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( hass.config.api.base_url ) + await hass.async_block_till_done() assert ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).is_file() @@ -153,6 +154,7 @@ async def test_setup_component_and_test_service_with_config_language( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( hass.config.api.base_url ) + await hass.async_block_till_done() assert ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" ).is_file() @@ -194,6 +196,7 @@ async def test_setup_component_and_test_service_with_service_language( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( hass.config.api.base_url ) + await hass.async_block_till_done() assert ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" ).is_file() @@ -221,6 +224,7 @@ async def test_setup_component_test_service_with_wrong_service_language( blocking=True, ) assert len(calls) == 0 + await hass.async_block_till_done() assert not ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3" ).is_file() @@ -257,6 +261,7 @@ async def test_setup_component_and_test_service_with_service_options( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( hass.config.api.base_url, opt_hash ) + await hass.async_block_till_done() assert ( empty_cache_dir / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" @@ -329,6 +334,7 @@ async def test_setup_component_and_test_service_with_service_options_wrong( opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value assert len(calls) == 0 + await hass.async_block_till_done() assert not ( empty_cache_dir / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" @@ -383,6 +389,7 @@ async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_di # To make sure the file is persisted await hass.async_block_till_done() assert len(calls) == 1 + await hass.async_block_till_done() assert ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).is_file() @@ -391,6 +398,7 @@ async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_di tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}, blocking=True ) + await hass.async_block_till_done() assert not ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).is_file() @@ -520,6 +528,7 @@ async def test_setup_component_test_without_cache(hass, empty_cache_dir): blocking=True, ) assert len(calls) == 1 + await hass.async_block_till_done() assert not ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).is_file() @@ -547,6 +556,7 @@ async def test_setup_component_test_with_cache_call_service_without_cache( blocking=True, ) assert len(calls) == 1 + await hass.async_block_till_done() assert not ( empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).is_file() From 70f14600d1f6d0d6db60f0577911ccd2447efa68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 17:18:09 -0700 Subject: [PATCH 13/13] Fix last flaky TTS test (#33849) --- tests/components/tts/test_init.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 5f0642474e0..f20b45b34b0 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,6 +1,5 @@ """The tests for the TTS component.""" import ctypes -import os from unittest.mock import PropertyMock, patch import pytest @@ -299,14 +298,11 @@ async def test_setup_component_and_test_with_service_options_def(hass, empty_cac ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( hass.config.api.base_url, opt_hash ) - assert os.path.isfile( - os.path.join( - empty_cache_dir, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( - opt_hash - ), - ) - ) + await hass.async_block_till_done() + assert ( + empty_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" + ).is_file() async def test_setup_component_and_test_service_with_service_options_wrong(