Merge branch 'dev' into histogram-reid

This commit is contained in:
Josh Hawkins 2025-02-11 09:12:18 -06:00
commit cd71e89d94
16 changed files with 302 additions and 62 deletions

View File

@ -340,6 +340,8 @@ objects:
review: review:
# Optional: alerts configuration # Optional: alerts configuration
alerts: alerts:
# Optional: enables alerts for the camera (default: shown below)
enabled: True
# Optional: labels that qualify as an alert (default: shown below) # Optional: labels that qualify as an alert (default: shown below)
labels: labels:
- car - car
@ -352,6 +354,8 @@ review:
- driveway - driveway
# Optional: detections configuration # Optional: detections configuration
detections: detections:
# Optional: enables detections for the camera (default: shown below)
enabled: True
# Optional: labels that qualify as a detection (default: all labels that are tracked / listened to) # Optional: labels that qualify as a detection (default: all labels that are tracked / listened to)
labels: labels:
- car - car

View File

@ -316,6 +316,22 @@ Topic with current state of the PTZ autotracker for a camera. Published values a
Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`. Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/review_alerts/set`
Topic to turn review alerts for a camera on or off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/review_alerts/state`
Topic with current state of review alerts for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/review_detections/set`
Topic to turn review detections for a camera on or off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/review_detections/state`
Topic with current state of review detections for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/birdseye/set` ### `frigate/<camera_name>/birdseye/set`
Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode

View File

@ -409,8 +409,6 @@ def all_recordings_summary(params: MediaRecordingsSummaryQueryParams = Depends()
if cameras != "all": if cameras != "all":
query = query.where(Recordings.camera << cameras.split(",")) query = query.where(Recordings.camera << cameras.split(","))
print(query)
recording_days = query.namedtuples() recording_days = query.namedtuples()
days = {day.day: True for day in recording_days} days = {day.day: True for day in recording_days}

View File

@ -65,6 +65,8 @@ class Dispatcher:
"snapshots": self._on_snapshots_command, "snapshots": self._on_snapshots_command,
"birdseye": self._on_birdseye_command, "birdseye": self._on_birdseye_command,
"birdseye_mode": self._on_birdseye_mode_command, "birdseye_mode": self._on_birdseye_mode_command,
"review_alerts": self._on_alerts_command,
"review_detections": self._on_detections_command,
} }
self._global_settings_handlers: dict[str, Callable] = { self._global_settings_handlers: dict[str, Callable] = {
"notifications": self._on_global_notification_command, "notifications": self._on_global_notification_command,
@ -178,6 +180,8 @@ class Dispatcher:
"autotracking": self.config.cameras[ "autotracking": self.config.cameras[
camera camera
].onvif.autotracking.enabled, ].onvif.autotracking.enabled,
"alerts": self.config.cameras[camera].review.alerts.enabled,
"detections": self.config.cameras[camera].review.detections.enabled,
} }
self.publish("camera_activity", json.dumps(camera_status)) self.publish("camera_activity", json.dumps(camera_status))
@ -565,3 +569,47 @@ class Dispatcher:
), ),
retain=True, retain=True,
) )
def _on_alerts_command(self, camera_name: str, payload: str) -> None:
"""Callback for alerts topic."""
review_settings = self.config.cameras[camera_name].review
if payload == "ON":
if not self.config.cameras[camera_name].review.alerts.enabled_in_config:
logger.error(
"Alerts must be enabled in the config to be turned on via MQTT."
)
return
if not review_settings.alerts.enabled:
logger.info(f"Turning on alerts for {camera_name}")
review_settings.alerts.enabled = True
elif payload == "OFF":
if review_settings.alerts.enabled:
logger.info(f"Turning off alerts for {camera_name}")
review_settings.alerts.enabled = False
self.config_updater.publish(f"config/review/{camera_name}", review_settings)
self.publish(f"{camera_name}/review_alerts/state", payload, retain=True)
def _on_detections_command(self, camera_name: str, payload: str) -> None:
"""Callback for detections topic."""
review_settings = self.config.cameras[camera_name].review
if payload == "ON":
if not self.config.cameras[camera_name].review.detections.enabled_in_config:
logger.error(
"Detections must be enabled in the config to be turned on via MQTT."
)
return
if not review_settings.detections.enabled:
logger.info(f"Turning on detections for {camera_name}")
review_settings.detections.enabled = True
elif payload == "OFF":
if review_settings.detections.enabled:
logger.info(f"Turning off detections for {camera_name}")
review_settings.detections.enabled = False
self.config_updater.publish(f"config/review/{camera_name}", review_settings)
self.publish(f"{camera_name}/review_detections/state", payload, retain=True)

