diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebbd2b2fb9e..71fe8af00ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,10 +155,15 @@ jobs: key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -517,10 +522,15 @@ jobs: key: >- ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index fc641548aff..63046d9d441 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.5"], + "requirements": ["bimmer_connected==0.8.7"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 37c9fd73632..988a96ce08e 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with async_timeout.timeout(10): things = await bapi.async_get_things(force=True) - return {thing.SERIAL: thing for thing in things} + return {thing.serial: thing for thing in things} except ServerDisconnectedError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err except ClientResponseError as err: diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index cc0ecd0feab..230534a0848 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._remove_update_listener = None - self._attr_name = self._thing.NAME + self._attr_name = self._thing.name self._attr_device_class = DEVICE_CLASS_SHADE self._attr_supported_features = COVER_FEATURES self._attr_attribution = ATTRIBUTION @@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity): name=self._attr_name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", - sw_version=self._thing.FW_VERSION, - model=self._thing.MODEL, + sw_version=self._thing.fw_version, + model=self._thing.model, ) async def async_added_to_hass(self) -> None: @@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].currentPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].current_position @property def request_cover_position(self) -> int | None: @@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): to Brunt, at times there is a diff of 1 to current None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].requestPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].request_position @property def move_state(self) -> int | None: @@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 when stopped, 1 when opening, 2 when closing """ - mov = self.coordinator.data[self.unique_id].moveState - return int(mov) if mov is not None else None + return self.coordinator.data[self.unique_id].move_state @property def is_opening(self) -> bool: @@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Set the cover to the new position and wait for the update to be reflected.""" try: await self._bapi.async_change_request_position( - position, thingUri=self._thing.thingUri + position, thing_uri=self._thing.thing_uri ) except ClientResponseError as exc: raise HomeAssistantError( - f"Unable to reposition {self._thing.NAME}" + f"Unable to reposition {self._thing.name}" ) from exc self.coordinator.update_interval = FAST_INTERVAL await self.coordinator.async_request_refresh() @@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Update the update interval after each refresh.""" if ( self.request_cover_position - == self._bapi.last_requested_positions[self._thing.thingUri] + == self._bapi.last_requested_positions[self._thing.thing_uri] and self.move_state == 0 ): self.coordinator.update_interval = REGULAR_INTERVAL diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 7b9307e8ef2..f970419b787 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.2"], + "requirements": ["brunt==1.1.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index d27555beb2c..8ed8ea24607 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -161,6 +161,9 @@ class WebDavCalendarData: ) event_list = [] for event in vevent_list: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent if not self.is_matching(vevent, self.search): continue @@ -198,6 +201,9 @@ class WebDavCalendarData: # and they would not be properly parsed using their original start/end dates. new_events = [] for event in results: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent for start_dt in vevent.getrruleset() or []: _start_of_today = start_of_today diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index bee18948a33..b084540bebb 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.1"], + "requirements": ["pychromecast==10.2.2"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 61922a4cd8b..8160c1f5bf0 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -395,11 +395,14 @@ class CastDevice(MediaPlayerEntity): return if self._chromecast.app_id is not None: - # Quit the previous app before starting splash screen + # Quit the previous app before starting splash screen or media player self._chromecast.quit_app() # The only way we can turn the Chromecast is on is by launching an app - self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: + self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + else: + self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) def turn_off(self): """Turn off the cast device.""" @@ -526,7 +529,7 @@ class CastDevice(MediaPlayerEntity): controller.play_media(media) else: app_data = {"media_id": media_id, "media_type": media_type, **extra} - quick_play(self._chromecast, "homeassistant_media", app_data) + quick_play(self._chromecast, "default_media_receiver", app_data) def _media_status(self): """ @@ -674,9 +677,9 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] - if ( - self._chromecast - and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST + if self._chromecast and self._chromecast.cast_type in ( + pychromecast.const.CAST_TYPE_CHROMECAST, + pychromecast.const.CAST_TYPE_AUDIO, ): support |= SUPPORT_TURN_ON diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 1bf15776cad..6133a67bcf1 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.1"], + "requirements": ["pydexcom==0.2.2"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9bc020f3693..ecc3cd4256d 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 2de121920af..0c638f0c455 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -112,10 +112,11 @@ def request_app_setup( Then come back here and hit the below button. """ except NoURLAvailableError: - error_msg = """Could not find a SSL enabled URL for your Home Assistant instance. - Fitbit requires that your Home Assistant instance is accessible via HTTPS. - """ - configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) + _LOGGER.error( + "Could not find an SSL enabled URL for your Home Assistant instance. " + "Fitbit requires that your Home Assistant instance is accessible via HTTPS" + ) + return submit = "I have saved my Client ID and Client Secret into fitbit.conf." diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 22a8aa405e4..8d3d7416c00 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.15"], + "requirements": ["flux_led==0.27.8"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index e89f828f47d..f5ea498e381 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from datetime import timedelta from typing import TYPE_CHECKING, Any, Dict, TypeVar -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.sensor import SensorEntityDescription from homeassistant.core import callback @@ -43,6 +43,8 @@ class FroniusCoordinatorBase( error_interval: timedelta valid_descriptions: list[SensorEntityDescription] + MAX_FAILED_UPDATES = 3 + def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None: """Set up the FroniusCoordinatorBase class.""" self._failed_update_count = 0 @@ -62,7 +64,7 @@ class FroniusCoordinatorBase( data = await self._update_method() except FroniusError as err: self._failed_update_count += 1 - if self._failed_update_count == 3: + if self._failed_update_count == self.MAX_FAILED_UPDATES: self.update_interval = self.error_interval raise UpdateFailed(err) from err @@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): error_interval = timedelta(minutes=10) valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS + SILENT_RETRIES = 3 + def __init__( self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any ) -> None: @@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): async def _update_method(self) -> dict[SolarNetId, Any]: """Return data per solar net id from pyfronius.""" - data = await self.solar_net.fronius.current_inverter_data( - self.inverter_info.solar_net_id - ) + # almost 1% of `current_inverter_data` requests on Symo devices result in + # `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal + # communication between the logger and the inverter. + for silent_retry in range(self.SILENT_RETRIES): + try: + data = await self.solar_net.fronius.current_inverter_data( + self.inverter_info.solar_net_id + ) + except BadStatusError as err: + if silent_retry == (self.SILENT_RETRIES - 1): + raise err + continue + break # wrap a single devices data in a dict with solar_net_id key for # FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys return {self.inverter_info.solar_net_id: data} diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4ac95c38afc..6ae1709e418 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==20211215.0" + "home-assistant-frontend==20211220.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c61e4fc18eb..c7bff647dda 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -30,14 +30,13 @@ async def async_setup_entry(hass, config): loc_id = config.data.get(CONF_LOC_ID) dev_id = config.data.get(CONF_DEV_ID) - devices = [] + devices = {} for location in client.locations_by_id.values(): - for device in location.devices_by_id.values(): - if (not loc_id or location.locationid == loc_id) and ( - not dev_id or device.deviceid == dev_id - ): - devices.append(device) + if not loc_id or location.locationid == loc_id: + for device in location.devices_by_id.values(): + if not dev_id or device.deviceid == dev_id: + devices[device.deviceid] = device if len(devices) == 0: _LOGGER.debug("No devices found") @@ -107,23 +106,30 @@ class HoneywellData: if self._client is None: return False - devices = [ + refreshed_devices = [ device for location in self._client.locations_by_id.values() for device in location.devices_by_id.values() ] - if len(devices) == 0: - _LOGGER.error("Failed to find any devices") + if len(refreshed_devices) == 0: + _LOGGER.error("Failed to find any devices after retry") return False - self.devices = devices + for updated_device in refreshed_devices: + if updated_device.deviceid in self.devices: + self.devices[updated_device.deviceid] = updated_device + else: + _LOGGER.info( + "New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant" + ) + await self._hass.config_entries.async_reload(self._config.entry_id) return True async def _refresh_devices(self): """Refresh each enabled device.""" - for device in self.devices: + for device in self.devices.values(): await self._hass.async_add_executor_job(device.refresh) await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) @@ -143,11 +149,16 @@ class HoneywellData: ) as exp: retries -= 1 if retries == 0: + _LOGGER.error( + "Ran out of retry attempts (3 attempts allocated). Error: %s", + exp, + ) raise exp result = await self._retry() if not result: + _LOGGER.error("Retry result was empty. Error: %s", exp) raise exp - _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) + _LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index d2766515595..6c686e92b8e 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -122,7 +122,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non async_add_entities( [ HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) - for device in data.devices + for device in data.devices.values() ] ) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7003a3a8ccf..ba2d97f44a5 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.6"], + "requirements": ["aiohue==3.0.7"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index ae345238c23..7371efff3bb 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -47,20 +47,9 @@ class HueBaseEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device.id)}, ) - # some (3th party) Hue lights report their connection status incorrectly - # causing the zigbee availability to report as disconnected while in fact - # it can be controlled. Although this is in fact something the device manufacturer - # should fix, we work around it here. If the light is reported unavailable at - # startup, we ignore the availability status of the zigbee connection - self._ignore_availability = False - if self.device is None: - return - if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): - self._ignore_availability = ( - # Official Hue lights are reliable - self.device.product_data.manufacturer_name != "Signify Netherlands B.V." - and zigbee.status != ConnectivityServiceStatus.CONNECTED - ) + # used for availability workaround + self._ignore_availability = None + self._last_state = None @property def name(self) -> str: @@ -82,6 +71,7 @@ class HueBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added.""" + self._check_availability_workaround() # Add value_changed callbacks. self.async_on_remove( self.controller.subscribe( @@ -140,5 +130,50 @@ class HueBaseEntity(Entity): ent_reg.async_remove(self.entity_id) else: self.logger.debug("Received status update for %s", self.entity_id) + self._check_availability_workaround() self.on_update() self.async_write_ha_state() + + @callback + def _check_availability_workaround(self): + """Check availability of the device.""" + if self.resource.type != ResourceTypes.LIGHT: + return + if self._ignore_availability is not None: + # already processed + return + cur_state = self.resource.on.on + if self._last_state is None: + self._last_state = cur_state + return + # some (3th party) Hue lights report their connection status incorrectly + # causing the zigbee availability to report as disconnected while in fact + # it can be controlled. Although this is in fact something the device manufacturer + # should fix, we work around it here. If the light is reported unavailable + # by the zigbee connectivity but the state changesm its considered as a + # malfunctioning device and we report it. + # while the user should actually fix this issue instead of ignoring it, we + # ignore the availability for this light from this point. + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + if ( + self._last_state != cur_state + and zigbee.status != ConnectivityServiceStatus.CONNECTED + ): + # the device state changed from on->off or off->on + # while it was reported as not connected! + self.logger.warning( + "Light %s changed state while reported as disconnected. " + "This is an indicator that routing is not working properly for this device. " + "Home Assistant will ignore availability for this light from now on. " + "Device details: %s - %s (%s) fw: %s", + self.name, + self.device.product_data.manufacturer_name, + self.device.product_data.product_name, + self.device.product_data.model_id, + self.device.product_data.software_version, + ) + # do we want to store this in some persistent storage? + self._ignore_availability = True + else: + self._ignore_availability = False + self._last_state = cur_state diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b793c667353..21ac4ce9ea4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.18.13" + "xknx==0.18.14" ], "codeowners": [ "@Julius2342", diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index a9d5cbdec7d..5dcb5fc557a 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.client_exceptions import ClientResponseError @@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation +from .api import ( + ConfigEntryLyricClient, + LyricLocalOAuth2Implementation, + OAuth2SessionLyric, +) from .config_flow import OAuth2FlowHandler from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) session = aiohttp_client.async_get_clientsession(hass) - oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + oauth_session = OAuth2SessionLyric(hass, entry, implementation) client = ConfigEntryLyricClient(session, oauth_session) client_id = hass.data[DOMAIN][CONF_CLIENT_ID] lyric = Lyric(client, client_id) - async def async_update_data() -> Lyric: + async def async_update_data(force_refresh_token: bool = False) -> Lyric: """Fetch data from Lyric.""" - await oauth_session.async_ensure_token_valid() + try: + if not force_refresh_token: + await oauth_session.async_ensure_token_valid() + else: + await oauth_session.force_refresh_token() + except ClientResponseError as exception: + if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + raise ConfigEntryAuthFailed from exception + raise UpdateFailed(exception) from exception + try: async with async_timeout.timeout(60): await lyric.get_locations() return lyric except LyricAuthenticationException as exception: + # Attempt to refresh the token before failing. + # Honeywell appear to have issues keeping tokens saved. + _LOGGER.debug("Authentication failed. Attempting to refresh token") + if not force_refresh_token: + return await async_update_data(force_refresh_token=True) raise ConfigEntryAuthFailed from exception except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 3b23f802ded..4a8aa44417f 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession +class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session): + """OAuth2Session for Lyric.""" + + async def force_refresh_token(self) -> None: + """Force a token refresh.""" + new_token = await self.implementation.async_refresh_token(self.token) + + self.hass.config_entries.async_update_entry( + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + class ConfigEntryLyricClient(LyricClient): """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index aa4c57ecdde..1f4a15f1de1 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.7.0"], + "requirements": ["pynetgear==0.8.0"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 2c1fbf5a3f4..6212541f897 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -11,6 +11,7 @@ from nexia.const import ( SYSTEM_STATUS_IDLE, UNIT_FAHRENHEIT, ) +from nexia.util import find_humidity_setpoint import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatZoneEntity from .util import percent_conv +PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time + SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" @@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidity(self, humidity): """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): - self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + self.set_dehumidify_setpoint(humidity) else: - self._thermostat.set_humidify_setpoint(humidity / 100.0) + self.set_humidify_setpoint(humidity) self._signal_thermostat_update() @property @@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" - self._thermostat.set_humidify_setpoint(humidity / 100.0) + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_humidify_setpoint() == target_humidity: + # Trying to set the humidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_humidify_setpoint(target_humidity) + self._signal_thermostat_update() + + def set_dehumidify_setpoint(self, humidity): + """Set the dehumidify setpoint.""" + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_dehumidify_setpoint() == target_humidity: + # Trying to set the dehumidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_dehumidify_setpoint(target_humidity) self._signal_thermostat_update() def _signal_thermostat_update(self): diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 105cbdb62b7..624eee41db7 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.11"], + "requirements": ["nexia==0.9.12"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 39884681967..989616f6367 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): await self.async_set_unique_id(controller.mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: ip_address} + updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False ) # A new rain machine: We will change out the unique id diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 77a8a522f65..82457bd767e 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt @@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity): if self._state and self._off_delay is not None: + @callback def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 527fb143aff..3e745dc2d4b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.7.1"], + "requirements": ["ring_doorbell==0.7.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 7a877e60109..06140fcba72 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -69,7 +69,6 @@ async def async_setup_climate_entities( ) -> None: """Set up online climate devices.""" - _LOGGER.info("Setup online climate device %s", wrapper.name) device_block: Block | None = None sensor_block: Block | None = None @@ -82,6 +81,7 @@ async def async_setup_climate_entities( sensor_block = block if sensor_block and device_block: + _LOGGER.debug("Setup online climate device %s", wrapper.name) async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) @@ -92,7 +92,6 @@ async def async_restore_climate_entities( wrapper: BlockDeviceWrapper, ) -> None: """Restore sleeping climate devices.""" - _LOGGER.info("Setup sleeping climate device %s", wrapper.name) ent_reg = await entity_registry.async_get_registry(hass) entries = entity_registry.async_entries_for_config_entry( @@ -104,6 +103,7 @@ async def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue + _LOGGER.debug("Setup sleeping climate device %s", wrapper.name) _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index dd3ff717e28..59142674e66 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -12,6 +12,7 @@ from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, SimplipyError, + WebsocketError, ) from simplipy.system import SystemNotification from simplipy.system.v3 import ( @@ -472,6 +473,7 @@ class SimpliSafe: self._api = api self._hass = hass self._system_notifications: dict[int, set[SystemNotification]] = {} + self._websocket_reconnect_task: asyncio.Task | None = None self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.systems: dict[int, SystemType] = {} @@ -516,11 +518,44 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications - async def _async_websocket_on_connect(self) -> None: - """Define a callback for connecting to the websocket.""" + async def _async_start_websocket_loop(self) -> None: + """Start a websocket reconnection loop.""" if TYPE_CHECKING: assert self._api.websocket - await self._api.websocket.async_listen() + + should_reconnect = True + + try: + await self._api.websocket.async_connect() + await self._api.websocket.async_listen() + except asyncio.CancelledError: + LOGGER.debug("Request to cancel websocket loop received") + raise + except WebsocketError as err: + LOGGER.error("Failed to connect to websocket: %s", err) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Unknown exception while connecting to websocket: %s", err) + + if should_reconnect: + LOGGER.info("Disconnected from websocket; reconnecting") + await self._async_cancel_websocket_loop() + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) + + async def _async_cancel_websocket_loop(self) -> None: + """Stop any existing websocket reconnection loop.""" + if self._websocket_reconnect_task: + self._websocket_reconnect_task.cancel() + try: + await self._websocket_reconnect_task + except asyncio.CancelledError: + LOGGER.debug("Websocket reconnection task successfully canceled") + self._websocket_reconnect_task = None + + if TYPE_CHECKING: + assert self._api.websocket + await self._api.websocket.async_disconnect() @callback def _async_websocket_on_event(self, event: WebsocketEvent) -> None: @@ -560,17 +595,17 @@ class SimpliSafe: assert self._api.refresh_token assert self._api.websocket - self._api.websocket.add_connect_callback(self._async_websocket_on_connect) self._api.websocket.add_event_callback(self._async_websocket_on_event) - asyncio.create_task(self._api.websocket.async_connect()) + self._websocket_reconnect_task = asyncio.create_task( + self._async_start_websocket_loop() + ) async def async_websocket_disconnect_listener(_: Event) -> None: """Define an event handler to disconnect from the websocket.""" if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - await self._api.websocket.async_disconnect() + await self._async_cancel_websocket_loop() self.entry.async_on_unload( self._hass.bus.async_listen_once( @@ -612,18 +647,18 @@ class SimpliSafe: data={**self.entry.data, CONF_TOKEN: token}, ) - @callback - def async_handle_refresh_token(token: str) -> None: + async def async_handle_refresh_token(token: str) -> None: """Handle a new refresh token.""" async_save_refresh_token(token) if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - # If a websocket connection is open, reconnect it to use the - # new access token: - asyncio.create_task(self._api.websocket.async_reconnect()) + # Open a new websocket connection with the fresh token: + await self._async_cancel_websocket_loop() + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) self.entry.async_on_unload( self._api.add_refresh_token_callback(async_handle_refresh_token) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0b6cb385be6..8e494af013a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.1"], + "requirements": ["simplisafe-python==2021.12.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ea8b8ff73a4..e95c0e13887 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index eaa51855d38..ac7cbe84459 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.5"], + "requirements": ["tailscale==0.1.6"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index da12c25c7d1..2644a91b20f 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index c11698b1358..5dce4033074 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity): None, ) + @property + def current_temperature(self) -> int | None: + """Return the current temperature.""" + return self._channel.get_state() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index bd903c76790..f1a90651716 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" _channel: VelbusDimmer - _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property def is_on(self) -> bool: @@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): _channel: VelbusButton _attr_entity_registry_enabled_default = False - _attr_supported_feature = SUPPORT_FLASH + _attr_supported_features = SUPPORT_FLASH def __init__(self, channel: VelbusChannel) -> None: """Initialize the button light (led).""" diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json new file mode 100644 index 00000000000..bf6b40fb6b2 --- /dev/null +++ b/homeassistant/components/vicare/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "scan_interval": "Scan Interval (seconds)", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "client_id": "[%key:common::config_flow::data::api_key%]", + "heating_type": "Heating type" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json new file mode 100644 index 00000000000..d693cbe76cc --- /dev/null +++ b/homeassistant/components/vicare/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Unexpected error" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "name": "Name", + "scan_interval": "Scan Interval (seconds)", + "client_id": "API Key", + "heating_type": "Heating type", + "password": "Password", + "username": "Email" + }, + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 59eae24c714..d0643ed51a9 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.7"], + "requirements": ["pywemo==0.7.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 60098514125..c46878c0ef3 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.12"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ef3ab223248..bfbd3925c55 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -490,7 +490,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 await platform.async_add_entities([entity]) if entity.unique_id: - hass.async_add_job(_add_node_to_component()) + hass.create_task(_add_node_to_component()) return @callback diff --git a/homeassistant/const.py b/homeassistant/const.py index 81734046d2b..bda65572d4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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 e52542ccea0..5ea2198a3aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.12 +async-upnp-client==0.23.1 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 @@ -30,7 +30,7 @@ pyyaml==6.0 requests==2.26.0 scapy==2.4.5 sqlalchemy==1.4.27 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.6.3 zeroconf==0.37.0 diff --git a/requirements.txt b/requirements.txt index 5832d0ea2d0..4c6af849ce8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ python-slugify==4.0.1 pyyaml==6.0 requests==2.26.0 voluptuous==0.12.2 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 151881e5613..0fa32a93289 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.imap aioimaplib==0.9.0 @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.1 # homeassistant.components.supla asyncpysupla==0.0.5 @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -440,7 +440,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -1065,7 +1065,7 @@ nettigo-air-monitor==1.2.1 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 @@ -1396,7 +1396,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 @@ -1435,7 +1435,7 @@ pydeconz==85 pydelijn==0.6.1 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -1661,7 +1661,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.netio pynetio==0.1.9.1 @@ -2007,7 +2007,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 @@ -2058,7 +2058,7 @@ rfk101py==0.0.1 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2146,7 +2146,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.sisyphus sisyphus-control==3.0 @@ -2272,7 +2272,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tank_utility tank_utility==1.4.0 @@ -2448,7 +2448,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c59e534e52d..e80eb4395a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.apache_kafka aiokafka==0.6.0 @@ -236,7 +236,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.1 # homeassistant.components.aurora auroranoaa==0.0.2 @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.blebox blebox_uniapi==1.3.3 @@ -281,7 +281,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -654,7 +654,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.3 @@ -850,7 +850,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.climacell pyclimacell==0.18.2 @@ -868,7 +868,7 @@ pydaikin==2.6.0 pydeconz==85 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -1019,7 +1019,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.nuki pynuki==1.4.1 @@ -1206,7 +1206,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 @@ -1230,7 +1230,7 @@ restrictedpython==5.2 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.roku rokuecp==0.8.4 @@ -1273,7 +1273,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.slack slackclient==2.5.0 @@ -1352,7 +1352,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tellduslive tellduslive==0.10.11 @@ -1450,7 +1450,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/setup.py b/setup.py index ee163bc79f4..270f5c58f58 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ REQUIRES = [ "pyyaml==6.0", "requests==2.26.0", "voluptuous==0.12.2", - "voluptuous-serialize==2.4.0", + "voluptuous-serialize==2.5.0", "yarl==1.6.3", ] diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 85562f39761..3c5d4705713 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, ), @@ -791,7 +793,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): chromecast.media_controller.play_media.assert_not_called() quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", { "media_id": "best.mp3", "media_type": "audio", @@ -907,7 +909,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): # Play_media await common.async_play_media(hass, "audio", "/best.mp3", entity_id) quick_play_mock.assert_called_once_with( - chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"} + chromecast, "default_media_receiver", {"media_id": ANY, "media_type": "audio"} ) assert quick_play_mock.call_args[0][2]["media_id"].startswith( "http://example.com:8123/best.mp3?authSig=" @@ -1311,7 +1313,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): assert not chromecast.media_controller.play_media.called quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", {"media_id": "best.mp3", "media_type": "music"}, ) diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index fe2a916fdcc..b8c10b47b60 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -381,7 +381,7 @@ async def test_event_subscribe_rejected( Device state will instead be obtained via polling in async_update. """ - dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(501) + dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501) mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_state = hass.states.get(mock_entity_id) diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py index b729c4d97ac..a2368975128 100644 --- a/tests/components/fronius/test_coordinator.py +++ b/tests/components/fronius/test_coordinator.py @@ -1,7 +1,7 @@ """Test the Fronius update coordinators.""" from unittest.mock import patch -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, @@ -18,27 +18,32 @@ async def test_adaptive_update_interval(hass, aioclient_mock): with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data: mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert mock_inverter_data.call_count == 1 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 2 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() - mock_inverter_data.side_effect = FroniusError - # first 3 requests at default interval - 4th has different interval - for _ in range(4): + mock_inverter_data.side_effect = FroniusError() + # first 3 bad requests at default interval - 4th has different interval + for _ in range(3): async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 5 + assert mock_inverter_data.call_count == 3 + mock_inverter_data.reset_mock() + async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 6 + assert mock_inverter_data.call_count == 1 + mock_inverter_data.reset_mock() mock_inverter_data.side_effect = None # next successful request resets to default interval @@ -46,10 +51,23 @@ async def test_adaptive_update_interval(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 7 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 8 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() + + # BadStatusError on inverter endpoints have special handling + mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8) + # first 3 requests at default interval - 4th has different interval + for _ in range(3): + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + # BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9 + assert mock_inverter_data.call_count == 9 diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 619d770c59e..49917aae151 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -1,6 +1,8 @@ """Test honeywell setup process.""" -from unittest.mock import patch +from unittest.mock import create_autospec, patch + +import somecomfort from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() == 2 + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_setup_multiple_thermostats_with_same_deviceid( + hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client +) -> None: + """Test Honeywell TCC API returning duplicate device IDs.""" + mock_location2 = create_autospec(somecomfort.Location, instance=True) + mock_location2.locationid.return_value = "location2" + mock_location2.devices_by_id = {device.deviceid: device} + client.locations_by_id["location2"] = mock_location2 + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert hass.states.async_entity_ids_count() == 1 + assert "Platform honeywell does not generate unique IDs" not in caplog.text diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 65289c2b173..4f3e1734b69 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -28,7 +28,15 @@ from tests.common import MockConfigEntry def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor: """Get mock gw descriptor.""" - return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True) + return GatewayDescriptor( + "Test", + ip, + port, + "eth0", + "127.0.0.1", + supports_routing=True, + supports_tunnelling=True, + ) async def test_user_single_instance(hass): diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 08abd140dac..13ec0cb2337 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -57,7 +57,7 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.port = MOCK_PORT device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER - device.model_name = pywemo_model + device.model_name = pywemo_model.replace("LongPress", "") device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 76016469b72..0ad7d95dd7a 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -3,7 +3,6 @@ import pytest from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT from homeassistant.const import ( CONF_DEVICE_ID, @@ -11,6 +10,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE, + Platform, ) from homeassistant.setup import async_setup_component @@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"} @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def setup_automation(hass, device_id, trigger_type): @@ -67,14 +67,14 @@ async def test_get_triggers(hass, wemo_entity): }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_off", }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_on", diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index e756e816a47..9ef9e6b5685 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -26,8 +26,8 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True)) @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def test_async_register_device_longpress_fails(hass, pywemo_device):