better motion tuner

This commit is contained in:
Josh Hawkins 2024-04-10 09:16:46 -05:00
parent 7d4dd7de8f
commit 96c44e65bd
3 changed files with 223 additions and 35 deletions

View File

@ -234,3 +234,17 @@ export function useMotionContourArea(camera: string): {
); );
return { payload: payload as string, send }; return { payload: payload as string, send };
} }
export function useImproveContrast(camera: string): {
payload: string;
send: (payload: string, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(
`${camera}/improve_contrast/state`,
`${camera}/improve_contrast/set`,
);
return { payload: payload as string, send };
}

View File

@ -8,18 +8,47 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import axios from "axios";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useMotionContourArea, useMotionThreshold } from "@/api/ws"; import {
useImproveContrast,
useMotionContourArea,
useMotionThreshold,
} from "@/api/ws";
import { Skeleton } from "../ui/skeleton"; import { Skeleton } from "../ui/skeleton";
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
type MotionSettings = {
threshold?: number;
contour_area?: number;
improve_contrast?: boolean;
};
export default function MotionTuner() { export default function MotionTuner() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const cameras = useMemo(() => { const cameras = useMemo(() => {
if (!config) { if (!config) {
@ -31,7 +60,18 @@ export default function MotionTuner() {
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]); }, [config]);
const [selectedCamera, setSelectedCamera] = useState(cameras[0].name); const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
const [nextSelectedCamera, setNextSelectedCamera] = useState("");
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
const { send: sendImproveContrast } = useImproveContrast(selectedCamera);
const [motionSettings, setMotionSettings] = useState<MotionSettings>({
threshold: undefined,
contour_area: undefined,
improve_contrast: undefined,
});
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
if (config && selectedCamera) { if (config && selectedCamera) {
@ -39,36 +79,114 @@ export default function MotionTuner() {
} }
}, [config, selectedCamera]); }, [config, selectedCamera]);
const motionThreshold = useMemo(() => { useEffect(() => {
return cameraConfig?.motion.threshold ?? 0; if (cameraConfig) {
}, [cameraConfig?.motion.threshold]); setMotionSettings({
threshold: cameraConfig.motion.threshold,
contour_area: cameraConfig.motion.contour_area,
improve_contrast: cameraConfig.motion.improve_contrast,
});
}
}, [cameraConfig]);
const motionContourArea = useMemo( useEffect(() => {
() => cameraConfig?.motion.contour_area ?? 0, if (cameraConfig) {
[cameraConfig?.motion.contour_area], const { threshold, contour_area, improve_contrast } = motionSettings;
);
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); if (
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); threshold !== undefined &&
cameraConfig.motion.threshold !== threshold
const setMotionThreshold = useCallback( ) {
(threshold: number) => {
if (cameraConfig && threshold != motionThreshold) {
cameraConfig.motion.threshold = threshold;
sendMotionThreshold(threshold); sendMotionThreshold(threshold);
} }
},
[cameraConfig, motionThreshold, sendMotionThreshold],
);
const setMotionContourArea = useCallback( if (
(contour_area: number) => { contour_area !== undefined &&
if (cameraConfig && contour_area != motionContourArea) { cameraConfig.motion.contour_area !== contour_area
cameraConfig.motion.contour_area = contour_area; ) {
sendMotionContourArea(contour_area); sendMotionContourArea(contour_area);
} }
if (
improve_contrast !== undefined &&
cameraConfig.motion.improve_contrast !== improve_contrast
) {
sendImproveContrast(improve_contrast ? "ON" : "OFF");
}
}
}, [
cameraConfig,
motionSettings,
sendMotionThreshold,
sendMotionContourArea,
sendImproveContrast,
]);
const handleMotionConfigChange = (newConfig: Partial<MotionSettings>) => {
setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
setChangedValue(true);
};
const saveToConfig = useCallback(async () => {
setIsLoading(true);
axios
.put(
`config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast}`,
{ requires_restart: 0 },
)
.then((res) => {
if (res.status === 200) {
toast.success("Motion settings saved.", { position: "top-center" });
setChangedValue(false);
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
}
})
.catch((error) => {
toast.error(
`Failed to save config changes: ${error.response.data.message}`,
{ position: "top-center" },
);
})
.finally(() => {
setIsLoading(false);
});
}, [
updateConfig,
motionSettings.threshold,
motionSettings.contour_area,
motionSettings.improve_contrast,
selectedCamera,
]);
const handleSelectedCameraChange = useCallback(
(camera: string) => {
if (changedValue) {
setNextSelectedCamera(camera);
setConfirmationDialogOpen(true);
} else {
setSelectedCamera(camera);
setNextSelectedCamera("");
}
}, },
[cameraConfig, motionContourArea, sendMotionContourArea], [setSelectedCamera, changedValue],
);
const handleDialog = useCallback(
(save: boolean) => {
if (save) {
saveToConfig();
}
setSelectedCamera(nextSelectedCamera);
setNextSelectedCamera("");
setConfirmationDialogOpen(false);
setChangedValue(false);
},
[saveToConfig, setSelectedCamera, nextSelectedCamera],
); );
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
@ -78,8 +196,12 @@ export default function MotionTuner() {
return ( return (
<> <>
<Heading as="h2">Motion Detection Tuner</Heading> <Heading as="h2">Motion Detection Tuner</Heading>
<Toaster />
<div className="flex items-center space-x-2 mt-5"> <div className="flex items-center space-x-2 mt-5">
<Select value={selectedCamera} onValueChange={setSelectedCamera}> <Select
value={selectedCamera}
onValueChange={handleSelectedCameraChange}
>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Camera" /> <SelectValue placeholder="Camera" />
</SelectTrigger> </SelectTrigger>
@ -89,7 +211,7 @@ export default function MotionTuner() {
{cameras.map((camera) => ( {cameras.map((camera) => (
<SelectItem <SelectItem
key={camera.name} key={camera.name}
value={`${camera.name}`} value={camera.name}
className="capitalize" className="capitalize"
> >
{camera.name} {camera.name}
@ -111,31 +233,84 @@ export default function MotionTuner() {
<Slider <Slider
id="motion-threshold" id="motion-threshold"
className="w-[300px]" className="w-[300px]"
value={[motionThreshold]} disabled={motionSettings.threshold === undefined}
value={[motionSettings.threshold ?? 0]}
min={10} min={10}
max={80} max={80}
step={1} step={1}
onValueChange={(value) => setMotionThreshold(value[0])} onValueChange={(value) => {
handleMotionConfigChange({ threshold: value[0] });
}}
/> />
<Label htmlFor="motion-threshold" className="px-2"> <Label htmlFor="motion-threshold" className="px-2">
Threshold: {motionThreshold} Threshold: {motionSettings.threshold}
</Label> </Label>
</div> </div>
<div className="flex flex-row"> <div className="flex flex-row">
<Slider <Slider
id="motion-contour-area" id="motion-contour-area"
className="w-[300px]" className="w-[300px]"
value={[motionContourArea]} disabled={motionSettings.contour_area === undefined}
value={[motionSettings.contour_area ?? 0]}
min={10} min={10}
max={200} max={200}
step={5} step={5}
onValueChange={(value) => setMotionContourArea(value[0])} onValueChange={(value) => {
handleMotionConfigChange({ contour_area: value[0] });
}}
/> />
<Label htmlFor="motion-contour-area" className="px-2"> <Label htmlFor="motion-contour-area" className="px-2">
Contour Area: {motionContourArea} Contour Area: {motionSettings.contour_area}
</Label> </Label>
</div> </div>
<div className="flex flex-row">
<Switch
id="improve-contrast"
disabled={motionSettings.improve_contrast === undefined}
checked={motionSettings.improve_contrast === true}
onCheckedChange={(isChecked) => {
handleMotionConfigChange({ improve_contrast: isChecked });
}}
/>
<Label htmlFor="improve-contrast">Improve Contrast</Label>
</div> </div>
<div className="flex">
<Button
size="sm"
variant={isLoading ? "ghost" : "select"}
disabled={!changedValue || isLoading}
onClick={saveToConfig}
>
{isLoading ? "Saving..." : "Save to Config"}
</Button>
</div>
</div>
{confirmationDialogOpen && (
<AlertDialog
open={confirmationDialogOpen}
onOpenChange={() => setConfirmationDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
You have unsaved changes on this camera.
</AlertDialogTitle>
<AlertDialogDescription>
Do you want to save your changes before continuing?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => handleDialog(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDialog(true)}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div> </div>
) : ( ) : (
<Skeleton className="size-full rounded-2xl" /> <Skeleton className="size-full rounded-2xl" />

View File

@ -226,7 +226,6 @@ export default function SettingsZones() {
</code> </code>
</TableCell> </TableCell>
<TableCell> <TableCell>
{" "}
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={() => setActivePolygonIndex(index)} onClick={() => setActivePolygonIndex(index)}