View File

@ -107,6 +107,16 @@ class MqttClient(Communicator): # type: ignore[misc]
), ),
retain=True, retain=True,
) )
self.publish(
f"{camera_name}/review_alerts/state",
"ON" if camera.review.alerts.enabled_in_config else "OFF",
retain=True,
)
self.publish(
f"{camera_name}/review_detections/state",
"ON" if camera.review.detections.enabled_in_config else "OFF",
retain=True,
)
if self.config.notifications.enabled_in_config: if self.config.notifications.enabled_in_config:
self.publish( self.publish(

View File

@ -13,6 +13,8 @@ DEFAULT_ALERT_OBJECTS = ["person", "car"]
class AlertsConfig(FrigateBaseModel): class AlertsConfig(FrigateBaseModel):
"""Configure alerts""" """Configure alerts"""
enabled: bool = Field(default=True, title="Enable alerts.")
labels: list[str] = Field( labels: list[str] = Field(
default=DEFAULT_ALERT_OBJECTS, title="Labels to create alerts for." default=DEFAULT_ALERT_OBJECTS, title="Labels to create alerts for."
) )
@ -21,6 +23,10 @@ class AlertsConfig(FrigateBaseModel):
title="List of required zones to be entered in order to save the event as an alert.", title="List of required zones to be entered in order to save the event as an alert.",
) )
enabled_in_config: Optional[bool] = Field(
default=None, title="Keep track of original state of alerts."
)
@field_validator("required_zones", mode="before") @field_validator("required_zones", mode="before")
@classmethod @classmethod
def validate_required_zones(cls, v): def validate_required_zones(cls, v):
@ -33,6 +39,8 @@ class AlertsConfig(FrigateBaseModel):
class DetectionsConfig(FrigateBaseModel): class DetectionsConfig(FrigateBaseModel):
"""Configure detections""" """Configure detections"""
enabled: bool = Field(default=True, title="Enable detections.")
labels: Optional[list[str]] = Field( labels: Optional[list[str]] = Field(
default=None, title="Labels to create detections for." default=None, title="Labels to create detections for."
) )
@ -41,6 +49,10 @@ class DetectionsConfig(FrigateBaseModel):
title="List of required zones to be entered in order to save the event as a detection.", title="List of required zones to be entered in order to save the event as a detection.",
) )
enabled_in_config: Optional[bool] = Field(
default=None, title="Keep track of original state of detections."
)
@field_validator("required_zones", mode="before") @field_validator("required_zones", mode="before")
@classmethod @classmethod
def validate_required_zones(cls, v): def validate_required_zones(cls, v):

View File

@ -534,6 +534,12 @@ class FrigateConfig(FrigateBaseModel):
camera_config.onvif.autotracking.enabled_in_config = ( camera_config.onvif.autotracking.enabled_in_config = (
camera_config.onvif.autotracking.enabled camera_config.onvif.autotracking.enabled
) )
camera_config.review.alerts.enabled_in_config = (
camera_config.review.alerts.enabled
)
camera_config.review.detections.enabled_in_config = (
camera_config.review.detections.enabled
)
# Add default filters # Add default filters
object_keys = camera_config.objects.track object_keys = camera_config.objects.track

View File

@ -187,7 +187,7 @@ class EventProcessor(threading.Thread):
) )
# keep these from being set back to false because the event # keep these from being set back to false because the event
# may have started while recordings and snapshots were enabled # may have started while recordings/snapshots/alerts/detections were enabled
# this would be an issue for long running events # this would be an issue for long running events
if self.events_in_process[event_data["id"]]["has_clip"]: if self.events_in_process[event_data["id"]]["has_clip"]:
event_data["has_clip"] = True event_data["has_clip"] = True

View File

@ -1,5 +1,6 @@
"""Automatically pan, tilt, and zoom on detected objects via onvif.""" """Automatically pan, tilt, and zoom on detected objects via onvif."""
import asyncio
import copy import copy
import logging import logging
import queue import queue
@ -253,7 +254,7 @@ class PtzAutoTracker:
return return
if not self.onvif.cams[camera]["init"]: if not self.onvif.cams[camera]["init"]:
if not self.onvif._init_onvif(camera): if not asyncio.run(self.onvif._init_onvif(camera)):
logger.warning( logger.warning(
f"Disabling autotracking for {camera}: Unable to initialize onvif" f"Disabling autotracking for {camera}: Unable to initialize onvif"
) )

