Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions lib/api3/alarmSocket.js
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) {
Copy link
Member

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

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;
151 changes: 151 additions & 0 deletions lib/api3/doc/alarmsockets.md
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"
}
```
6 changes: 4 additions & 2 deletions lib/api3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
const express = require('express')
, bodyParser = require('body-parser')
, renderer = require('./shared/renderer')
, StorageSocket = require('./storageSocket')
, storageSocket = require('./storageSocket')
, alarmSocket = require('./alarmSocket')
, apiConst = require('./const.json')
, security = require('./security')
, genericSetup = require('./generic/setup')
Expand Down Expand Up @@ -108,7 +109,8 @@ function configure (env, ctx) {
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION);
})

ctx.storageSocket = new StorageSocket(app, env, ctx);
ctx.storageSocket = new storageSocket(app, env, ctx);
ctx.alarmSocket = new alarmSocket(app, env, ctx);

return app;
}
Expand Down
Loading