From 47398f03dd001482ccac4c68d631cc2a25650249 Mon Sep 17 00:00:00 2001 From: Meow Date: Sat, 15 Apr 2023 21:41:34 +0200 Subject: [PATCH 01/19] Add SetSynchronizationPoint fallback to onvif (#86400) Co-authored-by: J. Nick Koston --- homeassistant/components/onvif/event.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 84d75bf8048..e9fc89d6ef6 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -20,11 +20,9 @@ from .models import Event from .parsers import PARSERS UNHANDLED_TOPICS: set[str] = set() -SUBSCRIPTION_ERRORS = ( - Fault, - asyncio.TimeoutError, - TransportError, -) + +SUBSCRIPTION_ERRORS = (Fault, asyncio.TimeoutError, TransportError) +SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError) def _stringify_onvif_error(error: Exception) -> str: @@ -99,7 +97,7 @@ class EventManager: # Initialize events pullpoint = self.device.create_pullpoint_service() - with suppress(*SUBSCRIPTION_ERRORS): + with suppress(*SET_SYNCHRONIZATION_POINT_ERRORS): await pullpoint.SetSynchronizationPoint() response = await pullpoint.PullMessages( {"MessageLimit": 100, "Timeout": dt.timedelta(seconds=5)} From 2bda40d35252f8822eb192724e680036b977b080 Mon Sep 17 00:00:00 2001 From: Mark Adkins Date: Sun, 16 Apr 2023 08:04:18 -0400 Subject: [PATCH 02/19] Fix SharkIQ token expiration (#89357) --- .../components/sharkiq/update_coordinator.py | 10 +++++++++- tests/components/sharkiq/test_vacuum.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 2afeb574f92..87f5aafe7a4 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from datetime import datetime, timedelta from async_timeout import timeout from sharkiq import ( @@ -60,6 +61,13 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]): async def _async_update_data(self) -> bool: """Update data device by device.""" try: + if self.ayla_api.token_expiring_soon: + await self.ayla_api.async_refresh_auth() + elif datetime.now() > self.ayla_api.auth_expiration - timedelta( + seconds=600 + ): + await self.ayla_api.async_refresh_auth() + all_vacuums = await self.ayla_api.async_list_devices() self._online_dsns = { v["dsn"] @@ -78,7 +86,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]): LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err) raise ConfigEntryAuthFailed from err except Exception as err: - LOGGER.exception("Unexpected error updating SharkIQ") + LOGGER.exception("Unexpected error updating SharkIQ. Attempting re-auth") raise UpdateFailed(err) from err return True diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index aa43f324bba..2305b117c3e 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable from copy import deepcopy +from datetime import datetime, timedelta import enum from typing import Any from unittest.mock import patch @@ -72,9 +73,14 @@ EXPECTED_FEATURES = ( class MockAyla(AylaApi): """Mocked AylaApi that doesn't do anything.""" + desired_expiry = False + async def async_sign_in(self): """Instead of signing in, just return.""" + async def async_refresh_auth(self): + """Instead of refreshing auth, just return.""" + async def async_list_devices(self) -> list[dict]: """Return the device list.""" return [SHARK_DEVICE_DICT] @@ -89,6 +95,18 @@ class MockAyla(AylaApi): async def async_request(self, http_method: str, url: str, **kwargs): """Don't make an HTTP request.""" + @property + def token_expiring_soon(self) -> bool: + """Toggling Property for Token Expiration Flag.""" + # Alternate expiry flag for each test + self.desired_expiry = not self.desired_expiry + return self.desired_expiry + + @property + def auth_expiration(self) -> datetime: + """Sample expiration timestamp that is always 1200 seconds behind now().""" + return datetime.now() - timedelta(seconds=1200) + class MockShark(SharkIqVacuum): """Mocked SharkIqVacuum that won't hit the API.""" From 8feab57d598afbadd44584a7d4e24455711c308b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 15 Apr 2023 03:05:22 +0200 Subject: [PATCH 03/19] Reolink prevent ONVIF push being lost due to ConnectionResetError (#91070) * Make "Connection lost" error less likely * Handle connection loss during ONVIF event reading * tweak * fix styling * catch asyncio.CancelledError from request.text() * missing () * re-raise cancelation for proper cleanup * Simplify * Also set webhook_reachable if connection lost * fix styntax * Send HTTP_OK directly after data read done * protect agains garbage collection * Protect shielded task (inner) not shielded future (outer) * fix black * Make sure exceptions are logged * fix spelling * fix black * fix spelling * Simplify using hass.async_create_task * clarify comment * Eleborate comment * Update homeassistant/components/reolink/host.py Co-authored-by: J. Nick Koston * Apply suggestions from bdraco --------- Co-authored-by: J. Nick Koston --- homeassistant/components/reolink/host.py | 66 ++++++++++++++++++------ 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index f7810746481..1710219cfb3 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -357,34 +357,70 @@ class ReolinkHost: async def handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request - ): + ) -> None: """Shield the incoming webhook callback from cancellation.""" - await asyncio.shield(self.handle_webhook_shielded(hass, webhook_id, request)) - - async def handle_webhook_shielded( - self, hass: HomeAssistant, webhook_id: str, request: Request - ): - """Handle incoming webhook from Reolink for inbound messages and calls.""" - + shielded_future = asyncio.shield( + self._handle_webhook(hass, webhook_id, request) + ) _LOGGER.debug("Webhook '%s' called", webhook_id) if not self._webhook_reachable.is_set(): self._webhook_reachable.set() + ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") + await shielded_future - if not request.body_exists: - _LOGGER.debug("Webhook '%s' triggered without payload", webhook_id) + async def _handle_webhook( + self, hass: HomeAssistant, webhook_id: str, request: Request + ) -> None: + """Handle incoming webhook from Reolink for inbound messages and calls.""" + try: + data = await request.text() + except ConnectionResetError: + # We lost the connection before reading the message, fallback to polling + # No need for a background task here as we already know the connection is lost + _LOGGER.debug( + "Webhook '%s' called, but lost connection before reading message, issuing poll", + webhook_id, + ) + if not await self._api.get_motion_state_all_ch(): + _LOGGER.error( + "Could not poll motion state after losing connection during receiving ONVIF event" + ) + return + async_dispatcher_send(hass, f"{webhook_id}_all", {}) return - data = await request.text() if not data: _LOGGER.debug( "Webhook '%s' triggered with unknown payload: %s", webhook_id, data ) return - channels = await self._api.ONVIF_event_callback(data) + # We received the data but we want handle_webhook to return as soon as possible + # so we process the data in the background + hass.async_create_background_task( + self._process_webhook_data(hass, webhook_id, data), + "Process Reolink webhook", + ) + + async def _process_webhook_data( + self, hass: HomeAssistant, webhook_id: str, data: str + ) -> None: + """Process the data from the webhook.""" + # This task is executed in the background so we need to catch exceptions + # and log them + try: + channels = await self._api.ONVIF_event_callback(data) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception( + "Error processing ONVIF event for Reolink %s: %s", + self._api.nvr_name, + ex, + ) + return if channels is None: async_dispatcher_send(hass, f"{webhook_id}_all", {}) - else: - for channel in channels: - async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) + return + + for channel in channels: + async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) From 0e3f462bfb03b6b381198970ad62fa6d17820705 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:33:38 +0200 Subject: [PATCH 04/19] Add missing mock in sharkiq tests (#91325) --- tests/components/sharkiq/test_vacuum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 2305b117c3e..cfd62c9deaf 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -81,6 +81,9 @@ class MockAyla(AylaApi): async def async_refresh_auth(self): """Instead of refreshing auth, just return.""" + async def async_sign_out(self): + """Instead of signing out, just return.""" + async def async_list_devices(self) -> list[dict]: """Return the device list.""" return [SHARK_DEVICE_DICT] From 5f7b447d7a91465af045fb3959dd243c6f56c3c5 Mon Sep 17 00:00:00 2001 From: rich-kettlewell <122128709+rich-kettlewell@users.noreply.github.com> Date: Thu, 13 Apr 2023 18:49:07 +0100 Subject: [PATCH 05/19] Tado set_water_heater_timer should use water_heater domain (#91364) --- homeassistant/components/tado/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index 3c5a830698d..211ae4cd1ff 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -41,7 +41,7 @@ set_water_heater_timer: target: entity: integration: tado - domain: climate + domain: water_heater fields: time_period: name: Time period From e29d5a135689df9d9933cf6e44893ecde2ed948e Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 15 Apr 2023 14:49:05 +0100 Subject: [PATCH 06/19] Fix listener running in foreground for System Bridge integration (#91391) Co-authored-by: J. Nick Koston --- homeassistant/components/system_bridge/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 809e2a4fd50..adb88efd5ec 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -188,7 +188,10 @@ class SystemBridgeDataUpdateCoordinator( session=async_get_clientsession(self.hass), ) - self.hass.async_create_task(self._listen_for_data()) + self.hass.async_create_background_task( + self._listen_for_data(), + name="System Bridge WebSocket Listener", + ) await self.websocket_client.register_data_listener( RegisterDataListener(modules=MODULES) From afe3fd5ec04d78449c12a79c9a7d1843526fa252 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Apr 2023 20:37:12 -1000 Subject: [PATCH 07/19] Bump onvif-zeep-async to 1.2.5 (#91399) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index ef4497fa284..9095bb2620e 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==1.2.3", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==1.2.5", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c2fa18abdf..651982d4722 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.3 +onvif-zeep-async==1.2.5 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 914f5a37256..6a8e16b9147 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.3 +onvif-zeep-async==1.2.5 # homeassistant.components.opengarage open-garage==0.2.0 From 2b9cc39d2bb447ce1751eaf1fed16ebda23720de Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 15 Apr 2023 09:48:34 -0400 Subject: [PATCH 08/19] Fix attribute reporting config failures in ZHA (#91403) --- .../components/zha/core/channels/base.py | 37 ++++++------ tests/components/zha/test_channels.py | 56 +++++++++++++++++++ 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 6d4899be37c..48f69ffbf2d 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -49,8 +49,8 @@ _LOGGER = logging.getLogger(__name__) class AttrReportConfig(TypedDict, total=True): """Configuration to report for the attributes.""" - # Could be either an attribute name or attribute id - attr: str | int + # An attribute name + attr: str # The config for the attribute reporting configuration consists of a tuple for # (minimum_reported_time_interval_s, maximum_reported_time_interval_s, value_delta) config: tuple[int, int, int | float] @@ -130,15 +130,13 @@ class ZigbeeChannel(LogMixin): unique_id = ch_pool.unique_id.replace("-", ":") self._unique_id = f"{unique_id}:0x{cluster.cluster_id:04x}" if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: - attr = self.REPORT_CONFIG[0].get("attr") - if isinstance(attr, str): - attribute: ZCLAttributeDef = self.cluster.attributes_by_name.get(attr) - if attribute is not None: - self.value_attribute = attribute.id - else: - self.value_attribute = None + attr_def: ZCLAttributeDef | None = self.cluster.attributes_by_name.get( + self.REPORT_CONFIG[0]["attr"] + ) + if attr_def is not None: + self.value_attribute = attr_def.id else: - self.value_attribute = attr + self.value_attribute = None self._status = ChannelStatus.CREATED self._cluster.add_listener(self) self.data_cache: dict[str, Enum] = {} @@ -233,7 +231,12 @@ class ZigbeeChannel(LogMixin): for attr_report in self.REPORT_CONFIG: attr, config = attr_report["attr"], attr_report["config"] - attr_name = self.cluster.attributes.get(attr, [attr])[0] + + try: + attr_name = self.cluster.find_attribute(attr).name + except KeyError: + attr_name = attr + event_data[attr_name] = { "min": config[0], "max": config[1], @@ -282,7 +285,7 @@ class ZigbeeChannel(LogMixin): ) def _configure_reporting_status( - self, attrs: dict[int | str, tuple[int, int, float | int]], res: list | tuple + self, attrs: dict[str, tuple[int, int, float | int]], res: list | tuple ) -> None: """Parse configure reporting result.""" if isinstance(res, (Exception, ConfigureReportingResponseRecord)): @@ -304,14 +307,14 @@ class ZigbeeChannel(LogMixin): return failed = [ - self.cluster.attributes.get(r.attrid, [r.attrid])[0] - for r in res - if r.status != Status.SUCCESS + self.cluster.find_attribute(record.attrid).name + for record in res + if record.status != Status.SUCCESS ] - attributes = {self.cluster.attributes.get(r, [r])[0] for r in attrs} + self.debug( "Successfully configured reporting for '%s' on '%s' cluster", - attributes - set(failed), + set(attrs) - set(failed), self.name, ) self.debug( diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 9c43a76ea85..b8542433e7c 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -5,9 +5,12 @@ from unittest import mock from unittest.mock import AsyncMock, patch import pytest +import zigpy.endpoint import zigpy.profiles.zha import zigpy.types as t +from zigpy.zcl import foundation import zigpy.zcl.clusters +import zigpy.zdo.types as zdo_t import homeassistant.components.zha.core.channels as zha_channels import homeassistant.components.zha.core.channels.base as base_channels @@ -726,3 +729,56 @@ async def test_cluster_no_ep_attribute(m1, zha_device_mock) -> None: pools = {pool.id: pool for pool in channels.pools} assert "1:0x042e" in pools[1].all_channels assert pools[1].all_channels["1:0x042e"].name + + +async def test_configure_reporting(hass: HomeAssistant) -> None: + """Test setting up a channel and configuring attribute reporting in two batches.""" + + class TestZigbeeChannel(base_channels.ZigbeeChannel): + BIND = True + REPORT_CONFIG = ( + # By name + base_channels.AttrReportConfig(attr="current_x", config=(1, 60, 1)), + base_channels.AttrReportConfig(attr="current_hue", config=(1, 60, 2)), + base_channels.AttrReportConfig(attr="color_temperature", config=(1, 60, 3)), + base_channels.AttrReportConfig(attr="current_y", config=(1, 60, 4)), + ) + + mock_ep = mock.AsyncMock(spec_set=zigpy.endpoint.Endpoint) + mock_ep.device.zdo = AsyncMock() + + cluster = zigpy.zcl.clusters.lighting.Color(mock_ep) + cluster.bind = AsyncMock( + spec_set=cluster.bind, + return_value=[zdo_t.Status.SUCCESS], # ZDOCmd.Bind_rsp + ) + cluster.configure_reporting_multiple = AsyncMock( + spec_set=cluster.configure_reporting_multiple, + return_value=[ + foundation.ConfigureReportingResponseRecord( + status=foundation.Status.SUCCESS + ) + ], + ) + + ch_pool = mock.AsyncMock(spec_set=zha_channels.ChannelPool) + ch_pool.skip_configuration = False + + channel = TestZigbeeChannel(cluster, ch_pool) + await channel.async_configure() + + # Since we request reporting for five attributes, we need to make two calls (3 + 1) + assert cluster.configure_reporting_multiple.mock_calls == [ + mock.call( + { + "current_x": (1, 60, 1), + "current_hue": (1, 60, 2), + "color_temperature": (1, 60, 3), + } + ), + mock.call( + { + "current_y": (1, 60, 4), + } + ), + ] From bf389440dc928d22d0cdb494616d8c8a0b76c694 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Apr 2023 11:48:03 +0200 Subject: [PATCH 09/19] Save Thread dataset store when changing preferred dataset (#91411) --- .../components/thread/dataset_store.py | 27 ++++++++++++++----- .../components/thread/websocket_api.py | 5 ++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index 786ea55b34f..bcadf3ca5fb 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -82,7 +82,7 @@ class DatasetStore: """Initialize the dataset store.""" self.hass = hass self.datasets: dict[str, DatasetEntry] = {} - self.preferred_dataset: str | None = None + self._preferred_dataset: str | None = None self._store: Store[dict[str, Any]] = Store( hass, STORAGE_VERSION_MAJOR, @@ -103,14 +103,14 @@ class DatasetStore: entry = DatasetEntry(source=source, tlv=tlv) self.datasets[entry.id] = entry # Set to preferred if there is no preferred dataset - if self.preferred_dataset is None: - self.preferred_dataset = entry.id + if self._preferred_dataset is None: + self._preferred_dataset = entry.id self.async_schedule_save() @callback def async_delete(self, dataset_id: str) -> None: """Delete dataset.""" - if self.preferred_dataset == dataset_id: + if self._preferred_dataset == dataset_id: raise DatasetPreferredError("attempt to remove preferred dataset") del self.datasets[dataset_id] self.async_schedule_save() @@ -120,6 +120,21 @@ class DatasetStore: """Get dataset by id.""" return self.datasets.get(dataset_id) + @property + @callback + def preferred_dataset(self) -> str | None: + """Get the id of the preferred dataset.""" + return self._preferred_dataset + + @preferred_dataset.setter + @callback + def preferred_dataset(self, dataset_id: str) -> None: + """Set the preferred dataset.""" + if dataset_id not in self.datasets: + raise KeyError("unknown dataset") + self._preferred_dataset = dataset_id + self.async_schedule_save() + async def async_load(self) -> None: """Load the datasets.""" data = await self._store.async_load() @@ -139,7 +154,7 @@ class DatasetStore: preferred_dataset = data["preferred_dataset"] self.datasets = datasets - self.preferred_dataset = preferred_dataset + self._preferred_dataset = preferred_dataset @callback def async_schedule_save(self) -> None: @@ -151,7 +166,7 @@ class DatasetStore: """Return data of datasets to store in a file.""" data: dict[str, Any] = {} data["datasets"] = [dataset.to_json() for dataset in self.datasets.values()] - data["preferred_dataset"] = self.preferred_dataset + data["preferred_dataset"] = self._preferred_dataset return data diff --git a/homeassistant/components/thread/websocket_api.py b/homeassistant/components/thread/websocket_api.py index aca0d5e5d96..60941426b7e 100644 --- a/homeassistant/components/thread/websocket_api.py +++ b/homeassistant/components/thread/websocket_api.py @@ -65,13 +65,14 @@ async def ws_set_preferred_dataset( dataset_id = msg["dataset_id"] store = await dataset_store.async_get_store(hass) - if not (store.async_get(dataset_id)): + try: + store.preferred_dataset = dataset_id + except KeyError: connection.send_error( msg["id"], websocket_api.const.ERR_NOT_FOUND, "unknown dataset" ) return - store.preferred_dataset = dataset_id connection.send_result(msg["id"]) From 89b1d5bb684bd971067d62acdd594c469432ba96 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Sat, 15 Apr 2023 15:44:07 -0400 Subject: [PATCH 10/19] Bump env_canada to v0.5.33 (#91468) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 79be96d9bf4..17e0ed6e2ac 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env_canada==0.5.32"] + "requirements": ["env_canada==0.5.33"] } diff --git a/requirements_all.txt b/requirements_all.txt index 651982d4722..064fc3c2eb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -661,7 +661,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.32 +env_canada==0.5.33 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a8e16b9147..4f1b57002c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -517,7 +517,7 @@ energyzero==0.4.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.32 +env_canada==0.5.33 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From b06d624d439a47f85a37e5a7edb80a29b4eae210 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Apr 2023 09:34:07 -1000 Subject: [PATCH 11/19] Fix creating onvif pull point subscriptions when InitialTerminationTime is required (#91470) * Fix creating onvif pull point subscriptions when InitialTerminationTime is required fixes #85902 * Bump again because I got it wrong the first time.. this is why retest is good --- homeassistant/components/onvif/event.py | 20 +++++++++++++------- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index e9fc89d6ef6..5bc2a8248fc 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -32,6 +32,15 @@ def _stringify_onvif_error(error: Exception) -> str: return str(error) +def _get_next_termination_time() -> str: + """Get next termination time.""" + return ( + (dt_util.utcnow() + dt.timedelta(days=1)) + .isoformat(timespec="seconds") + .replace("+00:00", "Z") + ) + + class EventManager: """ONVIF Event Manager.""" @@ -84,7 +93,9 @@ class EventManager: async def async_start(self) -> bool: """Start polling events.""" - if not await self.device.create_pullpoint_subscription(): + if not await self.device.create_pullpoint_subscription( + {"InitialTerminationTime": _get_next_termination_time()} + ): return False # Create subscription manager @@ -171,16 +182,11 @@ class EventManager: if not self._subscription: return - termination_time = ( - (dt_util.utcnow() + dt.timedelta(days=1)) - .isoformat(timespec="seconds") - .replace("+00:00", "Z") - ) with suppress(*SUBSCRIPTION_ERRORS): # The first time we renew, we may get a Fault error so we # suppress it. The subscription will be restarted in # async_restart later. - await self._subscription.Renew(termination_time) + await self._subscription.Renew(_get_next_termination_time()) def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 9095bb2620e..04e26980921 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==1.2.5", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==1.2.7", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 064fc3c2eb6..83ce86e49f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.5 +onvif-zeep-async==1.2.7 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f1b57002c9..af6012e493d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.5 +onvif-zeep-async==1.2.7 # homeassistant.components.opengarage open-garage==0.2.0 From 00a86757faf524814de7d2f5ab786193cb9deb3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Apr 2023 01:56:10 -1000 Subject: [PATCH 12/19] Bump onvif-zeep-async to 1.2.11 (#91472) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 04e26980921..aa06d9c028d 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==1.2.7", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==1.2.11", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 83ce86e49f2..71b961ce954 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.7 +onvif-zeep-async==1.2.11 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af6012e493d..3e75724010e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.7 +onvif-zeep-async==1.2.11 # homeassistant.components.opengarage open-garage==0.2.0 From 5321c60058a0de4ac7ed7eec5d28eb3d72914e30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Apr 2023 02:06:30 -1000 Subject: [PATCH 13/19] Handle a few more transient onvif errors (#91473) --- homeassistant/components/onvif/__init__.py | 22 ++++++- homeassistant/components/onvif/device.py | 71 ++++++++-------------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 77a5c6d1bd8..438a313c3ca 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,5 +1,7 @@ """The ONVIF integration.""" +from httpx import RequestError from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError +from zeep.exceptions import Fault from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS @@ -27,9 +29,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = ONVIFDevice(hass, entry) - if not await device.async_setup(): + try: + await device.async_setup() + except RequestError as err: await device.device.close() - return False + raise ConfigEntryNotReady( + f"Could not connect to camera {device.device.host}:{device.device.port}: {err}" + ) from err + except Fault as err: + await device.device.close() + # We do no know if the credentials are wrong or the camera is + # still booting up, so we will retry later + raise ConfigEntryNotReady( + f"Could not connect to camera, verify credentials are correct: {err}" + ) from err + except ONVIFError as err: + await device.device.close() + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {err}" + ) from err if not device.available: raise ConfigEntryNotReady() diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 1556ae8a1fe..d5baa829d05 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -83,7 +83,7 @@ class ONVIFDevice: """Return the password of this device.""" return self.config_entry.data[CONF_PASSWORD] - async def async_setup(self) -> bool: + async def async_setup(self) -> None: """Set up the device.""" self.device = get_device( self.hass, @@ -94,57 +94,34 @@ class ONVIFDevice: ) # Get all device info - try: - await self.device.update_xaddrs() - await self.async_check_date_and_time() + await self.device.update_xaddrs() + await self.async_check_date_and_time() - # Create event manager - assert self.config_entry.unique_id - self.events = EventManager( - self.hass, self.device, self.config_entry.unique_id - ) + # Create event manager + assert self.config_entry.unique_id + self.events = EventManager(self.hass, self.device, self.config_entry.unique_id) - # Fetch basic device info and capabilities - self.info = await self.async_get_device_info() - LOGGER.debug("Camera %s info = %s", self.name, self.info) - self.capabilities = await self.async_get_capabilities() - LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities) - self.profiles = await self.async_get_profiles() - LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles) + # Fetch basic device info and capabilities + self.info = await self.async_get_device_info() + LOGGER.debug("Camera %s info = %s", self.name, self.info) + self.capabilities = await self.async_get_capabilities() + LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities) + self.profiles = await self.async_get_profiles() + LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles) - # No camera profiles to add - if not self.profiles: - return False + # No camera profiles to add + if not self.profiles: + raise ONVIFError("No camera profiles found") - if self.capabilities.ptz: - self.device.create_ptz_service() + if self.capabilities.ptz: + self.device.create_ptz_service() - # Determine max resolution from profiles - self.max_resolution = max( - profile.video.resolution.width - for profile in self.profiles - if profile.video.encoding == "H264" - ) - except RequestError as err: - LOGGER.warning( - "Couldn't connect to camera '%s', but will retry later. Error: %s", - self.name, - err, - ) - self.available = False - await self.device.close() - except Fault as err: - LOGGER.error( - ( - "Couldn't connect to camera '%s', please verify " - "that the credentials are correct. Error: %s" - ), - self.name, - err, - ) - return False - - return True + # Determine max resolution from profiles + self.max_resolution = max( + profile.video.resolution.width + for profile in self.profiles + if profile.video.encoding == "H264" + ) async def async_stop(self, event=None): """Shut it all down.""" From 572f2cc1672918c030d204d923b86080cd333254 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 17 Apr 2023 10:48:39 +0200 Subject: [PATCH 14/19] Reolink ONVIF move read to primary callback (#91478) * Move read to primary callback * fix styling * Do not raise on ConnectionResetError * Split request.text() to .read() and decode("utf-8") --- homeassistant/components/reolink/host.py | 95 +++++++++++++----------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 1710219cfb3..6ddfa733d8d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -358,58 +358,65 @@ class ReolinkHost: async def handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request ) -> None: - """Shield the incoming webhook callback from cancellation.""" - shielded_future = asyncio.shield( - self._handle_webhook(hass, webhook_id, request) - ) + """Read the incoming webhook from Reolink for inbound messages and schedule processing.""" _LOGGER.debug("Webhook '%s' called", webhook_id) + data: bytes | None = None + try: + data = await request.read() + if not data: + _LOGGER.debug( + "Webhook '%s' triggered with unknown payload: %s", webhook_id, data + ) + except ConnectionResetError: + _LOGGER.debug( + "Webhook '%s' called, but lost connection before reading message " + "(ConnectionResetError), issuing poll", + webhook_id, + ) + return + except aiohttp.ClientResponseError: + _LOGGER.debug( + "Webhook '%s' called, but could not read the message, issuing poll", + webhook_id, + ) + return + except asyncio.CancelledError: + _LOGGER.debug( + "Webhook '%s' called, but lost connection before reading message " + "(CancelledError), issuing poll", + webhook_id, + ) + raise + finally: + # We want handle_webhook to return as soon as possible + # so we process the data in the background, this also shields from cancellation + hass.async_create_background_task( + self._process_webhook_data(hass, webhook_id, data), + "Process Reolink webhook", + ) + + async def _process_webhook_data( + self, hass: HomeAssistant, webhook_id: str, data: bytes | None + ) -> None: + """Process the data from the Reolink webhook.""" + # This task is executed in the background so we need to catch exceptions + # and log them if not self._webhook_reachable.is_set(): self._webhook_reachable.set() ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") - await shielded_future - async def _handle_webhook( - self, hass: HomeAssistant, webhook_id: str, request: Request - ) -> None: - """Handle incoming webhook from Reolink for inbound messages and calls.""" try: - data = await request.text() - except ConnectionResetError: - # We lost the connection before reading the message, fallback to polling - # No need for a background task here as we already know the connection is lost - _LOGGER.debug( - "Webhook '%s' called, but lost connection before reading message, issuing poll", - webhook_id, - ) - if not await self._api.get_motion_state_all_ch(): - _LOGGER.error( - "Could not poll motion state after losing connection during receiving ONVIF event" - ) + if not data: + if not await self._api.get_motion_state_all_ch(): + _LOGGER.error( + "Could not poll motion state after losing connection during receiving ONVIF event" + ) + return + async_dispatcher_send(hass, f"{webhook_id}_all", {}) return - async_dispatcher_send(hass, f"{webhook_id}_all", {}) - return - if not data: - _LOGGER.debug( - "Webhook '%s' triggered with unknown payload: %s", webhook_id, data - ) - return - - # We received the data but we want handle_webhook to return as soon as possible - # so we process the data in the background - hass.async_create_background_task( - self._process_webhook_data(hass, webhook_id, data), - "Process Reolink webhook", - ) - - async def _process_webhook_data( - self, hass: HomeAssistant, webhook_id: str, data: str - ) -> None: - """Process the data from the webhook.""" - # This task is executed in the background so we need to catch exceptions - # and log them - try: - channels = await self._api.ONVIF_event_callback(data) + message = data.decode("utf-8") + channels = await self._api.ONVIF_event_callback(message) except Exception as ex: # pylint: disable=broad-except _LOGGER.exception( "Error processing ONVIF event for Reolink %s: %s", From 489a6e766b56be705becaed1a76e2d3365e3ea67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Apr 2023 02:05:10 -1000 Subject: [PATCH 15/19] Fix onvif failing to reload (#91482) --- homeassistant/components/onvif/__init__.py | 16 ++++++---------- homeassistant/components/onvif/device.py | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 438a313c3ca..45fd04049ad 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -57,15 +57,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = device - platforms = [Platform.BUTTON, Platform.CAMERA] + device.platforms = [Platform.BUTTON, Platform.CAMERA] if device.capabilities.events: - platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] + device.platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] if device.capabilities.imaging: - platforms += [Platform.SWITCH] + device.platforms += [Platform.SWITCH] - await hass.config_entries.async_forward_entry_setups(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, device.platforms) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) @@ -77,16 +77,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - device = hass.data[DOMAIN][entry.unique_id] - platforms = ["camera"] + device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id] if device.capabilities.events and device.events.started: - platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] await device.events.async_stop() - if device.capabilities.imaging: - platforms += [Platform.SWITCH] - return await hass.config_entries.async_unload_platforms(entry, platforms) + return await hass.config_entries.async_unload_platforms(entry, device.platforms) async def _get_snapshot_auth(device): diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d5baa829d05..a9f8625521e 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -55,6 +56,7 @@ class ONVIFDevice: self.capabilities: Capabilities = Capabilities() self.profiles: list[Profile] = [] self.max_resolution: int = 0 + self.platforms: list[Platform] = [] self._dt_diff_seconds: float = 0 From 5ddc18f8ed4cbc783f06be420f8d6615067063a3 Mon Sep 17 00:00:00 2001 From: Ben Morton Date: Sun, 16 Apr 2023 20:32:51 +0100 Subject: [PATCH 16/19] Resolve issue with switchbot blind tilt devices getting stuck in opening/closing state (#91495) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 31ce20bea3f..c90a1a64289 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -40,5 +40,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.37.5"] + "requirements": ["PySwitchbot==0.37.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 71b961ce954..fb1aee74e76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.37.5 +PySwitchbot==0.37.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e75724010e..62831521f0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.37.5 +PySwitchbot==0.37.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 273e1fd2be122756e71ce1059c77b17b8b158252 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 17 Apr 2023 11:09:11 +0200 Subject: [PATCH 17/19] Fix state mapping in fibaro climate (#91505) --- homeassistant/components/fibaro/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 5f34e0d67dd..f4b1cd0c1f5 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -95,6 +95,7 @@ HA_OPMODES_HVAC = { HVACMode.COOL: 2, HVACMode.AUTO: 3, HVACMode.FAN_ONLY: 6, + HVACMode.DRY: 8, } TARGET_TEMP_ACTIONS = ( From 559ce6a275d5fa1c72178f0b7a0563d478d83274 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 16 Apr 2023 23:50:06 -0400 Subject: [PATCH 18/19] Bump unifiprotect to 4.8.1 (#91522) --- homeassistant/components/unifiprotect/config_flow.py | 3 +++ homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/utils.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 3 +++ 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 571922d8651..1ca030ce48e 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging +from pathlib import Path from typing import Any from aiohttp import CookieJar @@ -28,6 +29,7 @@ from homeassistant.helpers.aiohttp_client import ( async_create_clientsession, async_get_clientsession, ) +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address @@ -248,6 +250,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], verify_ssl=verify_ssl, + cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect_cache")), ) errors = {} diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 77570a1cb38..d229d8f71fe 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.7.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.8.1", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index f58bb14eb41..3152213cce8 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator, Iterable import contextlib from enum import Enum +from pathlib import Path import socket from typing import Any @@ -27,6 +28,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_ALL_UPDATES, @@ -142,4 +144,5 @@ def async_create_api_client( override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), ignore_unadopted=False, + cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect_cache")), ) diff --git a/requirements_all.txt b/requirements_all.txt index fb1aee74e76..2421e7303c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2150,7 +2150,7 @@ pytrafikverket==0.2.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.7.0 +pyunifiprotect==4.8.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62831521f0b..dd116a13753 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1540,7 +1540,7 @@ pytrafikverket==0.2.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.7.0 +pyunifiprotect==4.8.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index d66ed0ea060..fcfac60fa71 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -7,6 +7,8 @@ from datetime import datetime, timedelta from functools import partial from ipaddress import IPv4Address import json +from pathlib import Path +from tempfile import gettempdir from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -105,6 +107,7 @@ def mock_ufp_client(bootstrap: Bootstrap): client.bootstrap = bootstrap client._bootstrap = bootstrap client.api_path = "/api" + client.cache_dir = Path(gettempdir()) / "ufp_cache" # functionality from API client tests actually need client._stream_response = partial(ProtectApiClient._stream_response, client) client.get_camera_video = partial(ProtectApiClient.get_camera_video, client) From 940861e2be6c95985e2dab6df0ff17d21b19665f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 17 Apr 2023 15:37:08 +0200 Subject: [PATCH 19/19] Bumped version to 2023.4.5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 845445d6b07..712e8ff0eb6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index ea02d089a30..b2bcdb011b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.4.4" +version = "2023.4.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"