nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
621 lines (541 loc) • 20.9 kB
JavaScript
;
var times = require('../times');
var calcData = require('../data/calcdelta');
var ObjectID = require('mongodb').ObjectID;
const forwarded = require('forwarded-for');
function init (env, ctx, server) {
function websocket () {
return websocket;
}
var levels = ctx.levels;
//var log_yellow = '\x1B[33m';
var log_green = '\x1B[32m';
var log_magenta = '\x1B[35m';
var log_reset = '\x1B[0m';
var LOG_WS = log_green + 'WS: ' + log_reset;
var LOG_DEDUP = log_magenta + 'DEDUPE: ' + log_reset;
var io;
var watchers = 0;
var lastData = {};
var lastProfileSwitch = null;
// TODO: this would be better to have somehow integrated/improved
var supportedCollections = {
'treatments': env.treatments_collection
, 'entries': env.entries_collection
, 'devicestatus': env.devicestatus_collection
, 'profile': env.profile_collection
, 'food': env.food_collection
, 'activity': env.activity_collection
};
// This is little ugly copy but I was unable to pass testa after making module from status and share with /api/v1/status
function status () {
var versionNum = 0;
const vString = '' + env.version;
const verParse = vString.split('.');
if (verParse) {
versionNum = 10000 * Number(verParse[0]) + 100 * Number(verParse[1]) + 1 * Number(verParse[2]);
}
var apiEnabled = env.enclave.isApiKeySet();
var activeProfile = ctx.ddata.lastProfileFromSwitch;
var info = {
status: 'ok'
, name: env.name
, version: env.version
, versionNum: versionNum
, serverTime: new Date().toISOString()
, apiEnabled: apiEnabled
, careportalEnabled: apiEnabled && env.settings.enable.indexOf('careportal') > -1
, boluscalcEnabled: apiEnabled && env.settings.enable.indexOf('boluscalc') > -1
, settings: env.settings
, extendedSettings: ctx.plugins && ctx.plugins.extendedClientSettings ? ctx.plugins.extendedClientSettings(env.extendedSettings) : {}
};
if (activeProfile) {
info.activeProfile = activeProfile;
}
return info;
}
function start () {
io = require('socket.io')({
'transports': ['xhr-polling']
, 'log level': 0
}).listen(server, {
//these only effect the socket.io.js file that is sent to the client, but better than nothing
'browser client minification': true
, 'browser client etag': true
, 'browser client gzip': false
});
ctx.bus.on('teardown', function serverTeardown () {
Object.keys(io.sockets.sockets).forEach(function(s) {
io.sockets.sockets[s].disconnect(true);
});
io.close();
});
}
function verifyAuthorization (message, ip, callback) {
if (!message) message = {};
ctx.authorization.resolve({ api_secret: message.secret, token: message.token, ip: ip }, function resolved (err, result) {
if (err) {
return callback(err, {
read: false
, write: false
, write_treatment: false
, error: true
});
}
return callback(null, {
read: ctx.authorization.checkMultiple('api:*:read', result.shiros)
, write: ctx.authorization.checkMultiple('api:*:create,update,delete', result.shiros)
, write_treatment: ctx.authorization.checkMultiple('api:treatments:create,update,delete', result.shiros)
});
});
}
function emitData (delta) {
if (lastData.cals) {
// console.log(LOG_WS + 'running websocket.emitData', ctx.ddata.lastUpdated);
if (lastProfileSwitch !== ctx.ddata.lastProfileFromSwitch) {
// console.log(LOG_WS + 'profile switch detected OLD: ' + lastProfileSwitch + ' NEW: ' + ctx.ddata.lastProfileFromSwitch);
delta.status = status(ctx.ddata.profiles);
lastProfileSwitch = ctx.ddata.lastProfileFromSwitch;
}
io.to('DataReceivers').emit('dataUpdate', delta);
}
}
function listeners () {
io.sockets.on('connection', function onConnection (socket) {
var socketAuthorization = null;
var clientType = null;
var timeDiff;
var history;
const address = forwarded(socket.request, socket.request.headers);
const remoteIP = address.ip;
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);
});
function checkConditions (action, data) {
var collection = supportedCollections[data.collection];
if (!collection) {
console.log('WS dbUpdate/dbAdd call: ', 'Wrong collection', data);
return { result: 'Wrong collection' };
}
if (!socketAuthorization) {
console.log('WS dbUpdate/dbAdd call: ', 'Not authorized', data);
return { result: 'Not authorized' };
}
if (data.collection === 'treatments') {
if (!socketAuthorization.write_treatment) {
console.log('WS dbUpdate/dbAdd call: ', 'Not permitted', data);
return { result: 'Not permitted' };
}
} else {
if (!socketAuthorization.write) {
console.log('WS dbUpdate call: ', 'Not permitted', data);
return { result: 'Not permitted' };
}
}
if (action === 'dbUpdate' && !data._id) {
console.log('WS dbUpdate/dbAddnot sure abou documentati call: ', 'Missing _id', data);
return { result: 'Missing _id' };
}
return null;
}
socket.on('loadRetro', function loadRetro (opts, callback) {
if (callback) {
callback({ result: 'success' });
}
//TODO: use opts to only send delta for retro data
socket.emit('retroUpdate', { devicestatus: lastData.devicestatus });
console.info('sent retroUpdate', opts);
});
// dbUpdate message
// {
// collection: treatments
// _id: 'some mongo record id'
// data: {
// field_1: new_value,
// field_2: another_value
// }
// }
socket.on('dbUpdate', function dbUpdate (data, callback) {
console.log(LOG_WS + 'dbUpdate client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection];
var check = checkConditions('dbUpdate', data);
if (check) {
if (callback) {
callback(check);
}
return;
}
var id;
try {
id = new ObjectID(data._id);
} catch (err) {
console.error(err);
id = new ObjectID();
}
ctx.store.collection(collection).update({ '_id': id }
, { $set: data.data }
, function(err, results) {
if (!err) {
ctx.store.collection(collection).findOne({ '_id': id }
, function(err, results) {
console.log('Got results', results);
if (!err) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime([results])
});
}
});
}
}
);
if (callback) {
callback({ result: 'success' });
}
ctx.bus.emit('data-received');
});
// dbUpdateUnset message
// {
// collection: treatments
// _id: 'some mongo record id'
// data: {
// field_1: 1,
// field_2: 1
// }
// }
socket.on('dbUpdateUnset', function dbUpdateUnset (data, callback) {
console.log(LOG_WS + 'dbUpdateUnset client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection];
var check = checkConditions('dbUpdate', data);
if (check) {
if (callback) {
callback(check);
}
return;
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).update({ '_id': objId }, { $unset: data.data }
, function(err, results) {
if (!err) {
ctx.store.collection(collection).findOne({ '_id': objId }
, function(err, results) {
console.log('Got results', results);
if (!err) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime([results])
});
}
});
}
});
if (callback) {
callback({ result: 'success' });
}
ctx.bus.emit('data-received');
});
// dbAdd message
// {
// collection: treatments
// data: {
// field_1: new_value,
// field_2: another_value
// }
// }
socket.on('dbAdd', function dbAdd (data, callback) {
console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection];
var maxtimediff = times.mins(1).msecs;
var check = checkConditions('dbAdd', data);
if (check) {
if (callback) {
callback(check);
}
return;
}
if (data.collection === 'treatments' && !('eventType' in data.data)) {
data.data.eventType = '<none>';
}
if (!('created_at' in data.data)) {
data.data.created_at = new Date().toISOString();
}
// treatments deduping
if (data.collection === 'treatments') {
var query;
if (data.data.NSCLIENT_ID) {
query = { NSCLIENT_ID: data.data.NSCLIENT_ID };
} else {
query = {
created_at: data.data.created_at
, eventType: data.data.eventType
};
}
// try to find exact match
ctx.store.collection(collection).find(query).toArray(function findResult (err, array) {
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Exact match');
if (callback) {
callback([array[0]]);
}
return;
}
var selected = false;
var query_similiar = {
created_at: { $gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString() }
};
if (data.data.insulin) {
query_similiar.insulin = data.data.insulin;
selected = true;
}
if (data.data.carbs) {
query_similiar.carbs = data.data.carbs;
selected = true;
}
if (data.data.percent) {
query_similiar.percent = data.data.percent;
selected = true;
}
if (data.data.absolute) {
query_similiar.absolute = data.data.absolute;
selected = true;
}
if (data.data.duration) {
query_similiar.duration = data.data.duration;
selected = true;
}
if (data.data.NSCLIENT_ID) {
query_similiar.NSCLIENT_ID = data.data.NSCLIENT_ID;
selected = true;
}
// if none assigned add at least eventType
if (!selected) {
query_similiar.eventType = data.data.eventType;
}
// try to find similiar
ctx.store.collection(collection).find(query_similiar).toArray(function findSimiliarResult (err, array) {
// if found similiar just update date. next time it will match exactly
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Found similiar', array[0]);
array[0].created_at = data.data.created_at;
var objId = new ObjectID(array[0]._id);
ctx.store.collection(collection).update({ '_id': objId }, { $set: { created_at: data.data.created_at } });
if (callback) {
callback([array[0]]);
}
ctx.bus.emit('data-received');
return;
}
// if not found create new record
console.log(LOG_DEDUP + 'Adding new record');
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log('treatments data insertion error: ', err.message);
return;
}
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime(doc.ops)
});
if (callback) {
callback(doc.ops);
}
ctx.bus.emit('data-received');
});
});
});
// devicestatus deduping
} else if (data.collection === 'devicestatus') {
var queryDev;
if (data.data.NSCLIENT_ID) {
queryDev = { NSCLIENT_ID: data.data.NSCLIENT_ID };
} else {
queryDev = {
created_at: data.data.created_at
};
}
// try to find exact match
ctx.store.collection(collection).find(queryDev).toArray(function findResult (err, array) {
if (err) {
console.error(err);
callback([]);
return;
}
if (array.length > 0) {
console.log(LOG_DEDUP + 'Devicestatus exact match');
if (callback) {
callback([array[0]]);
}
return;
}
});
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log('devicestatus insertion error: ', err.message);
return;
}
ctx.bus.emit('data-update', {
type: 'devicestatus'
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime(doc.ops)
});
if (callback) {
callback(doc.ops);
}
ctx.bus.emit('data-received');
});
} else {
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log(data.collection + ' insertion error: ', err.message);
return;
}
ctx.bus.emit('data-update', {
type: data.collection
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime(doc.ops)
});
if (callback) {
callback(doc.ops);
}
ctx.bus.emit('data-received');
});
}
});
// dbRemove message
// {
// collection: treatments
// _id: 'some mongo record id'
// }
socket.on('dbRemove', function dbRemove (data, callback) {
console.log(LOG_WS + 'dbRemove client ID: ', socket.client.id, ' data: ', data);
var collection = supportedCollections[data.collection];
var check = checkConditions('dbUpdate', data);
if (check) {
if (callback) {
callback(check);
}
return;
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).remove({ '_id': objId }
, function(err, stat) {
if (!err) {
ctx.bus.emit('data-update', {
type: data.collection
, op: 'remove'
, count: stat.result.n
, changes: data._id
});
}
});
if (callback) {
callback({ result: 'success' });
}
ctx.bus.emit('data-received');
});
// Authorization message
// {
// client: 'web' | 'phone' | 'pump'
// , secret: 'secret_hash'
// [, history : history_in_hours ]
// [, status : true ]
// }
socket.on('authorize', function authorize (message, callback) {
const remoteIP = socket.request.connection.remoteAddress;
verifyAuthorization(message, remoteIP, function verified (err, authorization) {
if (err) {
console.log('Websocket authorization failed:', err);
socket.disconnect();
return;
}
socket.emit('connected');
socketAuthorization = authorization;
clientType = message.client;
history = message.history || 48; //default history is 48 hours
if (socketAuthorization.read) {
socket.join('DataReceivers');
if (lastData && lastData.dataWithRecentStatuses) {
let data = lastData.dataWithRecentStatuses();
if (message.status) {
data.status = status(data.profiles);
}
socket.emit('dataUpdate', data);
}
}
// console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history);
if (callback) {
callback(socketAuthorization);
}
});
});
// Pind message
// {
// mills: <local_time_in_milliseconds>
// }
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 () {
// console.log(LOG_WS + 'running websocket.update');
if (lastData.sgvs) {
var delta = calcData(lastData, ctx.ddata);
if (delta.delta) {
// console.log('lastData full size', JSON.stringify(lastData).length,'bytes');
// if (delta.sgvs) { console.log('patientData update size', JSON.stringify(delta).length,'bytes'); }
emitData(delta);
}; // else { console.log('delta calculation indicates no new data is present'); }
}
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();
if (ctx.storageSocket) {
ctx.storageSocket.init(io);
}
return websocket();
}
module.exports = init;