Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions docs/docs/configuration/zones.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ To create a zone, follow [the steps for a "Motion mask"](masks.md), but use the

### Restricting alerts and detections to specific zones

Often you will only want alerts to be created when an object enters areas of interest. This is done using zones along with setting required_zones. Let's say you only want to have an alert created when an object enters your entire_yard zone, the config would be:
You can flexibly define alert or detection zones, allowing you to focus on what matters most.

Often you will only want alerts to be created when an object enters areas of interest. This is done using zones along with setting review classification.

For example, you only want to have an alert created when an object enters your `Entire Yard` zone. Simply go to the `Camera → Review → Alerts` settings, check the `Entire Yard` zone you just created, and save the changes.

![Review Classification](/img/zones-review.png)

the config would be:

```yaml
cameras:
Expand All @@ -27,10 +35,13 @@ cameras:
- entire_yard
zones:
entire_yard:
friendly_name: Entire yard🏡 # You can use characters from any language, including emojis.
coordinates: ...
```

You may also want to filter detections to only be created when an object enters a secondary area of interest. This is done using zones along with setting required_zones. Let's say you want alerts when an object enters the inner area of the yard but detections when an object enters the edge of the yard, the config would be
You may also want to filter detections to only be created when an object enters a secondary area of interest. This is done using zones along with setting required_zones. Let's say you want alerts when an object enters the inner area of the yard but detections when an object enters the edge of the yard. Simply go to the `Detections` option on the previous page, check `Limit detections to specific zones`, and then select the desired zones.

the config would be

```yaml
cameras:
Expand All @@ -44,8 +55,10 @@ cameras:
- edge_yard
zones:
edge_yard:
friendly_name: Edge yard🚗 # You can use characters from any language, including emojis.
coordinates: ...
inner_yard:
friendly_name: Inner yard🪵 # You can use characters from any language, including emojis.
coordinates: ...
```

