Tweaks and fixes (#11541)

* Update config version to be stored inside of the config

* Don't remove items from list when navigating back

* Use video api instead of webps for live current hour filmstrip

* Check that the config file is writable

* Show camera name when camera is offline

* Show camera name when offline

* Cleanup
This commit is contained in:
Nicolas Mowen 2024-05-26 15:49:12 -06:00 committed by GitHub
parent 63d81bef45
commit c2eac10925
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 151 additions and 117 deletions

View File

@ -1355,6 +1355,7 @@ class FrigateConfig(FrigateBaseModel):
default_factory=TimestampStyleConfig, default_factory=TimestampStyleConfig,
title="Global timestamp style configuration.", title="Global timestamp style configuration.",
) )
version: Optional[float] = Field(default=None, title="Current config version.")
def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig: def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
"""Merge camera config with globals.""" """Merge camera config with globals."""

View File

@ -17,16 +17,17 @@ CURRENT_CONFIG_VERSION = 0.14
def migrate_frigate_config(config_file: str): def migrate_frigate_config(config_file: str):
"""handle migrating the frigate config.""" """handle migrating the frigate config."""
logger.info("Checking if frigate config needs migration...") logger.info("Checking if frigate config needs migration...")
version_file = os.path.join(CONFIG_DIR, ".version")
if not os.path.isfile(version_file): if not os.access(config_file, mode=os.W_OK):
previous_version = 0.13 logger.error("Config file is read-only, unable to migrate config file.")
else: return
with open(version_file) as f:
try: yaml = YAML()
previous_version = float(f.readline()) yaml.indent(mapping=2, sequence=4, offset=2)
except Exception: with open(config_file, "r") as f:
previous_version = 0.13 config: dict[str, dict[str, any]] = yaml.load(f)
previous_version = config.get("version", 0.13)
if previous_version == CURRENT_CONFIG_VERSION: if previous_version == CURRENT_CONFIG_VERSION:
logger.info("frigate config does not need migration...") logger.info("frigate config does not need migration...")
@ -35,11 +36,6 @@ def migrate_frigate_config(config_file: str):
logger.info("copying config as backup...") logger.info("copying config as backup...")
shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml")) shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml"))
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f:
config: dict[str, dict[str, any]] = yaml.load(f)
if previous_version < 0.14: if previous_version < 0.14:
logger.info(f"Migrating frigate config from {previous_version} to 0.14...") logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
new_config = migrate_014(config) new_config = migrate_014(config)
@ -57,9 +53,6 @@ def migrate_frigate_config(config_file: str):
os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name) os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name)
) )
with open(version_file, "w") as f:
f.write(str(CURRENT_CONFIG_VERSION))
logger.info("Finished frigate config migration...") logger.info("Finished frigate config migration...")
@ -141,6 +134,7 @@ def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
new_config["cameras"][name] = camera_config new_config["cameras"][name] = camera_config
new_config["version"] = 0.14
return new_config return new_config

View File

