UNPKG

iobroker.nightscout

Version:

Provides nightscout server and client for sugar monitoring

258 lines (219 loc) 11 kB
const io = require('socket.io-client'); const https = require('https'); const util = require('util'); const EventEmitter = require('events').EventEmitter; const moment = require('moment-timezone'); function NightscoutClient(adapter, URL, secretHash) { if (adapter.config.secure) { https.globalAgent.options.rejectUnauthorized = false; } this.URL = URL || `http${adapter.config.secure ? 's' : ''}://${adapter.config.bind}:${adapter.config.port}`; this.previousNotifyTimestamp = null; if (!secretHash && adapter.config.remoteSecret) { const shaSum = require('crypto').createHash('sha1'); shaSum.update(adapter.config.remoteSecret); secretHash = shaSum.digest('hex'); } else { secretHash = secretHash || null; } this.secretHash = secretHash; this.nsSocket = io(this.URL, { path: '/socket.io', agent: adapter.config.secure ? https.globalAgent : undefined }); this.nsSocket.on('disconnect', () => { this.emit('connection', false); adapter.log.debug('[CLIENT] own client disconnected'); }); this.nsSocket.on('connect', () => { this.previousNotifyTimestamp = null; adapter.log.debug('[CLIENT] own client connected'); this.emit('connection', true); this.nsSocket.emit('authorize', { client: 'iobroker', secret: this.secretHash, history: 48, }, data => { if (!data.read) { adapter.log.error(`Cannot authenticate: ${JSON.stringify(data)}`); } }); }); this.previousNotifyTimestamp = null; this.nsSocket.on('notification', data => { adapter.log.debug(`[CLIENT] notification: ${JSON.stringify(data)}`); if (data) { if (data.timestamp && data.timestamp === this.previousNotifyTimestamp) { // ignore return; } if (data.timestamp) { this.previousNotifyTimestamp = data.timestamp; } const ts = new Date(data.timestamp || undefined).getTime(); adapter.setState('data.notification', {ts: ts || Date.now(), ack: true, val: `${data.title} ${data.message}`}); } }); this.nsSocket.on('announcement', data => adapter.log.debug(`[CLIENT] announcement: ${data}`)); this.nsSocket.on('alarm', data => { adapter.log.debug(`[CLIENT] alarm: ${JSON.stringify(data)}`); adapter.setState('data.alarm', data && data.level && data.level === 1, true); }); this.nsSocket.on('urgent_alarm', data => { adapter.log.debug(`[CLIENT] urgentAlarm: ${JSON.stringify(data)}`); adapter.setState('data.urgentAlarm', data && data.level && data.level === 2, true); }); this.nsSocket.on('clear_alarm', data => { adapter.log.debug(`[CLIENT] clear_alarm: ${JSON.stringify(data)}`); if (data.clear) { adapter.setState('data.alarm', false, true); adapter.setState('data.urgentAlarm', false, true); } }); this.nsSocket.on('dataUpdate', dataUpdate => { adapter.log.debug(`[CLIENT] dataUpdate: ${JSON.stringify(dataUpdate)}`); try { adapter.setState('data.rawUpdate', JSON.stringify(dataUpdate), true); if (dataUpdate) { const now = Date.now(); const ts = dataUpdate.lastUpdated || now; if (dataUpdate.lastUpdated) { adapter.setState('data.lastUpdate', dataUpdate.lastUpdated, true); } if (dataUpdate.devicestatus && dataUpdate.devicestatus.length) { const status = dataUpdate.devicestatus.pop(); adapter.setState('data.device', status.device, true); if (status.pump) { adapter.setState('data.clock', {ts, ack: true, val: new Date(status.pump.clock).getTime()}); adapter.setState('data.reservoir', {ts, ack: true, val: status.pump.reservoir}); status.pump.iob && adapter.setState('data.bolusiob', {ts, ack: true, val: status.pump.iob.bolusiob}); status.pump.battery && adapter.setState('data.pumpBattery', {ts, ack: true, val: status.pump.battery.percent}); if (status.pump.status) { adapter.setState('data.bolusing', {ts, ack: true, val: status.pump.status.bolusing}); adapter.setState('data.status', {ts, ack: true, val: status.pump.status.status}); adapter.setState('data.suspended', {ts, ack: true, val: status.pump.status.suspended}); } } status.uploader && adapter.setState('data.uploaderBattery', status.uploader.battery, true); } if (dataUpdate.sgvs && dataUpdate.sgvs.length) { const sgv = dataUpdate.sgvs.pop(); adapter.setState('data.mgdl', {ts: sgv.mills, ack: true, val: sgv.mgdl}); adapter.setState('data.mgdlScaled', {ts: sgv.mills, ack: true, val: sgv.scaled}); adapter.setState('data.mgdlDirection', {ts: sgv.mills, ack: true, val: sgv.direction}); } let siteChangeUpdated = false; let sensorUpdated = false; if (dataUpdate.treatments && dataUpdate.treatments.length) { const sitechangeTreatments = dataUpdate.treatments.filter(treatment => treatment.eventType && treatment.eventType.includes('Site Change')); const cannulaInfo = this._getLatestTreatmentsInfo(sitechangeTreatments); if (cannulaInfo.found) { adapter.setState('data.cage.age', {ts: now, ack: true, val: cannulaInfo.age}); adapter.setState('data.cage.days', {ts: now, ack: true, val: cannulaInfo.days}); adapter.setState('data.cage.hours', {ts: now, ack: true, val: cannulaInfo.hours}); adapter.setState('data.cage.changed', { ts: cannulaInfo.millis, ack: true, val: new Date(cannulaInfo.millis).getTime(), }); siteChangeUpdated = true; } const sensorTreatments = dataUpdate.treatments.filter(treatment => treatment.eventType && ( treatment.eventType.includes('Sensor Start') || treatment.eventType.includes('Sensor Change') ) ); const sensorInfo = this._getLatestTreatmentsInfo(sensorTreatments); if (sensorInfo.found) { adapter.setState('data.sage.age', {ts: now, ack: true, val: sensorInfo.age}); adapter.setState('data.sage.days', {ts: now, ack: true, val: sensorInfo.days}); adapter.setState('data.sage.hours', {ts: now, ack: true, val: sensorInfo.hours}); adapter.setState('data.sage.changed', { ts: sensorInfo.millis, ack: true, val: new Date(sensorInfo.millis).getTime() }); sensorUpdated = true; } } if (dataUpdate.food && dataUpdate.food.length) { const sitechangeTreatments = dataUpdate.food.filter(treatment => treatment.eventType && treatment.eventType.includes('Site Change')); // todo: process food } if (!siteChangeUpdated) { adapter.log.debug('[CLIENT] No cannula treatments found. Recalculate age'); this._recalcAges('data.cage'); } if (!sensorUpdated) { adapter.log.debug('[CLIENT] No sensor treatments found. Recalculate age'); this._recalcAges('data.sage'); } } } catch (error) { adapter.log.error(`[CLIENT] Parse Error: ${error}`); } }); this.nsSocket.on('retroUpdate', function retroUpdate (retroData) { adapter.log.debug('[CLIENT] retroUpdate', retroData); }); this.close = () => { if (this.nsSocket) { this.nsSocket.close(); this.nsSocket = null; } }; this._getLatestTreatmentsInfo = (treatments) => { const treatmentInfo = { found: false, age: 0, days: 0, hours: 0, millis: 0 }; if (treatments.length) { const now = Date.now(), a = moment(now); let prevDate = 0; treatments.forEach(treatment => { const treatmentDate = treatment.mills; if (treatmentDate > prevDate && treatmentDate <= now) { prevDate = treatmentDate; const b = moment(treatmentDate); const days = a.diff(b, 'days'); const hours = a.diff(b, 'hours') - days * 24; const age = a.diff(b, 'hours'); if (!treatmentInfo.found || (age >= 0 && age < treatmentInfo.age)) { treatmentInfo.found = true; treatmentInfo.age = age; treatmentInfo.days = days; treatmentInfo.hours = hours; treatmentInfo.millis = treatmentDate; } } }); } return treatmentInfo; }; this._recalcAges = baseId => adapter.getStateAsync(`${baseId}.changed`) .then((state) => { if (state) { const now = Date.now(), a = moment(now); const treatmentDate = state.val; const b = moment(treatmentDate); const days = a.diff(b, 'days'); const hours = a.diff(b, 'hours') - days * 24; const age = a.diff(b, 'hours'); adapter.setState(`${baseId}.age`, {ts: now, ack: true, val: age}); adapter.setState(`${baseId}.days`, {ts: now, ack: true, val: days}); adapter.setState(`${baseId}.hours`, {ts: now, ack: true, val: hours}); } }); } // extend the EventEmitter class using our class util.inherits(NightscoutClient, EventEmitter); module.exports = NightscoutClient;