View File

@ -145,7 +145,7 @@ class OnvifController:
): ):
request = ptz.create_type("GetConfigurationOptions") request = ptz.create_type("GetConfigurationOptions")
request.ConfigurationToken = profile.PTZConfiguration.token request.ConfigurationToken = profile.PTZConfiguration.token
ptz_config = ptz.GetConfigurationOptions(request) ptz_config = await ptz.GetConfigurationOptions(request)
logger.debug(f"Onvif config for {camera_name}: {ptz_config}") logger.debug(f"Onvif config for {camera_name}: {ptz_config}")
service_capabilities_request = ptz.create_type("GetServiceCapabilities") service_capabilities_request = ptz.create_type("GetServiceCapabilities")
@ -169,7 +169,7 @@ class OnvifController:
status_request.ProfileToken = profile.token status_request.ProfileToken = profile.token
self.cams[camera_name]["status_request"] = status_request self.cams[camera_name]["status_request"] = status_request
try: try:
status = ptz.GetStatus(status_request) status = await ptz.GetStatus(status_request)
logger.debug(f"Onvif status config for {camera_name}: {status}") logger.debug(f"Onvif status config for {camera_name}: {status}")
except Exception as e: except Exception as e:
logger.warning(f"Unable to get status from camera: {camera_name}: {e}") logger.warning(f"Unable to get status from camera: {camera_name}: {e}")

View File

