mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Reolink conserve battery (#145452)
This commit is contained in:
parent
6c098c3e0a
commit
78d2bf736c
@ -3,8 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from reolink_aio.api import RETRY_ATTEMPTS
|
||||
@ -28,7 +30,13 @@ from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
|
||||
from .const import (
|
||||
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
|
||||
CONF_BC_PORT,
|
||||
CONF_SUPPORTS_PRIVACY_MODE,
|
||||
CONF_USE_HTTPS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin
|
||||
from .host import ReolinkHost
|
||||
from .services import async_setup_services
|
||||
@ -220,6 +228,24 @@ async def async_setup_entry(
|
||||
|
||||
hass.http.register_view(PlaybackProxyView(hass))
|
||||
|
||||
await register_callbacks(host, device_coordinator, hass)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
config_entry.add_update_listener(entry_update_listener)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def register_callbacks(
|
||||
host: ReolinkHost,
|
||||
device_coordinator: DataUpdateCoordinator[None],
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Register update callbacks."""
|
||||
|
||||
async def refresh(*args: Any) -> None:
|
||||
"""Request refresh of coordinator."""
|
||||
await device_coordinator.async_request_refresh()
|
||||
@ -233,17 +259,29 @@ async def async_setup_entry(
|
||||
host.cancel_refresh_privacy_mode = async_call_later(hass, 2, refresh)
|
||||
host.privacy_mode = host.api.baichuan.privacy_mode()
|
||||
|
||||
def generate_async_camera_wake(channel: int) -> Callable[[], None]:
|
||||
def async_camera_wake() -> None:
|
||||
"""Request update when a battery camera wakes up."""
|
||||
if (
|
||||
not host.api.sleeping(channel)
|
||||
and time() - host.last_wake[channel]
|
||||
> BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
|
||||
):
|
||||
hass.loop.create_task(device_coordinator.async_request_refresh())
|
||||
|
||||
return async_camera_wake
|
||||
|
||||
host.api.baichuan.register_callback(
|
||||
"privacy_mode_change", async_privacy_mode_change, 623
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
config_entry.add_update_listener(entry_update_listener)
|
||||
)
|
||||
|
||||
return True
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
host.api.baichuan.register_callback(
|
||||
f"camera_{channel}_wake",
|
||||
generate_async_camera_wake(channel),
|
||||
145,
|
||||
channel,
|
||||
)
|
||||
|
||||
|
||||
async def entry_update_listener(
|
||||
@ -262,6 +300,9 @@ async def async_unload_entry(
|
||||
await host.stop()
|
||||
|
||||
host.api.baichuan.unregister_callback("privacy_mode_change")
|
||||
for channel in host.api.channels:
|
||||
if host.api.supported(channel, "battery"):
|
||||
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
|
||||
if host.cancel_refresh_privacy_mode is not None:
|
||||
host.cancel_refresh_privacy_mode()
|
||||
|
||||
|
@ -5,3 +5,9 @@ DOMAIN = "reolink"
|
||||
CONF_USE_HTTPS = "use_https"
|
||||
CONF_BC_PORT = "baichuan_port"
|
||||
CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported"
|
||||
|
||||
# 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
|
||||
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL = 3600 # seconds
|
||||
BATTERY_WAKE_UPDATE_INTERVAL = 6 * BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
|
||||
BATTERY_ALL_WAKE_UPDATE_INTERVAL = 2 * BATTERY_WAKE_UPDATE_INTERVAL
|
||||
|
@ -142,7 +142,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Force full update from the generic entity update service."""
|
||||
self._host.last_wake = 0
|
||||
for channel in self._host.api.channels:
|
||||
if self._host.api.supported(channel, "battery"):
|
||||
self._host.last_wake[channel] = 0
|
||||
await super().async_update()
|
||||
|
||||
|
||||
|
@ -34,7 +34,15 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.ssl import SSLCipherList
|
||||
|
||||
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
|
||||
from .const import (
|
||||
BATTERY_ALL_WAKE_UPDATE_INTERVAL,
|
||||
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
|
||||
BATTERY_WAKE_UPDATE_INTERVAL,
|
||||
CONF_BC_PORT,
|
||||
CONF_SUPPORTS_PRIVACY_MODE,
|
||||
CONF_USE_HTTPS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .exceptions import (
|
||||
PasswordIncompatible,
|
||||
ReolinkSetupException,
|
||||
@ -52,10 +60,6 @@ POLL_INTERVAL_NO_PUSH = 5
|
||||
LONG_POLL_COOLDOWN = 0.75
|
||||
LONG_POLL_ERROR_COOLDOWN = 30
|
||||
|
||||
# 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
|
||||
BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -95,7 +99,8 @@ class ReolinkHost:
|
||||
bc_port=config.get(CONF_BC_PORT, DEFAULT_BC_PORT),
|
||||
)
|
||||
|
||||
self.last_wake: float = 0
|
||||
self.last_wake: defaultdict[int, float] = defaultdict(float)
|
||||
self.last_all_wake: float = 0
|
||||
self.update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict(
|
||||
lambda: defaultdict(int)
|
||||
)
|
||||
@ -459,15 +464,34 @@ class ReolinkHost:
|
||||
|
||||
async def update_states(self) -> None:
|
||||
"""Call the API of the camera device to update the internal states."""
|
||||
wake = False
|
||||
if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL:
|
||||
wake: dict[int, bool] = {}
|
||||
now = time()
|
||||
for channel in self._api.stream_channels:
|
||||
# wake the battery cameras for a complete update
|
||||
wake = True
|
||||
self.last_wake = time()
|
||||
if not self._api.supported(channel, "battery"):
|
||||
wake[channel] = True
|
||||
elif (
|
||||
(
|
||||
not self._api.sleeping(channel)
|
||||
and now - self.last_wake[channel]
|
||||
> BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
|
||||
)
|
||||
or (now - self.last_wake[channel] > BATTERY_WAKE_UPDATE_INTERVAL)
|
||||
or (now - self.last_all_wake > BATTERY_ALL_WAKE_UPDATE_INTERVAL)
|
||||
):
|
||||
# let a waking update coincide with the camera waking up by itself unless it did not wake for BATTERY_WAKE_UPDATE_INTERVAL
|
||||
wake[channel] = True
|
||||
self.last_wake[channel] = now
|
||||
else:
|
||||
wake[channel] = False
|
||||
|
||||
for channel in self._api.channels:
|
||||
# check privacy mode if enabled
|
||||
if self._api.baichuan.privacy_mode(channel):
|
||||
await self._api.baichuan.get_privacy_mode(channel)
|
||||
|
||||
if all(wake.values()):
|
||||
self.last_all_wake = now
|
||||
|
||||
if self._api.baichuan.privacy_mode():
|
||||
return # API is shutdown, no need to check states
|
||||
|
||||
|
@ -19,7 +19,12 @@ from homeassistant.components.reolink import (
|
||||
FIRMWARE_UPDATE_INTERVAL,
|
||||
NUM_CRED_ERRORS,
|
||||
)
|
||||
from homeassistant.components.reolink.const import CONF_BC_PORT, DOMAIN
|
||||
from homeassistant.components.reolink.const import (
|
||||
BATTERY_ALL_WAKE_UPDATE_INTERVAL,
|
||||
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
|
||||
CONF_BC_PORT,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -1111,6 +1116,76 @@ async def test_privacy_mode_change_callback(
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_camera_wake_callback(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera wake callback."""
|
||||
|
||||
class callback_mock_class:
|
||||
callback_func = None
|
||||
|
||||
def register_callback(
|
||||
self, callback_id: str, callback: Callable[[], None], *args, **key_args
|
||||
) -> None:
|
||||
if callback_id == "camera_0_wake":
|
||||
self.callback_func = callback
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.events_active = True
|
||||
reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_connect.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_connect.sleeping.return_value = True
|
||||
reolink_connect.audio_record.return_value = True
|
||||
reolink_connect.get_states = AsyncMock()
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]),
|
||||
patch(
|
||||
"homeassistant.components.reolink.host.time",
|
||||
return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_connect.sleeping.return_value = False
|
||||
reolink_connect.get_states.reset_mock()
|
||||
assert reolink_connect.get_states.call_count == 0
|
||||
|
||||
# simulate a TCP push callback signaling the battery camera woke up
|
||||
reolink_connect.audio_record.return_value = False
|
||||
assert callback_mock.callback_func is not None
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.reolink.host.time",
|
||||
return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL
|
||||
+ BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
|
||||
+ 5,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.reolink.time",
|
||||
return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL
|
||||
+ BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
|
||||
+ 5,
|
||||
),
|
||||
):
|
||||
callback_mock.callback_func()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# check that a coordinator update was scheduled.
|
||||
assert reolink_connect.get_states.call_count >= 1
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
|
||||
async def test_remove(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
|
Loading…
x
Reference in New Issue
Block a user