@ -1,33 +1,20 @@
import { useFrigateStats } from "@/api/ws";
import { import {
StatusBarMessagesContext, StatusBarMessagesContext,
StatusMessage, StatusMessage,
} from "@/context/statusbar-provider"; } from "@/context/statusbar-provider";
import useStats from "@/hooks/use-stats"; import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
import { FrigateStats } from "@/types/stats";
import { useContext, useEffect, useMemo } from "react"; import { useContext, useEffect, useMemo } from "react";
import { FaCheck } from "react-icons/fa"; import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io"; import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import useSWR from "swr";
export default function Statusbar() { export default function Statusbar() {
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();
const { messages, addMessage, clearMessages } = useContext( const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext, StatusBarMessagesContext,
)!; )!;
const stats = useMemo(() => { const stats = useAutoFrigateStats();
if (latestStats) {
return latestStats;
}
return initialStats;
}, [initialStats, latestStats]);
const cpuPercent = useMemo(() => { const cpuPercent = useMemo(() => {
const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu; const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu;

View File

@ -7,12 +7,10 @@ import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { RecordingStartingPoint } from "@/types/record"; import { RecordingStartingPoint } from "@/types/record";
import axios from "axios"; import axios from "axios";
import { import { VideoPreview } from "../player/PreviewThumbnailPlayer";
InProgressPreview,
VideoPreview,
} from "../player/PreviewThumbnailPlayer";
import { isCurrentHour } from "@/utils/dateUtil"; import { isCurrentHour } from "@/utils/dateUtil";
import { useCameraPreviews } from "@/hooks/use-camera-previews"; import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { baseUrl } from "@/api/baseUrl";
type AnimatedEventCardProps = { type AnimatedEventCardProps = {
event: ReviewSegment; event: ReviewSegment;
@ -105,18 +103,11 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
windowVisible={windowVisible} windowVisible={windowVisible}
/> />
) : ( ) : (
<InProgressPreview <video
review={event} src={`${baseUrl}api/review/${event.id}/preview?format=ts`}
timeRange={{ muted
after: event.start_time, autoPlay
before: event.end_time ?? event.start_time + 20,
}}
loop loop
showProgress={false}
setReviewed={() => {}}
setIgnoreClick={() => {}}
isPlayingBack={() => {}}
windowVisible={windowVisible}
/> />
)} )}
</div> </div>

View File

@ -46,7 +46,7 @@ export default function LivePlayer({
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
const { activeMotion, activeTracking, objects } = const { activeMotion, activeTracking, objects, offline } =
useCameraActivity(cameraConfig); useCameraActivity(cameraConfig);
const cameraActive = useMemo( const cameraActive = useMemo(
@ -224,9 +224,16 @@ export default function LivePlayer({
/> />
</div> </div>
<div className="absolute right-2 top-2 size-4"> <div className="absolute right-2 top-2">
{activeMotion && ( {!offline && activeMotion && (
<MdCircle className="size-2 animate-pulse text-danger shadow-danger drop-shadow-md" /> <MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
)}
{offline && (
<Chip
className={`z-0 flex items-start justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
>
{cameraConfig.name.replaceAll("_", " ")}
</Chip>
)} )}
</div> </div>
</div> </div>

View File

@ -10,11 +10,13 @@ import { useTimelineUtils } from "./use-timeline-utils";
import { ObjectType } from "@/types/ws"; import { ObjectType } from "@/types/ws";
import useDeepMemo from "./use-deep-memo"; import useDeepMemo from "./use-deep-memo";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { useAutoFrigateStats } from "./use-stats";
type useCameraActivityReturn = { type useCameraActivityReturn = {
activeTracking: boolean; activeTracking: boolean;
activeMotion: boolean; activeMotion: boolean;
objects: ObjectType[]; objects: ObjectType[];
offline: boolean;
}; };
export function useCameraActivity( export function useCameraActivity(
@ -116,12 +118,31 @@ export function useCameraActivity(
handleSetObjects(newObjects); handleSetObjects(newObjects);
}, [camera, updatedEvent, objects, handleSetObjects]); }, [camera, updatedEvent, objects, handleSetObjects]);
// determine if camera is offline
const stats = useAutoFrigateStats();
const offline = useMemo(() => {
if (!stats) {
return false;
}
const cameras = stats["cameras"];
if (!cameras) {
return false;
}
return cameras[camera.name].camera_fps == 0;
}, [camera, stats]);
return { return {
activeTracking: hasActiveObjects, activeTracking: hasActiveObjects,
activeMotion: detectingMotion activeMotion: detectingMotion
? detectingMotion === "ON" ? detectingMotion === "ON"
: initialCameraState?.motion === true, : initialCameraState?.motion === true,
objects, objects,
offline,
}; };
} }

View File

@ -9,6 +9,7 @@ import { useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import useDeepMemo from "./use-deep-memo"; import useDeepMemo from "./use-deep-memo";
import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { useFrigateStats } from "@/api/ws";
export default function useStats(stats: FrigateStats | undefined) { export default function useStats(stats: FrigateStats | undefined) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -91,3 +92,20 @@ export default function useStats(stats: FrigateStats | undefined) {
return { potentialProblems }; return { potentialProblems };
} }
export function useAutoFrigateStats() {
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();
const stats = useMemo(() => {
if (latestStats) {
return latestStats;
}
return initialStats;
}, [initialStats, latestStats]);
return stats;
}

View File

@ -10,6 +10,7 @@ import {
ReviewSegment, ReviewSegment,
ReviewSeverity, ReviewSeverity,
ReviewSummary, ReviewSummary,
SegmentedReviewData,
} from "@/types/review"; } from "@/types/review";
import { getTimestampOffset } from "@/utils/dateUtil"; import { getTimestampOffset } from "@/utils/dateUtil";
import EventView from "@/views/events/EventView"; import EventView from "@/views/events/EventView";
@ -138,6 +139,66 @@ export default function Events() {
}, },
); );
const reviewItems = useMemo<SegmentedReviewData>(() => {
if (!reviews) {
return undefined;
}
const all: ReviewSegment[] = [];
const alerts: ReviewSegment[] = [];
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
case "alert":
alerts.push(segment);
break;
case "detection":
detections.push(segment);
break;
default:
motion.push(segment);
break;
}
});
return {
all: all,
alert: alerts,
detection: detections,
significant_motion: motion,
};
}, [reviews]);
const currentItems = useMemo(() => {
if (!reviewItems || !severity) {
return null;
}
let current;
if (reviewFilter?.showAll) {
current = reviewItems.all;
} else {
current = reviewItems[severity];
}
if (!current || current.length == 0) {
return [];
}
if (reviewFilter?.showReviewed != 1) {
return current.filter((seg) => !seg.has_been_reviewed);
} else {
return current;
}
// only refresh when severity or filter changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity, reviewFilter, reviewItems?.all.length]);
// review summary // review summary
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>( const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
@ -353,7 +414,8 @@ export default function Events() {
} else { } else {
return ( return (
<EventView <EventView
reviews={reviews} reviewItems={reviewItems}
currentReviewItems={currentItems}
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
relevantPreviews={allPreviews} relevantPreviews={allPreviews}
timeRange={selectedTimeRange} timeRange={selectedTimeRange}

View File

@ -20,6 +20,15 @@ export type ReviewData = {
zones: string[]; zones: string[];
}; };
export type SegmentedReviewData =
| {
all: ReviewSegment[];
alert: ReviewSegment[];
detection: ReviewSegment[];
significant_motion: ReviewSegment[];
}
| undefined;
export type ReviewFilter = { export type ReviewFilter = {
cameras?: string[]; cameras?: string[];
labels?: string[]; labels?: string[];

View File

@ -17,6 +17,7 @@ import {
ReviewSegment, ReviewSegment,
ReviewSeverity, ReviewSeverity,
ReviewSummary, ReviewSummary,
SegmentedReviewData,
} from "@/types/review"; } from "@/types/review";
import { getChunkedTimeRange } from "@/utils/timelineUtil"; import { getChunkedTimeRange } from "@/utils/timelineUtil";
import axios from "axios"; import axios from "axios";
@ -49,7 +50,8 @@ import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner"; import { toast } from "sonner";
type EventViewProps = { type EventViewProps = {
reviews?: ReviewSegment[]; reviewItems?: SegmentedReviewData;
currentReviewItems: ReviewSegment[] | null;
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
relevantPreviews?: Preview[]; relevantPreviews?: Preview[];
timeRange: TimeRange; timeRange: TimeRange;
@ -64,7 +66,8 @@ type EventViewProps = {
updateFilter: (filter: ReviewFilter) => void; updateFilter: (filter: ReviewFilter) => void;
}; };
export default function EventView({ export default function EventView({
reviews, reviewItems,
currentReviewItems,
reviewSummary, reviewSummary,
relevantPreviews, relevantPreviews,
timeRange, timeRange,
@ -116,42 +119,6 @@ export default function EventView({
} }
}, [filter, reviewSummary]); }, [filter, reviewSummary]);
// review paging
const reviewItems = useMemo(() => {
if (!reviews) {
return undefined;
}
const all: ReviewSegment[] = [];
const alerts: ReviewSegment[] = [];
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
case "alert":
alerts.push(segment);
break;
case "detection":
detections.push(segment);
break;
default:
motion.push(segment);
break;
}
});
return {
all: all,
alert: alerts,
detection: detections,
significant_motion: motion,
};
}, [reviews]);
// review interaction // review interaction
const [selectedReviews, setSelectedReviews] = useState<string[]>([]); const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
@ -182,6 +149,7 @@ export default function EventView({
severity: review.severity, severity: review.severity,
}); });
review.has_been_reviewed = true;
markItemAsReviewed(review); markItemAsReviewed(review);
} }
}, },
@ -332,6 +300,7 @@ export default function EventView({
<DetectionReview <DetectionReview
contentRef={contentRef} contentRef={contentRef}
reviewItems={reviewItems} reviewItems={reviewItems}
currentItems={currentReviewItems}
relevantPreviews={relevantPreviews} relevantPreviews={relevantPreviews}
selectedReviews={selectedReviews} selectedReviews={selectedReviews}
itemsToReview={reviewCounts[severityToggle]} itemsToReview={reviewCounts[severityToggle]}
@ -372,6 +341,7 @@ type DetectionReviewProps = {
detection: ReviewSegment[]; detection: ReviewSegment[];
significant_motion: ReviewSegment[]; significant_motion: ReviewSegment[];
}; };
currentItems: ReviewSegment[] | null;
itemsToReview?: number; itemsToReview?: number;
relevantPreviews?: Preview[]; relevantPreviews?: Preview[];
selectedReviews: string[]; selectedReviews: string[];
@ -388,6 +358,7 @@ type DetectionReviewProps = {
function DetectionReview({ function DetectionReview({
contentRef, contentRef,
reviewItems, reviewItems,
currentItems,
itemsToReview, itemsToReview,
relevantPreviews, relevantPreviews,
selectedReviews, selectedReviews,
@ -405,33 +376,6 @@ function DetectionReview({
const segmentDuration = 60; const segmentDuration = 60;
// review data
const currentItems = useMemo(() => {
if (!reviewItems) {
return null;
}
let current;
if (filter?.showAll) {
current = reviewItems.all;
} else {
current = reviewItems[severity];
}
if (!current || current.length == 0) {
return [];
}
if (filter?.showReviewed != 1) {
return current.filter((seg) => !seg.has_been_reviewed);
} else {
return current;
}
// only refresh when severity or filter changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity, filter, reviewItems?.all.length]);
// preview // preview
const [previewTime, setPreviewTime] = useState<number>(); const [previewTime, setPreviewTime] = useState<number>();