mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-22 08:57:20 -06:00
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:
parent
63d81bef45
commit
c2eac10925
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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<FrigateStats>("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;
|
||||
|
@ -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}
|
||||
/>
|
||||
) : (
|
||||
<InProgressPreview
|
||||
review={event}
|
||||
timeRange={{
|
||||
after: event.start_time,
|
||||
before: event.end_time ?? event.start_time + 20,
|
||||
}}
|
||||
<video
|
||||
src={`${baseUrl}api/review/${event.id}/preview?format=ts`}
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
showProgress={false}
|
||||
setReviewed={() => {}}
|
||||
setIgnoreClick={() => {}}
|
||||
isPlayingBack={() => {}}
|
||||
windowVisible={windowVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-2 top-2 size-4">
|
||||
{activeMotion && (
|
||||
<MdCircle className="size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
|
||||
<div className="absolute right-2 top-2">
|
||||
{!offline && activeMotion && (
|
||||
<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>
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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<FrigateConfig>("config");
|
||||
@ -91,3 +92,20 @@ export default function useStats(stats: FrigateStats | undefined) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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<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
|
||||
|
||||
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
|
||||
@ -353,7 +414,8 @@ export default function Events() {
|
||||
} else {
|
||||
return (
|
||||
<EventView
|
||||
reviews={reviews}
|
||||
reviewItems={reviewItems}
|
||||
currentReviewItems={currentItems}
|
||||
reviewSummary={reviewSummary}
|
||||
relevantPreviews={allPreviews}
|
||||
timeRange={selectedTimeRange}
|
||||
|
@ -20,6 +20,15 @@ export type ReviewData = {
|
||||
zones: string[];
|
||||
};
|
||||
|
||||
export type SegmentedReviewData =
|
||||
| {
|
||||
all: ReviewSegment[];
|
||||
alert: ReviewSegment[];
|
||||
detection: ReviewSegment[];
|
||||
significant_motion: ReviewSegment[];
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export type ReviewFilter = {
|
||||
cameras?: string[];
|
||||
labels?: string[];
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
ReviewSegment,
|
||||
ReviewSeverity,
|
||||
ReviewSummary,
|
||||
SegmentedReviewData,
|
||||
} from "@/types/review";
|
||||
import { getChunkedTimeRange } from "@/utils/timelineUtil";
|
||||
import axios from "axios";
|
||||
@ -49,7 +50,8 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type EventViewProps = {
|
||||
reviews?: ReviewSegment[];
|
||||
reviewItems?: SegmentedReviewData;
|
||||
currentReviewItems: ReviewSegment[] | null;
|
||||
reviewSummary?: ReviewSummary;
|
||||
relevantPreviews?: Preview[];
|
||||
timeRange: TimeRange;
|
||||
@ -64,7 +66,8 @@ type EventViewProps = {
|
||||
updateFilter: (filter: ReviewFilter) => 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<string[]>([]);
|
||||
@ -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({
|
||||
<DetectionReview
|
||||
contentRef={contentRef}
|
||||
reviewItems={reviewItems}
|
||||
currentItems={currentReviewItems}
|
||||
relevantPreviews={relevantPreviews}
|
||||
selectedReviews={selectedReviews}
|
||||
itemsToReview={reviewCounts[severityToggle]}
|
||||
@ -372,6 +341,7 @@ type DetectionReviewProps = {
|
||||
detection: ReviewSegment[];
|
||||
significant_motion: ReviewSegment[];
|
||||
};
|
||||
currentItems: ReviewSegment[] | null;
|
||||
itemsToReview?: number;
|
||||
relevantPreviews?: Preview[];
|
||||
selectedReviews: string[];
|
||||
@ -388,6 +358,7 @@ type DetectionReviewProps = {
|
||||
function DetectionReview({
|
||||
contentRef,
|
||||
reviewItems,
|
||||
currentItems,
|
||||
itemsToReview,
|
||||
relevantPreviews,
|
||||
selectedReviews,
|
||||
@ -405,33 +376,6 @@ function DetectionReview({
|
||||
|
||||
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
|
||||
|
||||
const [previewTime, setPreviewTime] = useState<number>();
|
||||
|
Loading…
Reference in New Issue
Block a user