Expand All @@ -59,6 +72,7 @@ cameras:
- entire_yard
zones:
entire_yard:
friendly_name: Entire yard🏡
coordinates: ...
```

Expand All @@ -82,13 +96,16 @@ cameras:

Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. Objects will be tracked for any `person` that enter anywhere in the yard, and for cars only if they enter the street.

Of course, you can also manually select the desired tracked Object when editing zones on the `Masks / Zones` page.

### Zone Loitering

Sometimes objects are expected to be passing through a zone, but an object loitering in an area is unexpected. Zones can be configured to have a minimum loitering time after which the object will be considered in the zone.

:::note

When using loitering zones, a review item will behave in the following way:

- 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.

Expand Down
Binary file added docs/static/img/zones-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frigate/config/camera/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@


class ZoneConfig(BaseModel):
friendly_name: Optional[str] = Field(
None, title="Zone friendly name used in the Frigate UI."
)
filters: dict[str, FilterConfig] = Field(
default_factory=dict, title="Zone filters."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { CameraConfig } from "@/types/frigateConfig";
import { useZoneFriendlyName } from "@/hooks/use-zone-friendly-name";

interface CameraNameLabelProps
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
camera?: string | CameraConfig;
}

interface ZoneNameLabelProps
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
zone: string;
camera?: string;
}

const CameraNameLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
CameraNameLabelProps
Expand All @@ -21,4 +28,17 @@ const CameraNameLabel = React.forwardRef<
});
CameraNameLabel.displayName = LabelPrimitive.Root.displayName;

export { CameraNameLabel };
const ZoneNameLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
ZoneNameLabelProps
>(({ className, zone, camera, ...props }, ref) => {
const displayName = useZoneFriendlyName(zone, camera);
return (
<LabelPrimitive.Root ref={ref} className={className} {...props}>
{displayName}
</LabelPrimitive.Root>
);
});
ZoneNameLabel.displayName = LabelPrimitive.Root.displayName;

export { CameraNameLabel, ZoneNameLabel };
2 changes: 1 addition & 1 deletion web/src/components/filter/CameraGroupSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ import { CameraStreamingDialog } from "../settings/CameraStreamingDialog";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { useStreamingSettings } from "@/context/streaming-settings-provider";
import { Trans, useTranslation } from "react-i18next";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useIsCustomRole } from "@/hooks/use-is-custom-role";

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/filter/CamerasFilterButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export function CamerasFilterContent({
key={item}
isChecked={currentCameras?.includes(item) ?? false}
label={item}
isCameraName={true}
type={"camera"}
disabled={
mainCamera !== undefined &&
currentCameras !== undefined &&
Expand Down
16 changes: 13 additions & 3 deletions web/src/components/filter/FilterSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel";

type FilterSwitchProps = {
label: string;
disabled?: boolean;
isChecked: boolean;
isCameraName?: boolean;
type?: string;
extraValue?: string;
onCheckedChange: (checked: boolean) => void;
};
export default function FilterSwitch({
label,
disabled = false,
isChecked,
isCameraName = false,
type = "",
extraValue = "",
onCheckedChange,
}: FilterSwitchProps) {
return (
<div className="flex items-center justify-between gap-1">
{isCameraName ? (
{type === "camera" ? (
<CameraNameLabel
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" : ""}`}
htmlFor={label}
camera={label}
/>
) : type === "zone" ? (
<ZoneNameLabel
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" : ""}`}
htmlFor={label}
camera={extraValue}
zone={label}
/>
) : (
<Label
className={`mx-2 w-full cursor-pointer text-primary smart-capitalize ${disabled ? "text-secondary-foreground" : ""}`}
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/filter/ReviewFilterGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ export function GeneralFilterContent({
{allZones.map((item) => (
<FilterSwitch
key={item}
label={item.replaceAll("_", " ")}
label={item}
type={"zone"}
isChecked={filter.zones?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {
Expand Down
11 changes: 10 additions & 1 deletion web/src/components/input/InputWithTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import { MdImageSearch } from "react-icons/md";
import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel";

type InputWithTagsProps = {
inputFocused: boolean;
Expand Down Expand Up @@ -831,6 +831,8 @@ export default function InputWithTags({
getTranslatedLabel(value)
) : filterType === "cameras" ? (
<CameraNameLabel camera={value} />
) : filterType === "zones" ? (
<ZoneNameLabel zone={value} />
) : (
value.replaceAll("_", " ")
)}
Expand Down Expand Up @@ -934,6 +936,11 @@ export default function InputWithTags({
<CameraNameLabel camera={suggestion} />
{")"}
</>
) : currentFilterType === "zones" ? (
<>
{suggestion} {" ("} <ZoneNameLabel zone={suggestion} />
{")"}
</>
) : (
suggestion
)
Expand All @@ -943,6 +950,8 @@ export default function InputWithTags({
{currentFilterType ? (
currentFilterType === "cameras" ? (
<CameraNameLabel camera={suggestion} />
) : currentFilterType === "zones" ? (
<ZoneNameLabel zone={suggestion} />
) : (
formatFilterValues(currentFilterType, suggestion)
)
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/menu/LiveContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
import { useTranslation } from "react-i18next";
import { useDateLocale } from "@/hooks/use-date-locale";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";

type LiveContextMenuProps = {
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overlay/CreateRoleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "@/components/ui/dialog";
import { useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { isDesktop, isMobile } from "react-device-detect";
import { cn } from "@/lib/utils";
import {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overlay/EditRoleCamerasDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "@/components/ui/dialog";
import { Trans, useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";

type EditRoleCamerasOverlayProps = {
show: boolean;
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overlay/MobileCameraDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from "../ui/button";
import { FaVideo } from "react-icons/fa";
import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { CameraNameLabel } from "../camera/CameraNameLabel";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";

type MobileCameraDrawerProps = {
allCameras: string[];
Expand Down
20 changes: 18 additions & 2 deletions web/src/components/overlay/ObjectTrackOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { Event } from "@/types/event";
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";

// Use a small tolerance (10ms) for browsers with seek precision by-design issues
const TOLERANCE = 0.01;
Expand Down Expand Up @@ -73,6 +74,10 @@ export default function ObjectTrackOverlay({
{ revalidateOnFocus: false },
);

const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
return zones?.map((zone) => resolveZoneName(config, zone)) ?? [];
};

const timelineResults = useMemo(() => {
// Group timeline entries by source_id
if (!timelineData) return selectedObjectIds.map(() => []);
Expand All @@ -86,8 +91,19 @@ export default function ObjectTrackOverlay({
}

// Return timeline arrays in the same order as selectedObjectIds
return selectedObjectIds.map((id) => grouped[id] || []);
}, [selectedObjectIds, timelineData]);
return selectedObjectIds.map((id) => {
const entries = grouped[id] || [];
return entries.map((event) => ({
...event,
data: {
...event.data,
zones_friendly_names: config
? getZonesFriendlyNames(event.data?.zones, config)
: [],
},
}));
});
}, [selectedObjectIds, timelineData, config]);

const typeColorMap = useMemo(
() => ({
Expand Down
31 changes: 24 additions & 7 deletions web/src/components/overlay/detail/ObjectPath.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
import { useTranslation } from "react-i18next";
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";

type ObjectPathProps = {
positions?: Position[];
Expand Down Expand Up @@ -42,16 +45,30 @@ export function ObjectPath({
visible = true,
}: ObjectPathProps) {
const { t } = useTranslation(["views/explore"]);
const { data: config } = useSWR<FrigateConfig>("config");
const getAbsolutePositions = useCallback(() => {
if (!imgRef.current || !positions) return [];
const imgRect = imgRef.current.getBoundingClientRect();
return positions.map((pos) => ({
x: pos.x * imgRect.width,
y: pos.y * imgRect.height,
timestamp: pos.timestamp,
lifecycle_item: pos.lifecycle_item,
}));
}, [positions, imgRef]);
return positions.map((pos) => {
if (config && pos.lifecycle_item?.data?.zones) {
pos.lifecycle_item = {
...pos.lifecycle_item,
data: {
...pos.lifecycle_item.data,
zones_friendly_names: pos.lifecycle_item.data.zones.map((zone) => {
return resolveZoneName(config, zone);
}),
},
};
}
return {
x: pos.x * imgRect.width,
y: pos.y * imgRect.height,
timestamp: pos.timestamp,
lifecycle_item: pos.lifecycle_item,
};
});
}, [imgRef, positions, config]);

const generateStraightPath = useCallback((points: Position[]) => {
if (!points || points.length < 2) return "";
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overlay/detail/SearchDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import { useIsAdmin } from "@/hooks/use-is-admin";
import FaceSelectionDialog from "../FaceSelectionDialog";
import { getTranslatedLabel } from "@/utils/i18n";
import { CgTranscript } from "react-icons/cg";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { PiPath } from "react-icons/pi";
import Heading from "@/components/ui/heading";

Expand Down
3 changes: 2 additions & 1 deletion web/src/components/overlay/dialog/SearchFilterDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ export function ZoneFilterContent({
{allZones.map((item) => (
<FilterSwitch
key={item}
label={item.replaceAll("_", " ")}
label={item}
type={"zone"}
isChecked={zones?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/settings/PolygonCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export function PolygonCanvas({
const activePolygon = updatedPolygons[activePolygonIndex];

// add default points order for already completed polygons
if (!activePolygon.pointsOrder && activePolygon.isFinished) {
if (!activePolygon?.pointsOrder && activePolygon?.isFinished) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you seeing crashes here? Why was this change necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you seeing crashes here? Why was this change necessary?

yup, crashes may occur after saving, but the cause remains unknown.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've never had reports of crashes here. It would be good to make sure your changes are not causing an issue.

updatedPolygons[activePolygonIndex] = {
...activePolygon,
pointsOrder: activePolygon.points.map((_, index) => index),
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/settings/PolygonItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ export default function PolygonItem({
}}
/>
)}
<p className="cursor-default">{polygon.name}</p>
<p className="cursor-default">
{polygon.friendly_name ?? polygon.name}
</p>
</div>
<AlertDialog
open={deleteDialogOpen}
Expand All @@ -278,7 +280,7 @@ export default function PolygonItem({
ns="views/settings"
values={{
type: polygon.type.replace("_", " "),
name: polygon.name,
name: polygon.friendly_name ?? polygon.name,
}}
>
masksAndZones.form.polygonDrawing.delete.desc
Expand Down
Loading