From ebac7b7aad75962af659cd95e8229c57ecfc7c36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 12:36:45 -0700 Subject: [PATCH 01/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] =?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/62] 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/62] 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/62] 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( From f70a2ba1f71fa7dd13ee3216abcb5cc4ff2458cb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 9 Apr 2020 17:49:09 -0500 Subject: [PATCH 14/62] Improve Plex debounce/throttle logic (#33805) * Improve Plex debounce/throttle logic * Use Debouncer helper, rewrite affected tests * Mock storage so files aren't left behind * Don't bother with wrapper method, store debouncer call during init * Test cleanup from review * Don't patch own code in tests --- homeassistant/components/plex/server.py | 38 ++--- tests/components/plex/common.py | 20 --- tests/components/plex/test_config_flow.py | 6 +- tests/components/plex/test_init.py | 155 +++++++++++--------- tests/components/plex/test_server.py | 166 +++++++++++++--------- 5 files changed, 200 insertions(+), 185 deletions(-) delete mode 100644 tests/components/plex/common.py diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 4134ad4e32b..d9e2d2bd9cc 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,4 @@ """Shared class to maintain Plex server instances.""" -from functools import partial, wraps import logging import ssl from urllib.parse import urlparse @@ -13,8 +12,8 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import ( CONF_CLIENT_IDENTIFIER, @@ -43,31 +42,6 @@ plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION -def debounce(func): - """Decorate function to debounce callbacks from Plex websocket.""" - - unsub = None - - async def call_later_listener(self, _): - """Handle call_later callback.""" - nonlocal unsub - unsub = None - await func(self) - - @wraps(func) - async def wrapper(self): - """Schedule async callback.""" - nonlocal unsub - if unsub: - _LOGGER.debug("Throttling update of %s", self.friendly_name) - unsub() # pylint: disable=not-callable - unsub = async_call_later( - self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), - ) - - return wrapper - - class PlexServer: """Manages a single Plex server connection.""" @@ -87,6 +61,13 @@ class PlexServer: self._accounts = [] self._owner_username = None self._version = None + self.async_update_platforms = Debouncer( + hass, + _LOGGER, + cooldown=DEBOUNCE_TIMEOUT, + immediate=True, + function=self._async_update_platforms, + ).async_call # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -192,8 +173,7 @@ class PlexServer: """Fetch all data from the Plex server in a single method.""" return (self._plex_server.clients(), self._plex_server.sessions()) - @debounce - async def async_update_platforms(self): + async def _async_update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/tests/components/plex/common.py b/tests/components/plex/common.py deleted file mode 100644 index adc6f4e0299..00000000000 --- a/tests/components/plex/common.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Common fixtures and functions for Plex tests.""" -from datetime import timedelta - -from homeassistant.components.plex.const import ( - DEBOUNCE_TIMEOUT, - PLEX_UPDATE_PLATFORMS_SIGNAL, -) -from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util - -from tests.common import async_fire_time_changed - - -async def trigger_plex_update(hass, server_id): - """Update Plex by sending signal and jumping ahead by debounce timeout.""" - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index bd5d45c0246..d839ccc674b 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -15,13 +15,14 @@ from homeassistant.components.plex.const import ( CONF_USE_EPISODE_ART, DOMAIN, PLEX_SERVER_CONFIG, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.config_entries import ENTRY_STATE_LOADED from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer @@ -415,7 +416,8 @@ async def test_option_flow_new_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index cd1ea8725bd..ef2199b11c5 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -3,8 +3,9 @@ import copy from datetime import timedelta import ssl -from asynctest import patch +from asynctest import ClockedTestCase, patch import plexapi +import pytest import requests from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -23,14 +24,19 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_test_home_assistant, + mock_storage, +) async def test_setup_with_config(hass): @@ -67,70 +73,90 @@ async def test_setup_with_config(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" -async def test_setup_with_config_entry(hass, caplog): - """Test setup component with config.""" + @pytest.fixture(autouse=True) + def inject_fixture(self, caplog): + """Inject pytest fixtures as instance attributes.""" + self.caplog = caplog - mock_plex_server = MockPlexServer() + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + async def test_setup_with_config_entry(self): + """Test setup component with config.""" + hass = self.hass + + mock_plex_server = MockPlexServer() + + entry = MockConfigEntry( + domain=const.DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ) as mock_listen: + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_listen.called + + assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] + + assert loaded_server.plex_server == mock_plex_server + + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) await hass.async_block_till_done() - assert mock_listen.called + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED - - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - - assert loaded_server.plex_server == mock_plex_server - - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - - await trigger_plex_update(hass, server_id) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - await trigger_plex_update(hass, server_id) - - for test_exception in ( - plexapi.exceptions.BadRequest, - requests.exceptions.RequestException, - ): - with patch.object( - mock_plex_server, "clients", side_effect=test_exception - ) as patched_clients_bad_request: - await trigger_plex_update(hass, server_id) - - assert patched_clients_bad_request.called - assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in caplog.text + # Ensure existing entities refresh + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) ) - caplog.clear() + await hass.async_block_till_done() + + for test_exception in ( + plexapi.exceptions.BadRequest, + requests.exceptions.RequestException, + ): + with patch.object( + mock_plex_server, "clients", side_effect=test_exception + ) as patched_clients_bad_request: + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) + await hass.async_block_till_done() + + assert patched_clients_bad_request.called + assert ( + f"Could not connect to Plex server: {mock_plex_server.friendlyName}" + in self.caplog.text + ) + self.caplog.clear() async def test_set_config_entry_unique_id(hass): @@ -251,22 +277,12 @@ async def test_unload_config_entry(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - with patch("homeassistant.components.plex.PlexWebsocket.close") as mock_close: await hass.config_entries.async_unload(entry.entry_id) assert mock_close.called assert entry.state == ENTRY_STATE_NOT_LOADED - assert server_id not in hass.data[const.DOMAIN][const.SERVERS] - assert server_id not in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id not in hass.data[const.DOMAIN][const.WEBSOCKETS] - async def test_setup_with_photo_session(hass): """Test setup component with config.""" @@ -292,7 +308,8 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") assert media_player.state == "idle" diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 3b70f30189a..6eff97ae7dc 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,8 +1,7 @@ """Tests for Plex server.""" import copy -from datetime import timedelta -from asynctest import patch +from asynctest import ClockedTestCase, patch from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( @@ -14,13 +13,11 @@ from homeassistant.components.plex.const import ( SERVERS, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_test_home_assistant, mock_storage async def test_new_users_available(hass): @@ -48,7 +45,8 @@ async def test_new_users_available(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -86,7 +84,8 @@ async def test_new_ignored_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -100,72 +99,109 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_mark_sessions_idle(hass): - """Test marking media_players as idle when sessions end.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" - mock_plex_server = MockPlexServer(config_entry=entry) + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) + + async def test_mark_sessions_idle(self): + """Test marking media_players as idle when sessions end.""" + hass = self.hass + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - await trigger_plex_update(hass, server_id) + mock_plex_server.clear_clients() + mock_plex_server.clear_sessions() - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() - - await trigger_plex_update(hass, server_id) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == "0" - - -async def test_debouncer(hass, caplog): - """Test debouncer decorator logic.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + await self.advance(DEBOUNCE_TIMEOUT) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == "0" - # First two updates are skipped - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async def test_debouncer(self): + """Test debouncer behavior.""" + hass = self.hass - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) - assert ( - caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 2 - ) + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( + mock_plex_server, "sessions", return_value=[] + ) as mock_update: + # Called immediately + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 3 From 34fdf5a36f034c4429a599928f1574f36a49507c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 11:13:08 -0700 Subject: [PATCH 15/62] Update aioswitcher (#33821) --- homeassistant/components/switcher_kis/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 81f5d2085c6..bc608276897 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,6 +3,5 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi"], - "requirements": ["aioswitcher==2019.4.26"], - "dependencies": [] + "requirements": ["aioswitcher==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd5dd932d4f..1d7dbe1135f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,7 +208,7 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==2019.4.26 +aioswitcher==1.1.0 # homeassistant.components.unifi aiounifi==15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e09e7916f5..3ad8676c9c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==2019.4.26 +aioswitcher==1.1.0 # homeassistant.components.unifi aiounifi==15 From 9efbf2f880fd50dd4691ae1be0327afd043cf400 Mon Sep 17 00:00:00 2001 From: Lennart Henke Date: Thu, 9 Apr 2020 16:10:17 +0200 Subject: [PATCH 16/62] Fix nextcloud sensor mappings (#33840) --- homeassistant/components/nextcloud/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 39eb16ec265..12c17e6081d 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -63,7 +63,7 @@ SENSORS = ( "nextcloud_storage_num_files", "nextcloud_storage_num_storages", "nextcloud_storage_num_storages_local", - "nextcloud_storage_num_storage_home", + "nextcloud_storage_num_storages_home", "nextcloud_storage_num_storages_other", "nextcloud_shares_num_shares", "nextcloud_shares_num_shares_user", @@ -83,9 +83,9 @@ SENSORS = ( "nextcloud_database_type", "nextcloud_database_version", "nextcloud_database_version", - "nextcloud_activeusers_last5minutes", - "nextcloud_activeusers_last1hour", - "nextcloud_activeusers_last24hours", + "nextcloud_activeUsers_last5minutes", + "nextcloud_activeUsers_last1hour", + "nextcloud_activeUsers_last24hours", ) From 995f5db9136a03bb8a4fab4244f8473e3484a454 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 00:54:02 -0700 Subject: [PATCH 17/62] Check status code on onvif snapshot (#33865) --- homeassistant/components/onvif/camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c6a3bffa1b..f66f3a5c4aa 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -516,7 +516,8 @@ class ONVIFHassCamera(Camera): """Read image from a URL.""" try: response = requests.get(self._snapshot, timeout=5, auth=auth) - return response.content + if response.status_code < 300: + return response.content except requests.exceptions.RequestException as error: _LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", From 2ff255dedcf85007830f77583e325b24dc3ab7c1 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 9 Apr 2020 18:04:12 +0300 Subject: [PATCH 18/62] Fix Monoprice robustness (#33869) * Silently handle update failures * Limite parallel updates * Remove return values * Remove trailing return * Add test for empty update --- .../components/monoprice/media_player.py | 15 ++++-- .../components/monoprice/test_media_player.py | 52 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index d85c219691e..9073ab224f1 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,6 +1,8 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from serial import SerialException + from homeassistant import core from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -18,6 +20,8 @@ from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + SUPPORT_MONOPRICE = ( SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET @@ -127,9 +131,15 @@ class MonopriceZone(MediaPlayerDevice): def update(self): """Retrieve latest state.""" - state = self._monoprice.zone_status(self._zone_id) + try: + state = self._monoprice.zone_status(self._zone_id) + except SerialException: + _LOGGER.warning("Could not update zone %d", self._zone_id) + return + if not state: - return False + return + self._state = STATE_ON if state.power else STATE_OFF self._volume = state.volume self._mute = state.mute @@ -138,7 +148,6 @@ class MonopriceZone(MediaPlayerDevice): self._source = self._source_id_name[idx] else: self._source = None - return True @property def entity_registry_enabled_default(self): diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 3778f2af04b..bfe94023be2 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -294,6 +294,58 @@ async def test_update(hass): assert "three" == state.attributes[ATTR_INPUT_SOURCE] +async def test_failed_update(hass): + """Test updating failure from monoprice.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + monoprice.set_source(11, 3) + monoprice.set_volume(11, 38) + + with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + +async def test_empty_update(hass): + """Test updating with no state from monoprice.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + monoprice.set_source(11, 3) + monoprice.set_volume(11, 38) + + with patch.object(MockMonoprice, "zone_status", return_value=None): + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + async def test_supported_features(hass): """Test supported features property.""" await _setup_monoprice(hass, MockMonoprice()) From b2083a7bee9fd4f693cd30752ddc8c4fecbde268 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 9 Apr 2020 13:53:23 +0200 Subject: [PATCH 19/62] Fix modbus default delay (#33877) * solve modbus issue #33872 CONF_DELAY was used in a serial connection, which is not permitted. Sometimes async_update is called after async_setup is completed, but before event EVENT_HOMEASSISTANT_START is issued, leading to a missing object. * resolve review comment. Do not wait for start event, but activate pymodbus directly in async setup. * review 2 Remark, this does not work, async_setup hangs. clean start_modbus() from async calls, leaving only the pymodbus setup. * review 2a Moved listen_once back to start_modbus, since it is sync. --- homeassistant/components/modbus/__init__.py | 33 ++++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index eb0e1b30d8a..14584ea17a0 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -21,7 +21,6 @@ from homeassistant.const import ( CONF_PORT, CONF_TIMEOUT, CONF_TYPE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv @@ -107,7 +106,7 @@ async def async_setup(hass, config): for client in hub_collect.values(): del client - def start_modbus(event): + def start_modbus(): """Start Modbus service.""" for client in hub_collect.values(): _LOGGER.debug("setup hub %s", client.name) @@ -115,20 +114,6 @@ async def async_setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - # Register services for modbus - hass.services.async_register( - MODBUS_DOMAIN, - SERVICE_WRITE_REGISTER, - write_register, - schema=SERVICE_WRITE_REGISTER_SCHEMA, - ) - hass.services.async_register( - MODBUS_DOMAIN, - SERVICE_WRITE_COIL, - write_coil, - schema=SERVICE_WRITE_COIL_SCHEMA, - ) - async def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) @@ -152,8 +137,19 @@ async def async_setup(hass, config): client_name = service.data[ATTR_HUB] await hub_collect[client_name].write_coil(unit, address, state) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_modbus) + # do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now + await hass.async_add_executor_job(start_modbus) + # Register services for modbus + hass.services.async_register( + MODBUS_DOMAIN, + SERVICE_WRITE_REGISTER, + write_register, + schema=SERVICE_WRITE_REGISTER_SCHEMA, + ) + hass.services.async_register( + MODBUS_DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, + ) return True @@ -172,7 +168,7 @@ class ModbusHub: self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] - self._config_delay = client_config[CONF_DELAY] + self._config_delay = 0 if self._config_type == "serial": # serial configuration @@ -184,6 +180,7 @@ class ModbusHub: else: # network configuration self._config_host = client_config[CONF_HOST] + self._config_delay = client_config[CONF_DELAY] @property def name(self): From ecb37d0bdf3a95195739e064b56a8094002cb782 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 9 Apr 2020 17:21:01 +0200 Subject: [PATCH 20/62] Updated frontend to 20200407.2 (#33891) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index efd9f99b18a..3a4919dacae 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200407.1"], + "requirements": ["home-assistant-frontend==20200407.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf6888e7073..92564cd6781 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1d7dbe1135f..a923602b107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ad8676c9c8..c8368b00a80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c3ac8869b084f12dc82fdd7ba15e4b9f4125bfcd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 09:51:23 -0700 Subject: [PATCH 21/62] Fix onvif consistent return (#33898) --- homeassistant/components/onvif/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index f66f3a5c4aa..a0bfbab9b4f 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -525,6 +525,8 @@ class ONVIFHassCamera(Camera): error, ) + return None + image = await self.hass.async_add_job(fetch) if image is None: From 64bdf2d35b2b9276eb9c8f2a64d8b8510270a5c6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 6 Apr 2020 01:09:20 +0200 Subject: [PATCH 22/62] Modbus: isolate common test functions (#33447) Since all entity test functions are going to use the modbus class, isolate the common parts in conftest.py, and thereby make it simpler to write additional test cases. cleaned up test_modbus_sensor.py while splitting the code. --- tests/components/modbus/conftest.py | 96 +++++++++++ tests/components/modbus/test_modbus_sensor.py | 157 +++++++++--------- 2 files changed, 177 insertions(+), 76 deletions(-) create mode 100644 tests/components/modbus/conftest.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py new file mode 100644 index 00000000000..043236c503c --- /dev/null +++ b/tests/components/modbus/conftest.py @@ -0,0 +1,96 @@ +"""The tests for the Modbus sensor component.""" +from datetime import timedelta +import logging +from unittest import mock + +import pytest + +from homeassistant.components.modbus.const import ( + CALL_TYPE_REGISTER_INPUT, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + DEFAULT_HUB, + MODBUS_DOMAIN, +) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import MockModule, async_fire_time_changed, mock_integration + +_LOGGER = logging.getLogger(__name__) + + +@pytest.fixture() +def mock_hub(hass): + """Mock hub.""" + mock_integration(hass, MockModule(MODBUS_DOMAIN)) + hub = mock.MagicMock() + hub.name = "hub" + hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + return hub + + +common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} + + +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + + +read_result = None + + +async def simulate_read_registers(unit, address, count): + """Simulate modbus register read.""" + del unit, address, count # not used in simulation, but in real connection + global read_result + return read_result + + +async def run_test( + hass, mock_hub, register_config, entity_domain, register_words, expected +): + """Run test for given config and check that sensor outputs expected result.""" + + # Full sensor configuration + sensor_name = "modbus_test_sensor" + scan_interval = 5 + config = { + entity_domain: { + CONF_PLATFORM: "modbus", + CONF_SCAN_INTERVAL: scan_interval, + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ], + } + } + + # Setup inputs for the sensor + global read_result + read_result = ReadResult(register_words) + if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: + mock_hub.read_input_registers = simulate_read_registers + else: + mock_hub.read_holding_registers = simulate_read_registers + + # Initialize sensor + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, entity_domain, config) + + # Trigger update call with time_changed event + now += timedelta(seconds=scan_interval + 1) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check state + entity_id = f"{entity_domain}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 1c4094387a9..6207a363937 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,8 +1,5 @@ """The tests for the Modbus sensor component.""" -from datetime import timedelta -from unittest import mock - -import pytest +import logging from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, @@ -11,78 +8,18 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_OFFSET, CONF_PRECISION, - CONF_REGISTER, CONF_REGISTER_TYPE, - CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_UINT, - DEFAULT_HUB, - MODBUS_DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from tests.common import MockModule, async_fire_time_changed, mock_integration +from .conftest import run_test - -@pytest.fixture() -def mock_hub(hass): - """Mock hub.""" - mock_integration(hass, MockModule(MODBUS_DOMAIN)) - hub = mock.MagicMock() - hub.name = "hub" - hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} - return hub - - -common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} - - -class ReadResult: - """Storage class for register read results.""" - - def __init__(self, register_words): - """Init.""" - self.registers = register_words - - -async def run_test(hass, mock_hub, register_config, register_words, expected): - """Run test for given config and check that sensor outputs expected result.""" - - # Full sensor configuration - sensor_name = "modbus_test_sensor" - scan_interval = 5 - config = { - MODBUS_DOMAIN: { - CONF_PLATFORM: "modbus", - CONF_SCAN_INTERVAL: scan_interval, - CONF_REGISTERS: [ - dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) - ], - } - } - - # Setup inputs for the sensor - read_result = ReadResult(register_words) - if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: - mock_hub.read_input_registers.return_value = read_result - else: - mock_hub.read_holding_registers.return_value = read_result - - # Initialize sensor - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, MODBUS_DOMAIN, config) - - # Trigger update call with time_changed event - now += timedelta(seconds=scan_interval + 1) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() +_LOGGER = logging.getLogger(__name__) async def test_simple_word_register(hass, mock_hub): @@ -94,14 +31,26 @@ async def test_simple_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[0], expected="0") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[0], + expected="0", + ) async def test_optional_conf_keys(hass, mock_hub): """Test handling of optional configuration keys.""" register_config = {} await run_test( - hass, mock_hub, register_config, register_words=[0x8000], expected="-32768" + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[0x8000], + expected="-32768", ) @@ -114,7 +63,14 @@ async def test_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[7], expected="20") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="20", + ) async def test_scale_and_offset(hass, mock_hub): @@ -126,7 +82,14 @@ async def test_scale_and_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[7], expected="34") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="34", + ) async def test_ints_can_have_precision(hass, mock_hub): @@ -139,7 +102,12 @@ async def test_ints_can_have_precision(hass, mock_hub): CONF_PRECISION: 4, } await run_test( - hass, mock_hub, register_config, register_words=[7], expected="34.0000" + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="34.0000", ) @@ -152,7 +120,14 @@ async def test_floats_get_rounded_correctly(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[1], expected="2") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[1], + expected="2", + ) async def test_parameters_as_strings(hass, mock_hub): @@ -164,7 +139,14 @@ async def test_parameters_as_strings(hass, mock_hub): CONF_OFFSET: "5", CONF_PRECISION: "1", } - await run_test(hass, mock_hub, register_config, register_words=[9], expected="18.5") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[9], + expected="18.5", + ) async def test_floating_point_scale(hass, mock_hub): @@ -176,7 +158,14 @@ async def test_floating_point_scale(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 2, } - await run_test(hass, mock_hub, register_config, register_words=[1], expected="2.40") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[1], + expected="2.40", + ) async def test_floating_point_offset(hass, mock_hub): @@ -188,7 +177,14 @@ async def test_floating_point_offset(hass, mock_hub): CONF_OFFSET: -10.3, CONF_PRECISION: 1, } - await run_test(hass, mock_hub, register_config, register_words=[2], expected="-8.3") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[2], + expected="-8.3", + ) async def test_signed_two_word_register(hass, mock_hub): @@ -204,6 +200,7 @@ async def test_signed_two_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected="-1985229329", ) @@ -222,6 +219,7 @@ async def test_unsigned_two_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -238,6 +236,7 @@ async def test_reversed(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0xCDEF89AB), ) @@ -256,6 +255,7 @@ async def test_four_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], expected="9920249030613615975", ) @@ -274,6 +274,7 @@ async def test_four_word_register_precision_is_intact_with_int_params(hass, mock hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973793", ) @@ -292,6 +293,7 @@ async def test_four_word_register_precision_is_lost_with_float_params(hass, mock hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973792", ) @@ -311,6 +313,7 @@ async def test_two_word_input_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -330,6 +333,7 @@ async def test_two_word_holding_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -349,6 +353,7 @@ async def test_float_data_type(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[16286, 1617], expected="1.23457", ) From 27134696518ff196732d67730efc04ba5912d36f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 7 Apr 2020 16:56:48 +0200 Subject: [PATCH 23/62] Fix Modbus review comments (#33755) * update common test for modbus integration * remove log messages from modbus setup function. * Make global method local * Change parameter name to snake_case --- homeassistant/components/modbus/__init__.py | 4 ---- .../components/modbus/binary_sensor.py | 4 ++-- homeassistant/components/modbus/climate.py | 4 ++-- homeassistant/components/modbus/sensor.py | 4 ++-- homeassistant/components/modbus/switch.py | 4 ++-- tests/components/modbus/conftest.py | 22 +++++++------------ 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 14584ea17a0..3c488bd3245 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -97,7 +97,6 @@ async def async_setup(hass, config): """Set up Modbus component.""" hass.data[MODBUS_DOMAIN] = hub_collect = {} - _LOGGER.debug("registering hubs") for client_config in config[MODBUS_DOMAIN]: hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) @@ -109,7 +108,6 @@ async def async_setup(hass, config): def start_modbus(): """Start Modbus service.""" for client in hub_collect.values(): - _LOGGER.debug("setup hub %s", client.name) client.setup() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) @@ -158,7 +156,6 @@ class ModbusHub: def __init__(self, client_config, main_loop): """Initialize the Modbus hub.""" - _LOGGER.debug("Preparing setup: %s", client_config) # generic configuration self._loop = main_loop @@ -198,7 +195,6 @@ class ModbusHub: # Client* do deliver loop, client as result but # pylint does not accept that fact - _LOGGER.debug("doing setup") if self._config_type == "serial": _, self._client = ClientSerial( schedulers.ASYNC_IO, diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 51dfb7c5795..9989b9d530a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] for entry in config[CONF_INPUTS]: @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(sensors) + async_add_entities(sensors) class ModbusBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 182dfeef2de..e5fbcf4d421 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -72,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" name = config[CONF_NAME] modbus_slave = config[CONF_SLAVE] @@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): hub_name = config[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] - add_entities( + async_add_entities( [ ModbusThermostat( hub, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8c2b950648b..988d495eba5 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -89,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus sensors.""" sensors = [] data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}} @@ -148,7 +148,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): if not sensors: return False - add_entities(sensors) + async_add_entities(sensors) class ModbusRegisterSensor(RestoreEntity): diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index d7d6f121874..e4ec6a004fb 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -76,7 +76,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Read configuration and create Modbus devices.""" switches = [] if CONF_COILS in config: @@ -109,7 +109,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(switches) + async_add_entities(switches) class ModbusCoilSwitch(ToggleEntity, RestoreEntity): diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 043236c503c..d2fff820cdb 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -32,9 +32,6 @@ def mock_hub(hass): return hub -common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} - - class ReadResult: """Storage class for register read results.""" @@ -46,18 +43,16 @@ class ReadResult: read_result = None -async def simulate_read_registers(unit, address, count): - """Simulate modbus register read.""" - del unit, address, count # not used in simulation, but in real connection - global read_result - return read_result - - async def run_test( - hass, mock_hub, register_config, entity_domain, register_words, expected + hass, use_mock_hub, register_config, entity_domain, register_words, expected ): """Run test for given config and check that sensor outputs expected result.""" + async def simulate_read_registers(unit, address, count): + """Simulate modbus register read.""" + del unit, address, count # not used in simulation, but in real connection + return read_result + # Full sensor configuration sensor_name = "modbus_test_sensor" scan_interval = 5 @@ -72,12 +67,11 @@ async def run_test( } # Setup inputs for the sensor - global read_result read_result = ReadResult(register_words) if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: - mock_hub.read_input_registers = simulate_read_registers + use_mock_hub.read_input_registers = simulate_read_registers else: - mock_hub.read_holding_registers = simulate_read_registers + use_mock_hub.read_holding_registers = simulate_read_registers # Initialize sensor now = dt_util.utcnow() From 54bf83855ca6d0c4097a79edae978c610c1017d0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 9 Apr 2020 22:15:20 +0200 Subject: [PATCH 24/62] Rename domain import in modbus (#33906) --- homeassistant/components/modbus/__init__.py | 13 ++++++------- tests/components/modbus/conftest.py | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 3c488bd3245..1e889043fae 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -35,7 +35,7 @@ from .const import ( CONF_PARITY, CONF_STOPBITS, DEFAULT_HUB, - MODBUS_DOMAIN, + MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) @@ -68,7 +68,7 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend( ) CONFIG_SCHEMA = vol.Schema( - {MODBUS_DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, + {DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, extra=vol.ALLOW_EXTRA, ) @@ -95,9 +95,9 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up Modbus component.""" - hass.data[MODBUS_DOMAIN] = hub_collect = {} + hass.data[DOMAIN] = hub_collect = {} - for client_config in config[MODBUS_DOMAIN]: + for client_config in config[DOMAIN]: hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) def stop_modbus(event): @@ -140,13 +140,13 @@ async def async_setup(hass, config): # Register services for modbus hass.services.async_register( - MODBUS_DOMAIN, + DOMAIN, SERVICE_WRITE_REGISTER, write_register, schema=SERVICE_WRITE_REGISTER_SCHEMA, ) hass.services.async_register( - MODBUS_DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, + DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, ) return True @@ -204,7 +204,6 @@ class ModbusHub: stopbits=self._config_stopbits, bytesize=self._config_bytesize, parity=self._config_parity, - timeout=self._config_timeout, loop=self._loop, ) elif self._config_type == "rtuovertcp": diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d2fff820cdb..d9cd62313b4 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -11,7 +11,7 @@ from homeassistant.components.modbus.const import ( CONF_REGISTER_TYPE, CONF_REGISTERS, DEFAULT_HUB, - MODBUS_DOMAIN, + MODBUS_DOMAIN as DOMAIN, ) from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component @@ -25,10 +25,10 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture() def mock_hub(hass): """Mock hub.""" - mock_integration(hass, MockModule(MODBUS_DOMAIN)) + mock_integration(hass, MockModule(DOMAIN)) hub = mock.MagicMock() hub.name = "hub" - hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + hass.data[DOMAIN] = {DEFAULT_HUB: hub} return hub From e0595ce518efc0a9bb3b2736d11fb51c3eaafc47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2020 17:07:39 -0500 Subject: [PATCH 25/62] Fix tplink HS220 dimmers (#33909) * HS220 dimmers are handled as lights with a limited feature set --- homeassistant/components/tplink/light.py | 37 ++++- tests/components/tplink/test_light.py | 177 ++++++++++++++++++++++- 2 files changed, 210 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index fcb05bcdcea..ffafd1f6300 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -357,7 +357,7 @@ class TPLinkSmartBulb(Light): def _get_light_state(self) -> LightState: """Get the light state.""" self._update_emeter() - return self._light_state_from_params(self.smartbulb.get_light_state()) + return self._light_state_from_params(self._get_device_state()) def _update_emeter(self): if not self.smartbulb.has_emeter: @@ -439,7 +439,40 @@ class TPLinkSmartBulb(Light): if not diff: return - return self.smartbulb.set_light_state(diff) + return self._set_device_state(diff) + + def _get_device_state(self): + """State of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.get_light_state() + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + return { + LIGHT_STATE_ON_OFF: self.smartbulb.state, + LIGHT_STATE_BRIGHTNESS: self.smartbulb.brightness, + LIGHT_STATE_COLOR_TEMP: 0, + LIGHT_STATE_HUE: 0, + LIGHT_STATE_SATURATION: 0, + } + + def _set_device_state(self, state): + """Set state of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.set_light_state(state) + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + if LIGHT_STATE_BRIGHTNESS in state: + # Brightness of 0 is accepted by the + # device but the underlying library rejects it + # so we turn off instead. + if state[LIGHT_STATE_BRIGHTNESS]: + self.smartbulb.brightness = state[LIGHT_STATE_BRIGHTNESS] + else: + self.smartbulb.state = 0 + elif LIGHT_STATE_ON_OFF in state: + self.smartbulb.state = state[LIGHT_STATE_ON_OFF] + + return self._get_device_state() def _light_state_diff(old_light_state: LightState, new_light_state: LightState): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index f6f27a888c5..09c23c6f0e5 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from typing import Callable, NamedTuple -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -16,7 +16,11 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.tplink.common import CONF_DISCOVERY, CONF_LIGHT +from homeassistant.components.tplink.common import ( + CONF_DIMMER, + CONF_DISCOVERY, + CONF_LIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -41,6 +45,16 @@ class LightMockData(NamedTuple): get_emeter_monthly_mock: Mock +class SmartSwitchMockData(NamedTuple): + """Mock smart switch data.""" + + sys_info: dict + light_state: dict + state_mock: Mock + brightness_mock: Mock + get_sysinfo_mock: Mock + + @pytest.fixture(name="light_mock_data") def light_mock_data_fixture() -> None: """Create light mock data.""" @@ -152,6 +166,75 @@ def light_mock_data_fixture() -> None: ) +@pytest.fixture(name="dimmer_switch_mock_data") +def dimmer_switch_mock_data_fixture() -> None: + """Create dimmer switch mock data.""" + sys_info = { + "sw_ver": "1.2.3", + "hw_ver": "2.3.4", + "mac": "aa:bb:cc:dd:ee:ff", + "mic_mac": "00:11:22:33:44", + "type": "switch", + "hwId": "1234", + "fwId": "4567", + "oemId": "891011", + "dev_name": "dimmer1", + "rssi": 11, + "latitude": "0", + "longitude": "0", + "is_color": False, + "is_dimmable": True, + "is_variable_color_temp": False, + "model": "HS220", + "alias": "dimmer1", + "feature": ":", + } + + light_state = { + "on_off": 1, + "brightness": 13, + } + + def state(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["on_off"] + light_state["on_off"] = args[0] + + def brightness(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["brightness"] + if light_state["brightness"] == 0: + light_state["on_off"] = 0 + else: + light_state["on_off"] = 1 + light_state["brightness"] = args[0] + + get_sysinfo_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", + return_value=sys_info, + ) + state_patch = patch( + "homeassistant.components.tplink.common.SmartPlug.state", + new_callable=PropertyMock, + side_effect=state, + ) + brightness_patch = patch( + "homeassistant.components.tplink.common.SmartPlug.brightness", + new_callable=PropertyMock, + side_effect=brightness, + ) + with brightness_patch as brightness_mock, state_patch as state_mock, get_sysinfo_patch as get_sysinfo_mock: + yield SmartSwitchMockData( + sys_info=sys_info, + light_state=light_state, + brightness_mock=brightness_mock, + state_mock=state_mock, + get_sysinfo_mock=get_sysinfo_mock, + ) + + async def update_entity(hass: HomeAssistant, entity_id: str) -> None: """Run an update action for an entity.""" await hass.services.async_call( @@ -160,6 +243,96 @@ async def update_entity(hass: HomeAssistant, entity_id: str) -> None: await hass.async_block_till_done() +async def test_smartswitch( + hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData +) -> None: + """Test function.""" + light_state = dimmer_switch_mock_data.light_state + + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + + await async_setup_component( + hass, + tplink.DOMAIN, + { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_DIMMER: [{CONF_HOST: "123.123.123.123"}], + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.dimmer1") + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.dimmer1"}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + assert hass.states.get("light.dimmer1").state == "off" + assert light_state["on_off"] == 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 48.45 + assert light_state["on_off"] == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 55}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 53.55 + assert light_state["brightness"] == 21 + + light_state["on_off"] = 0 + light_state["brightness"] = 66 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.dimmer1"}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 168.3 + assert light_state["brightness"] == 66 + + async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: """Test function.""" light_state = light_mock_data.light_state From 3ad9052b5cf0c3d2d82c6d9b72bd3bd7039c364f Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:16:33 -0400 Subject: [PATCH 26/62] Exclude access token from host info updates in Konnected config flow (#33912) * black updates * test that host update doesn't impact access token --- homeassistant/components/konnected/config_flow.py | 11 +++++------ tests/components/konnected/test_config_flow.py | 8 +++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 6a3631a8c0d..a6b01560c50 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -283,11 +283,6 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # build config info and wait for user confirmation self.data[CONF_HOST] = user_input[CONF_HOST] self.data[CONF_PORT] = user_input[CONF_PORT] - self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( - CONF_ACCESS_TOKEN - ) or "".join( - random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) - ) # brief delay to allow processing of recent status req await asyncio.sleep(0.1) @@ -343,8 +338,12 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - # Attach default options and create entry + # Create access token, attach default options and create entry self.data[CONF_DEFAULT_OPTIONS] = self.options + self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( + CONF_ACCESS_TOKEN + ) or "".join(random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)) + return self.async_create_entry( title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data, ) diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 917afc5357a..0bf6e7846ae 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -362,10 +362,11 @@ async def test_ssdp_host_update(hass, mock_panel): ) assert result["type"] == "abort" - # confirm the host value was updated + # confirm the host value was updated, access_token was not entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0] assert entry.data["host"] == "1.1.1.1" assert entry.data["port"] == 1234 + assert entry.data["access_token"] == "11223344556677889900" async def test_import_existing_config(hass, mock_panel): @@ -494,6 +495,7 @@ async def test_import_existing_config_entry(hass, mock_panel): data={ "host": "0.0.0.0", "port": 1111, + "access_token": "ORIGINALTOKEN", "id": "112233445566", "extra": "something", }, @@ -546,14 +548,14 @@ async def test_import_existing_config_entry(hass, mock_panel): assert result["type"] == "abort" - # We should have updated the entry + # We should have updated the host info but not the access token assert len(hass.config_entries.async_entries("konnected")) == 1 assert hass.config_entries.async_entries("konnected")[0].data == { "host": "1.2.3.4", "port": 1234, + "access_token": "ORIGINALTOKEN", "id": "112233445566", "model": "Konnected Pro", - "access_token": "SUPERSECRETTOKEN", "extra": "something", } From f38ff3b622fe444c2205001d0a1c0e6170e805b1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:09:05 -0400 Subject: [PATCH 27/62] Bump pyvizio version for vizio (#33924) --- homeassistant/components/vizio/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 885cfacca41..02904bedbde 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,8 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.44"], - "dependencies": [], + "requirements": ["pyvizio==0.1.46"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index a923602b107..0a121a00178 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.46 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8368b00a80..f64d2f7c948 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.46 # homeassistant.components.html5 pywebpush==1.9.2 From 1b0ccf10e51efae56222bee8f87b7cd7adc586e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2020 19:10:02 -0500 Subject: [PATCH 28/62] Fix tplink HS220 dimmers (round 2) (#33928) * HS220 dimmers are handled as lights with a limited feature set * Dimmers look up has has_emeter every call so this is cached as well now to resovle the performance issue. --- homeassistant/components/tplink/light.py | 20 +++++++---- tests/components/tplink/test_light.py | 42 +++++++++++------------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index ffafd1f6300..b1a79a03c8c 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -40,6 +40,7 @@ ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh" LIGHT_STATE_DFT_ON = "dft_on_state" LIGHT_STATE_ON_OFF = "on_off" +LIGHT_STATE_RELAY_STATE = "relay_state" LIGHT_STATE_BRIGHTNESS = "brightness" LIGHT_STATE_COLOR_TEMP = "color_temp" LIGHT_STATE_HUE = "hue" @@ -128,6 +129,7 @@ class LightFeatures(NamedTuple): supported_features: int min_mireds: float max_mireds: float + has_emeter: bool class TPLinkSmartBulb(Light): @@ -285,8 +287,9 @@ class TPLinkSmartBulb(Light): model = sysinfo[LIGHT_SYSINFO_MODEL] min_mireds = None max_mireds = None + has_emeter = self.smartbulb.has_emeter - if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE): + if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE) or LIGHT_STATE_BRIGHTNESS in sysinfo: supported_features += SUPPORT_BRIGHTNESS if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP): supported_features += SUPPORT_COLOR_TEMP @@ -306,6 +309,7 @@ class TPLinkSmartBulb(Light): supported_features=supported_features, min_mireds=min_mireds, max_mireds=max_mireds, + has_emeter=has_emeter, ) def _get_light_state_retry(self) -> LightState: @@ -360,7 +364,7 @@ class TPLinkSmartBulb(Light): return self._light_state_from_params(self._get_device_state()) def _update_emeter(self): - if not self.smartbulb.has_emeter: + if not self._light_features.has_emeter: return now = dt_util.utcnow() @@ -446,10 +450,11 @@ class TPLinkSmartBulb(Light): if isinstance(self.smartbulb, SmartBulb): return self.smartbulb.get_light_state() + sysinfo = self.smartbulb.sys_info # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) return { - LIGHT_STATE_ON_OFF: self.smartbulb.state, - LIGHT_STATE_BRIGHTNESS: self.smartbulb.brightness, + LIGHT_STATE_ON_OFF: sysinfo[LIGHT_STATE_RELAY_STATE], + LIGHT_STATE_BRIGHTNESS: sysinfo.get(LIGHT_STATE_BRIGHTNESS, 0), LIGHT_STATE_COLOR_TEMP: 0, LIGHT_STATE_HUE: 0, LIGHT_STATE_SATURATION: 0, @@ -468,9 +473,12 @@ class TPLinkSmartBulb(Light): if state[LIGHT_STATE_BRIGHTNESS]: self.smartbulb.brightness = state[LIGHT_STATE_BRIGHTNESS] else: - self.smartbulb.state = 0 + self.smartbulb.state = self.smartbulb.SWITCH_STATE_OFF elif LIGHT_STATE_ON_OFF in state: - self.smartbulb.state = state[LIGHT_STATE_ON_OFF] + if state[LIGHT_STATE_ON_OFF]: + self.smartbulb.state = self.smartbulb.SWITCH_STATE_ON + else: + self.smartbulb.state = self.smartbulb.SWITCH_STATE_OFF return self._get_device_state() diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 09c23c6f0e5..27d00024706 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -49,7 +49,6 @@ class SmartSwitchMockData(NamedTuple): """Mock smart switch data.""" sys_info: dict - light_state: dict state_mock: Mock brightness_mock: Mock get_sysinfo_mock: Mock @@ -188,28 +187,28 @@ def dimmer_switch_mock_data_fixture() -> None: "model": "HS220", "alias": "dimmer1", "feature": ":", - } - - light_state = { - "on_off": 1, + "relay_state": 1, "brightness": 13, } def state(*args, **kwargs): - nonlocal light_state + nonlocal sys_info if len(args) == 0: - return light_state["on_off"] - light_state["on_off"] = args[0] + return sys_info["relay_state"] + if args[0] == "ON": + sys_info["relay_state"] = 1 + else: + sys_info["relay_state"] = 0 def brightness(*args, **kwargs): - nonlocal light_state + nonlocal sys_info if len(args) == 0: - return light_state["brightness"] - if light_state["brightness"] == 0: - light_state["on_off"] = 0 + return sys_info["brightness"] + if sys_info["brightness"] == 0: + sys_info["relay_state"] = 0 else: - light_state["on_off"] = 1 - light_state["brightness"] = args[0] + sys_info["relay_state"] = 1 + sys_info["brightness"] = args[0] get_sysinfo_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", @@ -228,7 +227,6 @@ def dimmer_switch_mock_data_fixture() -> None: with brightness_patch as brightness_mock, state_patch as state_mock, get_sysinfo_patch as get_sysinfo_mock: yield SmartSwitchMockData( sys_info=sys_info, - light_state=light_state, brightness_mock=brightness_mock, state_mock=state_mock, get_sysinfo_mock=get_sysinfo_mock, @@ -247,7 +245,7 @@ async def test_smartswitch( hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData ) -> None: """Test function.""" - light_state = dimmer_switch_mock_data.light_state + sys_info = dimmer_switch_mock_data.sys_info await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() @@ -276,7 +274,7 @@ async def test_smartswitch( await update_entity(hass, "light.dimmer1") assert hass.states.get("light.dimmer1").state == "off" - assert light_state["on_off"] == 0 + assert sys_info["relay_state"] == 0 await hass.services.async_call( LIGHT_DOMAIN, @@ -290,7 +288,7 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 48.45 - assert light_state["on_off"] == 1 + assert sys_info["relay_state"] == 1 await hass.services.async_call( LIGHT_DOMAIN, @@ -304,10 +302,10 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 53.55 - assert light_state["brightness"] == 21 + assert sys_info["brightness"] == 21 - light_state["on_off"] = 0 - light_state["brightness"] = 66 + sys_info["relay_state"] = 0 + sys_info["brightness"] = 66 await hass.services.async_call( LIGHT_DOMAIN, @@ -330,7 +328,7 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 168.3 - assert light_state["brightness"] == 66 + assert sys_info["brightness"] == 66 async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: From 3100e852cef18f113423c2e576a0a78bf91eaf30 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 17:23:21 -0700 Subject: [PATCH 29/62] Bumped version to 0.108.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9413cf6e4d..fb09e467c47 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 = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 8259a5a71ffbb95cb110b6ed88ebef8d34aa48c2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 9 Apr 2020 19:44:04 -0500 Subject: [PATCH 30/62] Guard IPP against negative ink levels (#33931) --- homeassistant/components/ipp/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 1ce162500c5..fd278d3df2e 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -116,7 +116,12 @@ class IPPMarkerSensor(IPPSensor): @property def state(self) -> Union[None, str, int, float]: """Return the state of the sensor.""" - return self.coordinator.data.markers[self.marker_index].level + level = self.coordinator.data.markers[self.marker_index].level + + if level >= 0: + return level + + return None class IPPPrinterSensor(IPPSensor): From 3331b81b64fc910a88d7db5cfa1cd602cc9761a9 Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Thu, 9 Apr 2020 12:37:53 -0700 Subject: [PATCH 31/62] Remove print() from Bayesian Binary Sensor (#33916) --- homeassistant/components/bayesian/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index b922c966ff5..74a0aaae295 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -298,8 +298,6 @@ class BayesianBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - print(self.current_observations) - print(self.observations_by_entity) return { ATTR_OBSERVATIONS: list(self.current_observations.values()), ATTR_OCCURRED_OBSERVATION_ENTITIES: list( From ab35ceab5a035dec225693485acd2ba2a75a8c18 Mon Sep 17 00:00:00 2001 From: Minims Date: Fri, 10 Apr 2020 17:44:56 +0200 Subject: [PATCH 32/62] Fix Onvif Camera that does not have SnapshotUri such as Sricam (#33902) --- homeassistant/components/onvif/camera.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index a0bfbab9b4f..6ec06a10988 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -411,8 +411,11 @@ class ONVIFHassCamera(Camera): req = media_service.create_type("GetSnapshotUri") req.ProfileToken = profiles[self._profile_index].token - snapshot_uri = await media_service.GetSnapshotUri(req) - self._snapshot = snapshot_uri.Uri + try: + snapshot_uri = await media_service.GetSnapshotUri(req) + self._snapshot = snapshot_uri.Uri + except ServerDisconnectedError as err: + _LOGGER.debug("Camera does not support GetSnapshotUri: %s", err) _LOGGER.debug( "ONVIF Camera Using the following URL for %s snapshot: %s", From b620c53662406ed20af38054e1a63ebab1a39e4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2020 07:42:34 -0500 Subject: [PATCH 33/62] Make homekit aware of DEVICE_CLASS_GATE (#33936) --- homeassistant/components/homekit/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c46bd754319..4fdad670f09 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -6,6 +6,7 @@ from zlib import adler32 import voluptuous as vol from homeassistant.components import cover +from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -200,7 +201,7 @@ def get_accessory(hass, driver, state, aid, config): device_class = state.attributes.get(ATTR_DEVICE_CLASS) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if device_class == "garage" and features & ( + if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE ): a_type = "GarageDoorOpener" From 9bc04d7b5ca875a24ae68241550f7421223b3d79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2020 11:33:58 -0500 Subject: [PATCH 34/62] Fix powerwall units (kW) (#33954) * Fix powerwall units (kW) * Fix test Co-authored-by: Paulus Schoutsen --- .../components/powerwall/binary_sensor.py | 2 +- homeassistant/components/powerwall/const.py | 6 ++--- homeassistant/components/powerwall/sensor.py | 6 ++--- tests/components/powerwall/test_sensor.py | 23 +++++++++++-------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 329b26221b8..3b73caecacd 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -129,7 +129,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): @property def is_on(self): - """Get the current value in kWh.""" + """Grid is online.""" return ( self._coordinator.data[POWERWALL_API_GRID_STATUS] == POWERWALL_GRID_ONLINE ) diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 2e9c3739c48..d05e42f6bf7 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -2,12 +2,10 @@ DOMAIN = "powerwall" -POWERWALL_SITE_NAME = "site_name" - POWERWALL_OBJECT = "powerwall" POWERWALL_COORDINATOR = "coordinator" -UPDATE_INTERVAL = 60 +UPDATE_INTERVAL = 30 ATTR_REGION = "region" ATTR_GRID_CODE = "grid_code" @@ -46,3 +44,5 @@ POWERWALL_RUNNING_KEY = "running" MODEL = "PowerWall 2" MANUFACTURER = "Tesla" + +ENERGY_KILO_WATT = "kW" diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index cf49b36a570..72dbd38a418 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -4,7 +4,6 @@ import logging from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, UNIT_PERCENTAGE, ) @@ -14,6 +13,7 @@ from .const import ( ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, DOMAIN, + ENERGY_KILO_WATT, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, POWERWALL_API_METERS, @@ -87,7 +87,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return ENERGY_KILO_WATT_HOUR + return ENERGY_KILO_WATT @property def name(self): @@ -106,7 +106,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def state(self): - """Get the current value in kWh.""" + """Get the current value in kW.""" meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] return round(float(meter.instant_power / 1000), 3) diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 7f092683b7c..5d21a11d4b4 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -39,13 +39,14 @@ async def test_sensors(hass): "energy_exported": 10429451.9916853, "energy_imported": 4824191.60668611, "instant_average_voltage": 120.650001525879, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Site Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_load_now") assert state.state == "1.971" @@ -54,13 +55,14 @@ async def test_sensors(hass): "energy_exported": 1056797.48917483, "energy_imported": 4692987.91889705, "instant_average_voltage": 120.650001525879, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Load Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_battery_now") assert state.state == "-8.55" @@ -69,13 +71,14 @@ async def test_sensors(hass): "energy_exported": 3620010, "energy_imported": 4216170, "instant_average_voltage": 240.56, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Battery Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_solar_now") assert state.state == "10.49" @@ -84,13 +87,14 @@ async def test_sensors(hass): "energy_exported": 9864205.82222448, "energy_imported": 28177.5358355867, "instant_average_voltage": 120.685001373291, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Solar Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_charge") assert state.state == "47.32" @@ -101,4 +105,5 @@ async def test_sensors(hass): } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value From bc2ac65b1e52e9a45fec0750057ca139c8530f7b Mon Sep 17 00:00:00 2001 From: Knapoc Date: Fri, 10 Apr 2020 19:34:10 +0200 Subject: [PATCH 35/62] Fix turning off/on light groups in homekit (#33965) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index b0c49a58a6a..d80fd3c5338 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,6 +2,6 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.1"], + "requirements": ["HAP-python==2.8.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 0a121a00178..d43dd112fd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.1 +HAP-python==2.8.2 # homeassistant.components.mastodon Mastodon.py==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f64d2f7c948..c284cb2cc3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.1 +HAP-python==2.8.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 82df4a3a4d9c967430ed03be67f91131f119f6ca Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 10 Apr 2020 12:49:58 -0500 Subject: [PATCH 36/62] Update pyipp to 0.9.2 (#33967) * Update manifest.json * Update requirements_test_all.txt * Update requirements_all.txt --- 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 4be57f13fbb..0cb7c108b63 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.1"], + "requirements": ["pyipp==0.9.2"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index d43dd112fd0..042e7bb46f4 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.1 +pyipp==0.9.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c284cb2cc3b..d8dd32b9a70 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.1 +pyipp==0.9.2 # homeassistant.components.iqvia pyiqvia==0.2.1 From 2243855209c2ce1f24a8a1911162169d4a7fb564 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2020 14:11:25 -0500 Subject: [PATCH 37/62] Handle 304 http responses in nexia (#33972) * Bump nexia to 0.8.1 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e69ea352c8e..a6ea270fb5b 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.8.0"], + "requirements": ["nexia==0.8.1"], "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 042e7bb46f4..2f1f566ea8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.8.0 +nexia==0.8.1 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8dd32b9a70..4ff29f7ff5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.8.0 +nexia==0.8.1 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From eb17b68ad3943d1c7c99dcff12990e64c0d74a37 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 10 Apr 2020 22:04:50 +0200 Subject: [PATCH 38/62] Fix shutdown timeout and make it upstream with Supervisor (#33973) * Fix shutdown timeout and make it upstream with Supervisor * Moved ENV command up * Update finish Co-authored-by: Franck Nijhof --- Dockerfile | 6 +++++- rootfs/etc/services.d/home-assistant/finish | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 647c2b8ac07..4646e9f01f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,15 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +ENV \ + S6_SERVICES_GRACETIME=60000 + WORKDIR /usr/src ## Setup Home Assistant COPY . homeassistant/ -RUN pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ +RUN \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 3afed0ca8d8..d039fc04c86 100644 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -1,7 +1,8 @@ -#!/usr/bin/execlineb -S0 +#!/usr/bin/execlineb -S1 # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== if { s6-test ${1} -ne 100 } +if { s6-test ${1} -ne 256 } -s6-svscanctl -t /var/run/s6/services \ No newline at end of file +s6-svscanctl -t /var/run/s6/services From da3ee0aa611bdea8c77afcde091796db0c774db8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 10 Apr 2020 16:17:48 -0400 Subject: [PATCH 39/62] Cleanup ZHA group entity lifecycle (#33977) * Clean up ZHA group entity lifecycle * group entities don't use state restore * add tests --- homeassistant/components/zha/__init__.py | 2 + homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 10 +++- .../components/zha/core/discovery.py | 25 +++++++++- homeassistant/components/zha/core/gateway.py | 50 +++++++++++++++---- homeassistant/components/zha/entity.py | 49 +++++++++++------- tests/components/zha/test_light.py | 18 +++++++ 7 files changed, 125 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2af35e8fb92..9e59b63adb4 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -32,6 +32,7 @@ from .core.const import ( SIGNAL_ADD_ENTITIES, RadioType, ) +from .core.discovery import GROUP_PROBE DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) @@ -138,6 +139,7 @@ async def async_unload_entry(hass, config_entry): """Unload ZHA config entry.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() + GROUP_PROBE.cleanup() api.async_unload_api(hass) dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, []) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index da151f67dbb..fe139a8239b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -208,6 +208,7 @@ SIGNAL_SET_LEVEL = "set_level" SIGNAL_STATE_ATTR = "update_state_attribute" SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" SIGNAL_REMOVE_GROUP = "remove_group" +SIGNAL_GROUP_ENTITY_REMOVED = "group_entity_removed" SIGNAL_GROUP_MEMBERSHIP_CHANGE = "group_membership_change" UNKNOWN = "unknown" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index ad3d1ff18ad..0215858721f 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -551,7 +551,15 @@ class ZHADevice(LogMixin): async def async_remove_from_group(self, group_id): """Remove this device from the provided zigbee group.""" - await self._zigpy_device.remove_from_group(group_id) + try: + await self._zigpy_device.remove_from_group(group_id) + except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove device '%s' from group: 0x%04x ex: %s", + self._zigpy_device.ieee, + group_id, + str(ex), + ) async def async_bind_to_group(self, group_id, cluster_bindings): """Directly bind this device to a group for the given clusters.""" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 90ec0e6e250..4540c9158de 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,7 +6,10 @@ from typing import Callable, List, Tuple from homeassistant import const as ha_const from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import HomeAssistantType @@ -166,10 +169,30 @@ class GroupProbe: def __init__(self): """Initialize instance.""" self._hass = None + self._unsubs = [] def initialize(self, hass: HomeAssistantType) -> None: """Initialize the group probe.""" self._hass = hass + self._unsubs.append( + async_dispatcher_connect( + hass, zha_const.SIGNAL_GROUP_ENTITY_REMOVED, self._reprobe_group + ) + ) + + def cleanup(self): + """Clean up on when zha shuts down.""" + for unsub in self._unsubs[:]: + unsub() + self._unsubs.remove(unsub) + + def _reprobe_group(self, group_id: int) -> None: + """Reprobe a group for entities after its members change.""" + zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_group = zha_gateway.groups.get(group_id) + if zha_group is None: + return + self.discover_group_entities(zha_group) @callback def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e032de4d94c..e97e2185dc5 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -20,7 +20,10 @@ from homeassistant.helpers.device_registry import ( async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry as get_ent_reg, +) from . import discovery, typing as zha_typing from .const import ( @@ -77,7 +80,7 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import ZHAGroup from .patches import apply_application_controller_patch -from .registries import RADIO_TYPES +from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES from .store import async_get_registry from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType @@ -273,6 +276,9 @@ class ZHAGateway: async_dispatcher_send( self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) + if len(zha_group.members) == 2: + # we need to do this because there wasn't already a group entity to remove and re-add + discovery.GROUP_PROBE.discover_group_entities(zha_group) def group_added(self, zigpy_group: ZigpyGroupType) -> None: """Handle zigpy group added event.""" @@ -289,6 +295,7 @@ class ZHAGateway: async_dispatcher_send( self._hass, f"{SIGNAL_REMOVE_GROUP}_0x{zigpy_group.group_id:04x}" ) + self._cleanup_group_entity_registry_entries(zigpy_group) def _send_group_gateway_message( self, zigpy_group: ZigpyGroupType, gateway_message_type: str @@ -368,6 +375,35 @@ class ZHAGateway: e for e in entity_refs if e.reference_id != entity.entity_id ] + def _cleanup_group_entity_registry_entries( + self, zigpy_group: ZigpyGroupType + ) -> None: + """Remove entity registry entries for group entities when the groups are removed from HA.""" + # first we collect the potential unique ids for entities that could be created from this group + possible_entity_unique_ids = [ + f"{domain}_zha_group_0x{zigpy_group.group_id:04x}" + for domain in GROUP_ENTITY_DOMAINS + ] + + # then we get all group entity entries tied to the coordinator + all_group_entity_entries = async_entries_for_device( + self.ha_entity_registry, self.coordinator_zha_device.device_id + ) + + # then we get the entity entries for this specific group by getting the entries that match + entries_to_remove = [ + entry + for entry in all_group_entity_entries + if entry.unique_id in possible_entity_unique_ids + ] + + # then we remove the entries from the entity registry + for entry in entries_to_remove: + _LOGGER.debug( + "cleaning up entity registry entry for entity: %s", entry.entity_id + ) + self.ha_entity_registry.async_remove(entry.entity_id) + @property def devices(self): """Return devices.""" @@ -557,15 +593,7 @@ class ZHAGateway: ) tasks.append(self.devices[ieee].async_add_to_group(group_id)) await asyncio.gather(*tasks) - zha_group = self.groups.get(group_id) - _LOGGER.debug( - "Probing group: %s:0x%04x for entity discovery", - zha_group.name, - zha_group.group_id, - ) - discovery.GROUP_PROBE.discover_group_entities(zha_group) - - return zha_group + return self.groups.get(group_id) async def async_remove_zigpy_group(self, group_id: int) -> None: """Remove a Zigbee group from Zigpy.""" diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 2d098d60bfb..0f44d7efa82 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -8,7 +8,10 @@ from typing import Any, Awaitable, Dict, List, Optional from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity @@ -19,6 +22,7 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, + SIGNAL_GROUP_ENTITY_REMOVED, SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, SIGNAL_REMOVE_GROUP, @@ -32,7 +36,7 @@ ENTITY_SUFFIX = "entity_suffix" RESTART_GRACE_PERIOD = 7200 # 2 hours -class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): +class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs): @@ -112,7 +116,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): @callback def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: """Set the entity state.""" - pass async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -136,7 +139,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): @callback def async_restore_last_state(self, last_state) -> None: """Restore previous state.""" - pass async def async_accept_signal( self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False @@ -158,7 +160,7 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): _LOGGER.log(level, msg, *args) -class ZhaEntity(BaseZhaEntity): +class ZhaEntity(BaseZhaEntity, RestoreEntity): """A base class for non group ZHA entities.""" def __init__( @@ -181,6 +183,13 @@ class ZhaEntity(BaseZhaEntity): async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() + self.remove_future = asyncio.Future() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE}_{self.zha_device.ieee}", + self.async_remove, + signal_override=True, + ) await self.async_check_recently_seen() await self.async_accept_signal( None, @@ -197,6 +206,16 @@ class ZhaEntity(BaseZhaEntity): self.remove_future, ) + async def async_will_remove_from_hass(self) -> None: + """Disconnect entity object when removed.""" + await super().async_will_remove_from_hass() + self.zha_device.gateway.remove_entity_reference(self) + self.remove_future.set_result(True) + + @callback + def async_restore_last_state(self, last_state) -> None: + """Restore previous state.""" + async def async_check_recently_seen(self) -> None: """Check if the device was seen within the last 2 hours.""" last_state = await self.async_get_last_state() @@ -246,13 +265,20 @@ class ZhaGroupEntity(BaseZhaEntity): await self.async_accept_signal( None, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", - self._update_group_entities, + self.async_remove, signal_override=True, ) self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, self.async_state_changed_listener ) + + def send_removed_signal(): + async_dispatcher_send( + self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id + ) + + self.async_on_remove(send_removed_signal) await self.async_update() @callback @@ -262,17 +288,6 @@ class ZhaGroupEntity(BaseZhaEntity): """Handle child updates.""" self.async_schedule_update_ha_state(True) - def _update_group_entities(self): - """Update tracked entities when membership changes.""" - group = self.zha_device.gateway.get_group(self._group_id) - self._entity_ids = group.get_domain_entity_ids(self.platform.domain) - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, self.async_state_changed_listener - ) - async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" await super().async_will_remove_from_hass() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 9bdd4966a4a..f297bfa5bf0 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -539,3 +539,21 @@ async def async_test_zha_group_light_entity( await zha_group.async_add_members([device_light_3.ieee]) await dev3_cluster_on_off.on() assert hass.states.get(entity_id).state == STATE_ON + + # make the group have only 1 member and now there should be no entity + await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee]) + assert len(zha_group.members) == 1 + assert hass.states.get(entity_id).state is None + # make sure the entity registry entry is still there + assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + + # add a member back and ensure that the group entity was created again + await zha_group.async_add_members([device_light_3.ieee]) + await dev3_cluster_on_off.on() + assert hass.states.get(entity_id).state == STATE_ON + + # remove the group and ensure that there is no entity and that the entity registry is cleaned up + assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + await zha_gateway.async_remove_zigpy_group(zha_group.group_id) + assert hass.states.get(entity_id).state is None + assert zha_gateway.ha_entity_registry.async_get(entity_id) is None From d90a3b6c4202282b3b05cf8eb4db1a5857a62b03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2020 15:56:40 -0500 Subject: [PATCH 40/62] Exclude non thermostats from being detected by nexia (#33979) * Fix detection of emergency heat * Bump nexia to 0.8.2 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index a6ea270fb5b..2102a2a8225 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.8.1"], + "requirements": ["nexia==0.8.2"], "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 2f1f566ea8f..3d4d5ad1394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.8.1 +nexia==0.8.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ff29f7ff5f..9e128f83ca1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.8.1 +nexia==0.8.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From c254b7155906ac975b298f0231720918c8ed4290 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Apr 2020 14:12:12 -0700 Subject: [PATCH 41/62] Bumped version to 0.108.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fb09e467c47..17af8a6e320 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 = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From d4dc7f806c6eddbe3662128f90d19337cdb5010d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Apr 2020 14:39:37 -0700 Subject: [PATCH 42/62] Fix cherry pick ZHA --- homeassistant/components/zha/entity.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 0f44d7efa82..d28bc622bbe 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -136,10 +136,6 @@ class BaseZhaEntity(LogMixin, entity.Entity): self.zha_device.gateway.remove_entity_reference(self) self.remove_future.set_result(True) - @callback - def async_restore_last_state(self, last_state) -> None: - """Restore previous state.""" - async def async_accept_signal( self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False ): From f00f3d6b0cabe0943ea8d1aa42cdf0da62d1532d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 10 Apr 2020 17:23:07 -0500 Subject: [PATCH 43/62] Use zeroconf UUID if not available via IPP properties (#33991) --- homeassistant/components/ipp/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index fe0808414ad..b7239c8bf49 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -116,7 +116,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("IPP Parse Error") return self.async_abort(reason="parse_error") - self.discovery_info[CONF_UUID] = info[CONF_UUID] + if info[CONF_UUID] is not None: + self.discovery_info[CONF_UUID] = info[CONF_UUID] await self.async_set_unique_id(self.discovery_info[CONF_UUID]) self._abort_if_unique_id_configured( From bb60286ed950bc9d155553346e842fbd0765c083 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 11 Apr 2020 16:47:07 +0200 Subject: [PATCH 44/62] Fix #33995 Use "now" if departure is None (#34017) --- homeassistant/components/here_travel_time/sensor.py | 3 +++ tests/components/here_travel_time/test_sensor.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index c88aeb8e5a0..f73d3bccaa6 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -424,6 +424,9 @@ class HERETravelTimeData: if departure is not None: departure = convert_time_to_isodate(departure) + if departure is None and arrival is None: + departure = "now" + _LOGGER.debug( "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", origin, diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 642b774f1e5..d399f5b67aa 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -80,6 +80,8 @@ def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival parameters["arrival"] = arrival if departure is not None: parameters["departure"] = departure + if departure is None and arrival is None: + parameters["departure"] = "now" url = base_url + urllib.parse.urlencode(parameters) print(url) return url From 908e044db1d3ef9ab2c10a88c7aaf6df63fd274d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Apr 2020 08:25:57 -0500 Subject: [PATCH 45/62] Fix nexia fan and hold modes for XL824 thermostats (#34042) * Fix nexia fan and hold modes for XL824 thermostats * Update nexia to 0.9.0 * Update tests to reflect the modes that now come directly in --- homeassistant/components/nexia/climate.py | 3 +-- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nexia/test_climate.py | 8 ++++---- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 8af1be20b1e..d7e93e511fb 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -2,7 +2,6 @@ import logging from nexia.const import ( - FAN_MODES, OPERATION_MODE_AUTO, OPERATION_MODE_COOL, OPERATION_MODE_HEAT, @@ -192,7 +191,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice): @property def fan_modes(self): """Return the list of available fan modes.""" - return FAN_MODES + return self._thermostat.get_fan_modes() @property def min_temp(self): diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 2102a2a8225..a58330ad227 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.8.2"], + "requirements": ["nexia==0.9.1"], "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 3d4d5ad1394..cfe84803c54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.8.2 +nexia==0.9.1 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e128f83ca1..531b2550dea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.8.2 +nexia==0.9.1 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 7f3ed900d3c..fe47ceeffe4 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -18,8 +18,8 @@ async def test_climate_zones(hass): "current_temperature": 22.8, "dehumidify_setpoint": 45.0, "dehumidify_supported": True, - "fan_mode": "auto", - "fan_modes": ["auto", "on", "circulate"], + "fan_mode": "Auto", + "fan_modes": ["Auto", "On", "Circulate"], "friendly_name": "Nick Office", "humidify_supported": False, "humidity": 45.0, @@ -53,8 +53,8 @@ async def test_climate_zones(hass): "current_temperature": 25.0, "dehumidify_setpoint": 50.0, "dehumidify_supported": True, - "fan_mode": "auto", - "fan_modes": ["auto", "on", "circulate"], + "fan_mode": "Auto", + "fan_modes": ["Auto", "On", "Circulate"], "friendly_name": "Kitchen", "humidify_supported": False, "humidity": 50.0, From fc286900d3c3792d63e9a74a6246626cc26400ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Apr 2020 17:29:35 -0500 Subject: [PATCH 46/62] Handle all zero serial numbers in NUT (#34045) * Handle all zero serial numbers in NUT * Add additional nut tests * Update homeassistant/components/nut/__init__.py Co-Authored-By: Paulus Schoutsen * remove re Co-authored-by: Paulus Schoutsen --- homeassistant/components/nut/__init__.py | 2 +- tests/fixtures/nut/BACKUPSES600M1.json | 47 ++++++++++++++++++++++++ tests/fixtures/nut/CP1500PFCLCD.json | 43 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/nut/BACKUPSES600M1.json create mode 100644 tests/fixtures/nut/CP1500PFCLCD.json diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index a990cdf94b8..b8561dde303 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -109,7 +109,7 @@ def _firmware_from_status(status): def _serial_from_status(status): """Find the best serialvalue from the status.""" serial = status.get("device.serial") or status.get("ups.serial") - if serial and serial == "unknown": + if serial and (serial.lower() == "unknown" or serial.count("0") == len(serial)): return None return serial diff --git a/tests/fixtures/nut/BACKUPSES600M1.json b/tests/fixtures/nut/BACKUPSES600M1.json new file mode 100644 index 00000000000..1acd0ef0444 --- /dev/null +++ b/tests/fixtures/nut/BACKUPSES600M1.json @@ -0,0 +1,47 @@ +{ + "ups.realpower.nominal" : "330", + "input.voltage" : "123.0", + "ups.mfr" : "American Power Conversion", + "driver.version" : "2.7.4", + "ups.test.result" : "No test initiated", + "input.voltage.nominal" : "120", + "input.transfer.low" : "92", + "driver.parameter.pollinterval" : "15", + "driver.version.data" : "APC HID 0.96", + "driver.parameter.pollfreq" : "30", + "battery.mfr.date" : "2017/04/01", + "ups.beeper.status" : "enabled", + "battery.date" : "2001/09/25", + "driver.name" : "usbhid-ups", + "battery.charge" : "100", + "ups.status" : "OL", + "ups.model" : "Back-UPS ES 600M1", + "battery.runtime.low" : "120", + "ups.firmware" : "928.a5 .D", + "ups.delay.shutdown" : "20", + "device.model" : "Back-UPS ES 600M1", + "device.serial" : "4B1713P32195 ", + "input.sensitivity" : "medium", + "ups.firmware.aux" : "a5 ", + "input.transfer.reason" : "input voltage out of range", + "ups.timer.reboot" : "0", + "battery.voltage.nominal" : "12.0", + "ups.vendorid" : "051d", + "input.transfer.high" : "139", + "battery.voltage" : "13.7", + "battery.charge.low" : "10", + "battery.type" : "PbAc", + "ups.mfr.date" : "2017/04/01", + "ups.timer.shutdown" : "-1", + "device.mfr" : "American Power Conversion", + "driver.parameter.port" : "auto", + "battery.charge.warning" : "50", + "device.type" : "ups", + "driver.parameter.vendorid" : "051d", + "ups.serial" : "4B1713P32195 ", + "ups.load" : "22", + "driver.version.internal" : "0.41", + "battery.runtime" : "1968", + "driver.parameter.synchronous" : "no", + "ups.productid" : "0002" +} diff --git a/tests/fixtures/nut/CP1500PFCLCD.json b/tests/fixtures/nut/CP1500PFCLCD.json new file mode 100644 index 00000000000..8f12ae96df6 --- /dev/null +++ b/tests/fixtures/nut/CP1500PFCLCD.json @@ -0,0 +1,43 @@ +{ + "battery.runtime.low" : "300", + "driver.parameter.port" : "auto", + "ups.delay.shutdown" : "20", + "driver.parameter.pollfreq" : "30", + "ups.beeper.status" : "disabled", + "input.voltage.nominal" : "120", + "device.serial" : "000000000000", + "ups.timer.shutdown" : "-60", + "input.voltage" : "122.0", + "ups.status" : "OL", + "ups.model" : "CP1500PFCLCD", + "device.mfr" : "CPS", + "device.model" : "CP1500PFCLCD", + "input.transfer.low" : "88", + "battery.mfr.date" : "CPS", + "driver.version" : "2.7.4", + "driver.version.data" : "CyberPower HID 0.4", + "driver.parameter.synchronous" : "no", + "ups.realpower.nominal" : "900", + "ups.productid" : "0501", + "ups.mfr" : "CPS", + "ups.vendorid" : "0764", + "driver.version.internal" : "0.41", + "output.voltage" : "138.0", + "battery.runtime" : "10530", + "device.type" : "ups", + "battery.charge.low" : "10", + "ups.timer.start" : "-60", + "driver.parameter.pollinterval" : "15", + "ups.load" : "0", + "ups.serial" : "000000000000", + "input.transfer.high" : "139", + "battery.charge.warning" : "20", + "battery.voltage.nominal" : "24", + "driver.parameter.vendorid" : "0764", + "driver.name" : "usbhid-ups", + "battery.type" : "PbAcid", + "ups.delay.start" : "30", + "battery.voltage" : "24.0", + "battery.charge" : "100", + "ups.test.result" : "No test initiated" +} From 4337dd68645dbbd23e3bb787a107e98e50a42249 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 12 Apr 2020 16:18:44 +0200 Subject: [PATCH 47/62] UniFi - Fix unit of measurement from B to MB (#34091) --- homeassistant/components/unifi/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 1b6667f2e80..2e82ecb4f6f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.unifi.config_flow import get_controller_from_config_entry -from homeassistant.const import DATA_BYTES +from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -116,7 +116,7 @@ class UniFiRxBandwidthSensor(UniFiClient): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return DATA_BYTES + return DATA_MEGABYTES class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor): From 667a87988d168a4dbd9b0d86267b445d91f1460b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 13 Apr 2020 01:53:01 +0200 Subject: [PATCH 48/62] Fix Daikin sensor temperature_unit & cleanup (#34116) --- homeassistant/components/daikin/sensor.py | 31 +++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index e3e2e6a0f27..2982abd261c 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -1,17 +1,11 @@ """Support for Daikin AC sensors.""" import logging -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_ICON, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.util.unit_system import UnitSystem from . import DOMAIN as DAIKIN_DOMAIN -from .const import ( - ATTR_INSIDE_TEMPERATURE, - ATTR_OUTSIDE_TEMPERATURE, - SENSOR_TYPE_TEMPERATURE, - SENSOR_TYPES, -) +from .const import ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -31,30 +25,19 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [ATTR_INSIDE_TEMPERATURE] if daikin_api.device.support_outside_temperature: sensors.append(ATTR_OUTSIDE_TEMPERATURE) - async_add_entities( - [ - DaikinClimateSensor(daikin_api, sensor, hass.config.units) - for sensor in sensors - ] - ) + async_add_entities([DaikinClimateSensor(daikin_api, sensor) for sensor in sensors]) class DaikinClimateSensor(Entity): """Representation of a Sensor.""" - def __init__(self, api, monitored_state, units: UnitSystem, name=None) -> None: + def __init__(self, api, monitored_state) -> None: """Initialize the sensor.""" self._api = api - self._sensor = SENSOR_TYPES.get(monitored_state) - if name is None: - name = f"{self._sensor[CONF_NAME]} {api.name}" - - self._name = f"{name} {monitored_state.replace('_', ' ')}" + self._sensor = SENSOR_TYPES[monitored_state] + self._name = f"{api.name} {self._sensor[CONF_NAME]}" self._device_attribute = monitored_state - if self._sensor[CONF_TYPE] == SENSOR_TYPE_TEMPERATURE: - self._unit_of_measurement = units.temperature_unit - @property def unique_id(self): """Return a unique ID.""" @@ -82,7 +65,7 @@ class DaikinClimateSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return self._unit_of_measurement + return TEMP_CELSIUS async def async_update(self): """Retrieve latest state.""" From e742711a76cce032b189ebce5319ff857dbd10bc Mon Sep 17 00:00:00 2001 From: James Nimmo Date: Mon, 13 Apr 2020 15:18:27 +1200 Subject: [PATCH 49/62] Bump pyIntesisHome to 1.7.3 (#34125) --- homeassistant/components/intesishome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index f1647f5d97e..e38e1a7dd3b 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/intesishome", "dependencies": [], "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.7.1"] + "requirements": ["pyintesishome==1.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index cfe84803c54..36c54eab31b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1330,7 +1330,7 @@ pyialarm==0.3 pyicloud==0.9.6.1 # homeassistant.components.intesishome -pyintesishome==1.7.1 +pyintesishome==1.7.3 # homeassistant.components.ipma pyipma==2.0.5 From 87504806b1d98c8587d5543ac25b35ddc4c56f2a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 13 Apr 2020 15:32:23 -0600 Subject: [PATCH 50/62] Fix deprecated icon/username logic in Slack (#34156) * Fix deprecated icon/username logic in Slack * hassfest --- homeassistant/components/slack/notify.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index fe6f7ab0d26..8cfffc1722a 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -74,11 +74,7 @@ class SlackNotificationService(BaseNotificationService): self._default_channel = default_channel self._hass = hass self._icon = icon - - if username or self._icon: - self._as_user = False - else: - self._as_user = True + self._username = username async def _async_send_local_file_message(self, path, targets, message, title): """Upload a local file (with message) to Slack.""" @@ -108,11 +104,11 @@ class SlackNotificationService(BaseNotificationService): target: self._client.chat_postMessage( channel=target, text=message, - as_user=self._as_user, attachments=attachments, blocks=blocks, icon_emoji=self._icon, link_names=True, + username=self._username, ) for target in targets } From e365f807c11aa8d4fdd2d77a8aeef47a454ce9e0 Mon Sep 17 00:00:00 2001 From: Jason Swails Date: Mon, 13 Apr 2020 18:07:32 -0400 Subject: [PATCH 51/62] Improve rounding the light level conversion in Lutron Caseta (#34167) --- homeassistant/components/lutron_caseta/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index ba4342ecfce..350c35fffa8 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def to_lutron_level(level): """Convert the given Home Assistant light level (0-255) to Lutron (0-100).""" - return int((level * 100) // 255) + return int(round((level * 100) / 255)) def to_hass_level(level): From 2d8bb8e6d2b911a38c1f497802039e93eb3c31b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2020 17:51:35 -0500 Subject: [PATCH 52/62] Fix z-wave brightness off by one (#34170) Z-wave would drop the floating point by calling int() instead of round() which would result in the brightness being off by one in many cases. --- homeassistant/components/zwave/light.py | 2 +- tests/components/zwave/test_light.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index b32daf71f54..745400e5c44 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -104,7 +104,7 @@ def byte_to_zwave_brightness(value): `value` -- (int) Brightness byte value from 0-255. """ if value > 0: - return max(1, int((value / 255) * 99)) + return max(1, round((value / 255) * 99)) return 0 diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 10efed24bf2..fc62ef880f6 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -100,13 +100,23 @@ def test_dimmer_turn_on(mock_openzwave): node.reset_mock() + device.turn_on(**{ATTR_BRIGHTNESS: 224}) + + assert node.set_dimmer.called + value_id, brightness = node.set_dimmer.mock_calls[0][1] + + assert value_id == value.value_id + assert brightness == 87 # round(224 / 255 * 99) + + node.reset_mock() + device.turn_on(**{ATTR_BRIGHTNESS: 120}) assert node.set_dimmer.called value_id, brightness = node.set_dimmer.mock_calls[0][1] assert value_id == value.value_id - assert brightness == 46 # int(120 / 255 * 99) + assert brightness == 47 # round(120 / 255 * 99) with patch.object(light, "_LOGGER", MagicMock()) as mock_logger: device.turn_on(**{ATTR_TRANSITION: 35}) From 2553b0d1e028ab6a4deee664613a1ac0926293a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2020 18:16:17 -0500 Subject: [PATCH 53/62] =?UTF-8?q?Increase=20timeout=20and=20log=20the=20ur?= =?UTF-8?q?l=20of=20the=20elkm1=20system=20that=20time=E2=80=A6=20(#34172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Log the url of the elkm1 system that times out * Bump timeout to 120s --- homeassistant/components/elkm1/__init__.py | 6 ++++-- homeassistant/components/elkm1/config_flow.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 183897d306e..5c6fbf71738 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -39,7 +39,7 @@ from .const import ( ELK_ELEMENTS, ) -SYNC_TIMEOUT = 55 +SYNC_TIMEOUT = 120 _LOGGER = logging.getLogger(__name__) @@ -215,7 +215,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT): _LOGGER.error( - "Timed out after %d seconds while trying to sync with ElkM1", SYNC_TIMEOUT, + "Timed out after %d seconds while trying to sync with ElkM1 at %s", + SYNC_TIMEOUT, + conf[CONF_HOST], ) elk.disconnect() raise ConfigEntryNotReady diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index cad3ecac42a..c96e6e549c0 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -64,8 +64,9 @@ async def validate_input(data): timed_out = False if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT): _LOGGER.error( - "Timed out after %d seconds while trying to sync with elkm1", + "Timed out after %d seconds while trying to sync with ElkM1 at %s", VALIDATE_TIMEOUT, + url, ) timed_out = True From 4e3414fc8aa65451e569866bbca16bc0ae78afbe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Apr 2020 17:03:24 -0700 Subject: [PATCH 54/62] Bumped version to 0.108.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17af8a6e320..da6dd4bb24e 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 = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From d6e1bc3e75bec21b3a5289416bb694f20e7301a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2020 19:08:37 -0500 Subject: [PATCH 55/62] Convert sense to use DataUpdateCoordinator for trends data (#34160) * Convert sense to use DataUpdateCoordinator for trends * remove unused * request update right away * clarify * call async refresh later * Update homeassistant/components/sense/__init__.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/sense/__init__.py Co-Authored-By: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/sense/__init__.py | 18 +++- .../components/sense/binary_sensor.py | 16 ++-- homeassistant/components/sense/const.py | 1 + homeassistant/components/sense/sensor.py | 91 +++++++++---------- 4 files changed, 68 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 80e75bce400..f295b0d926c 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -17,6 +17,7 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( ACTIVE_UPDATE_RATE, @@ -27,6 +28,7 @@ from .const import ( SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, SENSE_TIMEOUT_EXCEPTIONS, + SENSE_TRENDS_COORDINATOR, ) _LOGGER = logging.getLogger(__name__) @@ -111,9 +113,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except SENSE_TIMEOUT_EXCEPTIONS: raise ConfigEntryNotReady + trends_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"Sense Trends {email}", + update_method=gateway.update_trend_data, + update_interval=timedelta(seconds=300), + ) + + # This can take longer than 60s and we already know + # sense is online since get_discovered_device_data was + # successful so we do it later. + hass.loop.create_task(trends_coordinator.async_request_refresh()) + hass.data[DOMAIN][entry.entry_id] = { SENSE_DATA: gateway, SENSE_DEVICES_DATA: sense_devices_data, + SENSE_TRENDS_COORDINATOR: trends_coordinator, SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } @@ -122,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, component) ) - async def async_sense_update(now): + async def async_sense_update(_): """Retrieve latest state.""" try: await gateway.update_realtime() diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 50fb3fd7dc7..af1b2f34b4a 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -71,7 +71,6 @@ class SenseDevice(BinarySensorDevice): self._unique_id = f"{sense_monitor_id}-{self._id}" self._icon = sense_to_mdi(device["icon"]) self._sense_devices_data = sense_devices_data - self._undo_dispatch_subscription = None self._state = None self._available = False @@ -117,17 +116,14 @@ class SenseDevice(BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" - self._undo_dispatch_subscription = async_dispatcher_connect( - self.hass, - f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", - self._async_update_from_data, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", + self._async_update_from_data, + ) ) - async def async_will_remove_from_hass(self): - """Undo subscription.""" - if self._undo_dispatch_subscription: - self._undo_dispatch_subscription() - @callback def _async_update_from_data(self): """Get the latest data, update state. Must not do I/O.""" diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index 882c3c9d79f..cd1d2bfcf4a 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -12,6 +12,7 @@ SENSE_DATA = "sense_data" SENSE_DEVICE_UPDATE = "sense_devices_update" SENSE_DEVICES_DATA = "sense_devices_data" SENSE_DISCOVERED_DEVICES_DATA = "sense_discovered_devices" +SENSE_TRENDS_COORDINATOR = "sense_trends_coorindator" ACTIVE_NAME = "Energy" ACTIVE_TYPE = "active" diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 06cfb90d2b5..0c6bfd7ee1f 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,12 +1,10 @@ """Support for monitoring a Sense energy sensor.""" -from datetime import timedelta import logging from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle from .const import ( ACTIVE_NAME, @@ -22,12 +20,9 @@ from .const import ( SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, - SENSE_TIMEOUT_EXCEPTIONS, + SENSE_TRENDS_COORDINATOR, ) -MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=300) - - _LOGGER = logging.getLogger(__name__) @@ -64,17 +59,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sense sensor.""" data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA] sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA] + trends_coordinator = hass.data[DOMAIN][config_entry.entry_id][ + SENSE_TRENDS_COORDINATOR + ] - @Throttle(MIN_TIME_BETWEEN_DAILY_UPDATES) - async def update_trends(): - """Update the daily power usage.""" - await data.update_trend_data() + # Request only in case it takes longer + # than 60s + await trends_coordinator.async_request_refresh() sense_monitor_id = data.sense_monitor_id sense_devices = hass.data[DOMAIN][config_entry.entry_id][ SENSE_DISCOVERED_DEVICES_DATA ] - await data.update_trend_data() devices = [ SenseEnergyDevice(sense_devices_data, device, sense_monitor_id) @@ -108,8 +104,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name, sensor_type, is_production, - update_trends, - var, + trends_coordinator, unique_id, ) ) @@ -140,7 +135,6 @@ class SenseActiveSensor(Entity): self._sensor_type = sensor_type self._is_production = is_production self._state = None - self._undo_dispatch_subscription = None @property def name(self): @@ -179,17 +173,14 @@ class SenseActiveSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self._undo_dispatch_subscription = async_dispatcher_connect( - self.hass, - f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", - self._async_update_from_data, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", + self._async_update_from_data, + ) ) - async def async_will_remove_from_hass(self): - """Undo subscription.""" - if self._undo_dispatch_subscription: - self._undo_dispatch_subscription() - @callback def _async_update_from_data(self): """Update the sensor from the data. Must not do I/O.""" @@ -206,7 +197,7 @@ class SenseTrendsSensor(Entity): """Implementation of a Sense energy sensor.""" def __init__( - self, data, name, sensor_type, is_production, update_call, sensor_id, unique_id + self, data, name, sensor_type, is_production, trends_coordinator, unique_id, ): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME @@ -215,10 +206,11 @@ class SenseTrendsSensor(Entity): self._available = False self._data = data self._sensor_type = sensor_type - self.update_sensor = update_call + self._coordinator = trends_coordinator self._is_production = is_production self._state = None self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._had_any_update = False @property def name(self): @@ -228,12 +220,12 @@ class SenseTrendsSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + return round(self._data.get_trend(self._sensor_type, self._is_production), 1) @property def available(self): - """Return the availability of the sensor.""" - return self._available + """Return if entity is available.""" + return self._had_any_update and self._coordinator.last_update_success @property def unit_of_measurement(self): @@ -250,18 +242,27 @@ class SenseTrendsSensor(Entity): """Return the unique id.""" return self._unique_id + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @callback + def _async_update(self): + """Track if we had an update so we do not report zero data.""" + self._had_any_update = True + self.async_write_ha_state() + async def async_update(self): - """Get the latest data, update state.""" + """Update the entity. - try: - await self.update_sensor() - except SENSE_TIMEOUT_EXCEPTIONS: - _LOGGER.error("Timeout retrieving data") - return + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() - state = self._data.get_trend(self._sensor_type, self._is_production) - self._state = round(state, 1) - self._available = True + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove(self._coordinator.async_add_listener(self._async_update)) class SenseEnergyDevice(Entity): @@ -276,7 +277,6 @@ class SenseEnergyDevice(Entity): self._unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}" self._icon = sense_to_mdi(device["icon"]) self._sense_devices_data = sense_devices_data - self._undo_dispatch_subscription = None self._state = None @property @@ -321,17 +321,14 @@ class SenseEnergyDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self._undo_dispatch_subscription = async_dispatcher_connect( - self.hass, - f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", - self._async_update_from_data, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", + self._async_update_from_data, + ) ) - async def async_will_remove_from_hass(self): - """Undo subscription.""" - if self._undo_dispatch_subscription: - self._undo_dispatch_subscription() - @callback def _async_update_from_data(self): """Get the latest data, update state. Must not do I/O.""" From 9142fa1aa683f5e4256fd4a2292c30208d12e086 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Apr 2020 12:37:57 +0200 Subject: [PATCH 56/62] Temporary transition Docker init (#34135) --- rootfs/init | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 rootfs/init diff --git a/rootfs/init b/rootfs/init new file mode 100755 index 00000000000..7bea7ed88a9 --- /dev/null +++ b/rootfs/init @@ -0,0 +1,23 @@ +#!/bin/execlineb -S0 + +## +## load default PATH (the same that Docker includes if not provided) if it doesn't exist, +## then go ahead with stage1. +## this was motivated due to this issue: +## - https://github.com/just-containers/s6-overlay/issues/108 +## + +/bin/importas -D /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH PATH +export PATH ${PATH} + +## +## Skip further init if the user has a given CMD. +## This is to prevent Home Assistant from starting twice if the user +## decided to override/start via the CMD. +## + +ifelse { s6-test $# -ne 0 } +{ + $@ +} +/etc/s6/init/init-stage1 $@ \ No newline at end of file From ca4814815023786ae5345fbbc65bcd69f6f76be3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 14 Apr 2020 13:30:41 -0500 Subject: [PATCH 57/62] Catch IPPVersionNotSupportedError in IPP (#34184) * Update config_flow.py * squash. * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py --- homeassistant/components/ipp/config_flow.py | 18 ++++- homeassistant/components/ipp/manifest.json | 2 +- homeassistant/components/ipp/strings.json | 2 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipp/test_config_flow.py | 70 +++++++++++++++++- .../get-printer-attributes-error-0x0503.bin | Bin 0 -> 75 bytes 7 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index b7239c8bf49..32474881a87 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -6,8 +6,10 @@ from pyipp import ( IPP, IPPConnectionError, IPPConnectionUpgradeRequired, + IPPError, IPPParseError, IPPResponseError, + IPPVersionNotSupportedError, ) import voluptuous as vol @@ -70,10 +72,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): except IPPConnectionUpgradeRequired: return self._show_setup_form({"base": "connection_upgrade"}) except (IPPConnectionError, IPPResponseError): + _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self._show_setup_form({"base": "connection_error"}) except IPPParseError: - _LOGGER.exception("IPP Parse Error") + _LOGGER.debug("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") + except IPPVersionNotSupportedError: + return self.async_abort(reason="ipp_version_error") + except IPPError: + _LOGGER.debug("IPP Error", exc_info=True) + return self.async_abort(reason="ipp_error") user_input[CONF_UUID] = info[CONF_UUID] @@ -111,10 +119,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): except IPPConnectionUpgradeRequired: return self.async_abort(reason="connection_upgrade") except (IPPConnectionError, IPPResponseError): + _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self.async_abort(reason="connection_error") except IPPParseError: - _LOGGER.exception("IPP Parse Error") + _LOGGER.debug("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") + except IPPVersionNotSupportedError: + return self.async_abort(reason="ipp_version_error") + except IPPError: + _LOGGER.debug("IPP Error", exc_info=True) + return self.async_abort(reason="ipp_error") if info[CONF_UUID] is not None: self.discovery_info[CONF_UUID] = info[CONF_UUID] diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 0cb7c108b63..216ee519a3a 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.2"], + "requirements": ["pyipp==0.10.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index a80a7f2e0ba..c77d1eec161 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -27,6 +27,8 @@ "already_configured": "This printer is already configured.", "connection_error": "Failed to connect to printer.", "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", + "ipp_error": "Encountered IPP error.", + "ipp_version_error": "IPP version not supported by printer.", "parse_error": "Failed to parse response from printer." } } diff --git a/requirements_all.txt b/requirements_all.txt index 36c54eab31b..5c0c9559216 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyintesishome==1.7.3 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.2 +pyipp==0.10.1 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 531b2550dea..4f9812c7113 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.2 +pyipp==0.10.1 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 7e16a9fc6e0..5229881fbf4 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for the IPP config flow.""" import aiohttp -from pyipp import IPPConnectionUpgradeRequired +from pyipp import IPPConnectionUpgradeRequired, IPPError from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF @@ -172,6 +172,74 @@ async def test_zeroconf_parse_error( assert result["reason"] == "parse_error" +async def test_user_ipp_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort the user flow on IPP error.""" + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_error" + + +async def test_zeroconf_ipp_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP error.""" + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_error" + + +async def test_user_ipp_version_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort user flow on IPP version not supported error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + user_input = {**MOCK_USER_INPUT} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_version_error" + + +async def test_zeroconf_ipp_version_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP version not supported error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_version_error" + + async def test_user_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin b/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin new file mode 100644 index 0000000000000000000000000000000000000000..c92134b9e3bc72859ffd410f8235e36622c2d57f GIT binary patch literal 75 zcmZQ%WMyVxk7is_i literal 0 HcmV?d00001 From 5f97937ba05055a774242b90146c810773cee79c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 14 Apr 2020 21:30:15 -0600 Subject: [PATCH 58/62] Fix websocket connection bug/errant logic in Ambient PWS (#34217) --- homeassistant/components/ambient_station/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index f3f2397d214..d50ba727467 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -347,12 +347,17 @@ class AmbientStation: async def _attempt_connect(self): """Attempt to connect to the socket (retrying later on fail).""" - try: + + async def connect(timestamp=None): + """Connect.""" await self.client.websocket.connect() + + try: + await connect() except WebsocketError as err: _LOGGER.error("Error with the websocket connection: %s", err) self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) - async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect) + async_call_later(self._hass, self._ws_reconnect_delay, connect) async def ws_connect(self): """Register handlers and connect to the websocket.""" From 0afb849e7ffcf72b432adfe4d88c6817dee00f3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Apr 2020 08:43:43 -0700 Subject: [PATCH 59/62] Fix Cloud UI bug preventing managing Google 2FA (#34241) * Fix Cloud UI bug preventing managing Google 2FA * Update comment --- homeassistant/components/cloud/http_api.py | 2 +- .../components/google_assistant/helpers.py | 11 ++++++++--- tests/components/cloud/test_http_api.py | 14 ++++++++++++-- tests/components/google_assistant/__init__.py | 5 +++++ tests/components/google_assistant/test_trait.py | 6 ++---- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c532a2063a7..69b5796e8ba 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -482,7 +482,7 @@ async def google_assistant_list(hass, connection, msg): { "entity_id": entity.entity_id, "traits": [trait.name for trait in entity.traits()], - "might_2fa": entity.might_2fa(), + "might_2fa": entity.might_2fa_traits(), } ) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6ba301c01e8..bbdb8a82183 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -372,14 +372,19 @@ class GoogleEntity: @callback def might_2fa(self) -> bool: """Return if the entity might encounter 2FA.""" + if not self.config.should_2fa(self.state): + return False + + return self.might_2fa_traits() + + @callback + def might_2fa_traits(self) -> bool: + """Return if the entity might encounter 2FA based on just traits.""" state = self.state domain = state.domain features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if not self.config.should_2fa(state): - return False - return any( trait.might_2fa(domain, features, device_class) for trait in self.traits() ) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8bfa6185e9b..d6bca023064 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -687,20 +687,30 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_ entity = GoogleEntity( hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on") ) + entity2 = GoogleEntity( + hass, + MockConfig(should_expose=lambda *_: True, should_2fa=lambda *_: False), + State("cover.garage", "open", {"device_class": "garage"}), + ) with patch( "homeassistant.components.google_assistant.helpers.async_get_entities", - return_value=[entity], + return_value=[entity, entity2], ): await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"}) response = await client.receive_json() assert response["success"] - assert len(response["result"]) == 1 + assert len(response["result"]) == 2 assert response["result"][0] == { "entity_id": "light.kitchen", "might_2fa": False, "traits": ["action.devices.traits.OnOff"], } + assert response["result"][1] == { + "entity_id": "cover.garage", + "might_2fa": True, + "traits": ["action.devices.traits.OpenClose"], + } async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_login): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 802b7968ee6..79684bdeb44 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -33,6 +33,7 @@ class MockConfig(helpers.AbstractConfig): """Initialize config.""" super().__init__(hass) self._should_expose = should_expose + self._should_2fa = should_2fa self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} self._local_sdk_webhook_id = local_sdk_webhook_id @@ -73,6 +74,10 @@ class MockConfig(helpers.AbstractConfig): """Expose it all.""" return self._should_expose is None or self._should_expose(state) + def should_2fa(self, state): + """Expose it all.""" + return self._should_2fa is None or self._should_2fa(state) + BASIC_CONFIG = MockConfig() diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d0ed9a9d33c..a2b8f2e9ea7 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -845,10 +845,8 @@ async def test_lock_unlock_unlock(hass): assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP # Test with 2FA override - with patch( - "homeassistant.components.google_assistant.helpers" - ".AbstractConfig.should_2fa", - return_value=False, + with patch.object( + BASIC_CONFIG, "should_2fa", return_value=False, ): await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {}) assert len(calls) == 2 From 1b36a34ae46c1fa839ba7d465cd3a931f6118c36 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 15 Apr 2020 12:40:47 +0200 Subject: [PATCH 60/62] Add daikin update_before_add (#34248) --- homeassistant/components/daikin/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d46ea26d487..cd45fbdd74b 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -84,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) - async_add_entities([DaikinClimate(daikin_api)]) + async_add_entities([DaikinClimate(daikin_api)], update_before_add=True) class DaikinClimate(ClimateDevice): From 632d44c7b7bae3a8ca6ac3fe5c3a10d161072806 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 15 Apr 2020 15:12:10 +0200 Subject: [PATCH 61/62] Fix various Daikin issues (#34249) * various Daikin fixes * make timeout a constant --- CODEOWNERS | 2 +- homeassistant/components/daikin/__init__.py | 3 ++- homeassistant/components/daikin/config_flow.py | 4 ++-- homeassistant/components/daikin/const.py | 2 ++ homeassistant/components/daikin/manifest.json | 5 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4d4c7d3d900..3e2959cc043 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,7 +77,7 @@ homeassistant/components/counter/* @fabaff homeassistant/components/cover/* @home-assistant/core homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cups/* @fabaff -homeassistant/components/daikin/* @fredrike @rofrantz +homeassistant/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @kane610 homeassistant/components/delijn/* @bollewolle diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 209bf71e594..f6384cfd4b8 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from . import config_flow # noqa: F401 +from .const import TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -91,7 +92,7 @@ async def daikin_api_setup(hass, host): session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with timeout(10): + with timeout(TIMEOUT): device = Appliance(host, session) await device.init() except asyncio.TimeoutError: diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index bd90a87db86..35f21ef3e0d 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST -from .const import KEY_IP, KEY_MAC +from .const import KEY_IP, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class FlowHandler(config_entries.ConfigFlow): device = Appliance( host, self.hass.helpers.aiohttp_client.async_get_clientsession() ) - with timeout(10): + with timeout(TIMEOUT): await device.init() except asyncio.TimeoutError: return self.async_abort(reason="device_timeout") diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index ef24a51be89..15ae5321bf3 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -25,3 +25,5 @@ SENSOR_TYPES = { KEY_MAC = "mac" KEY_IP = "ip" + +TIMEOUT = 60 diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a752642335f..c501fa7c120 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,8 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==1.6.2"], - "dependencies": [], - "codeowners": ["@fredrike", "@rofrantz"], + "requirements": ["pydaikin==1.6.3"], + "codeowners": ["@fredrike"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 5c0c9559216..43a036b19f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.6.2 +pydaikin==1.6.3 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f9812c7113..f5318ffc955 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -479,7 +479,7 @@ pychromecast==4.2.0 pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==1.6.2 +pydaikin==1.6.3 # homeassistant.components.deconz pydeconz==70 From 76b65c5779c0cfa2f2bd2f3c4215faeaaa7c1447 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Apr 2020 08:46:21 -0700 Subject: [PATCH 62/62] Bumped version to 0.108.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index da6dd4bb24e..a1998252008 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 = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)