diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js
new file mode 100644
index 00000000000..5fe30a620c1
--- /dev/null
+++ b/lib/api3/alarmSocket.js
@@ -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;
diff --git a/lib/api3/doc/alarmsockets.md b/lib/api3/doc/alarmsockets.md
new file mode 100644
index 00000000000..54d86e93a97
--- /dev/null
+++ b/lib/api3/doc/alarmsockets.md
@@ -0,0 +1,151 @@
+# APIv3: Socket.IO alarm channel
+
+### Complete sample client code
+```html
+
+
+
+
+
+
+
+ APIv3 Socket.IO sample for alarms
+
+
+
+
+
+
+
+
+
+
+```
+
+### 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"
+ }
+```
\ No newline at end of file
diff --git a/lib/api3/index.js b/lib/api3/index.js
index 83db322a452..2ba0aa762b1 100644
--- a/lib/api3/index.js
+++ b/lib/api3/index.js
@@ -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')
@@ -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;
}
diff --git a/lib/client/index.js b/lib/client/index.js
index fb1d17d1430..bee6134e947 100644
--- a/lib/client/index.js
+++ b/lib/client/index.js
@@ -153,6 +153,7 @@ client.load = function load (serverSettings, callback) {
var chart
, socket
+ , alarmSocket
, isInitialData = false
, opacity = { current: 1, DAY: 1, NIGHT: 0.5 }
, clientAlarms = {}
@@ -811,7 +812,7 @@ client.load = function load (serverSettings, callback) {
// only emit ack if client invoke by button press
if (isClient && currentNotify) {
- socket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
+ alarmSocket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
}
currentNotify = null;
@@ -1041,6 +1042,7 @@ client.load = function load (serverSettings, callback) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* global io */
client.socket = socket = io.connect({ transports: ["polling"] });
+ client.alarmSocket = alarmSocket = io.connect("/alarm", { multiplex: true, transports: ["polling"] });
socket.on('dataUpdate', dataUpdate);
@@ -1127,6 +1129,41 @@ client.load = function load (serverSettings, callback) {
client.authorizeSocket();
});
+ client.subscribeForAlarms = function subscribeForAlarms () {
+
+ var auth_data = {
+ secret: client.authorized && client.authorized.token ? null : client.hashauth.hash()
+ , jwtToken: client.authorized && client.authorized.token
+ };
+
+ alarmSocket.emit(
+ 'subscribe'
+ , auth_data
+ , function subscribeCallback (data) {
+ if (!data) {
+ console.log('Crashed!');
+ client.crashed();
+ }
+
+ console.log('Subscribed for alarms', data);
+ if (!data.success) {
+ client.hashauth.requestAuthentication(function afterRequest () {
+ client.hashauth.updateSocketAuth();
+ if (callback) {
+ callback();
+ }
+ });
+ } else if (callback) {
+ callback();
+ }
+ }
+ );
+ }
+
+ alarmSocket.on('connect', function() {
+ client.subscribeForAlarms();
+ });
+
function hasRequiredPermission () {
if (client.requiredPermission) {
if (client.hashauth && client.hashauth.isAuthenticated()) {
@@ -1151,7 +1188,7 @@ client.load = function load (serverSettings, callback) {
return client.latestSGV && client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop;
}
- socket.on('notification', function(notify) {
+ alarmSocket.on('notification', function(notify) {
console.log('notification from server:', notify);
if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) {
previousNotifyTimestamp = notify.timestamp;
@@ -1161,14 +1198,14 @@ client.load = function load (serverSettings, callback) {
}
});
- socket.on('announcement', function(notify) {
+ alarmSocket.on('announcement', function(notify) {
console.info('announcement received from server');
currentAnnouncement = notify;
currentAnnouncement.received = Date.now();
updateTitle();
});
- socket.on('alarm', function(notify) {
+ alarmSocket.on('alarm', function(notify) {
console.info('alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow);
if (enabled) {
@@ -1180,7 +1217,7 @@ client.load = function load (serverSettings, callback) {
chart.update(false);
});
- socket.on('urgent_alarm', function(notify) {
+ alarmSocket.on('urgent_alarm', function(notify) {
console.info('urgent alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow);
if (enabled) {
@@ -1192,7 +1229,7 @@ client.load = function load (serverSettings, callback) {
chart.update(false);
});
- socket.on('clear_alarm', function(notify) {
+ alarmSocket.on('clear_alarm', function(notify) {
if (alarmInProgress) {
console.log('clearing alarm');
stopAlarm(false, null, notify);
diff --git a/lib/server/server.js b/lib/server/server.js
index ea75d5ac636..49efaeb9394 100644
--- a/lib/server/server.js
+++ b/lib/server/server.js
@@ -70,14 +70,6 @@ require('./bootevent')(env, language).boot(function booted (ctx) {
///////////////////////////////////////////////////
var websocket = require('./websocket')(env, ctx, server);
- ctx.bus.on('data-processed', function() {
- websocket.update();
- });
-
- ctx.bus.on('notification', function(notify) {
- websocket.emitNotification(notify);
- });
-
//after startup if there are no alarms send all clear
let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () {
var alarm = ctx.notifications.findHighestAlarm();
diff --git a/lib/server/websocket.js b/lib/server/websocket.js
index 08ae6d662c0..2924db0554d 100644
--- a/lib/server/websocket.js
+++ b/lib/server/websocket.js
@@ -16,8 +16,6 @@ function init (env, ctx, server) {
return websocket;
}
- var levels = ctx.levels;
-
//var log_yellow = '\x1B[33m';
var log_green = '\x1B[32m';
var log_magenta = '\x1B[35m';
@@ -95,6 +93,11 @@ function init (env, ctx, server) {
});
io.close();
});
+
+ ctx.bus.on('data-processed', function() {
+ update();
+ });
+
}
function verifyAuthorization (message, ip, callback) {
@@ -143,10 +146,6 @@ function init (env, ctx, server) {
console.log(LOG_WS + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP);
io.emit('clients', ++watchers);
- socket.on('ack', function onAck (level, group, silenceTime) {
- ctx.notifications.ack(level, group, silenceTime, true);
- });
-
socket.on('disconnect', function onDisconnect () {
io.emit('clients', --watchers);
console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id);
@@ -569,23 +568,10 @@ function init (env, ctx, server) {
}
});
});
-
- // Pind message
- // {
- // mills:
- // }
- socket.on('nsping', function ping (message, callback) {
- var clientTime = message.mills;
- timeDiff = new Date().getTime() - clientTime;
- // console.log(LOG_WS + 'Ping from client ID: ',socket.client.id, ' client: ', clientType, ' timeDiff: ', (timeDiff/1000).toFixed(1) + 'sec');
- if (callback) {
- callback({ result: 'pong', mills: new Date().getTime(), authorization: socketAuthorization });
- }
- });
});
}
- websocket.update = function update () {
+ function update () {
// console.log(LOG_WS + 'running websocket.update');
if (lastData.sgvs) {
var delta = calcData(lastData, ctx.ddata);
@@ -598,25 +584,6 @@ function init (env, ctx, server) {
lastData = ctx.ddata.clone();
};
- websocket.emitNotification = function emitNotification (notify) {
- if (notify.clear) {
- io.emit('clear_alarm', notify);
- console.info(LOG_WS + 'emitted clear_alarm to all clients');
- } else if (notify.level === levels.WARN) {
- io.emit('alarm', notify);
- console.info(LOG_WS + 'emitted alarm to all clients');
- } else if (notify.level === levels.URGENT) {
- io.emit('urgent_alarm', notify);
- console.info(LOG_WS + 'emitted urgent_alarm to all clients');
- } else if (notify.isAnnouncement) {
- io.emit('announcement', notify);
- console.info(LOG_WS + 'emitted announcement to all clients');
- } else {
- io.emit('notification', notify);
- console.info(LOG_WS + 'emitted notification to all clients');
- }
- };
-
start();
listeners();
@@ -624,6 +591,10 @@ function init (env, ctx, server) {
ctx.storageSocket.init(io);
}
+ if (ctx.alarmSocket) {
+ ctx.alarmSocket.init(io);
+ }
+
return websocket();
}