-
Notifications
You must be signed in to change notification settings - Fork 72.8k
Alarm sockets for api v3 #7858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Alarm sockets for api v3 #7858
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ef7c921
Alarm sockets for api v3
MilosKozak 4d69c9a
Migrate to alarm websockets
MilosKozak e0d061a
Merge branch 'dev' into milos/alarmsocket
MilosKozak ca7c5f2
Fix unit tests
sulkaharo f3069ed
Merge branch 'dev' into milos/alarmsocket
sulkaharo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| 'use strict'; | ||
|
|
||
| const apiConst = require('./const'); | ||
| const forwarded = require('forwarded-for'); | ||
|
|
||
| function getRemoteIP (req) { | ||
| const address = forwarded(req, req.headers); | ||
| return address.ip; | ||
| } | ||
|
|
||
| /** | ||
| * Socket.IO broadcaster of alarm and annoucements | ||
| */ | ||
| function AlarmSocket (app, env, ctx) { | ||
|
|
||
| const self = this; | ||
|
|
||
| var levels = ctx.levels; | ||
|
|
||
| const LOG_GREEN = '\x1B[32m' | ||
| , LOG_MAGENTA = '\x1B[35m' | ||
| , LOG_RESET = '\x1B[0m' | ||
| , LOG = LOG_GREEN + 'ALARM SOCKET: ' + LOG_RESET | ||
| , LOG_ERROR = LOG_MAGENTA + 'ALARM SOCKET: ' + LOG_RESET | ||
| , NAMESPACE = '/alarm' | ||
| ; | ||
|
|
||
|
|
||
| /** | ||
| * Initialize socket namespace and bind the events | ||
| * @param {Object} io Socket.IO object to multiplex namespaces | ||
| */ | ||
| self.init = function init (io) { | ||
| self.io = io; | ||
|
|
||
| self.namespace = io.of(NAMESPACE); | ||
| self.namespace.on('connection', function onConnected (socket) { | ||
|
|
||
| const remoteIP = getRemoteIP(socket.request); | ||
| console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); | ||
|
|
||
| socket.on('disconnect', function onDisconnect () { | ||
| console.log(LOG + 'Disconnected client ID: ', socket.client.id); | ||
| }); | ||
|
|
||
| socket.on('subscribe', function onSubscribe (message, returnCallback) { | ||
| self.subscribe(socket, message, returnCallback); | ||
| }); | ||
|
|
||
| }); | ||
|
|
||
| ctx.bus.on('notification', self.emitNotification); | ||
| }; | ||
|
|
||
|
|
||
| /** | ||
| * Authorize Socket.IO client and subscribe him to authorized rooms | ||
| * | ||
| * Support webclient authorization with api_secret is added | ||
| * | ||
| * @param {Object} socket | ||
| * @param {Object} message input message from the client | ||
| * @param {Function} returnCallback function for returning a value back to the client | ||
| */ | ||
| self.subscribe = function subscribe (socket, message, returnCallback) { | ||
| const shouldCallBack = typeof(returnCallback) === 'function'; | ||
|
|
||
| // Native client | ||
| if (message && message.accessToken) { | ||
| return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinishForToken (err, auth) { | ||
| if (err) { | ||
| console.log(`${LOG_ERROR} Authorization failed for accessToken:`, message.accessToken); | ||
|
|
||
| if (shouldCallBack) { | ||
| returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); | ||
| } | ||
| return err; | ||
| } else { | ||
| // Subscribe for acking alarms | ||
| socket.on('ack', function onAck (level, group, silenceTime) { | ||
| ctx.notifications.ack(level, group, silenceTime, true); | ||
| console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); | ||
| }); | ||
|
|
||
| var okResponse = { success: true, message: 'Subscribed for alarms' } | ||
| if (shouldCallBack) { | ||
| returnCallback(okResponse); | ||
| } | ||
| return okResponse; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Web client (jwt access token or api_hash) | ||
| if (message && (message.jwtToken || message.secret)) { | ||
| return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) { | ||
| if (err) { | ||
| console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken); | ||
|
|
||
| if (shouldCallBack) { | ||
| returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); | ||
| } | ||
| return err; | ||
| } else { | ||
| // Subscribe for acking alarms | ||
| socket.on('ack', function onAck (level, group, silenceTime) { | ||
| ctx.notifications.ack(level, group, silenceTime, true); | ||
| console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); | ||
| }); | ||
|
|
||
| var okResponse = { success: true, message: 'Subscribed for alarms' } | ||
| if (shouldCallBack) { | ||
| returnCallback(okResponse); | ||
| } | ||
| return okResponse; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| console.log(`${LOG_ERROR} Authorization failed for message:`, message); | ||
| if (shouldCallBack) { | ||
| returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN}); | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| /** | ||
| * Emit alarm to subscribed clients | ||
| * @param {Object} notofication to emit | ||
| */ | ||
|
|
||
| self.emitNotification = function emitNotification (notify) { | ||
| if (notify.clear) { | ||
| self.namespace.emit('clear_alarm', notify); | ||
| console.info(LOG + 'emitted clear_alarm to all clients'); | ||
| } else if (notify.level === levels.WARN) { | ||
| self.namespace.emit('alarm', notify); | ||
| console.info(LOG + 'emitted alarm to all clients'); | ||
| } else if (notify.level === levels.URGENT) { | ||
| self.namespace.emit('urgent_alarm', notify); | ||
| console.info(LOG + 'emitted urgent_alarm to all clients'); | ||
| } else if (notify.isAnnouncement) { | ||
| self.namespace.emit('announcement', notify); | ||
| console.info(LOG + 'emitted announcement to all clients'); | ||
| } else { | ||
| self.namespace.emit('notification', notify); | ||
| console.info(LOG + 'emitted notification to all clients'); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| module.exports = AlarmSocket; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| # APIv3: Socket.IO alarm channel | ||
|
|
||
| ### Complete sample client code | ||
| ```html | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta http-equiv="x-ua-compatible" content="ie=edge" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
|
|
||
| <title>APIv3 Socket.IO sample for alarms</title> | ||
|
|
||
| <link rel="icon" href="images/favicon.png" /> | ||
| </head> | ||
|
|
||
| <body> | ||
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script> | ||
|
|
||
| <script> | ||
| const socket = io('https://nsapiv3.herokuapp.com/alarm'); | ||
|
|
||
| socket.on('connect', function () { | ||
| socket.emit('subscribe', { | ||
| accessToken: 'testadmin-ad3b1f9d7b3f59d5' | ||
| }, function (data) { | ||
| if (data.success) { | ||
| console.log('subscribed for alarms', data.message); | ||
| } | ||
| else { | ||
| console.error(data.message); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| socket.on('announcement', function (data) { | ||
| console.log(data); | ||
| }); | ||
|
|
||
| socket.on('alarm', function (data) { | ||
| console.log(data); | ||
| }); | ||
|
|
||
| socket.on('urgent_alarm', function (data) { | ||
| console.log(data); | ||
| }); | ||
|
|
||
| socket.on('clear_alarm', function (data) { | ||
| console.log(data); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
|
|
||
| ### Subscription (authorization) | ||
| The client must first subscribe to the channel that is exposed at `alarm` namespace, ie the `/alarm` subadress of the base Nightscout's web address (without `/api/v3` subaddress). | ||
| ```javascript | ||
| const socket = io('https://nsapiv3.herokuapp.com/alarm'); | ||
| ``` | ||
|
|
||
|
|
||
| Subscription is requested by emitting `subscribe` event to the server, while including document with parameter: | ||
| * `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout. | ||
|
|
||
| ```javascript | ||
| socket.on('connect', function () { | ||
| socket.emit('subscribe', { | ||
| accessToken: 'testadmin-ad3b1f9d7b3f59d5' | ||
| }, ... | ||
| ``` | ||
|
|
||
|
|
||
| On the server, the subject is identified and authenticated (by the accessToken). Ne special rights are required. | ||
|
|
||
| If the authentication was successful `success` = `true` is set in the response object and the field `message` contains a text response. | ||
| In other case `success` = `false` is set in the response object and the field `message` contains an error message. | ||
|
|
||
| ```javascript | ||
| function (data) { | ||
| if (data.success) { | ||
| console.log('subscribed for alarms', data.message); | ||
| } | ||
| else { | ||
| console.error(data.message); | ||
| } | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Acking alarms and announcements | ||
| If the client is successfully subscribed it can ack alarms and announcements by emitting `ack` message. | ||
|
|
||
| ```javascript | ||
| socket.emit('ack', level, group, silenceTimeInMilliseconds); | ||
| ``` | ||
|
|
||
| where `level` and `group` are values from alarm being acked and `silenceTimeInMilliseconds` is duration. During this time alarms of the same type are not emmited. | ||
|
|
||
| ### Receiving events | ||
| After the successful subscription the client can start listening to `announcement`, `alarm` , `urgent_alarm` and/or `clear_alarm` events of the socket. | ||
|
|
||
|
|
||
| ##### announcement | ||
|
|
||
| The received object contains similiar json: | ||
|
|
||
| ```javascript | ||
| { | ||
| "level":0, | ||
| "title":"Announcement", | ||
| "message":"test", | ||
| "plugin":{"name":"treatmentnotify","label":"Treatment Notifications","pluginType":"notification","enabled":true}, | ||
| "group":"Announcement", | ||
| "isAnnouncement":true, | ||
| "key":"9ac46ad9a1dcda79dd87dae418fce0e7955c68da" | ||
| } | ||
| ``` | ||
|
|
||
|
|
||
| ##### alarm, urgent_alarm | ||
|
|
||
| The received object contains similiar json: | ||
|
|
||
| ```javascript | ||
| { | ||
| "level":1, | ||
| "title":"Warning HIGH", | ||
| "message":"BG Now: 5 -0.2 → mmol\/L\nRaw BG: 4.8 mmol\/L Čistý\nBG 15m: 4.8 mmol\/L\nIOB: -0.02U\nCOB: 0g", | ||
| "eventName":"high", | ||
| "plugin":{"name":"simplealarms","label":"Simple Alarms","pluginType":"notification","enabled":true}, | ||
| "pushoverSound":"climb", | ||
| "debug":{"lastSGV":5,"thresholds":{"bgHigh":180,"bgTargetTop":75,"bgTargetBottom":72,"bgLow":70}}, | ||
| "group":"default", | ||
| "key":"simplealarms_1" | ||
| } | ||
| ``` | ||
|
|
||
|
|
||
| ##### clear_alarm | ||
|
|
||
| The received object contains similiar json: | ||
|
|
||
| ```javascript | ||
| { | ||
| "clear":true, | ||
| "title":"All Clear", | ||
| "message":"default - Urgent was ack'd", | ||
| "group":"default" | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this copied to multiple places. :( I can clean this after the merge