Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions homeassistant/components/reolink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

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

Expand Down Expand Up @@ -34,6 +35,7 @@
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
CONF_BC_ONLY,
CONF_BC_PORT,
CONF_FIRMWARE_CHECK_TIME,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
Expand Down Expand Up @@ -212,15 +214,41 @@ async def async_check_firmware_update() -> None:
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()
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/reolink/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/reolink/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def get_aiohttp_session() -> aiohttp.ClientSession:
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:
Expand Down
57 changes: 56 additions & 1 deletion tests/components/reolink/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,6 +23,7 @@
BATTERY_ALL_WAKE_UPDATE_INTERVAL,
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
CONF_BC_PORT,
CONF_FIRMWARE_CHECK_TIME,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
Expand All @@ -47,6 +49,7 @@
from homeassistant.setup import async_setup_component

from .conftest import (
CONF_BC_ONLY,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DEFAULT_PROTOCOL,
Expand All @@ -58,6 +61,7 @@
TEST_MAC,
TEST_MAC_CAM,
TEST_NVR_NAME,
TEST_PASSWORD,
TEST_PORT,
TEST_PRIVACY,
TEST_UID,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -1130,6 +1138,53 @@ def register_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,
Expand Down
Loading