Skip to content

Commit d3af748

Browse files
authored
feat: Add camera nickname (#19567)
* refactor: Refactor camera nickname * fix: fix cameraNameLabel visually * chore: The Explore search function also displays the Camera's nickname in English * chore: add mobile page camera nickname * feat: webpush support camera nickname * fix: fix storage camera name is null * chore: fix review detail and context menu camera nickname * chore: fix use-stats and notification setting camera nickname * fix: fix stats camera if not nickname need capitalize * fix: fix debug page open camera web ui i18n and camera nickname support * fix: fix camera metrics not use nickname * refactor: refactor use-camera-nickname hook.
1 parent 195f705 commit d3af748

31 files changed

+276
-99
lines changed

frigate/comms/webpush.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ def send_alert(self, payload: dict[str, Any]) -> None:
334334
return
335335

336336
camera: str = payload["after"]["camera"]
337+
camera_name: str = getattr(
338+
self.config.cameras[camera], "nickname", None
339+
) or titlecase(camera.replace("_", " "))
337340
current_time = datetime.datetime.now().timestamp()
338341

339342
if self._within_cooldown(camera):
@@ -375,7 +378,7 @@ def send_alert(self, payload: dict[str, Any]) -> None:
375378
if state == "genai" and payload["after"]["data"]["metadata"]:
376379
message = payload["after"]["data"]["metadata"]["scene"]
377380
else:
378-
message = f"Detected on {titlecase(camera.replace('_', ' '))}"
381+
message = f"Detected on {camera_name}"
379382

380383
if ended:
381384
logger.debug(
@@ -406,6 +409,9 @@ def send_trigger(self, payload: dict[str, Any]) -> None:
406409
return
407410

408411
camera: str = payload["camera"]
412+
camera_name: str = getattr(
413+
self.config.cameras[camera], "nickname", None
414+
) or titlecase(camera.replace("_", " "))
409415
current_time = datetime.datetime.now().timestamp()
410416

411417
if self._within_cooldown(camera):
@@ -421,14 +427,16 @@ def send_trigger(self, payload: dict[str, Any]) -> None:
421427
name = payload["name"]
422428
score = payload["score"]
423429

424-
title = f"{name.replace('_', ' ')} triggered on {titlecase(camera.replace('_', ' '))}"
425-
message = f"{titlecase(trigger_type)} trigger fired for {titlecase(camera.replace('_', ' '))} with score {score:.2f}"
430+
title = f"{name.replace('_', ' ')} triggered on {camera_name}"
431+
message = f"{titlecase(trigger_type)} trigger fired for {camera_name} with score {score:.2f}"
426432
image = f"clips/triggers/{camera}/{event_id}.webp"
427433

428434
direct_url = f"/explore?event_id={event_id}"
429435
ttl = 0
430436

431-
logger.debug(f"Sending push notification for {camera}, trigger name {name}")
437+
logger.debug(
438+
f"Sending push notification for {camera_name}, trigger name {name}"
439+
)
432440

433441
for user in self.web_pushers:
434442
self.send_push_notification(

frigate/config/camera/camera.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import Enum
33
from typing import Optional
44

5-
from pydantic import Field, PrivateAttr
5+
from pydantic import Field, PrivateAttr, model_validator
66

77
from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME
88
from frigate.ffmpeg_presets import (
@@ -51,6 +51,16 @@ class CameraTypeEnum(str, Enum):
5151

5252
class CameraConfig(FrigateBaseModel):
5353
name: Optional[str] = Field(None, title="Camera name.", pattern=REGEX_CAMERA_NAME)
54+
55+
nickname: Optional[str] = Field(None, title="Camera nickname. Only for display.")
56+
57+
@model_validator(mode="before")
58+
@classmethod
59+
def handle_nickname(cls, values):
60+
if isinstance(values, dict) and "nickname" in values:
61+
pass
62+
return values
63+
5464
enabled: bool = Field(default=True, title="Enable camera.")
5565

5666
# Options with global fallback

frigate/storage.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ def calculate_camera_usages(self) -> dict[str, dict]:
7777
.scalar()
7878
)
7979

80-
usages[camera] = {
80+
camera_key = (
81+
getattr(self.config.cameras[camera], "nickname", None) or camera
82+
)
83+
usages[camera_key] = {
8184
"usage": camera_storage,
8285
"bandwidth": self.camera_storage_stats.get(camera, {}).get(
8386
"bandwidth", 0

web/public/locales/en/components/camera.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"icon": "Icon",
2828
"success": "Camera group ({{name}}) has been saved.",
2929
"camera": {
30+
"birdseye": "Birdseye",
3031
"setting": {
3132
"label": "Camera Streaming Settings",
3233
"title": "{{cameraName}} Streaming Settings",

web/public/locales/en/views/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"cameras": "Camera Settings",
1818
"masksAndZones": "Masks / Zones",
1919
"motionTuner": "Motion Tuner",
20+
"triggers": "Triggers",
2021
"debug": "Debug",
2122
"users": "Users",
2223
"notifications": "Notifications",
@@ -192,7 +193,7 @@
192193
"description": "Configure camera settings including stream inputs and roles.",
193194
"name": "Camera Name",
194195
"nameRequired": "Camera name is required",
195-
"nameInvalid": "Camera name must contain only letters, numbers, underscores, or hyphens",
196+
"nameLength": "Camera name must be less than 24 characters.",
196197
"namePlaceholder": "e.g., front_door",
197198
"enabled": "Enabled",
198199
"ffmpeg": {
@@ -408,6 +409,7 @@
408409
"title": "Debug",
409410
"detectorDesc": "Frigate uses your detectors ({{detectors}}) to detect objects in your camera's video stream.",
410411
"desc": "Debugging view shows a real-time view of tracked objects and their statistics. The object list shows a time-delayed summary of detected objects.",
412+
"openCameraWebUI": "Open {{camera}}'s Web UI",
411413
"debugging": "Debugging",
412414
"objectList": "Object List",
413415
"noObjects": "No objects",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react";
2+
import * as LabelPrimitive from "@radix-ui/react-label";
3+
import { useCameraNickname } from "@/hooks/use-camera-nickname";
4+
import { CameraConfig } from "@/types/frigateConfig";
5+
6+
interface CameraNameLabelProps
7+
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
8+
camera?: string | CameraConfig;
9+
}
10+
11+
const CameraNameLabel = React.forwardRef<
12+
React.ElementRef<typeof LabelPrimitive.Root>,
13+
CameraNameLabelProps
14+
>(({ className, camera, ...props }, ref) => {
15+
const displayName = useCameraNickname(camera);
16+
return (
17+
<LabelPrimitive.Root ref={ref} className={className} {...props}>
18+
{displayName}
19+
</LabelPrimitive.Root>
20+
);
21+
});
22+
CameraNameLabel.displayName = LabelPrimitive.Root.displayName;
23+
24+
export { CameraNameLabel };

web/src/components/filter/CameraGroupSelector.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ import {
7171
MobilePageTitle,
7272
} from "../mobile/MobilePage";
7373

74-
import { Label } from "../ui/label";
7574
import { Switch } from "../ui/switch";
7675
import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
7776
import { DialogTrigger } from "@radix-ui/react-dialog";
7877
import { useStreamingSettings } from "@/context/streaming-settings-provider";
7978
import { Trans, useTranslation } from "react-i18next";
79+
import { CameraNameLabel } from "../camera/CameraNameLabel";
8080

8181
type CameraGroupSelectorProps = {
8282
className?: string;
@@ -846,12 +846,11 @@ export function CameraGroupEdit({
846846
].map((camera) => (
847847
<FormControl key={camera}>
848848
<div className="flex items-center justify-between gap-1">
849-
<Label
849+
<CameraNameLabel
850850
className="mx-2 w-full cursor-pointer text-primary smart-capitalize"
851851
htmlFor={camera.replaceAll("_", " ")}
852-
>
853-
{camera.replaceAll("_", " ")}
854-
</Label>
852+
camera={camera}
853+
/>
855854

856855
<div className="flex items-center gap-x-2">
857856
{camera !== "birdseye" && (

web/src/components/filter/CamerasFilterButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ export function CamerasFilterContent({
189189
<FilterSwitch
190190
key={item}
191191
isChecked={currentCameras?.includes(item) ?? false}
192-
label={item.replaceAll("_", " ")}
192+
label={item}
193+
isCameraName={true}
193194
disabled={
194195
mainCamera !== undefined &&
195196
currentCameras !== undefined &&

web/src/components/filter/FilterSwitch.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import { Switch } from "../ui/switch";
22
import { Label } from "../ui/label";
3+
import { CameraNameLabel } from "../camera/CameraNameLabel";
34

45
type FilterSwitchProps = {
56
label: string;
67
disabled?: boolean;
78
isChecked: boolean;
9+
isCameraName?: boolean;
810
onCheckedChange: (checked: boolean) => void;
911
};
1012
export default function FilterSwitch({
1113
label,
1214
disabled = false,
1315
isChecked,
16+
isCameraName = false,
1417
onCheckedChange,
1518
}: FilterSwitchProps) {
1619
return (
1720
<div className="flex items-center justify-between gap-1">
18-
<Label
19-
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
20-
htmlFor={label}
21-
>
22-
{label}
23-
</Label>
21+
{isCameraName ? (
22+
<CameraNameLabel
23+
className={`mx-2 w-full cursor-pointer text-sm font-medium leading-none text-primary smart-capitalize peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${disabled ? "text-secondary-foreground" : ""}`}
24+
htmlFor={label}
25+
camera={label}
26+
/>
27+
) : (
28+
<Label
29+
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
30+
htmlFor={label}
31+
>
32+
{label}
33+
</Label>
34+
)}
2435
<Switch
2536
id={label}
2637
disabled={disabled}

web/src/components/input/InputWithTags.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
5353
import { MdImageSearch } from "react-icons/md";
5454
import { useTranslation } from "react-i18next";
5555
import { getTranslatedLabel } from "@/utils/i18n";
56+
import { CameraNameLabel } from "../camera/CameraNameLabel";
5657

5758
type InputWithTagsProps = {
5859
inputFocused: boolean;
@@ -826,9 +827,13 @@ export default function InputWithTags({
826827
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm text-green-800 smart-capitalize"
827828
>
828829
{t("filter.label." + filterType)}:{" "}
829-
{filterType === "labels"
830-
? getTranslatedLabel(value)
831-
: value.replaceAll("_", " ")}
830+
{filterType === "labels" ? (
831+
getTranslatedLabel(value)
832+
) : filterType === "cameras" ? (
833+
<CameraNameLabel camera={value} />
834+
) : (
835+
value.replaceAll("_", " ")
836+
)}
832837
<button
833838
onClick={() =>
834839
removeFilter(filterType as FilterType, value)
@@ -923,13 +928,27 @@ export default function InputWithTags({
923928
onSelect={() => handleSuggestionClick(suggestion)}
924929
>
925930
{i18n.language === "en" ? (
926-
suggestion
931+
currentFilterType && currentFilterType === "cameras" ? (
932+
<>
933+
{suggestion} {" ("}{" "}
934+
<CameraNameLabel camera={suggestion} />
935+
{")"}
936+
</>
937+
) : (
938+
suggestion
939+
)
927940
) : (
928941
<>
929942
{suggestion} {" ("}
930-
{currentFilterType
931-
? formatFilterValues(currentFilterType, suggestion)
932-
: t("filter.label." + suggestion)}
943+
{currentFilterType ? (
944+
currentFilterType === "cameras" ? (
945+
<CameraNameLabel camera={suggestion} />
946+
) : (
947+
formatFilterValues(currentFilterType, suggestion)
948+
)
949+
) : (
950+
t("filter.label." + suggestion)
951+
)}
933952
{")"}
934953
</>
935954
)}

0 commit comments

Comments
 (0)