From 412ee305849c39e815fb390d399c2726494e7405 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Dec 2025 13:27:09 +0100 Subject: [PATCH] Do not check Reolink firmware at start (#158275) --- homeassistant/components/reolink/__init__.py | 42 ++++++++++++--- homeassistant/components/reolink/const.py | 1 + homeassistant/components/reolink/host.py | 1 + tests/components/reolink/test_init.py | 57 +++++++++++++++++++- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 0a4c88124e8..5fbe1ba3951 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -4,8 +4,9 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from datetime import timedelta +from datetime import UTC, datetime, timedelta import logging +from random import uniform from time import time from typing import Any @@ -34,6 +35,7 @@ from .const import ( BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, CONF_BC_ONLY, CONF_BC_PORT, + CONF_FIRMWARE_CHECK_TIME, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN, @@ -212,15 +214,41 @@ async def async_setup_entry( config_entry=config_entry, name=f"reolink.{host.api.nvr_name}.firmware", update_method=async_check_firmware_update, - update_interval=FIRMWARE_UPDATE_INTERVAL, + update_interval=None, # Do not fetch data automatically, resume 24h schedule ) + async def first_firmware_check(*args: Any) -> None: + """Start first firmware check delayed to continue 24h schedule.""" + firmware_coordinator.update_interval = FIRMWARE_UPDATE_INTERVAL + await firmware_coordinator.async_refresh() + host.cancel_first_firmware_check = None + + # get update time from config entry + check_time_sec = config_entry.data.get(CONF_FIRMWARE_CHECK_TIME) + if check_time_sec is None: + check_time_sec = uniform(0, 86400) + data = { + **config_entry.data, + CONF_FIRMWARE_CHECK_TIME: check_time_sec, + } + hass.config_entries.async_update_entry(config_entry, data=data) + # If camera WAN blocked, firmware check fails and takes long, do not prevent setup - config_entry.async_create_background_task( - hass, - firmware_coordinator.async_refresh(), - f"Reolink firmware check {config_entry.entry_id}", + now = datetime.now(UTC) + check_time = timedelta(seconds=check_time_sec) + delta_midnight = now - now.replace(hour=0, minute=0, second=0, microsecond=0) + firmware_check_delay = check_time - delta_midnight + if firmware_check_delay < timedelta(0): + firmware_check_delay += timedelta(days=1) + _LOGGER.debug( + "Scheduling first Reolink %s firmware check in %s", + host.api.nvr_name, + firmware_check_delay, ) + host.cancel_first_firmware_check = async_call_later( + hass, firmware_check_delay, first_firmware_check + ) + # Fetch initial data so we have data when entities subscribe try: await device_coordinator.async_config_entry_first_refresh() @@ -312,6 +340,8 @@ async def async_unload_entry( host.api.baichuan.unregister_callback(f"camera_{channel}_wake") if host.cancel_refresh_privacy_mode is not None: host.cancel_refresh_privacy_mode() + if host.cancel_first_firmware_check is not None: + host.cancel_first_firmware_check() return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index db2d105984b..59d594a5406 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -6,6 +6,7 @@ CONF_USE_HTTPS = "use_https" CONF_BC_PORT = "baichuan_port" CONF_BC_ONLY = "baichuan_only" CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported" +CONF_FIRMWARE_CHECK_TIME = "firmware_check_time" # Conserve battery by not waking the battery cameras each minute during normal update # Most props are cached in the Home Hub and updated, but some are skipped diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 57af2404321..7b7cc48c1dd 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -130,6 +130,7 @@ class ReolinkHost: self._lost_subscription_start: bool = False self._lost_subscription: bool = False self.cancel_refresh_privacy_mode: CALLBACK_TYPE | None = None + self.cancel_first_firmware_check: CALLBACK_TYPE | None = None @callback def async_register_update_cmd(self, cmd: str, channel: int | None = None) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 0e712542f8d..593d51997fc 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Callable +from datetime import UTC, datetime, timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -22,6 +23,7 @@ from homeassistant.components.reolink.const import ( BATTERY_ALL_WAKE_UPDATE_INTERVAL, BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, CONF_BC_PORT, + CONF_FIRMWARE_CHECK_TIME, DOMAIN, ) from homeassistant.config_entries import ConfigEntryState @@ -47,6 +49,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format from homeassistant.setup import async_setup_component from .conftest import ( + CONF_BC_ONLY, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DEFAULT_PROTOCOL, @@ -58,6 +61,7 @@ from .conftest import ( TEST_MAC, TEST_MAC_CAM, TEST_NVR_NAME, + TEST_PASSWORD, TEST_PORT, TEST_PRIVACY, TEST_UID, @@ -146,10 +150,14 @@ async def test_firmware_error_twice( assert config_entry.state is ConfigEntryState.LOADED + freezer.tick(FIRMWARE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" assert hass.states.get(entity_id).state == STATE_OFF - freezer.tick(FIRMWARE_UPDATE_INTERVAL) + freezer.tick(2 * FIRMWARE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -1130,6 +1138,53 @@ async def test_camera_wake_callback( assert hass.states.get(entity_id).state == STATE_OFF +@pytest.mark.parametrize(("seconds", "call_count"), [(10, 1), (3600, 0)]) +async def test_firmware_update_delay( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + reolink_host: MagicMock, + seconds: int, + call_count: int, +) -> None: + """Test delay of firmware update check.""" + now = datetime.now(UTC) + check_delay = ( + now + + timedelta(seconds=seconds) + - now.replace(hour=0, minute=0, second=0, microsecond=0) + ).total_seconds() + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, + CONF_BC_PORT: TEST_BC_PORT, + CONF_BC_ONLY: False, + CONF_FIRMWARE_CHECK_TIME: check_delay, + }, + options={ + CONF_PROTOCOL: DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert reolink_host.check_new_firmware.call_count == call_count + + async def test_baichaun_only( hass: HomeAssistant, reolink_host: MagicMock,