mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-03 06:50:58 +00:00
Compare commits
No commits in common. "9b2f84d3e92fea9ff2564b9fb4094f11355746d6" and "5d2328599ff717f4c7a73f4a141dac57e7fa5a38" have entirely different histories.
9b2f84d3e9
...
5d2328599f
@ -88,9 +88,7 @@ Sometimes objects are expected to be passing through a zone, but an object loite
|
|||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|
||||||
When using loitering zones, a review item will behave in the following way:
|
When using loitering zones, a review item will remain active until the object leaves. Loitering zones are only meant to be used in areas where loitering is not expected behavior.
|
||||||
- When a person is in a loitering zone, the review item will remain active until the person leaves the loitering zone, regardless of if they are stationary.
|
|
||||||
- When any other object is in a loitering zone, the review item will remain active until the loitering time is met. Then if the object is stationary the review item will end.
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@ -1094,7 +1094,7 @@ def set_sub_label(
|
|||||||
new_score = None
|
new_score = None
|
||||||
|
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
(event_id, new_sub_label, new_score), EventMetadataTypeEnum.sub_label.value
|
EventMetadataTypeEnum.sub_label, (event_id, new_sub_label, new_score)
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1148,8 +1148,8 @@ def set_plate(
|
|||||||
new_score = None
|
new_score = None
|
||||||
|
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
|
EventMetadataTypeEnum.attribute,
|
||||||
(event_id, "recognized_license_plate", new_plate, new_score),
|
(event_id, "recognized_license_plate", new_plate, new_score),
|
||||||
EventMetadataTypeEnum.attribute.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1232,8 +1232,8 @@ def regenerate_description(
|
|||||||
|
|
||||||
if camera_config.genai.enabled or params.force:
|
if camera_config.genai.enabled or params.force:
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
|
EventMetadataTypeEnum.regenerate_description,
|
||||||
(event.id, params.source, params.force),
|
(event.id, params.source, params.force),
|
||||||
EventMetadataTypeEnum.regenerate_description.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1390,6 +1390,7 @@ def create_event(
|
|||||||
event_id = f"{now}-{rand_id}"
|
event_id = f"{now}-{rand_id}"
|
||||||
|
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
|
EventMetadataTypeEnum.manual_event_create,
|
||||||
(
|
(
|
||||||
now,
|
now,
|
||||||
camera_name,
|
camera_name,
|
||||||
@ -1402,7 +1403,6 @@ def create_event(
|
|||||||
body.source_type,
|
body.source_type,
|
||||||
body.draw,
|
body.draw,
|
||||||
),
|
),
|
||||||
EventMetadataTypeEnum.manual_event_create.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1426,7 +1426,7 @@ def end_event(request: Request, event_id: str, body: EventsEndBody):
|
|||||||
try:
|
try:
|
||||||
end_time = body.end_time or datetime.datetime.now().timestamp()
|
end_time = body.end_time or datetime.datetime.now().timestamp()
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
(event_id, end_time), EventMetadataTypeEnum.manual_event_end.value
|
EventMetadataTypeEnum.manual_event_end, (event_id, end_time)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
from _pickle import UnpicklingError
|
from _pickle import UnpicklingError
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class ConfigPublisher:
|
|||||||
class ConfigSubscriber:
|
class ConfigSubscriber:
|
||||||
"""Simplifies receiving an updated config."""
|
"""Simplifies receiving an updated config."""
|
||||||
|
|
||||||
def __init__(self, topic: str, exact: bool = False) -> None:
|
def __init__(self, topic: str, exact=False) -> None:
|
||||||
self.topic = topic
|
self.topic = topic
|
||||||
self.exact = exact
|
self.exact = exact
|
||||||
self.context = zmq.Context()
|
self.context = zmq.Context()
|
||||||
@ -41,7 +41,7 @@ class ConfigSubscriber:
|
|||||||
self.socket.setsockopt_string(zmq.SUBSCRIBE, topic)
|
self.socket.setsockopt_string(zmq.SUBSCRIBE, topic)
|
||||||
self.socket.connect(SOCKET_PUB_SUB)
|
self.socket.connect(SOCKET_PUB_SUB)
|
||||||
|
|
||||||
def check_for_update(self) -> tuple[str, Any] | tuple[None, None]:
|
def check_for_update(self) -> Optional[tuple[str, Any]]:
|
||||||
"""Returns updated config or None if no update."""
|
"""Returns updated config or None if no update."""
|
||||||
try:
|
try:
|
||||||
topic = self.socket.recv_string(flags=zmq.NOBLOCK)
|
topic = self.socket.recv_string(flags=zmq.NOBLOCK)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from .zmq_proxy import Publisher, Subscriber
|
from .zmq_proxy import Publisher, Subscriber
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ class DetectionPublisher(Publisher):
|
|||||||
|
|
||||||
topic_base = "detection/"
|
topic_base = "detection/"
|
||||||
|
|
||||||
def __init__(self, topic: str) -> None:
|
def __init__(self, topic: DetectionTypeEnum) -> None:
|
||||||
|
topic = topic.value
|
||||||
super().__init__(topic)
|
super().__init__(topic)
|
||||||
|
|
||||||
|
|
||||||
@ -28,15 +29,16 @@ class DetectionSubscriber(Subscriber):
|
|||||||
|
|
||||||
topic_base = "detection/"
|
topic_base = "detection/"
|
||||||
|
|
||||||
def __init__(self, topic: str) -> None:
|
def __init__(self, topic: DetectionTypeEnum) -> None:
|
||||||
|
topic = topic.value
|
||||||
super().__init__(topic)
|
super().__init__(topic)
|
||||||
|
|
||||||
def check_for_update(
|
def check_for_update(
|
||||||
self, timeout: float | None = None
|
self, timeout: float = None
|
||||||
) -> tuple[str, Any] | tuple[None, None] | None:
|
) -> Optional[tuple[DetectionTypeEnum, Any]]:
|
||||||
return super().check_for_update(timeout)
|
return super().check_for_update(timeout)
|
||||||
|
|
||||||
def _return_object(self, topic: str, payload: Any) -> Any:
|
def _return_object(self, topic: str, payload: Any) -> Any:
|
||||||
if payload is None:
|
if payload is None:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
return (topic[len(self.topic_base) :], payload)
|
return (DetectionTypeEnum[topic[len(self.topic_base) :]], payload)
|
||||||
|
|||||||
@ -54,9 +54,10 @@ class Dispatcher:
|
|||||||
self.ptz_metrics = ptz_metrics
|
self.ptz_metrics = ptz_metrics
|
||||||
self.comms = communicators
|
self.comms = communicators
|
||||||
self.camera_activity = CameraActivityManager(config, self.publish)
|
self.camera_activity = CameraActivityManager(config, self.publish)
|
||||||
self.model_state: dict[str, ModelStatusTypesEnum] = {}
|
self.model_state = {}
|
||||||
self.embeddings_reindex: dict[str, Any] = {}
|
self.embeddings_reindex = {}
|
||||||
self.birdseye_layout: dict[str, Any] = {}
|
self.birdseye_layout = {}
|
||||||
|
|
||||||
self._camera_settings_handlers: dict[str, Callable] = {
|
self._camera_settings_handlers: dict[str, Callable] = {
|
||||||
"audio": self._on_audio_command,
|
"audio": self._on_audio_command,
|
||||||
"audio_transcription": self._on_audio_transcription_command,
|
"audio_transcription": self._on_audio_transcription_command,
|
||||||
@ -87,12 +88,10 @@ class Dispatcher:
|
|||||||
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
|
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _receive(self, topic: str, payload: Any) -> Optional[Any]:
|
def _receive(self, topic: str, payload: str) -> Optional[Any]:
|
||||||
"""Handle receiving of payload from communicators."""
|
"""Handle receiving of payload from communicators."""
|
||||||
|
|
||||||
def handle_camera_command(
|
def handle_camera_command(command_type, camera_name, command, payload):
|
||||||
command_type: str, camera_name: str, command: str, payload: str
|
|
||||||
) -> None:
|
|
||||||
try:
|
try:
|
||||||
if command_type == "set":
|
if command_type == "set":
|
||||||
self._camera_settings_handlers[command](camera_name, payload)
|
self._camera_settings_handlers[command](camera_name, payload)
|
||||||
@ -101,13 +100,13 @@ class Dispatcher:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
logger.error(f"Invalid command type or handler: {command_type}")
|
logger.error(f"Invalid command type or handler: {command_type}")
|
||||||
|
|
||||||
def handle_restart() -> None:
|
def handle_restart():
|
||||||
restart_frigate()
|
restart_frigate()
|
||||||
|
|
||||||
def handle_insert_many_recordings() -> None:
|
def handle_insert_many_recordings():
|
||||||
Recordings.insert_many(payload).execute()
|
Recordings.insert_many(payload).execute()
|
||||||
|
|
||||||
def handle_request_region_grid() -> Any:
|
def handle_request_region_grid():
|
||||||
camera = payload
|
camera = payload
|
||||||
grid = get_camera_regions_grid(
|
grid = get_camera_regions_grid(
|
||||||
camera,
|
camera,
|
||||||
@ -116,24 +115,24 @@ class Dispatcher:
|
|||||||
)
|
)
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def handle_insert_preview() -> None:
|
def handle_insert_preview():
|
||||||
Previews.insert(payload).execute()
|
Previews.insert(payload).execute()
|
||||||
|
|
||||||
def handle_upsert_review_segment() -> None:
|
def handle_upsert_review_segment():
|
||||||
ReviewSegment.insert(payload).on_conflict(
|
ReviewSegment.insert(payload).on_conflict(
|
||||||
conflict_target=[ReviewSegment.id],
|
conflict_target=[ReviewSegment.id],
|
||||||
update=payload,
|
update=payload,
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
def handle_clear_ongoing_review_segments() -> None:
|
def handle_clear_ongoing_review_segments():
|
||||||
ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where(
|
ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where(
|
||||||
ReviewSegment.end_time.is_null(True)
|
ReviewSegment.end_time.is_null(True)
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
def handle_update_camera_activity() -> None:
|
def handle_update_camera_activity():
|
||||||
self.camera_activity.update_activity(payload)
|
self.camera_activity.update_activity(payload)
|
||||||
|
|
||||||
def handle_update_event_description() -> None:
|
def handle_update_event_description():
|
||||||
event: Event = Event.get(Event.id == payload["id"])
|
event: Event = Event.get(Event.id == payload["id"])
|
||||||
event.data["description"] = payload["description"]
|
event.data["description"] = payload["description"]
|
||||||
event.save()
|
event.save()
|
||||||
@ -149,38 +148,38 @@ class Dispatcher:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_update_model_state() -> None:
|
def handle_update_model_state():
|
||||||
if payload:
|
if payload:
|
||||||
model = payload["model"]
|
model = payload["model"]
|
||||||
state = payload["state"]
|
state = payload["state"]
|
||||||
self.model_state[model] = ModelStatusTypesEnum[state]
|
self.model_state[model] = ModelStatusTypesEnum[state]
|
||||||
self.publish("model_state", json.dumps(self.model_state))
|
self.publish("model_state", json.dumps(self.model_state))
|
||||||
|
|
||||||
def handle_model_state() -> None:
|
def handle_model_state():
|
||||||
self.publish("model_state", json.dumps(self.model_state.copy()))
|
self.publish("model_state", json.dumps(self.model_state.copy()))
|
||||||
|
|
||||||
def handle_update_embeddings_reindex_progress() -> None:
|
def handle_update_embeddings_reindex_progress():
|
||||||
self.embeddings_reindex = payload
|
self.embeddings_reindex = payload
|
||||||
self.publish(
|
self.publish(
|
||||||
"embeddings_reindex_progress",
|
"embeddings_reindex_progress",
|
||||||
json.dumps(payload),
|
json.dumps(payload),
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_embeddings_reindex_progress() -> None:
|
def handle_embeddings_reindex_progress():
|
||||||
self.publish(
|
self.publish(
|
||||||
"embeddings_reindex_progress",
|
"embeddings_reindex_progress",
|
||||||
json.dumps(self.embeddings_reindex.copy()),
|
json.dumps(self.embeddings_reindex.copy()),
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_update_birdseye_layout() -> None:
|
def handle_update_birdseye_layout():
|
||||||
if payload:
|
if payload:
|
||||||
self.birdseye_layout = payload
|
self.birdseye_layout = payload
|
||||||
self.publish("birdseye_layout", json.dumps(self.birdseye_layout))
|
self.publish("birdseye_layout", json.dumps(self.birdseye_layout))
|
||||||
|
|
||||||
def handle_birdseye_layout() -> None:
|
def handle_birdseye_layout():
|
||||||
self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy()))
|
self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy()))
|
||||||
|
|
||||||
def handle_on_connect() -> None:
|
def handle_on_connect():
|
||||||
camera_status = self.camera_activity.last_camera_activity.copy()
|
camera_status = self.camera_activity.last_camera_activity.copy()
|
||||||
cameras_with_status = camera_status.keys()
|
cameras_with_status = camera_status.keys()
|
||||||
|
|
||||||
@ -220,7 +219,7 @@ class Dispatcher:
|
|||||||
)
|
)
|
||||||
self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy()))
|
self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy()))
|
||||||
|
|
||||||
def handle_notification_test() -> None:
|
def handle_notification_test():
|
||||||
self.publish("notification_test", "Test notification")
|
self.publish("notification_test", "Test notification")
|
||||||
|
|
||||||
# Dictionary mapping topic to handlers
|
# Dictionary mapping topic to handlers
|
||||||
@ -267,12 +266,11 @@ class Dispatcher:
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"Received invalid {topic.split('/')[-1]} command: {topic}"
|
f"Received invalid {topic.split('/')[-1]} command: {topic}"
|
||||||
)
|
)
|
||||||
return None
|
return
|
||||||
elif topic in topic_handlers:
|
elif topic in topic_handlers:
|
||||||
return topic_handlers[topic]()
|
return topic_handlers[topic]()
|
||||||
else:
|
else:
|
||||||
self.publish(topic, payload, retain=False)
|
self.publish(topic, payload, retain=False)
|
||||||
return None
|
|
||||||
|
|
||||||
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
||||||
"""Handle publishing to communicators."""
|
"""Handle publishing to communicators."""
|
||||||
@ -375,11 +373,11 @@ class Dispatcher:
|
|||||||
if payload == "ON":
|
if payload == "ON":
|
||||||
if not motion_settings.improve_contrast:
|
if not motion_settings.improve_contrast:
|
||||||
logger.info(f"Turning on improve contrast for {camera_name}")
|
logger.info(f"Turning on improve contrast for {camera_name}")
|
||||||
motion_settings.improve_contrast = True
|
motion_settings.improve_contrast = True # type: ignore[union-attr]
|
||||||
elif payload == "OFF":
|
elif payload == "OFF":
|
||||||
if motion_settings.improve_contrast:
|
if motion_settings.improve_contrast:
|
||||||
logger.info(f"Turning off improve contrast for {camera_name}")
|
logger.info(f"Turning off improve contrast for {camera_name}")
|
||||||
motion_settings.improve_contrast = False
|
motion_settings.improve_contrast = False # type: ignore[union-attr]
|
||||||
|
|
||||||
self.config_updater.publish_update(
|
self.config_updater.publish_update(
|
||||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
||||||
@ -423,7 +421,7 @@ class Dispatcher:
|
|||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
logger.info(f"Setting motion contour area for {camera_name}: {payload}")
|
logger.info(f"Setting motion contour area for {camera_name}: {payload}")
|
||||||
motion_settings.contour_area = payload
|
motion_settings.contour_area = payload # type: ignore[union-attr]
|
||||||
self.config_updater.publish_update(
|
self.config_updater.publish_update(
|
||||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
||||||
motion_settings,
|
motion_settings,
|
||||||
@ -440,7 +438,7 @@ class Dispatcher:
|
|||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
logger.info(f"Setting motion threshold for {camera_name}: {payload}")
|
logger.info(f"Setting motion threshold for {camera_name}: {payload}")
|
||||||
motion_settings.threshold = payload
|
motion_settings.threshold = payload # type: ignore[union-attr]
|
||||||
self.config_updater.publish_update(
|
self.config_updater.publish_update(
|
||||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name),
|
||||||
motion_settings,
|
motion_settings,
|
||||||
@ -455,7 +453,7 @@ class Dispatcher:
|
|||||||
|
|
||||||
notification_settings = self.config.notifications
|
notification_settings = self.config.notifications
|
||||||
logger.info(f"Setting all notifications: {payload}")
|
logger.info(f"Setting all notifications: {payload}")
|
||||||
notification_settings.enabled = payload == "ON"
|
notification_settings.enabled = payload == "ON" # type: ignore[union-attr]
|
||||||
self.config_updater.publisher.publish(
|
self.config_updater.publisher.publish(
|
||||||
"config/notifications", notification_settings
|
"config/notifications", notification_settings
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
import logging
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
SOCKET_REP_REQ = "ipc:///tmp/cache/embeddings"
|
SOCKET_REP_REQ = "ipc:///tmp/cache/embeddings"
|
||||||
|
|
||||||
|
|
||||||
@ -45,16 +41,9 @@ class EmbeddingsResponder:
|
|||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = self.socket.recv_json(flags=zmq.NOBLOCK)
|
(topic, value) = self.socket.recv_json(flags=zmq.NOBLOCK)
|
||||||
|
|
||||||
if isinstance(raw, list):
|
response = process(topic, value)
|
||||||
(topic, value) = raw
|
|
||||||
response = process(topic, value)
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f"Received unexpected data type in ZMQ recv_json: {type(raw)}"
|
|
||||||
)
|
|
||||||
response = None
|
|
||||||
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
self.socket.send_json(response)
|
self.socket.send_json(response)
|
||||||
@ -76,7 +65,7 @@ class EmbeddingsRequestor:
|
|||||||
self.socket = self.context.socket(zmq.REQ)
|
self.socket = self.context.socket(zmq.REQ)
|
||||||
self.socket.connect(SOCKET_REP_REQ)
|
self.socket.connect(SOCKET_REP_REQ)
|
||||||
|
|
||||||
def send_data(self, topic: str, data: Any) -> Any:
|
def send_data(self, topic: str, data: Any) -> str:
|
||||||
"""Sends data and then waits for reply."""
|
"""Sends data and then waits for reply."""
|
||||||
try:
|
try:
|
||||||
self.socket.send_json((topic, data))
|
self.socket.send_json((topic, data))
|
||||||
|
|||||||
@ -28,8 +28,8 @@ class EventMetadataPublisher(Publisher):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def publish(self, payload: Any, sub_topic: str = "") -> None:
|
def publish(self, topic: EventMetadataTypeEnum, payload: Any) -> None:
|
||||||
super().publish(payload, sub_topic)
|
super().publish(payload, topic.value)
|
||||||
|
|
||||||
|
|
||||||
class EventMetadataSubscriber(Subscriber):
|
class EventMetadataSubscriber(Subscriber):
|
||||||
@ -40,10 +40,9 @@ class EventMetadataSubscriber(Subscriber):
|
|||||||
def __init__(self, topic: EventMetadataTypeEnum) -> None:
|
def __init__(self, topic: EventMetadataTypeEnum) -> None:
|
||||||
super().__init__(topic.value)
|
super().__init__(topic.value)
|
||||||
|
|
||||||
def _return_object(
|
def _return_object(self, topic: str, payload: tuple) -> tuple:
|
||||||
self, topic: str, payload: tuple | None
|
|
||||||
) -> tuple[str, Any] | tuple[None, None]:
|
|
||||||
if payload is None:
|
if payload is None:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
|
topic = EventMetadataTypeEnum[topic[len(self.topic_base) :]]
|
||||||
return (topic, payload)
|
return (topic, payload)
|
||||||
|
|||||||
@ -7,9 +7,7 @@ from frigate.events.types import EventStateEnum, EventTypeEnum
|
|||||||
from .zmq_proxy import Publisher, Subscriber
|
from .zmq_proxy import Publisher, Subscriber
|
||||||
|
|
||||||
|
|
||||||
class EventUpdatePublisher(
|
class EventUpdatePublisher(Publisher):
|
||||||
Publisher[tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]]]
|
|
||||||
):
|
|
||||||
"""Publishes events (objects, audio, manual)."""
|
"""Publishes events (objects, audio, manual)."""
|
||||||
|
|
||||||
topic_base = "event/"
|
topic_base = "event/"
|
||||||
@ -18,11 +16,9 @@ class EventUpdatePublisher(
|
|||||||
super().__init__("update")
|
super().__init__("update")
|
||||||
|
|
||||||
def publish(
|
def publish(
|
||||||
self,
|
self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]]
|
||||||
payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]],
|
|
||||||
sub_topic: str = "",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().publish(payload, sub_topic)
|
super().publish(payload)
|
||||||
|
|
||||||
|
|
||||||
class EventUpdateSubscriber(Subscriber):
|
class EventUpdateSubscriber(Subscriber):
|
||||||
@ -34,9 +30,7 @@ class EventUpdateSubscriber(Subscriber):
|
|||||||
super().__init__("update")
|
super().__init__("update")
|
||||||
|
|
||||||
|
|
||||||
class EventEndPublisher(
|
class EventEndPublisher(Publisher):
|
||||||
Publisher[tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]]
|
|
||||||
):
|
|
||||||
"""Publishes events that have ended."""
|
"""Publishes events that have ended."""
|
||||||
|
|
||||||
topic_base = "event/"
|
topic_base = "event/"
|
||||||
@ -45,11 +39,9 @@ class EventEndPublisher(
|
|||||||
super().__init__("finalized")
|
super().__init__("finalized")
|
||||||
|
|
||||||
def publish(
|
def publish(
|
||||||
self,
|
self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]
|
||||||
payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]],
|
|
||||||
sub_topic: str = "",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().publish(payload, sub_topic)
|
super().publish(payload)
|
||||||
|
|
||||||
|
|
||||||
class EventEndSubscriber(Subscriber):
|
class EventEndSubscriber(Subscriber):
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
import logging
|
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import threading
|
import threading
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
@ -10,8 +9,6 @@ import zmq
|
|||||||
|
|
||||||
from frigate.comms.base_communicator import Communicator
|
from frigate.comms.base_communicator import Communicator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SOCKET_REP_REQ = "ipc:///tmp/cache/comms"
|
SOCKET_REP_REQ = "ipc:///tmp/cache/comms"
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +19,7 @@ class InterProcessCommunicator(Communicator):
|
|||||||
self.socket.bind(SOCKET_REP_REQ)
|
self.socket.bind(SOCKET_REP_REQ)
|
||||||
self.stop_event: MpEvent = mp.Event()
|
self.stop_event: MpEvent = mp.Event()
|
||||||
|
|
||||||
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
def publish(self, topic: str, payload: str, retain: bool) -> None:
|
||||||
"""There is no communication back to the processes."""
|
"""There is no communication back to the processes."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -40,16 +37,9 @@ class InterProcessCommunicator(Communicator):
|
|||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = self.socket.recv_json(flags=zmq.NOBLOCK)
|
(topic, value) = self.socket.recv_json(flags=zmq.NOBLOCK)
|
||||||
|
|
||||||
if isinstance(raw, list):
|
response = self._dispatcher(topic, value)
|
||||||
(topic, value) = raw
|
|
||||||
response = self._dispatcher(topic, value)
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f"Received unexpected data type in ZMQ recv_json: {type(raw)}"
|
|
||||||
)
|
|
||||||
response = None
|
|
||||||
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
self.socket.send_json(response)
|
self.socket.send_json(response)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from frigate.config import FrigateConfig
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MqttClient(Communicator):
|
class MqttClient(Communicator): # type: ignore[misc]
|
||||||
"""Frigate wrapper for mqtt client."""
|
"""Frigate wrapper for mqtt client."""
|
||||||
|
|
||||||
def __init__(self, config: FrigateConfig) -> None:
|
def __init__(self, config: FrigateConfig) -> None:
|
||||||
@ -75,7 +75,7 @@ class MqttClient(Communicator):
|
|||||||
)
|
)
|
||||||
self.publish(
|
self.publish(
|
||||||
f"{camera_name}/improve_contrast/state",
|
f"{camera_name}/improve_contrast/state",
|
||||||
"ON" if camera.motion.improve_contrast else "OFF",
|
"ON" if camera.motion.improve_contrast else "OFF", # type: ignore[union-attr]
|
||||||
retain=True,
|
retain=True,
|
||||||
)
|
)
|
||||||
self.publish(
|
self.publish(
|
||||||
@ -85,12 +85,12 @@ class MqttClient(Communicator):
|
|||||||
)
|
)
|
||||||
self.publish(
|
self.publish(
|
||||||
f"{camera_name}/motion_threshold/state",
|
f"{camera_name}/motion_threshold/state",
|
||||||
camera.motion.threshold,
|
camera.motion.threshold, # type: ignore[union-attr]
|
||||||
retain=True,
|
retain=True,
|
||||||
)
|
)
|
||||||
self.publish(
|
self.publish(
|
||||||
f"{camera_name}/motion_contour_area/state",
|
f"{camera_name}/motion_contour_area/state",
|
||||||
camera.motion.contour_area,
|
camera.motion.contour_area, # type: ignore[union-attr]
|
||||||
retain=True,
|
retain=True,
|
||||||
)
|
)
|
||||||
self.publish(
|
self.publish(
|
||||||
@ -150,7 +150,7 @@ class MqttClient(Communicator):
|
|||||||
client: mqtt.Client,
|
client: mqtt.Client,
|
||||||
userdata: Any,
|
userdata: Any,
|
||||||
flags: Any,
|
flags: Any,
|
||||||
reason_code: mqtt.ReasonCode, # type: ignore[name-defined]
|
reason_code: mqtt.ReasonCode,
|
||||||
properties: Any,
|
properties: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Mqtt connection callback."""
|
"""Mqtt connection callback."""
|
||||||
@ -182,7 +182,7 @@ class MqttClient(Communicator):
|
|||||||
client: mqtt.Client,
|
client: mqtt.Client,
|
||||||
userdata: Any,
|
userdata: Any,
|
||||||
flags: Any,
|
flags: Any,
|
||||||
reason_code: mqtt.ReasonCode, # type: ignore[name-defined]
|
reason_code: mqtt.ReasonCode,
|
||||||
properties: Any,
|
properties: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Mqtt disconnection callback."""
|
"""Mqtt disconnection callback."""
|
||||||
|
|||||||
@ -13,16 +13,17 @@ class RecordingsDataTypeEnum(str, Enum):
|
|||||||
recordings_available_through = "recordings_available_through"
|
recordings_available_through = "recordings_available_through"
|
||||||
|
|
||||||
|
|
||||||
class RecordingsDataPublisher(Publisher[tuple[str, float]]):
|
class RecordingsDataPublisher(Publisher):
|
||||||
"""Publishes latest recording data."""
|
"""Publishes latest recording data."""
|
||||||
|
|
||||||
topic_base = "recordings/"
|
topic_base = "recordings/"
|
||||||
|
|
||||||
def __init__(self, topic: RecordingsDataTypeEnum) -> None:
|
def __init__(self, topic: RecordingsDataTypeEnum) -> None:
|
||||||
super().__init__(topic.value)
|
topic = topic.value
|
||||||
|
super().__init__(topic)
|
||||||
|
|
||||||
def publish(self, payload: tuple[str, float], sub_topic: str = "") -> None:
|
def publish(self, payload: tuple[str, float]) -> None:
|
||||||
super().publish(payload, sub_topic)
|
super().publish(payload)
|
||||||
|
|
||||||
|
|
||||||
class RecordingsDataSubscriber(Subscriber):
|
class RecordingsDataSubscriber(Subscriber):
|
||||||
@ -31,4 +32,5 @@ class RecordingsDataSubscriber(Subscriber):
|
|||||||
topic_base = "recordings/"
|
topic_base = "recordings/"
|
||||||
|
|
||||||
def __init__(self, topic: RecordingsDataTypeEnum) -> None:
|
def __init__(self, topic: RecordingsDataTypeEnum) -> None:
|
||||||
super().__init__(topic.value)
|
topic = topic.value
|
||||||
|
super().__init__(topic)
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class PushNotification:
|
|||||||
ttl: int = 0
|
ttl: int = 0
|
||||||
|
|
||||||
|
|
||||||
class WebPushClient(Communicator):
|
class WebPushClient(Communicator): # type: ignore[misc]
|
||||||
"""Frigate wrapper for webpush client."""
|
"""Frigate wrapper for webpush client."""
|
||||||
|
|
||||||
def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
|
def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
|
||||||
@ -50,12 +50,10 @@ class WebPushClient(Communicator):
|
|||||||
self.web_pushers: dict[str, list[WebPusher]] = {}
|
self.web_pushers: dict[str, list[WebPusher]] = {}
|
||||||
self.expired_subs: dict[str, list[str]] = {}
|
self.expired_subs: dict[str, list[str]] = {}
|
||||||
self.suspended_cameras: dict[str, int] = {
|
self.suspended_cameras: dict[str, int] = {
|
||||||
c.name: 0 # type: ignore[misc]
|
c.name: 0 for c in self.config.cameras.values()
|
||||||
for c in self.config.cameras.values()
|
|
||||||
}
|
}
|
||||||
self.last_camera_notification_time: dict[str, float] = {
|
self.last_camera_notification_time: dict[str, float] = {
|
||||||
c.name: 0 # type: ignore[misc]
|
c.name: 0 for c in self.config.cameras.values()
|
||||||
for c in self.config.cameras.values()
|
|
||||||
}
|
}
|
||||||
self.last_notification_time: float = 0
|
self.last_notification_time: float = 0
|
||||||
self.notification_queue: queue.Queue[PushNotification] = queue.Queue()
|
self.notification_queue: queue.Queue[PushNotification] = queue.Queue()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import errno
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Callable
|
from typing import Callable
|
||||||
from wsgiref.simple_server import make_server
|
from wsgiref.simple_server import make_server
|
||||||
|
|
||||||
from ws4py.server.wsgirefserver import (
|
from ws4py.server.wsgirefserver import (
|
||||||
@ -21,8 +21,8 @@ from frigate.config import FrigateConfig
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(WebSocket_): # type: ignore[misc]
|
class WebSocket(WebSocket_):
|
||||||
def unhandled_error(self, error: Any) -> None:
|
def unhandled_error(self, error):
|
||||||
"""
|
"""
|
||||||
Handles the unfriendly socket closures on the server side
|
Handles the unfriendly socket closures on the server side
|
||||||
without showing a confusing error message
|
without showing a confusing error message
|
||||||
@ -33,12 +33,12 @@ class WebSocket(WebSocket_): # type: ignore[misc]
|
|||||||
logging.getLogger("ws4py").exception("Failed to receive data")
|
logging.getLogger("ws4py").exception("Failed to receive data")
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClient(Communicator):
|
class WebSocketClient(Communicator): # type: ignore[misc]
|
||||||
"""Frigate wrapper for ws client."""
|
"""Frigate wrapper for ws client."""
|
||||||
|
|
||||||
def __init__(self, config: FrigateConfig) -> None:
|
def __init__(self, config: FrigateConfig) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.websocket_server: WSGIServer | None = None
|
self.websocket_server = None
|
||||||
|
|
||||||
def subscribe(self, receiver: Callable) -> None:
|
def subscribe(self, receiver: Callable) -> None:
|
||||||
self._dispatcher = receiver
|
self._dispatcher = receiver
|
||||||
@ -47,10 +47,10 @@ class WebSocketClient(Communicator):
|
|||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start the websocket client."""
|
"""Start the websocket client."""
|
||||||
|
|
||||||
class _WebSocketHandler(WebSocket):
|
class _WebSocketHandler(WebSocket): # type: ignore[misc]
|
||||||
receiver = self._dispatcher
|
receiver = self._dispatcher
|
||||||
|
|
||||||
def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined]
|
def received_message(self, message: WebSocket.received_message) -> None:
|
||||||
try:
|
try:
|
||||||
json_message = json.loads(message.data.decode("utf-8"))
|
json_message = json.loads(message.data.decode("utf-8"))
|
||||||
json_message = {
|
json_message = {
|
||||||
@ -86,7 +86,7 @@ class WebSocketClient(Communicator):
|
|||||||
)
|
)
|
||||||
self.websocket_thread.start()
|
self.websocket_thread.start()
|
||||||
|
|
||||||
def publish(self, topic: str, payload: Any, _: bool = False) -> None:
|
def publish(self, topic: str, payload: str, _: bool) -> None:
|
||||||
try:
|
try:
|
||||||
ws_message = json.dumps(
|
ws_message = json.dumps(
|
||||||
{
|
{
|
||||||
@ -109,11 +109,9 @@ class WebSocketClient(Communicator):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
if self.websocket_server is not None:
|
self.websocket_server.manager.close_all()
|
||||||
self.websocket_server.manager.close_all()
|
self.websocket_server.manager.stop()
|
||||||
self.websocket_server.manager.stop()
|
self.websocket_server.manager.join()
|
||||||
self.websocket_server.manager.join()
|
self.websocket_server.shutdown()
|
||||||
self.websocket_server.shutdown()
|
|
||||||
|
|
||||||
self.websocket_thread.join()
|
self.websocket_thread.join()
|
||||||
logger.info("Exiting websocket client...")
|
logger.info("Exiting websocket client...")
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Generic, Optional, TypeVar
|
from typing import Any, Optional
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
@ -47,10 +47,7 @@ class ZmqProxy:
|
|||||||
self.runner.join()
|
self.runner.join()
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
class Publisher:
|
||||||
|
|
||||||
|
|
||||||
class Publisher(Generic[T]):
|
|
||||||
"""Publishes messages."""
|
"""Publishes messages."""
|
||||||
|
|
||||||
topic_base: str = ""
|
topic_base: str = ""
|
||||||
@ -61,7 +58,7 @@ class Publisher(Generic[T]):
|
|||||||
self.socket = self.context.socket(zmq.PUB)
|
self.socket = self.context.socket(zmq.PUB)
|
||||||
self.socket.connect(SOCKET_PUB)
|
self.socket.connect(SOCKET_PUB)
|
||||||
|
|
||||||
def publish(self, payload: T, sub_topic: str = "") -> None:
|
def publish(self, payload: Any, sub_topic: str = "") -> None:
|
||||||
"""Publish message."""
|
"""Publish message."""
|
||||||
self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}")
|
self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}")
|
||||||
|
|
||||||
@ -83,8 +80,8 @@ class Subscriber:
|
|||||||
self.socket.connect(SOCKET_SUB)
|
self.socket.connect(SOCKET_SUB)
|
||||||
|
|
||||||
def check_for_update(
|
def check_for_update(
|
||||||
self, timeout: float | None = FAST_QUEUE_TIMEOUT
|
self, timeout: float = FAST_QUEUE_TIMEOUT
|
||||||
) -> tuple[str, Any] | tuple[None, None] | None:
|
) -> Optional[tuple[str, Any]]:
|
||||||
"""Returns message or None if no update."""
|
"""Returns message or None if no update."""
|
||||||
try:
|
try:
|
||||||
has_update, _, _ = zmq.select([self.socket], [], [], timeout)
|
has_update, _, _ = zmq.select([self.socket], [], [], timeout)
|
||||||
@ -101,7 +98,5 @@ class Subscriber:
|
|||||||
self.socket.close()
|
self.socket.close()
|
||||||
self.context.destroy()
|
self.context.destroy()
|
||||||
|
|
||||||
def _return_object(
|
def _return_object(self, topic: str, payload: Any) -> Any:
|
||||||
self, topic: str, payload: Optional[tuple[str, Any]]
|
|
||||||
) -> tuple[str, Any] | tuple[None, None] | None:
|
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@ -80,7 +80,9 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
lpr: CameraLicensePlateRecognitionConfig = Field(
|
lpr: CameraLicensePlateRecognitionConfig = Field(
|
||||||
default_factory=CameraLicensePlateRecognitionConfig, title="LPR config."
|
default_factory=CameraLicensePlateRecognitionConfig, title="LPR config."
|
||||||
)
|
)
|
||||||
motion: MotionConfig = Field(None, title="Motion detection configuration.")
|
motion: Optional[MotionConfig] = Field(
|
||||||
|
None, title="Motion detection configuration."
|
||||||
|
)
|
||||||
objects: ObjectConfig = Field(
|
objects: ObjectConfig = Field(
|
||||||
default_factory=ObjectConfig, title="Object configuration."
|
default_factory=ObjectConfig, title="Object configuration."
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ __all__ = ["NotificationConfig"]
|
|||||||
class NotificationConfig(FrigateBaseModel):
|
class NotificationConfig(FrigateBaseModel):
|
||||||
enabled: bool = Field(default=False, title="Enable notifications")
|
enabled: bool = Field(default=False, title="Enable notifications")
|
||||||
email: Optional[str] = Field(default=None, title="Email required for push.")
|
email: Optional[str] = Field(default=None, title="Email required for push.")
|
||||||
cooldown: int = Field(
|
cooldown: Optional[int] = Field(
|
||||||
default=0, ge=0, title="Cooldown period for notifications (time in seconds)."
|
default=0, ge=0, title="Cooldown period for notifications (time in seconds)."
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: Optional[bool] = Field(
|
||||||
|
|||||||
@ -142,7 +142,7 @@ class TriggerConfig(FrigateBaseModel):
|
|||||||
gt=0.0,
|
gt=0.0,
|
||||||
le=1.0,
|
le=1.0,
|
||||||
)
|
)
|
||||||
actions: List[TriggerAction] = Field(
|
actions: Optional[List[TriggerAction]] = Field(
|
||||||
default=[], title="Actions to perform when trigger is matched"
|
default=[], title="Actions to perform when trigger is matched"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,8 +150,8 @@ class TriggerConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CameraSemanticSearchConfig(FrigateBaseModel):
|
class CameraSemanticSearchConfig(FrigateBaseModel):
|
||||||
triggers: Dict[str, TriggerConfig] = Field(
|
triggers: Optional[Dict[str, TriggerConfig]] = Field(
|
||||||
default={},
|
default=None,
|
||||||
title="Trigger actions on tracked objects that match existing thumbnails or descriptions",
|
title="Trigger actions on tracked objects that match existing thumbnails or descriptions",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class MqttConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
tls_client_key: Optional[str] = Field(default=None, title="MQTT TLS Client Key")
|
tls_client_key: Optional[str] = Field(default=None, title="MQTT TLS Client Key")
|
||||||
tls_insecure: Optional[bool] = Field(default=None, title="MQTT TLS Insecure")
|
tls_insecure: Optional[bool] = Field(default=None, title="MQTT TLS Insecure")
|
||||||
qos: int = Field(default=0, title="MQTT QoS")
|
qos: Optional[int] = Field(default=0, title="MQTT QoS")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def user_requires_pass(self, info: ValidationInfo) -> Self:
|
def user_requires_pass(self, info: ValidationInfo) -> Self:
|
||||||
|
|||||||
@ -1172,6 +1172,7 @@ class LicensePlateProcessingMixin:
|
|||||||
event_id = f"{now}-{rand_id}"
|
event_id = f"{now}-{rand_id}"
|
||||||
|
|
||||||
self.event_metadata_publisher.publish(
|
self.event_metadata_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.lpr_event_create,
|
||||||
(
|
(
|
||||||
now,
|
now,
|
||||||
camera,
|
camera,
|
||||||
@ -1182,7 +1183,6 @@ class LicensePlateProcessingMixin:
|
|||||||
None,
|
None,
|
||||||
plate,
|
plate,
|
||||||
),
|
),
|
||||||
EventMetadataTypeEnum.lpr_event_create.value,
|
|
||||||
)
|
)
|
||||||
return event_id
|
return event_id
|
||||||
|
|
||||||
@ -1526,7 +1526,7 @@ class LicensePlateProcessingMixin:
|
|||||||
# If it's a known plate, publish to sub_label
|
# If it's a known plate, publish to sub_label
|
||||||
if sub_label is not None:
|
if sub_label is not None:
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
(id, sub_label, avg_confidence), EventMetadataTypeEnum.sub_label.value
|
EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence)
|
||||||
)
|
)
|
||||||
|
|
||||||
# always publish to recognized_license_plate field
|
# always publish to recognized_license_plate field
|
||||||
@ -1545,8 +1545,8 @@ class LicensePlateProcessingMixin:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.attribute,
|
||||||
(id, "recognized_license_plate", top_plate, avg_confidence),
|
(id, "recognized_license_plate", top_plate, avg_confidence),
|
||||||
EventMetadataTypeEnum.attribute.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# save the best snapshot for dedicated lpr cams not using frigate+
|
# save the best snapshot for dedicated lpr cams not using frigate+
|
||||||
@ -1560,8 +1560,8 @@ class LicensePlateProcessingMixin:
|
|||||||
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
||||||
_, encoded_img = cv2.imencode(".jpg", frame_bgr)
|
_, encoded_img = cv2.imencode(".jpg", frame_bgr)
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.save_lpr_snapshot,
|
||||||
(base64.b64encode(encoded_img).decode("ASCII"), id, camera),
|
(base64.b64encode(encoded_img).decode("ASCII"), id, camera),
|
||||||
EventMetadataTypeEnum.save_lpr_snapshot.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if id not in self.detected_license_plates:
|
if id not in self.detected_license_plates:
|
||||||
|
|||||||
@ -156,8 +156,8 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.sub_label,
|
||||||
(obj_data["id"], self.labelmap[best_id], score),
|
(obj_data["id"], self.labelmap[best_id], score),
|
||||||
EventMetadataTypeEnum.sub_label.value,
|
|
||||||
)
|
)
|
||||||
self.detected_birds[obj_data["id"]] = score
|
self.detected_birds[obj_data["id"]] = score
|
||||||
|
|
||||||
|
|||||||
@ -294,16 +294,16 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
):
|
):
|
||||||
if sub_label != "none":
|
if sub_label != "none":
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
(obj_data["id"], sub_label, score),
|
|
||||||
EventMetadataTypeEnum.sub_label,
|
EventMetadataTypeEnum.sub_label,
|
||||||
|
(obj_data["id"], sub_label, score),
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
self.model_config.object_config.classification_type
|
self.model_config.object_config.classification_type
|
||||||
== ObjectClassificationType.attribute
|
== ObjectClassificationType.attribute
|
||||||
):
|
):
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.attribute,
|
||||||
(obj_data["id"], self.model_config.name, sub_label, score),
|
(obj_data["id"], self.model_config.name, sub_label, score),
|
||||||
EventMetadataTypeEnum.attribute.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_request(self, topic, request_data):
|
def handle_request(self, topic, request_data):
|
||||||
|
|||||||
@ -319,8 +319,8 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
|
|
||||||
if weighted_score >= self.face_config.recognition_threshold:
|
if weighted_score >= self.face_config.recognition_threshold:
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.sub_label,
|
||||||
(id, weighted_sub_label, weighted_score),
|
(id, weighted_sub_label, weighted_score),
|
||||||
EventMetadataTypeEnum.sub_label.value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__update_metrics(datetime.datetime.now().timestamp() - start)
|
self.__update_metrics(datetime.datetime.now().timestamp() - start)
|
||||||
|
|||||||
@ -146,7 +146,7 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
RecordingsDataTypeEnum.recordings_available_through
|
RecordingsDataTypeEnum.recordings_available_through
|
||||||
)
|
)
|
||||||
self.review_subscriber = ReviewDataSubscriber("")
|
self.review_subscriber = ReviewDataSubscriber("")
|
||||||
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value)
|
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video)
|
||||||
self.embeddings_responder = EmbeddingsResponder()
|
self.embeddings_responder = EmbeddingsResponder()
|
||||||
self.frame_manager = SharedMemoryFrameManager()
|
self.frame_manager = SharedMemoryFrameManager()
|
||||||
|
|
||||||
@ -504,8 +504,8 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
to_remove.append(id)
|
to_remove.append(id)
|
||||||
for id in to_remove:
|
for id in to_remove:
|
||||||
self.event_metadata_publisher.publish(
|
self.event_metadata_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.manual_event_end,
|
||||||
(id, now),
|
(id, now),
|
||||||
EventMetadataTypeEnum.manual_event_end.value,
|
|
||||||
)
|
)
|
||||||
self.detected_license_plates.pop(id)
|
self.detected_license_plates.pop(id)
|
||||||
|
|
||||||
|
|||||||
@ -183,7 +183,7 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
CameraConfigUpdateEnum.audio_transcription,
|
CameraConfigUpdateEnum.audio_transcription,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value)
|
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio)
|
||||||
self.event_metadata_publisher = EventMetadataPublisher()
|
self.event_metadata_publisher = EventMetadataPublisher()
|
||||||
|
|
||||||
if self.camera_config.audio_transcription.enabled_in_config:
|
if self.camera_config.audio_transcription.enabled_in_config:
|
||||||
@ -293,6 +293,7 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
self.requestor.send_data(f"{self.camera_config.name}/audio/{label}", "ON")
|
self.requestor.send_data(f"{self.camera_config.name}/audio/{label}", "ON")
|
||||||
|
|
||||||
self.event_metadata_publisher.publish(
|
self.event_metadata_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.manual_event_create,
|
||||||
(
|
(
|
||||||
now,
|
now,
|
||||||
self.camera_config.name,
|
self.camera_config.name,
|
||||||
@ -305,7 +306,6 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
"audio",
|
"audio",
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
EventMetadataTypeEnum.manual_event_create.value,
|
|
||||||
)
|
)
|
||||||
self.detections[label] = {
|
self.detections[label] = {
|
||||||
"id": event_id,
|
"id": event_id,
|
||||||
@ -329,8 +329,8 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.event_metadata_publisher.publish(
|
self.event_metadata_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.manual_event_end,
|
||||||
(detection["id"], detection["last_detection"]),
|
(detection["id"], detection["last_detection"]),
|
||||||
EventMetadataTypeEnum.manual_event_end.value,
|
|
||||||
)
|
)
|
||||||
self.detections[detection["label"]] = None
|
self.detections[detection["label"]] = None
|
||||||
|
|
||||||
@ -343,8 +343,8 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
f"{self.camera_config.name}/audio/{label}", "OFF"
|
f"{self.camera_config.name}/audio/{label}", "OFF"
|
||||||
)
|
)
|
||||||
self.event_metadata_publisher.publish(
|
self.event_metadata_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.manual_event_end,
|
||||||
(detection["id"], now),
|
(detection["id"], now),
|
||||||
EventMetadataTypeEnum.manual_event_end.value,
|
|
||||||
)
|
)
|
||||||
self.detections[label] = None
|
self.detections[label] = None
|
||||||
|
|
||||||
|
|||||||
@ -35,9 +35,6 @@ disallow_untyped_calls = false
|
|||||||
[mypy-frigate.const]
|
[mypy-frigate.const]
|
||||||
ignore_errors = false
|
ignore_errors = false
|
||||||
|
|
||||||
[mypy-frigate.comms.*]
|
|
||||||
ignore_errors = false
|
|
||||||
|
|
||||||
[mypy-frigate.events]
|
[mypy-frigate.events]
|
||||||
ignore_errors = false
|
ignore_errors = false
|
||||||
|
|
||||||
|
|||||||
@ -96,7 +96,7 @@ class OutputProcess(FrigateProcess):
|
|||||||
websocket_server.initialize_websockets_manager()
|
websocket_server.initialize_websockets_manager()
|
||||||
websocket_thread = threading.Thread(target=websocket_server.serve_forever)
|
websocket_thread = threading.Thread(target=websocket_server.serve_forever)
|
||||||
|
|
||||||
detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value)
|
detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video)
|
||||||
config_subscriber = CameraConfigUpdateSubscriber(
|
config_subscriber = CameraConfigUpdateSubscriber(
|
||||||
self.config,
|
self.config,
|
||||||
self.config.cameras,
|
self.config.cameras,
|
||||||
|
|||||||
@ -79,7 +79,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
self.config.cameras,
|
self.config.cameras,
|
||||||
[CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.record],
|
[CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.record],
|
||||||
)
|
)
|
||||||
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value)
|
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
|
||||||
self.recordings_publisher = RecordingsDataPublisher(
|
self.recordings_publisher = RecordingsDataPublisher(
|
||||||
RecordingsDataTypeEnum.recordings_available_through
|
RecordingsDataTypeEnum.recordings_available_through
|
||||||
)
|
)
|
||||||
@ -545,7 +545,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
if not topic:
|
if not topic:
|
||||||
break
|
break
|
||||||
|
|
||||||
if topic == DetectionTypeEnum.video.value:
|
if topic == DetectionTypeEnum.video:
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
_,
|
_,
|
||||||
@ -564,7 +564,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
regions,
|
regions,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif topic == DetectionTypeEnum.audio.value:
|
elif topic == DetectionTypeEnum.audio:
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
frame_time,
|
frame_time,
|
||||||
@ -580,9 +580,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
audio_detections,
|
audio_detections,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif (
|
elif topic == DetectionTypeEnum.api or DetectionTypeEnum.lpr:
|
||||||
topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if frame_time < run_start - stale_frame_count_threshold:
|
if frame_time < run_start - stale_frame_count_threshold:
|
||||||
|
|||||||
@ -169,7 +169,7 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
CameraConfigUpdateEnum.review,
|
CameraConfigUpdateEnum.review,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value)
|
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
|
||||||
self.review_publisher = ReviewDataPublisher("")
|
self.review_publisher = ReviewDataPublisher("")
|
||||||
|
|
||||||
# manual events
|
# manual events
|
||||||
@ -490,7 +490,7 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
if not topic:
|
if not topic:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if topic == DetectionTypeEnum.video.value:
|
if topic == DetectionTypeEnum.video:
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
frame_name,
|
frame_name,
|
||||||
@ -499,14 +499,14 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) = data
|
) = data
|
||||||
elif topic == DetectionTypeEnum.audio.value:
|
elif topic == DetectionTypeEnum.audio:
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
frame_time,
|
frame_time,
|
||||||
_,
|
_,
|
||||||
audio_detections,
|
audio_detections,
|
||||||
) = data
|
) = data
|
||||||
elif topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value:
|
elif topic == DetectionTypeEnum.api or DetectionTypeEnum.lpr:
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
frame_time,
|
frame_time,
|
||||||
|
|||||||
@ -214,7 +214,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
sub_label = "sub"
|
sub_label = "sub"
|
||||||
|
|
||||||
def update_event(payload: Any, topic: str):
|
def update_event(topic, payload):
|
||||||
event = Event.get(id=id)
|
event = Event.get(id=id)
|
||||||
event.sub_label = payload[1]
|
event.sub_label = payload[1]
|
||||||
event.save()
|
event.save()
|
||||||
@ -250,7 +250,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
sub_label = "sub"
|
sub_label = "sub"
|
||||||
|
|
||||||
def update_event(payload: Any, _: str):
|
def update_event(topic, payload):
|
||||||
event = Event.get(id=id)
|
event = Event.get(id=id)
|
||||||
event.sub_label = payload[1]
|
event.sub_label = payload[1]
|
||||||
event.save()
|
event.save()
|
||||||
|
|||||||
@ -82,7 +82,7 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.requestor = InterProcessRequestor()
|
self.requestor = InterProcessRequestor()
|
||||||
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value)
|
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all)
|
||||||
self.event_sender = EventUpdatePublisher()
|
self.event_sender = EventUpdatePublisher()
|
||||||
self.event_end_subscriber = EventEndSubscriber()
|
self.event_end_subscriber = EventEndSubscriber()
|
||||||
self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all)
|
self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all)
|
||||||
|
|||||||
@ -32,14 +32,6 @@ from frigate.util.velocity import calculate_real_world_speed
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# In most cases objects that loiter in a loitering zone should alert,
|
|
||||||
# but can still be expected to stay stationary for extended periods of time
|
|
||||||
# (ex: car loitering on the street vs when a known person parks on the street)
|
|
||||||
# person is the main object that should keep alerts going as long as they loiter
|
|
||||||
# even if they are stationary.
|
|
||||||
EXTENDED_LOITERING_OBJECTS = ["person"]
|
|
||||||
|
|
||||||
|
|
||||||
class TrackedObject:
|
class TrackedObject:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -255,12 +247,8 @@ class TrackedObject:
|
|||||||
if zone.distances and not in_speed_zone:
|
if zone.distances and not in_speed_zone:
|
||||||
continue # Skip zone entry for speed zones until speed threshold met
|
continue # Skip zone entry for speed zones until speed threshold met
|
||||||
|
|
||||||
# if the zone has loitering time, and the object is an extended loiter object
|
# if the zone has loitering time, update loitering status
|
||||||
# always mark it as loitering actively
|
if zone.loitering_time > 0:
|
||||||
if (
|
|
||||||
self.obj_data["label"] in EXTENDED_LOITERING_OBJECTS
|
|
||||||
and zone.loitering_time > 0
|
|
||||||
):
|
|
||||||
in_loitering_zone = True
|
in_loitering_zone = True
|
||||||
|
|
||||||
loitering_score = self.zone_loitering.get(name, 0) + 1
|
loitering_score = self.zone_loitering.get(name, 0) + 1
|
||||||
@ -276,10 +264,6 @@ class TrackedObject:
|
|||||||
self.entered_zones.append(name)
|
self.entered_zones.append(name)
|
||||||
else:
|
else:
|
||||||
self.zone_loitering[name] = loitering_score
|
self.zone_loitering[name] = loitering_score
|
||||||
|
|
||||||
# this object is pending loitering but has not entered the zone yet
|
|
||||||
if zone.loitering_time > 0:
|
|
||||||
in_loitering_zone = True
|
|
||||||
else:
|
else:
|
||||||
self.zone_presence[name] = zone_score
|
self.zone_presence[name] = zone_score
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -91,10 +91,7 @@ export function AnimatedEventCard({
|
|||||||
|
|
||||||
// image behavior
|
// image behavior
|
||||||
|
|
||||||
const [alertVideos, _, alertVideosLoaded] = usePersistence(
|
const [alertVideos] = usePersistence("alertVideos", true);
|
||||||
"alertVideos",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const aspectRatio = useMemo(() => {
|
const aspectRatio = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
@ -138,7 +135,7 @@ export function AnimatedEventCard({
|
|||||||
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
|
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{previews != undefined && alertVideosLoaded && (
|
{previews != undefined && (
|
||||||
<div
|
<div
|
||||||
className="size-full cursor-pointer"
|
className="size-full cursor-pointer"
|
||||||
onClick={onOpenReview}
|
onClick={onOpenReview}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user