@ -148,7 +148,8 @@ class ReviewSegmentMaintainer(threading.Thread):
# create communication for review segments # create communication for review segments
self.requestor = InterProcessRequestor() self.requestor = InterProcessRequestor()
self.config_subscriber = ConfigSubscriber("config/record/") self.record_config_subscriber = ConfigSubscriber("config/record/")
self.review_config_subscriber = ConfigSubscriber("config/review/")
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all)
# manual events # manual events
@ -226,6 +227,13 @@ class ReviewSegmentMaintainer(threading.Thread):
) )
self.active_review_segments[segment.camera] = None self.active_review_segments[segment.camera] = None
def end_segment(self, camera: str) -> None:
"""End the pending segment for a camera."""
segment = self.active_review_segments.get(camera)
if segment:
prev_data = segment.get_data(False)
self._publish_segment_end(segment, prev_data)
def update_existing_segment( def update_existing_segment(
self, self,
segment: PendingReviewSegment, segment: PendingReviewSegment,
@ -273,6 +281,7 @@ class ReviewSegmentMaintainer(threading.Thread):
& set(camera_config.review.alerts.required_zones) & set(camera_config.review.alerts.required_zones)
) )
) )
and camera_config.review.alerts.enabled
): ):
segment.severity = SeverityEnum.alert segment.severity = SeverityEnum.alert
should_update = True should_update = True
@ -369,13 +378,14 @@ class ReviewSegmentMaintainer(threading.Thread):
& set(camera_config.review.alerts.required_zones) & set(camera_config.review.alerts.required_zones)
) )
) )
and camera_config.review.alerts.enabled
): ):
severity = SeverityEnum.alert severity = SeverityEnum.alert
# if object is detection label # if object is detection label
# and review is not already a detection or alert # and review is not already a detection or alert
# and has entered required zones or required zones is not set # and has entered required zones or required zones is not set
# mark this review as alert # mark this review as detection
if ( if (
not severity not severity
and ( and (
@ -390,6 +400,7 @@ class ReviewSegmentMaintainer(threading.Thread):
& set(camera_config.review.detections.required_zones) & set(camera_config.review.detections.required_zones)
) )
) )
and camera_config.review.detections.enabled
): ):
severity = SeverityEnum.detection severity = SeverityEnum.detection
@ -430,16 +441,26 @@ class ReviewSegmentMaintainer(threading.Thread):
# check if there is an updated config # check if there is an updated config
while True: while True:
( (
updated_topic, updated_record_topic,
updated_record_config, updated_record_config,
) = self.config_subscriber.check_for_update() ) = self.record_config_subscriber.check_for_update()
if not updated_topic: (
updated_review_topic,
updated_review_config,
) = self.review_config_subscriber.check_for_update()
if not updated_record_topic and not updated_review_topic:
break break
camera_name = updated_topic.rpartition("/")[-1] if updated_record_topic:
camera_name = updated_record_topic.rpartition("/")[-1]
self.config.cameras[camera_name].record = updated_record_config self.config.cameras[camera_name].record = updated_record_config
if updated_review_topic:
camera_name = updated_review_topic.rpartition("/")[-1]
self.config.cameras[camera_name].review = updated_review_config
(topic, data) = self.detection_subscriber.check_for_update(timeout=1) (topic, data) = self.detection_subscriber.check_for_update(timeout=1)
if not topic: if not topic:
@ -475,12 +496,22 @@ class ReviewSegmentMaintainer(threading.Thread):
if not self.config.cameras[camera].record.enabled: if not self.config.cameras[camera].record.enabled:
if current_segment: if current_segment:
self.update_existing_segment( self.end_segment(camera)
current_segment, frame_name, frame_time, []
)
continue continue
# Check if the current segment should be processed based on enabled settings
if current_segment:
if (
current_segment.severity == SeverityEnum.alert
and not self.config.cameras[camera].review.alerts.enabled
) or (
current_segment.severity == SeverityEnum.detection
and not self.config.cameras[camera].review.detections.enabled
):
self.end_segment(camera)
continue
# If we reach here, the segment can be processed (if it exists)
if current_segment is not None: if current_segment is not None:
if topic == DetectionTypeEnum.video: if topic == DetectionTypeEnum.video:
self.update_existing_segment( self.update_existing_segment(
@ -496,19 +527,23 @@ class ReviewSegmentMaintainer(threading.Thread):
current_segment.last_update = frame_time current_segment.last_update = frame_time
for audio in audio_detections: for audio in audio_detections:
if audio in camera_config.review.alerts.labels: if (
audio in camera_config.review.alerts.labels
and camera_config.review.alerts.enabled
):
current_segment.audio.add(audio) current_segment.audio.add(audio)
current_segment.severity = SeverityEnum.alert current_segment.severity = SeverityEnum.alert
elif ( elif (
camera_config.review.detections.labels is None camera_config.review.detections.labels is None
or audio in camera_config.review.detections.labels or audio in camera_config.review.detections.labels
): ) and camera_config.review.detections.enabled:
current_segment.audio.add(audio) current_segment.audio.add(audio)
elif topic == DetectionTypeEnum.api: elif topic == DetectionTypeEnum.api:
if manual_info["state"] == ManualEventState.complete: if manual_info["state"] == ManualEventState.complete:
current_segment.detections[manual_info["event_id"]] = ( current_segment.detections[manual_info["event_id"]] = (
manual_info["label"] manual_info["label"]
) )
if self.config.cameras[camera].review.alerts.enabled:
current_segment.severity = SeverityEnum.alert current_segment.severity = SeverityEnum.alert
current_segment.last_update = manual_info["end_time"] current_segment.last_update = manual_info["end_time"]
elif manual_info["state"] == ManualEventState.start: elif manual_info["state"] == ManualEventState.start:
@ -518,6 +553,7 @@ class ReviewSegmentMaintainer(threading.Thread):
current_segment.detections[manual_info["event_id"]] = ( current_segment.detections[manual_info["event_id"]] = (
manual_info["label"] manual_info["label"]
) )
if self.config.cameras[camera].review.alerts.enabled:
current_segment.severity = SeverityEnum.alert current_segment.severity = SeverityEnum.alert
# temporarily make it so this event can not end # temporarily make it so this event can not end
@ -536,6 +572,10 @@ class ReviewSegmentMaintainer(threading.Thread):
) )
else: else:
if topic == DetectionTypeEnum.video: if topic == DetectionTypeEnum.video:
if (
self.config.cameras[camera].review.alerts.enabled
or self.config.cameras[camera].review.detections.enabled
):
self.check_if_new_segment( self.check_if_new_segment(
camera, camera,
frame_name, frame_name,
@ -549,13 +589,16 @@ class ReviewSegmentMaintainer(threading.Thread):
detections = set() detections = set()
for audio in audio_detections: for audio in audio_detections:
if audio in camera_config.review.alerts.labels: if (
audio in camera_config.review.alerts.labels
and camera_config.review.alerts.enabled
):
detections.add(audio) detections.add(audio)
severity = SeverityEnum.alert severity = SeverityEnum.alert
elif ( elif (
camera_config.review.detections.labels is None camera_config.review.detections.labels is None
or audio in camera_config.review.detections.labels or audio in camera_config.review.detections.labels
): ) and camera_config.review.detections.enabled:
detections.add(audio) detections.add(audio)
if not severity: if not severity:
@ -572,6 +615,7 @@ class ReviewSegmentMaintainer(threading.Thread):
detections, detections,
) )
elif topic == DetectionTypeEnum.api: elif topic == DetectionTypeEnum.api:
if self.config.cameras[camera].review.alerts.enabled:
self.active_review_segments[camera] = PendingReviewSegment( self.active_review_segments[camera] = PendingReviewSegment(
camera, camera,
frame_time, frame_time,
@ -587,13 +631,20 @@ class ReviewSegmentMaintainer(threading.Thread):
manual_info["label"] manual_info["label"]
) )
# temporarily make it so this event can not end # temporarily make it so this event can not end
self.active_review_segments[camera].last_update = sys.maxsize self.active_review_segments[
camera
].last_update = sys.maxsize
elif manual_info["state"] == ManualEventState.complete: elif manual_info["state"] == ManualEventState.complete:
self.active_review_segments[camera].last_update = manual_info[ self.active_review_segments[
"end_time" camera
] ].last_update = manual_info["end_time"]
else:
logger.warning(
f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert."
)
self.config_subscriber.stop() self.record_config_subscriber.stop()
self.review_config_subscriber.stop()
self.requestor.stop() self.requestor.stop()
self.detection_subscriber.stop() self.detection_subscriber.stop()
logger.info("Exiting review maintainer...") logger.info("Exiting review maintainer...")

