From 812e5308a3e0a329cee9badec2480d7defb74a94 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:51:44 -0500 Subject: [PATCH] fix notification suspend state lost on page reload (#23989) /notifications/suspended arrives as a string over the live connection but as a number in the camera_activity snapshot, and the truthiness guard dropped the numeric 0, so a camera with notifications off rendered as active after a reload. Normalize to a string and derive isSuspended instead of storing it. --- docs/docs/integrations/mqtt.md | 16 +++++++++++----- web/src/api/ws.ts | 4 ++-- .../NotificationsSettingsExtras.tsx | 12 ++---------- web/src/components/menu/LiveContextMenu.tsx | 11 ++--------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 6a9bdeb324..a7a8740bd5 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -292,7 +292,9 @@ Topic with the currently active profile name. Published value is the profile nam ### `frigate/notifications/set` -Topic to turn notifications on and off. Expected values are `ON` and `OFF`. +Topic to turn notifications on and off for all cameras. Expected values are `ON` and `OFF`. + +Only available when notifications are enabled in the config. Not persisted across Frigate restarts. ### `frigate/notifications/state` @@ -570,16 +572,20 @@ Topic with current state of the Birdseye mode for a camera. Published values are ### `frigate//notifications/set` -Topic to turn notifications on and off. Expected values are `ON` and `OFF`. +Topic to turn notifications for a camera on and off. Expected values are `ON` and `OFF`. + +`ON` is ignored unless notifications are enabled in the config for the camera. This is not persisted across Frigate restarts. It is the same control the UI labels **Suspend until restart**. ### `frigate//notifications/state` -Topic with current state of notifications. Published values are `ON` and `OFF`. +Topic with current state of notifications. Published values are `ON` and `OFF`. This is the authoritative topic for whether a camera will notify. ### `frigate//notifications/suspend` -Topic to suspend notifications for a certain number of minutes. Expected value is an integer. +Topic to suspend notifications for a certain number of minutes. Expected value is an integer. Separate from `notifications/set`: it does not change `notifications/state`, and is ignored while notifications are off. ### `frigate//notifications/suspended` -Topic with timestamp that notifications are suspended until. Published value is a UNIX timestamp, or 0 if notifications are not suspended. +Topic with timestamp that notifications are suspended until. Published value is a UNIX timestamp, or 0 if there is no timed suspension. + +`0` does not mean notifications are enabled: `notifications/set` `OFF` clears the timed suspension, so this publishes `0` while `notifications/state` is `OFF`. diff --git a/web/src/api/ws.ts b/web/src/api/ws.ts index 0a91a88da7..5ac31a40a1 100644 --- a/web/src/api/ws.ts +++ b/web/src/api/ws.ts @@ -205,7 +205,7 @@ function applyCameraActivity(payload: string) { ); applyTopicUpdate( `${name}/notifications/suspended`, - notifications_suspended || 0, + String(notifications_suspended ?? 0), ); applyTopicUpdate( `${name}/ptz_autotracker/state`, @@ -806,7 +806,7 @@ export function useNotificationSuspend(camera: string): { `${camera}/notifications/suspended`, `${camera}/notifications/suspend`, ); - return { payload: payload as string, send }; + return { payload: String(payload ?? 0), send }; } export function useNotificationTest(): { diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index 337f37f23a..2cf5d93c56 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -746,18 +746,10 @@ export function CameraNotificationSwitch({ useNotifications(camera); const { payload: notificationSuspendUntil, send: sendNotificationSuspend } = useNotificationSuspend(camera); - const [isSuspended, setIsSuspended] = useState(false); - - useEffect(() => { - if (notificationSuspendUntil) { - setIsSuspended( - notificationSuspendUntil !== "0" || notificationState === "OFF", - ); - } - }, [notificationSuspendUntil, notificationState]); + const isSuspended = + notificationSuspendUntil !== "0" || notificationState === "OFF"; const handleSuspend = (duration: string) => { - setIsSuspended(true); if (duration == "off") { sendNotification("OFF"); } else { diff --git a/web/src/components/menu/LiveContextMenu.tsx b/web/src/components/menu/LiveContextMenu.tsx index 6aa37ec02c..b6db13ee50 100644 --- a/web/src/components/menu/LiveContextMenu.tsx +++ b/web/src/components/menu/LiveContextMenu.tsx @@ -228,15 +228,8 @@ export default function LiveContextMenu({ useNotifications(camera); const { payload: notificationSuspendUntil, send: sendNotificationSuspend } = useNotificationSuspend(camera); - const [isSuspended, setIsSuspended] = useState(false); - - useEffect(() => { - if (notificationSuspendUntil) { - setIsSuspended( - notificationSuspendUntil !== "0" || notificationState === "OFF", - ); - } - }, [notificationSuspendUntil, notificationState]); + const isSuspended = + notificationSuspendUntil !== "0" || notificationState === "OFF"; const handleSuspend = (duration: string) => { if (duration === "off") {