diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 61208ac6423..3e4fd8fec01 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -276,6 +276,10 @@ class DHCPWatcher(WatcherBase): self._sniffer.stop() async def async_start(self): + """Start watching for dhcp packets.""" + await self.hass.async_add_executor_job(self._start) + + def _start(self): """Start watching for dhcp packets.""" # Local import because importing from scapy has side effects such as opening # sockets @@ -319,7 +323,7 @@ class DHCPWatcher(WatcherBase): conf.sniff_promisc = 0 try: - await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER) + _verify_l2socket_setup(FILTER) except (Scapy_Exception, OSError) as ex: if os.geteuid() == 0: _LOGGER.error("Cannot watch for dhcp packets: %s", ex) @@ -330,7 +334,7 @@ class DHCPWatcher(WatcherBase): return try: - await self.hass.async_add_executor_job(_verify_working_pcap, FILTER) + _verify_working_pcap(FILTER) except (Scapy_Exception, ImportError) as ex: _LOGGER.error( "Cannot watch for dhcp packets without a functional packet filter: %s", diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 338609cf342..a11fe5f3ac6 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,7 +1,9 @@ """Support for Efergy sensors.""" from __future__ import annotations -from pyefergy import Efergy +import logging + +from pyefergy import Efergy, exceptions import voluptuous as vol from homeassistant.components.sensor import ( @@ -20,6 +22,7 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,6 +42,8 @@ CONF_CURRENT_VALUES = "current_values" DEFAULT_PERIOD = "year" DEFAULT_UTC_OFFSET = "0" +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES: dict[str, SensorEntityDescription] = { CONF_INSTANT: SensorEntityDescription( key=CONF_INSTANT, @@ -102,7 +107,10 @@ async def async_setup_platform( ) dev = [] - sensors = await api.get_sids() + try: + sensors = await api.get_sids() + except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + raise PlatformNotReady("Error getting data from Efergy:") from ex for variable in config[CONF_MONITORED_VARIABLES]: if variable[CONF_TYPE] == CONF_CURRENT_VALUES: for sensor in sensors: @@ -150,6 +158,15 @@ class EfergySensor(SensorEntity): async def async_update(self) -> None: """Get the Efergy monitor data from the web service.""" - self._attr_native_value = await self.api.async_get_reading( - self.entity_description.key, period=self.period, sid=self.sid - ) + try: + self._attr_native_value = await self.api.async_get_reading( + self.entity_description.key, period=self.period, sid=self.sid + ) + except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.error("Error getting data from Efergy: %s", ex) + return + if not self._attr_available: + self._attr_available = True + _LOGGER.info("Connection to Efergy has resumed") diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f36d72a967c..5b908efb2df 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211007.0" + "home-assistant-frontend==20211007.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 02bbea29bdb..a5ceeaea1d8 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -205,21 +205,18 @@ async def async_setup_entry( # Load platforms for the devices in the ISY controller that we support. hass.config_entries.async_setup_platforms(entry, PLATFORMS) - def _start_auto_update() -> None: - """Start isy auto update.""" - _LOGGER.debug("ISY Starting Event Stream and automatic updates") - isy.websocket.start() - - def _stop_auto_update(event) -> None: + @callback + def _async_stop_auto_update(event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() - await hass.async_add_executor_job(_start_auto_update) + _LOGGER.debug("ISY Starting Event Stream and automatic updates") + isy.websocket.start() entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) ) # Register Integration-wide Services: diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index 325d9e68cd8..bfcf76a6119 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -35,7 +35,7 @@ DEVICE_ICONS = { 0: "mdi:access-point-network", # Router (Orbi ...) 1: "mdi:book-open-variant", # Amazon Kindle 2: "mdi:android", # Android Device - 3: "mdi:cellphone-android", # Android Phone + 3: "mdi:cellphone", # Android Phone 4: "mdi:tablet-android", # Android Tablet 5: "mdi:router-wireless", # Apple Airport Express 6: "mdi:disc-player", # Blu-ray Player @@ -46,15 +46,15 @@ DEVICE_ICONS = { 11: "mdi:play-network", # DVR 12: "mdi:gamepad-variant", # Gaming Console 13: "mdi:desktop-mac", # iMac - 14: "mdi:tablet-ipad", # iPad - 15: "mdi:tablet-ipad", # iPad Mini - 16: "mdi:cellphone-iphone", # iPhone 5/5S/5C - 17: "mdi:cellphone-iphone", # iPhone + 14: "mdi:tablet", # iPad + 15: "mdi:tablet", # iPad Mini + 16: "mdi:cellphone", # iPhone 5/5S/5C + 17: "mdi:cellphone", # iPhone 18: "mdi:ipod", # iPod Touch 19: "mdi:linux", # Linux PC 20: "mdi:apple-finder", # Mac Mini 21: "mdi:desktop-tower", # Mac Pro - 22: "mdi:laptop-mac", # MacBook + 22: "mdi:laptop", # MacBook 23: "mdi:play-network", # Media Device 24: "mdi:network", # Network Device 25: "mdi:play-network", # Other STB @@ -71,7 +71,7 @@ DEVICE_ICONS = { 36: "mdi:tablet", # Tablet 37: "mdi:desktop-classic", # UNIX PC 38: "mdi:desktop-tower-monitor", # Windows PC - 39: "mdi:laptop-windows", # Surface + 39: "mdi:laptop", # Surface 40: "mdi:access-point-network", # Wifi Extender - 41: "mdi:apple-airplay", # Apple TV + 41: "mdi:cast-variant", # Apple TV } diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 30b00fde15a..2e6f58028e0 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -3,7 +3,7 @@ "name": "National Weather Service (NWS)", "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==1.3.1"], + "requirements": ["pynws==1.3.2"], "quality_scale": "platinum", "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index e3ec9ddef13..f54a0e783cc 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -156,8 +156,9 @@ def register_services(hass): vol.Required(ATTR_GW_ID): vol.All( cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS]) ), - vol.Optional(ATTR_DATE, default=date.today()): cv.date, - vol.Optional(ATTR_TIME, default=datetime.now().time()): cv.time, + # pylint: disable=unnecessary-lambda + vol.Optional(ATTR_DATE, default=lambda: date.today()): cv.date, + vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time, } ) service_set_control_setpoint_schema = vol.Schema( diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 02f2e71053f..fc0b0011d7c 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -54,7 +54,7 @@ set_clock: selector: text: time: - name: Name + name: Time description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. example: "19:34" selector: diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b0df4d4cb7f..2f1a178721f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -275,7 +275,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): if block.type != "device": continue - if block.wakeupEvent[0] == "button": + if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": self._last_input_events_count[1] = -1 break diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 3c9c24b1f7f..a6f3ab12c6b 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -21,6 +21,11 @@ LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226 # max light transition time in milliseconds MAX_TRANSITION_TIME: Final = 5000 +RGBW_MODELS: Final = ( + "SHBLB-1", + "SHRGBW2", +) + MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( "SHBDUO-1", "SHCB-1", diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index cd034c1e7e5..3e0fce43681 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -46,6 +46,7 @@ from .const import ( LIGHT_TRANSITION_MIN_FIRMWARE_DATE, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, + RGBW_MODELS, RPC, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, @@ -143,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._min_kelvin = KELVIN_MIN_VALUE_COLOR - if hasattr(block, "white"): + if wrapper.model in RGBW_MODELS: self._supported_color_modes.add(COLOR_MODE_RGBW) else: self._supported_color_modes.add(COLOR_MODE_RGB) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6f24b4a64be..783153e2746 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -133,6 +133,10 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool: if settings["device"]["type"] in SHBTN_MODELS: return True + if settings.get("mode") == "roller": + button_type = settings["rollers"][0]["button_type"] + return button_type in ["momentary", "momentary_on_release"] + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") if button is None: return False diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 38743981ed5..6dd23e2eec5 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.11.0"], + "requirements": ["PySwitchbot==0.12.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling" diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index ff0526490f5..d17815d2344 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, @@ -19,7 +20,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -158,12 +163,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SmartDeviceException as ex: raise ConfigEntryNotReady from ex + if device.is_dimmer: + async_fix_dimmer_unique_id(hass, entry, device) + hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True +@callback +def async_fix_dimmer_unique_id( + hass: HomeAssistant, entry: ConfigEntry, device: SmartDevice +) -> None: + """Migrate the unique id of dimmers back to the legacy one. + + Dimmers used to use the switch format since pyHS100 treated them as SmartPlug but + the old code created them as lights + + https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 + """ + + # This is the unique id before 2021.0/2021.1 + original_unique_id = legacy_device_id(device) + + # This is the unique id that was used in 2021.0/2021.1 rollout + rollout_unique_id = device.mac.replace(":", "").upper() + + entity_registry = er.async_get(hass) + + rollout_entity_id = entity_registry.async_get_entity_id( + LIGHT_DOMAIN, DOMAIN, rollout_unique_id + ) + original_entry_id = entity_registry.async_get_entity_id( + LIGHT_DOMAIN, DOMAIN, original_unique_id + ) + + # If they are now using the 2021.0/2021.1 rollout entity id + # and have deleted the original entity id, we want to update that entity id + # so they don't end up with another _2 entity, but only if they deleted + # the original + if rollout_entity_id and not original_entry_id: + entity_registry.async_update_entity( + rollout_entity_id, new_unique_id=original_unique_id + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass_data: dict[str, Any] = hass.data[DOMAIN] diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 3f4b130a5cc..ad423e84fa5 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -26,6 +26,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) +from . import legacy_device_id from .const import DOMAIN from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after @@ -58,7 +59,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Initialize the switch.""" super().__init__(device, coordinator) # For backwards compat with pyHS100 - self._attr_unique_id = self.device.mac.replace(":", "").upper() + if self.device.is_dimmer: + # Dimmers used to use the switch format since + # pyHS100 treated them as SmartPlug but the old code + # created them as lights + # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 + self._attr_unique_id = legacy_device_id(device) + else: + self._attr_unique_id = self.device.mac.replace(":", "").upper() @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index de43a47d505..aa8a2659dcd 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ - "pyhaversion==21.7.0" + "pyhaversion==21.10.0" ], "codeowners": [ "@fabaff", diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a1dce44893b..fb908775d1b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -36,8 +36,8 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -STATE_CHANGE_TIME = 0.25 # seconds - +STATE_CHANGE_TIME = 0.40 # seconds +POWER_STATE_CHANGE_TIME = 1 # seconds DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 69dde0e75b6..67c9dc2ba07 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_call_later import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -62,6 +63,7 @@ from . import ( DATA_DEVICE, DATA_UPDATED, DOMAIN, + POWER_STATE_CHANGE_TIME, YEELIGHT_FLOW_TRANSITION_SCHEMA, YeelightEntity, ) @@ -247,7 +249,7 @@ def _async_cmd(func): except BULB_NETWORK_EXCEPTIONS as ex: # A network error happened, the bulb is likely offline now self.device.async_mark_unavailable() - self.async_write_ha_state() + self.async_state_changed() exc_message = str(ex) or type(ex) raise HomeAssistantError( f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" @@ -419,13 +421,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._custom_effects = {} + self._unexpected_state_check = None + + @callback + def async_state_changed(self): + """Call when the device changes state.""" + if not self._device.available: + self._async_cancel_pending_state_check() + self.async_write_ha_state() + async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.host), - self.async_write_ha_state, + self.async_state_changed, ) ) await super().async_added_to_hass() @@ -760,6 +771,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): await self.async_set_default() + self._async_schedule_state_check(True) + + @callback + def _async_cancel_pending_state_check(self): + """Cancel a pending state check.""" + if self._unexpected_state_check: + self._unexpected_state_check() + self._unexpected_state_check = None + + @callback + def _async_schedule_state_check(self, expected_power_state): + """Schedule a poll if the change failed to get pushed back to us. + + Some devices (mainly nightlights) will not send back the on state + so we need to force a refresh. + """ + self._async_cancel_pending_state_check() + + async def _async_update_if_state_unexpected(*_): + self._unexpected_state_check = None + if self.is_on != expected_power_state: + await self.device.async_update(True) + + self._unexpected_state_check = async_call_later( + self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected + ) + @_async_cmd async def _async_turn_off(self, duration) -> None: """Turn off with a given transition duration wrapped with _async_cmd.""" @@ -775,6 +813,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s await self._async_turn_off(duration) + self._async_schedule_state_check(False) @_async_cmd async def async_set_mode(self, mode: str): diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e38a8d92a94..e89697f2131 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.36.7"], + "requirements": ["zeroconf==0.36.8"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/const.py b/homeassistant/const.py index 1281d39fad4..17e7abfdf53 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3c3645f5ece..d02ed121618 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.1 @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.7 +zeroconf==0.36.8 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index d708ec1497e..dabaa995c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ PyRMVtransport==0.3.2 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.11.0 +# PySwitchbot==0.12.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -810,7 +810,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -1514,7 +1514,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.7.0 +pyhaversion==21.10.0 # homeassistant.components.heos pyheos==0.7.2 @@ -1670,7 +1670,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.1 +pynws==1.3.2 # homeassistant.components.nx584 pynx584==0.5 @@ -2474,7 +2474,7 @@ youtube_dl==2021.04.26 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.7 +zeroconf==0.36.8 # homeassistant.components.zha zha-quirks==0.0.62 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7f5c0328a6..84eba6ada81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.2 # homeassistant.components.switchbot -# PySwitchbot==0.11.0 +# PySwitchbot==0.12.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -485,7 +485,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -881,7 +881,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.7.0 +pyhaversion==21.10.0 # homeassistant.components.heos pyheos==0.7.2 @@ -986,7 +986,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.1 +pynws==1.3.2 # homeassistant.components.nx584 pynx584==0.5 @@ -1409,7 +1409,7 @@ yeelight==0.7.7 youless-api==0.13 # homeassistant.components.zeroconf -zeroconf==0.36.7 +zeroconf==0.36.8 # homeassistant.components.zha zha-quirks==0.0.62 diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 97478155483..94a381b9048 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -1,9 +1,15 @@ """The tests for Efergy sensor platform.""" +import asyncio +from datetime import timedelta + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import load_fixture +from tests.common import async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" @@ -30,9 +36,14 @@ MULTI_SENSOR_CONFIG = { } -def mock_responses(aioclient_mock: AiohttpClientMocker): +def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False): """Mock responses for Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" + if error: + aioclient_mock.get( + f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError + ) + return aioclient_mock.get( f"{base_url}getInstant?token={token}", text=load_fixture("efergy/efergy_instant.json"), @@ -64,7 +75,9 @@ async def test_single_sensor_readings( ): """Test for successfully setting up the Efergy platform.""" mock_responses(aioclient_mock) - assert await async_setup_component(hass, "sensor", {"sensor": ONE_SENSOR_CONFIG}) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) await hass.async_block_till_done() assert hass.states.get("sensor.energy_consumed").state == "38.21" @@ -79,9 +92,44 @@ async def test_multi_sensor_readings( ): """Test for multiple sensors in one household.""" mock_responses(aioclient_mock) - assert await async_setup_component(hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG}) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG} + ) await hass.async_block_till_done() assert hass.states.get("sensor.efergy_728386").state == "218" assert hass.states.get("sensor.efergy_0").state == "1808" assert hass.states.get("sensor.efergy_728387").state == "312" + + +async def test_failed_getting_sids( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test failed gettings sids.""" + mock_responses(aioclient_mock, error=True) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) + assert not hass.states.async_all("sensor") + + +async def test_failed_update_and_reconnection( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test failed update and reconnection.""" + mock_responses(aioclient_mock) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) + aioclient_mock.clear_requests() + mock_responses(aioclient_mock, error=True) + next_update = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE + aioclient_mock.clear_requests() + mock_responses(aioclient_mock) + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get("sensor.efergy_728386").state == "1628" diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index f25fc13784a..4e6dbb9dae7 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch -from kasa import SmartBulb, SmartPlug, SmartStrip +from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip from kasa.exceptions import SmartDeviceException from kasa.protocol import TPLinkSmartHomeProtocol @@ -48,6 +48,33 @@ def _mocked_bulb() -> SmartBulb: return bulb +def _mocked_dimmer() -> SmartDimmer: + dimmer = MagicMock(auto_spec=SmartDimmer) + dimmer.update = AsyncMock() + dimmer.mac = MAC_ADDRESS + dimmer.alias = ALIAS + dimmer.model = MODEL + dimmer.host = IP_ADDRESS + dimmer.brightness = 50 + dimmer.color_temp = 4000 + dimmer.is_color = True + dimmer.is_strip = False + dimmer.is_plug = False + dimmer.is_dimmer = True + dimmer.hsv = (10, 30, 5) + dimmer.device_id = MAC_ADDRESS + dimmer.valid_temperature_range.min = 4000 + dimmer.valid_temperature_range.max = 9000 + dimmer.hw_info = {"sw_ver": "1.0.0"} + dimmer.turn_off = AsyncMock() + dimmer.turn_on = AsyncMock() + dimmer.set_brightness = AsyncMock() + dimmer.set_hsv = AsyncMock() + dimmer.set_color_temp = AsyncMock() + dimmer.protocol = _mock_protocol() + return dimmer + + def _mocked_plug() -> SmartPlug: plug = MagicMock(auto_spec=SmartPlug) plug.update = AsyncMock() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c166fccc9b5..73edc63e28c 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,14 +4,23 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, patch +from homeassistant import setup from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery +from . import ( + IP_ADDRESS, + MAC_ADDRESS, + _mocked_dimmer, + _patch_discovery, + _patch_single_discovery, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -63,3 +72,73 @@ async def test_config_entry_retry(hass): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted( + hass: HomeAssistant, entity_reg: EntityRegistry +): + """Test that roll out unique id entity id changed to the original unique id.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + dimmer = _mocked_dimmer() + rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() + original_unique_id = tplink.legacy_device_id(dimmer) + rollout_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=rollout_unique_id, + original_name="Rollout dimmer", + ) + + with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Migrated dimmer", + ) + assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id + + +async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( + hass: HomeAssistant, entity_reg: EntityRegistry +): + """Test no migration happens if the original entity id still exists.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + dimmer = _mocked_dimmer() + rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() + original_unique_id = tplink.legacy_device_id(dimmer) + original_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Original dimmer", + ) + rollout_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=rollout_unique_id, + original_name="Rollout dimmer", + ) + + with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Migrated dimmer", + ) + assert migrated_dimmer_entity_reg.entity_id == original_dimmer_entity_reg.entity_id + assert migrated_dimmer_entity_reg.entity_id != rollout_dimmer_entity_reg.entity_id diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index fd6e12f2635..9c5a76e4a4b 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,5 +1,6 @@ """Test the Yeelight light.""" import asyncio +from datetime import timedelta import logging import socket from unittest.mock import ANY, AsyncMock, MagicMock, call, patch @@ -98,6 +99,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.color import ( color_hs_to_RGB, color_hs_to_xy, @@ -121,7 +123,7 @@ from . import ( _patch_discovery_interval, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed CONFIG_ENTRY_DATA = { CONF_HOST: IP_ADDRESS, @@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant): assert state.state == "on" # bg_power off should not set the brightness to 0 assert state.attributes[ATTR_BRIGHTNESS] == 128 + + +async def test_state_fails_to_update_triggers_update(hass: HomeAssistant): + """Ensure we call async_get_properties if the turn on/off fails to update the state.""" + mocked_bulb = _mocked_bulb() + properties = {**PROPERTIES} + properties.pop("active_mode") + properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties = properties + mocked_bulb.bulb_type = BulbType.Color + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again + await hass.async_block_till_done() + + assert len(mocked_bulb.async_get_properties.mock_calls) == 1 + + mocked_bulb.last_properties["power"] = "off" + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_on.mock_calls) == 1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 2 + + mocked_bulb.last_properties["power"] = "on" + for _ in range(5): + await hass.services.async_call( + "light", + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_off.mock_calls) == 5 + # Even with five calls we only do one state request + # since each successive call should cancel the unexpected + # state check + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 3 + + # But if the state is correct no calls + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_on.mock_calls) == 1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 3