diff --git a/docs/docs/configuration/notifications.md b/docs/docs/configuration/notifications.md new file mode 100644 index 000000000..bc4882429 --- /dev/null +++ b/docs/docs/configuration/notifications.md @@ -0,0 +1,22 @@ +--- +id: notifications +title: Notifications +--- + +# Notifications + +Frigate offers native notifications using the [WebPush Protocol](https://web.dev/articles/push-notifications-web-push-protocol) which uses the [VAPID spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid) to deliver notifications to web apps using encryption. + +## Configuration + +To configure notifications, go to the Frigate WebUI -> Settings -> Notifications and enable, then fill out the fields and save. + +:::note + +Currently, notifications are only supported in Chrome and Firefox browsers. + +::: + +## Registration + +Once notifications are enabled, press the `Register for Notifications` button on all devices that you would like to receive notifications on. This will register the background worker. After this Frigate must be restarted and then notifications will begin to be sent. \ No newline at end of file diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index fa94c98aa..8c11836b4 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -372,6 +372,14 @@ motion: # Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below). mqtt_off_delay: 30 +# Optional: Notification Configuration +notifications: + # Optional: Enable notification service (default: shown below) + enabled: False + # Optional: Email for push service to reach out to + # NOTE: This is required to use notifications + email: "admin@example.com" + # Optional: Record configuration # NOTE: Can be overridden at the camera level record: @@ -642,8 +650,8 @@ cameras: user: admin # Optional: password for login. password: admin - # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. - # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. + # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. + # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. ignore_time_mismatch: False # Optional: PTZ camera object autotracking. Keeps a moving object in # the center of the frame by automatically moving the PTZ camera. diff --git a/docs/sidebars.js b/docs/sidebars.js index 1e1a27046..9a6ba0df9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -54,6 +54,7 @@ module.exports = { ], "Extra Configuration": [ "configuration/authentication", + "configuration/notifications", "configuration/hardware_acceleration", "configuration/ffmpeg_presets", "configuration/tls", diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index 85f4ac7d6..2320692b3 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -62,7 +62,6 @@ class WebPushClient(Communicator): # type: ignore[misc] ).timestamp(), } self.claim_headers = self.vapid.sign(self.claim) - logger.info(f"Updated claim with new headers {self.claim_headers}") # Only notify for alerts if payload["after"]["severity"] != "alert": @@ -94,8 +93,6 @@ class WebPushClient(Communicator): # type: ignore[misc] direct_url = f"/review?id={reviewId}" image = f'{payload["after"]["thumb_path"].replace("/media/frigate", "")}' - logger.info(f"the image for testing is {image}") - for pusher in self.web_pushers: pusher.send( headers=self.claim_headers, diff --git a/frigate/config.py b/frigate/config.py index 0c801d67d..126d964ff 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -171,9 +171,7 @@ class AuthConfig(FrigateBaseModel): class NotificationConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable notifications") - email: Optional[str] = Field( - default=None, title="Email required for push." - ) + email: Optional[str] = Field(default=None, title="Email required for push.") class StatsConfig(FrigateBaseModel): diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index bc2167484..6d147b332 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -60,17 +60,13 @@ export default function Settings() { const settingsViews = useMemo(() => { const views = [...allSettingsViews]; - if ( - !("Notification" in window) || - !window.isSecureContext || - !config?.notifications.enabled - ) { + if (!("Notification" in window) || !window.isSecureContext) { const index = views.indexOf("notifications"); views.splice(index, 1); } return views; - }, [config]); + }, []); // TODO: confirm leave page const [unsavedChanges, setUnsavedChanges] = useState(false); @@ -200,7 +196,9 @@ export default function Settings() { /> )} {page == "users" && } - {page == "notifications" && } + {page == "notifications" && ( + + )} {confirmationDialogOpen && ( ("config", { - revalidateOnFocus: false, - }); +type NotificationSettingsValueType = { + enabled: boolean; + email?: string; +}; + +type NotificationsSettingsViewProps = { + setUnsavedChanges: React.Dispatch>; +}; +export default function NotificationView({ + setUnsavedChanges, +}: NotificationsSettingsViewProps) { + const { data: config, mutate: updateConfig } = useSWR( + "config", + { + revalidateOnFocus: false, + }, + ); + + // status bar + + const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; // notification key handling @@ -23,6 +59,13 @@ export default function NotificationView() { const subscribeToNotifications = useCallback( (registration: ServiceWorkerRegistration) => { if (registration) { + addMessage( + "notification_settings", + "Unsaved Notification Registrations", + undefined, + "registration", + ); + registration.pushManager .subscribe({ userVisibleOnly: true, @@ -32,10 +75,16 @@ export default function NotificationView() { axios.post("notifications/register", { sub: pushSubscription, }); + toast.success( + "Successfully registered for notifications. Restart to start receiving notifications.", + { + position: "top-center", + }, + ); }); } }, - [publicKey], + [publicKey, addMessage], ); // notification state @@ -58,6 +107,78 @@ export default function NotificationView() { }); }, []); + // form + + const [isLoading, setIsLoading] = useState(false); + const formSchema = z.object({ + enabled: z.boolean(), + email: z.string(), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + enabled: config?.notifications.enabled, + email: config?.notifications.email, + }, + }); + + const onCancel = useCallback(() => { + if (!config) { + return; + } + + setUnsavedChanges(false); + form.reset({ + enabled: config.notifications.enabled, + email: config.notifications.email || "", + }); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config, removeMessage, setUnsavedChanges]); + + const saveToConfig = useCallback( + async ( + { enabled, email }: NotificationSettingsValueType, // values submitted via the form + ) => { + axios + .put( + `config/set?notifications.enabled=${enabled}¬ifications.email=${email}`, + { + requires_restart: 0, + }, + ) + .then((res) => { + if (res.status === 200) { + toast.success("Notification settings have been saved.", { + position: "top-center", + }); + updateConfig(); + } else { + toast.error(`Failed to save config changes: ${res.statusText}`, { + position: "top-center", + }); + } + }) + .catch((error) => { + toast.error( + `Failed to save config changes: ${error.response.data.message}`, + { position: "top-center" }, + ); + }) + .finally(() => { + setIsLoading(false); + }); + }, + [updateConfig, setIsLoading], + ); + + function onSubmit(values: z.infer) { + setIsLoading(true); + saveToConfig(values as NotificationSettingsValueType); + } + return ( <>
@@ -67,13 +188,85 @@ export default function NotificationView() { Notification Settings +
+ + ( + + +
+ + { + return field.onChange(checked); + }} + /> +
+
+
+ )} + /> + ( + + Loitering Time + + + + + Entering a valid email is required, as this is used by the + push server in case problems occur. + + + + )} + /> +
+ + +
+ + + {config?.notifications.enabled && ( -
+
- { - // TODO need to register the worker before enabling the notifications button - // TODO make the notifications button show enable / disable depending on current state - } +