Adjustments and fixes (#10346)

* Increase duration of alerts and detections

* Add key

* Fix cancel button

* Fix motion review when switching days

* Add reset buttons and make calendar apply immediately

* Adjust apis for motion and audio activity

* Write review thumbs as webp and reduce size
This commit is contained in:
Nicolas Mowen 2024-03-09 07:08:06 -07:00 committed by GitHub
parent eeb2187b97
commit 62d13024f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 111 additions and 50 deletions

View File

@ -353,8 +353,8 @@ def delete_reviews(ids: str):
return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200) return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200)
@ReviewBp.route("/review/activity") @ReviewBp.route("/review/activity/motion")
def review_activity(): def motion_activity():
"""Get motion and audio activity.""" """Get motion and audio activity."""
cameras = request.args.get("cameras", "all") cameras = request.args.get("cameras", "all")
before = request.args.get("before", type=float, default=datetime.now().timestamp()) before = request.args.get("before", type=float, default=datetime.now().timestamp())
@ -374,6 +374,68 @@ def review_activity():
Recordings.duration, Recordings.duration,
Recordings.objects, Recordings.objects,
Recordings.motion, Recordings.motion,
)
.where(reduce(operator.and_, clauses))
.order_by(Recordings.start_time.asc())
.iterator()
)
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
# periods where active objects / audio was detected will cause motion to be scaled down
data: list[dict[str, float]] = []
for rec in all_recordings:
data.append(
{
"start_time": rec.start_time,
"motion": rec.motion if rec.objects == 0 else 0,
}
)
# get scale in seconds
scale = request.args.get("scale", type=int, default=30)
# resample data using pandas to get activity on scaled basis
df = pd.DataFrame(data, columns=["start_time", "motion"])
# set date as datetime index
df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
df.set_index(["start_time"], inplace=True)
# normalize data
df = df.resample(f"{scale}S").sum().fillna(0.0)
df["motion"] = (
(df["motion"] - df["motion"].min())
/ (df["motion"].max() - df["motion"].min())
* 100
)
# change types for output
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
return jsonify(normalized)
@ReviewBp.route("/review/activity/audio")
def audio_activity():
"""Get motion and audio activity."""
cameras = request.args.get("cameras", "all")
before = request.args.get("before", type=float, default=datetime.now().timestamp())
after = request.args.get(
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
)
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((Recordings.camera << camera_list))
all_recordings: list[Recordings] = (
Recordings.select(
Recordings.start_time,
Recordings.duration,
Recordings.objects,
Recordings.dBFS, Recordings.dBFS,
) )
.where(reduce(operator.and_, clauses)) .where(reduce(operator.and_, clauses))
@ -382,14 +444,13 @@ def review_activity():
) )
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] } # format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
# periods where active objects / audio was detected will cause motion / audio to be scaled down # periods where active objects / audio was detected will cause audio to be scaled down
data: list[dict[str, float]] = [] data: list[dict[str, float]] = []
for rec in all_recordings: for rec in all_recordings:
data.append( data.append(
{ {
"start_time": rec.start_time, "start_time": rec.start_time,
"motion": rec.motion if rec.objects == 0 else 0,
"audio": rec.dBFS if rec.objects == 0 else 0, "audio": rec.dBFS if rec.objects == 0 else 0,
} }
) )
@ -398,7 +459,7 @@ def review_activity():
scale = request.args.get("scale", type=int, default=30) scale = request.args.get("scale", type=int, default=30)
# resample data using pandas to get activity on scaled basis # resample data using pandas to get activity on scaled basis
df = pd.DataFrame(data, columns=["start_time", "motion", "audio"]) df = pd.DataFrame(data, columns=["start_time", "audio"])
# set date as datetime index # set date as datetime index
df["start_time"] = pd.to_datetime(df["start_time"], unit="s") df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
@ -406,11 +467,6 @@ def review_activity():
# normalize data # normalize data
df = df.resample(f"{scale}S").mean().fillna(0.0) df = df.resample(f"{scale}S").mean().fillna(0.0)
df["motion"] = (
(df["motion"] - df["motion"].min())
/ (df["motion"].max() - df["motion"].min())
* 100
)
df["audio"] = ( df["audio"] = (
(df["audio"] - df["audio"].max()) (df["audio"] - df["audio"].max())
/ (df["audio"].min() - df["audio"].max()) / (df["audio"].min() - df["audio"].max())

View File

@ -28,6 +28,10 @@ logger = logging.getLogger(__name__)
THUMB_HEIGHT = 180 THUMB_HEIGHT = 180
THUMB_WIDTH = 320 THUMB_WIDTH = 320
THRESHOLD_ALERT_ACTIVITY = 120
THRESHOLD_DETECTION_ACTIVITY = 30
THRESHOLD_MOTION_ACTIVITY = 30
class SeverityEnum(str, Enum): class SeverityEnum(str, Enum):
alert = "alert" alert = "alert"
@ -100,7 +104,7 @@ class PendingReviewSegment:
path = os.path.join(CLIPS_DIR, f"thumb-{self.camera}-{self.id}.jpg") path = os.path.join(CLIPS_DIR, f"thumb-{self.camera}-{self.id}.jpg")
if self.frame is not None: if self.frame is not None:
cv2.imwrite(path, self.frame) cv2.imwrite(path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60])
return { return {
ReviewSegment.id: self.id, ReviewSegment.id: self.id,
@ -195,15 +199,16 @@ class ReviewSegmentMaintainer(threading.Thread):
if len(object["current_zones"]) > 0: if len(object["current_zones"]) > 0:
segment.zones.update(object["current_zones"]) segment.zones.update(object["current_zones"])
elif ( elif (
segment.severity == SeverityEnum.signification_motion and len(motion) >= 20 segment.severity == SeverityEnum.signification_motion
and len(motion) >= THRESHOLD_MOTION_ACTIVITY
): ):
segment.last_update = frame_time segment.last_update = frame_time
else: else:
if segment.severity == SeverityEnum.alert and frame_time > ( if segment.severity == SeverityEnum.alert and frame_time > (
segment.last_update + 60 segment.last_update + THRESHOLD_ALERT_ACTIVITY
): ):
self.end_segment(segment) self.end_segment(segment)
elif frame_time > (segment.last_update + 10): elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY):
self.end_segment(segment) self.end_segment(segment)
def check_if_new_segment( def check_if_new_segment(

View File

@ -222,7 +222,7 @@ function NewGroupDialog({ open, setOpen, currentGroups }: NewGroupDialogProps) {
<DialogContent className="min-w-0 w-96"> <DialogContent className="min-w-0 w-96">
<DialogTitle>Camera Groups</DialogTitle> <DialogTitle>Camera Groups</DialogTitle>
{currentGroups.map((group) => ( {currentGroups.map((group) => (
<div className="flex justify-between items-center"> <div key={group[0]} className="flex justify-between items-center">
{group[0]} {group[0]}
<div className="flex justify-center gap-1"> <div className="flex justify-center gap-1">
<Button <Button

View File

@ -212,7 +212,7 @@ function CamerasFilterButton({
))} ))}
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex justify-center items-center"> <div className="flex justify-evenly items-center">
<Button <Button
variant="select" variant="select"
onClick={() => { onClick={() => {
@ -222,6 +222,15 @@ function CamerasFilterButton({
> >
Apply Apply
</Button> </Button>
<Button
variant="secondary"
onClick={() => {
setCurrentCameras(undefined);
updateCameraFilter(undefined);
}}
>
Reset
</Button>
</div> </div>
</> </>
); );
@ -271,8 +280,6 @@ function CalendarFilterButton({
day, day,
updateSelectedDay, updateSelectedDay,
}: CalendarFilterButtonProps) { }: CalendarFilterButtonProps) {
const [open, setOpen] = useState(false);
const [selectedDay, setSelectedDay] = useState(day);
const disabledDates = useMemo(() => { const disabledDates = useMemo(() => {
const tomorrow = new Date(); const tomorrow = new Date();
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
@ -298,22 +305,21 @@ function CalendarFilterButton({
<Calendar <Calendar
mode="single" mode="single"
disabled={disabledDates} disabled={disabledDates}
selected={selectedDay} selected={day}
showOutsideDays={false} showOutsideDays={false}
onSelect={(day) => { onSelect={(day) => {
setSelectedDay(day); updateSelectedDay(day);
}} }}
/> />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Button <Button
variant="select" variant="secondary"
onClick={() => { onClick={() => {
updateSelectedDay(selectedDay); updateSelectedDay(undefined);
setOpen(false);
}} }}
> >
Apply Reset
</Button> </Button>
</div> </div>
</> </>
@ -321,16 +327,7 @@ function CalendarFilterButton({
if (isMobile) { if (isMobile) {
return ( return (
<Drawer <Drawer>
open={open}
onOpenChange={(open) => {
if (!open) {
setSelectedDay(day);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger> <DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent>{content}</DrawerContent> <DrawerContent>{content}</DrawerContent>
</Drawer> </Drawer>
@ -338,16 +335,7 @@ function CalendarFilterButton({
} }
return ( return (
<Popover <Popover>
open={open}
onOpenChange={(open) => {
if (!open) {
setSelectedDay(day);
}
setOpen(open);
}}
>
<PopoverTrigger asChild>{trigger}</PopoverTrigger> <PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent> <PopoverContent>{content}</PopoverContent>
</Popover> </Popover>
@ -433,7 +421,7 @@ function GeneralFilterButton({
))} ))}
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex justify-center items-center"> <div className="flex justify-evenly items-center">
<Button <Button
variant="select" variant="select"
onClick={() => { onClick={() => {
@ -450,6 +438,17 @@ function GeneralFilterButton({
> >
Apply Apply
</Button> </Button>
<Button
variant="secondary"
onClick={() => {
setReviewed(0);
setShowReviewed(undefined);
setCurrentLabels(undefined);
updateLabelFilter(undefined);
}}
>
Reset
</Button>
</div> </div>
</> </>
); );

View File

@ -107,7 +107,7 @@ export default function SubmitPlus() {
alt={`${upload?.label}`} alt={`${upload?.label}`}
/> />
<DialogFooter> <DialogFooter>
<Button>Cancel</Button> <Button onClick={() => setUpload(undefined)}>Cancel</Button>
<Button <Button
className="bg-success" className="bg-success"
onClick={() => onSubmitToPlus(false)} onClick={() => onSubmitToPlus(false)}

View File

@ -44,6 +44,6 @@ export type ReviewSummary = {
export type MotionData = { export type MotionData = {
start_time: number; start_time: number;
motion: number; motion?: number;
audio: number; audio?: number;
}; };

View File

@ -260,6 +260,7 @@ export default function EventView({
)} )}
{severity == "significant_motion" && ( {severity == "significant_motion" && (
<MotionReview <MotionReview
key={timeRange.before}
contentRef={contentRef} contentRef={contentRef}
reviewItems={reviewItems} reviewItems={reviewItems}
relevantPreviews={relevantPreviews} relevantPreviews={relevantPreviews}
@ -563,7 +564,7 @@ function MotionReview({
// motion data // motion data
const { data: motionData } = useSWR<MotionData[]>([ const { data: motionData } = useSWR<MotionData[]>([
"review/activity", "review/activity/motion",
{ {
before: timeRange.before, before: timeRange.before,
after: timeRange.after, after: timeRange.after,
@ -598,7 +599,7 @@ function MotionReview({
const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex); const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex);
const [currentTime, setCurrentTime] = useState<number>( const [currentTime, setCurrentTime] = useState<number>(
startTime ?? timeRangeSegments.ranges[selectedRangeIdx].start, startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.start,
); );
const currentTimeRange = useMemo( const currentTimeRange = useMemo(
() => timeRangeSegments.ranges[selectedRangeIdx], () => timeRangeSegments.ranges[selectedRangeIdx],