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 765755d1248532..d8623e7bbe53d5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -7,7 +7,11 @@ 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 d163708ce28a34..b4fae8e6f3d654 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 da5e68595447ac..89ad0dd241037a 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 25c42f92cef839..949c7336ea438d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -266,7 +266,7 @@ class NetatmoSensorEntityDescription(SensorEntityDescription, NetatmoRequiredKey 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 6a86ce81445d29..ef2ecc7aa23e94 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -123,7 +123,7 @@ def update_state(self, sia_event: SIAEvent) -> bool: """ 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 715fa26eee98cd..db0845473fdb99 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -132,7 +132,7 @@ def update_state(self, sia_event: SIAEvent) -> bool: """ 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 cbe5e70f688088..202b4a98aa9464 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 cacf317f7bd71e..f263821a46008e 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -34,7 +34,7 @@ def __init__(self, device: ONVIFDevice) -> None: 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 68a4ce525119bd..27f279266ddb99 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -275,7 +275,7 @@ async def async_setup_profiles( 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 @@ async def async_setup_profiles( } ) # 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 f93529ea612cd7..ea2325f271c74b 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -136,7 +136,7 @@ async def async_setup(self) -> None: 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 @@ async def async_stop(self, event=None): 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 @@ async def async_manually_set_date_and_time(self) -> None: 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 @@ async def async_check_date_and_time(self) -> None: 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 @@ async def async_get_capabilities(self): """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 @@ async def async_get_capabilities(self): 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 @@ async def async_start_events(self): 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 @@ async def async_get_profiles(self) -> list[Profile]: ) 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 @@ async def async_get_profiles(self) -> list[Profile]: 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 @@ async def async_perform_ptz( 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 @@ async def async_run_aux_command( 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 @@ async def async_set_imaging_settings( ) 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 851b0f26d1bcbf..92f76b6a95083c 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -392,12 +392,12 @@ async def _async_create_pullpoint_subscription(self) -> bool: 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 17e7f1f0f29daa..9fc0d4178380d0 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 2cdf860f642156..143377025d03e3 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 048f57d1f3d041..114b394ca48c85 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 18de9839e1bbaf..a56e0a477e7a86 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 4c2dda760e4952..4b30bc7bdd155e 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 cdbea2ca346fd0..159bfebc6241b9 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -3,7 +3,7 @@ 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 4ba32c338b5c60..b7f0b5f676349e 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -199,14 +199,10 @@ def _migrate_alexa_entity_settings_v1(self): # 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 16848acc19d043..02aa5760597a78 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -175,23 +175,10 @@ def _migrate_google_entity_settings_v1(self): # 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 3576dadd1c011e..ff78996f3aa6c0 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 143377025d03e3..05145ca6a3e37a 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 114b394ca48c85..8a85ca7465c5cb 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 d134c033ed7752..53a3fb883ef30c 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -137,7 +137,19 @@ def add_all_cluster_handlers(self) -> None: ): 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 c0c455542d3b40..1897383b6c4fa2 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 @@ class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): } ), ] + + +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 61d6262250c0e1..7ce195d68fce08 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -25,6 +25,7 @@ NumberInfo, SelectInfo, SensorInfo, + SensorState, SwitchInfo, TextSensorInfo, UserService, @@ -240,9 +241,18 @@ def async_update_state(self, state: EntityState) -> None: 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 26fab34f0e13fa..d7094a2e60b106 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 05145ca6a3e37a..d045d6adf7c894 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 8a85ca7465c5cb..a98888f3f91cc6 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 b7f0b5f676349e..53bf44d8aa1240 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -216,8 +216,18 @@ async def async_initialize(self): 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 02aa5760597a78..351de5d0e654e5 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -12,6 +12,7 @@ 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 @@ async def async_initialize(self): 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 75e1856503c0dd..5ccc007e5241a8 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -41,8 +41,8 @@ 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 3a7e5a0874ec81..2be2a8eb2bb062 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 45bc56a1700a1a..fe60ca971a197c 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 78d974fe9cfef6..42a51c218b12e2 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -590,7 +590,7 @@ async def async_handle_core_service(call: ServiceCall) -> None: 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_hardware_integration(hass): ) ) - 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 c9c135fcccb818..a5a8ed2f0d23a9 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 d045d6adf7c894..1bfc913792d09f 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 a98888f3f91cc6..31d94ed110750d 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 7eee8fcbe7cbea..1d9334a2657a79 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -102,7 +102,7 @@ } -NESTED_SENSOR_CONFIG = { +NESTED_SENSOR_CONFIG_1 = { "sn": { "Time": "2020-03-03T00:00:00+00:00", "TX23": { @@ -119,6 +119,17 @@ } } +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 badec5be56f9ae..7c9681ff2b4863 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ 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 d3c150305bd0c3..20a02528aff3fa 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 92f76b6a95083c..35df9221934fc9 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -9,7 +9,7 @@ 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 @@ 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 @@ # 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 @@ async def async_stop(self) -> None: 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 @@ async def _async_renew_or_restart_pullpoint( # 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 @@ async def _async_unsubscribe_pullpoint(self) -> None: ) 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 @@ async def _async_renew_pullpoint(self) -> bool: # 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 @@ async def _async_pull_messages_with_lock(self) -> bool: 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 @@ async def _async_pull_messages_with_lock(self) -> bool: # 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 @@ def _async_schedule_webhook_renew(self, delay: float) -> None: 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 @@ async def _async_create_webhook_subscription(self) -> None: 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 @@ async def _async_restart_webhook(self) -> bool: 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 @@ async def _async_renew_webhook(self) -> bool: ): 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 @@ async def _async_renew_or_restart_webhook( # 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 9fc0d4178380d0..f29fd562104955 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 978473caa24909..a88a37f5d20558 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 1bfc913792d09f..5086fd1cd84ee6 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 31d94ed110750d..567ff42482091e 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 4c96f2575f0568..03e242f40b2974 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -38,7 +38,8 @@ 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 41b363b63889bd..4e1e0a74fe9ba7 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 63b89bbe5de7f0..a30652fac8d271 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 5086fd1cd84ee6..5e3d4d52d5c61f 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 567ff42482091e..6762e481d02bac 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 35df9221934fc9..507eda60097122 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -289,7 +289,13 @@ def async_pause(self) -> None: """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 443254e125a0ea..abb1f114ce52f8 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 abb1f114ce52f8..8e6e3e25861c86 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -15,6 +15,19 @@ 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":