diff --git a/frigate/config.py b/frigate/config.py index 05ca61f4d..5cf8b2da4 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -1355,6 +1355,7 @@ class FrigateConfig(FrigateBaseModel): default_factory=TimestampStyleConfig, title="Global timestamp style configuration.", ) + version: Optional[float] = Field(default=None, title="Current config version.") def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig: """Merge camera config with globals.""" diff --git a/frigate/util/config.py b/frigate/util/config.py index f395ee125..df2d0c1f1 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -17,16 +17,17 @@ CURRENT_CONFIG_VERSION = 0.14 def migrate_frigate_config(config_file: str): """handle migrating the frigate config.""" logger.info("Checking if frigate config needs migration...") - version_file = os.path.join(CONFIG_DIR, ".version") - if not os.path.isfile(version_file): - previous_version = 0.13 - else: - with open(version_file) as f: - try: - previous_version = float(f.readline()) - except Exception: - previous_version = 0.13 + if not os.access(config_file, mode=os.W_OK): + logger.error("Config file is read-only, unable to migrate config file.") + return + + 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) + + previous_version = config.get("version", 0.13) if previous_version == CURRENT_CONFIG_VERSION: 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...") 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: logger.info(f"Migrating frigate config from {previous_version} to 0.14...") 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) ) - with open(version_file, "w") as f: - f.write(str(CURRENT_CONFIG_VERSION)) - 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["version"] = 0.14 return new_config diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index b2574f2ee..67af68af9 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -1,33 +1,20 @@ -import { useFrigateStats } from "@/api/ws"; import { StatusBarMessagesContext, StatusMessage, } from "@/context/statusbar-provider"; -import useStats from "@/hooks/use-stats"; -import { FrigateStats } from "@/types/stats"; +import useStats, { useAutoFrigateStats } from "@/hooks/use-stats"; import { useContext, useEffect, useMemo } from "react"; import { FaCheck } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { MdCircle } from "react-icons/md"; import { Link } from "react-router-dom"; -import useSWR from "swr"; export default function Statusbar() { - const { data: initialStats } = useSWR("stats", { - revalidateOnFocus: false, - }); - const { payload: latestStats } = useFrigateStats(); const { messages, addMessage, clearMessages } = useContext( StatusBarMessagesContext, )!; - const stats = useMemo(() => { - if (latestStats) { - return latestStats; - } - - return initialStats; - }, [initialStats, latestStats]); + const stats = useAutoFrigateStats(); const cpuPercent = useMemo(() => { const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu; diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index b2a4750a9..c454d5ea7 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -7,12 +7,10 @@ import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; import { useNavigate } from "react-router-dom"; import { RecordingStartingPoint } from "@/types/record"; import axios from "axios"; -import { - InProgressPreview, - VideoPreview, -} from "../player/PreviewThumbnailPlayer"; +import { VideoPreview } from "../player/PreviewThumbnailPlayer"; import { isCurrentHour } from "@/utils/dateUtil"; import { useCameraPreviews } from "@/hooks/use-camera-previews"; +import { baseUrl } from "@/api/baseUrl"; type AnimatedEventCardProps = { event: ReviewSegment; @@ -105,18 +103,11 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { windowVisible={windowVisible} /> ) : ( - {}} - setIgnoreClick={() => {}} - isPlayingBack={() => {}} - windowVisible={windowVisible} /> )} diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 52323a4ba..a57c697d4 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -46,7 +46,7 @@ export default function LivePlayer({ }: LivePlayerProps) { // camera activity - const { activeMotion, activeTracking, objects } = + const { activeMotion, activeTracking, objects, offline } = useCameraActivity(cameraConfig); const cameraActive = useMemo( @@ -224,9 +224,16 @@ export default function LivePlayer({ /> -
- {activeMotion && ( - +
+ {!offline && activeMotion && ( + + )} + {offline && ( + + {cameraConfig.name.replaceAll("_", " ")} + )}
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 82cda969a..15b35de7f 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -10,11 +10,13 @@ import { useTimelineUtils } from "./use-timeline-utils"; import { ObjectType } from "@/types/ws"; import useDeepMemo from "./use-deep-memo"; import { isEqual } from "lodash"; +import { useAutoFrigateStats } from "./use-stats"; type useCameraActivityReturn = { activeTracking: boolean; activeMotion: boolean; objects: ObjectType[]; + offline: boolean; }; export function useCameraActivity( @@ -116,12 +118,31 @@ export function useCameraActivity( handleSetObjects(newObjects); }, [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 { activeTracking: hasActiveObjects, activeMotion: detectingMotion ? detectingMotion === "ON" : initialCameraState?.motion === true, objects, + offline, }; } diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index bafc9e538..099f057c0 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -9,6 +9,7 @@ import { useMemo } from "react"; import useSWR from "swr"; import useDeepMemo from "./use-deep-memo"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; +import { useFrigateStats } from "@/api/ws"; export default function useStats(stats: FrigateStats | undefined) { const { data: config } = useSWR("config"); @@ -91,3 +92,20 @@ export default function useStats(stats: FrigateStats | undefined) { return { potentialProblems }; } + +export function useAutoFrigateStats() { + const { data: initialStats } = useSWR("stats", { + revalidateOnFocus: false, + }); + const { payload: latestStats } = useFrigateStats(); + + const stats = useMemo(() => { + if (latestStats) { + return latestStats; + } + + return initialStats; + }, [initialStats, latestStats]); + + return stats; +} diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index e54482cf1..58b3f4bdd 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -10,6 +10,7 @@ import { ReviewSegment, ReviewSeverity, ReviewSummary, + SegmentedReviewData, } from "@/types/review"; import { getTimestampOffset } from "@/utils/dateUtil"; import EventView from "@/views/events/EventView"; @@ -138,6 +139,66 @@ export default function Events() { }, ); + 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]); + + 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 const { data: reviewSummary, mutate: updateSummary } = useSWR( @@ -353,7 +414,8 @@ export default function Events() { } else { return ( void; }; export default function EventView({ - reviews, + reviewItems, + currentReviewItems, reviewSummary, relevantPreviews, timeRange, @@ -116,42 +119,6 @@ export default function EventView({ } }, [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 const [selectedReviews, setSelectedReviews] = useState([]); @@ -182,6 +149,7 @@ export default function EventView({ severity: review.severity, }); + review.has_been_reviewed = true; markItemAsReviewed(review); } }, @@ -332,6 +300,7 @@ export default function EventView({ { - 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 const [previewTime, setPreviewTime] = useState();