Home Assistant Git Exporter
This commit is contained in:
@@ -38,6 +38,7 @@ from .const import (
|
||||
ATTR_CLIENT,
|
||||
ATTR_CONFIG,
|
||||
ATTR_COORDINATOR,
|
||||
ATTR_WS_EVENT_PROXY,
|
||||
ATTRIBUTE_LABELS,
|
||||
CONF_CAMERA_STATIC_IMAGE_HEIGHT,
|
||||
DOMAIN,
|
||||
@@ -52,6 +53,7 @@ from .const import (
|
||||
)
|
||||
from .views import async_setup as views_async_setup
|
||||
from .ws_api import async_setup as ws_api_async_setup
|
||||
from .ws_event_proxy import WSEventProxy
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
@@ -215,11 +217,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
model = f"{(await async_get_integration(hass, DOMAIN)).version}/{server_version}"
|
||||
|
||||
ws_event_proxy = WSEventProxy(config["mqtt"]["topic_prefix"])
|
||||
entry.async_on_unload(lambda: ws_event_proxy.unsubscribe_all(hass))
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ATTR_COORDINATOR: coordinator,
|
||||
ATTR_CLIENT: client,
|
||||
ATTR_CONFIG: config,
|
||||
ATTR_MODEL: model,
|
||||
ATTR_WS_EVENT_PROXY: ws_event_proxy,
|
||||
}
|
||||
|
||||
# Remove old devices associated with cameras that have since been removed
|
||||
|
||||
@@ -38,6 +38,7 @@ ATTR_PLAYBACK_FACTOR = "playback_factor"
|
||||
ATTR_PTZ_ACTION = "action"
|
||||
ATTR_PTZ_ARGUMENT = "argument"
|
||||
ATTR_START_TIME = "start_time"
|
||||
ATTR_WS_EVENT_PROXY = "ws_event_proxy"
|
||||
|
||||
# Frigate Attribute Labels
|
||||
# These are labels that are not individually tracked as they are
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"iot_class": "local_push",
|
||||
"issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues",
|
||||
"requirements": ["pytz"],
|
||||
"version": "5.3.0"
|
||||
"version": "5.4.0"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.frigate.api import FrigateApiClient, FrigateApiClientError
|
||||
from custom_components.frigate.views import get_client_for_frigate_instance_id
|
||||
from custom_components.frigate.const import ATTR_WS_EVENT_PROXY, DOMAIN
|
||||
from custom_components.frigate.views import (
|
||||
get_client_for_frigate_instance_id,
|
||||
get_config_entry_for_frigate_instance_id,
|
||||
)
|
||||
from custom_components.frigate.ws_event_proxy import WSEventProxy
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -21,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_get_events)
|
||||
websocket_api.async_register_command(hass, ws_get_events_summary)
|
||||
websocket_api.async_register_command(hass, ws_get_ptz_info)
|
||||
websocket_api.async_register_command(hass, ws_subscribe_events)
|
||||
websocket_api.async_register_command(hass, ws_unsubscribe_events)
|
||||
|
||||
|
||||
def _get_client_or_send_error(
|
||||
@@ -157,7 +164,6 @@ async def ws_get_recordings_summary(
|
||||
vol.Optional("limit"): int,
|
||||
vol.Optional("has_clip"): bool,
|
||||
vol.Optional("has_snapshot"): bool,
|
||||
vol.Optional("has_snapshot"): bool,
|
||||
vol.Optional("favorites"): bool,
|
||||
}
|
||||
) # type: ignore[misc]
|
||||
@@ -237,6 +243,70 @@ async def ws_get_events_summary(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frigate/events/subscribe",
|
||||
vol.Required("instance_id"): str,
|
||||
}
|
||||
) # type: ignore[misc]
|
||||
@websocket_api.async_response # type: ignore[misc]
|
||||
async def ws_subscribe_events(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
) -> None:
|
||||
"""Subscribe to events."""
|
||||
|
||||
entry = get_config_entry_for_frigate_instance_id(hass, msg["instance_id"])
|
||||
if not entry:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"not_found",
|
||||
f"API error whilst subscribing to events for unknown Frigate instance "
|
||||
f"{msg['instance_id']}",
|
||||
)
|
||||
return
|
||||
|
||||
event_proxy: WSEventProxy = hass.data[DOMAIN][entry.entry_id][ATTR_WS_EVENT_PROXY]
|
||||
connection.send_result(
|
||||
msg["id"], await event_proxy.subscribe(hass, msg["id"], connection)
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frigate/events/unsubscribe",
|
||||
vol.Required("instance_id"): str,
|
||||
vol.Required("subscription_id"): int,
|
||||
}
|
||||
) # type: ignore[misc]
|
||||
@websocket_api.async_response # type: ignore[misc]
|
||||
async def ws_unsubscribe_events(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
) -> None:
|
||||
"""Unsubscribe from events."""
|
||||
|
||||
entry = get_config_entry_for_frigate_instance_id(hass, msg["instance_id"])
|
||||
if not entry:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"not_found",
|
||||
f"API error whilst unsubscribing to events for unknown Frigate instance "
|
||||
f"{msg['instance_id']}",
|
||||
)
|
||||
return
|
||||
|
||||
event_proxy: WSEventProxy = hass.data[DOMAIN][entry.entry_id][ATTR_WS_EVENT_PROXY]
|
||||
if event_proxy.unsubscribe(hass, msg["subscription_id"]):
|
||||
connection.send_result(msg["id"])
|
||||
else:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Subscription not found."
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frigate/ptz/info",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Frigate event proxy."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.mqtt.models import ReceiveMessage
|
||||
from homeassistant.components.mqtt.subscription import (
|
||||
async_prepare_subscribe_topics,
|
||||
async_subscribe_topics,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from homeassistant.components.websocket_api import messages
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WSEventProxy:
|
||||
"""Frigate event MQTT to WS proxy.
|
||||
|
||||
This class subscribes to the MQTT events topic for a given Frigate topic and
|
||||
forwards the messages to a list of subscribers. MQTT payload is directly
|
||||
passed to subscribers to avoid JSON serialization/deserialization overhead
|
||||
within HA.
|
||||
"""
|
||||
|
||||
def __init__(self, topic_prefix: str) -> None:
|
||||
self._subscriptions: dict[int, websocket_api.ActiveConnection] = {}
|
||||
self._topics = {
|
||||
"events": {
|
||||
"topic": f"{topic_prefix}/events",
|
||||
"msg_callback": lambda msg: self._receive_message(msg),
|
||||
"qos": 0,
|
||||
}
|
||||
}
|
||||
self._sub_state = None
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
subscription_id: int,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
) -> int:
|
||||
"""Subscribe to events."""
|
||||
|
||||
if self._sub_state is None:
|
||||
self._sub_state = async_prepare_subscribe_topics(
|
||||
hass, self._sub_state, self._topics
|
||||
)
|
||||
await async_subscribe_topics(hass, self._sub_state)
|
||||
|
||||
# Add a callback to the websocket to unsubscribe if closed.
|
||||
connection.subscriptions[subscription_id] = lambda: self._unsubscribe_internal(
|
||||
hass, subscription_id
|
||||
)
|
||||
self._subscriptions[subscription_id] = connection
|
||||
return subscription_id
|
||||
|
||||
def unsubscribe(self, hass: HomeAssistant, subscription_id: int) -> bool:
|
||||
"""Unsubscribe from events."""
|
||||
|
||||
if (
|
||||
subscription_id in self._subscriptions
|
||||
and subscription_id in self._subscriptions[subscription_id].subscriptions
|
||||
):
|
||||
self._subscriptions[subscription_id].subscriptions.pop(subscription_id)
|
||||
return self._unsubscribe_internal(hass, subscription_id)
|
||||
|
||||
def _unsubscribe_internal(self, hass: HomeAssistant, subscription_id: int) -> bool:
|
||||
"""Unsubscribe from events.
|
||||
|
||||
May be called from the websocket connection close handler. As a result
|
||||
must not change the size of connection.subscriptions which is iterated
|
||||
over in that handler.
|
||||
"""
|
||||
|
||||
if subscription_id not in self._subscriptions:
|
||||
return False
|
||||
self._subscriptions.pop(subscription_id)
|
||||
|
||||
if not self._subscriptions:
|
||||
async_unsubscribe_topics(hass, self._sub_state)
|
||||
self._sub_state = None
|
||||
return True
|
||||
|
||||
def unsubscribe_all(self, hass: HomeAssistant) -> None:
|
||||
"""Unsubscribe all subscribers."""
|
||||
for subscription_id in list(self._subscriptions.keys()):
|
||||
self.unsubscribe(hass, subscription_id)
|
||||
|
||||
def _receive_message(self, msg: ReceiveMessage) -> None:
|
||||
"""Handle a new received MQTT message."""
|
||||
for id, connection in self._subscriptions.items():
|
||||
connection.send_message(messages.event_message(id, msg.payload))
|
||||
Reference in New Issue
Block a user