View File

@ -72,18 +72,27 @@ class TrackedObject:
def max_severity(self) -> Optional[str]: def max_severity(self) -> Optional[str]:
review_config = self.camera_config.review review_config = self.camera_config.review
if self.obj_data["label"] in review_config.alerts.labels and ( if (
self.camera_config.review.alerts.enabled
and self.obj_data["label"] in review_config.alerts.labels
and (
not review_config.alerts.required_zones not review_config.alerts.required_zones
or set(self.entered_zones) & set(review_config.alerts.required_zones) or set(self.entered_zones) & set(review_config.alerts.required_zones)
)
): ):
return SeverityEnum.alert return SeverityEnum.alert
if ( if (
self.camera_config.review.detections.enabled
and (
not review_config.detections.labels not review_config.detections.labels
or self.obj_data["label"] in review_config.detections.labels or self.obj_data["label"] in review_config.detections.labels
) and ( )
and (
not review_config.detections.required_zones not review_config.detections.required_zones
or set(self.entered_zones) & set(review_config.detections.required_zones) or set(self.entered_zones)
& set(review_config.detections.required_zones)
)
): ):
return SeverityEnum.detection return SeverityEnum.detection

View File

@ -61,6 +61,8 @@ function useValue(): useValueReturn {
notifications, notifications,
notifications_suspended, notifications_suspended,
autotracking, autotracking,
alerts,
detections,
} = } =
// @ts-expect-error we know this is correct // @ts-expect-error we know this is correct
state["config"]; state["config"];
@ -76,6 +78,10 @@ function useValue(): useValueReturn {
cameraStates[`${name}/ptz_autotracker/state`] = autotracking cameraStates[`${name}/ptz_autotracker/state`] = autotracking
? "ON" ? "ON"
: "OFF"; : "OFF";
cameraStates[`${name}/review_alerts/state`] = alerts ? "ON" : "OFF";
cameraStates[`${name}/review_detections/state`] = detections
? "ON"
: "OFF";
}); });
setWsState((prevState) => ({ setWsState((prevState) => ({
@ -213,6 +219,31 @@ export function useAutotrackingState(camera: string): {
return { payload: payload as ToggleableSetting, send }; return { payload: payload as ToggleableSetting, send };
} }
export function useAlertsState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/review_alerts/state`, `${camera}/review_alerts/set`);
return { payload: payload as ToggleableSetting, send };
}
export function useDetectionsState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(
`${camera}/review_detections/state`,
`${camera}/review_detections/set`,
);
return { payload: payload as ToggleableSetting, send };
}
export function usePtzCommand(camera: string): { export function usePtzCommand(camera: string): {
payload: string; payload: string;
send: (payload: string, retain?: boolean) => void; send: (payload: string, retain?: boolean) => void;

View File

@ -144,7 +144,7 @@ export default function HlsVideoPlayer({
const [tallCamera, setTallCamera] = useState(false); const [tallCamera, setTallCamera] = useState(false);
const [isPlaying, setIsPlaying] = useState(true); const [isPlaying, setIsPlaying] = useState(true);
const [muted, setMuted] = useOverlayState("playerMuted", true); const [muted, setMuted] = usePersistence("hlsPlayerMuted", true);
const [volume, setVolume] = useOverlayState("playerVolume", 1.0); const [volume, setVolume] = useOverlayState("playerVolume", 1.0);
const [defaultPlaybackRate] = usePersistence("playbackRate", 1); const [defaultPlaybackRate] = usePersistence("playbackRate", 1);
const [playbackRate, setPlaybackRate] = useOverlayState( const [playbackRate, setPlaybackRate] = useOverlayState(
@ -211,7 +211,7 @@ export default function HlsVideoPlayer({
fullscreen: supportsFullscreen, fullscreen: supportsFullscreen,
}} }}
setControlsOpen={setControlsOpen} setControlsOpen={setControlsOpen}
setMuted={(muted) => setMuted(muted, true)} setMuted={(muted) => setMuted(muted)}
playbackRate={playbackRate ?? 1} playbackRate={playbackRate ?? 1}
hotKeys={hotKeys} hotKeys={hotKeys}
onPlayPause={onPlayPause} onPlayPause={onPlayPause}
@ -280,9 +280,12 @@ export default function HlsVideoPlayer({
} }
: undefined : undefined
} }
onVolumeChange={() => onVolumeChange={() => {
setVolume(videoRef.current?.volume ?? 1.0, true) setVolume(videoRef.current?.volume ?? 1.0, true);
if (!frigateControls) {
setMuted(videoRef.current?.muted);
} }
}}
onPlay={() => { onPlay={() => {
setIsPlaying(true); setIsPlaying(true);

View File

@ -179,6 +179,7 @@ export interface CameraConfig {
}; };
review: { review: {
alerts: { alerts: {
enabled: boolean;
required_zones: string[]; required_zones: string[];
labels: string[]; labels: string[];
retain: { retain: {
@ -187,6 +188,7 @@ export interface CameraConfig {
}; };
}; };
detections: { detections: {
enabled: boolean;
required_zones: string[]; required_zones: string[];
labels: string[]; labels: string[];
retain: { retain: {

View File

@ -27,6 +27,9 @@ import { LuExternalLink } from "react-icons/lu";
import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useAlertsState, useDetectionsState } from "@/api/ws";
type CameraSettingsViewProps = { type CameraSettingsViewProps = {
selectedCamera: string; selectedCamera: string;
@ -105,6 +108,11 @@ export default function CameraSettingsView({
const watchedAlertsZones = form.watch("alerts_zones"); const watchedAlertsZones = form.watch("alerts_zones");
const watchedDetectionsZones = form.watch("detections_zones"); const watchedDetectionsZones = form.watch("detections_zones");
const { payload: alertsState, send: sendAlerts } =
useAlertsState(selectedCamera);
const { payload: detectionsState, send: sendDetections } =
useDetectionsState(selectedCamera);
const handleCheckedChange = useCallback( const handleCheckedChange = useCallback(
(isChecked: boolean) => { (isChecked: boolean) => {
if (!isChecked) { if (!isChecked) {
@ -244,6 +252,47 @@ export default function CameraSettingsView({
<Separator className="my-2 flex bg-secondary" /> <Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2">
Review
</Heading>
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
<div className="flex flex-row items-center">
<Switch
id="alerts-enabled"
className="mr-3"
checked={alertsState == "ON"}
onCheckedChange={(isChecked) => {
sendAlerts(isChecked ? "ON" : "OFF");
}}
/>
<div className="space-y-0.5">
<Label htmlFor="alerts-enabled">Alerts</Label>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<Switch
id="detections-enabled"
className="mr-3"
checked={detectionsState == "ON"}
onCheckedChange={(isChecked) => {
sendDetections(isChecked ? "ON" : "OFF");
}}
/>
<div className="space-y-0.5">
<Label htmlFor="detections-enabled">Detections</Label>
</div>
</div>
<div className="mt-3 text-sm text-muted-foreground">
Enable/disable alerts and detections for this camera. When
disabled, no new review items will be generated.
</div>
</div>
</div>
<Separator className="my-2 flex bg-secondary" />
<Heading as="h4" className="my-2"> <Heading as="h4" className="my-2">
Review Classification Review Classification
</Heading> </Heading>