forked from nightscout/cgm-remote-monitor
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Alarm sockets for api v3 (nightscout#7858)
* Alarm sockets for api v3 * Migrate to alarm websockets * Fix unit tests --------- Co-authored-by: Sulka Haro <sulka@sulka.net>
- Loading branch information
1 parent
4e1f364
commit 89d7eb6
Showing
6 changed files
with
360 additions
and
55 deletions.
There are no files selected for viewing
This file contains 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 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 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.