From f4830216a8b5e27fe204db1d97df49791ed644b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 4 May 2024 20:17:21 +0200 Subject: [PATCH 01/22] Add workaround for data entry flow show progress (#116704) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/data_entry_flow.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f628879a7fd153..0bd494992b6dde 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -352,6 +352,18 @@ async def async_configure( ) -> _FlowResultT: """Continue a data entry flow.""" result: _FlowResultT | None = None + + # Workaround for flow handlers which have not been upgraded to pass a show + # progress task, needed because of the change to eager tasks in HA Core 2024.5, + # can be removed in HA Core 2024.8. + flow = self._progress.get(flow_id) + if flow and flow.deprecated_show_progress: + if (cur_step := flow.cur_step) and cur_step[ + "type" + ] == FlowResultType.SHOW_PROGRESS: + # Allow the progress task to finish before we call the flow handler + await asyncio.sleep(0) + while not result or result["type"] == FlowResultType.SHOW_PROGRESS_DONE: result = await self._async_configure(flow_id, user_input) flow = self._progress.get(flow_id) From 17c5aa287190c4d61c88a4b7ed0abe5be6f04c65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 May 2024 17:35:44 -0500 Subject: [PATCH 02/22] Improve logging of _TrackPointUTCTime objects (#116711) --- homeassistant/helpers/event.py | 14 ++++++++++---- tests/helpers/test_event.py | 18 ++++++++++++++++++ tests/ignore_uncaught_exceptions.py | 6 ++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5cffe992c0d3dd..5c026064c28d7e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1436,12 +1436,18 @@ def async_attach(self) -> None: """Initialize track job.""" loop = self.hass.loop self._cancel_callback = loop.call_at( - loop.time() + self.expected_fire_timestamp - time.time(), self._run_action + loop.time() + self.expected_fire_timestamp - time.time(), self ) @callback - def _run_action(self) -> None: - """Call the action.""" + def __call__(self) -> None: + """Call the action. + + We implement this as __call__ so when debug logging logs the object + it shows the name of the job. This is especially helpful when asyncio + debug logging is enabled as we can see the name of the job that is + being called that is blocking the event loop. + """ # Depending on the available clock support (including timer hardware # and the OS kernel) it can happen that we fire a little bit too early # as measured by utcnow(). That is bad when callbacks have assumptions @@ -1450,7 +1456,7 @@ def _run_action(self) -> None: if (delta := (self.expected_fire_timestamp - time_tracker_timestamp())) > 0: _LOGGER.debug("Called %f seconds too early, rearming", delta) loop = self.hass.loop - self._cancel_callback = loop.call_at(loop.time() + delta, self._run_action) + self._cancel_callback = loop.call_at(loop.time() + delta, self) return self.hass.async_run_hass_job(self.job, self.utc_point_in_time) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 07228abcc2cbd3..a6fad968eac650 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4819,3 +4819,21 @@ async def test_track_state_change_deprecated( "of `async_track_state_change_event` which is deprecated and " "will be removed in Home Assistant 2025.5. Please report this issue." ) in caplog.text + + +async def test_track_point_in_time_repr( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test track point in time.""" + + @ha.callback + def _raise_exception(_): + raise RuntimeError("something happened and its poorly described") + + async_track_point_in_utc_time(hass, _raise_exception, dt_util.utcnow()) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Exception in callback _TrackPointUTCTime" in caplog.text + assert "._raise_exception" in caplog.text + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 3be2093057bd23..aaf6cbe3efe159 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -7,6 +7,12 @@ "tests.test_runner", "test_unhandled_exception_traceback", ), + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.helpers.test_event", + "test_track_point_in_time_repr", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup", From 6d537e2a6690b0593d0d8a093eb22f9376167e48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 May 2024 10:29:00 -0500 Subject: [PATCH 03/22] Bump aiohttp-isal to 0.3.1 (#116720) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 800e4d90009a4b..024cdd3eab7d56 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-isal==0.2.0 +aiohttp-isal==0.3.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 diff --git a/pyproject.toml b/pyproject.toml index ac3c84d67f6509..51023a501e6377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-isal==0.2.0", + "aiohttp-isal==0.3.1", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 44c60aec07a45a..df001251a04849 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-isal==0.2.0 +aiohttp-isal==0.3.1 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From bbb94d9e1780b01a52be7c3618febb742b9a779b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 3 May 2024 23:54:27 +0200 Subject: [PATCH 04/22] Fix Bosch-SHC switch state (#116721) --- homeassistant/components/bosch_shc/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index e6ccd2aa9aaa78..58370a120f29ca 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -43,21 +43,21 @@ class SHCSwitchEntityDescription(SwitchEntityDescription): "smartplug": SHCSwitchEntityDescription( key="smartplug", device_class=SwitchDeviceClass.OUTLET, - on_key="state", + on_key="switchstate", on_value=SHCSmartPlug.PowerSwitchService.State.ON, should_poll=False, ), "smartplugcompact": SHCSwitchEntityDescription( key="smartplugcompact", device_class=SwitchDeviceClass.OUTLET, - on_key="state", + on_key="switchstate", on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON, should_poll=False, ), "lightswitch": SHCSwitchEntityDescription( key="lightswitch", device_class=SwitchDeviceClass.SWITCH, - on_key="state", + on_key="switchstate", on_value=SHCLightSwitch.PowerSwitchService.State.ON, should_poll=False, ), From f068b8cdb8accb8a1b19115004021ef321035cb8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 4 May 2024 17:29:42 +0200 Subject: [PATCH 05/22] Remove suggested UoM from Opower (#116728) --- homeassistant/components/opower/sensor.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index 9f467dce1c6a11..c75ffb9614b157 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -69,7 +69,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill electric cost to date", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -79,7 +78,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill electric forecasted cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -89,7 +87,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Typical monthly electric cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, @@ -101,7 +98,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill gas usage to date", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.usage_to_date, @@ -111,7 +107,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill gas forecasted usage", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_usage, @@ -121,7 +116,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Typical monthly gas usage", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, - suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_usage, @@ -131,7 +125,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill gas cost to date", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -141,7 +134,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Current bill gas forecasted cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -151,7 +143,6 @@ class OpowerEntityDescription(SensorEntityDescription): name="Typical monthly gas cost", device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement="USD", - suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, From 57bbd105171da4a79602777ceffbbb17f15faa4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 May 2024 15:39:45 -0500 Subject: [PATCH 06/22] Refactor statistics to avoid creating tasks (#116743) --- homeassistant/components/statistics/sensor.py | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 713a8d3e894e3d..fef10f7296ff0a 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -285,6 +285,9 @@ async def async_setup_platform( class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" + _attr_should_poll = False + _attr_icon = ICON + def __init__( self, source_entity_id: str, @@ -298,9 +301,7 @@ def __init__( percentile: int, ) -> None: """Initialize the Statistics sensor.""" - self._attr_icon: str = ICON self._attr_name: str = name - self._attr_should_poll: bool = False self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id self.is_binary: bool = ( @@ -326,35 +327,37 @@ def __init__( self._update_listener: CALLBACK_TYPE | None = None - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_stats_sensor_state_listener( - event: Event[EventStateChangedData], - ) -> None: - """Handle the sensor state changes.""" - if (new_state := event.data["new_state"]) is None: - return - self._add_state_to_queue(new_state) - self.async_schedule_update_ha_state(True) - - async def async_stats_sensor_startup(_: HomeAssistant) -> None: - """Add listener and get recorded state.""" - _LOGGER.debug("Startup for %s", self.entity_id) - - self.async_on_remove( - async_track_state_change_event( - self.hass, - [self._source_entity_id], - async_stats_sensor_state_listener, - ) + @callback + def _async_stats_sensor_state_listener( + self, + event: Event[EventStateChangedData], + ) -> None: + """Handle the sensor state changes.""" + if (new_state := event.data["new_state"]) is None: + return + self._add_state_to_queue(new_state) + self._async_purge_update_and_schedule() + self.async_write_ha_state() + + @callback + def _async_stats_sensor_startup(self, _: HomeAssistant) -> None: + """Add listener and get recorded state.""" + _LOGGER.debug("Startup for %s", self.entity_id) + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._source_entity_id], + self._async_stats_sensor_state_listener, ) + ) + if "recorder" in self.hass.config.components: + self.hass.async_create_task(self._initialize_from_database()) - if "recorder" in self.hass.config.components: - self.hass.async_create_task(self._initialize_from_database()) - - self.async_on_remove(async_at_start(self.hass, async_stats_sensor_startup)) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + async_at_start(self.hass, self._async_stats_sensor_startup) + ) def _add_state_to_queue(self, new_state: State) -> None: """Add the state to the queue.""" @@ -499,7 +502,8 @@ def _purge_old_states(self, max_age: timedelta) -> None: self.ages.popleft() self.states.popleft() - def _next_to_purge_timestamp(self) -> datetime | None: + @callback + def _async_next_to_purge_timestamp(self) -> datetime | None: """Find the timestamp when the next purge would occur.""" if self.ages and self._samples_max_age: if self.samples_keep_last and len(self.ages) == 1: @@ -521,6 +525,10 @@ def _next_to_purge_timestamp(self) -> datetime | None: async def async_update(self) -> None: """Get the latest data and updates the states.""" + self._async_purge_update_and_schedule() + + def _async_purge_update_and_schedule(self) -> None: + """Purge old states, update the sensor and schedule the next update.""" _LOGGER.debug("%s: updating statistics", self.entity_id) if self._samples_max_age is not None: self._purge_old_states(self._samples_max_age) @@ -531,23 +539,28 @@ async def async_update(self) -> None: # If max_age is set, ensure to update again after the defined interval. # By basing updates off the timestamps of sampled data we avoid updating # when none of the observed entities change. - if timestamp := self._next_to_purge_timestamp(): + if timestamp := self._async_next_to_purge_timestamp(): _LOGGER.debug("%s: scheduling update at %s", self.entity_id, timestamp) - if self._update_listener: - self._update_listener() - self._update_listener = None - - @callback - def _scheduled_update(now: datetime) -> None: - """Timer callback for sensor update.""" - _LOGGER.debug("%s: executing scheduled update", self.entity_id) - self.async_schedule_update_ha_state(True) - self._update_listener = None - + self._async_cancel_update_listener() self._update_listener = async_track_point_in_utc_time( - self.hass, _scheduled_update, timestamp + self.hass, self._async_scheduled_update, timestamp ) + @callback + def _async_cancel_update_listener(self) -> None: + """Cancel the scheduled update listener.""" + if self._update_listener: + self._update_listener() + self._update_listener = None + + @callback + def _async_scheduled_update(self, now: datetime) -> None: + """Timer callback for sensor update.""" + _LOGGER.debug("%s: executing scheduled update", self.entity_id) + self._async_cancel_update_listener() + self._async_purge_update_and_schedule() + self.async_write_ha_state() + def _fetch_states_from_database(self) -> list[State]: """Fetch the states from the database.""" _LOGGER.debug("%s: initializing values from the database", self.entity_id) @@ -589,8 +602,8 @@ async def _initialize_from_database(self) -> None: for state in reversed(states): self._add_state_to_queue(state) - self.async_schedule_update_ha_state(True) - + self._async_purge_update_and_schedule() + self.async_write_ha_state() _LOGGER.debug("%s: initializing from database completed", self.entity_id) def _update_attributes(self) -> None: From 18bcc61427fddfb2d853a5757a11415cab7f41c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 May 2024 10:26:14 -0500 Subject: [PATCH 07/22] Bump bluetooth-adapters to 0.19.2 (#116785) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 754e8faf996511..fe5867191e285b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.1", + "bluetooth-adapters==0.19.2", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 024cdd3eab7d56..4fd8ebccd7e9c9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6f741478f205b5..462d33d69d0404 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14951b8a6ecf20..5233e7e70f8dd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.1 +bluetooth-adapters==0.19.2 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 79460cb017f6c3d2985f78d502728b70cdded0d6 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Sat, 4 May 2024 20:16:58 +0200 Subject: [PATCH 08/22] fix UnboundLocalError on modified_statistic_ids in compile_statistics (#116795) Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/statistics.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 41cf4e22b53d46..572731a9fed476 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -485,6 +485,12 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) - The actual calculation is delegated to the platforms. """ + # Define modified_statistic_ids outside of the "with" statement as + # _compile_statistics may raise and be trapped by + # filter_unique_constraint_integrity_error which would make + # modified_statistic_ids unbound. + modified_statistic_ids: set[str] | None = None + # Return if we already have 5-minute statistics for the requested period with session_scope( session=instance.get_session(), From ad7688197ff6ae4869bb29b741360f5c3759ba71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 May 2024 05:09:57 -0500 Subject: [PATCH 09/22] Ensure all synology_dsm coordinators handle expired sessions (#116796) * Ensure all synology_dsm coordinators handle expired sessions * Ensure all synology_dsm coordinators handle expired sessions * Ensure all synology_dsm coordinators handle expired sessions * handle cancellation * add a debug log message --------- Co-authored-by: mib1185 --- .../components/synology_dsm/__init__.py | 6 ++- .../components/synology_dsm/common.py | 28 ++++++++++- .../components/synology_dsm/coordinator.py | 50 +++++++++++++------ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2748b27c93d78c..6598ed304f7fdf 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -7,6 +7,7 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station.camera import SynoCamera +from synology_dsm.exceptions import SynologyDSMNotLoggedInException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL @@ -69,7 +70,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await api.async_setup() except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: raise_config_entry_auth_error(err) - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + except (*SYNOLOGY_CONNECTION_EXCEPTIONS, SynologyDSMNotLoggedInException) as err: + # SynologyDSMNotLoggedInException may be raised even if the user is + # logged in because the session may have expired, and we need to retry + # the login later. if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 04e8ae29ceb6f9..c871dd7b70546a 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from contextlib import suppress import logging @@ -82,6 +83,31 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self._with_upgrade = True self._with_utilisation = True + self._login_future: asyncio.Future[None] | None = None + + async def async_login(self) -> None: + """Login to the Synology DSM API. + + This function will only login once if called multiple times + by multiple different callers. + + If a login is already in progress, the function will await the + login to complete before returning. + """ + if self._login_future: + return await self._login_future + + self._login_future = self._hass.loop.create_future() + try: + await self.dsm.login() + self._login_future.set_result(None) + except BaseException as err: + if not self._login_future.done(): + self._login_future.set_exception(err) + raise + finally: + self._login_future = None + async def async_setup(self) -> None: """Start interacting with the NAS.""" session = async_get_clientsession(self._hass, self._entry.data[CONF_VERIFY_SSL]) @@ -95,7 +121,7 @@ async def async_setup(self) -> None: timeout=self._entry.options.get(CONF_TIMEOUT) or 10, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) - await self.dsm.login() + await self.async_login() # check if surveillance station is used self._with_surveillance_station = bool( diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 34886828a58313..52a3e1de1ebf6a 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import ( @@ -30,6 +31,36 @@ _DataT = TypeVar("_DataT") +_T = TypeVar("_T", bound="SynologyDSMUpdateCoordinator") +_P = ParamSpec("_P") + + +def async_re_login_on_expired( + func: Callable[Concatenate[_T, _P], Awaitable[_DataT]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _DataT]]: + """Define a wrapper to re-login when expired.""" + + async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _DataT: + for attempts in range(2): + try: + return await func(self, *args, **kwargs) + except SynologyDSMNotLoggedInException: + # If login is expired, try to login again + _LOGGER.debug("login is expired, try to login again") + try: + await self.api.async_login() + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: + raise_config_entry_auth_error(err) + if attempts == 0: + continue + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + raise UpdateFailed("Unknown error when communicating with API") + + return _async_wrap + + class SynologyDSMUpdateCoordinator(DataUpdateCoordinator[_DataT]): """DataUpdateCoordinator base class for synology_dsm.""" @@ -72,6 +103,7 @@ async def async_setup(self) -> None: assert info is not None self.version = info["data"]["CMSMinVersion"] + @async_re_login_on_expired async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station @@ -102,21 +134,10 @@ def __init__( ), ) + @async_re_login_on_expired async def _async_update_data(self) -> None: """Fetch all data from api.""" - for attempts in range(2): - try: - await self.api.async_update() - except SynologyDSMNotLoggedInException: - # If login is expired, try to login again - try: - await self.api.dsm.login() - except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: - raise_config_entry_auth_error(err) - if attempts == 0: - continue - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + await self.api.async_update() class SynologyDSMCameraUpdateCoordinator( @@ -133,6 +154,7 @@ def __init__( """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) + @async_re_login_on_expired async def _async_update_data(self) -> dict[str, dict[int, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station From dbe303d95ee34ace5051d45916e58c15b13a19cf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 4 May 2024 20:18:26 +0200 Subject: [PATCH 10/22] Fix IMAP config entry setup (#116797) --- homeassistant/components/imap/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 62ed4d42a07576..6f93ce71d844e2 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -75,7 +75,7 @@ vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, # The default for new entries is to not include text and headers - vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, + vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR, } ) CONFIG_SCHEMA_ADVANCED = { From ae28c604e5dfb516f05581bff60bfabe18e4f788 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 May 2024 15:37:10 -0500 Subject: [PATCH 11/22] Fix airthings-ble data drop outs when Bluetooth connection is flakey (#116805) * Fix airthings-ble data drop outs when Bluetooth adapter is flakey fixes #116770 * add missing file * update --- homeassistant/components/airthings_ble/__init__.py | 8 +++++++- homeassistant/components/airthings_ble/const.py | 2 ++ homeassistant/components/airthings_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index 39617a8a019955..219a384bae0c1b 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.unit_system import METRIC_SYSTEM -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -61,6 +61,12 @@ async def _async_update_method() -> AirthingsDevice: await coordinator.async_config_entry_first_refresh() + # Once its setup and we know we are not going to delay + # the startup of Home Assistant, we can set the max attempts + # to a higher value. If the first connection attempt fails, + # Home Assistant's built-in retry logic will take over. + airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP) + hass.data[DOMAIN][entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/airthings_ble/const.py b/homeassistant/components/airthings_ble/const.py index 96372919e70aeb..fdfebea8bff179 100644 --- a/homeassistant/components/airthings_ble/const.py +++ b/homeassistant/components/airthings_ble/const.py @@ -7,3 +7,5 @@ VOLUME_PICOCURIE = "pCi/L" DEFAULT_SCAN_INTERVAL = 300 + +MAX_RETRIES_AFTER_STARTUP = 5 diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index d93e3a0b8cb56f..b86bc314819369 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.8.0"] + "requirements": ["airthings-ble==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 462d33d69d0404..e3f2bd0e36cbde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.8.0 +airthings-ble==0.9.0 # homeassistant.components.airthings airthings-cloud==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5233e7e70f8dd4..27e70c28916061 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.8.0 +airthings-ble==0.9.0 # homeassistant.components.airthings airthings-cloud==0.2.0 From ad5e0949b6b10cb10fe9909e0799bc2d881c664a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 May 2024 20:09:38 -0400 Subject: [PATCH 12/22] Hide conversation agents that are exposed as agent entities (#116813) --- homeassistant/components/conversation/http.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index beda7ba155087b..e582dacf284bd4 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -142,6 +142,9 @@ async def websocket_list_agents( agent = manager.async_get_agent(agent_info.id) assert agent is not None + if isinstance(agent, ConversationEntity): + continue + supported_languages = agent.supported_languages if language and supported_languages != MATCH_ALL: supported_languages = language_util.matches( From c049888b00094f17c0c4e8e2c88dc2ac15c9efa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 May 2024 08:43:39 -0500 Subject: [PATCH 13/22] Fix non-thread-safe state write in lutron event (#116829) fixes #116746 --- homeassistant/components/lutron/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py index 710f942a006033..f231c33a296268 100644 --- a/homeassistant/components/lutron/event.py +++ b/homeassistant/components/lutron/event.py @@ -106,4 +106,4 @@ def handle_event( } self.hass.bus.fire("lutron_event", data) self._trigger_event(action) - self.async_write_ha_state() + self.schedule_update_ha_state() From 421f74cd7f3f2b20c8e402402086571e5286a79e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 5 May 2024 15:07:18 +0200 Subject: [PATCH 14/22] Increase default timeout to 30 seconds in Synology DSM (#116836) increase default timeout to 30s and use it consequently --- homeassistant/components/synology_dsm/common.py | 3 ++- homeassistant/components/synology_dsm/config_flow.py | 4 +++- homeassistant/components/synology_dsm/const.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index c871dd7b70546a..91c4cfc4ae275a 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -39,6 +39,7 @@ from .const import ( CONF_DEVICE_TOKEN, + DEFAULT_TIMEOUT, EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, SYNOLOGY_CONNECTION_EXCEPTIONS, @@ -118,7 +119,7 @@ async def async_setup(self) -> None: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT) or 10, + timeout=self._entry.options.get(CONF_TIMEOUT) or DEFAULT_TIMEOUT, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) await self.async_login() diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 785baa50b29bae..d6c0c6fe3e86cf 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -179,7 +179,9 @@ async def async_validate_input_create_entry( port = DEFAULT_PORT session = async_get_clientsession(self.hass, verify_ssl) - api = SynologyDSM(session, host, port, username, password, use_ssl, timeout=30) + api = SynologyDSM( + session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT + ) errors = {} try: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 140e07e975b58f..35d3008b41631f 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -40,7 +40,7 @@ DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min -DEFAULT_TIMEOUT = 10 # sec +DEFAULT_TIMEOUT = 30 # sec DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" From 834c2e2a09730549e8d7fddc0aa90bdb43e3a99f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 5 May 2024 15:09:26 +0200 Subject: [PATCH 15/22] Avoid duplicate data fetch during Synologs DSM setup (#116839) don't do first refresh of central coordinator, is already done by api.setup before --- homeassistant/components/synology_dsm/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 6598ed304f7fdf..d42dacca638781 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -90,12 +90,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api) - await coordinator_central.async_config_entry_first_refresh() available_apis = api.dsm.apis - # The central coordinator needs to be refreshed first since - # the next two rely on data from it coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None = None if api.surveillance_station is not None: coordinator_cameras = SynologyDSMCameraUpdateCoordinator(hass, entry, api) From 73eabe821cb80ca1fe2a54df832c2fa1c6dfa519 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 5 May 2024 06:44:40 -0700 Subject: [PATCH 16/22] Bump androidtvremote2 to v0.0.15 (#116844) --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index f45dee34afef67..915586b38794c1 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.14"], + "requirements": ["androidtvremote2==0.0.15"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e3f2bd0e36cbde..be856c1fa851e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -437,7 +437,7 @@ amcrest==1.9.8 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.14 +androidtvremote2==0.0.15 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27e70c28916061..80b1c2a345bacf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ amberelectric==1.1.0 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.14 +androidtvremote2==0.0.15 # homeassistant.components.anova anova-wifi==0.10.0 From 7c9653e3974ce06c464e5224ef7ec7f64d1bf8f4 Mon Sep 17 00:00:00 2001 From: mletenay Date: Mon, 6 May 2024 01:05:21 +0200 Subject: [PATCH 17/22] Bump goodwe to 0.3.4 (#116849) --- homeassistant/components/goodwe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 6f1bdd2b449f20..59c259524c8f82 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/goodwe", "iot_class": "local_polling", "loggers": ["goodwe"], - "requirements": ["goodwe==0.3.2"] + "requirements": ["goodwe==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index be856c1fa851e7..bf73d7792e7c4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ glances-api==0.6.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.3.2 +goodwe==0.3.4 # homeassistant.components.google_mail # homeassistant.components.google_tasks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80b1c2a345bacf..77802e3d5c7b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -781,7 +781,7 @@ glances-api==0.6.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.3.2 +goodwe==0.3.4 # homeassistant.components.google_mail # homeassistant.components.google_tasks From 9533f5b49006c6e5b8b31a247571e95ea0072ba5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 May 2024 15:58:38 -0500 Subject: [PATCH 18/22] Fix non-thread-safe operations in amcrest (#116859) * Fix non-thread-safe operations in amcrest fixes #116850 * fix locking * fix locking * fix locking --- homeassistant/components/amcrest/__init__.py | 93 +++++++++++++++----- homeassistant/components/amcrest/camera.py | 5 +- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index c12aa6d7916944..624e0145b86031 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -35,7 +35,7 @@ HTTP_BASIC_AUTHENTICATION, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -177,7 +177,8 @@ def async_available_flag(self) -> asyncio.Event: """Return event flag that indicates if camera's API is responding.""" return self._async_wrap_event_flag - def _start_recovery(self) -> None: + @callback + def _async_start_recovery(self) -> None: self.available_flag.clear() self.async_available_flag.clear() async_dispatcher_send( @@ -222,50 +223,98 @@ async def _async_command_wrapper(self) -> AsyncIterator[None]: yield except LoginError as ex: async with self._async_wrap_lock: - self._handle_offline(ex) + self._async_handle_offline(ex) raise except AmcrestError: async with self._async_wrap_lock: - self._handle_error() + self._async_handle_error() raise async with self._async_wrap_lock: - self._set_online() + self._async_set_online() - def _handle_offline(self, ex: Exception) -> None: + def _handle_offline_thread_safe(self, ex: Exception) -> bool: + """Handle camera offline status shared between threads and event loop. + + Returns if the camera was online as a bool. + """ with self._wrap_lock: was_online = self.available was_login_err = self._wrap_login_err self._wrap_login_err = True if not was_login_err: _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) - if was_online: - self._start_recovery() + return was_online - def _handle_error(self) -> None: + def _handle_offline(self, ex: Exception) -> None: + """Handle camera offline status from a thread.""" + if self._handle_offline_thread_safe(ex): + self._hass.loop.call_soon_threadsafe(self._async_start_recovery) + + @callback + def _async_handle_offline(self, ex: Exception) -> None: + if self._handle_offline_thread_safe(ex): + self._async_start_recovery() + + def _handle_error_thread_safe(self) -> bool: + """Handle camera error status shared between threads and event loop. + + Returns if the camera was online and is now offline as + a bool. + """ with self._wrap_lock: was_online = self.available errs = self._wrap_errors = self._wrap_errors + 1 offline = not self.available _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) - if was_online and offline: + return was_online and offline + + def _handle_error(self) -> None: + """Handle camera error status from a thread.""" + if self._handle_error_thread_safe(): _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - self._start_recovery() + self._hass.loop.call_soon_threadsafe(self._async_start_recovery) - def _set_online(self) -> None: + @callback + def _async_handle_error(self) -> None: + """Handle camera error status from the event loop.""" + if self._handle_error_thread_safe(): + _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + self._async_start_recovery() + + def _set_online_thread_safe(self) -> bool: + """Set camera online status shared between threads and event loop. + + Returns if the camera was offline as a bool. + """ with self._wrap_lock: was_offline = not self.available self._wrap_errors = 0 self._wrap_login_err = False - if was_offline: - assert self._unsub_recheck is not None - self._unsub_recheck() - self._unsub_recheck = None - _LOGGER.error("%s camera back online", self._wrap_name) - self.available_flag.set() - self.async_available_flag.set() - async_dispatcher_send( - self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) - ) + return was_offline + + def _set_online(self) -> None: + """Set camera online status from a thread.""" + if self._set_online_thread_safe(): + self._hass.loop.call_soon_threadsafe(self._async_signal_online) + + @callback + def _async_set_online(self) -> None: + """Set camera online status from the event loop.""" + if self._set_online_thread_safe(): + self._async_signal_online() + + @callback + def _async_signal_online(self) -> None: + """Signal that camera is back online.""" + assert self._unsub_recheck is not None + self._unsub_recheck() + self._unsub_recheck = None + _LOGGER.error("%s camera back online", self._wrap_name) + self.available_flag.set() + self.async_available_flag.set() + async_dispatcher_send( + self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) + ) async def _wrap_test_online(self, now: datetime) -> None: """Test if camera is back online.""" diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 1cbf5af4b70ded..a55f9c81e64091 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -16,7 +16,7 @@ from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, @@ -325,7 +325,8 @@ def is_on(self) -> bool: # Other Entity method overrides - async def async_on_demand_update(self) -> None: + @callback + def async_on_demand_update(self) -> None: """Update state.""" self.async_schedule_update_ha_state(True) From ed6788ca3fdce964df5948ed69422db4de457b77 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 May 2024 12:54:17 -0400 Subject: [PATCH 19/22] fix radarr coordinator updates (#116874) --- homeassistant/components/radarr/coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 0580fdcc020537..47a1862b8ae3c9 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -46,7 +46,7 @@ class RadarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): """Data update coordinator for the Radarr integration.""" config_entry: ConfigEntry - update_interval = timedelta(seconds=30) + _update_interval = timedelta(seconds=30) def __init__( self, @@ -59,7 +59,7 @@ def __init__( hass=hass, logger=LOGGER, name=DOMAIN, - update_interval=self.update_interval, + update_interval=self._update_interval, ) self.api_client = api_client self.host_configuration = host_configuration @@ -133,7 +133,7 @@ async def _fetch_data(self) -> int: class CalendarUpdateCoordinator(RadarrDataUpdateCoordinator[None]): """Calendar update coordinator.""" - update_interval = timedelta(hours=1) + _update_interval = timedelta(hours=1) def __init__( self, From ab113570c3a063870ae297345eb4d3e474823ba9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 6 May 2024 14:32:37 +0200 Subject: [PATCH 20/22] Fix initial mqtt subcribe cooldown timeout (#116904) --- homeassistant/components/mqtt/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 4fa9f4a1d49b21..4b05442d71b13d 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -84,7 +84,7 @@ _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 5 -INITIAL_SUBSCRIBE_COOLDOWN = 1.0 +INITIAL_SUBSCRIBE_COOLDOWN = 3.0 SUBSCRIBE_COOLDOWN = 0.1 UNSUBSCRIBE_COOLDOWN = 0.1 TIMEOUT_ACK = 10 @@ -891,6 +891,7 @@ async def _async_resubscribe_and_publish_birth_message( qos=birth_message.qos, retain=birth_message.retain, ) + _LOGGER.info("MQTT client initialized, birth message sent") @callback def _async_mqtt_on_connect( @@ -950,6 +951,7 @@ def _async_mqtt_on_connect( name="mqtt re-subscribe", ) self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) + _LOGGER.info("MQTT client initialized") self._async_connection_result(True) From 6b93f8d997f2b3986916f676917bfb29a3444a88 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 6 May 2024 17:19:04 +0200 Subject: [PATCH 21/22] Bump version to 2024.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 31dc771d96653c..e9e1231712ec8d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 51023a501e6377..887083304cf8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.1" +version = "2024.5.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From eb6ccea8aa65ebf33711fbdd649832c084d26249 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 6 May 2024 18:40:01 +0200 Subject: [PATCH 22/22] Update frontend to 20240501.1 (#116939) --- 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 6abe8df1d7c0b7..1c4245d93b6cda 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==20240501.0"] + "requirements": ["home-assistant-frontend==20240501.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4fd8ebccd7e9c9..0f69f7d63c9682 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.1 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240501.0 +home-assistant-frontend==20240501.1 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index bf73d7792e7c4c..e4c84b11ab82f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240501.0 +home-assistant-frontend==20240501.1 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77802e3d5c7b64..e9dc44b3765c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240501.0 +home-assistant-frontend==20240501.1 # homeassistant.components.conversation home-assistant-intents==2024.4.24