From affece88575969f3940910c617fb13290acc3117 Mon Sep 17 00:00:00 2001 From: DDanii Date: Fri, 5 May 2023 08:42:51 +0200 Subject: [PATCH 01/20] Fix transmission error handling (#91548) * transmission error handle fix * added unexpected case tests --- .../components/transmission/__init__.py | 19 +++++++----- .../transmission/test_config_flow.py | 29 +++++++++++++++---- tests/components/transmission/test_init.py | 24 +++++++++++++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 765755d1248..d8623e7bbe5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -7,7 +7,11 @@ import logging from typing import Any import transmission_rpc -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -137,14 +141,13 @@ async def get_api(hass, entry): _LOGGER.debug("Successfully connected to %s", host) return api + except TransmissionAuthError as error: + _LOGGER.error("Credentials for Transmission client are not valid") + raise AuthenticationError from error + except TransmissionConnectError as error: + _LOGGER.error("Connecting to the Transmission client %s failed", host) + raise CannotConnect from error except TransmissionError as error: - if "401: Unauthorized" in str(error): - _LOGGER.error("Credentials for Transmission client are not valid") - raise AuthenticationError from error - if "111: Connection refused" in str(error): - _LOGGER.error("Connecting to the Transmission client %s failed", host) - raise CannotConnect from error - _LOGGER.error(error) raise UnknownError from error diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index d163708ce28..b4fae8e6f3d 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -2,7 +2,11 @@ from unittest.mock import MagicMock, patch import pytest -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) from homeassistant import config_entries from homeassistant.components import transmission @@ -125,7 +129,7 @@ async def test_error_on_wrong_credentials( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG_DATA, @@ -137,6 +141,21 @@ async def test_error_on_wrong_credentials( } +async def test_unexpected_error(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test we handle unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_error_on_connection_failure( hass: HomeAssistant, mock_api: MagicMock ) -> None: @@ -145,7 +164,7 @@ async def test_error_on_connection_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG_DATA, @@ -213,7 +232,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -248,7 +267,7 @@ async def test_reauth_failed_connection_error( assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index da5e6859544..89ad0dd2410 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -3,7 +3,11 @@ from unittest.mock import MagicMock, patch import pytest -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) from homeassistant.components.transmission.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -40,7 +44,7 @@ async def test_setup_failed_connection_error( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.SETUP_RETRY @@ -54,7 +58,21 @@ async def test_setup_failed_auth_error( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() + + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_setup_failed_unexpected_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to unexpected error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + mock_api.side_effect = TransmissionError() await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.SETUP_ERROR From d96b37a0047ccce9dfca8de141013af2c399df24 Mon Sep 17 00:00:00 2001 From: Francesco Carnielli Date: Thu, 4 May 2023 17:36:31 +0200 Subject: [PATCH 02/20] Fix power sensor state_class in Netatmo integration (#92468) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 25c42f92cef..949c7336ea4 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -266,7 +266,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="power", entity_registry_enabled_default=True, native_unit_of_measurement=UnitOfPower.WATT, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, ), ) From b2fcbbe50e16ebb7757ce7bf7db67b00f1322616 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 5 May 2023 10:47:49 +0200 Subject: [PATCH 03/20] Fix for SIA Code not being handled well (#92469) * updated sia requirements * updates because of changes in package * linting and other small fixes * fix for unknown code * added same to alarm_control_panel --- homeassistant/components/sia/alarm_control_panel.py | 2 +- homeassistant/components/sia/binary_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 6a86ce81445..ef2ecc7aa23 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -123,7 +123,7 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity): """ new_state = None if sia_event.code: - new_state = self.entity_description.code_consequences[sia_event.code] + new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False _LOGGER.debug("New state will be %s", new_state) diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index 715fa26eee9..db0845473fd 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -132,7 +132,7 @@ class SIABinarySensor(SIABaseEntity, BinarySensorEntity): """ new_state = None if sia_event.code: - new_state = self.entity_description.code_consequences[sia_event.code] + new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False _LOGGER.debug("New state will be %s", new_state) From b973825833474075aaddf7ca4b09a8eb27570d9f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 4 May 2023 08:35:52 -0700 Subject: [PATCH 04/20] Fix scene service examples (#92501) --- homeassistant/components/scene/services.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index cbe5e70f688..202b4a98aa9 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -29,7 +29,7 @@ apply: name: Entities state description: The entities and the state that they need to be. required: true - example: + example: | light.kitchen: "on" light.ceiling: state: "on" @@ -60,7 +60,7 @@ create: entities: name: Entities state description: The entities to control with the scene. - example: + example: | light.tv_back_light: "on" light.ceiling: state: "on" @@ -70,7 +70,7 @@ create: snapshot_entities: name: Snapshot entities description: The entities of which a snapshot is to be taken - example: + example: | - light.ceiling - light.kitchen selector: From e3762724a3516f69e6956b06c153a55bd7332a7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 May 2023 12:05:29 -0500 Subject: [PATCH 05/20] Fix blocking I/O in the event loop when starting ONVIF (#92518) --- homeassistant/components/onvif/button.py | 2 +- homeassistant/components/onvif/config_flow.py | 4 ++-- homeassistant/components/onvif/device.py | 24 +++++++++---------- homeassistant/components/onvif/event.py | 4 ++-- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/onvif/__init__.py | 4 ++-- tests/components/onvif/test_button.py | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index cacf317f7bd..f263821a460 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -34,7 +34,7 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): async def async_press(self) -> None: """Send out a SystemReboot command.""" - device_mgmt = self.device.device.create_devicemgmt_service() + device_mgmt = await self.device.device.create_devicemgmt_service() await device_mgmt.SystemReboot() diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 68a4ce52511..27f279266dd 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -275,7 +275,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await device.update_xaddrs() - device_mgmt = device.create_devicemgmt_service() + device_mgmt = await device.create_devicemgmt_service() # Get the MAC address to use as the unique ID for the config flow if not self.device_id: try: @@ -314,7 +314,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) # Verify there is an H264 profile - media_service = device.create_media_service() + media_service = await device.create_media_service() profiles = await media_service.GetProfiles() except AttributeError: # Likely an empty document or 404 from the wrong port LOGGER.debug( diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index f93529ea612..ea2325f271c 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -136,7 +136,7 @@ class ONVIFDevice: if self.capabilities.ptz: LOGGER.debug("%s: creating PTZ service", self.name) - self.device.create_ptz_service() + await self.device.create_ptz_service() # Determine max resolution from profiles self.max_resolution = max( @@ -159,7 +159,7 @@ class ONVIFDevice: async def async_manually_set_date_and_time(self) -> None: """Set Date and Time Manually using SetSystemDateAndTime command.""" - device_mgmt = self.device.create_devicemgmt_service() + device_mgmt = await self.device.create_devicemgmt_service() # Retrieve DateTime object from camera to use as template for Set operation device_time = await device_mgmt.GetSystemDateAndTime() @@ -202,7 +202,7 @@ class ONVIFDevice: async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("%s: Setting up the ONVIF device management service", self.name) - device_mgmt = self.device.create_devicemgmt_service() + device_mgmt = await self.device.create_devicemgmt_service() system_date = dt_util.utcnow() LOGGER.debug("%s: Retrieving current device date/time", self.name) @@ -285,7 +285,7 @@ class ONVIFDevice: async def async_get_device_info(self) -> DeviceInfo: """Obtain information about this device.""" - device_mgmt = self.device.create_devicemgmt_service() + device_mgmt = await self.device.create_devicemgmt_service() manufacturer = None model = None firmware_version = None @@ -331,7 +331,7 @@ class ONVIFDevice: """Obtain information about the available services on the device.""" snapshot = False with suppress(*GET_CAPABILITIES_EXCEPTIONS): - media_service = self.device.create_media_service() + media_service = await self.device.create_media_service() media_capabilities = await media_service.GetServiceCapabilities() snapshot = media_capabilities and media_capabilities.SnapshotUri @@ -342,7 +342,7 @@ class ONVIFDevice: imaging = False with suppress(*GET_CAPABILITIES_EXCEPTIONS): - self.device.create_imaging_service() + await self.device.create_imaging_service() imaging = True return Capabilities(snapshot=snapshot, ptz=ptz, imaging=imaging) @@ -361,7 +361,7 @@ class ONVIFDevice: async def async_get_profiles(self) -> list[Profile]: """Obtain media profiles for this device.""" - media_service = self.device.create_media_service() + media_service = await self.device.create_media_service() LOGGER.debug("%s: xaddr for media_service: %s", self.name, media_service.xaddr) try: result = await media_service.GetProfiles() @@ -408,7 +408,7 @@ class ONVIFDevice: ) try: - ptz_service = self.device.create_ptz_service() + ptz_service = await self.device.create_ptz_service() presets = await ptz_service.GetPresets(profile.token) profile.ptz.presets = [preset.token for preset in presets if preset] except GET_CAPABILITIES_EXCEPTIONS: @@ -427,7 +427,7 @@ class ONVIFDevice: async def async_get_stream_uri(self, profile: Profile) -> str: """Get the stream URI for a specified profile.""" - media_service = self.device.create_media_service() + media_service = await self.device.create_media_service() req = media_service.create_type("GetStreamUri") req.ProfileToken = profile.token req.StreamSetup = { @@ -454,7 +454,7 @@ class ONVIFDevice: LOGGER.warning("PTZ actions are not supported on device '%s'", self.name) return - ptz_service = self.device.create_ptz_service() + ptz_service = await self.device.create_ptz_service() pan_val = distance * PAN_FACTOR.get(pan, 0) tilt_val = distance * TILT_FACTOR.get(tilt, 0) @@ -576,7 +576,7 @@ class ONVIFDevice: LOGGER.warning("PTZ actions are not supported on device '%s'", self.name) return - ptz_service = self.device.create_ptz_service() + ptz_service = await self.device.create_ptz_service() LOGGER.debug( "Running Aux Command | Cmd = %s", @@ -607,7 +607,7 @@ class ONVIFDevice: ) return - imaging_service = self.device.create_imaging_service() + imaging_service = await self.device.create_imaging_service() LOGGER.debug("Setting Imaging Setting | Settings = %s", settings) try: diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 851b0f26d1b..92f76b6a950 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -392,12 +392,12 @@ class PullPointManager: return False # Create subscription manager - self._pullpoint_subscription = self._device.create_subscription_service( + self._pullpoint_subscription = await self._device.create_subscription_service( "PullPointSubscription" ) # Create the service that will be used to pull messages from the device. - self._pullpoint_service = self._device.create_pullpoint_service() + self._pullpoint_service = await self._device.create_pullpoint_service() # Initialize events with suppress(*SET_SYNCHRONIZATION_POINT_ERRORS): diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 17e7f1f0f29..9fc0d417838 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==1.3.1", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==2.0.0", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2cdf860f642..143377025d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1264,7 +1264,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.3.1 +onvif-zeep-async==2.0.0 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 048f57d1f3d..114b394ca48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -945,7 +945,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.3.1 +onvif-zeep-async==2.0.0 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 18de9839e1b..a56e0a477e7 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -98,8 +98,8 @@ def setup_mock_onvif_camera( ) else: mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True) - mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt) - mock_onvif_camera.create_media_service = MagicMock(return_value=media_service) + mock_onvif_camera.create_devicemgmt_service = AsyncMock(return_value=devicemgmt) + mock_onvif_camera.create_media_service = AsyncMock(return_value=media_service) mock_onvif_camera.close = AsyncMock(return_value=None) def mock_constructor( diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py index 4c2dda760e4..4b30bc7bdd1 100644 --- a/tests/components/onvif/test_button.py +++ b/tests/components/onvif/test_button.py @@ -27,7 +27,7 @@ async def test_reboot_button(hass: HomeAssistant) -> None: async def test_reboot_button_press(hass: HomeAssistant) -> None: """Test Reboot button press.""" _, camera, _ = await setup_onvif_integration(hass) - devicemgmt = camera.create_devicemgmt_service() + devicemgmt = await camera.create_devicemgmt_service() devicemgmt.SystemReboot = AsyncMock(return_value=True) await hass.services.async_call( From 8a11ee81c41e7327199fa2bc802184f12468304b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 May 2023 05:10:43 +0200 Subject: [PATCH 06/20] Improve cloud migration (#92520) * Improve cloud migration * Tweak * Use entity_ids func --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/alexa/config.py | 4 ++-- .../components/cloud/alexa_config.py | 12 ++++------- .../components/cloud/google_config.py | 21 ++++--------------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index cdbea2ca346..159bfebc624 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod import asyncio import logging -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN @@ -19,7 +19,7 @@ class AbstractConfig(ABC): _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize abstract config.""" self.hass = hass self._store = None diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 4ba32c338b5..b7f0b5f6763 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -199,14 +199,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): # Don't migrate if there's a YAML config return - for state in self.hass.states.async_all(): - async_expose_entity( - self.hass, - CLOUD_ALEXA, - state.entity_id, - self._should_expose_legacy(state.entity_id), - ) - for entity_id in self._prefs.alexa_entity_configs: + for entity_id in { + *self.hass.states.async_entity_ids(), + *self._prefs.alexa_entity_configs, + }: async_expose_entity( self.hass, CLOUD_ALEXA, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 16848acc19d..02aa5760597 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -175,23 +175,10 @@ class CloudGoogleConfig(AbstractConfig): # Don't migrate if there's a YAML config return - for state in self.hass.states.async_all(): - entity_id = state.entity_id - async_expose_entity( - self.hass, - CLOUD_GOOGLE, - entity_id, - self._should_expose_legacy(entity_id), - ) - if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None): - async_set_assistant_option( - self.hass, - CLOUD_GOOGLE, - entity_id, - PREF_DISABLE_2FA, - _2fa_disabled, - ) - for entity_id in self._prefs.google_entity_configs: + for entity_id in { + *self.hass.states.async_entity_ids(), + *self._prefs.google_entity_configs, + }: async_expose_entity( self.hass, CLOUD_GOOGLE, From 241cacde62ec9b80e243c9e3aaac62b9aa314329 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 May 2023 21:18:20 -0500 Subject: [PATCH 07/20] Bump aioesphomeapi to 13.7.3 to fix disconnecting while handshake is in progress (#92537) Bump aioesphomeapi to 13.7.3 fixes #92432 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 3576dadd1c0..ff78996f3aa 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==13.7.2", + "aioesphomeapi==13.7.3", "bluetooth-data-tools==0.4.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 143377025d0..05145ca6a3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.2 +aioesphomeapi==13.7.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 114b394ca48..8a85ca7465c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.2 +aioesphomeapi==13.7.3 # homeassistant.components.flo aioflo==2021.11.0 From 2dd1ce204701463ecbf394d3443aaaa19059aa42 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 4 May 2023 20:02:17 -0400 Subject: [PATCH 08/20] Handle invalid ZHA cluster handlers (#92543) * Do not crash on startup when an invalid cluster handler is encountered * Add a unit test --- homeassistant/components/zha/core/endpoint.py | 14 ++++++- tests/components/zha/test_cluster_handlers.py | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index d134c033ed7..53a3fb883ef 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -137,7 +137,19 @@ class Endpoint: ): cluster_handler_class = MultistateInput # end of ugly hack - cluster_handler = cluster_handler_class(cluster, self) + + try: + cluster_handler = cluster_handler_class(cluster, self) + except KeyError as err: + _LOGGER.warning( + "Cluster handler %s for cluster %s on endpoint %s is invalid: %s", + cluster_handler_class, + cluster, + self, + err, + ) + continue + if cluster_handler.name == const.CLUSTER_HANDLER_POWER_CONFIGURATION: self._device.power_configuration_ch = cluster_handler elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY: diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index c0c455542d3..1897383b6c4 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -1,11 +1,13 @@ """Test ZHA Core cluster handlers.""" import asyncio from collections.abc import Callable +import logging import math from unittest import mock from unittest.mock import AsyncMock, patch import pytest +import zigpy.device import zigpy.endpoint from zigpy.endpoint import Endpoint as ZigpyEndpoint import zigpy.profiles.zha @@ -791,3 +793,41 @@ async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None: } ), ] + + +async def test_invalid_cluster_handler(hass: HomeAssistant, caplog) -> None: + """Test setting up a cluster handler that fails to match properly.""" + + class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): + REPORT_CONFIG = ( + cluster_handlers.AttrReportConfig(attr="missing_attr", config=(1, 60, 1)), + ) + + mock_device = mock.AsyncMock(spec_set=zigpy.device.Device) + zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1) + + cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id) + cluster.configure_reporting_multiple = AsyncMock( + spec_set=cluster.configure_reporting_multiple, + return_value=[ + foundation.ConfigureReportingResponseRecord( + status=foundation.Status.SUCCESS + ) + ], + ) + + mock_zha_device = mock.AsyncMock(spec_set=ZHADevice) + zha_endpoint = Endpoint(zigpy_ep, mock_zha_device) + + # The cluster handler throws an error when matching this cluster + with pytest.raises(KeyError): + TestZigbeeClusterHandler(cluster, zha_endpoint) + + # And one is also logged at runtime + with patch.dict( + registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY, + {cluster.cluster_id: TestZigbeeClusterHandler}, + ), caplog.at_level(logging.WARNING): + zha_endpoint.add_all_cluster_handlers() + + assert "missing_attr" in caplog.text From 163823d2a52370c8dd95d44236646196a54e4878 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 May 2023 21:21:42 -0500 Subject: [PATCH 09/20] Allow duplicate state updates when force_update is set on an esphome sensor (#92553) * Allow duplicate states when force_update is set on an esphome sensor fixes #91221 * Update homeassistant/components/esphome/entry_data.py Co-authored-by: pdw-mb --------- Co-authored-by: pdw-mb --- homeassistant/components/esphome/entry_data.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 61d6262250c..7ce195d68fc 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -25,6 +25,7 @@ from aioesphomeapi import ( NumberInfo, SelectInfo, SensorInfo, + SensorState, SwitchInfo, TextSensorInfo, UserService, @@ -240,9 +241,18 @@ class RuntimeEntryData: current_state_by_type = self.state[state_type] current_state = current_state_by_type.get(key, _SENTINEL) subscription_key = (state_type, key) - if current_state == state and subscription_key not in stale_state: + if ( + current_state == state + and subscription_key not in stale_state + and not ( + type(state) is SensorState # pylint: disable=unidiomatic-typecheck + and (platform_info := self.info.get(Platform.SENSOR)) + and (entity_info := platform_info.get(state.key)) + and (cast(SensorInfo, entity_info)).force_update + ) + ): _LOGGER.debug( - "%s: ignoring duplicate update with and key %s: %s", + "%s: ignoring duplicate update with key %s: %s", self.name, key, state, From 82c0967716fdbf71c298878d44de4824b8af861a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 May 2023 21:20:25 -0500 Subject: [PATCH 10/20] Bump elkm1-lib to 2.2.2 (#92560) changelog: https://github.com/gwww/elkm1/compare/2.2.1...2.2.2 fixes #92467 --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 26fab34f0e1..d7094a2e60b 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/elkm1", "iot_class": "local_push", "loggers": ["elkm1_lib"], - "requirements": ["elkm1-lib==2.2.1"] + "requirements": ["elkm1-lib==2.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 05145ca6a3e..d045d6adf7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -644,7 +644,7 @@ elgato==4.0.1 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.2.1 +elkm1-lib==2.2.2 # homeassistant.components.elmax elmax_api==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a85ca7465c..a98888f3f91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -506,7 +506,7 @@ easyenergy==0.3.0 elgato==4.0.1 # homeassistant.components.elkm1 -elkm1-lib==2.2.1 +elkm1-lib==2.2.2 # homeassistant.components.elmax elmax_api==0.0.4 From e8808b5fe7f00f6566a8e4009ae85be3812810de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 May 2023 08:11:09 -0400 Subject: [PATCH 11/20] Re-run expose entities migration if first time failed (#92564) * Re-run expose entities migration if first time failed * Count number of exposed entities * Add tests --------- Co-authored-by: Erik --- .../components/cloud/alexa_config.py | 12 ++- .../components/cloud/google_config.py | 13 ++- homeassistant/components/cloud/prefs.py | 4 +- tests/components/cloud/test_alexa_config.py | 98 ++++++++++++++++++- tests/components/cloud/test_google_config.py | 98 ++++++++++++++++++- 5 files changed, 219 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index b7f0b5f6763..53bf44d8aa1 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -216,8 +216,18 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): async def on_hass_started(hass): if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION: - if self._prefs.alexa_settings_version < 2: + if self._prefs.alexa_settings_version < 2 or ( + # Recover from a bug we had in 2023.5.0 where entities didn't get exposed + self._prefs.alexa_settings_version < 3 + and not any( + settings.get("should_expose", False) + for settings in async_get_assistant_settings( + hass, CLOUD_ALEXA + ).values() + ) + ): self._migrate_alexa_entity_settings_v1() + await self._prefs.async_update( alexa_settings_version=ALEXA_SETTINGS_VERSION ) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 02aa5760597..351de5d0e65 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -12,6 +12,7 @@ from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.components.homeassistant.exposed_entities import ( async_expose_entity, + async_get_assistant_settings, async_get_entity_settings, async_listen_entity_updates, async_set_assistant_option, @@ -200,8 +201,18 @@ class CloudGoogleConfig(AbstractConfig): async def on_hass_started(hass: HomeAssistant) -> None: if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION: - if self._prefs.google_settings_version < 2: + if self._prefs.google_settings_version < 2 or ( + # Recover from a bug we had in 2023.5.0 where entities didn't get exposed + self._prefs.google_settings_version < 3 + and not any( + settings.get("should_expose", False) + for settings in async_get_assistant_settings( + hass, CLOUD_GOOGLE + ).values() + ) + ): self._migrate_google_entity_settings_v1() + await self._prefs.async_update( google_settings_version=GOOGLE_SETTINGS_VERSION ) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 75e1856503c..5ccc007e524 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -41,8 +41,8 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 STORAGE_VERSION_MINOR = 2 -ALEXA_SETTINGS_VERSION = 2 -GOOGLE_SETTINGS_VERSION = 2 +ALEXA_SETTINGS_VERSION = 3 +GOOGLE_SETTINGS_VERSION = 3 class CloudPreferencesStore(Store): diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 3a7e5a0874e..2be2a8eb2bb 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -542,11 +542,13 @@ async def test_alexa_handle_logout( assert len(mock_enable.return_value.mock_calls) == 1 +@pytest.mark.parametrize("alexa_settings_version", [1, 2]) async def test_alexa_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub, entity_registry: er.EntityRegistry, + alexa_settings_version: int, ) -> None: """Test migrating Alexa entity config.""" hass.state = CoreState.starting @@ -593,7 +595,7 @@ async def test_alexa_config_migrate_expose_entity_prefs( await cloud_prefs.async_update( alexa_enabled=True, alexa_report_state=False, - alexa_settings_version=1, + alexa_settings_version=alexa_settings_version, ) expose_entity(hass, entity_migrated.entity_id, False) @@ -641,6 +643,100 @@ async def test_alexa_config_migrate_expose_entity_prefs( } +async def test_alexa_config_migrate_expose_entity_prefs_v2_no_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Alexa entity config from v2 to v3 when no entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + alexa_enabled=True, + alexa_report_state=False, + alexa_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, False) + + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = alexa_config.CloudAlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.alexa": {"should_expose": True} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.alexa": {"should_expose": True} + } + + +async def test_alexa_config_migrate_expose_entity_prefs_v2_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Alexa entity config from v2 to v3 when an entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + alexa_enabled=True, + alexa_report_state=False, + alexa_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, True) + + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = alexa_config.CloudAlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.alexa": {"should_expose": False} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.alexa": {"should_expose": True} + } + + async def test_alexa_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 45bc56a1700..fe60ca971a1 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -483,10 +483,12 @@ async def test_google_handle_logout( assert len(mock_enable.return_value.mock_calls) == 1 +@pytest.mark.parametrize("google_settings_version", [1, 2]) async def test_google_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, entity_registry: er.EntityRegistry, + google_settings_version: int, ) -> None: """Test migrating Google entity config.""" hass.state = CoreState.starting @@ -540,7 +542,7 @@ async def test_google_config_migrate_expose_entity_prefs( await cloud_prefs.async_update( google_enabled=True, google_report_state=False, - google_settings_version=1, + google_settings_version=google_settings_version, ) expose_entity(hass, entity_migrated.entity_id, False) @@ -596,6 +598,100 @@ async def test_google_config_migrate_expose_entity_prefs( } +async def test_google_config_migrate_expose_entity_prefs_v2_no_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Google entity config from v2 to v3 when no entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + google_enabled=True, + google_report_state=False, + google_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, False) + + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.google_assistant": {"should_expose": True} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.google_assistant": {"should_expose": True} + } + + +async def test_google_config_migrate_expose_entity_prefs_v2_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Google entity config from v2 to v3 when an entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + google_enabled=True, + google_report_state=False, + google_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, True) + + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.google_assistant": {"should_expose": False} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.google_assistant": {"should_expose": True} + } + + async def test_google_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, From f8c3586f6bf41b45029024932fca169d708ce265 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 May 2023 14:43:56 +0200 Subject: [PATCH 12/20] Fix hassio get_os_info retry (#92569) --- homeassistant/components/hassio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 78d974fe9cf..42a51c218b1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -590,7 +590,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await async_setup_addon_panel(hass, hassio) # Setup hardware integration for the detected board type - async def _async_setup_hardware_integration(hass): + async def _async_setup_hardware_integration(_: datetime) -> None: """Set up hardaware integration for the detected board type.""" if (os_info := get_os_info(hass)) is None: # os info not yet fetched from supervisor, retry later @@ -610,7 +610,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ) ) - await _async_setup_hardware_integration(hass) + await _async_setup_hardware_integration(datetime.now()) hass.async_create_task( hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) From fb29e1a14e5971c04821356fcc025a9acecd5c6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 May 2023 14:40:30 +0200 Subject: [PATCH 13/20] Bump hatasmota to 0.6.5 (#92585) * Bump hatasmota to 0.6.5 * Fix tests --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 102 ++++++++++++++---- 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index c9c135fcccb..a5a8ed2f0d2 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["hatasmota==0.6.4"] + "requirements": ["hatasmota==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index d045d6adf7c..1bfc913792d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -881,7 +881,7 @@ hass_splunk==0.1.1 hassil==1.0.6 # homeassistant.components.tasmota -hatasmota==0.6.4 +hatasmota==0.6.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a98888f3f91..31d94ed1107 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -679,7 +679,7 @@ hass-nabucasa==0.66.2 hassil==1.0.6 # homeassistant.components.tasmota -hatasmota==0.6.4 +hatasmota==0.6.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 7eee8fcbe7c..1d9334a2657 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -102,7 +102,7 @@ INDEXED_SENSOR_CONFIG_2 = { } -NESTED_SENSOR_CONFIG = { +NESTED_SENSOR_CONFIG_1 = { "sn": { "Time": "2020-03-03T00:00:00+00:00", "TX23": { @@ -119,6 +119,17 @@ NESTED_SENSOR_CONFIG = { } } +NESTED_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2023-01-27T11:04:56", + "DS18B20": { + "Id": "01191ED79190", + "Temperature": 2.4, + }, + "TempUnit": "C", + } +} + async def test_controlling_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota @@ -174,12 +185,59 @@ async def test_controlling_state_via_mqtt( assert state.state == "20.0" +@pytest.mark.parametrize( + ("sensor_config", "entity_ids", "messages", "states"), + [ + ( + NESTED_SENSOR_CONFIG_1, + ["sensor.tasmota_tx23_speed_act", "sensor.tasmota_tx23_dir_card"], + ( + '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', + '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', + ), + ( + { + "sensor.tasmota_tx23_speed_act": "12.3", + "sensor.tasmota_tx23_dir_card": "WSW", + }, + { + "sensor.tasmota_tx23_speed_act": "23.4", + "sensor.tasmota_tx23_dir_card": "ESE", + }, + ), + ), + ( + NESTED_SENSOR_CONFIG_2, + ["sensor.tasmota_ds18b20_temperature", "sensor.tasmota_ds18b20_id"], + ( + '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', + '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', + ), + ( + { + "sensor.tasmota_ds18b20_temperature": "12.3", + "sensor.tasmota_ds18b20_id": "01191ED79190", + }, + { + "sensor.tasmota_ds18b20_temperature": "23.4", + "sensor.tasmota_ds18b20_id": "meep", + }, + ), + ), + ], +) async def test_nested_sensor_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + setup_tasmota, + sensor_config, + entity_ids, + messages, + states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) + sensor_config = copy.deepcopy(sensor_config) mac = config["mac"] async_fire_mqtt_message( @@ -195,31 +253,29 @@ async def test_nested_sensor_state_via_mqtt( ) await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"TX23":{"Speed":{"Act":"12.3"}}}' - ) - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "12.3" + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == states[0][entity_id] # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"}}}}', - ) - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "23.4" + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == states[1][entity_id] async def test_indexed_sensor_state_via_mqtt( @@ -728,7 +784,7 @@ async def test_nested_sensor_attributes( ) -> None: """Test correct attributes for sensors.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) + sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1) mac = config["mac"] async_fire_mqtt_message( @@ -754,7 +810,7 @@ async def test_nested_sensor_attributes( assert state.attributes.get("device_class") is None assert state.attributes.get("friendly_name") == "Tasmota TX23 Dir Avg" assert state.attributes.get("icon") is None - assert state.attributes.get("unit_of_measurement") == " " + assert state.attributes.get("unit_of_measurement") is None async def test_indexed_sensor_attributes( From 15ef53cd9ade48ce7e847eace14bde7a9508a393 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 May 2023 08:47:12 -0400 Subject: [PATCH 14/20] Bumped version to 2023.5.2 --- 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 badec5be56f..7c9681ff2b4 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 = 5 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __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 d3c150305bd..20a02528aff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.5.1" +version = "2023.5.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 35c48d3d0ec82e68dd27a1ea4bc829ceab368624 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 May 2023 13:26:58 -0500 Subject: [PATCH 15/20] Improve reliability of ONVIF subscription renewals (#92551) * Improve reliablity of onvif subscription renewals upstream changelog: https://github.com/hunterjm/python-onvif-zeep-async/compare/v2.0.0...v2.1.0 * ``` Traceback (most recent call last): File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/onvif/client.py", line 75, in _async_wrap_connection_error_retry return await func(*args, **kwargs) File "/Users/bdraco/home-assistant/homeassistant/components/onvif/event.py", line 441, in _async_call_pullpoint_subscription_renew await self._pullpoint_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/zeep/proxy.py", line 64, in __call__ return await self._proxy._binding.send_async( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/zeep/wsdl/bindings/soap.py", line 156, in send_async response = await client.transport.post_xml( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/zeep/transports.py", line 235, in post_xml response = await self.post(address, message, headers) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/zeep/transports.py", line 220, in post response = await self.client.post( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1845, in post return await self.request( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1530, in request return await self.send(request, auth=auth, follow_redirects=follow_redirects) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1617, in send response = await self._send_handling_auth( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1645, in _send_handling_auth response = await self._send_handling_redirects( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1682, in _send_handling_redirects response = await self._send_single_request(request) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_client.py", line 1719, in _send_single_request response = await transport.handle_async_request(request) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 352, in handle_async_request with map_httpcore_exceptions(): File "/opt/homebrew/Cellar/python@3.10/3.10.10_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/contextlib.py", line 153, in __exit__ self.gen.throw(typ, value, traceback) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 77, in map_httpcore_exceptions raise mapped_exc(message) from exc httpx.ReadTimeout ``` * adjust timeouts for slower tplink cameras * tweak * more debug * tweak * adjust message * tweak * Revert "tweak" This reverts commit 10ee2a8de70e93dc5be85b1992ec4d30c2188344. * give time in seconds * revert * revert * Update homeassistant/components/onvif/event.py * Update homeassistant/components/onvif/event.py --- homeassistant/components/onvif/event.py | 134 ++++++++----------- homeassistant/components/onvif/manifest.json | 2 +- homeassistant/components/onvif/util.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 62 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 92f76b6a950..35df9221934 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -9,7 +9,7 @@ import datetime as dt from aiohttp.web import Request from httpx import RemoteProtocolError, RequestError, TransportError from onvif import ONVIFCamera, ONVIFService -from onvif.client import NotificationManager +from onvif.client import NotificationManager, retry_connection_error from onvif.exceptions import ONVIFError from zeep.exceptions import Fault, ValidationError, XMLParseError @@ -40,8 +40,8 @@ SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError) UNSUBSCRIBE_ERRORS = (XMLParseError, *SUBSCRIPTION_ERRORS) RENEW_ERRORS = (ONVIFError, RequestError, XMLParseError, *SUBSCRIPTION_ERRORS) # -# We only keep the subscription alive for 3 minutes, and will keep -# renewing it every 1.5 minutes. This is to avoid the camera +# We only keep the subscription alive for 10 minutes, and will keep +# renewing it every 8 minutes. This is to avoid the camera # accumulating subscriptions which will be impossible to clean up # since ONVIF does not provide a way to list existing subscriptions. # @@ -49,12 +49,25 @@ RENEW_ERRORS = (ONVIFError, RequestError, XMLParseError, *SUBSCRIPTION_ERRORS) # sending events to us, and we will not be able to recover until # the subscriptions expire or the camera is rebooted. # -SUBSCRIPTION_TIME = dt.timedelta(minutes=3) -SUBSCRIPTION_RELATIVE_TIME = ( - "PT3M" # use relative time since the time on the camera is not reliable -) -SUBSCRIPTION_RENEW_INTERVAL = SUBSCRIPTION_TIME.total_seconds() / 2 -SUBSCRIPTION_RENEW_INTERVAL_ON_ERROR = 60.0 +SUBSCRIPTION_TIME = dt.timedelta(minutes=10) + +# SUBSCRIPTION_RELATIVE_TIME uses a relative time since the time on the camera +# is not reliable. We use 600 seconds (10 minutes) since some cameras cannot +# parse time in the format "PT10M" (10 minutes). +SUBSCRIPTION_RELATIVE_TIME = "PT600S" + +# SUBSCRIPTION_RENEW_INTERVAL Must be less than the +# overall timeout of 90 * (SUBSCRIPTION_ATTEMPTS) 2 = 180 seconds +# +# We use 8 minutes between renewals to make sure we never hit the +# 10 minute limit even if the first renewal attempt fails +SUBSCRIPTION_RENEW_INTERVAL = 8 * 60 + +# The number of attempts to make when creating or renewing a subscription +SUBSCRIPTION_ATTEMPTS = 2 + +# The time to wait before trying to restart the subscription if it fails +SUBSCRIPTION_RESTART_INTERVAL_ON_ERROR = 60 PULLPOINT_POLL_TIME = dt.timedelta(seconds=60) PULLPOINT_MESSAGE_LIMIT = 100 @@ -327,20 +340,7 @@ class PullPointManager: async def _async_start_pullpoint(self) -> bool: """Start pullpoint subscription.""" try: - try: - started = await self._async_create_pullpoint_subscription() - except RequestError: - # - # We should only need to retry on RemoteProtocolError but some cameras - # are flaky and sometimes do not respond to the Renew request so we - # retry on RequestError as well. - # - # For RemoteProtocolError: - # http://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 allows the server - # to close the connection at any time, we treat this as a normal and try again - # once since we do not want to declare the camera as not supporting PullPoint - # if it just happened to close the connection at the wrong time. - started = await self._async_create_pullpoint_subscription() + started = await self._async_create_pullpoint_subscription() except CREATE_ERRORS as err: LOGGER.debug( "%s: Device does not support PullPoint service or has too many subscriptions: %s", @@ -372,16 +372,16 @@ class PullPointManager: # scheduled when the current one is done if needed. return async with self._renew_lock: - next_attempt = SUBSCRIPTION_RENEW_INTERVAL_ON_ERROR + next_attempt = SUBSCRIPTION_RESTART_INTERVAL_ON_ERROR try: - if ( - await self._async_renew_pullpoint() - or await self._async_restart_pullpoint() - ): + if await self._async_renew_pullpoint(): next_attempt = SUBSCRIPTION_RENEW_INTERVAL + else: + await self._async_restart_pullpoint() finally: self.async_schedule_pullpoint_renew(next_attempt) + @retry_connection_error(SUBSCRIPTION_ATTEMPTS) async def _async_create_pullpoint_subscription(self) -> bool: """Create pullpoint subscription.""" @@ -447,6 +447,11 @@ class PullPointManager: ) self._pullpoint_subscription = None + @retry_connection_error(SUBSCRIPTION_ATTEMPTS) + async def _async_call_pullpoint_subscription_renew(self) -> None: + """Call PullPoint subscription Renew.""" + await self._pullpoint_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) + async def _async_renew_pullpoint(self) -> bool: """Renew the PullPoint subscription.""" if ( @@ -458,20 +463,7 @@ class PullPointManager: # The first time we renew, we may get a Fault error so we # suppress it. The subscription will be restarted in # async_restart later. - try: - await self._pullpoint_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) - except RequestError: - # - # We should only need to retry on RemoteProtocolError but some cameras - # are flaky and sometimes do not respond to the Renew request so we - # retry on RequestError as well. - # - # For RemoteProtocolError: - # http://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 allows the server - # to close the connection at any time, we treat this as a normal and try again - # once since we do not want to mark events as stale - # if it just happened to close the connection at the wrong time. - await self._pullpoint_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) + await self._async_call_pullpoint_subscription_renew() LOGGER.debug("%s: Renewed PullPoint subscription", self._name) return True except RENEW_ERRORS as err: @@ -521,7 +513,7 @@ class PullPointManager: stringify_onvif_error(err), ) return True - except (XMLParseError, *SUBSCRIPTION_ERRORS) as err: + except Fault as err: # Device may not support subscriptions so log at debug level # when we get an XMLParseError LOGGER.debug( @@ -532,6 +524,16 @@ class PullPointManager: # Treat errors as if the camera restarted. Assume that the pullpoint # subscription is no longer valid. return False + except (XMLParseError, RequestError, TimeoutError, TransportError) as err: + LOGGER.debug( + "%s: PullPoint subscription encountered an unexpected error and will be retried " + "(this is normal for some cameras): %s", + self._name, + stringify_onvif_error(err), + ) + # Avoid renewing the subscription too often since it causes problems + # for some cameras, mainly the Tapo ones. + return True if self.state != PullPointManagerState.STARTED: # If the webhook became started working during the long poll, @@ -655,6 +657,7 @@ class WebHookManager: self._renew_or_restart_job, ) + @retry_connection_error(SUBSCRIPTION_ATTEMPTS) async def _async_create_webhook_subscription(self) -> None: """Create webhook subscription.""" LOGGER.debug( @@ -689,20 +692,7 @@ class WebHookManager: async def _async_start_webhook(self) -> bool: """Start webhook.""" try: - try: - await self._async_create_webhook_subscription() - except RequestError: - # - # We should only need to retry on RemoteProtocolError but some cameras - # are flaky and sometimes do not respond to the Renew request so we - # retry on RequestError as well. - # - # For RemoteProtocolError: - # http://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 allows the server - # to close the connection at any time, we treat this as a normal and try again - # once since we do not want to declare the camera as not supporting webhooks - # if it just happened to close the connection at the wrong time. - await self._async_create_webhook_subscription() + await self._async_create_webhook_subscription() except CREATE_ERRORS as err: self._event_manager.async_webhook_failed() LOGGER.debug( @@ -720,6 +710,12 @@ class WebHookManager: await self._async_unsubscribe_webhook() return await self._async_start_webhook() + @retry_connection_error(SUBSCRIPTION_ATTEMPTS) + async def _async_call_webhook_subscription_renew(self) -> None: + """Call PullPoint subscription Renew.""" + assert self._webhook_subscription is not None + await self._webhook_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) + async def _async_renew_webhook(self) -> bool: """Renew webhook subscription.""" if ( @@ -728,20 +724,7 @@ class WebHookManager: ): return False try: - try: - await self._webhook_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) - except RequestError: - # - # We should only need to retry on RemoteProtocolError but some cameras - # are flaky and sometimes do not respond to the Renew request so we - # retry on RequestError as well. - # - # For RemoteProtocolError: - # http://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 allows the server - # to close the connection at any time, we treat this as a normal and try again - # once since we do not want to mark events as stale - # if it just happened to close the connection at the wrong time. - await self._webhook_subscription.Renew(SUBSCRIPTION_RELATIVE_TIME) + await self._async_call_webhook_subscription_renew() LOGGER.debug("%s: Renewed Webhook subscription", self._name) return True except RENEW_ERRORS as err: @@ -765,13 +748,12 @@ class WebHookManager: # scheduled when the current one is done if needed. return async with self._renew_lock: - next_attempt = SUBSCRIPTION_RENEW_INTERVAL_ON_ERROR + next_attempt = SUBSCRIPTION_RESTART_INTERVAL_ON_ERROR try: - if ( - await self._async_renew_webhook() - or await self._async_restart_webhook() - ): + if await self._async_renew_webhook(): next_attempt = SUBSCRIPTION_RENEW_INTERVAL + else: + await self._async_restart_webhook() finally: self._async_schedule_webhook_renew(next_attempt) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 9fc0d417838..f29fd562104 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==2.0.0", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==2.1.1", "WSDiscovery==2.0.0"] } diff --git a/homeassistant/components/onvif/util.py b/homeassistant/components/onvif/util.py index 978473caa24..a88a37f5d20 100644 --- a/homeassistant/components/onvif/util.py +++ b/homeassistant/components/onvif/util.py @@ -34,7 +34,7 @@ def stringify_onvif_error(error: Exception) -> str: message += f" (actor:{error.actor})" else: message = str(error) - return message or "Device sent empty error" + return message or f"Device sent empty error with type {type(error)}" def is_auth_error(error: Exception) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 1bfc913792d..5086fd1cd84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1264,7 +1264,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==2.0.0 +onvif-zeep-async==2.1.1 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31d94ed1107..567ff424820 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -945,7 +945,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==2.0.0 +onvif-zeep-async==2.1.1 # homeassistant.components.opengarage open-garage==0.2.0 From cf243fbe1117b36f4d5cd8eadd28461c4a43aa71 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 May 2023 20:27:28 +0200 Subject: [PATCH 16/20] Lower scan interval for OpenSky (#92593) * Lower scan interval for opensky to avoid hitting rate limit * Lower scan interval for opensky to avoid hitting rate limit * Update homeassistant/components/opensky/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/opensky/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/opensky/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 4c96f2575f0..03e242f40b2 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -38,7 +38,8 @@ DEFAULT_ALTITUDE = 0 EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry" EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" -SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds +# OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour +SCAN_INTERVAL = timedelta(minutes=15) OPENSKY_API_URL = "https://opensky-network.org/api/states/all" OPENSKY_API_FIELDS = [ From f1bccef224e8cde1aacde6d721e269119d99fd0b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 5 May 2023 20:27:48 +0200 Subject: [PATCH 17/20] Update frontend to 20230503.3 (#92617) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 41b363b6388..4e1e0a74fe9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230503.2"] + "requirements": ["home-assistant-frontend==20230503.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63b89bbe5de..a30652fac8d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.0.0 hass-nabucasa==0.66.2 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230503.2 +home-assistant-frontend==20230503.3 home-assistant-intents==2023.4.26 httpx==0.24.0 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 5086fd1cd84..5e3d4d52d5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -911,7 +911,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230503.2 +home-assistant-frontend==20230503.3 # homeassistant.components.conversation home-assistant-intents==2023.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 567ff424820..6762e481d02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -700,7 +700,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230503.2 +home-assistant-frontend==20230503.3 # homeassistant.components.conversation home-assistant-intents==2023.4.26 From 73d4c73dbb73555559f6c66ba6b9f88a85963403 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 May 2023 13:38:36 -0500 Subject: [PATCH 18/20] Fix missing ONVIF events when switching from PullPoint to webhooks (#92627) We now let the PullPoint subscription expire instead of explicitly unsubscribing when pausing the subscription. We will still unsubscribe it if Home Assistant is shutdown or the integration is reloaded Some cameras will cancel ALL subscriptions when we do an unsubscribe so we want to let the PullPoint subscription expire instead of explicitly cancelling it. --- homeassistant/components/onvif/event.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 35df9221934..507eda60097 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -289,7 +289,13 @@ class PullPointManager: """Pause pullpoint subscription.""" LOGGER.debug("%s: Pausing PullPoint manager", self._name) self.state = PullPointManagerState.PAUSED - self._hass.async_create_task(self._async_cancel_and_unsubscribe()) + # Cancel the renew job so we don't renew the subscription + # and stop pulling messages. + self._async_cancel_pullpoint_renew() + self.async_cancel_pull_messages() + # We do not unsubscribe from the pullpoint subscription and instead + # let the subscription expire since some cameras will terminate all + # subscriptions if we unsubscribe which will break the webhook. @callback def async_resume(self) -> None: From fe57901b5f7c5be935ea33cc4ab18a436ee3dc72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 May 2023 05:19:27 -0500 Subject: [PATCH 19/20] Add support for visitor detections to onvif (#92350) --- homeassistant/components/onvif/parsers.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 443254e125a..abb1f114ce5 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -401,6 +401,31 @@ async def async_parse_face_detector(uid: str, msg) -> Event | None: return None +@PARSERS.register("tns1:RuleEngine/MyRuleDetector/Visitor") +# pylint: disable=protected-access +async def async_parse_visitor_detector(uid: str, msg) -> Event | None: + """Handle parsing event message. + + Topic: tns1:RuleEngine/MyRuleDetector/Visitor + """ + try: + video_source = "" + for source in msg.Message._value_1.Source.SimpleItem: + if source.Name == "Source": + video_source = source.Value + + return Event( + f"{uid}_{msg.Topic._value_1}_{video_source}", + "Visitor Detection", + "binary_sensor", + "occupancy", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + @PARSERS.register("tns1:Device/Trigger/DigitalInput") # pylint: disable=protected-access async def async_parse_digital_input(uid: str, msg) -> Event | None: From ddebfb3ac5a97fe4c4eba4989698c0faa9ac0bb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 May 2023 13:32:55 -0500 Subject: [PATCH 20/20] Fix duplicate ONVIF sensors (#92629) Some cameras do not configure the video source correctly when using webhooks but work fine with PullPoint which results in duplicate sensors --- homeassistant/components/onvif/parsers.py | 33 ++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index abb1f114ce5..8e6e3e25861 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -15,6 +15,19 @@ PARSERS: Registry[ str, Callable[[str, Any], Coroutine[Any, Any, Event | None]] ] = Registry() +VIDEO_SOURCE_MAPPING = { + "vsconf": "VideoSourceToken", +} + + +def _normalize_video_source(source: str) -> str: + """Normalize video source. + + Some cameras do not set the VideoSourceToken correctly so we get duplicate + sensors, so we need to normalize it to the correct value. + """ + return VIDEO_SOURCE_MAPPING.get(source, source) + def local_datetime_or_none(value: str) -> datetime.datetime | None: """Convert strings to datetimes, if invalid, return None.""" @@ -188,7 +201,7 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None: rule = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": - video_source = source.Value + video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": video_analytics = source.Value if source.Name == "Rule": @@ -220,7 +233,7 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None: rule = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": - video_source = source.Value + video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": video_analytics = source.Value if source.Name == "Rule": @@ -251,7 +264,7 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None: rule = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": - video_source = source.Value + video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": video_analytics = source.Value if source.Name == "Rule": @@ -282,7 +295,7 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None: rule = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": - video_source = source.Value + video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": video_analytics = source.Value if source.Name == "Rule": @@ -312,7 +325,7 @@ async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None: video_source = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "Source": - video_source = source.Value + video_source = _normalize_video_source(source.Value) return Event( f"{uid}_{msg.Topic._value_1}_{video_source}", @@ -337,7 +350,7 @@ async def async_parse_vehicle_detector(uid: str, msg) -> Event | None: video_source = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "Source": - video_source = source.Value + video_source = _normalize_video_source(source.Value) return Event( f"{uid}_{msg.Topic._value_1}_{video_source}", @@ -362,7 +375,7 @@ async def async_parse_person_detector(uid: str, msg) -> Event | None: video_source = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "Source": - video_source = source.Value + video_source = _normalize_video_source(source.Value) return Event( f"{uid}_{msg.Topic._value_1}_{video_source}", @@ -387,7 +400,7 @@ async def async_parse_face_detector(uid: str, msg) -> Event | None: video_source = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "Source": - video_source = source.Value + video_source = _normalize_video_source(source.Value) return Event( f"{uid}_{msg.Topic._value_1}_{video_source}", @@ -412,7 +425,7 @@ async def async_parse_visitor_detector(uid: str, msg) -> Event | None: video_source = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "Source": - video_source = source.Value + video_source = _normalize_video_source(source.Value) return Event( f"{uid}_{msg.Topic._value_1}_{video_source}", @@ -683,7 +696,7 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None: rule = "" for source in msg.Message._value_1.Source.SimpleItem: if source.Name == "VideoSourceConfigurationToken": - video_source = source.Value + video_source = _normalize_video_source(source.Value) if source.Name == "VideoAnalyticsConfigurationToken": video_analytics = source.Value if source.Name == "Rule":