Skip to content

Conversation

@starkillerOG
Copy link
Contributor

@starkillerOG starkillerOG commented Dec 8, 2025

Breaking change

Proposed change

Do not check Reolink firmware at start, but continue the 24hour scheduale from before a Home Assistant restart.

I got a complaint from Reolink that they see increased server load after a Home Assistant release when many users update and therefore restart the reolink integration.

This PR will ensure a consitant firmware check intervall even accros restarts.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

To help with the load of incoming pull requests:

@starkillerOG
Copy link
Contributor Author

A alternative would be to always scheduale the firmware check at 00:00 at night local time with a random +- 30 minutes and relaying on the natural spread accross timezones to distribute the server load.
However, I think the server load would be less spread than it is with the proposal in this PR.
I don't know the distribution accross the timezones of the Reolink HA users, but I guess it will not be that well distributed. For instance UTC-2 is only a part of Greenland, Or UTC-9 is also not very populated I would imagine.

@starkillerOG starkillerOG added this to the 2025.12.3 milestone Dec 8, 2025
@starkillerOG
Copy link
Contributor Author

Since this was requested by Reolink themselfs, (server load) I added it to the milestone.
But feel free to remove it from the milestone.
I don't think Reolink would mind too much to wait untill january to even the server load a bit better.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Reolink integration's firmware check scheduling to reduce server load during Home Assistant restarts. Instead of checking firmware immediately on startup, it now maintains a consistent 24-hour check interval across restarts by persisting the last check timestamp to storage.

  • Adds persistent storage to track the last firmware check time
  • Calculates appropriate delay before first firmware check based on stored timestamp
  • Schedules firmware checks using async_call_later with dynamic delay instead of immediate execution
  • Properly cleans up scheduled checks during integration unload

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
homeassistant/components/reolink/init.py Implements delayed firmware check logic with persistent storage, calculates delay based on last check time, and adds cleanup for scheduled callbacks
homeassistant/components/reolink/host.py Adds cancel_first_firmware_check field to track scheduled firmware check callback
tests/components/reolink/test_init.py Adds parametrized test for firmware update delay and updates existing test to verify scheduled execution

hass,
firmware_coordinator.async_refresh(),
f"Reolink firmware check {config_entry.entry_id}",
firmware_check_delay: int | timedelta = 5
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type inconsistency: firmware_check_delay is declared as int | timedelta but initialized with an integer 5 (seconds). When last_check is not None, it's assigned a timedelta object from the calculation. However, async_call_later expects either a timedelta or a numeric value in seconds.

For consistency and clarity, initialize as timedelta(seconds=5) instead of 5:

firmware_check_delay: timedelta = timedelta(seconds=5)

This makes the type consistent and clearer that it represents a time duration.

Copilot uses AI. Check for mistakes.
firmware_check_delay < timedelta(0)
or firmware_check_delay > FIRMWARE_UPDATE_INTERVAL
):
firmware_check_delay = 5
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback value should be timedelta(seconds=5) to match the type of firmware_check_delay. Currently, it's set to integer 5, which creates a type inconsistency. While async_call_later accepts both types, keeping the type consistent improves code clarity.

firmware_check_delay = timedelta(seconds=5)

Copilot uses AI. Check for mistakes.

store = MagicMock()
last_firmware_check = (datetime.now(UTC) - timedelta(hours=last_check)).isoformat()
store.async_load = AsyncMock(return_value=last_firmware_check)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing async_save method mock on the store. The test mocks async_load but not async_save, which is called in the async_check_firmware_update function (line 200 in init.py). This will cause the test to fail when the firmware check actually executes because the mock doesn't have the async_save method.

Add:

store.async_save = AsyncMock()
Suggested change
store.async_load = AsyncMock(return_value=last_firmware_check)
store.async_load = AsyncMock(return_value=last_firmware_check)
store.async_save = AsyncMock()

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +238
firmware_check_delay = FIRMWARE_UPDATE_INTERVAL - (
datetime.now(UTC) - datetime.fromisoformat(last_check)
)
if (
firmware_check_delay < timedelta(0)
or firmware_check_delay > FIRMWARE_UPDATE_INTERVAL
):
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for datetime.fromisoformat(last_check). If the stored value is corrupted or in an unexpected format, this will raise a ValueError and crash the setup. Consider wrapping this in a try-except block:

if last_check is not None:
    try:
        firmware_check_delay = FIRMWARE_UPDATE_INTERVAL - (
            datetime.now(UTC) - datetime.fromisoformat(last_check)
        )
        if (
            firmware_check_delay < timedelta(0)
            or firmware_check_delay > FIRMWARE_UPDATE_INTERVAL
        ):
            firmware_check_delay = timedelta(seconds=5)
    except (ValueError, TypeError):
        _LOGGER.debug("Invalid firmware check timestamp, using default delay")
        firmware_check_delay = timedelta(seconds=5)
Suggested change
firmware_check_delay = FIRMWARE_UPDATE_INTERVAL - (
datetime.now(UTC) - datetime.fromisoformat(last_check)
)
if (
firmware_check_delay < timedelta(0)
or firmware_check_delay > FIRMWARE_UPDATE_INTERVAL
):
try:
firmware_check_delay = FIRMWARE_UPDATE_INTERVAL - (
datetime.now(UTC) - datetime.fromisoformat(last_check)
)
if (
firmware_check_delay < timedelta(0)
or firmware_check_delay > FIRMWARE_UPDATE_INTERVAL
):
firmware_check_delay = 5
except (ValueError, TypeError):
_LOGGER.debug("Invalid firmware check timestamp, using default delay")

Copilot uses AI. Check for mistakes.
Comment on lines +1138 to +1146
@pytest.mark.parametrize(("last_check", "call_count"), [(25, 1), (23, 0)])
async def test_firmware_update_delay(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
reolink_host: MagicMock,
config_entry: MockConfigEntry,
last_check: int,
call_count: int,
) -> None:
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case for when last_check is None (no previous firmware check). The parametrized test only covers cases where a last check exists (25 and 23 hours ago), but doesn't test the initial setup scenario where the store returns None. This is an important path since it's what happens on first setup.

Consider adding a test case:

@pytest.mark.parametrize(("last_check", "call_count"), [(25, 1), (23, 0), (None, 0)])

And update the test to handle None:

if last_check is not None:
    last_firmware_check = (datetime.now(UTC) - timedelta(hours=last_check)).isoformat()
    store.async_load = AsyncMock(return_value=last_firmware_check)
else:
    store.async_load = AsyncMock(return_value=None)

Copilot uses AI. Check for mistakes.
Comment on lines +1138 to +1163
@pytest.mark.parametrize(("last_check", "call_count"), [(25, 1), (23, 0)])
async def test_firmware_update_delay(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
reolink_host: MagicMock,
config_entry: MockConfigEntry,
last_check: int,
call_count: int,
) -> None:
"""Test delay of firmware update check."""
reolink_host.baichuan_only = True

store = MagicMock()
last_firmware_check = (datetime.now(UTC) - timedelta(hours=last_check)).isoformat()
store.async_load = AsyncMock(return_value=last_firmware_check)

with patch("homeassistant.components.reolink.get_store", return_value=store):
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

Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the cleanup of cancel_first_firmware_check during unload. The test should verify that the scheduled firmware check is properly cancelled when the integration is unloaded before the first firmware check executes.

Consider adding a test that:

  1. Sets up the integration with a delayed firmware check
  2. Unloads the integration before the firmware check fires
  3. Verifies that the callback was cancelled and doesn't execute after unload

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant