mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Reolink ONVIF long polling (#94770)
This commit is contained in:
parent
f9707cc87b
commit
0bec93fa37
@ -25,9 +25,11 @@ from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN
|
|||||||
from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin
|
from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 60
|
DEFAULT_TIMEOUT = 60
|
||||||
FIRST_ONVIF_TIMEOUT = 15
|
FIRST_ONVIF_TIMEOUT = 10
|
||||||
SUBSCRIPTION_RENEW_THRESHOLD = 300
|
SUBSCRIPTION_RENEW_THRESHOLD = 300
|
||||||
POLL_INTERVAL_NO_PUSH = 5
|
POLL_INTERVAL_NO_PUSH = 5
|
||||||
|
LONG_POLL_COOLDOWN = 0.75
|
||||||
|
LONG_POLL_ERROR_COOLDOWN = 30
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -60,10 +62,14 @@ class ReolinkHost:
|
|||||||
self.webhook_id: str | None = None
|
self.webhook_id: str | None = None
|
||||||
self._base_url: str = ""
|
self._base_url: str = ""
|
||||||
self._webhook_url: str = ""
|
self._webhook_url: str = ""
|
||||||
self._webhook_reachable: asyncio.Event = asyncio.Event()
|
self._webhook_reachable: bool = False
|
||||||
|
self._long_poll_received: bool = False
|
||||||
|
self._long_poll_error: bool = False
|
||||||
self._cancel_poll: CALLBACK_TYPE | None = None
|
self._cancel_poll: CALLBACK_TYPE | None = None
|
||||||
self._cancel_onvif_check: CALLBACK_TYPE | None = None
|
self._cancel_onvif_check: CALLBACK_TYPE | None = None
|
||||||
|
self._cancel_long_poll_check: CALLBACK_TYPE | None = None
|
||||||
self._poll_job = HassJob(self._async_poll_all_motion, cancel_on_shutdown=True)
|
self._poll_job = HassJob(self._async_poll_all_motion, cancel_on_shutdown=True)
|
||||||
|
self._long_poll_task: asyncio.Task | None = None
|
||||||
self._lost_subscription: bool = False
|
self._lost_subscription: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -185,15 +191,32 @@ class ReolinkHost:
|
|||||||
|
|
||||||
async def _async_check_onvif(self, *_) -> None:
|
async def _async_check_onvif(self, *_) -> None:
|
||||||
"""Check the ONVIF subscription."""
|
"""Check the ONVIF subscription."""
|
||||||
if (
|
if self._webhook_reachable:
|
||||||
self._api.supported(None, "initial_ONVIF_state")
|
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
||||||
and not self._webhook_reachable.is_set()
|
self._cancel_onvif_check = None
|
||||||
):
|
return
|
||||||
|
if self._api.supported(None, "initial_ONVIF_state"):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Did not receive initial ONVIF state on webhook '%s' after %i seconds",
|
"Did not receive initial ONVIF state on webhook '%s' after %i seconds",
|
||||||
self._webhook_url,
|
self._webhook_url,
|
||||||
FIRST_ONVIF_TIMEOUT,
|
FIRST_ONVIF_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ONVIF push is not received, start long polling and schedule check
|
||||||
|
await self._async_start_long_polling()
|
||||||
|
self._cancel_long_poll_check = async_call_later(
|
||||||
|
self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif_long_poll
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cancel_onvif_check = None
|
||||||
|
|
||||||
|
async def _async_check_onvif_long_poll(self, *_) -> None:
|
||||||
|
"""Check if ONVIF long polling is working."""
|
||||||
|
if not self._long_poll_received:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Did not receive state through ONVIF long polling after %i seconds",
|
||||||
|
FIRST_ONVIF_TIMEOUT,
|
||||||
|
)
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
self._hass,
|
self._hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -210,10 +233,10 @@ class ReolinkHost:
|
|||||||
else:
|
else:
|
||||||
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
||||||
|
|
||||||
# If no ONVIF push is received, start fast polling
|
# If no ONVIF push or long polling state is received, start fast polling
|
||||||
await self._async_poll_all_motion()
|
await self._async_poll_all_motion()
|
||||||
|
|
||||||
self._cancel_onvif_check = None
|
self._cancel_long_poll_check = None
|
||||||
|
|
||||||
async def update_states(self) -> None:
|
async def update_states(self) -> None:
|
||||||
"""Call the API of the camera device to update the internal states."""
|
"""Call the API of the camera device to update the internal states."""
|
||||||
@ -241,6 +264,20 @@ class ReolinkHost:
|
|||||||
str(err),
|
str(err),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _async_start_long_polling(self):
|
||||||
|
"""Start ONVIF long polling task."""
|
||||||
|
if self._long_poll_task is None:
|
||||||
|
await self._api.subscribe(sub_type=SubType.long_poll)
|
||||||
|
self._long_poll_task = asyncio.create_task(self._async_long_polling())
|
||||||
|
|
||||||
|
async def _async_stop_long_polling(self):
|
||||||
|
"""Stop ONVIF long polling task."""
|
||||||
|
if self._long_poll_task is not None:
|
||||||
|
self._long_poll_task.cancel()
|
||||||
|
self._long_poll_task = None
|
||||||
|
|
||||||
|
await self._api.unsubscribe(sub_type=SubType.long_poll)
|
||||||
|
|
||||||
async def stop(self, event=None):
|
async def stop(self, event=None):
|
||||||
"""Disconnect the API."""
|
"""Disconnect the API."""
|
||||||
if self._cancel_poll is not None:
|
if self._cancel_poll is not None:
|
||||||
@ -249,6 +286,10 @@ class ReolinkHost:
|
|||||||
if self._cancel_onvif_check is not None:
|
if self._cancel_onvif_check is not None:
|
||||||
self._cancel_onvif_check()
|
self._cancel_onvif_check()
|
||||||
self._cancel_onvif_check = None
|
self._cancel_onvif_check = None
|
||||||
|
if self._cancel_long_poll_check is not None:
|
||||||
|
self._cancel_long_poll_check()
|
||||||
|
self._cancel_long_poll_check = None
|
||||||
|
await self._async_stop_long_polling()
|
||||||
self.unregister_webhook()
|
self.unregister_webhook()
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
|
|
||||||
@ -277,6 +318,8 @@ class ReolinkHost:
|
|||||||
"""Renew the subscription of motion events (lease time is 15 minutes)."""
|
"""Renew the subscription of motion events (lease time is 15 minutes)."""
|
||||||
try:
|
try:
|
||||||
await self._renew(SubType.push)
|
await self._renew(SubType.push)
|
||||||
|
if self._long_poll_task is not None:
|
||||||
|
await self._renew(SubType.long_poll)
|
||||||
except SubscriptionError as err:
|
except SubscriptionError as err:
|
||||||
if not self._lost_subscription:
|
if not self._lost_subscription:
|
||||||
self._lost_subscription = True
|
self._lost_subscription = True
|
||||||
@ -297,7 +340,10 @@ class ReolinkHost:
|
|||||||
self._api.host,
|
self._api.host,
|
||||||
sub_type,
|
sub_type,
|
||||||
)
|
)
|
||||||
await self.subscribe()
|
if sub_type == SubType.push:
|
||||||
|
await self.subscribe()
|
||||||
|
else:
|
||||||
|
await self._api.subscribe(self._webhook_url, sub_type)
|
||||||
return
|
return
|
||||||
|
|
||||||
timer = self._api.renewtimer(sub_type)
|
timer = self._api.renewtimer(sub_type)
|
||||||
@ -386,10 +432,44 @@ class ReolinkHost:
|
|||||||
webhook.async_unregister(self._hass, self.webhook_id)
|
webhook.async_unregister(self._hass, self.webhook_id)
|
||||||
self.webhook_id = None
|
self.webhook_id = None
|
||||||
|
|
||||||
|
async def _async_long_polling(self, *_) -> None:
|
||||||
|
"""Use ONVIF long polling to immediately receive events."""
|
||||||
|
# This task will be cancelled once _async_stop_long_polling is called
|
||||||
|
while True:
|
||||||
|
if self._webhook_reachable:
|
||||||
|
self._long_poll_task = None
|
||||||
|
await self._async_stop_long_polling()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
channels = await self._api.pull_point_request()
|
||||||
|
except ReolinkError as ex:
|
||||||
|
if not self._long_poll_error:
|
||||||
|
_LOGGER.error("Error while requesting ONVIF pull point: %s", ex)
|
||||||
|
await self._api.unsubscribe(sub_type=SubType.long_poll)
|
||||||
|
self._long_poll_error = True
|
||||||
|
await asyncio.sleep(LONG_POLL_ERROR_COOLDOWN)
|
||||||
|
continue
|
||||||
|
except Exception as ex:
|
||||||
|
_LOGGER.exception("Error while requesting ONVIF pull point: %s", ex)
|
||||||
|
await self._api.unsubscribe(sub_type=SubType.long_poll)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
self._long_poll_error = False
|
||||||
|
|
||||||
|
if not self._long_poll_received and channels != []:
|
||||||
|
self._long_poll_received = True
|
||||||
|
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
||||||
|
|
||||||
|
self._signal_write_ha_state(channels)
|
||||||
|
|
||||||
|
# Cooldown to prevent CPU over usage on camera freezes
|
||||||
|
await asyncio.sleep(LONG_POLL_COOLDOWN)
|
||||||
|
|
||||||
async def _async_poll_all_motion(self, *_) -> None:
|
async def _async_poll_all_motion(self, *_) -> None:
|
||||||
"""Poll motion and AI states until the first ONVIF push is received."""
|
"""Poll motion and AI states until the first ONVIF push is received."""
|
||||||
if self._webhook_reachable.is_set():
|
if self._webhook_reachable or self._long_poll_received:
|
||||||
# ONVIF push is working, stop polling
|
# ONVIF push or long polling is working, stop fast polling
|
||||||
self._cancel_poll = None
|
self._cancel_poll = None
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -409,10 +489,7 @@ class ReolinkHost:
|
|||||||
self._hass, POLL_INTERVAL_NO_PUSH, self._poll_job
|
self._hass, POLL_INTERVAL_NO_PUSH, self._poll_job
|
||||||
)
|
)
|
||||||
|
|
||||||
# After receiving the new motion states in the upstream lib,
|
self._signal_write_ha_state(None)
|
||||||
# update the binary sensors with async_write_ha_state
|
|
||||||
# The same dispatch as for the webhook can be used
|
|
||||||
async_dispatcher_send(self._hass, f"{self.webhook_id}_all", {})
|
|
||||||
|
|
||||||
async def handle_webhook(
|
async def handle_webhook(
|
||||||
self, hass: HomeAssistant, webhook_id: str, request: Request
|
self, hass: HomeAssistant, webhook_id: str, request: Request
|
||||||
@ -460,8 +537,8 @@ class ReolinkHost:
|
|||||||
"""Process the data from the Reolink webhook."""
|
"""Process the data from the Reolink webhook."""
|
||||||
# This task is executed in the background so we need to catch exceptions
|
# This task is executed in the background so we need to catch exceptions
|
||||||
# and log them
|
# and log them
|
||||||
if not self._webhook_reachable.is_set():
|
if not self._webhook_reachable:
|
||||||
self._webhook_reachable.set()
|
self._webhook_reachable = True
|
||||||
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -484,9 +561,13 @@ class ReolinkHost:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._signal_write_ha_state(channels)
|
||||||
|
|
||||||
|
def _signal_write_ha_state(self, channels: list[int] | None) -> None:
|
||||||
|
"""Update the binary sensors with async_write_ha_state."""
|
||||||
if channels is None:
|
if channels is None:
|
||||||
async_dispatcher_send(hass, f"{webhook_id}_all", {})
|
async_dispatcher_send(self._hass, f"{self.webhook_id}_all", {})
|
||||||
return
|
return
|
||||||
|
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
async_dispatcher_send(hass, f"{webhook_id}_{channel}", {})
|
async_dispatcher_send(self._hass, f"{self.webhook_id}_{channel}", {})
|
||||||
|
@ -148,7 +148,11 @@ async def test_webhook_repair_issue(
|
|||||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test repairs issue is raised when the webhook url is unreachable."""
|
"""Test repairs issue is raised when the webhook url is unreachable."""
|
||||||
with patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0):
|
with patch(
|
||||||
|
"homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.reolink.host.ReolinkHost._async_long_polling",
|
||||||
|
):
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user