From 202402fbc42c08f058174fd35bb97bd9823ee273 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Fri, 6 Sep 2019 17:40:46 -0600 Subject: [PATCH 001/134] Copied work done by mdomox --- README.md | 4 ++ docs/plugins/googlehome-plugin.md | 34 +++++++++ lib/api/googlehome/index.js | 115 ++++++++++++++++++++++++++++++ lib/api/index.js | 4 ++ lib/plugins/basalprofile.js | 14 ++++ lib/plugins/cob.js | 19 +++++ lib/plugins/googlehome.js | 59 +++++++++++++++ lib/plugins/iob.js | 15 ++++ lib/plugins/openaps.js | 24 +++++++ lib/server/bootevent.js | 4 ++ 10 files changed, 292 insertions(+) create mode 100644 docs/plugins/googlehome-plugin.md create mode 100644 lib/api/googlehome/index.js create mode 100644 lib/plugins/googlehome.js diff --git a/README.md b/README.md index fd85b987107..b6d12295d03 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Community maintained fork of the - [`override` (Override Mode)](#override-override-mode) - [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) @@ -511,6 +512,9 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ##### `alexa` (Amazon Alexa) Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) +##### `googlehome` (Google Home/DialogFLow) + Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md) + ##### `speech` (Speech) Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms. diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md new file mode 100644 index 00000000000..6bc55b01e62 --- /dev/null +++ b/docs/plugins/googlehome-plugin.md @@ -0,0 +1,34 @@ +Nightscout Google Home/DialogFlow Plugin +======================================== + +## Overview + +To add Google Home support for your Nightscout site, here's what you need to do: + +1. Activate the `googlehome` plugin on your Nightscout site, so your site will respond correctly to Google's requests. +2. Create a custom DialogFlow agent that points at your site and defines certain questions you want to be able to ask. (You'll copy and paste a basic template for this, to keep things simple.) +3. Create desired integrations with DialogFlow + + +## Activate the Nightscout Google Home Plugin + +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. . +2. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) + +## Create Your DialogFlow Agent + +### Signin to DialogFlow + +- Sign in to DialogFlow with your Google account (https://console.dialogflow.com/api-client/#/login). If you don't already have one, signup with Google. + +### Create a new custom DialogFlow agent + +1. Select "Create new agent" in the main menu bar. +2. Input a custom name for your agent and click "CREATE". +3. Download the simple agent template : ( https://drive.google.com/drive/folders/18z2kQSEInvH4O_jfjB4Qh8z9508P9Oao?usp=sharing ) +4. Select IMPORT FROM ZIP , in order to import the template. +5. SAVE +6. Go to "Fullfillment" menu and enter details about your webhook. +7. SAVE +8. Go to "Integration" menu and select your desired integration. +9. Follow instructions for each desired integration. \ No newline at end of file diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js new file mode 100644 index 00000000000..a833b4abab3 --- /dev/null +++ b/lib/api/googlehome/index.js @@ -0,0 +1,115 @@ +'use strict'; + +var _ = require('lodash'); +var moment = require('moment'); + +function configure (app, wares, ctx, env) { + var express = require('express'); + var api = express.Router(); + var entries = ctx.entries; + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.plugins.eachEnabledPlugin(function each(plugin) { + if (plugin.googleHome) { + if (plugin.googleHome.intentHandlers) { + console.log('Plugin ' + plugin.name + ' is Google Home enabled'); + _.each(plugin.googleHome.intentHandlers, function (handler) { + if (handler) { + ctx.googleHome.configureIntentHandler(handler.intent, handler.intentHandler, handler.routableSlot, handler.slots); + } + }); + } + } else { + console.log('Plugin ' + plugin.name + ' is not Google Home enabled'); + } + }); + + ctx.googleHome.configureIntentHandler('CurrentMetric', function (result, next, sbx) { + entries.list({count: 1}, function(err, records) { + var response = ''; + if (records && records.length > 0) { + var direction = ''; + if (records[0].direction === 'FortyFiveDown') { + direction = ' and slightly dropping'; + } else if (records[0].direction === 'FortyFiveUp') { + direction = ' and slightly rising'; + } else if (records[0].direction === 'Flat') { + direction = ' and holding'; + } else if (records[0].direction === 'SingleUp') { + direction = ' and rising'; + } else if (records[0].direction === 'SingleDown') { + direction = ' and dropping'; + } else if (records[0].direction === 'DoubleDown') { + direction = ' and rapidly dropping'; + } else if (records[0].direction === 'DoubleUp') { + direction = ' and rapidly rising'; + } + response = buildPreamble(result.parameters); + response += sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); + } else { + response = buildPreamble(result.parameters) + 'unknown'; + } + next(response); + }); + }, 'metric', ['bg', 'blood glucose', 'blood sugar', 'number']); + + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + onIntent(req.body, function (response) { + res.json(ctx.googleHome.buildResponse(response)); + next(); + }); + }); + + function buildPreamble(parameters) { + var preamble = ''; + if (parameters && parameters.givenName) { + preamble = parameters.givenName + '\'s current '; + } else { + preamble = 'Your current '; + } + if (parameters && parameters.readingType) { + preamble += parameters.readingType + ' is '; + } else { + preamble += 'blood glucose is '; + } + return preamble; + } + + function onIntent(body, next) { + console.log('Received intent request'); + console.log(JSON.stringify(body)); + handleIntent(body, next); + } + + // https://docs.api.ai/docs/webhook#section-format-of-request-to-the-service + function handleIntent(body, next) { + var displayName = body.queryResult.intent.displayName; + var metric = body.queryResult.parameters ? body.queryResult.parameters.metric : null; + var handler = ctx.googleHome.getIntentHandler(displayName, metric); + if (handler) { + var sbx = initializeSandbox(); + handler(body.queryResult, next, sbx); + } else { + next('I\'m sorry I don\'t know what you\'re asking for'); + } + } + + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } + + return api; +} + +module.exports = configure; \ No newline at end of file diff --git a/lib/api/index.js b/lib/api/index.js index 47a8a7bac3d..943ee2b6b14 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -65,6 +65,10 @@ function create (env, ctx) { app.all('/alexa*', require('./alexa/')(app, wares, ctx, env)); } + if (ctx.googleHome) { + app.all('/googlehome*', require('./googlehome/')(app, wares, ctx, env)); + } + return app; } diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index db47e2bf279..b1e97d816be 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -156,6 +156,20 @@ function init (ctx) { }] }; + function googleHomeCurrentBasalhandler (result, next, sbx) { + var pwd = result.parameters && result.parameters.givenName ? result.parameters.givenName : null; + next(basalMessage(pwd, sbx)); + } + + basal.googleHome = { + intentHandlers: [{ + intent: 'CurrentMetric' + , routableSlot:'metric' + , slots:['basal', 'current basal'] + , intentHandler: googleHomeCurrentBasalhandler + }] + }; + return basal; } diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index bc769197c2b..7bd5b422b26 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -308,6 +308,25 @@ function init (ctx) { }] }; + function googleHomeCOBHandler(result, next, sbx) { + var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has' : 'You have'; + var value = 'no'; + if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { + value = Math.round(sbx.properties.cob.cob); + } + var response = preamble + ' ' + value + ' carbohydrates on board'; + next(response); + } + + cob.googleHome = { + intentHandlers: [{ + intent: 'CurrentMetric' + , routableSlot:'metric' + , slots:['cob', 'carbs on board', 'carbohydrates on board', 'carbohydrates'] + , intentHandler: googleHomeCOBHandler + }] + }; + return cob; } diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js new file mode 100644 index 00000000000..42da3f14b14 --- /dev/null +++ b/lib/plugins/googlehome.js @@ -0,0 +1,59 @@ + +function init(env, ctx) { + + console.log('Configuring Google Home...'); + + function googleHome() { + return googleHome; + } + + var intentHandlers = {}; + + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; + } + if (routableSlot && slotValues) { + for (var i = 0, len = slotValues.length; i < len; i++) { + if (!intentHandlers[intent][routableSlot]) { + intentHandlers[intent][routableSlot] = {}; + } + if (!intentHandlers[intent][routableSlot][slotValues[i]]) { + intentHandlers[intent][routableSlot][slotValues[i]] = {}; + } + intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + } + } else { + intentHandlers[intent].handler = handler; + } + }; + + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + if (intentName && intentHandlers[intentName]) { + if (metric && intentHandlers[intentName]['metric'] && + intentHandlers[intentName]['metric'][metric] && + intentHandlers[intentName]['metric'][metric].handler) { + return intentHandlers[intentName]['metric'][metric].handler; + } else if (intentHandlers[intentName].handler) { + return intentHandlers[intentName].handler; + } else { + return null; + } + } else { + return null; + } + + }; + + googleHome.buildResponse = function buildResponse(output) { + return { + fulfillmentText: output + // , fulfillmentMessages: [output] + , source: 'Nightscout' + }; + }; + + return googleHome; + } + + module.exports = init; \ No newline at end of file diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index f9bf082d0f4..f4f1d5e05cc 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -288,6 +288,21 @@ function init(ctx) { }] }; + function googleHomeIOBIntentHandler(result, next, sbx) { + var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has ' : 'You have '; + var message = preamble + getIob(sbx) + ' insulin on board'; + next(message); + } + + iob.googleHome = { + intentHandlers: [{ + intent: 'CurrentMetric' + , routableSlot: 'metric' + , slots: ['iob', 'insulin on board', 'insulin'] + , intentHandler: googleHomeIOBIntentHandler + }] + }; + return iob; } diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 27b7a31ae95..1e03ce218ce 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -543,6 +543,30 @@ function init (ctx) { }] }; + function googleHomeForecastHandler(response, next, sbx) { + if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { + var response = 'The Open APS eventual BG is ' + sbx.properties.openaps.lastEventualBG; + next(response); + } + } + + function googleHomeLastLoopHandler (response, next, sbx) { + var response = 'The last successful loop was ' + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)); + next(response); + } + + openaps.googleHome = { + intentHandlers: [{ + intent: 'CurrentMetric' + , routableSlot: 'metric' + , slots: ['openaps', 'openaps forecast', 'forecast'] + , intentHandler: googleHomeForecastHandler + }, { + intent: 'LastLoop' + , intentHandler: googleHomeLastLoopHandler + }] + }; + function statusClass (prop, prefs, sbx) { var level = statusLevel(prop, prefs, sbx); return levels.toStatusClass(level); diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 9e53c4f77ac..11eb624720f 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -182,6 +182,10 @@ function boot (env, language) { ctx.alexa = require('../plugins/alexa')(env, ctx); } + if (env.settings.isEnabled('googlehome')) { + ctx.googleHome = require('../plugins/googlehome')(env, ctx); + } + next( ); } From e36a6f1e162fd8340ca594f875c75133f6ec16d1 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Fri, 6 Sep 2019 17:55:08 -0600 Subject: [PATCH 002/134] Updates and fixes to CONTRIBUTING.md --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ede1c7e88c8..e00200d5713 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -202,13 +202,13 @@ Also if you can't code, it's possible to contribute by improving the documentati | Release coordination 0.11.x: | [@PieterGit] | | Issue/Pull request coordination: | Please volunteer | | Cleaning up git fork spam: | Please volunteer | -| Documentation writers: | [@andrew-warrington][@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | +| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | ### Plugin contributors | Contribution area | List of developers | List of testers | ------------------------------------- | -------------------- | -------------------- | -| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| Please volunteer | Please volunteer | +| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| [@inventor96] | Please volunteer | | [`ar2` (AR2 Forecasting)](README.md#ar2-ar2-forecasting)| Please volunteer | Please volunteer | | [`basal` (Basal Profile)](README.md#basal-basal-profile)| Please volunteer | Please volunteer | | [`boluscalc` (Bolus Wizard)](README.md#boluscalc-bolus-wizard)| Please volunteer | Please volunteer | @@ -223,7 +223,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | | [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | | [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | -| [`googlehome` (Google Home)](README.md#google-home) |[@mdomox] [@rickfriele] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`googlehome` (Google Home/DialogFLow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | | [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | | [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | | [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | @@ -234,7 +234,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | | [`sage` (Sensor Age)](README.md#sage-sensor-age)| @jpcunningh | Please volunteer | | [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | -| [`speech` (Speech)](README.md#speech-speech) | [@sulkaharo] | Please volunteer | +| [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | | [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | | [`treatmentnotify` (Treatment Notifications)](README.md#treatmentnotify-treatment-notifications)| Please volunteer | Please volunteer | | [`upbat` (Uploader Battery)](README.md#upbat-uploader-battery)| [@jpcunningh] | Please volunteer | @@ -251,7 +251,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Čeština (`cs`) |Please volunteer|OK | | Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK | | Dansk (`dk`) | [@janrpn] |OK | -| Ελληνικά `(el`)|Please volunteer|Needs attention: 68.5%| +| Ελληνικά `el`)|Please volunteer|Needs attention: 68.5%| | English (`en`)|Please volunteer|OK| | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| From 93f8eb9d6590b1cacda07729f17dcd06267b46ea Mon Sep 17 00:00:00 2001 From: inventor96 Date: Fri, 6 Sep 2019 18:10:47 -0600 Subject: [PATCH 003/134] Spacing unification --- lib/language.js | 210 ++++++++++++++++++++++++------------------------ 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/lib/language.js b/lib/language.js index b7c826d3a73..55b80756418 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13378,32 +13378,32 @@ function init() { , zh_cn: '并且你有 %1 的活性胰岛素.' , zh_tw: 'and you have %1 insulin on board.' }, - 'alexaIobIntent': { - bg: 'Máte %1 jednotek aktivního inzulínu' - , cs: 'You have %1 insulin on board' - , de: 'Du hast noch %1 Insulin wirkend' - , dk: 'Du har %1 insulin i kroppen' - , el: 'You have %1 insulin on board' - , en: 'You have %1 insulin on board' - , es: 'Tienes %1 insulina activa' - , fi: 'Sinulla on %1 aktiivista insuliinia' - , fr: 'You have %1 insulin on board' - , he: 'You have %1 insulin on board' - , hr: 'You have %1 insulin on board' - , it: 'Tu hai %1 insulina attiva' - , ko: 'You have %1 insulin on board' - , nb: 'You have %1 insulin on board' - , pl: 'Masz %1 aktywnej insuliny' - , pt: 'You have %1 insulin on board' - , ro: 'Aveți %1 insulină activă' - , ru: 'вы имеете %1 инсулина в организме' - , sk: 'You have %1 insulin on board' - , sv: 'You have %1 insulin on board' - , nl: 'You have %1 insulin on board' - , tr: 'Sizde %1 aktif insülin var' - , zh_cn: '你有 %1 的活性胰岛素' - , zh_tw: 'You have %1 insulin on board' - }, + 'alexaIobIntent': { + bg: 'Máte %1 jednotek aktivního inzulínu' + , cs: 'You have %1 insulin on board' + , de: 'Du hast noch %1 Insulin wirkend' + , dk: 'Du har %1 insulin i kroppen' + , el: 'You have %1 insulin on board' + , en: 'You have %1 insulin on board' + , es: 'Tienes %1 insulina activa' + , fi: 'Sinulla on %1 aktiivista insuliinia' + , fr: 'You have %1 insulin on board' + , he: 'You have %1 insulin on board' + , hr: 'You have %1 insulin on board' + , it: 'Tu hai %1 insulina attiva' + , ko: 'You have %1 insulin on board' + , nb: 'You have %1 insulin on board' + , pl: 'Masz %1 aktywnej insuliny' + , pt: 'You have %1 insulin on board' + , ro: 'Aveți %1 insulină activă' + , ru: 'вы имеете %1 инсулина в организме' + , sk: 'You have %1 insulin on board' + , sv: 'You have %1 insulin on board' + , nl: 'You have %1 insulin on board' + , tr: 'Sizde %1 aktif insülin var' + , zh_cn: '你有 %1 的活性胰岛素' + , zh_tw: 'You have %1 insulin on board' + }, 'alexaIobUnits': { bg: '%1 units of' , cs: '%1 jednotek' @@ -13508,74 +13508,74 @@ function init() { , zh_cn: '否' , zh_tw: 'no' }, - 'alexaUploadBattery': { - bg: 'Your uploader battery is at %1' - ,cs: 'Baterie mobilu má %1' - , en: 'Your uploader battery is at %1' - , hr: 'Your uploader battery is at %1' - , de: 'Der Akku deines Uploader Handys ist bei %1' - , dk: 'Din uploaders batteri er %1' - , ko: 'Your uploader battery is at %1' - , nl: 'De batterij van je mobiel is bij %l' - ,zh_cn: '你的手机电池电量是 %1 ' - , sv: 'Din uppladdares batteri är %1' - , fi: 'Lähettimen paristoa jäljellä %1' - , ro: 'Bateria uploaderului este la %1' - , pl: 'Twoja bateria ma %1' - , ru: 'батарея загрузчика %1' - , tr: 'Yükleyici piliniz %1' - }, - 'alexaReservoir': { - bg: 'You have %1 units remaining' - , cs: 'V zásobníku zbývá %1 jednotek' - , en: 'You have %1 units remaining' - , hr: 'You have %1 units remaining' - , de: 'Du hast %1 Einheiten übrig' - , dk: 'Du har %1 enheder tilbage' - , ko: 'You have %1 units remaining' - , nl: 'Je hebt nog %l eenheden in je reservoir' - ,zh_cn: '你剩余%1 U的胰岛素' - , sv: 'Du har %1 enheter kvar' - , fi: '%1 yksikköä insuliinia jäljellä' - , ro: 'Mai aveți %1 unități rămase' - , pl: 'W zbiorniku pozostało %1 jednostek' - , ru: 'остается %1 ед' - , tr: '%1 birim kaldı' - }, - 'alexaPumpBattery': { - bg: 'Your pump battery is at %1 %2' - , cs: 'Baterie v pumpě má %1 %2' - , en: 'Your pump battery is at %1 %2' - , hr: 'Your pump battery is at %1 %2' - , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' - , dk: 'Din pumpes batteri er %1 %2' - , ko: 'Your pump battery is at %1 %2' - , nl: 'Je pomp batterij is bij %1 %2' - ,zh_cn: '你的泵电池电量是%1 %2' - , sv: 'Din pumps batteri är %1 %2' - , fi: 'Pumppu on %1 %2' - , ro: 'Bateria pompei este la %1 %2' - , pl: 'Bateria pompy jest w %1 %2' - , ru: 'батарея помпы %1 %2' - , tr: 'Pompa piliniz %1 %2' - }, - 'alexaLastLoop': { - bg: 'The last successful loop was %1' - , cs: 'Poslední úšpěšné provedení smyčky %1' - , en: 'The last successful loop was %1' - , hr: 'The last successful loop was %1' - , de: 'Der letzte erfolgreiche Loop war %1' - , dk: 'Seneste successfulde loop var %1' - , ko: 'The last successful loop was %1' - , nl: 'De meest recente goede loop was %1' - ,zh_cn: '最后一次成功闭环的是在%1' - , sv: 'Senaste lyckade loop var %1' - , fi: 'Viimeisin onnistunut loop oli %1' - , ro: 'Ultima decizie loop implementată cu succes a fost %1' - , pl: 'Ostatnia pomyślna pętla była %1' - , ru: 'недавний успешный цикл был %1' - , tr: 'Son başarılı döngü %1 oldu' - }, + 'alexaUploadBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Baterie mobilu má %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Der Akku deines Uploader Handys ist bei %1' + , dk: 'Din uploaders batteri er %1' + , ko: 'Your uploader battery is at %1' + , nl: 'De batterij van je mobiel is bij %l' + , zh_cn: '你的手机电池电量是 %1 ' + , sv: 'Din uppladdares batteri är %1' + , fi: 'Lähettimen paristoa jäljellä %1' + , ro: 'Bateria uploaderului este la %1' + , pl: 'Twoja bateria ma %1' + , ru: 'батарея загрузчика %1' + , tr: 'Yükleyici piliniz %1' + }, + 'alexaReservoir': { + bg: 'You have %1 units remaining' + , cs: 'V zásobníku zbývá %1 jednotek' + , en: 'You have %1 units remaining' + , hr: 'You have %1 units remaining' + , de: 'Du hast %1 Einheiten übrig' + , dk: 'Du har %1 enheder tilbage' + , ko: 'You have %1 units remaining' + , nl: 'Je hebt nog %l eenheden in je reservoir' + , zh_cn: '你剩余%1 U的胰岛素' + , sv: 'Du har %1 enheter kvar' + , fi: '%1 yksikköä insuliinia jäljellä' + , ro: 'Mai aveți %1 unități rămase' + , pl: 'W zbiorniku pozostało %1 jednostek' + , ru: 'остается %1 ед' + , tr: '%1 birim kaldı' + }, + 'alexaPumpBattery': { + bg: 'Your pump battery is at %1 %2' + , cs: 'Baterie v pumpě má %1 %2' + , en: 'Your pump battery is at %1 %2' + , hr: 'Your pump battery is at %1 %2' + , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' + , dk: 'Din pumpes batteri er %1 %2' + , ko: 'Your pump battery is at %1 %2' + , nl: 'Je pomp batterij is bij %1 %2' + , zh_cn: '你的泵电池电量是%1 %2' + , sv: 'Din pumps batteri är %1 %2' + , fi: 'Pumppu on %1 %2' + , ro: 'Bateria pompei este la %1 %2' + , pl: 'Bateria pompy jest w %1 %2' + , ru: 'батарея помпы %1 %2' + , tr: 'Pompa piliniz %1 %2' + }, + 'alexaLastLoop': { + bg: 'The last successful loop was %1' + , cs: 'Poslední úšpěšné provedení smyčky %1' + , en: 'The last successful loop was %1' + , hr: 'The last successful loop was %1' + , de: 'Der letzte erfolgreiche Loop war %1' + , dk: 'Seneste successfulde loop var %1' + , ko: 'The last successful loop was %1' + , nl: 'De meest recente goede loop was %1' + , zh_cn: '最后一次成功闭环的是在%1' + , sv: 'Senaste lyckade loop var %1' + , fi: 'Viimeisin onnistunut loop oli %1' + , ro: 'Ultima decizie loop implementată cu succes a fost %1' + , pl: 'Ostatnia pomyślna pętla była %1' + , ru: 'недавний успешный цикл был %1' + , tr: 'Son başarılı döngü %1 oldu' + }, 'alexaLoopNotAvailable': { bg: 'Loop plugin does not seem to be enabled' , cs: 'Plugin smyčka není patrně povolený' @@ -13585,7 +13585,7 @@ function init() { , dk: 'Loop plugin lader ikke til at være slået til' , ko: 'Loop plugin does not seem to be enabled' , nl: 'De Loop plugin is niet geactiveerd' - ,zh_cn: 'Loop插件看起来没有被启用' + , zh_cn: 'Loop插件看起来没有被启用' , sv: 'Loop plugin verkar inte vara aktiverad' , fi: 'Loop plugin ei ole aktivoitu' , ro: 'Extensia loop pare a fi dezactivată' @@ -13602,7 +13602,7 @@ function init() { , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' , ko: 'According to the loop forecast you are expected to be %1 over the next %2' , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - ,zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' + , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' @@ -13619,7 +13619,7 @@ function init() { , dk: 'Det er ikke muligt at forudsige md de tilgængelige data' , ko: 'Unable to forecast with the data that is available' , nl: 'Niet mogelijk om een voorspelling te doen met de data die beschikbaar is' - ,zh_cn: '血糖数据不可用,无法预测未来走势' + , zh_cn: '血糖数据不可用,无法预测未来走势' , sv: 'Förutsägelse ej möjlig med tillgänlig data' , fi: 'Ennusteet eivät ole toiminnassa puuttuvan tiedon vuoksi' , ro: 'Estimarea este imposibilă pe baza datelor disponibile' @@ -13628,13 +13628,13 @@ function init() { , tr: 'Mevcut verilerle tahmin edilemedi' }, 'alexaRawBG': { - en: 'Your raw bg is %1' + en: 'Your raw bg is %1' , cs: 'Raw glykémie je %1' , de: 'Dein Rohblutzucker ist %1' , dk: 'Dit raw blodsukker er %1' , ko: 'Your raw bg is %1' , nl: 'Je raw bloedwaarde is %1' - ,zh_cn: '你的血糖是 %1' + , zh_cn: '你的血糖是 %1' , sv: 'Ditt raw blodsocker är %1' , fi: 'Suodattamaton verensokeriarvo on %1' , ro: 'Glicemia brută este %1' @@ -13651,12 +13651,12 @@ function init() { , dk: 'OpenAPS forventet blodsukker er %1' , ko: 'The OpenAPS Eventual BG is %1' , nl: 'OpenAPS uiteindelijke bloedglucose van %1' - ,zh_cn: 'OpenAPS 预测最终血糖是 %1' + , zh_cn: 'OpenAPS 预测最终血糖是 %1' , sv: 'OpenAPS slutgiltigt blodsocker är %1' , fi: 'OpenAPS verensokeriarvio on %1' , ro: 'Glicemia estimată de OpenAPS este %1' - ,bg: 'The OpenAPS Eventual BG is %1' - ,hr: 'The OpenAPS Eventual BG is %1' + , bg: 'The OpenAPS Eventual BG is %1' + , hr: 'The OpenAPS Eventual BG is %1' , pl: 'Glikemia prognozowana przez OpenAPS wynosi %1' , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' @@ -13668,12 +13668,12 @@ function init() { , dk: '%1 %2 gram aktive kulhydrater' , ko: '%1 %2 carbohydrates on board' , nl: '%1 %2 actieve koolhydraten' - ,zh_cn: '%1 %2 活性碳水化合物' + , zh_cn: '%1 %2 活性碳水化合物' , sv: '%1 %2 gram aktiva kolhydrater' , fi: '%1 %2 aktiivista hiilihydraattia' , ro: '%1 %2 carbohidrați activi în corp' - ,bg: '%1 %2 carbohydrates on board' - ,hr: '%1 %2 carbohydrates on board' + , bg: '%1 %2 carbohydrates on board' + , hr: '%1 %2 carbohydrates on board' , pl: '%1 %2 aktywnych węglowodanów' , ru: '%1 $2 активных углеводов' , tr: '%1 %2 aktif karbonhidrat' From 3b183a2137cc7fd8f5f16b21880ec7fd5e5ac669 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Fri, 6 Sep 2019 23:53:17 -0600 Subject: [PATCH 004/134] One more fix for CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e00200d5713..a1ac0799d58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -232,7 +232,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`profile` (Treatment Profile)](README.md#profile-treatment-profile)| Please volunteer | Please volunteer | | [`pump` (Pump Monitoring)](README.md#pump-pump-monitoring)| Please volunteer | Please volunteer | | [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | -| [`sage` (Sensor Age)](README.md#sage-sensor-age)| @jpcunningh | Please volunteer | +| [`sage` (Sensor Age)](README.md#sage-sensor-age)| [@jpcunningh] | Please volunteer | | [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | | [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | | [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | From a7bf320042338e39a07e0f296503e815f2d13778 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 00:06:32 -0600 Subject: [PATCH 005/134] Minor code formatting improvements --- lib/api/alexa/index.js | 278 +++++++++++++++++++------------------- lib/plugins/alexa.js | 4 +- lib/plugins/googlehome.js | 95 +++++++------ 3 files changed, 187 insertions(+), 190 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 65f477ad85d..241460d1cb0 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -4,156 +4,154 @@ var moment = require('moment'); var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; - var express = require('express') - , api = express.Router( ); - var translate = ctx.language.translate; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.alexa) { - if (plugin.alexa.intentHandlers) { - console.log(plugin.name + ' is Alexa enabled'); - _each(plugin.alexa.intentHandlers, function (route) { - if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); - } - }); - } - if (plugin.alexa.rollupHandlers) { - console.log(plugin.name + ' is Alexa rollup enabled'); - _each(plugin.alexa.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); - } - }); - - api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Alexa'); - var locale = req.body.request.locale; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - switch (req.body.request.type) { - case 'IntentRequest': - onIntent(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'LaunchRequest': - onLaunch(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'SessionEndedRequest': - onSessionEnded(req.body.request.intent, function (alexaResponse) { - res.json(alexaResponse); - next( ); - }); - break; - } - }); - - ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)) + '.'; - callback(null, {results: status, priority: -1}); + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.plugins.eachEnabledPlugin(function each(plugin){ + if (plugin.alexa) { + if (plugin.alexa.intentHandlers) { + console.log(plugin.name + ' is Alexa enabled'); + _each(plugin.alexa.intentHandlers, function (route) { + if (route) { + ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); + } }); - // console.log('BG results called'); - // callback(null, 'BG results'); - }, 'BG Status'); - - ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); - callback('Current blood glucose', status); + } + if (plugin.alexa.rollupHandlers) { + console.log(plugin.name + ' is Alexa rollup enabled'); + _each(plugin.alexa.rollupHandlers, function (route) { + console.log('Route'); + console.log(route); + if (route) { + ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } }); - }, 'metric', ['bg', 'blood glucose', 'number']); - - ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); - }); - }); - - - function onLaunch() { - console.log('Session launched'); + } + } else { + console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); } - - function onIntent(intent, next) { - console.log('Received intent request'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); + }); + + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = req.body.request.locale; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); } - function onSessionEnded() { - console.log('Session ended'); + switch (req.body.request.type) { + case 'IntentRequest': + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'LaunchRequest': + onLaunch(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'SessionEndedRequest': + onSessionEnded(req.body.request.intent, function (alexaResponse) { + res.json(alexaResponse); + next( ); + }); + break; } + }); + + ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('alexaStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('alexaStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback('Current blood glucose', status); + }); + }, 'metric', ['bg', 'blood glucose', 'number']); - function handleIntent(intentName, slots, next) { - var handler = ctx.alexa.getIntentHandler(intentName, slots); - if (handler){ - var sbx = initializeSandbox(); - handler(next, slots, sbx); - } else { - next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); - } + ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { + ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { + callback('Full status', status); + }); + }); + + + function onLaunch() { + console.log('Session launched'); + } + + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } + + function onSessionEnded() { + console.log('Session ended'); + } + + function handleIntent(intentName, slots, next) { + var handler = ctx.alexa.getIntentHandler(intentName, slots); + if (handler){ + var sbx = initializeSandbox(); + handler(next, slots, sbx); + } else { + next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); } + } - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - return api; + return api; } module.exports = configure; diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index 38ae449c249..bfb8cf6308f 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -1,8 +1,8 @@ var _ = require('lodash'); var async = require('async'); -function init(env, ctx) { - console.log('Configuring Alexa.'); +function init (env, ctx) { + console.log('Configuring Alexa...'); function alexa() { return alexa; } diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js index 42da3f14b14..655b6a485ff 100644 --- a/lib/plugins/googlehome.js +++ b/lib/plugins/googlehome.js @@ -1,59 +1,58 @@ -function init(env, ctx) { +function init (env, ctx) { - console.log('Configuring Google Home...'); - - function googleHome() { - return googleHome; + console.log('Configuring Google Home...'); + function googleHome() { + return googleHome; + } + + var intentHandlers = {}; + + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; } - - var intentHandlers = {}; - - googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (!intentHandlers[intent]) { - intentHandlers[intent] = {}; - } - if (routableSlot && slotValues) { - for (var i = 0, len = slotValues.length; i < len; i++) { - if (!intentHandlers[intent][routableSlot]) { - intentHandlers[intent][routableSlot] = {}; - } - if (!intentHandlers[intent][routableSlot][slotValues[i]]) { - intentHandlers[intent][routableSlot][slotValues[i]] = {}; - } - intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + if (routableSlot && slotValues) { + for (var i = 0, len = slotValues.length; i < len; i++) { + if (!intentHandlers[intent][routableSlot]) { + intentHandlers[intent][routableSlot] = {}; } - } else { - intentHandlers[intent].handler = handler; - } - }; - - googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { - if (intentName && intentHandlers[intentName]) { - if (metric && intentHandlers[intentName]['metric'] && - intentHandlers[intentName]['metric'][metric] && - intentHandlers[intentName]['metric'][metric].handler) { - return intentHandlers[intentName]['metric'][metric].handler; - } else if (intentHandlers[intentName].handler) { - return intentHandlers[intentName].handler; - } else { - return null; + if (!intentHandlers[intent][routableSlot][slotValues[i]]) { + intentHandlers[intent][routableSlot][slotValues[i]] = {}; } + intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + } + } else { + intentHandlers[intent].handler = handler; + } + }; + + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + if (intentName && intentHandlers[intentName]) { + if (metric && intentHandlers[intentName]['metric'] && + intentHandlers[intentName]['metric'][metric] && + intentHandlers[intentName]['metric'][metric].handler) { + return intentHandlers[intentName]['metric'][metric].handler; + } else if (intentHandlers[intentName].handler) { + return intentHandlers[intentName].handler; } else { return null; } - - }; - - googleHome.buildResponse = function buildResponse(output) { - return { - fulfillmentText: output - // , fulfillmentMessages: [output] - , source: 'Nightscout' - }; + } else { + return null; + } + + }; + + googleHome.buildResponse = function buildResponse(output) { + return { + fulfillmentText: output +// , fulfillmentMessages: [output] + , source: 'Nightscout' }; - - return googleHome; - } + }; + + return googleHome; +} module.exports = init; \ No newline at end of file From ce00c945b034100d57201cf6d8f0fa030ef02cc4 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 00:25:29 -0600 Subject: [PATCH 006/134] One more time... --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1ac0799d58..79266e47c82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -223,7 +223,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | | [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | | [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | -| [`googlehome` (Google Home/DialogFLow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`googlehome` (Google Home/DialogFlow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | | [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | | [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | | [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | From efa35c46160bb0bbf21a47171502b2f59257d754 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 17:51:31 -0600 Subject: [PATCH 007/134] Renamed Alexa stuff to virtAsst for generic-ness --- lib/api/alexa/index.js | 16 +++++++------- lib/language.js | 38 ++++++++++++++++---------------- lib/plugins/ar2.js | 6 ++--- lib/plugins/basalprofile.js | 22 +++++++++--------- lib/plugins/cob.js | 6 ++--- lib/plugins/iob.js | 18 +++++++-------- lib/plugins/loop.js | 10 ++++----- lib/plugins/openaps.js | 14 ++++++------ lib/plugins/pump.js | 14 ++++++------ lib/plugins/rawbg.js | 6 ++--- lib/plugins/upbat.js | 6 ++--- tests/ar2.test.js | 6 ++--- tests/basalprofileplugin.test.js | 10 ++++----- tests/cob.test.js | 6 ++--- tests/iob.test.js | 10 ++++----- tests/loop.test.js | 8 +++---- tests/openaps.test.js | 8 +++---- tests/pump.test.js | 8 +++---- tests/rawbg.test.js | 6 ++--- tests/upbat.test.js | 6 ++--- 20 files changed, 112 insertions(+), 112 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 241460d1cb0..cd46a1acdd9 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -17,18 +17,18 @@ function configure (app, wares, ctx, env) { api.use(wares.bodyParser.json()); ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.alexa) { - if (plugin.alexa.intentHandlers) { - console.log(plugin.name + ' is Alexa enabled'); - _each(plugin.alexa.intentHandlers, function (route) { + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { if (route) { ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); } }); } - if (plugin.alexa.rollupHandlers) { - console.log(plugin.name + ' is Alexa rollup enabled'); - _each(plugin.alexa.rollupHandlers, function (route) { + if (plugin.virtAsst.rollupHandlers) { + console.log('Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { console.log('Route'); console.log(route); if (route) { @@ -37,7 +37,7 @@ function configure (app, wares, ctx, env) { }); } } else { - console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); + console.log('Plugin ' + plugin.name + ' does not support Virtual Assistants'); } }); diff --git a/lib/language.js b/lib/language.js index 55b80756418..f1560e122ef 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13274,7 +13274,7 @@ function init() { , zh_cn: '快速上升' , zh_tw: 'rapidly rising' }, - 'alexaStatus': { + 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' , de: '%1 und bis %3 %2.' @@ -13300,7 +13300,7 @@ function init() { , zh_cn: '%1 和 %2 到 %3.' , zh_tw: '%1 and %2 as of %3.' }, - 'alexaBasal': { + 'virtAsstBasal': { bg: '%1 současný bazál je %2 jednotek za hodinu' , cs: '%1 current basal is %2 units per hour' , de: '%1 aktuelle Basalrate ist %2 Einheiten je Stunde' @@ -13326,7 +13326,7 @@ function init() { , zh_cn: '%1 当前基础率是 %2 U/小时' , zh_tw: '%1 current basal is %2 units per hour' }, - 'alexaBasalTemp': { + 'virtAsstBasalTemp': { bg: '%1 dočasný bazál %2 jednotek za hodinu skončí %3' , cs: '%1 temp basal of %2 units per hour will end %3' , de: '%1 temporäre Basalrate von %2 Einheiten endet %3' @@ -13352,7 +13352,7 @@ function init() { , zh_cn: '%1 临时基础率 %2 U/小时将会在 %3结束' , zh_tw: '%1 temp basal of %2 units per hour will end %3' }, - 'alexaIob': { + 'virtAsstIob': { bg: 'a máte %1 jednotek aktivního inzulínu.' , cs: 'and you have %1 insulin on board.' , de: 'und du hast %1 Insulin wirkend.' @@ -13378,7 +13378,7 @@ function init() { , zh_cn: '并且你有 %1 的活性胰岛素.' , zh_tw: 'and you have %1 insulin on board.' }, - 'alexaIobIntent': { + 'virtAsstIobIntent': { bg: 'Máte %1 jednotek aktivního inzulínu' , cs: 'You have %1 insulin on board' , de: 'Du hast noch %1 Insulin wirkend' @@ -13404,7 +13404,7 @@ function init() { , zh_cn: '你有 %1 的活性胰岛素' , zh_tw: 'You have %1 insulin on board' }, - 'alexaIobUnits': { + 'virtAsstIobUnits': { bg: '%1 units of' , cs: '%1 jednotek' , de: 'noch %1 Einheiten' @@ -13430,7 +13430,7 @@ function init() { , zh_cn: '%1 单位' , zh_tw: '%1 units of' }, - 'alexaPreamble': { + 'virtAsstPreamble': { bg: 'Your' , cs: 'Vaše' , de: 'Deine' @@ -13456,7 +13456,7 @@ function init() { , zh_cn: '你的' , zh_tw: 'Your' }, - 'alexaPreamble3person': { + 'virtAsstPreamble3person': { bg: '%1 has a ' , cs: '%1 má ' , de: '%1 hat eine' @@ -13482,7 +13482,7 @@ function init() { , zh_cn: '%1 有一个 ' , zh_tw: '%1 has a ' }, - 'alexaNoInsulin': { + 'virtAsstNoInsulin': { bg: 'no' , cs: 'žádný' , de: 'kein' @@ -13508,7 +13508,7 @@ function init() { , zh_cn: '否' , zh_tw: 'no' }, - 'alexaUploadBattery': { + 'virtAsstUploadBattery': { bg: 'Your uploader battery is at %1' , cs: 'Baterie mobilu má %1' , en: 'Your uploader battery is at %1' @@ -13525,7 +13525,7 @@ function init() { , ru: 'батарея загрузчика %1' , tr: 'Yükleyici piliniz %1' }, - 'alexaReservoir': { + 'virtAsstReservoir': { bg: 'You have %1 units remaining' , cs: 'V zásobníku zbývá %1 jednotek' , en: 'You have %1 units remaining' @@ -13542,7 +13542,7 @@ function init() { , ru: 'остается %1 ед' , tr: '%1 birim kaldı' }, - 'alexaPumpBattery': { + 'virtAsstPumpBattery': { bg: 'Your pump battery is at %1 %2' , cs: 'Baterie v pumpě má %1 %2' , en: 'Your pump battery is at %1 %2' @@ -13559,7 +13559,7 @@ function init() { , ru: 'батарея помпы %1 %2' , tr: 'Pompa piliniz %1 %2' }, - 'alexaLastLoop': { + 'virtAsstLastLoop': { bg: 'The last successful loop was %1' , cs: 'Poslední úšpěšné provedení smyčky %1' , en: 'The last successful loop was %1' @@ -13576,7 +13576,7 @@ function init() { , ru: 'недавний успешный цикл был %1' , tr: 'Son başarılı döngü %1 oldu' }, - 'alexaLoopNotAvailable': { + 'virtAsstLoopNotAvailable': { bg: 'Loop plugin does not seem to be enabled' , cs: 'Plugin smyčka není patrně povolený' , en: 'Loop plugin does not seem to be enabled' @@ -13593,7 +13593,7 @@ function init() { , ru: 'плагин ЗЦ Loop не активирован ' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'alexaLoopForecast': { + 'virtAsstLoopForecast': { bg: 'According to the loop forecast you are expected to be %1 over the next %2' , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' , en: 'According to the loop forecast you are expected to be %1 over the next %2' @@ -13610,7 +13610,7 @@ function init() { , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' }, - 'alexaForecastUnavailable': { + 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' , en: 'Unable to forecast with the data that is available' @@ -13627,7 +13627,7 @@ function init() { , ru: 'прогноз при таких данных невозможен' , tr: 'Mevcut verilerle tahmin edilemedi' }, - 'alexaRawBG': { + 'virtAsstRawBG': { en: 'Your raw bg is %1' , cs: 'Raw glykémie je %1' , de: 'Dein Rohblutzucker ist %1' @@ -13644,7 +13644,7 @@ function init() { , ru: 'ваши необработанные данные RAW %1' , tr: 'Ham kan şekeriniz %1' }, - 'alexaOpenAPSForecast': { + 'virtAsstOpenAPSForecast': { en: 'The OpenAPS Eventual BG is %1' , cs: 'OpenAPS Eventual BG je %1' , de: 'Der von OpenAPS vorhergesagte Blutzucker ist %1' @@ -13661,7 +13661,7 @@ function init() { , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' }, - 'alexaCOB': { + 'virtAsstCOB': { en: '%1 %2 carbohydrates on board' , cs: '%1 %2 aktivních sachridů' , de: '%1 %2 Gramm Kohlenhydrate wirkend.' diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index e25a2b36229..24fe192cac3 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -146,7 +146,7 @@ function init (ctx) { return result.points; }; - function alexaAr2Handler (next, slots, sbx) { + function virtAsstAr2Handler (next, slots, sbx) { if (sbx.properties.ar2.forecast.predicted) { var forecast = sbx.properties.ar2.forecast.predicted; var max = forecast[0].mgdl; @@ -170,12 +170,12 @@ function init (ctx) { } } - ar2.alexa = { + ar2.virtAsst = { intentHandlers: [{ intent: 'MetricNow' , routableSlot: 'metric' , slots: ['ar2 forecast', 'forecast'] - , intentHandler: alexaAr2Handler + , intentHandler: virtAsstAr2Handler }] }; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index b1e97d816be..10f27145fdc 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -105,13 +105,13 @@ function init (ctx) { var response = 'Unable to determine current basal'; var preamble = ''; if (basalValue.treatment) { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); + }) : translate('virtAsstPreamble'); var minutesLeft = moment(basalValue.treatment.endmills).from(moment(sbx.time)); - response = translate('alexaBasalTemp', { + response = translate('virtAsstBasalTemp', { params: [ preamble, basalValue.totalbasal, @@ -119,12 +119,12 @@ function init (ctx) { ] }); } else { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); - response = translate('alexaBasal', { + }) : translate('virtAsstPreamble'); + response = translate('virtAsstBasal', { params: [ preamble, basalValue.totalbasal @@ -134,25 +134,25 @@ function init (ctx) { return response; } - function alexaRollupCurrentBasalHandler (slots, sbx, callback) { + function virtAsstRollupCurrentBasalHandler (slots, sbx, callback) { callback(null, {results: basalMessage(slots, sbx), priority: 1}); } - function alexaCurrentBasalhandler (next, slots, sbx) { + function virtAsstCurrentBasalhandler (next, slots, sbx) { next('Current Basal', basalMessage(slots, sbx)); } - basal.alexa = { + basal.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current basal' - , rollupHandler: alexaRollupCurrentBasalHandler + , rollupHandler: virtAsstRollupCurrentBasalHandler }], intentHandlers: [{ intent: 'MetricNow' , routableSlot:'metric' , slots:['basal', 'current basal'] - , intentHandler: alexaCurrentBasalhandler + , intentHandler: virtAsstCurrentBasalhandler }] }; diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 7bd5b422b26..8706e182444 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -289,7 +289,7 @@ function init (ctx) { }); }; - function alexaCOBHandler (next, slots, sbx) { + function virtAsstCOBHandler (next, slots, sbx) { var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; var value = 'no'; if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { @@ -299,12 +299,12 @@ function init (ctx) { next('Current COB', response); } - cob.alexa = { + cob.virtAsst = { intentHandlers: [{ intent: 'MetricNow' , routableSlot: 'metric' , slots: ['cob', 'carbs on board', 'carbohydrates on board'] - , intentHandler: alexaCOBHandler + , intentHandler: virtAsstCOBHandler }] }; diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index f4f1d5e05cc..0b9a5ff5576 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -243,9 +243,9 @@ function init(ctx) { }; - function alexaIOBIntentHandler (callback, slots, sbx) { + function virtAsstIOBIntentHandler (callback, slots, sbx) { - var message = translate('alexaIobIntent', { + var message = translate('virtAsstIobIntent', { params: [ //preamble, getIob(sbx) @@ -255,9 +255,9 @@ function init(ctx) { callback('Current IOB', message); } - function alexaIOBRollupHandler (slots, sbx, callback) { + function virtAsstIOBRollupHandler (slots, sbx, callback) { var iob = getIob(sbx); - var message = translate('alexaIob', { + var message = translate('virtAsstIob', { params: [iob] }); callback(null, {results: message, priority: 2}); @@ -265,26 +265,26 @@ function init(ctx) { function getIob(sbx) { if (sbx.properties.iob && sbx.properties.iob.iob !== 0) { - return translate('alexaIobUnits', { + return translate('virtAsstIobUnits', { params: [ utils.toFixed(sbx.properties.iob.iob) ] }); } - return translate('alexaNoInsulin'); + return translate('virtAsstNoInsulin'); } - iob.alexa = { + iob.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current iob' - , rollupHandler: alexaIOBRollupHandler + , rollupHandler: virtAsstIOBRollupHandler }] , intentHandlers: [{ intent: 'MetricNow' , routableSlot: 'metric' , slots: ['iob', 'insulin on board'] - , intentHandler: alexaIOBIntentHandler + , intentHandler: virtAsstIOBIntentHandler }] }; diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index fc8dcbb3282..c94e969a7ca 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -431,7 +431,7 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.loop.lastLoop.predicted) { var forecast = sbx.properties.loop.lastLoop.predicted.values; var max = forecast[0]; @@ -465,21 +465,21 @@ function init (ctx) { } } - function alexaLastLoopHandler (next, slots, sbx) { + function virtAsstLastLoopHandler (next, slots, sbx) { console.log(JSON.stringify(sbx.properties.loop.lastLoop)); var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); next('Last loop', response); } - loop.alexa = { + loop.virtAsst = { intentHandlers: [{ intent: 'MetricNow' , routableSlot: 'metric' , slots: ['loop forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 1e03ce218ce..1ae4741285e 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -510,9 +510,9 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { - var response = translate('alexaOpenAPSForecast', { + var response = translate('virtAsstOpenAPSForecast', { params: [ sbx.properties.openaps.lastEventualBG ] @@ -521,9 +521,9 @@ function init (ctx) { } } - function alexaLastLoopHandler (next, slots, sbx) { + function virtAsstLastLoopHandler (next, slots, sbx) { console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); - var response = translate('alexaLastLoop', { + var response = translate('virtAsstLastLoop', { params: [ moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) ] @@ -531,15 +531,15 @@ function init (ctx) { next('Last loop', response); } - openaps.alexa = { + openaps.virtAsst = { intentHandlers: [{ intent: 'MetricNow' , routableSlot: 'metric' , slots: ['openaps forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 842d8536b6b..b63c69bb2c3 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -135,8 +135,8 @@ function init (ctx) { }); }; - function alexaReservoirHandler (next, slots, sbx) { - var response = translate('alexaReservoir', { + function virtAsstReservoirHandler (next, slots, sbx) { + var response = translate('virtAsstReservoir', { params: [ sbx.properties.pump.pump.reservoir ] @@ -144,10 +144,10 @@ function init (ctx) { next('Remaining insulin', response); } - function alexaBatteryHandler (next, slots, sbx) { + function virtAsstBatteryHandler (next, slots, sbx) { var battery = _.get(sbx, 'properties.pump.data.battery'); if (battery) { - var response = translate('alexaPumpBattery', { + var response = translate('virtAsstPumpBattery', { params: [ battery.value, battery.unit @@ -159,13 +159,13 @@ function init (ctx) { } } - pump.alexa = { + pump.virtAsst = { intentHandlers:[{ intent: 'InsulinRemaining', - intentHandler: alexaReservoirHandler + intentHandler: virtAsstReservoirHandler }, { intent: 'PumpBattery', - intentHandler: alexaBatteryHandler + intentHandler: virtAsstBatteryHandler }] }; diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index f19e669f63b..2bfea16ba73 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -106,17 +106,17 @@ function init (ctx) { return display; }; - function alexaRawBGHandler (next, slots, sbx) { + function virtAsstRawBGHandler (next, slots, sbx) { var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; next('Current Raw BG', response); } - rawbg.alexa = { + rawbg.virtAsst = { intentHandlers: [{ intent: 'MetricNow' , routableSlot:'metric' , slots:['raw bg', 'raw blood glucose'] - , intentHandler: alexaRawBGHandler + , intentHandler: virtAsstRawBGHandler }] }; diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index eda42a3901f..72c3bce2021 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -221,15 +221,15 @@ function init() { }); }; - function alexaUploaderBatteryHandler (next, slots, sbx) { + function virtAsstUploaderBatteryHandler (next, slots, sbx) { var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; next('Uploader battery', response); } - upbat.alexa = { + upbat.virtAsst = { intentHandlers: [{ intent: 'UploaderBattery' - , intentHandler: alexaUploaderBatteryHandler + , intentHandler: virtAsstUploaderBatteryHandler }] }; diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 9dbf6de14cd..8fee3ec1c63 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -147,16 +147,16 @@ describe('ar2', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var now = Date.now(); var before = now - FIVE_MINS; ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); - ar2.alexa.intentHandlers.length.should.equal(1); + ar2.virtAsst.intentHandlers.length.should.equal(1); - ar2.alexa.intentHandlers[0].intentHandler(function next(title, response) { + ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); done(); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index 0bcfd3bc268..fa97f84274e 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -77,7 +77,7 @@ describe('basalprofile', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = {}; var ctx = { @@ -92,14 +92,14 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; - basal.alexa.intentHandlers.length.should.equal(1); - basal.alexa.rollupHandlers.length.should.equal(1); + basal.virtAsst.intentHandlers.length.should.equal(1); + basal.virtAsst.rollupHandlers.length.should.equal(1); - basal.alexa.intentHandlers[0].intentHandler(function next(title, response) { + basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); - basal.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + basal.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('Your current basal is 0.175 units per hour'); response.priority.should.equal(1); diff --git a/tests/cob.test.js b/tests/cob.test.js index dbbecda0b67..54fbcb6c50d 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -97,7 +97,7 @@ describe('COB', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = { treatments: [{ carbs: '8' @@ -110,9 +110,9 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); - cob.alexa.intentHandlers.length.should.equal(1); + cob.virtAsst.intentHandlers.length.should.equal(1); - cob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); done(); diff --git a/tests/iob.test.js b/tests/iob.test.js index 30872e4fb4d..b6c5c2430ec 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -10,7 +10,7 @@ describe('IOB', function() { var iob = require('../lib/plugins/iob')(ctx); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sbx = { properties: { @@ -20,14 +20,14 @@ describe('IOB', function() { } }; - iob.alexa.intentHandlers.length.should.equal(1); - iob.alexa.rollupHandlers.length.should.equal(1); + iob.virtAsst.intentHandlers.length.should.equal(1); + iob.virtAsst.rollupHandlers.length.should.equal(1); - iob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); - iob.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + iob.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('and you have 1.50 units of insulin on board.'); response.priority.should.equal(2); diff --git a/tests/loop.test.js b/tests/loop.test.js index 9c65ff9bdd1..8506de4555b 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -243,7 +243,7 @@ describe('loop', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -255,13 +255,13 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); - loop.alexa.intentHandlers.length.should.equal(2); + loop.virtAsst.intentHandlers.length.should.equal(2); - loop.alexa.intentHandlers[0].intentHandler(function next(title, response) { + loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); - loop.alexa.intentHandlers[1].intentHandler(function next(title, response) { + loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { title.should.equal('Last loop'); response.should.equal('The last successful loop was a few seconds ago'); done(); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index ed3dd6d3b9f..9bfc5161969 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -370,7 +370,7 @@ describe('openaps', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -382,13 +382,13 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); - openaps.alexa.intentHandlers.length.should.equal(2); + openaps.virtAsst.intentHandlers.length.should.equal(2); - openaps.alexa.intentHandlers[0].intentHandler(function next(title, response) { + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); - openaps.alexa.intentHandlers[1].intentHandler(function next(title, response) { + openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { title.should.equal('Last loop'); response.should.equal('The last successful loop was 2 minutes ago'); done(); diff --git a/tests/pump.test.js b/tests/pump.test.js index c6def822058..afc96481976 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -254,7 +254,7 @@ describe('pump', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -266,13 +266,13 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); - pump.alexa.intentHandlers.length.should.equal(2); + pump.virtAsst.intentHandlers.length.should.equal(2); - pump.alexa.intentHandlers[0].intentHandler(function next(title, response) { + pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Remaining insulin'); response.should.equal('You have 86.4 units remaining'); - pump.alexa.intentHandlers[1].intentHandler(function next(title, response) { + pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { title.should.equal('Pump battery'); response.should.equal('Your pump battery is at 1.52 volts'); done(); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index ab91d2bf722..48c21186cc5 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -35,16 +35,16 @@ describe('Raw BG', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sandbox = require('../lib/sandbox')(); var sbx = sandbox.clientInit(ctx, Date.now(), data); rawbg.setProperties(sbx); - rawbg.alexa.intentHandlers.length.should.equal(1); + rawbg.virtAsst.intentHandlers.length.should.equal(1); - rawbg.alexa.intentHandlers[0].intentHandler(function next(title, response) { + rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 9b48c3b845e..515d1a8d218 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -93,7 +93,7 @@ describe('Uploader Battery', function ( ) { upbat.updateVisualisation(sbx); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: {} @@ -106,9 +106,9 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); - upbat.alexa.intentHandlers.length.should.equal(1); + upbat.virtAsst.intentHandlers.length.should.equal(1); - upbat.alexa.intentHandlers[0].intentHandler(function next(title, response) { + upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Uploader battery'); response.should.equal('Your uploader battery is at 20%'); From 51c8d2cddadfa1337a7da13278083bc17e75a645 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 18:39:55 -0600 Subject: [PATCH 008/134] Corrected missed translate() text --- lib/api/alexa/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index cd46a1acdd9..f54eeba6fa3 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -82,7 +82,7 @@ function configure (app, wares, ctx, env) { } else { direction = records[0].direction; } - var status = translate('alexaStatus', { + var status = translate('virtAsstStatus', { params: [ sbx.scaleMgdl(records[0].sgv), direction, @@ -102,7 +102,7 @@ function configure (app, wares, ctx, env) { } else { direction = records[0].direction; } - var status = translate('alexaStatus', { + var status = translate('virtAsstStatus', { params: [ sbx.scaleMgdl(records[0].sgv), direction, From 58162c218322f670e6c396eb7aa78ca3691605af Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 19:30:35 -0600 Subject: [PATCH 009/134] Updated googlehome plugin to mimic the alexa plugin --- lib/api/alexa/index.js | 4 +- lib/api/googlehome/index.js | 194 +++++++++++++++++++----------------- lib/plugins/googlehome.js | 85 ++++++++++++---- 3 files changed, 170 insertions(+), 113 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index f54eeba6fa3..61f343a38f6 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -94,7 +94,7 @@ function configure (app, wares, ctx, env) { }); }, 'BG Status'); - ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { + ctx.alexa.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { entries.list({count: 1}, function(err, records) { var direction; if(translate(records[0].direction)){ @@ -113,7 +113,7 @@ function configure (app, wares, ctx, env) { }); }, 'metric', ['bg', 'blood glucose', 'number']); - ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { + ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { callback('Full status', status); }); diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index a833b4abab3..5cd791d0488 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -1,115 +1,123 @@ 'use strict'; -var _ = require('lodash'); var moment = require('moment'); +var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var express = require('express'); - var api = express.Router(); - var entries = ctx.entries; - var translate = ctx.language.translate; + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); - ctx.plugins.eachEnabledPlugin(function each(plugin) { - if (plugin.googleHome) { - if (plugin.googleHome.intentHandlers) { - console.log('Plugin ' + plugin.name + ' is Google Home enabled'); - _.each(plugin.googleHome.intentHandlers, function (handler) { - if (handler) { - ctx.googleHome.configureIntentHandler(handler.intent, handler.intentHandler, handler.routableSlot, handler.slots); - } - }); - } - } else { - console.log('Plugin ' + plugin.name + ' is not Google Home enabled'); + ctx.plugins.eachEnabledPlugin(function each(plugin){ + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); + } + }); } - }); - - ctx.googleHome.configureIntentHandler('CurrentMetric', function (result, next, sbx) { - entries.list({count: 1}, function(err, records) { - var response = ''; - if (records && records.length > 0) { - var direction = ''; - if (records[0].direction === 'FortyFiveDown') { - direction = ' and slightly dropping'; - } else if (records[0].direction === 'FortyFiveUp') { - direction = ' and slightly rising'; - } else if (records[0].direction === 'Flat') { - direction = ' and holding'; - } else if (records[0].direction === 'SingleUp') { - direction = ' and rising'; - } else if (records[0].direction === 'SingleDown') { - direction = ' and dropping'; - } else if (records[0].direction === 'DoubleDown') { - direction = ' and rapidly dropping'; - } else if (records[0].direction === 'DoubleUp') { - direction = ' and rapidly rising'; + if (plugin.virtAsst.rollupHandlers) { + console.log('Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + console.log('Route'); + console.log(route); + if (route) { + ctx.googleHome.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); } - response = buildPreamble(result.parameters); - response += sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); - } else { - response = buildPreamble(result.parameters) + 'unknown'; - } - next(response); - }); - }, 'metric', ['bg', 'blood glucose', 'blood sugar', 'number']); - - api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Google Home'); - onIntent(req.body, function (response) { - res.json(ctx.googleHome.buildResponse(response)); - next(); - }); - }); - - function buildPreamble(parameters) { - var preamble = ''; - if (parameters && parameters.givenName) { - preamble = parameters.givenName + '\'s current '; - } else { - preamble = 'Your current '; + }); } - if (parameters && parameters.readingType) { - preamble += parameters.readingType + ' is '; - } else { - preamble += 'blood glucose is '; + } else { + console.log('Plugin ' + plugin.name + ' does not support Virtual Assistants'); + } + }); + + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = req.body.queryResult.languageCode; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); } - return preamble; + ctx.language.set(locale); + moment.locale(locale); } - function onIntent(body, next) { - console.log('Received intent request'); - console.log(JSON.stringify(body)); - handleIntent(body, next); + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); + if (handler){ + var sbx = initializeSandbox(); + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); + next( ); } + }); - // https://docs.api.ai/docs/webhook#section-format-of-request-to-the-service - function handleIntent(body, next) { - var displayName = body.queryResult.intent.displayName; - var metric = body.queryResult.parameters ? body.queryResult.parameters.metric : null; - var handler = ctx.googleHome.getIntentHandler(displayName, metric); - if (handler) { - var sbx = initializeSandbox(); - handler(body.queryResult, next, sbx); + ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); } else { - next('I\'m sorry I don\'t know what you\'re asking for'); + direction = records[0].direction; } - } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } + ctx.googleHome.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback('Current blood glucose', status); + }); + }, 'metric', ['bg', 'blood glucose', 'number']); + + ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { + callback('Full status', status); + }); + }); + + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - return api; + return api; } module.exports = configure; \ No newline at end of file diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js index 655b6a485ff..27f7d98c7ac 100644 --- a/lib/plugins/googlehome.js +++ b/lib/plugins/googlehome.js @@ -1,20 +1,26 @@ +var _ = require('lodash'); +var async = require('async'); function init (env, ctx) { - console.log('Configuring Google Home...'); function googleHome() { return googleHome; } - var intentHandlers = {}; + var rollup = {}; + // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues + // are the values that determine the routing. This allows for specific intent handlers based on the value of a + // specific slot. Routing is only supported on one slot for now. + // There is no protection for a previously configured handler - one plugin can overwrite the handler of another + // plugin. googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (!intentHandlers[intent]) { + if (! intentHandlers[intent]) { intentHandlers[intent] = {}; } if (routableSlot && slotValues) { for (var i = 0, len = slotValues.length; i < len; i++) { - if (!intentHandlers[intent][routableSlot]) { + if (! intentHandlers[intent][routableSlot]) { intentHandlers[intent][routableSlot] = {}; } if (!intentHandlers[intent][routableSlot][slotValues[i]]) { @@ -27,32 +33,75 @@ function init (env, ctx) { } }; - googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + // This function retrieves a handler based on the intent name and slots requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, slots) { if (intentName && intentHandlers[intentName]) { - if (metric && intentHandlers[intentName]['metric'] && - intentHandlers[intentName]['metric'][metric] && - intentHandlers[intentName]['metric'][metric].handler) { - return intentHandlers[intentName]['metric'][metric].handler; - } else if (intentHandlers[intentName].handler) { + if (slots) { + var slotKeys = Object.keys(slots); + for (var i = 0, len = slotKeys.length; i < len; i++) { + if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && + intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && + intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { + + return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; + } + } + } + if (intentHandlers[intentName].handler) { return intentHandlers[intentName].handler; - } else { - return null; } + return null; } else { return null; } }; - googleHome.buildResponse = function buildResponse(output) { + googleHome.addToRollup = function(rollupGroup, handler, rollupName) { + if (!rollup[rollupGroup]) { + console.log('Creating the rollup group: ', rollupGroup); + rollup[rollupGroup] = []; + } + rollup[rollupGroup].push({handler: handler, name: rollupName}); + }; + + googleHome.getRollup = function(rollupGroup, sbx, slots, locale, callback) { + var handlers = _.map(rollup[rollupGroup], 'handler'); + console.log('Rollup array for ', rollupGroup); + console.log(rollup[rollupGroup]); + var nHandlers = []; + _.each(handlers, function (handler) { + nHandlers.push(handler.bind(null, slots, sbx)); + }); + async.parallelLimit(nHandlers, 10, function(err, results) { + if (err) { + console.error('Error: ', err); + } + callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); + }); + }; + + // This creates the expected Google Home response + googleHome.buildSpeechletResponse = function buildSpeechletResponse(output, expectUserResponse) { return { - fulfillmentText: output -// , fulfillmentMessages: [output] - , source: 'Nightscout' + payload: { + google: { + expectUserResponse: expectUserResponse, + richResponse: { + items: [ + { + simpleResponse: { + textToSpeech: output + } + } + ] + } + } + } }; }; return googleHome; } - - module.exports = init; \ No newline at end of file + +module.exports = init; \ No newline at end of file From b39eb8f672cea607119e9af1bed85d30e1b12f57 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 19:49:32 -0600 Subject: [PATCH 010/134] Changed order of operations --- lib/api/googlehome/index.js | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index 5cd791d0488..5de70cae25e 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -41,30 +41,6 @@ function configure (app, wares, ctx, env) { } }); - api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Google Home'); - var locale = req.body.queryResult.languageCode; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); - if (handler){ - var sbx = initializeSandbox(); - handler(function (title, response) { - res.json(ctx.googleHome.buildSpeechletResponse(response, false)); - next( ); - }, req.body.queryResult.parameters, sbx); - } else { - res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); - next( ); - } - }); - ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { entries.list({count: 1}, function (err, records) { var direction; @@ -117,6 +93,30 @@ function configure (app, wares, ctx, env) { return sbx; } + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = req.body.queryResult.languageCode; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); + if (handler){ + var sbx = initializeSandbox(); + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); + next( ); + } + }); + return api; } From 8a200151e0b6ef12171d9b6be9efb8a4c92bd214 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 20:10:27 -0600 Subject: [PATCH 011/134] Fixed parameter referencing in googlehome --- lib/api/googlehome/index.js | 48 ++++++++++++++++++------------------- lib/plugins/googlehome.js | 18 +++++++------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index 5de70cae25e..5cd791d0488 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -41,6 +41,30 @@ function configure (app, wares, ctx, env) { } }); + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = req.body.queryResult.languageCode; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); + if (handler){ + var sbx = initializeSandbox(); + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); + next( ); + } + }); + ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { entries.list({count: 1}, function (err, records) { var direction; @@ -93,30 +117,6 @@ function configure (app, wares, ctx, env) { return sbx; } - api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Google Home'); - var locale = req.body.queryResult.languageCode; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); - if (handler){ - var sbx = initializeSandbox(); - handler(function (title, response) { - res.json(ctx.googleHome.buildSpeechletResponse(response, false)); - next( ); - }, req.body.queryResult.parameters, sbx); - } else { - res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); - next( ); - } - }); - return api; } diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js index 27f7d98c7ac..2f721cf887d 100644 --- a/lib/plugins/googlehome.js +++ b/lib/plugins/googlehome.js @@ -33,17 +33,17 @@ function init (env, ctx) { } }; - // This function retrieves a handler based on the intent name and slots requested. - googleHome.getIntentHandler = function getIntentHandler(intentName, slots) { + // This function retrieves a handler based on the intent name and parameters requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, parameters) { if (intentName && intentHandlers[intentName]) { - if (slots) { - var slotKeys = Object.keys(slots); - for (var i = 0, len = slotKeys.length; i < len; i++) { - if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { + if (parameters) { + var parameterKeys = Object.keys(parameters); + for (var i = 0, len = parameterKeys.length; i < len; i++) { + if (intentHandlers[intentName][parameterKeys[i]] && parameters[parameterKeys[i]] && + intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]] && + intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]].handler) { - return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; + return intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]].handler; } } } From 90562a336de375147230a4fe0b239cccba92272e Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 20:21:28 -0600 Subject: [PATCH 012/134] Yet another CONTRIBUTING fix --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79266e47c82..a4674b241bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,7 +251,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Čeština (`cs`) |Please volunteer|OK | | Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK | | Dansk (`dk`) | [@janrpn] |OK | -| Ελληνικά `el`)|Please volunteer|Needs attention: 68.5%| +| Ελληνικά (`el`)|Please volunteer|Needs attention: 68.5%| | English (`en`)|Please volunteer|OK| | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| From 855b64dac02fa482884595d5a6d938610c51bea6 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 20:30:52 -0600 Subject: [PATCH 013/134] Removed extra google stuff --- lib/plugins/basalprofile.js | 14 -------------- lib/plugins/cob.js | 19 ------------------- lib/plugins/iob.js | 15 --------------- lib/plugins/openaps.js | 24 ------------------------ 4 files changed, 72 deletions(-) diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 10f27145fdc..5d29cda3d13 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -156,20 +156,6 @@ function init (ctx) { }] }; - function googleHomeCurrentBasalhandler (result, next, sbx) { - var pwd = result.parameters && result.parameters.givenName ? result.parameters.givenName : null; - next(basalMessage(pwd, sbx)); - } - - basal.googleHome = { - intentHandlers: [{ - intent: 'CurrentMetric' - , routableSlot:'metric' - , slots:['basal', 'current basal'] - , intentHandler: googleHomeCurrentBasalhandler - }] - }; - return basal; } diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 8706e182444..51af7391e40 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -308,25 +308,6 @@ function init (ctx) { }] }; - function googleHomeCOBHandler(result, next, sbx) { - var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has' : 'You have'; - var value = 'no'; - if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { - value = Math.round(sbx.properties.cob.cob); - } - var response = preamble + ' ' + value + ' carbohydrates on board'; - next(response); - } - - cob.googleHome = { - intentHandlers: [{ - intent: 'CurrentMetric' - , routableSlot:'metric' - , slots:['cob', 'carbs on board', 'carbohydrates on board', 'carbohydrates'] - , intentHandler: googleHomeCOBHandler - }] - }; - return cob; } diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 0b9a5ff5576..12d4548e81e 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -288,21 +288,6 @@ function init(ctx) { }] }; - function googleHomeIOBIntentHandler(result, next, sbx) { - var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has ' : 'You have '; - var message = preamble + getIob(sbx) + ' insulin on board'; - next(message); - } - - iob.googleHome = { - intentHandlers: [{ - intent: 'CurrentMetric' - , routableSlot: 'metric' - , slots: ['iob', 'insulin on board', 'insulin'] - , intentHandler: googleHomeIOBIntentHandler - }] - }; - return iob; } diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 1ae4741285e..e28bf1453ce 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -543,30 +543,6 @@ function init (ctx) { }] }; - function googleHomeForecastHandler(response, next, sbx) { - if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { - var response = 'The Open APS eventual BG is ' + sbx.properties.openaps.lastEventualBG; - next(response); - } - } - - function googleHomeLastLoopHandler (response, next, sbx) { - var response = 'The last successful loop was ' + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)); - next(response); - } - - openaps.googleHome = { - intentHandlers: [{ - intent: 'CurrentMetric' - , routableSlot: 'metric' - , slots: ['openaps', 'openaps forecast', 'forecast'] - , intentHandler: googleHomeForecastHandler - }, { - intent: 'LastLoop' - , intentHandler: googleHomeLastLoopHandler - }] - }; - function statusClass (prop, prefs, sbx) { var level = statusLevel(prop, prefs, sbx); return levels.toStatusClass(level); From 223806701c3069076a974ac22f3dc4f3542d1392 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 21:55:42 -0600 Subject: [PATCH 014/134] Migrated standalone intents to MetricNow intent --- lib/plugins/basalprofile.js | 4 ++-- lib/plugins/pump.js | 31 ++++++++++++++++++++++++------- lib/plugins/upbat.js | 17 +++++++++++++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 5d29cda3d13..06a2ebd288d 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -150,8 +150,8 @@ function init (ctx) { }], intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['basal', 'current basal'] + , routableSlot: 'metric' + , slots: ['basal', 'current basal'] , intentHandler: virtAsstCurrentBasalhandler }] }; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index b63c69bb2c3..6b8c0a7c76e 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -160,13 +160,30 @@ function init (ctx) { } pump.virtAsst = { - intentHandlers:[{ - intent: 'InsulinRemaining', - intentHandler: virtAsstReservoirHandler - }, { - intent: 'PumpBattery', - intentHandler: virtAsstBatteryHandler - }] + intentHandlers:[ + { + // backwards compatibility + intent: 'InsulinRemaining', + intentHandler: virtAsstReservoirHandler + } + , { + // backwards compatibility + intent: 'PumpBattery', + intentHandler: virtAsstBatteryHandler + } + , { + intent: 'MetricNow' + , routableSlot: 'metric' + , slots: ['pump reservoir'] + , intentHandler: virtAsstReservoirHandler + } + , { + intent: 'MetricNow' + , routableSlot: 'metric' + , slots: ['pump battery'] + , intentHandler: virtAsstBatteryHandler + } + ] }; function statusClass (level) { diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 72c3bce2021..b21bc190a77 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -227,10 +227,19 @@ function init() { } upbat.virtAsst = { - intentHandlers: [{ - intent: 'UploaderBattery' - , intentHandler: virtAsstUploaderBatteryHandler - }] + intentHandlers: [ + { + // for backwards compatibility + intent: 'UploaderBattery' + , intentHandler: virtAsstUploaderBatteryHandler + } + , { + intent: 'MetricNow' + , routableSlot: 'metric' + , slots: ['uploader battery'] + , intentHandler: virtAsstUploaderBatteryHandler + } + ] }; return upbat; From b3a47b36e02d70af40bbe0099186e5cd7b321381 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 23:39:17 -0600 Subject: [PATCH 015/134] Simplified route handling --- lib/api/alexa/index.js | 10 ++++---- lib/api/googlehome/index.js | 4 ++-- lib/plugins/alexa.js | 47 +++++++++++++------------------------ lib/plugins/ar2.js | 3 +-- lib/plugins/basalprofile.js | 4 +--- lib/plugins/cob.js | 3 +-- lib/plugins/googlehome.js | 44 ++++++++++++---------------------- lib/plugins/iob.js | 3 +-- lib/plugins/loop.js | 3 +-- lib/plugins/openaps.js | 3 +-- lib/plugins/pump.js | 6 ++--- lib/plugins/rawbg.js | 3 +-- lib/plugins/upbat.js | 3 +-- 13 files changed, 48 insertions(+), 88 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 61f343a38f6..c919d4544ff 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -22,7 +22,7 @@ function configure (app, wares, ctx, env) { console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); _each(plugin.virtAsst.intentHandlers, function (route) { if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); + ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.metrics); } }); } @@ -111,7 +111,7 @@ function configure (app, wares, ctx, env) { callback('Current blood glucose', status); }); - }, 'metric', ['bg', 'blood glucose', 'number']); + }, ['bg', 'blood glucose', 'number']); ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { @@ -135,12 +135,12 @@ function configure (app, wares, ctx, env) { } function handleIntent(intentName, slots, next) { - var handler = ctx.alexa.getIntentHandler(intentName, slots); + var handler = ctx.alexa.getIntentHandler(intentName, slots.metric.value); if (handler){ var sbx = initializeSandbox(); handler(next, slots, sbx); } else { - next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); + next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); } } @@ -154,4 +154,4 @@ function configure (app, wares, ctx, env) { return api; } -module.exports = configure; +module.exports = configure; \ No newline at end of file diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index 5cd791d0488..35571e94193 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -22,7 +22,7 @@ function configure (app, wares, ctx, env) { console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); _each(plugin.virtAsst.intentHandlers, function (route) { if (route) { - ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); + ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.metrics); } }); } @@ -102,7 +102,7 @@ function configure (app, wares, ctx, env) { callback('Current blood glucose', status); }); - }, 'metric', ['bg', 'blood glucose', 'number']); + }, ['bg', 'blood glucose', 'number']); ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index bfb8cf6308f..65721cd3193 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -9,52 +9,38 @@ function init (env, ctx) { var intentHandlers = {}; var rollup = {}; - // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues - // are the values that determine the routing. This allows for specific intent handlers based on the value of a - // specific slot. Routing is only supported on one slot for now. - // There is no protection for a previously configured handler - one plugin can overwrite the handler of another - // plugin. - alexa.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (! intentHandlers[intent]) { + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + alexa.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { intentHandlers[intent] = {}; } - if (routableSlot && slotValues) { - for (var i = 0, len = slotValues.length; i < len; i++) { - if (! intentHandlers[intent][routableSlot]) { - intentHandlers[intent][routableSlot] = {}; + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; } - if (!intentHandlers[intent][routableSlot][slotValues[i]]) { - intentHandlers[intent][routableSlot][slotValues[i]] = {}; + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; } - intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + intentHandlers[intent][metrics[i]].handler = handler; } } else { intentHandlers[intent].handler = handler; } }; - // This function retrieves a handler based on the intent name and slots requested. - alexa.getIntentHandler = function getIntentHandler(intentName, slots) { + // This function retrieves a handler based on the intent name and metric requested. + alexa.getIntentHandler = function getIntentHandler(intentName, metric) { if (intentName && intentHandlers[intentName]) { - if (slots) { - var slotKeys = Object.keys(slots); - for (var i = 0, len = slotKeys.length; i < len; i++) { - if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { - - return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; - } - } - } - if (intentHandlers[intentName].handler) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { return intentHandlers[intentName].handler; } return null; } else { return null; } - }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { @@ -63,7 +49,6 @@ function init (env, ctx) { rollup[rollupGroup] = []; } rollup[rollupGroup].push({handler: handler, name: rollupName}); - // status = _.orderBy(status, ['priority'], ['asc']) }; alexa.getRollup = function(rollupGroup, sbx, slots, locale, callback) { @@ -110,4 +95,4 @@ function init (env, ctx) { return alexa; } -module.exports = init; +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 24fe192cac3..78b0d10e0ba 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -173,8 +173,7 @@ function init (ctx) { ar2.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['ar2 forecast', 'forecast'] + , metrics: ['ar2 forecast', 'forecast'] , intentHandler: virtAsstAr2Handler }] }; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 06a2ebd288d..e229ff0fac4 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -150,8 +150,7 @@ function init (ctx) { }], intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['basal', 'current basal'] + , metrics: ['basal', 'current basal'] , intentHandler: virtAsstCurrentBasalhandler }] }; @@ -159,5 +158,4 @@ function init (ctx) { return basal; } - module.exports = init; diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 51af7391e40..3bed71a6c36 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -302,8 +302,7 @@ function init (ctx) { cob.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['cob', 'carbs on board', 'carbohydrates on board'] + , metrics: ['cob', 'carbs on board', 'carbohydrates on board'] , intentHandler: virtAsstCOBHandler }] }; diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js index 2f721cf887d..b0d5230ff0d 100644 --- a/lib/plugins/googlehome.js +++ b/lib/plugins/googlehome.js @@ -9,52 +9,38 @@ function init (env, ctx) { var intentHandlers = {}; var rollup = {}; - // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues - // are the values that determine the routing. This allows for specific intent handlers based on the value of a - // specific slot. Routing is only supported on one slot for now. - // There is no protection for a previously configured handler - one plugin can overwrite the handler of another - // plugin. - googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (! intentHandlers[intent]) { + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { intentHandlers[intent] = {}; } - if (routableSlot && slotValues) { - for (var i = 0, len = slotValues.length; i < len; i++) { - if (! intentHandlers[intent][routableSlot]) { - intentHandlers[intent][routableSlot] = {}; + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (! intentHandlers[intent]) { + intentHandlers[intent] = {}; } - if (!intentHandlers[intent][routableSlot][slotValues[i]]) { - intentHandlers[intent][routableSlot][slotValues[i]] = {}; + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; } - intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + intentHandlers[intent][metrics[i]].handler = handler; } } else { intentHandlers[intent].handler = handler; } }; - // This function retrieves a handler based on the intent name and parameters requested. - googleHome.getIntentHandler = function getIntentHandler(intentName, parameters) { + // This function retrieves a handler based on the intent name and metric requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { if (intentName && intentHandlers[intentName]) { - if (parameters) { - var parameterKeys = Object.keys(parameters); - for (var i = 0, len = parameterKeys.length; i < len; i++) { - if (intentHandlers[intentName][parameterKeys[i]] && parameters[parameterKeys[i]] && - intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]] && - intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]].handler) { - - return intentHandlers[intentName][parameterKeys[i]][parameters[parameterKeys[i]]].handler; - } - } - } - if (intentHandlers[intentName].handler) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { return intentHandlers[intentName].handler; } return null; } else { return null; } - }; googleHome.addToRollup = function(rollupGroup, handler, rollupName) { diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 12d4548e81e..567968cd5e2 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -282,8 +282,7 @@ function init(ctx) { }] , intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['iob', 'insulin on board'] + , metrics: ['iob', 'insulin on board'] , intentHandler: virtAsstIOBIntentHandler }] }; diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index c94e969a7ca..7b30eafb644 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -474,8 +474,7 @@ function init (ctx) { loop.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['loop forecast', 'forecast'] + , metrics: ['loop forecast', 'forecast'] , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index e28bf1453ce..0865b3352fe 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -534,8 +534,7 @@ function init (ctx) { openaps.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['openaps forecast', 'forecast'] + , metrics: ['openaps forecast', 'forecast'] , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 6b8c0a7c76e..a2d95a68c9d 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -173,14 +173,12 @@ function init (ctx) { } , { intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['pump reservoir'] + , metrics: ['pump reservoir'] , intentHandler: virtAsstReservoirHandler } , { intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['pump battery'] + , metrics: ['pump battery'] , intentHandler: virtAsstBatteryHandler } ] diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 2bfea16ba73..91ebdb849dd 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -114,8 +114,7 @@ function init (ctx) { rawbg.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['raw bg', 'raw blood glucose'] + , metrics:['raw bg', 'raw blood glucose'] , intentHandler: virtAsstRawBGHandler }] }; diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index b21bc190a77..509d9347f89 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -235,8 +235,7 @@ function init() { } , { intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['uploader battery'] + , metrics: ['uploader battery'] , intentHandler: virtAsstUploaderBatteryHandler } ] From 79f021701140c3ff0d00226f7d180ed041d8b60a Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 7 Sep 2019 23:55:26 -0600 Subject: [PATCH 016/134] Added logging --- lib/plugins/alexa.js | 10 +++++++--- lib/plugins/googlehome.js | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index 65721cd3193..d41aa567885 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -16,29 +16,33 @@ function init (env, ctx) { } if (metrics) { for (var i = 0, len = metrics.length; i < len; i++) { - if (!intentHandlers[intent]) { - intentHandlers[intent] = {}; - } if (!intentHandlers[intent][metrics[i]]) { intentHandlers[intent][metrics[i]] = {}; } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); intentHandlers[intent][metrics[i]].handler = handler; } } else { + console.log('Storing handler for intent \'' + intent + '\''); intentHandlers[intent].handler = handler; } }; // This function retrieves a handler based on the intent name and metric requested. alexa.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); if (intentName && intentHandlers[intentName]) { if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); return intentHandlers[intentName][metric].handler } else if (intentHandlers[intentName].handler) { + console.log('Found!'); return intentHandlers[intentName].handler; } + console.log('Not found!'); return null; } else { + console.log('Not found!'); return null; } }; diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js index b0d5230ff0d..8e8181512c8 100644 --- a/lib/plugins/googlehome.js +++ b/lib/plugins/googlehome.js @@ -16,29 +16,33 @@ function init (env, ctx) { } if (metrics) { for (var i = 0, len = metrics.length; i < len; i++) { - if (! intentHandlers[intent]) { - intentHandlers[intent] = {}; - } if (!intentHandlers[intent][metrics[i]]) { intentHandlers[intent][metrics[i]] = {}; } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); intentHandlers[intent][metrics[i]].handler = handler; } } else { + console.log('Storing handler for intent \'' + intent + '\''); intentHandlers[intent].handler = handler; } }; // This function retrieves a handler based on the intent name and metric requested. googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); if (intentName && intentHandlers[intentName]) { if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); return intentHandlers[intentName][metric].handler } else if (intentHandlers[intentName].handler) { + console.log('Found!'); return intentHandlers[intentName].handler; } + console.log('Not found!'); return null; } else { + console.log('Not found!'); return null; } }; From cc56ef822dbbb91feec6950e8376abddd2c7d9ee Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sun, 8 Sep 2019 00:03:32 -0600 Subject: [PATCH 017/134] Added forgotten path selector --- lib/api/googlehome/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index 35571e94193..a18a2b17371 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -52,7 +52,7 @@ function configure (app, wares, ctx, env) { moment.locale(locale); } - var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters); + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric); if (handler){ var sbx = initializeSandbox(); handler(function (title, response) { From 0b1ce34b25b5f0483697aae1a523e08882c84100 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sun, 8 Sep 2019 00:48:28 -0600 Subject: [PATCH 018/134] Separated instructions for adding virtual assistant support in a plugin --- ...add-virtual-assistant-support-to-plugin.md | 52 +++++++++++++++ docs/plugins/alexa-plugin.md | 63 ++----------------- docs/plugins/googlehome-plugin.md | 6 +- 3 files changed, 61 insertions(+), 60 deletions(-) create mode 100644 docs/plugins/add-virtual-assistant-support-to-plugin.md diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md new file mode 100644 index 00000000000..eccb9b4ae33 --- /dev/null +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -0,0 +1,52 @@ +Adding Virtual Assistant Support to a Plugin +========================================= + +To add virtual assistant support to a plugin, the `init` method should return an object that contains a `virtAsst` key. Here is an example: + +```javascript +iob.virtAsst = { + intentHandlers: [{ + intent: "MetricNow" + , metrics: ["iob"] + , intentHandler: virtAsstIOBIntentHandler + }] + , rollupHandlers: [{ + rollupGroup: "Status" + , rollupName: "current iob" + , rollupHandler: virtAsstIOBRollupHandler + }] +}; +``` + +There are 2 types of handlers that you will need to supply: +* Intent handler - enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### Intent Handlers + +A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). ++ `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) ++ `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Make sure to add the metric name and its synonyms to the list of metrics used by the virtual assistant(s). + - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! + - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. This remains an array type for backwards compatibility. ++ `intenthandler` - This is a callback function that receives 3 arguments + - `callback` Call this at the end of your function. It requires 2 arguments + - `title` - Title of the handler. This is the value that will be displayed on the Alexa card. The Google Home response doesn't currently display a card, so it doesn't use this value. + - `text` - This is text that the virtual assistant should speak (and show, if the user is using a device with a screen). + - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected. + - `sandbox` - This is the Nightscout sandbox that allows access to various functions. + +### Rollup handlers + +A plugin can also expose multiple rollup handlers ++ `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked ++ `rollupName` - This is the name of the handler. Primarily used for debugging ++ `rollupHandler` - this is a callback function that receives 3 arguments + - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot + - `sandbox` - This is the nightscout sandbox that allows access to various functions. + - `callback` - + - `error` - This would be an error message + - `response` - A simple object that expects a `results` string and a `priority` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: + ```javascript + callback(null, {results: "Hello world", priority: 1}); + ``` diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index a5dcb886e9c..73466678a17 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -41,9 +41,9 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p ### Get an Amazon Developer account -- Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. -- [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. -- Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). +1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. +1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. +1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). ### Create a new Alexa skill @@ -291,59 +291,4 @@ If your device is [registered](https://developer.amazon.com/docs/devconsole/test ## Adding Alexa support to a plugin -This document assumes some familiarity with the Alexa interface. You can find more information [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide). - -To add alexa support to a plugin the ``init`` should return an object that contains an "alexa" key. Here is an example: - -```javascript -var iob = { - name: 'iob' - , label: 'Insulin-on-Board' - , pluginType: 'pill-major' - , alexa : { - rollupHandlers: [{ - rollupGroup: "Status" - , rollupName: "current iob" - , rollupHandler: alexaIOBRollupHandler - }] - , intentHandlers: [{ - intent: "MetricNow" - , routableSlot: "metric" - , slots: ["iob", "insulin on board"] - , intentHandler: alexaIOBIntentHandler - }] - } -}; -``` - -There are 2 types of handlers that you will need to supply: -* Intent handler - enables you to "teach" Alexa how to respond to a user's question. -* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the Alexa "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. - -### Intent Handlers - -A plugin can expose multiple intent handlers. -+ ``intent`` - this is the intent in the "intent schema" above -+ ``routeableSlot`` - This enables routing by a slot name to the appropriate intent handler for overloaded intents e.g. "What is my " - iob, bg, cob, etc. This value should match the slot named in the "intent schema" -+ ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot -+ ``intenthandler`` - this is a callback function that receives 3 arguments - - ``callback`` Call this at the end of your function. It requires 2 arguments - - ``title`` - Title of the handler. This is the value that will be displayed on the Alexa card - - ``text`` - This is text that Alexa should speak. - - ``slots`` - these are the slots that Alexa detected - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - -### Rollup handlers - -A plugin can also expose multiple rollup handlers -+ ``rollupGroup`` - This is the key that is used to aggregate the responses when the intent is invoked -+ ``rollupName`` - This is the name of the handler. Primarily used for debugging -+ ``rollupHandler`` - this is a callback function that receives 3 arguments - - ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - - ``callback`` - - - ``error`` - This would be an error message - - ``response`` - A simple object that expects a ``results`` string and a ``priority`` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: - ```javascript - callback(null, {results: "Hello world", priority: 1}); - ``` +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 6bc55b01e62..e4ae349832a 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -31,4 +31,8 @@ To add Google Home support for your Nightscout site, here's what you need to do: 6. Go to "Fullfillment" menu and enter details about your webhook. 7. SAVE 8. Go to "Integration" menu and select your desired integration. -9. Follow instructions for each desired integration. \ No newline at end of file +9. Follow instructions for each desired integration. + +## Adding Google Home support to a plugin + +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file From 0d85439030c8457a0164c2cbd868d0ea1a70bb79 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sun, 8 Sep 2019 00:56:10 -0600 Subject: [PATCH 019/134] A few typo fixes --- ...add-virtual-assistant-support-to-plugin.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md index eccb9b4ae33..764cfc7c4ea 100644 --- a/docs/plugins/add-virtual-assistant-support-to-plugin.md +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -1,7 +1,7 @@ Adding Virtual Assistant Support to a Plugin ========================================= -To add virtual assistant support to a plugin, the `init` method should return an object that contains a `virtAsst` key. Here is an example: +To add virtual assistant support to a plugin, the `init` method of the plugin should return an object that contains a `virtAsst` key. Here is an example: ```javascript iob.virtAsst = { @@ -19,21 +19,21 @@ iob.virtAsst = { ``` There are 2 types of handlers that you will need to supply: -* Intent handler - enables you to "teach" the virtual assistant how to respond to a user's question. -* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. +* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. ### Intent Handlers -A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). +A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). Each intent handler should be structured as follows: + `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) + `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Make sure to add the metric name and its synonyms to the list of metrics used by the virtual assistant(s). - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! - - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. This remains an array type for backwards compatibility. -+ `intenthandler` - This is a callback function that receives 3 arguments - - `callback` Call this at the end of your function. It requires 2 arguments - - `title` - Title of the handler. This is the value that will be displayed on the Alexa card. The Google Home response doesn't currently display a card, so it doesn't use this value. - - `text` - This is text that the virtual assistant should speak (and show, if the user is using a device with a screen). - - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected. + - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. We keep this value as an array for backwards compatibility. ++ `intenthandler` - This is a callback function that receives 3 arguments: + - `callback` Call this at the end of your function. It requires 2 arguments: + - `title` - Title of the handler. This is the value that will be displayed on the Alexa card (for devices with a screen). The Google Home response doesn't currently display a card, so it doesn't use this value. + - `text` - This is text that the virtual assistant should speak (and show, for devices with a screen). + - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected (e.g. `pwd` as seen in the templates is a slot/parameter. `metric` is technically a slot, too). - `sandbox` - This is the Nightscout sandbox that allows access to various functions. ### Rollup handlers @@ -41,7 +41,7 @@ A plugin can expose multiple intent handlers (e.g. useful when it can supply mul A plugin can also expose multiple rollup handlers + `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked + `rollupName` - This is the name of the handler. Primarily used for debugging -+ `rollupHandler` - this is a callback function that receives 3 arguments ++ `rollupHandler` - This is a callback function that receives 3 arguments - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot - `sandbox` - This is the nightscout sandbox that allows access to various functions. - `callback` - From f74d6a8c956d598d979a1b2f1aea33ebd2b0158c Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sun, 8 Sep 2019 01:00:33 -0600 Subject: [PATCH 020/134] Improved logging --- lib/api/alexa/index.js | 6 +++--- lib/api/googlehome/index.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index c919d4544ff..6669a894823 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -19,7 +19,7 @@ function configure (app, wares, ctx, env) { ctx.plugins.eachEnabledPlugin(function each(plugin){ if (plugin.virtAsst) { if (plugin.virtAsst.intentHandlers) { - console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); + console.log('Alexa: Plugin ' + plugin.name + ' supports Virtual Assistants'); _each(plugin.virtAsst.intentHandlers, function (route) { if (route) { ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.metrics); @@ -27,7 +27,7 @@ function configure (app, wares, ctx, env) { }); } if (plugin.virtAsst.rollupHandlers) { - console.log('Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + console.log('Alexa: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); _each(plugin.virtAsst.rollupHandlers, function (route) { console.log('Route'); console.log(route); @@ -37,7 +37,7 @@ function configure (app, wares, ctx, env) { }); } } else { - console.log('Plugin ' + plugin.name + ' does not support Virtual Assistants'); + console.log('Alexa: Plugin ' + plugin.name + ' does not support Virtual Assistants'); } }); diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index a18a2b17371..b03ac42bfe3 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -19,7 +19,7 @@ function configure (app, wares, ctx, env) { ctx.plugins.eachEnabledPlugin(function each(plugin){ if (plugin.virtAsst) { if (plugin.virtAsst.intentHandlers) { - console.log('Plugin ' + plugin.name + ' supports Virtual Assistants'); + console.log('Google Home: Plugin ' + plugin.name + ' supports Virtual Assistants'); _each(plugin.virtAsst.intentHandlers, function (route) { if (route) { ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.metrics); @@ -27,7 +27,7 @@ function configure (app, wares, ctx, env) { }); } if (plugin.virtAsst.rollupHandlers) { - console.log('Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + console.log('Google Home: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); _each(plugin.virtAsst.rollupHandlers, function (route) { console.log('Route'); console.log(route); @@ -37,7 +37,7 @@ function configure (app, wares, ctx, env) { }); } } else { - console.log('Plugin ' + plugin.name + ' does not support Virtual Assistants'); + console.log('Google Home: Plugin ' + plugin.name + ' does not support Virtual Assistants'); } }); From c266cab040474c252217576558d59370373f6579 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 9 Sep 2019 17:52:55 -0600 Subject: [PATCH 021/134] Updated Google Home plugin instructions --- .../googlehome-nightscout-template.zip | Bin 0 -> 12637 bytes docs/plugins/googlehome-plugin.md | 50 ++++++++++-------- 2 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 docs/plugins/googlehome-nightscout-template.zip diff --git a/docs/plugins/googlehome-nightscout-template.zip b/docs/plugins/googlehome-nightscout-template.zip new file mode 100644 index 0000000000000000000000000000000000000000..2cc2216888e07fa04e3090c9e89c6a5402a489ac GIT binary patch literal 12637 zcmdT~cRbbY`#<(bMwuZaLUfGml-00jHrd-b$T72yl}*TsWR)#tg^Z9Sju4qy8D%D< z!tba@eRvLfp5OP6Ctiv_&ilHr>v~`FzLjJ#F!4cKU!lHiir;_v>z_j)VvvHRxdqI@ z*wzuosisK)8tZV>8(a}qr!c?nP{{q zMg#-VG#y1;+;K@sKE!E_j_cQ_U2SF3K?&rMMwa4Q{Vx?PE5GUSdfwomuyC7yJYf4; z<<*@qdyJymTg6%MSSsih?Jq}(vA;4{4JlV@HIc;<^cb5{fbFZsvaZ-cS zFbk7%KPC&{kH+(ONj!Z+chpN{S(>znA>kxmL$m=eRfjZ2YTHKTi?FYgw>Jy?L*=Rb zxD%K?#oYOmqgV;d8dj;&n^}YIdoI*itG(qsJ(*qp4xJ_)BEK!!$9TdX)ZI@%@<>e6F5ABR+U*VfRc?DH}2H)(LH%w|-cn|M9cN>@ggLcZGa z;XV=3QYAz_Pua&=DanZ1^=!>!&toNS_aY@-Hyx!DxDyPA`>t62!Vyt_5 zDyQTbM$@FVR>3%DZf$Fa5L=sSUhD&_(Tf;}JBYbZ4y{mYldgf8AJw59FVBAApjtM(8}$f)wIJF4YBj9mD#5so(} z=L}9%T0&h}un~t%PuHH5F<(krCAG^U8-`=ryVup=cQu{%ETa}M6KI8YFAo+5c}2bN z^-yug!JZ$$b-T5aZ|hq@Du?6PRxKPXufS9F@wO-k_f|D~xdBU?UfmnAc#Xq)m7*7# zv-MI#QZ~_*WN~osW=K32#Q=d?u|eM>@<%A6`|S*6JY+=b>&uHvDynM!4CJj~-VS8c zA0q{R3`HscBA`wM^m`zH_4m#S#s*}@<_1Ov3-WU63q*wUe2=sGn@@>X+ zDI`V1*LVpT$I+gxEb6O>jR~I^>ucn>`Ob$l(LgMr>TU?6_k1>%7PgOAxYuf^OgJ7%=dtpOTJM#c`cU&5v$qTN z8`@uwoxepC7$se8{b(&%H8b`(Pq8=x_uQI53wk7vY|l(xvT8UqI^fe1DP2yMC%zWp zN=agS$i(<<=+n*_A{I4|`iJUSs^R=)`1xtclh~G@87ur7@$J{=#Q4qK8dxme@^)OX z%pZ;J#;2RcqWEA{mq_aFI-Wu%JU8`i!~WYhSV|Y6&JDV)h_hp|xMGV20xhHOm(OVS z2!Y)s`Fn!eO%kC4kih!?fG_{y82gc)cWcjqIz$vyRbhrOM+bdJ2h(c~hOQ3!rcjgt z!WYg>qqqLL(_>rPkIrh^<^PQe4}lj#~fEb?WAKAp|cKo*C=GhU~R?Pg{4lDgkha%i$~@N zRrFY78tvaVl?oi_{ogjlALsk6(hR6t+o7|qFiL;x-E4tAhAxJPuwz5Wr`dx z=x|POMVg*q+c*P_qKTpm$>>|_vz9Ih>Nk_!dXD-Q00?*e(&WYcjcW44pDFqW!!aF8@28!B#mM1|a3~-B7UgKONgIgPpH@m7 zi4mGyUlZr8-b3N9#Giqe!-^k?x%QshR7uBBR}ymqE?|()YMV2tP9QMV^+kMi`t%iv zI8WjBZ{hvqV(M3W-5u?ds`y`~ao?xv=VF5)0=2vYNO&+L)NOR)$h3lJxyK~$Eeehq z_q7n~-EK=IPELz3RGXJkg5O(|)YX|}Hp(`5v*OKZ9A4D9zB*nbGcRUs#mkqRv~uJt zXZr~gveA)F2~X=`?;Z;$d`mrcwHvLWLv|F|a?k6Z^`=cL_|RQ;lROzGxP4?fWCQ48Jadc!rO-FfC*fmRI1zTs7TE143wN2t8uAbD;%xWSo;^3%! zQmm3A#m=YPb;C`{fpP|3g3e!Z!$w1S<9b*x1)HUt1^X>uQ?IuoB4Y_7BYs6*{|s8Do;cXQ$D`ISy9wm5yzjUEf6O;(K;7%b#s>9tNz`qX{Yfb&R5#3_L}{7G`<`OEG*C` z)ycV+8P@=lRd}E5Cli;xKAxH3!FhW2m_35#9VFw)NiNy@odi$)?)&Qm3%+EKk+?dp zmDFms$QW@4QYhYZ{aU-elQBZZHR9AKJ-$BkR&9|U6H(<&)sC{t`i9&HnlMq^W!Dhw zm6YuFwfuqZuen;vKbr>hDp>g8>ricVL*c*z=P@JI27%cjpQ zO&wdhMSV79*1%_e_z>j0dLr0FO*$pP<1jW<<6Z51>|8$SVOH41a*rp`P2YQtvzy}n z63Ac_$JwvT->Q{AXDAej6Rf{KSO2((+@m>=GjxE-+1lFuMsv`Lk1Q5`^B%#><`_lt zklEhL#3C`rOPk3LO%t8Gc+uTCI{tX9X+nZ+X)EZwC+JO<7Dk?mCf$cdUr}EmOkT)NjcD9r$%k;M7EvePgFNKV9CTJb&--oTzM_n7Yk=?=Q|Fc8* zZF~H21lXfP*j5;2M%KHjXY0d78Zf_3CSTadC933XZDeE=YuZwfM1iFM*L0J*9)IT9 z=6P+(G~LL~NeH6nIxY9JkeC`C@Vimw#2{A9rbk@y2ytxoB3;Z}OVT=FnVv;hThm-M zIdeJ1bwkSt&5|o!eutS=eMMQa@I0IOXY4*Tc;_qWEM%dD>aEw@^5$QqP+|Sh>7piu zhbLa)sxbXpCjK4+`ci}J19cN$tNP##Y`s$gQi8`Nm|+w5Fk0!#^FdJqY?brE1ovB_ zw0I|Y49~{m4)Pe0-!<`j=`D3_McqWG7t`A4nZMka>(j5rW#9^dWFOL!vT+779#FFSwv?evJ?m%0s-P|kv>ed2-?p@Jh()L(^{I!q@DH_DHvlNDxHcKABqoTT`2HP09(QX0=OagClW3 zPK$%L0x2Xf!(UuX&}>Y~d{qbUQfqyrZChdq;~@_AxpuP1<%xNoy2Uz3ZiVb>ltcOP zUMihn0%$Imi!hZh{<5%3YMoev-o+T(tAQ84q4|-1QB1z4p-T_&P*u~59%L3*s9$lE3MVK5dszF2w)5C9*$krM_p8~!tBCuA*#~)l8pB!7Q z-aIy!HO07^9?|BE+`H-ooUQDq)2e^{q1ZJ8Hq`HodxYCHyH#50v6cn$gcql6aoaWhjtlwXLS#D#&>sk5LA&ab} zQh0*~CytN5N&cOz=%cmDxAKU~w$6iWz8Z8nSm!%}aw+c+8ArXJIFVl}YcsvjZxMJK zI*4OFcD?NM8jVj;ui$O-=N3&os@lG6#Bjz!de%?F{ypJJILV*bFPwyAJ+5W!E>BCB zeKXhjYE3|~^)_PpME;0IkC#qU>)NeX+4K-LHwv}KcsSrxGQOs=W-ch>cCKFEJB8`HX6^f)U17=kqfQz4V_9^4g|gw{el zgIn5cTKA!-JY%v~y4&DD%viS@IFD3}04LlUx4WgoEzsh0od5f25jn8L;?*Hh$56cf z0hn(*dS$_dP!2X_5>Z+ojLZkH`yP|N3DI${>{wp*HpELkxyqLQ66=$$1o^|b8cH

F(EV~D=}c@g z+Vlm>94&AM*+VSY5jM@6sv>bzM@b4O1}yAOgi2u%MawCRhP`8Im8G7_J0#K6LD_dE zu}1Q-0%lxD25e=4b}KxY8gQ%mkj=Buk^RC`@WAl=eX9QFgLC&qD(xQ1gZ2Mfx@_Nz zZtt}J!y2OA)o)!C0Y)O^qTUi_Y5Gr=^{4hgfPA0-k^4vF8o+eVJ!F`wc(^YYLDec5 z`Q)9Ia{X*KyIRu|{**$}?8SXGJ%{=;#S$6ORLoAU$jK?-c-wlQ<*-*=c+GMJ8>8aA zV5;H5%ndNB{V@_ve3%vs$w!itE*B7A)`cdl${L@}>b@47z}AklA}kJ&v{E)}zuy*F zWvTawZMi_1z>Cl4Ci(-Mde&Ce=Qod^^u#lN*OzBA`BZ{0K=Y&DM>KX?8>drg<{gNq z*YtJHV}Fpm)@{z%TXyqH*YX#N1}UBxHxn2;say5Us$oCj;-N225wxjd*G$>ZN{E`S zpT+VJ=1ceI(FI+OD`);VHRG0G1yNe`ViO6y<%>Ub*S&~5xJlTn&NEwYm2Ek^h@tn) zB1Ip;qdVUM5b~RL)fijxnd0UVPk}6-&;Kf)sBilAqUhf|dGJP^Zx6QrgaYgTo>TwB zQGj}7N4GDBQI1P$p>>2$>4~xC*MkEdF{!hs4o$yq5PkD7mjSI$NhxpfZHfBPCv8=( zt7#kw;-#Dk-n7zhEC{WcuJ#PZ3Ivj|NW$j+Y|bh|A5;OhlIz)3?1> z>sjQ~i5SJXiwXu!oWdOm9~W>K8i&N0uDF6k6Nfgdy-U_hgsd|M1W-T_b;(p@vZ-VsfWBz#{K!MNlA7KPztDc^ z2YlW@4?xR+U)Td}=ev#Fs{Trgz?TdZ0OVOX8`ci*x0b-8G6DcI z;PFU6REmT3_s2%fm7@UcS#dAqs~UhUx9`ZdJ+#%Y?LH{*0vikjgV&-4+n!5NzDL+? z5Y%L%0mxYwzm1F9AiGb$n-ROsECfK!fWJEc^?-}=-HQC83h<_u10Zr1K8An^aFnH6V0zQbPIUG|>M+E(cT4!WlRhn$5&_CSN|v0K>9 zD%3?6Xoz+389LO4_}kGBs=9oDqi4Vc=nsJ1dzjv>#jlDBobl)Y;%4F845;n?!%_!E zH;^Yl7i7Sx8GloceP!(C;&H&?GvN3qP|E;LX}ihz!zjCPfxW&rd=`kib2I|NVIkW) R5Qq`^?Ts8anwYl!`afE$LbU(@ literal 0 HcmV?d00001 diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index e4ae349832a..148ecd0dfa9 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -5,33 +5,41 @@ Nightscout Google Home/DialogFlow Plugin To add Google Home support for your Nightscout site, here's what you need to do: -1. Activate the `googlehome` plugin on your Nightscout site, so your site will respond correctly to Google's requests. -2. Create a custom DialogFlow agent that points at your site and defines certain questions you want to be able to ask. (You'll copy and paste a basic template for this, to keep things simple.) -3. Create desired integrations with DialogFlow - +1. [Activate the `googlehome` plugin](#activate-the-nightscout-google-home-plugin) on your Nightscout site, so your site will respond correctly to Google's requests. +1. [Create a custom DialogFlow agent](#create-your-dialogflow-agent) that points at your site and defines certain questions you want to be able to ask. ## Activate the Nightscout Google Home Plugin -1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. . -2. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version VERSION_NUMBER (VERSION_NAME)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/VERSION_NUMBER) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) ## Create Your DialogFlow Agent -### Signin to DialogFlow - -- Sign in to DialogFlow with your Google account (https://console.dialogflow.com/api-client/#/login). If you don't already have one, signup with Google. - -### Create a new custom DialogFlow agent - -1. Select "Create new agent" in the main menu bar. -2. Input a custom name for your agent and click "CREATE". -3. Download the simple agent template : ( https://drive.google.com/drive/folders/18z2kQSEInvH4O_jfjB4Qh8z9508P9Oao?usp=sharing ) -4. Select IMPORT FROM ZIP , in order to import the template. -5. SAVE -6. Go to "Fullfillment" menu and enter details about your webhook. -7. SAVE -8. Go to "Integration" menu and select your desired integration. -9. Follow instructions for each desired integration. +1. Download the [Google Home Nightscout agent template](googlehome-nightscout-template.zip). +1. [Sign in to Google's Action Console](https://console.actions.google.com) +1. Click on the "New Project" button. +1. If prompted, agree to the Terms of Service. +1. Give your project a name (e.g. "Nightscout") and then click "Create project". +1. Click any of the "development experience" options (it really doesn't matter). +1. Click on the "Develop" tab at the top of the sreen +1. Click on "Invocation" in the left navigation pane. +1. Set the display name (e.g. "Nightscout") and set your Google Assistant voice. + - Unfortunately, the name needs to be two words, and has to be unique across all of Google, even though you won't be publishing for everyone on Google to use. So you'll have to be creative in the name since "Night Scout" is already taken. +1. Click "Save" in the upper right corner. +1. Navigate to "Actions" in the left nagivation pane, then click on the "Add your first action" button. +1. Make sure you're on "Cutom intent" and then click "Build" to open DialogFlow in a new tab. +1. Sign in with the same Google account you used to sign in to the Actions Console. + - You'll have to go through the account setup steps if this is your first time using DialogFlow. +1. Verify the name for your agent (e.g. "Nightscout") and click "CREATE". +1. In the navigation pane on the left, click the gear icon next to your agent name. +1. Click on the "Export and Import" tab in the main area of the page. +1. Click the "IMPORT FROM ZIP" button. +1. Select the template file downloaded in step 1. +1. Type "IMPORT" where requested and then click the "IMPORT" button. +1. After the import finishes, click the "DONE" button followed by the "SAVE" button. +1. In the navigation pane on the left, click on "Fulfillment". +1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` +1. Scroll down to the bottom of the page and click the "SAVE" button. ## Adding Google Home support to a plugin From 895407dc19b6995ca6427cce155b22cac4657466 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 9 Sep 2019 17:56:15 -0600 Subject: [PATCH 022/134] Attempt to trigger download of template file --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 148ecd0dfa9..50b7e56ae22 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -15,7 +15,7 @@ To add Google Home support for your Nightscout site, here's what you need to do: ## Create Your DialogFlow Agent -1. Download the [Google Home Nightscout agent template](googlehome-nightscout-template.zip). +1. Download the [Google Home Nightscout agent template](googlehome-nightscout-template.zip?raw=true). 1. [Sign in to Google's Action Console](https://console.actions.google.com) 1. Click on the "New Project" button. 1. If prompted, agree to the Terms of Service. From 478b25bdd320823bd5c2a7c8cda3634c1f53aef0 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 9 Sep 2019 17:59:48 -0600 Subject: [PATCH 023/134] Small wording tweaks --- docs/plugins/googlehome-plugin.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 50b7e56ae22..a2aebe0fd5e 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -15,8 +15,9 @@ To add Google Home support for your Nightscout site, here's what you need to do: ## Create Your DialogFlow Agent -1. Download the [Google Home Nightscout agent template](googlehome-nightscout-template.zip?raw=true). +1. Download the [Nightscout agent template for Google Home](googlehome-nightscout-template.zip?raw=true). 1. [Sign in to Google's Action Console](https://console.actions.google.com) + - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. 1. Click on the "New Project" button. 1. If prompted, agree to the Terms of Service. 1. Give your project a name (e.g. "Nightscout") and then click "Create project". @@ -41,6 +42,8 @@ To add Google Home support for your Nightscout site, here's what you need to do: 1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` 1. Scroll down to the bottom of the page and click the "SAVE" button. +That's it! Now try asking Google "Hey Google, ask Nightscout how am I doing?" + ## Adding Google Home support to a plugin See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file From c562e7d06a633beeab911ef411f4b91e254ea9fa Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 9 Sep 2019 20:32:12 -0600 Subject: [PATCH 024/134] Updated Alexa plugin documentation --- docs/plugins/alexa-plugin.md | 135 +++++++++++++++++------------------ 1 file changed, 66 insertions(+), 69 deletions(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 73466678a17..65930f6954b 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -73,20 +73,6 @@ To get up and running with a basic interaction model, which will allow you to as "How am I doing" ] }, - { - "name": "UploaderBattery", - "slots": [], - "samples": [ - "How is my uploader battery" - ] - }, - { - "name": "PumpBattery", - "slots": [], - "samples": [ - "How is my pump battery" - ] - }, { "name": "LastLoop", "slots": [], @@ -107,25 +93,22 @@ To get up and running with a basic interaction model, which will allow you to as } ], "samples": [ - "What is my {metric}", - "What my {metric} is", - "What is {pwd} {metric}" + "how is {metric}", + "how is my {metric}", + "how is {pwd} {metric}", + "how my {metric} is", + "what is {metric}", + "how much {metric} do I have", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "what is my {metric}", + "what my {metric} is", + "what is {pwd} {metric}" ] }, { - "name": "InsulinRemaining", - "slots": [ - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "How much insulin do I have left", - "How much insulin do I have remaining", - "How much insulin does {pwd} have left", - "How much insulin does {pwd} have remaining" - ] + "name": "AMAZON.NavigateHomeIntent", + "samples": [] } ], "types": [ @@ -134,77 +117,91 @@ To get up and running with a basic interaction model, which will allow you to as "values": [ { "name": { - "value": "bg" - } - }, - { - "name": { - "value": "blood glucose" - } - }, - { - "name": { - "value": "number" - } - }, - { - "name": { - "value": "iob" - } - }, - { - "name": { - "value": "insulin on board" - } - }, - { - "name": { - "value": "current basal" - } - }, - { - "name": { - "value": "basal" + "value": "uploader battery", + "synonyms": [ + "uploader battery remaining", + "uploader battery power" + ] } }, { "name": { - "value": "cob" + "value": "pump reservoir", + "synonyms": [ + "remaining insulin", + "insulin remaining", + "insulin is left", + "insulin left", + "insulin in my pump", + "insulin" + ] } }, { "name": { - "value": "carbs on board" + "value": "pump battery", + "synonyms": [ + "pump battery remaining", + "pump battery power" + ] } }, { "name": { - "value": "carbohydrates on board" + "value": "bg", + "synonyms": [ + "number", + "blood sugar", + "blood glucose" + ] } }, { "name": { - "value": "loop forecast" + "value": "iob", + "synonyms": [ + "insulin on board" + ] } }, { "name": { - "value": "ar2 forecast" + "value": "basal", + "synonyms": [ + "current basil", + "basil", + "current basal" + ] } }, { "name": { - "value": "forecast" + "value": "cob", + "synonyms": [ + "carbs", + "carbs on board", + "carboydrates", + "carbohydrates on board" + ] } }, { "name": { - "value": "raw bg" + "value": "forecast", + "synonyms": [ + "ar2 forecast", + "loop forecast" + ] } }, { "name": { - "value": "raw blood glucose" + "value": "raw bg", + "synonyms": [ + "raw number", + "raw blood sugar", + "raw blood glucose" + ] } } ] From 3563158aaea64ab7de47c87d010f4e847f1b95e8 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 00:06:24 -0600 Subject: [PATCH 025/134] Updated test files --- tests/ar2.test.js | 2 -- tests/basalprofileplugin.test.js | 3 --- tests/cob.test.js | 2 -- tests/iob.test.js | 3 --- tests/loop.test.js | 2 -- tests/openaps.test.js | 2 -- tests/pump.test.js | 16 +++++++++++++--- tests/rawbg.test.js | 2 -- tests/upbat.test.js | 12 ++++++++---- 9 files changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 8fee3ec1c63..6e62b6be20a 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -154,8 +154,6 @@ describe('ar2', function ( ) { ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); - ar2.virtAsst.intentHandlers.length.should.equal(1); - ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index fa97f84274e..3f1e5d5cfd3 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -92,9 +92,6 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; - basal.virtAsst.intentHandlers.length.should.equal(1); - basal.virtAsst.rollupHandlers.length.should.equal(1); - basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); diff --git a/tests/cob.test.js b/tests/cob.test.js index 54fbcb6c50d..8d1f2fdd5c6 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -110,8 +110,6 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); - cob.virtAsst.intentHandlers.length.should.equal(1); - cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); diff --git a/tests/iob.test.js b/tests/iob.test.js index b6c5c2430ec..b10cfd0bcde 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -20,9 +20,6 @@ describe('IOB', function() { } }; - iob.virtAsst.intentHandlers.length.should.equal(1); - iob.virtAsst.rollupHandlers.length.should.equal(1); - iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); diff --git a/tests/loop.test.js b/tests/loop.test.js index 8506de4555b..2b3585c172f 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -255,8 +255,6 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); - loop.virtAsst.intentHandlers.length.should.equal(2); - loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index 9bfc5161969..8d864428fe5 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -382,8 +382,6 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); - openaps.virtAsst.intentHandlers.length.should.equal(2); - openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); diff --git a/tests/pump.test.js b/tests/pump.test.js index afc96481976..5f924662d86 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -266,8 +266,6 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); - pump.virtAsst.intentHandlers.length.should.equal(2); - pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Remaining insulin'); response.should.equal('You have 86.4 units remaining'); @@ -275,7 +273,19 @@ describe('pump', function ( ) { pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { title.should.equal('Pump battery'); response.should.equal('Your pump battery is at 1.52 volts'); - done(); + + pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { + title.should.equal('Remaining insulin'); + response.should.equal('You have 86.4 units remaining'); + + pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { + title.should.equal('Pump battery'); + response.should.equal('Your pump battery is at 1.52 volts'); + done(); + }, [], sbx); + + }, [], sbx); + }, [], sbx); }, [], sbx); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index 48c21186cc5..b541d9b4427 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -42,8 +42,6 @@ describe('Raw BG', function ( ) { rawbg.setProperties(sbx); - rawbg.virtAsst.intentHandlers.length.should.equal(1); - rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 515d1a8d218..d575dc9d8bd 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -106,13 +106,17 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); - upbat.virtAsst.intentHandlers.length.should.equal(1); - upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Uploader battery'); response.should.equal('Your uploader battery is at 20%'); - - done(); + + upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Uploader battery'); + response.should.equal('Your uploader battery is at 20%'); + + done(); + }, [], sbx); + }, [], sbx); }); From 0a48c1d81a2ca724e3f112c8c91c692a7533c6c0 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 00:14:21 -0600 Subject: [PATCH 026/134] Re-added handler count tests so devs are prompted to write tests for new handlers --- tests/ar2.test.js | 2 ++ tests/basalprofileplugin.test.js | 3 +++ tests/cob.test.js | 2 ++ tests/iob.test.js | 3 +++ tests/loop.test.js | 2 ++ tests/openaps.test.js | 2 ++ tests/pump.test.js | 2 ++ tests/rawbg.test.js | 2 ++ tests/upbat.test.js | 2 ++ 9 files changed, 20 insertions(+) diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 6e62b6be20a..8fee3ec1c63 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -154,6 +154,8 @@ describe('ar2', function ( ) { ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); + ar2.virtAsst.intentHandlers.length.should.equal(1); + ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index 3f1e5d5cfd3..fa97f84274e 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -92,6 +92,9 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; + basal.virtAsst.intentHandlers.length.should.equal(1); + basal.virtAsst.rollupHandlers.length.should.equal(1); + basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); diff --git a/tests/cob.test.js b/tests/cob.test.js index 8d1f2fdd5c6..54fbcb6c50d 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -110,6 +110,8 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); + cob.virtAsst.intentHandlers.length.should.equal(1); + cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); diff --git a/tests/iob.test.js b/tests/iob.test.js index b10cfd0bcde..b6c5c2430ec 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -20,6 +20,9 @@ describe('IOB', function() { } }; + iob.virtAsst.intentHandlers.length.should.equal(1); + iob.virtAsst.rollupHandlers.length.should.equal(1); + iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); diff --git a/tests/loop.test.js b/tests/loop.test.js index 2b3585c172f..8506de4555b 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -255,6 +255,8 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); + loop.virtAsst.intentHandlers.length.should.equal(2); + loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index 8d864428fe5..9bfc5161969 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -382,6 +382,8 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); + openaps.virtAsst.intentHandlers.length.should.equal(2); + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); diff --git a/tests/pump.test.js b/tests/pump.test.js index 5f924662d86..483d5a49316 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -266,6 +266,8 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); + pump.virtAsst.intentHandlers.length.should.equal(4); + pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Remaining insulin'); response.should.equal('You have 86.4 units remaining'); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index b541d9b4427..48c21186cc5 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -42,6 +42,8 @@ describe('Raw BG', function ( ) { rawbg.setProperties(sbx); + rawbg.virtAsst.intentHandlers.length.should.equal(1); + rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index d575dc9d8bd..cac1439160c 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -106,6 +106,8 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); + upbat.virtAsst.intentHandlers.length.should.equal(2); + upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Uploader battery'); response.should.equal('Your uploader battery is at 20%'); From 658ca161b7c2806a3b5c8a98370c837fda0597e2 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 16:07:35 -0600 Subject: [PATCH 027/134] Updated Alexa documentation --- docs/plugins/alexa-plugin.md | 236 +++--------------- docs/plugins/alexa-templates/en-us.json | 212 ++++++++++++++++ .../interacting-with-virtual-assistants.md | 56 +++++ 3 files changed, 309 insertions(+), 195 deletions(-) create mode 100644 docs/plugins/alexa-templates/en-us.json create mode 100644 docs/plugins/interacting-with-virtual-assistants.md diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 65930f6954b..41b14966136 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -58,159 +58,9 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p Your Alexa skill's "interaction model" defines how your spoken questions get translated into requests to your Nightscout site, and how your Nightscout site's responses get translated into the audio responses that Alexa says back to you. -To get up and running with a basic interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code below. - -```json -{ - "interactionModel": { - "languageModel": { - "invocationName": "nightscout", - "intents": [ - { - "name": "NSStatus", - "slots": [], - "samples": [ - "How am I doing" - ] - }, - { - "name": "LastLoop", - "slots": [], - "samples": [ - "When was my last loop" - ] - }, - { - "name": "MetricNow", - "slots": [ - { - "name": "metric", - "type": "LIST_OF_METRICS" - }, - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "how is {metric}", - "how is my {metric}", - "how is {pwd} {metric}", - "how my {metric} is", - "what is {metric}", - "how much {metric} do I have", - "how much {metric} does {pwd} have", - "how much {metric} I have", - "what is my {metric}", - "what my {metric} is", - "what is {pwd} {metric}" - ] - }, - { - "name": "AMAZON.NavigateHomeIntent", - "samples": [] - } - ], - "types": [ - { - "name": "LIST_OF_METRICS", - "values": [ - { - "name": { - "value": "uploader battery", - "synonyms": [ - "uploader battery remaining", - "uploader battery power" - ] - } - }, - { - "name": { - "value": "pump reservoir", - "synonyms": [ - "remaining insulin", - "insulin remaining", - "insulin is left", - "insulin left", - "insulin in my pump", - "insulin" - ] - } - }, - { - "name": { - "value": "pump battery", - "synonyms": [ - "pump battery remaining", - "pump battery power" - ] - } - }, - { - "name": { - "value": "bg", - "synonyms": [ - "number", - "blood sugar", - "blood glucose" - ] - } - }, - { - "name": { - "value": "iob", - "synonyms": [ - "insulin on board" - ] - } - }, - { - "name": { - "value": "basal", - "synonyms": [ - "current basil", - "basil", - "current basal" - ] - } - }, - { - "name": { - "value": "cob", - "synonyms": [ - "carbs", - "carbs on board", - "carboydrates", - "carbohydrates on board" - ] - } - }, - { - "name": { - "value": "forecast", - "synonyms": [ - "ar2 forecast", - "loop forecast" - ] - } - }, - { - "name": { - "value": "raw bg", - "synonyms": [ - "raw number", - "raw blood sugar", - "raw blood glucose" - ] - } - } - ] - } - ] - } - } -} -``` +To get up and running with an interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code for your language from [the list of templates](alexa-templates/). + +- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. @@ -239,53 +89,49 @@ After you enable testing, you can also use the Alexa Simulator in the left colum ##### What questions can you ask it? -*Forecast:* - -- "Alexa, ask Nightscout how am I doing" -- "Alexa, ask Nightscout how I'm doing" - -*Uploader Battery:* - -- "Alexa, ask Nightscout how is my uploader battery" - -*Pump Battery:* - -- "Alexa, ask Nightscout how is my pump battery" - -*Metrics:* - -- "Alexa, ask Nightscout what my bg is" -- "Alexa, ask Nightscout what my blood glucose is" -- "Alexa, ask Nightscout what my number is" -- "Alexa, ask Nightscout what is my insulin on board" -- "Alexa, ask Nightscout what is my basal" -- "Alexa, ask Nightscout what is my current basal" -- "Alexa, ask Nightscout what is my cob" -- "Alexa, ask Nightscout what is Charlie's carbs on board" -- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" -- "Alexa, ask Nightscout what is Harper's loop forecast" -- "Alexa, ask Nightscout what is Alicia's ar2 forecast" -- "Alexa, ask Nightscout what is Peter's forecast" -- "Alexa, ask Nightscout what is Arden's raw bg" -- "Alexa, ask Nightscout what is Dana's raw blood glucose" - -*Insulin Remaining:* - -- "Alexa, ask Nightscout how much insulin do I have left" -- "Alexa, ask Nightscout how much insulin do I have remaining" -- "Alexa, ask Nightscout how much insulin does Dana have left? -- "Alexa, ask Nightscout how much insulin does Arden have remaining? - -*Last Loop:* - -- "Alexa, ask Nightscout when was my last loop" - -(Note: all the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name.) +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. ### Activate the skill on your Echo or other device If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. + +1. Open the Build tab of your Alexa Skill. + - Get to your list of Alexa Skills at [https://developer.amazon.com/alexa/console/ask] and click on the name of the skill. +1. Click on the language drop-down box in the upper right corner of the window. +1. Click "Language settings". +1. Add your desired language. +1. Click the "Save" button. +1. Navigate to "CUSTOM" in the left navigation pane. +1. Select your new language in the language drop-down box. +1. Go to "JSON Editor" (just above "Interfaces" in the left navigation pane). +1. Remove the existing contents in the text box, and copy and paste the configuration code from a familiar language in [the list of templates](alexa-templates/). +1. Click "Save Model". +1. Click the "Add" button next to the "Slot Types" section in the left pane. +1. Click the radio button for "Use an existing slot type from Alexa's built-in library" +1. In the search box just below that option, search for "first name" +1. If your language has an option, click the "Add Slot Type" button for that option. + - If your language doesn't have an option, you won't be able to ask Nightscout a question that includes a name. +1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): + 1. Click on the Intent name. + 1. Scroll down to the "Slots" section + 1. If there's a slot with the name "pwd", change the slot type to the one found above. + - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. + 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. + 1. Set the "Alexa speech prompts" in your language, and remove the old ones. + 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. + 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot in the previous step, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. Delete the phrase you're replacing. +1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. +1. For each metric listed, add synonyms in your language, and delete the old synonyms. + - What ever you do, **DO NOT** change the text in the "VALUE" column! Only change the synonyms. +1. Click "Save Model" at the top, and then click on "Build Model". +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. + ## Adding Alexa support to a plugin See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json new file mode 100644 index 00000000000..bb1ff0932c5 --- /dev/null +++ b/docs/plugins/alexa-templates/en-us.json @@ -0,0 +1,212 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "nightscout", + "intents": [ + { + "name": "NSStatus", + "slots": [], + "samples": [ + "How am I doing" + ] + }, + { + "name": "LastLoop", + "slots": [], + "samples": [ + "When was my last loop" + ] + }, + { + "name": "MetricNow", + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "samples": [ + "what {pwd} {metric} is", + "what my {metric} is", + "how {pwd} {metric} is", + "how my {metric} is", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "how much {metric}", + "{pwd} {metric}", + "{metric}", + "my {metric}" + ] + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "how is {metric}", + "how is my {metric}", + "how is {pwd} {metric}", + "how my {metric} is", + "what is {metric}", + "how much {metric} do I have", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "what is my {metric}", + "what my {metric} is", + "what is {pwd} {metric}" + ] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + } + ], + "types": [ + { + "name": "LIST_OF_METRICS", + "values": [ + { + "name": { + "value": "uploader battery", + "synonyms": [ + "uploader battery remaining", + "uploader battery power" + ] + } + }, + { + "name": { + "value": "pump reservoir", + "synonyms": [ + "remaining insulin", + "insulin remaining", + "insulin is left", + "insulin left", + "insulin in my pump", + "insulin" + ] + } + }, + { + "name": { + "value": "pump battery", + "synonyms": [ + "pump battery remaining", + "pump battery power" + ] + } + }, + { + "name": { + "value": "bg", + "synonyms": [ + "number", + "blood sugar", + "blood glucose" + ] + } + }, + { + "name": { + "value": "iob", + "synonyms": [ + "insulin on board" + ] + } + }, + { + "name": { + "value": "basal", + "synonyms": [ + "current basil", + "basil", + "current basal" + ] + } + }, + { + "name": { + "value": "cob", + "synonyms": [ + "carbs", + "carbs on board", + "carboydrates", + "carbohydrates on board" + ] + } + }, + { + "name": { + "value": "forecast", + "synonyms": [ + "ar2 forecast", + "loop forecast" + ] + } + }, + { + "name": { + "value": "raw bg", + "synonyms": [ + "raw number", + "raw blood sugar", + "raw blood glucose" + ] + } + } + ] + } + ] + }, + "dialog": { + "intents": [ + { + "name": "MetricNow", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.1421281086569.34001419564" + } + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME", + "confirmationRequired": false, + "elicitationRequired": false, + "prompts": {} + } + ] + } + ], + "delegationStrategy": "ALWAYS" + }, + "prompts": [ + { + "id": "Elicit.Slot.1421281086569.34001419564", + "variations": [ + { + "type": "PlainText", + "value": "What metric are you looking for?" + }, + { + "type": "PlainText", + "value": "What value are you looking for?" + }, + { + "type": "PlainText", + "value": "What metric do you want to know?" + }, + { + "type": "PlainText", + "value": "What value do you want to know?" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md new file mode 100644 index 00000000000..e22cd9f2e87 --- /dev/null +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -0,0 +1,56 @@ +Interacting with Virtual Assistants +=================================== + +# Alexa vs. Google Home + +Although these example phrases reference Alexa, the exact same questions could be asked of Google. +Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's name* ..." + +# What questions can you ask it? + +This list is not meant to be comprehensive, nor does it include every way you can ask the questions. In the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. + +*Forecast:* + +- "Alexa, ask Nightscout how am I doing" +- "Alexa, ask Nightscout how I'm doing" + +*Uploader Battery:* + +- "Alexa, ask Nightscout how is my uploader battery" + +*Pump Battery:* + +- "Alexa, ask Nightscout how is my pump battery" + +*Metrics:* + +- "Alexa, ask Nightscout what my bg is" +- "Alexa, ask Nightscout what my blood glucose is" +- "Alexa, ask Nightscout what my number is" +- "Alexa, ask Nightscout what is my insulin on board" +- "Alexa, ask Nightscout what is my basal" +- "Alexa, ask Nightscout what is my current basal" +- "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is Charlie's carbs on board" +- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" +- "Alexa, ask Nightscout what is Harper's loop forecast" +- "Alexa, ask Nightscout what is Alicia's ar2 forecast" +- "Alexa, ask Nightscout what is Peter's forecast" +- "Alexa, ask Nightscout what is Arden's raw bg" +- "Alexa, ask Nightscout what is Dana's raw blood glucose" + +*Insulin Remaining:* + +- "Alexa, ask Nightscout how much insulin do I have left" +- "Alexa, ask Nightscout how much insulin do I have remaining" +- "Alexa, ask Nightscout how much insulin does Dana have left? +- "Alexa, ask Nightscout how much insulin does Arden have remaining? + +*Last Loop:* + +- "Alexa, ask Nightscout when was my last loop" + +## A note about names + +All the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name. \ No newline at end of file From 2b3d783d7055de6410e2eedd38fe5f1276be759d Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 16:11:56 -0600 Subject: [PATCH 028/134] Small typo fix --- docs/plugins/alexa-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 41b14966136..9c6e9ecbc4c 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -62,7 +62,7 @@ To get up and running with an interaction model, which will allow you to ask Ale - If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. -Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". From 73d4e049364986345db6ff9a5a6eda6395c3b5b4 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 16:14:06 -0600 Subject: [PATCH 029/134] Clarification --- docs/plugins/interacting-with-virtual-assistants.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md index e22cd9f2e87..a9f2541d8d8 100644 --- a/docs/plugins/interacting-with-virtual-assistants.md +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -8,7 +8,7 @@ Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's na # What questions can you ask it? -This list is not meant to be comprehensive, nor does it include every way you can ask the questions. In the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. +This list is not meant to be comprehensive, nor does it include every way you can ask the questions. To get the full picture, in the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. *Forecast:* From f946f4f634777100d8177c46fc43fe07e1d62f4f Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 10 Sep 2019 16:22:06 -0600 Subject: [PATCH 030/134] Further clarifications and typos --- docs/plugins/alexa-plugin.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 9c6e9ecbc4c..dd2be406559 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -97,7 +97,7 @@ If your device is [registered](https://developer.amazon.com/docs/devconsole/test ## Adding support for additional languages -If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. @@ -120,17 +120,18 @@ If you add support for another language, please consider [making a pull request] 1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): 1. Click on the Intent name. 1. Scroll down to the "Slots" section - 1. If there's a slot with the name "pwd", change the slot type to the one found above. + 1. If there's a slot with the name "pwd", change the Slot Type to the one found above. - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. 1. Set the "Alexa speech prompts" in your language, and remove the old ones. 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. - 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot in the previous step, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. Delete the phrase you're replacing. + 1. Click on the Intent name (just to the left of "metric") to return to the previous screen. + 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. 1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. 1. For each metric listed, add synonyms in your language, and delete the old synonyms. - What ever you do, **DO NOT** change the text in the "VALUE" column! Only change the synonyms. 1. Click "Save Model" at the top, and then click on "Build Model". -1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. ## Adding Alexa support to a plugin From 6ff8aa39b7e714db0835cf20563d990f27d3345d Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 22:54:47 -0600 Subject: [PATCH 031/134] Added language info to Google Home plugin doc --- docs/plugins/alexa-plugin.md | 2 +- docs/plugins/google-home-templates/en-us.zip | Bin 0 -> 12265 bytes .../googlehome-nightscout-template.zip | Bin 12637 -> 0 bytes docs/plugins/googlehome-plugin.md | 57 +++++++++++++++++- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 docs/plugins/google-home-templates/en-us.zip delete mode 100644 docs/plugins/googlehome-nightscout-template.zip diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index dd2be406559..452d14fe5d2 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -129,7 +129,7 @@ If you add support for another language, please consider [making a pull request] 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. 1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. 1. For each metric listed, add synonyms in your language, and delete the old synonyms. - - What ever you do, **DO NOT** change the text in the "VALUE" column! Only change the synonyms. + - What ever you do, **DO NOT** change the text in the "VALUE" column! Nightscout will be looking for these exact values. Only change the synonyms. 1. Click "Save Model" at the top, and then click on "Build Model". 1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip new file mode 100644 index 0000000000000000000000000000000000000000..71dc40256bf9958fba2cef7d4c1cd35f4d25e718 GIT binary patch literal 12265 zcmdT~cRUs9|F>6mX2=d1*{i`J+4F>B@61DT%oNAU-dT~%jF3b{D%r_aR!T-jju8>p z_#U}*94B?}@At>ec^Q9r-k<05`Mf{tc^(aAOe_Ml?XRfdlj`69`1=PA8Y!BUBd@E= zDQyrTnjVjb(FV7N(N}Z|G!6I~3N$?UrvPaU>E0SMYb!?xpPh@7@~%a z#8Bn5OT$6y`G$FdI!{DtCVks=#S^T(`U2Gcbrc*q#0}UX)bjx*Gukxq~z7b zrlA+C^n_>2GtP`6Vm@`X#x(po|LulN;UvE(GDe|>RR_0o_F8$>Qc;GTU6YLJTc`U7 ztv6~Q;!E-ZjBDurO;IPhpD|*4zhq8Yj=}cf0nta{#*wM4>j_W)1@H|ZY-`+vh$jy_ zytM9|r>rShnM)X{c1>~6VDA5u0R{~E&zJNY25t}IScI#Q=&~0U+RhZav^=;Ni)pcY$EbEDU z`Uv|}J2ZZGb!2u!^_w#g(_X)Z`=M!{zVQv+uqwdmbA*Dy0TgP(WucTqT&&q@LcGuK zl(v5PCg^YzOSgoO3S*AC;2m1~3H3e+F4`b`9ib1R9TwFh{6U755ef@{!ec6-7x*Vz zxsC{b-F(t$Q}}iEzTlUXs3Mo-YlEM!Z9uM$mL%Q^+%j`66VFQP@K;$11(dw!HP9yq zzkDK>#Z=Gtk{3g}%k8sqepGM(MnC|yeaL!s_oZSW>8s=JA&uE>t5t^D+}+2&t-9aJ z$+YvOAd&CZiDREBn8EK=ePQv$RkAu>`m+#~O|eMH9Zh{}(yD}-i^fM>^R-oY3qiJ> zQgm+~F}Dj=5;v8k)H>T>M=3ps9X1Dd@gGfFqjudmtHHEF@=Zd%?87k0qg)msFUif~ zC>5ghQ#o@}it`ve*R#nXEHM3{W2*gpH!jLS--Bx>AZw6<3m+a_CMK!?S#=<2FDAF+ zayKSXFC#%*wm+Z|7o!enx`T`POnx5Q7N(RI)~4pB^QtO3^CUXK8on6OG?*Nubm$8# zOgIZA{G{0QvNC>mRH7qkr5*fp1IU>9Qc}|k6G0l$aRs`d6i|GUZftVmD~&uSQ&Te= zaEiI3S$uq~B`j4lKK+&*Y;t_9Wj*lk9|`N~xA2pehQ>){-ji*w z+kp?XGz5EuI)pSv3b4OR$`UK~I0lzObEqT+RF*tU=zrLjs0#-1o-J)1A@J;fSm8kS z1J{AI=dCW8xjI0YbgdjLoWNF0ir+C&x(BRPqlO_sTD?eV)o!YTj%j@64qnKGECKrP z8(nQOvQrKNdNHJ%Llsr`!zypIE>(*Om8A7QqEU`laRhbwKm5SBTwIk;^UBdjcx0$v znz(W*nxsV9chZD7R;`2X88q4%m-du!se}!b&QwU&G_Pmgufx8HinJZ$?5b*;1!Fo{ z>F^a{VwN%VhUEnlE$LB7*3p4_!I*JBiWF1n zKGI!|JVoc0l91`==K0utz}7G2meoAQ35i-GVkRBydX{t8MWwB|kFEUZL@l^GBnHi# zuO`ghBnggHd}d#^8VbxwXcBw~XvaIdD%OG#EvVc(QG)?3Uxp?imse?Q69Ubn<|aqK^oQGSen-Y`?Ko2G$ceZ_G-IT6@)T*nAM~I&WJv z68DyXaSEIIvt3OJg}3KeI;G_7{p{BsaKlYhXH|8P9*vPIfl`=Tnh%2keg1V09;#07Ig#|X<2lazm#=pG(>%A-X3l2gwfs|CJ z%~)lA&K;$Pltodk(gq@&uNoiNJ>l%fkD=eh=S?F_<}*6Y!A1OmudBG_$QYqmM_6f8 zsxtBOkBfd;r8M^P65ynIr-5s;{DTY=uCc`K22?#G;2G1^WqS{f8W0)t2FW&YtlNPY z3GhTYcxbW|Jk006ZG}J1N2eRX=N12sgm@e4Q!qOn$|1*|j-%nW6*K0(Dyi#VMMhI9zUCVSQZBU8K=WfHGT zcC1GZQc3F=_j|iKr#=;Zohfjac95S7(jKhm8$>3EDWl_HkVvT)!XPjzo4X)BYBA74 zYILhDgETEO%1nDsNdua@AZuta!ET;!+PCa`$|AC~dt+tnxze1py`8W~TIw?SU#B`) zEh$GncFSD1ANK9FaU-xb;?};=8vgz=b-qedZ9{+Nl$sx-yqD~WM5%2;YQ?630Psw9 z>F-R2Iu=j-4k1xG>5n5J{Q(lHT0ySZTBtd>qg3#Xwz5+yKk2gxT~J;lX(tN8nh7cjsC0RfwZ%yUB!$JhFjIv&kfpkPA+Vbf=8Fj*)Absv?TB^bPo~;PwwL>s zcCXga@vM1=sJ`A(G$zf}d3$x{G)7^9ynAA!dYlAf0VC`AitVDN?;`M8Yi3Kqr@9+K zY~C2O)Mn(3`39F*Qe{VEZih8PBwH-PV~3YO?Zc0Bj#0=%VG`(A>7o(s^@9T(7I{yV zduZHAv_CiNS~yrhhC29f-xctzA-LGD2N2=shb&lO*R6*@0TrFRg%6*_6<7v}I{86I zMyJxh^x-b68!ZC}U<}0oGS=4Fc;hV?lTYn<=ak!6>8VbAPG`{IZQV8U;F4k!3WI{& zoWweavf6Z7fKp=C##m1FwNt#XW6tgLQcmecd6 zUx)^KzY%CD0aMiLwcZoGnCn_mb09X`(5N9@6(dXT2&`UbvNTRA7zVd(OA()<*eDo$zzK* z=}xD^O#SADkATnVr0`j4E2bx3Bf@di9e**GFk3`H#0imme62dJ@q0sYdJ7OJ0}nyK z-y;y}Fx)PQdn3^POvz_*I8Fb!I6PSXRm@xXz}&S^c5BR1RWPhy zp7c!o@rq`uBU2q<^0JryEWe{ixi+?W2H$K9i62b{;EaOjV2jFm*8e$dNQq)K)^)5i#8fTwTu|KB1 zYGxY+D!hkVM#Y-N00gckdbt7=Y?@NP@2p719@ zv(pK9LxSd1cPs;5`zl;n*0Gf6$FetX2viyMeEHYIM|_oHX?_&tnNg1LCLsDeu#$PU zXpv9=*a5DpeRNL#!_-IdwURDLjbsc}K;i|1fZ4?*Lgz@2wFl9}L^JbLt#@SxsFWzL z3ss9f4Y|k@+g)5L*X@rbMPy4vJGA+ol8#b9ecAt87jUSX{x}+*JkSN~j#?MY930Hy zhb4ziY`Dd#XG&Yv<+SnXEq150=y*jcpds(^<`%wJ)3wjmAZDTNt_- zOf4%9eI=I+s!z@7tbz7ux0dNUmD@rDNkjdvoGA6Ew!Wuhvw^0vOlchJ^7wc^twAWE zVinzwZ?=*DPyG-U1VF8BFaz7SE` zfCXAQ@a4F$-B~z$)+m&AouCEBs7G?jOYm*Q(I(ZRI_Ibt-qwkjK4X)FR~IDoPpx%+ zODKqtlb`!4X3cS}2P>x(i>Cz`LZUV`{2`TcwGM45XlpV;y!}CURK){U-$Z@A9Mf~gRPoCn+ra&`eaE{MuH`FM+#}s$;K(LFf?b}c zEv8Sb7_V~4p?#%wVqxQ6!Lx+If={j~&x=0UT0wr!DZ+WHEJmD^N zhP(pgJ$vJkY-&wcS7Jp}Mc+)JHZ}E|NkN*bAh$WLUi)eJ*N+`+gs)fq<&sBPUM0Cn zkDDkW+Ne6NELFBz^+B~=-pPH4%U_qV0Q+25NFmK_5{uaBPpm~1$_`WWgEqmp9EWhN zN3TBOU8VOc?H9jg-DJ}!2-NrIB89S)FmWyo2lhs4;HE8cpFaW4d-8(i?c>ZW<-XbO z&Q&q>)?4jgS&KgE_WBq!wyxgn%x3~~c~NUW!N=vxK!^701N>-bP1v6s%Pep|WU)+S zwpO|Ws;Q?98osHaXL&_uu)C)J)cvAq<@naGrB7m$joxY0p$yf7#OE~V!02IC z>akc0woxJ_aKV&{5`~|aNu&~6OGb!_CSUOje%6a@A)WUPX4X)^G)Ek~kGjsfcm5R%N=$dq-p%yJ!Z4Ql85` zQNv0M%Z4n^Gi(PXTOHmr5qMK9L4H_J3LO@dzt7VDd`Kc+P|_UKo6qF$gA?Q{%m1)) zs1MV(Z*6u49QaAPEyUL9pPZ(%A6yLnKEuP$3*n=@)lbh2BY*%$e}2-Z6O`O-5?}jD z&P(vz&qxTTE`puUoBmK1nCm+#uHgGn2vf*nv>Sccz{z(yR8;FLzIMcMuxqNOl%$)* z+)Z&+JO}JW8y$qNB|7}kxNaIkN>wK%I!9kSeT2NOE_y!>p{BCyxS@4Eo`B7DH+r$xeb*E`Z#uwqNcVmjG53B6f8Yw*9?q&g4X7MHm__UeLLys_(s z0d}<)Q5NH?%g1?)UJAv?C-w~L*h<%L<;>&h@SXJV{~|HSdE905>4sKSq%J*=h4KUI zMiUz$mC8kRZ^spqiPPH=Npcha2?M-LZv0&)QD3>CVCDfY{`%mX{~*8jQ20!K&!PWe z-lLxIkNu3gD5oFo@EYQmOr+Rz8=*mEY&zT-@2B3>N%cJ_WJVv*(73nopji%;^T#NKjzNiuy7u6p5yI*F{Y&})_tVuOdnqGkgz0xS$5fU~>B-7BhHM{K_iL*5?ZH8B?=4itO7eF_i}{e}X+#7De& zphv)mLBGNAcfxVkE_>6|FHsTAj_B8HXguj5yZ(Ivh==Hi0$@;IGURRV9!>r~5yuxk zj{cN5|4J6(@#}u#xF7wQGXKa0`-yXu@;4OtB|hSj*~xvx*@WZoC(dqiLw$Jqd*l~E zkdY0gfIm0;sT2OKIK%@7P6TliA&5VpQ<3-n!!(F9)G-7}FsK{T0UPg_=9jUG$OJ@l zXG0B`e{PT6-5&~%Of*`AEoVdJSPsA=4nU|0f((h+Q2RnzSrH)jrVd0TYv~`FzLjJ#F!4cKU!lHiir;_v>z_j)VvvHRxdqI@ z*wzuosisK)8tZV>8(a}qr!c?nP{{q zMg#-VG#y1;+;K@sKE!E_j_cQ_U2SF3K?&rMMwa4Q{Vx?PE5GUSdfwomuyC7yJYf4; z<<*@qdyJymTg6%MSSsih?Jq}(vA;4{4JlV@HIc;<^cb5{fbFZsvaZ-cS zFbk7%KPC&{kH+(ONj!Z+chpN{S(>znA>kxmL$m=eRfjZ2YTHKTi?FYgw>Jy?L*=Rb zxD%K?#oYOmqgV;d8dj;&n^}YIdoI*itG(qsJ(*qp4xJ_)BEK!!$9TdX)ZI@%@<>e6F5ABR+U*VfRc?DH}2H)(LH%w|-cn|M9cN>@ggLcZGa z;XV=3QYAz_Pua&=DanZ1^=!>!&toNS_aY@-Hyx!DxDyPA`>t62!Vyt_5 zDyQTbM$@FVR>3%DZf$Fa5L=sSUhD&_(Tf;}JBYbZ4y{mYldgf8AJw59FVBAApjtM(8}$f)wIJF4YBj9mD#5so(} z=L}9%T0&h}un~t%PuHH5F<(krCAG^U8-`=ryVup=cQu{%ETa}M6KI8YFAo+5c}2bN z^-yug!JZ$$b-T5aZ|hq@Du?6PRxKPXufS9F@wO-k_f|D~xdBU?UfmnAc#Xq)m7*7# zv-MI#QZ~_*WN~osW=K32#Q=d?u|eM>@<%A6`|S*6JY+=b>&uHvDynM!4CJj~-VS8c zA0q{R3`HscBA`wM^m`zH_4m#S#s*}@<_1Ov3-WU63q*wUe2=sGn@@>X+ zDI`V1*LVpT$I+gxEb6O>jR~I^>ucn>`Ob$l(LgMr>TU?6_k1>%7PgOAxYuf^OgJ7%=dtpOTJM#c`cU&5v$qTN z8`@uwoxepC7$se8{b(&%H8b`(Pq8=x_uQI53wk7vY|l(xvT8UqI^fe1DP2yMC%zWp zN=agS$i(<<=+n*_A{I4|`iJUSs^R=)`1xtclh~G@87ur7@$J{=#Q4qK8dxme@^)OX z%pZ;J#;2RcqWEA{mq_aFI-Wu%JU8`i!~WYhSV|Y6&JDV)h_hp|xMGV20xhHOm(OVS z2!Y)s`Fn!eO%kC4kih!?fG_{y82gc)cWcjqIz$vyRbhrOM+bdJ2h(c~hOQ3!rcjgt z!WYg>qqqLL(_>rPkIrh^<^PQe4}lj#~fEb?WAKAp|cKo*C=GhU~R?Pg{4lDgkha%i$~@N zRrFY78tvaVl?oi_{ogjlALsk6(hR6t+o7|qFiL;x-E4tAhAxJPuwz5Wr`dx z=x|POMVg*q+c*P_qKTpm$>>|_vz9Ih>Nk_!dXD-Q00?*e(&WYcjcW44pDFqW!!aF8@28!B#mM1|a3~-B7UgKONgIgPpH@m7 zi4mGyUlZr8-b3N9#Giqe!-^k?x%QshR7uBBR}ymqE?|()YMV2tP9QMV^+kMi`t%iv zI8WjBZ{hvqV(M3W-5u?ds`y`~ao?xv=VF5)0=2vYNO&+L)NOR)$h3lJxyK~$Eeehq z_q7n~-EK=IPELz3RGXJkg5O(|)YX|}Hp(`5v*OKZ9A4D9zB*nbGcRUs#mkqRv~uJt zXZr~gveA)F2~X=`?;Z;$d`mrcwHvLWLv|F|a?k6Z^`=cL_|RQ;lROzGxP4?fWCQ48Jadc!rO-FfC*fmRI1zTs7TE143wN2t8uAbD;%xWSo;^3%! zQmm3A#m=YPb;C`{fpP|3g3e!Z!$w1S<9b*x1)HUt1^X>uQ?IuoB4Y_7BYs6*{|s8Do;cXQ$D`ISy9wm5yzjUEf6O;(K;7%b#s>9tNz`qX{Yfb&R5#3_L}{7G`<`OEG*C` z)ycV+8P@=lRd}E5Cli;xKAxH3!FhW2m_35#9VFw)NiNy@odi$)?)&Qm3%+EKk+?dp zmDFms$QW@4QYhYZ{aU-elQBZZHR9AKJ-$BkR&9|U6H(<&)sC{t`i9&HnlMq^W!Dhw zm6YuFwfuqZuen;vKbr>hDp>g8>ricVL*c*z=P@JI27%cjpQ zO&wdhMSV79*1%_e_z>j0dLr0FO*$pP<1jW<<6Z51>|8$SVOH41a*rp`P2YQtvzy}n z63Ac_$JwvT->Q{AXDAej6Rf{KSO2((+@m>=GjxE-+1lFuMsv`Lk1Q5`^B%#><`_lt zklEhL#3C`rOPk3LO%t8Gc+uTCI{tX9X+nZ+X)EZwC+JO<7Dk?mCf$cdUr}EmOkT)NjcD9r$%k;M7EvePgFNKV9CTJb&--oTzM_n7Yk=?=Q|Fc8* zZF~H21lXfP*j5;2M%KHjXY0d78Zf_3CSTadC933XZDeE=YuZwfM1iFM*L0J*9)IT9 z=6P+(G~LL~NeH6nIxY9JkeC`C@Vimw#2{A9rbk@y2ytxoB3;Z}OVT=FnVv;hThm-M zIdeJ1bwkSt&5|o!eutS=eMMQa@I0IOXY4*Tc;_qWEM%dD>aEw@^5$QqP+|Sh>7piu zhbLa)sxbXpCjK4+`ci}J19cN$tNP##Y`s$gQi8`Nm|+w5Fk0!#^FdJqY?brE1ovB_ zw0I|Y49~{m4)Pe0-!<`j=`D3_McqWG7t`A4nZMka>(j5rW#9^dWFOL!vT+779#FFSwv?evJ?m%0s-P|kv>ed2-?p@Jh()L(^{I!q@DH_DHvlNDxHcKABqoTT`2HP09(QX0=OagClW3 zPK$%L0x2Xf!(UuX&}>Y~d{qbUQfqyrZChdq;~@_AxpuP1<%xNoy2Uz3ZiVb>ltcOP zUMihn0%$Imi!hZh{<5%3YMoev-o+T(tAQ84q4|-1QB1z4p-T_&P*u~59%L3*s9$lE3MVK5dszF2w)5C9*$krM_p8~!tBCuA*#~)l8pB!7Q z-aIy!HO07^9?|BE+`H-ooUQDq)2e^{q1ZJ8Hq`HodxYCHyH#50v6cn$gcql6aoaWhjtlwXLS#D#&>sk5LA&ab} zQh0*~CytN5N&cOz=%cmDxAKU~w$6iWz8Z8nSm!%}aw+c+8ArXJIFVl}YcsvjZxMJK zI*4OFcD?NM8jVj;ui$O-=N3&os@lG6#Bjz!de%?F{ypJJILV*bFPwyAJ+5W!E>BCB zeKXhjYE3|~^)_PpME;0IkC#qU>)NeX+4K-LHwv}KcsSrxGQOs=W-ch>cCKFEJB8`HX6^f)U17=kqfQz4V_9^4g|gw{el zgIn5cTKA!-JY%v~y4&DD%viS@IFD3}04LlUx4WgoEzsh0od5f25jn8L;?*Hh$56cf z0hn(*dS$_dP!2X_5>Z+ojLZkH`yP|N3DI${>{wp*HpELkxyqLQ66=$$1o^|b8cH

F(EV~D=}c@g z+Vlm>94&AM*+VSY5jM@6sv>bzM@b4O1}yAOgi2u%MawCRhP`8Im8G7_J0#K6LD_dE zu}1Q-0%lxD25e=4b}KxY8gQ%mkj=Buk^RC`@WAl=eX9QFgLC&qD(xQ1gZ2Mfx@_Nz zZtt}J!y2OA)o)!C0Y)O^qTUi_Y5Gr=^{4hgfPA0-k^4vF8o+eVJ!F`wc(^YYLDec5 z`Q)9Ia{X*KyIRu|{**$}?8SXGJ%{=;#S$6ORLoAU$jK?-c-wlQ<*-*=c+GMJ8>8aA zV5;H5%ndNB{V@_ve3%vs$w!itE*B7A)`cdl${L@}>b@47z}AklA}kJ&v{E)}zuy*F zWvTawZMi_1z>Cl4Ci(-Mde&Ce=Qod^^u#lN*OzBA`BZ{0K=Y&DM>KX?8>drg<{gNq z*YtJHV}Fpm)@{z%TXyqH*YX#N1}UBxHxn2;say5Us$oCj;-N225wxjd*G$>ZN{E`S zpT+VJ=1ceI(FI+OD`);VHRG0G1yNe`ViO6y<%>Ub*S&~5xJlTn&NEwYm2Ek^h@tn) zB1Ip;qdVUM5b~RL)fijxnd0UVPk}6-&;Kf)sBilAqUhf|dGJP^Zx6QrgaYgTo>TwB zQGj}7N4GDBQI1P$p>>2$>4~xC*MkEdF{!hs4o$yq5PkD7mjSI$NhxpfZHfBPCv8=( zt7#kw;-#Dk-n7zhEC{WcuJ#PZ3Ivj|NW$j+Y|bh|A5;OhlIz)3?1> z>sjQ~i5SJXiwXu!oWdOm9~W>K8i&N0uDF6k6Nfgdy-U_hgsd|M1W-T_b;(p@vZ-VsfWBz#{K!MNlA7KPztDc^ z2YlW@4?xR+U)Td}=ev#Fs{Trgz?TdZ0OVOX8`ci*x0b-8G6DcI z;PFU6REmT3_s2%fm7@UcS#dAqs~UhUx9`ZdJ+#%Y?LH{*0vikjgV&-4+n!5NzDL+? z5Y%L%0mxYwzm1F9AiGb$n-ROsECfK!fWJEc^?-}=-HQC83h<_u10Zr1K8An^aFnH6V0zQbPIUG|>M+E(cT4!WlRhn$5&_CSN|v0K>9 zD%3?6Xoz+389LO4_}kGBs=9oDqi4Vc=nsJ1dzjv>#jlDBobl)Y;%4F845;n?!%_!E zH;^Yl7i7Sx8GloceP!(C;&H&?GvN3qP|E;LX}ihz!zjCPfxW&rd=`kib2I|NVIkW) R5Qq`^?Ts8anwYl!`afE$LbU(@ diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index a2aebe0fd5e..6faa0ef2c4a 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -15,7 +15,8 @@ To add Google Home support for your Nightscout site, here's what you need to do: ## Create Your DialogFlow Agent -1. Download the [Nightscout agent template for Google Home](googlehome-nightscout-template.zip?raw=true). +1. Download the agent template in your language for Google Home [here](google-home-templates/). + - If you're language doesn't have a template, please consider starting with [the en-us template](google-home-templates/en-us.zip), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. 1. [Sign in to Google's Action Console](https://console.actions.google.com) - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. 1. Click on the "New Project" button. @@ -42,7 +43,59 @@ To add Google Home support for your Nightscout site, here's what you need to do: 1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` 1. Scroll down to the bottom of the page and click the "SAVE" button. -That's it! Now try asking Google "Hey Google, ask Nightscout how am I doing?" +That's it! Now try asking Google "Hey Google, ask *your agent's name* how am I doing?" + +### What questions can you ask it? + +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". + +1. Open your DialogFlow agent. + - Get to your list of agents at [https://console.dialogflow.com/api-client/#/agents] and click on the name of your Nightscout agent. +1. Click on the "Languages" tab. +1. Click the "Add Additional Language" drop-down box. +1. Select your desired language. +1. Click the "SAVE" button. + - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". +1. Click on "Intents" in the left navigation pane. +1. For each intent in the list (excluding those that start with "Default" in the name): + 1. Click on the intent name. + 1. Note the phrases used in the "Training phrases" section, especially colored blocks (e.g. `metric` or `pwd`). + 1. Click on the new language code (beneath the agent name near the top of the navigation pane). + 1. Add equivalent or similar training phrases as those you noted a couple steps ago. + - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: + 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. + 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. + 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the word you're working with (e.g. if the word is "metric", you would select the option that ends with ":metric"). + 1. Press the Enter key to add the phrase. + 1. Click the "SAVE" button. + 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. + 1. Scroll down to the "Action and parameters" section. + 1. If any of the items in that list have the "REQUIRED" option checked, click the "Define prompts..." link on the right side of that item. + 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). + 1. Click "CLOSE". + 1. Scroll down to the "Responses" section. + 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. + 1. Click the "SAVE" button at the top of the window. +1. Click on the "Entities" section in the navigation pane. +1. For each entity listed: + 1. Click the entity name. + 1. Switch to the starting language (beneath the agent name near the top of the left navigation pane). + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. Select all the text in the text box and copy it. + 1. Switch back to your new language. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. In the text box, paste the text you just copied. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to editor mode". + 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. + - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. + 1. Click the "SAVE" button. +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. ## Adding Google Home support to a plugin From 92d2f703b16ca1946893eecf5a6939786b9d32a2 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 22:56:10 -0600 Subject: [PATCH 032/134] URL correction --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 6faa0ef2c4a..85ddf2468b5 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -56,7 +56,7 @@ If the translations in Nightscout are configured correctly for the desired langu If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". 1. Open your DialogFlow agent. - - Get to your list of agents at [https://console.dialogflow.com/api-client/#/agents] and click on the name of your Nightscout agent. + - Get to your list of agents at (https://console.dialogflow.com/api-client/#/agents) and click on the name of your Nightscout agent. 1. Click on the "Languages" tab. 1. Click the "Add Additional Language" drop-down box. 1. Select your desired language. From 41c03352856a1da4850c5dd4f87ab6d27a18e6b2 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 22:57:33 -0600 Subject: [PATCH 033/134] URL fix v2 --- docs/plugins/alexa-plugin.md | 2 +- docs/plugins/googlehome-plugin.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 452d14fe5d2..8ba8188143e 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -102,7 +102,7 @@ If the translations in Nightscout are configured correctly for the desired langu If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. 1. Open the Build tab of your Alexa Skill. - - Get to your list of Alexa Skills at [https://developer.amazon.com/alexa/console/ask] and click on the name of the skill. + - Get to your list of Alexa Skills at https://developer.amazon.com/alexa/console/ask and click on the name of the skill. 1. Click on the language drop-down box in the upper right corner of the window. 1. Click "Language settings". 1. Add your desired language. diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 85ddf2468b5..6b4d5ca372f 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -56,7 +56,7 @@ If the translations in Nightscout are configured correctly for the desired langu If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". 1. Open your DialogFlow agent. - - Get to your list of agents at (https://console.dialogflow.com/api-client/#/agents) and click on the name of your Nightscout agent. + - Get to your list of agents at https://console.dialogflow.com/api-client/#/agents and click on the name of your Nightscout agent. 1. Click on the "Languages" tab. 1. Click the "Add Additional Language" drop-down box. 1. Select your desired language. From f188f4f58b758f1d57cd61287e25f34bbcd80ca9 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 22:59:06 -0600 Subject: [PATCH 034/134] Wording clarification --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 6b4d5ca372f..5a8c46ecf77 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -63,7 +63,7 @@ If you add support for another language, please consider [making a pull request] 1. Click the "SAVE" button. - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". 1. Click on "Intents" in the left navigation pane. -1. For each intent in the list (excluding those that start with "Default" in the name): +1. For each intent in the list (NOT icluding those that start with "Default" in the name): 1. Click on the intent name. 1. Note the phrases used in the "Training phrases" section, especially colored blocks (e.g. `metric` or `pwd`). 1. Click on the new language code (beneath the agent name near the top of the navigation pane). From 071d98024cb8a9270d879d700b206a3e54d69902 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 22:59:34 -0600 Subject: [PATCH 035/134] Ugh... --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 5a8c46ecf77..fed3b842bc5 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -63,7 +63,7 @@ If you add support for another language, please consider [making a pull request] 1. Click the "SAVE" button. - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". 1. Click on "Intents" in the left navigation pane. -1. For each intent in the list (NOT icluding those that start with "Default" in the name): +1. For each intent in the list (NOT including those that start with "Default" in the name): 1. Click on the intent name. 1. Note the phrases used in the "Training phrases" section, especially colored blocks (e.g. `metric` or `pwd`). 1. Click on the new language code (beneath the agent name near the top of the navigation pane). From 313bb60f3c97cef94bbdf9b2a778df3f06f2fb49 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 23:05:07 -0600 Subject: [PATCH 036/134] Minor instruction fix --- docs/plugins/googlehome-plugin.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index fed3b842bc5..ae3b267f0da 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -65,13 +65,14 @@ If you add support for another language, please consider [making a pull request] 1. Click on "Intents" in the left navigation pane. 1. For each intent in the list (NOT including those that start with "Default" in the name): 1. Click on the intent name. - 1. Note the phrases used in the "Training phrases" section, especially colored blocks (e.g. `metric` or `pwd`). + 1. Note the phrases used in the "Training phrases" section. + - If the phrase has a colored block (e.g. `metric` or `pwd`), click the phrase (but NOT the colored block) and note the "PARAMETER NAME" of the item with the same-colored "ENTITY". 1. Click on the new language code (beneath the agent name near the top of the navigation pane). 1. Add equivalent or similar training phrases as those you noted a couple steps ago. - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. - 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the word you're working with (e.g. if the word is "metric", you would select the option that ends with ":metric"). + 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the one you determined just a few steps ago. 1. Press the Enter key to add the phrase. 1. Click the "SAVE" button. 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. From 67ae84deecfaecdfc9c2a86164b28fbf22c54356 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 23:06:33 -0600 Subject: [PATCH 037/134] Sub steps fix --- docs/plugins/googlehome-plugin.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index ae3b267f0da..8d287d8be71 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -77,9 +77,10 @@ If you add support for another language, please consider [making a pull request] 1. Click the "SAVE" button. 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. 1. Scroll down to the "Action and parameters" section. - 1. If any of the items in that list have the "REQUIRED" option checked, click the "Define prompts..." link on the right side of that item. - 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). - 1. Click "CLOSE". + 1. If any of the items in that list have the "REQUIRED" option checked: + 1. Click the "Define prompts..." link on the right side of that item. + 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). + 1. Click "CLOSE". 1. Scroll down to the "Responses" section. 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. 1. Click the "SAVE" button at the top of the window. From dd6e69151169adb4cf6718e4549adcadeca04c24 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 23:09:30 -0600 Subject: [PATCH 038/134] Fixed Alexa references in Google Home --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 8d287d8be71..2b8556a9f47 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -97,7 +97,7 @@ If you add support for another language, please consider [making a pull request] 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. 1. Click the "SAVE" button. -1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. +1. You should be good to go! Feel free to try it out by click the "See how it works in Google Assistant" link in the right navigation pane, or start asking your Google-Home-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. ## Adding Google Home support to a plugin From 6032aab7a82cf9777f19e0772d53299afa4ff418 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Wed, 11 Sep 2019 23:59:27 -0600 Subject: [PATCH 039/134] Added a couple steps for improved user experience --- docs/plugins/googlehome-plugin.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 2b8556a9f47..00a7a721b82 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -42,6 +42,10 @@ To add Google Home support for your Nightscout site, here's what you need to do: 1. In the navigation pane on the left, click on "Fulfillment". 1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` 1. Scroll down to the bottom of the page and click the "SAVE" button. +1. Click on "Integrations" in the navigation pane. +1. Click on "INTEGRATION SETTINGS" for "Google Assistant". +1. Under "Implicit invocation", add every intent listed. +1. Click "CLOSE". That's it! Now try asking Google "Hey Google, ask *your agent's name* how am I doing?" From 87261afad4be8b0d32a712e07b5bce3861768e82 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Thu, 12 Sep 2019 00:00:34 -0600 Subject: [PATCH 040/134] One more forgotten step --- docs/plugins/googlehome-plugin.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 00a7a721b82..8474cee5ba8 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -45,6 +45,7 @@ To add Google Home support for your Nightscout site, here's what you need to do: 1. Click on "Integrations" in the navigation pane. 1. Click on "INTEGRATION SETTINGS" for "Google Assistant". 1. Under "Implicit invocation", add every intent listed. +1. Turn on the toggle for "Auto-preview changes". 1. Click "CLOSE". That's it! Now try asking Google "Hey Google, ask *your agent's name* how am I doing?" From 450fbc2572c9aa762e582aa4e9864b4c22046d0e Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 16 Sep 2019 14:33:50 -0600 Subject: [PATCH 041/134] Updated pump reservoir handler to handle undefined values --- lib/language.js | 26 ++++++++++++++++++++++++++ lib/plugins/pump.js | 13 +++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/language.js b/lib/language.js index f1560e122ef..93bfb7eab2b 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13274,6 +13274,32 @@ function init() { , zh_cn: '快速上升' , zh_tw: 'rapidly rising' }, + 'virtAsstUnknown': { + bg: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , cs: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , de: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , dk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , el: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , en: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , es: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fi: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , he: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , hr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , it: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ko: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nb: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pt: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ro: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ru: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , sk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , sv: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , tr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_cn: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_tw: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + }, 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index a2d95a68c9d..8eee2477bb7 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -136,12 +136,17 @@ function init (ctx) { }; function virtAsstReservoirHandler (next, slots, sbx) { + var reservoir = _.get(sbx, 'properties.pump.pump.reservoir'); + if (reservoir) { var response = translate('virtAsstReservoir', { params: [ - sbx.properties.pump.pump.reservoir + reservoir ] - }); - next('Remaining insulin', response); + }); + next('Remaining insulin', response); + } else { + next('Remaining insulin', translate('virtAsstUnknown')); + } } function virtAsstBatteryHandler (next, slots, sbx) { @@ -155,7 +160,7 @@ function init (ctx) { }); next('Pump battery', response); } else { - next(); + next('Pump battery', translate('virtAsstUnknown')); } } From bf57dfec782aa084d4f424a2fa0e11368297b4ba Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 16 Sep 2019 15:12:59 -0600 Subject: [PATCH 042/134] Updated titles and unknown-value responses --- lib/plugins/ar2.js | 2 +- lib/plugins/basalprofile.js | 2 +- lib/plugins/iob.js | 2 -- lib/plugins/loop.js | 14 +++++++++----- lib/plugins/openaps.js | 20 +++++++++++++------- lib/plugins/pump.js | 11 +++++------ lib/plugins/rawbg.js | 8 ++++++-- lib/plugins/upbat.js | 8 ++++++-- 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 78b0d10e0ba..9f4b8af33b6 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -166,7 +166,7 @@ function init (ctx) { var response = 'You are expected to be between ' + min + ' and ' + max + ' over the ' + moment(maxForecastMills).from(moment(sbx.time)); next('AR2 Forecast', response); } else { - next('AR2 Forecast', 'AR2 plugin does not seem to be enabled'); + next('AR2 Forecast', translate('virtAsstUnknown')); } } diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index e229ff0fac4..e30e1238c92 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -102,7 +102,7 @@ function init (ctx) { function basalMessage(slots, sbx) { var basalValue = sbx.data.profile.getTempBasal(sbx.time); - var response = 'Unable to determine current basal'; + var response = translate('virtAsstUnknown'); var preamble = ''; if (basalValue.treatment) { preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 567968cd5e2..614cc56981b 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -247,11 +247,9 @@ function init(ctx) { var message = translate('virtAsstIobIntent', { params: [ - //preamble, getIob(sbx) ] }); - //preamble + + ' insulin on board'; callback('Current IOB', message); } diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index 7b30eafb644..ad7d06e5a51 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -441,7 +441,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', 'Unable to forecast with the data that is available'); + next('Loop Forecast', translate('virtAsstUnknown')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -461,14 +461,18 @@ function init (ctx) { next('Loop Forecast', response); } } else { - next('Loop forecast', 'Loop plugin does not seem to be enabled'); + next('Loop Forecast', translate('virtAsstUnknown')); } } function virtAsstLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.loop.lastLoop)); - var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); - next('Last loop', response); + if (sbx.properties.loop.lastLoop) { + console.log(JSON.stringify(sbx.properties.loop.lastLoop)); + var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); + next('Last Loop', response); + } else { + next('Last Loop', translate('virtAsstUnknown')); + } } loop.virtAsst = { diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 0865b3352fe..293aadf00d4 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -518,17 +518,23 @@ function init (ctx) { ] }); next('Loop Forecast', response); + } else { + next('Loop Forecast', translate('virtAsstUnknown')); } } function virtAsstLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); - var response = translate('virtAsstLastLoop', { - params: [ - moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) - ] - }); - next('Last loop', response); + if (sbx.properties.openaps.lastLoopMoment) { + console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) + ] + }); + next('Last Loop', response); + } else { + next('Last Loop', translate('virtAsstUnknown')); + } } openaps.virtAsst = { diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 8eee2477bb7..840afa8aeae 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -136,16 +136,15 @@ function init (ctx) { }; function virtAsstReservoirHandler (next, slots, sbx) { - var reservoir = _.get(sbx, 'properties.pump.pump.reservoir'); - if (reservoir) { + if (sbx.properties.pump.pump.reservoir) { var response = translate('virtAsstReservoir', { params: [ reservoir ] }); - next('Remaining insulin', response); + next('Remaining Insulin', response); } else { - next('Remaining insulin', translate('virtAsstUnknown')); + next('Remaining Insulin', translate('virtAsstUnknown')); } } @@ -158,9 +157,9 @@ function init (ctx) { battery.unit ] }); - next('Pump battery', response); + next('Pump Battery', response); } else { - next('Pump battery', translate('virtAsstUnknown')); + next('Pump Battery', translate('virtAsstUnknown')); } } diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 91ebdb849dd..997cf7c55a3 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -107,8 +107,12 @@ function init (ctx) { }; function virtAsstRawBGHandler (next, slots, sbx) { - var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; - next('Current Raw BG', response); + if (sbx.properties.rawbg.mgdl) { + var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; + next('Current Raw BG', response); + } else { + next('Current Raw BG', translate('virtAsstUnknown')); + } } rawbg.virtAsst = { diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 509d9347f89..1bd8795ef7c 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -222,8 +222,12 @@ function init() { }; function virtAsstUploaderBatteryHandler (next, slots, sbx) { - var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; - next('Uploader battery', response); + if (sbx.properties.upbat.display) { + var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; + next('Uploader Battery', response); + } else { + next('Uploader Battery', translate('virtAsstUnknown')); + } } upbat.virtAsst = { From ddd8f63cbd060dd8d6784956faa24bafcdd507c8 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 16 Sep 2019 15:47:06 -0600 Subject: [PATCH 043/134] Modified forecast responses to use translate() --- lib/language.js | 92 +++++++++++++++++++++++++++++++++++++++++++++ lib/plugins/ar2.js | 13 ++++++- lib/plugins/loop.js | 19 +++++++--- 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/lib/language.js b/lib/language.js index 93bfb7eab2b..9227f4ae33d 100644 --- a/lib/language.js +++ b/lib/language.js @@ -667,6 +667,81 @@ function init() { ,tr: 'Son 3 ay' ,zh_cn: '过去3个月' } + , 'between': { + cs: 'between' + ,de: 'between' + ,es: 'between' + ,fr: 'between' + ,el: 'between' + ,pt: 'between' + ,sv: 'between' + ,ro: 'between' + ,bg: 'between' + ,hr: 'between' + ,it: 'between' + ,ja: 'between' + ,dk: 'between' + ,fi: 'between' + ,nb: 'between' + ,he: 'between' + ,pl: 'between' + ,ru: 'between' + ,sk: 'between' + ,nl: 'between' + ,ko: 'between' + ,tr: 'between' + ,zh_cn: 'between' + } + , 'around': { + cs: 'around' + ,de: 'around' + ,es: 'around' + ,fr: 'around' + ,el: 'around' + ,pt: 'around' + ,sv: 'around' + ,ro: 'around' + ,bg: 'around' + ,hr: 'around' + ,it: 'around' + ,ja: 'around' + ,dk: 'around' + ,fi: 'around' + ,nb: 'around' + ,he: 'around' + ,pl: 'around' + ,ru: 'around' + ,sk: 'around' + ,nl: 'around' + ,ko: 'around' + ,tr: 'around' + ,zh_cn: 'around' + } + , 'and': { + cs: 'and' + ,de: 'and' + ,es: 'and' + ,fr: 'and' + ,el: 'and' + ,pt: 'and' + ,sv: 'and' + ,ro: 'and' + ,bg: 'and' + ,hr: 'and' + ,it: 'and' + ,ja: 'and' + ,dk: 'and' + ,fi: 'and' + ,nb: 'and' + ,he: 'and' + ,pl: 'and' + ,ru: 'and' + ,sk: 'and' + ,nl: 'and' + ,ko: 'and' + ,tr: 'and' + ,zh_cn: 'and' + } ,'From' : { cs: 'Od' ,de: 'Von' @@ -13636,6 +13711,23 @@ function init() { , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' }, + 'virtAsstAR2Forecast': { + bg: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , cs: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , en: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , hr: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , de: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , dk: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ko: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , nl: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , zh_cn: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , sv: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , fi: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ro: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , pl: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ru: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , tr: 'According to the AR2 forecast you are expected to be %1 over the next %2' + }, 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 9f4b8af33b6..cab989c8150 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -163,7 +163,18 @@ function init (ctx) { maxForecastMills = forecast[i].mills; } } - var response = 'You are expected to be between ' + min + ' and ' + max + ' over the ' + moment(maxForecastMills).from(moment(sbx.time)); + var value = ''; + if (min === max) { + value = translate('around') + ' ' + max; + } else { + value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; + } + var response = translate('virtAsstAR2Forecast', { + params: [ + value + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); next('AR2 Forecast', response); } else { next('AR2 Forecast', translate('virtAsstUnknown')); diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index ad7d06e5a51..2f627409c45 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -441,7 +441,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', translate('virtAsstUnknown')); + next('Loop Forecast', translate('virtAsstForecastUnavailable')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -453,11 +453,16 @@ function init (ctx) { } var value = ''; if (min === max) { - value = 'around ' + max; + value = translate('around') + ' ' + max; } else { - value = 'between ' + min + ' and ' + max; + value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; } - var response = 'According to the loop forecast you are expected to be ' + value + ' over the next ' + moment(endPrediction).from(moment(sbx.time)); + var response = translate('virtAsstLoopForecast', { + params: [ + value + , moment(endPrediction).from(moment(sbx.time)) + ] + }); next('Loop Forecast', response); } } else { @@ -468,7 +473,11 @@ function init (ctx) { function virtAsstLastLoopHandler (next, slots, sbx) { if (sbx.properties.loop.lastLoop) { console.log(JSON.stringify(sbx.properties.loop.lastLoop)); - var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) + ] + }); next('Last Loop', response); } else { next('Last Loop', translate('virtAsstUnknown')); From d3346fe3ddceb9d56d938ae3ac088f7fbbc6105e Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 16 Sep 2019 15:56:26 -0600 Subject: [PATCH 044/134] Updated tests --- tests/ar2.test.js | 2 +- tests/loop.test.js | 2 +- tests/openaps.test.js | 2 +- tests/pump.test.js | 8 ++++---- tests/upbat.test.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 8fee3ec1c63..01f4f3d41a1 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -158,7 +158,7 @@ describe('ar2', function ( ) { ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); - response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); + response.should.equal('According to the AR2 forecast you are expected to be between 109 and 120 over the next in 30 minutes'); done(); }, [], sbx); }); diff --git a/tests/loop.test.js b/tests/loop.test.js index 8506de4555b..71bc0860bda 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -262,7 +262,7 @@ describe('loop', function ( ) { response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + title.should.equal('Last Loop'); response.should.equal('The last successful loop was a few seconds ago'); done(); }, [], sbx); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index 9bfc5161969..5c76deeaaf3 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -389,7 +389,7 @@ describe('openaps', function ( ) { response.should.equal('The OpenAPS Eventual BG is 125'); openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + title.should.equal('Last Loop'); response.should.equal('The last successful loop was 2 minutes ago'); done(); }, [], sbx); diff --git a/tests/pump.test.js b/tests/pump.test.js index 483d5a49316..374ba03f06c 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -269,19 +269,19 @@ describe('pump', function ( ) { pump.virtAsst.intentHandlers.length.should.equal(4); pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Remaining insulin'); + title.should.equal('Remaining Insulin'); response.should.equal('You have 86.4 units remaining'); pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Pump battery'); + title.should.equal('Pump Battery'); response.should.equal('Your pump battery is at 1.52 volts'); pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { - title.should.equal('Remaining insulin'); + title.should.equal('Remaining Insulin'); response.should.equal('You have 86.4 units remaining'); pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { - title.should.equal('Pump battery'); + title.should.equal('Pump Battery'); response.should.equal('Your pump battery is at 1.52 volts'); done(); }, [], sbx); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index cac1439160c..42d18bb0854 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -109,11 +109,11 @@ describe('Uploader Battery', function ( ) { upbat.virtAsst.intentHandlers.length.should.equal(2); upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Uploader battery'); + title.should.equal('Uploader Battery'); response.should.equal('Your uploader battery is at 20%'); upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Uploader battery'); + title.should.equal('Uploader Battery'); response.should.equal('Your uploader battery is at 20%'); done(); From 6dee062f1840f83af8845010d856c8b60766518d Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 16 Sep 2019 16:20:30 -0600 Subject: [PATCH 045/134] Improved training phrases --- docs/plugins/alexa-templates/en-us.json | 6 ++++++ docs/plugins/google-home-templates/en-us.zip | Bin 12265 -> 13230 bytes 2 files changed, 6 insertions(+) diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json index bb1ff0932c5..cf90a710b88 100644 --- a/docs/plugins/alexa-templates/en-us.json +++ b/docs/plugins/alexa-templates/en-us.json @@ -42,6 +42,12 @@ } ], "samples": [ + "how much {metric} does {pwd} have left", + "what's {metric}", + "what's my {metric}", + "how much {metric} is left", + "what's {pwd} {metric}", + "how much {metric}", "how is {metric}", "how is my {metric}", "how is {pwd} {metric}", diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip index 71dc40256bf9958fba2cef7d4c1cd35f4d25e718..6a8498b0b19e0c1822efaf4652f737cf282b350d 100644 GIT binary patch literal 13230 zcmdT~cRUq(*f)>8S4KtID?6jCNYX*(kvP_|M;Y1U7{>_7jAR~?%n~8lB70TMv4Zp<#BR2 zqX{BJTfp(r>qLC7pi`o$BF<2v;UYfN`_7Q;o#B;J2dg{U*+X^@&;D_E{vN+=jWntJ z57AfLPH&HJHjmrl{bkMXs zt>dJzNXHx4Oz{}ZC@F?OQ$go%nU~BDt$XAS(51GEg_cHb0J}qlsW;ONyDn^9!DI3# zopfuVO0rw0l@0Q4ERbXt?|v{O3hsEv%+?j$=J-LRfyz-4M|}J5_h}Go$xu6yTkO;y zUid`6kIIYAR}fYqvN9FJ4}MrjfCH^LLkM1^kDI>Y zYJY`v=&LUf>eDlB939QksrdA!kpko35(HI=3L^k;L45ydE@=AzAl)+;LoSd&OXx>}>on=T~havwF_XHn>6F3u_k{ z%padxvB-!Yh!-=nZFgT^Czc0NDiKB1h>bcY`pGuR)X~;~E~#n=R7AdR@iGg$69H#% znw6rMm2P9UXkzaswNE7Q!@o2!A=?ljiIG=m^F&CxSEG{{FxQ#tKq~FH8zzc4qq8i$iJ6V41bl<9ecJLKlLD0IHSVXI={Y8-lue#UD zmkSh|`M=ruEv|b&7=nMr zH-v1u#hN-RdvnQW;_~a2TM!eflU&AXp2HtKDqdFH%zvNU=J#%SP2@)0()e}=oLESR~hvhEGilUgNVy*?QfL0*&7w#G%IrRa_}ev6&1M^HHmRy z*%fGYB7}r#xK05X!ScbbEb*?72xkZAjbuWSE{zyrbq1^f0MxMFOb%)Q;61fzZyT-r z_tHnb06?a4Y-V0wSzdS>pp2Ee&9*F-x1piTwxOW`77J*AwV7rTZU>sF^QO`Th#*6V zjtay?!Oyvjd@s=4Ff_J8Ag~u|au8wWz3xuPef(ZcRh3hThqJAvUHyrgsz8rmhoI_Y zHug5B^s{*`bO=s}kd6}4_H#OZ801GWJp%MojDXMie5BY67i4`?u0({>N?ZG7`jav7 zCL|_5jsvMiL}hD(53aTJh@v?*OH;i=W}nLuPzFWi^|HrLe0CWKS5j*2RwddS7(*Y43EuS3Fn5!-9RLdG*r zmm|XaDZ?o%iIsV}@rth=51zrN^j?h00pm2zFO{?|>J!~y7r&#Yo9mqYK43-fbCTOk zM)zBxGnjqNQ*j;G%BFJQlP5IYhjll?hs&8>b~jA-%wHrsC5Zn0*+7Lzz!>^>v-09u zi84>Rm*>VzafqG$FqeAuJ3n`>(I5>3q*WW^g>W=pMC3tc^~38Enu9K|-%<|NDf=x& z=+{Je!GFVO{>AQfxQnnIa1mv7bw@)-CkL>T1H|6J(A5D9u|3p5gv0qseECQNoW1HZ zo&qnYvbPbJm6O4`PGsUyp0W|*-*O(h25WxzWpsVi3A>7o5RE?}5o-Rn>`_L_DoWb|6RxWJSATp*+5~SgnwX0*qBBILBk+DJB?zZnMa#N z@g*NfXU~Fm5y0E1eC%|6@2%+KtmE?CvLL}+Qm;<&TcQA?Dv@hCxQ*ztc@txbK9ARf zeJ|5@VZ~5m^I^SoQ>N{K(K$0=E!9~S#KxvuPK6la=+oIT$A_oFvzI?f%aaU$pTG#v z()WnA#;oFOKRad<311K{9K6>YYK@zoZG4Ga8mLNv8Ler;^%|e2n}>YrLfWz`HIap# zr{lNU)Rf#hy|H=~#lnuvuUnt;($r?vWTk>mjKLUcfh4b*ykl%d!^j44Ux?20dqqiqGp?J&mL5lf6uqAQH-Bf47B0!e+RRQu5N`RO7S`VOWO!SudtnP4n9( zibFsetpEFB_17-q9EY;j?OB|*`S6De)qK;hLhK{#z{nb8FB1I+9NE;FC6#e`cq|R=Yj7QL1#OH)Z6c zPLD#R=a~M(DG;+G1}(&p{M92}Q|`nIlkDvSCgKm?@n2NDF<8>Qnma+KbEhBO*kEF* z_-m;P_`3;V*tcN1+7r@3Cp-jUz1K9K9slCm*2IM$0hz^qS5?hd7bgswRKXI8%(Vtf@mg|PiVoH=2%XpYSt zB`3VtynL{x?(4F`{6~)(#B)g){5ciYo2+gw-K8(LtHbPS9=hZXG>BDxNb8RWBq*W0 zdGA8Be^KL8(fh5F@+T_#_5H%Xji*$$^d`n^phKq1EsNS`h}<7FdHHTVgfHBB6|B92 z5e5#L|3Xc)QK|Z79&S2p&!T$+H=gcpp6>TARuu9#waC(oMw9Ncq%umxvj7S$XZ25a z2$xR28>E}LL@{C;JPUg|PUCTmx&zn!sr$)1Y|vSj?AWopnyQ?d_`|@-2dkGkWp5-GU2p~1ve~D6Q*sXL zB4%`48Sq{t4#vLO?Y*wWrXsanmAWr9Y-k{y((R{KXMeqbOq=_LBD>L)7ksqg{^vaN zV2N5d?aC&m-ldvx_2TL z5+83@+=?dYh1Q>?gPEfSI=OWDvpSgidL+B6-gHRwQp!@I$(_puwNj~_sqICd2gR=5 zT(*baD(@y#DdMZBPH{4bg#bUB4|AXA6$%v$_=+Bu!R$5lN$rh!zECdU_?FZ|GU0Dx}$9j=KyX`t*BP7c# zL|pPUqE5iW15$J=hh7*Iw~?pQ4witC{_s>BjFBaYS$Yxtps&rT4y0;YO;6wtdQm5M zBJi;Zb-Ay$0p(=4Yrk&RL?0SY|R5fCDCX7Vrpw5L9 zrTI{~DW>S_@7j)MjYD;%bQD=Q*(5J#0xl7}Dj5S(0^_IzvsNHA#oX_*A$5e(^|F&|8=}&B!)%1$%dACnSMoH_5c{^*%gx^z|26Rs7CZ|_^ zoDf~h?;?KK%Q6$mtEDUVX=#~I5$dug5J6-%G(j|@5?sWQ!R^_-R){SUiKn8XE5R>M zLcReKaG~}zE*&NTH{Nty$3zyhBCvoZ&Hr7caHQh?bB4)zpisTy+vnvi>JQz_6eUZ~8Y4{}$WgTS-HU2p zRi=#%F$}((%Rf?`ZrU{*Szyu~(H~3M`^duAdqB89UsIa9w8mTk-4e~m)1`!IvOeN& zs=W-W96!XO$eGdU>X)1z2^-GiME>0A(Td!opJ>yt2Q$%F*8%NKV#A?xp*+}tzJN35 zl0szy+@>lz3>e3O)v_nTl3>?mqHR^a;;o*>@k+gf5#hvzkJB{J~S?n6y z+STu|A-VDj3-7dPxIDYCN(-^LmmGpgls`_7U8npmhrWDrWvkuw>)W}4i*Gf5cuv2B z&2kOedVDOK(P1)cc)umSKyu#dxm#{*pT$sNK^LgWi9*Tf2UbzqO7FdqI}M}HN$#w6 z^=m_1IAS7Gi2xUSfdRJcp%>9NEqpo|rzNo8G`q0oSsC*{ewYGGot!&fC%R6OJr#5hvBkroEJKn(nI5O@Kybt)UI!8A({E zE?35V5SQ-GlsPW?T(r&LnAx>xIY~*$(E-Ii&p4Gn;F`GW81Onf{4Te799d51iADk6 zDeHvO8bq6Km-|94b43c9-G3+{Mc*7!nG3M%zJ1^JC2zNFxry=hGv8-Umv~jzhKHFd zjna81ZVkOj)YkU=K0?y!c1_Yc5)6w1N^aPcRj$uiZc5#EUh$1{e_rSNeq)@9*KWq| z19+5o%yT~972TWHbMSnoW9vxh(2Uf=lkbva@5{kn@4vqb)$;gOlAC%xhsMwQnWsJ& zUr3~_r|GNoH)+@#O^6bP%{9zfHQylLq8X*zVsb|GcSub(?KgT%;1TI&ieV8xt-==s zD5YZ(Id$vi2DyN(OIXo4Y1U6La;JP&#+mSAWLayKXR8Y5NIULxI%oy=0LD%buk9G|SiGIQnW> zu~7lm95OX;PenD?@F?7i?}HC0)N7}kKgCm9E(~G~Wq)z42~#afvOepDvO;ivoa#m_ z#(UPTC&GD*M(WEM+E`6#HvOUinp;**wG8&SrLL?3D~dO3@jJDcx^u|i%91e%NMv8c z9;KQKOszYSd_|$&1y|9S4>!l$?3CEP*Wo>^P50fM@*hKGf>g@tNk0yeI< zk>#*ledzZkHB&5p(rI|b2IBoTN?ny-y$e8>0NuKbo;@JVNhj8u)j!7<@hP{5SUB-b z8k(a^rJ}WoxzU-H3?7$vozx;4FX+-GDzEnSH(t8N^(GC6;;P&;OIf{A>=g4Lhbpb+ zyhc&P`)jrA1@~nT%gL8+hEfUwidJ`_{B@yuL41S~L6zn9=ObRsvB23rNTDNhYc;6g z(F~&4S7$gXk_a9ZNq*n8{`GNW|LFbLLAiOsf3!Kf@9eun_?`aIH zYK;VW9c(iD^K#kPIWuGxGv8(tFL#*C-w!B(#+1@T7dOYoi3o9f}({zDt z0=>)%B326b4JL_(79a6Q=v8hzD2GQDEjJ?7L$Hr}0yxA__w=%BsDJ$iz<1Ex_o(oK ze^0;vVmCN^{*PSH*qH|J$+{!yUXwSDV!oO!&B%T}V8Syw&BUh4lIYR;b1Dx%3$tix zUKW~Msxdvkkh0A!F^9)92^J{ZAS}u51`tWZ(X?s^67he=_YzSkiBB~?_|QS}Ok=8+$9>)&U9`rd)cU>=%4vH#k; zGvogw!%3h#LV|z93w193v*FyO`js$$O9ek0jug#r2=JHmQAd}YKN$`uV$p9WC+r5C z-QafkRQvagUl@f49C{V8Q20yl{ip0uXDI9_?B=0L^!r8HGu3}=Re~rS;L!Ugju7bY zGeefis7`q7CuUJ64=_U+hwPLVebaUZoAW<>U#vm-Ja~=EM@=Q|AYp$ z6GSZu5h?NX0gQK`+4I%i26ulUg4#-jPC{MIpJ@675hzR!OQ#*oZn34UHM`aY5vGMmFSs{{zrEGOqvt literal 12265 zcmdT~cRUs9|F>6mX2=d1*{i`J+4F>B@61DT%oNAU-dT~%jF3b{D%r_aR!T-jju8>p z_#U}*94B?}@At>ec^Q9r-k<05`Mf{tc^(aAOe_Ml?XRfdlj`69`1=PA8Y!BUBd@E= zDQyrTnjVjb(FV7N(N}Z|G!6I~3N$?UrvPaU>E0SMYb!?xpPh@7@~%a z#8Bn5OT$6y`G$FdI!{DtCVks=#S^T(`U2Gcbrc*q#0}UX)bjx*Gukxq~z7b zrlA+C^n_>2GtP`6Vm@`X#x(po|LulN;UvE(GDe|>RR_0o_F8$>Qc;GTU6YLJTc`U7 ztv6~Q;!E-ZjBDurO;IPhpD|*4zhq8Yj=}cf0nta{#*wM4>j_W)1@H|ZY-`+vh$jy_ zytM9|r>rShnM)X{c1>~6VDA5u0R{~E&zJNY25t}IScI#Q=&~0U+RhZav^=;Ni)pcY$EbEDU z`Uv|}J2ZZGb!2u!^_w#g(_X)Z`=M!{zVQv+uqwdmbA*Dy0TgP(WucTqT&&q@LcGuK zl(v5PCg^YzOSgoO3S*AC;2m1~3H3e+F4`b`9ib1R9TwFh{6U755ef@{!ec6-7x*Vz zxsC{b-F(t$Q}}iEzTlUXs3Mo-YlEM!Z9uM$mL%Q^+%j`66VFQP@K;$11(dw!HP9yq zzkDK>#Z=Gtk{3g}%k8sqepGM(MnC|yeaL!s_oZSW>8s=JA&uE>t5t^D+}+2&t-9aJ z$+YvOAd&CZiDREBn8EK=ePQv$RkAu>`m+#~O|eMH9Zh{}(yD}-i^fM>^R-oY3qiJ> zQgm+~F}Dj=5;v8k)H>T>M=3ps9X1Dd@gGfFqjudmtHHEF@=Zd%?87k0qg)msFUif~ zC>5ghQ#o@}it`ve*R#nXEHM3{W2*gpH!jLS--Bx>AZw6<3m+a_CMK!?S#=<2FDAF+ zayKSXFC#%*wm+Z|7o!enx`T`POnx5Q7N(RI)~4pB^QtO3^CUXK8on6OG?*Nubm$8# zOgIZA{G{0QvNC>mRH7qkr5*fp1IU>9Qc}|k6G0l$aRs`d6i|GUZftVmD~&uSQ&Te= zaEiI3S$uq~B`j4lKK+&*Y;t_9Wj*lk9|`N~xA2pehQ>){-ji*w z+kp?XGz5EuI)pSv3b4OR$`UK~I0lzObEqT+RF*tU=zrLjs0#-1o-J)1A@J;fSm8kS z1J{AI=dCW8xjI0YbgdjLoWNF0ir+C&x(BRPqlO_sTD?eV)o!YTj%j@64qnKGECKrP z8(nQOvQrKNdNHJ%Llsr`!zypIE>(*Om8A7QqEU`laRhbwKm5SBTwIk;^UBdjcx0$v znz(W*nxsV9chZD7R;`2X88q4%m-du!se}!b&QwU&G_Pmgufx8HinJZ$?5b*;1!Fo{ z>F^a{VwN%VhUEnlE$LB7*3p4_!I*JBiWF1n zKGI!|JVoc0l91`==K0utz}7G2meoAQ35i-GVkRBydX{t8MWwB|kFEUZL@l^GBnHi# zuO`ghBnggHd}d#^8VbxwXcBw~XvaIdD%OG#EvVc(QG)?3Uxp?imse?Q69Ubn<|aqK^oQGSen-Y`?Ko2G$ceZ_G-IT6@)T*nAM~I&WJv z68DyXaSEIIvt3OJg}3KeI;G_7{p{BsaKlYhXH|8P9*vPIfl`=Tnh%2keg1V09;#07Ig#|X<2lazm#=pG(>%A-X3l2gwfs|CJ z%~)lA&K;$Pltodk(gq@&uNoiNJ>l%fkD=eh=S?F_<}*6Y!A1OmudBG_$QYqmM_6f8 zsxtBOkBfd;r8M^P65ynIr-5s;{DTY=uCc`K22?#G;2G1^WqS{f8W0)t2FW&YtlNPY z3GhTYcxbW|Jk006ZG}J1N2eRX=N12sgm@e4Q!qOn$|1*|j-%nW6*K0(Dyi#VMMhI9zUCVSQZBU8K=WfHGT zcC1GZQc3F=_j|iKr#=;Zohfjac95S7(jKhm8$>3EDWl_HkVvT)!XPjzo4X)BYBA74 zYILhDgETEO%1nDsNdua@AZuta!ET;!+PCa`$|AC~dt+tnxze1py`8W~TIw?SU#B`) zEh$GncFSD1ANK9FaU-xb;?};=8vgz=b-qedZ9{+Nl$sx-yqD~WM5%2;YQ?630Psw9 z>F-R2Iu=j-4k1xG>5n5J{Q(lHT0ySZTBtd>qg3#Xwz5+yKk2gxT~J;lX(tN8nh7cjsC0RfwZ%yUB!$JhFjIv&kfpkPA+Vbf=8Fj*)Absv?TB^bPo~;PwwL>s zcCXga@vM1=sJ`A(G$zf}d3$x{G)7^9ynAA!dYlAf0VC`AitVDN?;`M8Yi3Kqr@9+K zY~C2O)Mn(3`39F*Qe{VEZih8PBwH-PV~3YO?Zc0Bj#0=%VG`(A>7o(s^@9T(7I{yV zduZHAv_CiNS~yrhhC29f-xctzA-LGD2N2=shb&lO*R6*@0TrFRg%6*_6<7v}I{86I zMyJxh^x-b68!ZC}U<}0oGS=4Fc;hV?lTYn<=ak!6>8VbAPG`{IZQV8U;F4k!3WI{& zoWweavf6Z7fKp=C##m1FwNt#XW6tgLQcmecd6 zUx)^KzY%CD0aMiLwcZoGnCn_mb09X`(5N9@6(dXT2&`UbvNTRA7zVd(OA()<*eDo$zzK* z=}xD^O#SADkATnVr0`j4E2bx3Bf@di9e**GFk3`H#0imme62dJ@q0sYdJ7OJ0}nyK z-y;y}Fx)PQdn3^POvz_*I8Fb!I6PSXRm@xXz}&S^c5BR1RWPhy zp7c!o@rq`uBU2q<^0JryEWe{ixi+?W2H$K9i62b{;EaOjV2jFm*8e$dNQq)K)^)5i#8fTwTu|KB1 zYGxY+D!hkVM#Y-N00gckdbt7=Y?@NP@2p719@ zv(pK9LxSd1cPs;5`zl;n*0Gf6$FetX2viyMeEHYIM|_oHX?_&tnNg1LCLsDeu#$PU zXpv9=*a5DpeRNL#!_-IdwURDLjbsc}K;i|1fZ4?*Lgz@2wFl9}L^JbLt#@SxsFWzL z3ss9f4Y|k@+g)5L*X@rbMPy4vJGA+ol8#b9ecAt87jUSX{x}+*JkSN~j#?MY930Hy zhb4ziY`Dd#XG&Yv<+SnXEq150=y*jcpds(^<`%wJ)3wjmAZDTNt_- zOf4%9eI=I+s!z@7tbz7ux0dNUmD@rDNkjdvoGA6Ew!Wuhvw^0vOlchJ^7wc^twAWE zVinzwZ?=*DPyG-U1VF8BFaz7SE` zfCXAQ@a4F$-B~z$)+m&AouCEBs7G?jOYm*Q(I(ZRI_Ibt-qwkjK4X)FR~IDoPpx%+ zODKqtlb`!4X3cS}2P>x(i>Cz`LZUV`{2`TcwGM45XlpV;y!}CURK){U-$Z@A9Mf~gRPoCn+ra&`eaE{MuH`FM+#}s$;K(LFf?b}c zEv8Sb7_V~4p?#%wVqxQ6!Lx+If={j~&x=0UT0wr!DZ+WHEJmD^N zhP(pgJ$vJkY-&wcS7Jp}Mc+)JHZ}E|NkN*bAh$WLUi)eJ*N+`+gs)fq<&sBPUM0Cn zkDDkW+Ne6NELFBz^+B~=-pPH4%U_qV0Q+25NFmK_5{uaBPpm~1$_`WWgEqmp9EWhN zN3TBOU8VOc?H9jg-DJ}!2-NrIB89S)FmWyo2lhs4;HE8cpFaW4d-8(i?c>ZW<-XbO z&Q&q>)?4jgS&KgE_WBq!wyxgn%x3~~c~NUW!N=vxK!^701N>-bP1v6s%Pep|WU)+S zwpO|Ws;Q?98osHaXL&_uu)C)J)cvAq<@naGrB7m$joxY0p$yf7#OE~V!02IC z>akc0woxJ_aKV&{5`~|aNu&~6OGb!_CSUOje%6a@A)WUPX4X)^G)Ek~kGjsfcm5R%N=$dq-p%yJ!Z4Ql85` zQNv0M%Z4n^Gi(PXTOHmr5qMK9L4H_J3LO@dzt7VDd`Kc+P|_UKo6qF$gA?Q{%m1)) zs1MV(Z*6u49QaAPEyUL9pPZ(%A6yLnKEuP$3*n=@)lbh2BY*%$e}2-Z6O`O-5?}jD z&P(vz&qxTTE`puUoBmK1nCm+#uHgGn2vf*nv>Sccz{z(yR8;FLzIMcMuxqNOl%$)* z+)Z&+JO}JW8y$qNB|7}kxNaIkN>wK%I!9kSeT2NOE_y!>p{BCyxS@4Eo`B7DH+r$xeb*E`Z#uwqNcVmjG53B6f8Yw*9?q&g4X7MHm__UeLLys_(s z0d}<)Q5NH?%g1?)UJAv?C-w~L*h<%L<;>&h@SXJV{~|HSdE905>4sKSq%J*=h4KUI zMiUz$mC8kRZ^spqiPPH=Npcha2?M-LZv0&)QD3>CVCDfY{`%mX{~*8jQ20!K&!PWe z-lLxIkNu3gD5oFo@EYQmOr+Rz8=*mEY&zT-@2B3>N%cJ_WJVv*(73nopji%;^T#NKjzNiuy7u6p5yI*F{Y&})_tVuOdnqGkgz0xS$5fU~>B-7BhHM{K_iL*5?ZH8B?=4itO7eF_i}{e}X+#7De& zphv)mLBGNAcfxVkE_>6|FHsTAj_B8HXguj5yZ(Ivh==Hi0$@;IGURRV9!>r~5yuxk zj{cN5|4J6(@#}u#xF7wQGXKa0`-yXu@;4OtB|hSj*~xvx*@WZoC(dqiLw$Jqd*l~E zkdY0gfIm0;sT2OKIK%@7P6TliA&5VpQ<3-n!!(F9)G-7}FsK{T0UPg_=9jUG$OJ@l zXG0B`e{PT6-5&~%Of*`AEoVdJSPsA=4nU|0f((h+Q2RnzSrH)jrVd0TY Date: Sun, 6 Oct 2019 23:07:34 +0300 Subject: [PATCH 046/134] Update release to 0.12.6-dev --- npm-shrinkwrap.json | 2 +- package.json | 2 +- swagger.json | 2 +- swagger.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1a9c1dd716b..fd0c8b9e816 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.5", + "version": "0.12.6-dev", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 39f4d2e2bfa..121ff4d27c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.5", + "version": "0.12.6-dev", "description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.", "license": "AGPL-3.0", "author": "Nightscout Team", diff --git a/swagger.json b/swagger.json index 1a9c24c985b..726e79faee6 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "0.12.5", + "version": "0.12.6-dev", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" diff --git a/swagger.yaml b/swagger.yaml index 3469ed0c7a8..f77aeb1ceef 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ servers: info: title: Nightscout API description: Own your DData with the Nightscout API - version: 0.12.5 + version: 0.12.6-dev license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt' From 2dd576a629f5e01a0c1378e3a21832d6769f7945 Mon Sep 17 00:00:00 2001 From: Petr Ondrusek <34578008+PetrOndrusek@users.noreply.github.com> Date: Wed, 9 Oct 2019 21:53:55 +0200 Subject: [PATCH 047/134] API V3 (#4250) * extended .gitignore for Visual Studio 2017 * creating a lib for api3 and exposing it's swagger file * adding pilot test (for /swagger.yaml) * implementing public GET /version * setting api version to 3.0.0-alpha * creating authorization skeleton + fetching some API env variables * reusing authorization library * implementing security * forcing HTTPS and removing x-powered-by from response * moving messages to constants, creating https instance fixture * testing HTTPS requiring * testing Date header * testing permission check * testing allowed operation * refactoring + storage stub * create architecture for generic operations * beginning of READ operation * tidying the code up * basic READ part * going further with READ operation * DELETE operation * handling fields parameter * refactoring to classes * going further with SEARCH operation * refactoring file structure * filtering for SEARCH operation * preparations for fallback deduplication * CREATE operation * UPDATE operation * PATCH operation * HISTORY operation * creating more precise variant of HISTORY operation * autopruning * long for timestamps in swagger * bug fix (when search fields=srvCreated) * creating skeleton for generic collection API test * specific HISTORY skeleton * distinguish between collection logical and storage name * renaming operation to LAST MODIFIED and getting it to work * fallback for LAST MODIFIED operation * tidying a bit * LAST MODIFIED documentation * bugfix + emitting data-received * adding some validations * bugfix - remove 'token' parameter from filtering * testing and debugging generic workflow * test fix for empty db * fixing security test fixture * trying to fix Travis CI testing DB problem * multiple auth callback bugfix + adding user field on authed create/update * messages for Travis CI debugging * messages for Travis CI debugging * messages for Travis CI debugging * test fix (to be prepared for future dates in db) * test fix * adding fallback created_at filling on each create/update * STATUS operation with API permissions * querying srvDate from storage + include storage version info * bugfix of missing apiConst require * getting mongo version with read-only user rights * getting mongo current date with read-only user rights * trying to diagnose travis CI timeout * refactoring storage version caching (due to some environments problems) * making VERSION work on empty database * more fixes * skipping API HTTPS test for node 8 * making code more readable using ES6 (Promises, async + await) * extending treatments collection docs by inspecting the careportal code * tidying existing API3 tests up to allow further grow * tidying the authorization code up to increase readability and performance a bit * more refactoring to ES6 and making APIv3 files structure more extendable * normalizing incoming dates to UTC and storing utcOffset * fixing srvDate to be of node.js server, not the mongo DB * preparing test fixtures for permissions testing + skeleton for CREATE operation test * intensive CREATE operation testing + minor bug fixes * correcting the deduplication test * more deduplication testing of CREATE operation * adding test skeletons for other generic operations * added variability in filtering by date, created_at, srvModified, srvCreated fields * fixing test accordingly to previous commit * adding new collection settings for centralized apps' settings storage * trying to solve travis CI testing problem - adding default collections names * another attempt to travis CI test fix * adding some tests for READ operation * adding custom error handler (overriding bodyparser's errors) * securing settings collection more and updating swagger accordingly * making HISTORY timestamp parameter more flexible + updating swagger documentation * more testing and bug fixing * sending only HTTP status with empty body, when there is no message + minor bug fixing * more refactoring and testing (especially of UPDATE operation) * PATCH testing + adding userModified field for troubleshooting purposes * basic SEARCH operation testing * more SEARCH operation testing * adding alternative 'now' query parameter to 'Date' header to make GET easier * adding 'now' to reserved query parameters for SEARCH operation * more testing * renaming field user to subject (and modifiedBy) * bugfix - fixing RFC 2822 constant for moment parsing * storageSocket: creating skeleton for new Socket.IO namespace * storageSocket: authentication by accessToken * storageSocket: authorizing to subscribe rooms * storageSocket: emitting create, update and delete events * APIv3: adding support for swagger UI at /api/v3/swagger-ui-dist * solving some problems detected by eslint * solving some problems detected by eslint * APIv3: testing and debugging Socket.IO * APIv3: testing and debugging Socket.IO * APIv3: Socket.IO documentation * APIv3: making the sample real * APIv3: starting to create a simple tutorial MD file * APIv3: small corrections * APIv3: minor corrections after dev merge * APIv3: adding CREATE and READ operations to the tutorial.md * APIv3: adding SEARCH, LAST MODIFIED, UPDATE operations to the tutorial.md * APIv3: finishing the tutorial.md * APIv3: minor bugfix (bad location after upsert) * APIv3: refactoring SEARCH complexity * APIv3: refactoring mongoCollection complexity * APIv3: refactoring complexity * APIv3: tidying up a bit * APIv3: refactoring security (start) * APIv3: refactoring lastModified * APIv3: refactoring create (start) * APIv3: refactoring create (finish) * APIv3: refactoring delete * APIv3: refactoring history * APIv3: refactoring update * APIv3: refactoring patch * APIv3: refactoring read * APIv3: refactoring search + removing deprecated authorizationBuilder * APIv3: adding best practise for identifier constructing * APIv3: refactoring and enhancing the validation (immutable fields) * APIv3: adding security.md documentation file * APIv3: refactoring - splitting index.js into multiple files * APIv3: calculating identifier on server side + deduplicating * APIv3: refactoring cosmetics * APIv3: updating the documentation * APIv3: making basic and security tests more readable using async/await * APIv3: making the rest of tests more readable using async/await * APIv3: adapting test of previous API * APIv3: adapting test of previous API --- .eslintrc.js | 9 + .gitignore | 6 + app.js | 13 +- env.js | 1 + lib/api3/const.json | 51 + lib/api3/doc/security.md | 48 + lib/api3/doc/socket.md | 142 ++ lib/api3/doc/tutorial.md | 329 ++++ lib/api3/generic/collection.js | 193 +++ lib/api3/generic/create/insert.js | 45 + lib/api3/generic/create/operation.js | 63 + lib/api3/generic/create/validate.js | 26 + lib/api3/generic/delete/operation.js | 93 ++ lib/api3/generic/history/operation.js | 151 ++ lib/api3/generic/patch/operation.js | 118 ++ lib/api3/generic/patch/validate.js | 19 + lib/api3/generic/read/operation.js | 75 + lib/api3/generic/search/input.js | 140 ++ lib/api3/generic/search/operation.js | 77 + lib/api3/generic/setup.js | 103 ++ lib/api3/generic/update/operation.js | 86 ++ lib/api3/generic/update/replace.js | 52 + lib/api3/generic/update/validate.js | 43 + lib/api3/index.js | 106 ++ lib/api3/security.js | 122 ++ lib/api3/shared/dateTools.js | 78 + lib/api3/shared/fieldsProjector.js | 82 + lib/api3/shared/operationTools.js | 111 ++ lib/api3/shared/storageTools.js | 63 + lib/api3/shared/stringTools.js | 28 + lib/api3/specific/lastModified.js | 101 ++ lib/api3/specific/status.js | 71 + lib/api3/specific/version.js | 28 + lib/api3/storage/mongoCollection/find.js | 93 ++ lib/api3/storage/mongoCollection/index.js | 90 ++ lib/api3/storage/mongoCollection/modify.js | 123 ++ lib/api3/storage/mongoCollection/utils.js | 178 +++ lib/api3/storageSocket.js | 145 ++ lib/api3/swagger.js | 41 + lib/api3/swagger.yaml | 1592 ++++++++++++++++++++ lib/authorization/index.js | 21 +- lib/server/bootevent.js | 2 +- lib/server/websocket.js | 4 + package.json | 1 + tests/api.devicestatus.test.js | 1 + tests/api3.basic.test.js | 49 + tests/api3.create.test.js | 487 ++++++ tests/api3.delete.test.js | 53 + tests/api3.generic.workflow.test.js | 257 ++++ tests/api3.patch.test.js | 219 +++ tests/api3.read.test.js | 180 +++ tests/api3.search.test.js | 261 ++++ tests/api3.security.test.js | 189 +++ tests/api3.socket.test.js | 178 +++ tests/api3.update.test.js | 289 ++++ tests/fixtures/api3/authSubject.js | 94 ++ tests/fixtures/api3/const.json | 138 ++ tests/fixtures/api3/instance.js | 163 ++ tests/fixtures/api3/localhost.crt | 18 + tests/fixtures/api3/localhost.key | 27 + tests/fixtures/api3/utils.js | 21 + 61 files changed, 7578 insertions(+), 9 deletions(-) create mode 100644 lib/api3/const.json create mode 100644 lib/api3/doc/security.md create mode 100644 lib/api3/doc/socket.md create mode 100644 lib/api3/doc/tutorial.md create mode 100644 lib/api3/generic/collection.js create mode 100644 lib/api3/generic/create/insert.js create mode 100644 lib/api3/generic/create/operation.js create mode 100644 lib/api3/generic/create/validate.js create mode 100644 lib/api3/generic/delete/operation.js create mode 100644 lib/api3/generic/history/operation.js create mode 100644 lib/api3/generic/patch/operation.js create mode 100644 lib/api3/generic/patch/validate.js create mode 100644 lib/api3/generic/read/operation.js create mode 100644 lib/api3/generic/search/input.js create mode 100644 lib/api3/generic/search/operation.js create mode 100644 lib/api3/generic/setup.js create mode 100644 lib/api3/generic/update/operation.js create mode 100644 lib/api3/generic/update/replace.js create mode 100644 lib/api3/generic/update/validate.js create mode 100644 lib/api3/index.js create mode 100644 lib/api3/security.js create mode 100644 lib/api3/shared/dateTools.js create mode 100644 lib/api3/shared/fieldsProjector.js create mode 100644 lib/api3/shared/operationTools.js create mode 100644 lib/api3/shared/storageTools.js create mode 100644 lib/api3/shared/stringTools.js create mode 100644 lib/api3/specific/lastModified.js create mode 100644 lib/api3/specific/status.js create mode 100644 lib/api3/specific/version.js create mode 100644 lib/api3/storage/mongoCollection/find.js create mode 100644 lib/api3/storage/mongoCollection/index.js create mode 100644 lib/api3/storage/mongoCollection/modify.js create mode 100644 lib/api3/storage/mongoCollection/utils.js create mode 100644 lib/api3/storageSocket.js create mode 100644 lib/api3/swagger.js create mode 100644 lib/api3/swagger.yaml create mode 100644 tests/api3.basic.test.js create mode 100644 tests/api3.create.test.js create mode 100644 tests/api3.delete.test.js create mode 100644 tests/api3.generic.workflow.test.js create mode 100644 tests/api3.patch.test.js create mode 100644 tests/api3.read.test.js create mode 100644 tests/api3.search.test.js create mode 100644 tests/api3.security.test.js create mode 100644 tests/api3.socket.test.js create mode 100644 tests/api3.update.test.js create mode 100644 tests/fixtures/api3/authSubject.js create mode 100644 tests/fixtures/api3/const.json create mode 100644 tests/fixtures/api3/instance.js create mode 100644 tests/fixtures/api3/localhost.crt create mode 100644 tests/fixtures/api3/localhost.key create mode 100644 tests/fixtures/api3/utils.js diff --git a/.eslintrc.js b/.eslintrc.js index 4bb1e696f5a..974a562c7c6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,15 @@ module.exports = { "commonjs": true, "es6": true, "node": true, + "mocha": true, "jquery": true + }, + "rules": { + "no-unused-vars": [ + "error", + { + "varsIgnorePattern": "should|expect" + } + ] } }; \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2cb28800650..5f82d7929f7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ npm-debug.log *.heapsnapshot /tmp +/.vs +/cgm-remote-monitor.njsproj +/cgm-remote-monitor.sln +/obj/Debug +/bin +/*.bat diff --git a/app.js b/app.js index 5b6f49b9708..c2f5d3858eb 100644 --- a/app.js +++ b/app.js @@ -114,11 +114,12 @@ function create (env, ctx) { }); } - /////////////////////////////////////////////////// - // api and json object variables - /////////////////////////////////////////////////// - var api = require('./lib/api/')(env, ctx); - var ddata = require('./lib/data/endpoints')(env, ctx); + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + var api = require('./lib/api/')(env, ctx); + var api3 = require('./lib/api3/')(env, ctx); + var ddata = require('./lib/data/endpoints')(env, ctx); app.use(compression({ filter: function shouldCompress (req, res) { @@ -172,6 +173,8 @@ function create (env, ctx) { app.use('/api/v2/authorization', ctx.authorization.endpoints); app.use('/api/v2/ddata', ddata); + app.use('/api/v3', api3); + // pebble data app.get('/pebble', ctx.pebble); diff --git a/env.js b/env.js index 9114e7297fc..e8aa36dc89f 100644 --- a/env.js +++ b/env.js @@ -112,6 +112,7 @@ function setStorage() { env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_'); env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); + env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food'); env.activity_collection = readENV('MONGO_ACTIVITY_COLLECTION', 'activity'); diff --git a/lib/api3/const.json b/lib/api3/const.json new file mode 100644 index 00000000000..f041134480d --- /dev/null +++ b/lib/api3/const.json @@ -0,0 +1,51 @@ +{ + "API3_VERSION": "3.0.0-alpha", + "API3_SECURITY_ENABLE": true, + "API3_TIME_SKEW_TOLERANCE": 5, + "API3_DEDUP_FALLBACK_ENABLED": true, + "API3_CREATED_AT_FALLBACK_ENABLED": true, + "API3_MAX_LIMIT": 1000, + + "HTTP": { + "OK": 200, + "CREATED": 201, + "NO_CONTENT": 204, + "NOT_MODIFIED": 304, + "BAD_REQUEST": 400, + "UNAUTHORIZED": 401, + "FORBIDDEN": 403, + "NOT_FOUND": 404, + "GONE": 410, + "PRECONDITION_FAILED": 412, + "INTERNAL_ERROR": 500 + }, + + "MSG": { + "HTTP_400_BAD_LAST_MODIFIED": "Bad or missing Last-Modified header/parameter", + "HTTP_400_BAD_LIMIT": "Parameter limit out of tolerance", + "HTTP_400_BAD_REQUEST_BODY": "Bad or missing request body", + "HTTP_400_BAD_FIELD_IDENTIFIER": "Bad or missing identifier field", + "HTTP_400_BAD_FIELD_DATE": "Bad or missing date field", + "HTTP_400_BAD_FIELD_UTC": "Bad or missing utcOffset field", + "HTTP_400_BAD_FIELD_APP": "Bad or missing app field", + "HTTP_400_BAD_SKIP": "Parameter skip out of tolerance", + "HTTP_400_SORT_SORT_DESC": "Parameters sort and sort_desc cannot be combined", + "HTTP_400_UNSUPPORTED_FILTER_OPERATOR": "Unsupported filter operator {0}", + "HTTP_400_IMMUTABLE_FIELD": "Field {0} cannot be modified by the client", + "HTTP_401_BAD_DATE": "Bad Date header", + "HTTP_401_BAD_TOKEN": "Bad access token or JWT", + "HTTP_401_DATE_OUT_OF_TOLERANCE": "Date header out of tolerance", + "HTTP_401_MISSING_DATE": "Missing Date header", + "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", + "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", + "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", + "HTTP_500_INTERNAL_ERROR": "Internal Server Error", + "STORAGE_ERROR": "Database error", + "SOCKET_MISSING_OR_BAD_ACCESS_TOKEN": "Missing or bad accessToken", + "SOCKET_UNAUTHORIZED_TO_ANY": "Unauthorized to receive any collection" + }, + + "MIN_TIMESTAMP": 946684800000, + "MIN_UTC_OFFSET": -1440, + "MAX_UTC_OFFSET": 1440 +} \ No newline at end of file diff --git a/lib/api3/doc/security.md b/lib/api3/doc/security.md new file mode 100644 index 00000000000..0fdf4c7d2aa --- /dev/null +++ b/lib/api3/doc/security.md @@ -0,0 +1,48 @@ +# APIv3: Security + +### Enforcing HTTPS +APIv3 is ment to run only under SSL version of HTTP protocol, which provides: +- **message secrecy** - once the secure channel between client and server is closed the communication cannot be eavesdropped by any third party +- **message consistency** - each request/response is protected against modification by any third party (any forgery would be detected) +- **authenticity of identities** - once the client and server establish the secured channel, it is guaranteed that the identity of the client or server does not change during the whole session + +HTTPS (in use with APIv3) does not address the true identity of the client, but ensures the correct identity of the server. Furthermore, HTTPS does not prevent the resending of previously intercepted encrypted messages by an attacker. + + +--- +### Authentication and authorization +In APIv3, *API_SECRET* can no longer be used for authentication or authorization. Instead, a roles/permissions security model is used, which is managed in the *Admin tools* section of the web application. + + +The identity of the client is represented by the *subject* to whom the access level is set by assigning security *roles*. One or more *permissions* can be assigned to each role. Permissions are used in an [Apache Shiro-like style](http://shiro.apache.org/permissions.html "Apache Shiro-like style"). + + +For each security *subject*, the system automatically generates an *access token* that is difficult to guess since it is derived from the secret *API_SECRET*. The *access token* must be included in every secured API operation to decode the client's identity and determine its authorization level. In this way, it is then possible to resolve whether the client has the permission required by a particular API operation. + + +There are two ways to authorize API calls: +- use `token` query parameter to pass the *access token*, eg. `token=testreadab-76eaff2418bfb7e0` +- use so-called [JSON Web Tokens](https://jwt.io "JSON Web Tokens") + - at first let the `/api/v2/authorization/request` generates you a particular JWT, eg. `GET https://nsapiv3.herokuapp.com/api/v2/authorization/request/testreadab-76eaff2418bfb7e0` + - then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires) + + + +--- +### Client timestamps +As previously mentioned, a potential attacker cannot decrypt the captured messages, but he can send them back to the client/server at any later time. APIv3 is partially preventing this by the temporal validity of each secured API call. + + +The client must include his current timestamp to each call so that the server can compare it against its clock. If the timestamp difference is not within the limit, the request is considered invalid. The tolerance limit is set in minutes in the `API3_TIME_SKEW_TOLERANCE` environment variable. + +There are two ways to include the client timestamp to the call: +- use `now` query parameter with UNIX epoch millisecond timestamp, eg. `now=1565041446908` +- add HTTP `Date` header to the request, eg. `Date: Sun, 12 May 2019 07:49:58 GMT` + + +The client can check each server response in the same way, because each response contains a server timestamp in the HTTP *Date* header as well. + + +--- +APIv3 security is enabled by default, but it can be completely disabled for development and debugging purposes by setting the web environment variable `API3_SECURITY_ENABLE=false`. +This setting is hazardous and it is strongly discouraged to be used for production purposes! diff --git a/lib/api3/doc/socket.md b/lib/api3/doc/socket.md new file mode 100644 index 00000000000..802a85e0235 --- /dev/null +++ b/lib/api3/doc/socket.md @@ -0,0 +1,142 @@ +# APIv3: Socket.IO storage modifications channel + +APIv3 has the ability to broadcast events about all created, edited and deleted documents, using Socket.IO library. + +This provides a real-time data exchange experience in combination with API REST operations. + +### Complete sample client code +```html + + + + + + + + APIv3 Socket.IO sample + + + + + + + + + + +``` + +**Important notice: Only changes made via APIv3 are being broadcasted. All direct database or APIv1 modifications are not included by this channel.** + +### Subscription (authorization) +The client must first subscribe to the channel that is exposed at `storage` namespace, ie the `/storage` subadress of the base Nightscout's web address (without `/api/v3` subaddress). +```javascript +const socket = io('https://nsapiv3.herokuapp.com/storage'); +``` + + +Subscription is requested by emitting `subscribe` event to the server, while including document with parameters: +* `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout. +* `collections`: optional array of collections which the client wants to subscribe to, by default all collections are requested) + +```javascript +socket.on('connect', function () { + socket.emit('subscribe', { + accessToken: 'testadmin-ad3b1f9d7b3f59d5', + collections: [ 'entries', 'treatments' ] + }, +``` + + +On the server, the subject is first identified and authenticated (by the accessToken) and then a verification takes place, if the subject has read access to each required collection. + +An exception is the `settings` collection for which `api:settings:admin` permission is required, for all other collections `api::read` permission is required. + + +If the authentication was successful and the client has read access to at least one collection, `success` = `true` is set in the response object and the field `collections` contains an array of collections which were actually subscribed (granted). +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 collections', data.collections); + } + else { + console.error(data.message); + } + }); +}); +``` + +### Receiving events +After the successful subscription the client can start listening to `create`, `update` and/or `delete` events of the socket. + + +##### create +`create` event fires each time a new document is inserted into the storage, regardless of whether it was CREATE or UPDATE operation of APIv3 (both of these operations are upserting/deduplicating, so they are "insert capable"). If the document already existed in the storage, the `update` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the inserted document in `doc` field + +```javascript +socket.on('create', function (data) { + console.log(`${data.colName}:created document`, data.doc); +}); +``` + + +##### update +`update` event fires each time an existing document is modified in the storage, regardless of whether it was CREATE, UPDATE or PATCH operation of APIv3 (all of these operations are "update capable"). If the document did not yet exist in the storage, the `create` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the new version of the modified document in `doc` field + +```javascript +socket.on('update', function (data) { + console.log(`${data.colName}:updated document`, data.doc); +}); +``` + + +##### delete +`delete` event fires each time an existing document is deleted in the storage, regardless of whether it was "soft" (marking as invalid) or permanent deleting. + +The received object contains: +* `colName` field with the name of the affected collection +* the identifier of the deleted document in the `identifier` field + +```javascript +socket.on('delete', function (data) { + console.log(`${data.colName}:deleted document with identifier`, data.identifier); +}); +``` \ No newline at end of file diff --git a/lib/api3/doc/tutorial.md b/lib/api3/doc/tutorial.md new file mode 100644 index 00000000000..3d8c656dfbd --- /dev/null +++ b/lib/api3/doc/tutorial.md @@ -0,0 +1,329 @@ +# APIv3: Basics tutorial + +Nightscout API v3 is a component of [cgm-remote-monitor](https://github.com/nightscout/cgm-remote-monitor) project. +It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + +There is a list of REST operations that the API v3 offers (inside `/api/v3` relative URL namespace), we will briefly introduce them in this file. + +Each NS instance with API v3 contains self-included OpenAPI specification at [/api/v3/swagger-ui-dist/](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/) relative URL. + + +--- +### VERSION + +[VERSION](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/other/get_version) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/version` client code (to get actual versions): +```javascript +const request = require('request'); + +request('https://nsapiv3.herokuapp.com/api/v3/version', + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "version":"0.12.2", + "apiVersion":"3.0.0-alpha", + "srvDate":1564386001772, + "storage":{ + "storage":"mongodb", + "version":"3.6.12" + } +} +``` + + +--- +### STATUS + +[STATUS](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/other/get_status) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/status` client code (to get my actual permissions): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "version":"0.12.2", + "apiVersion":"3.0.0-alpha", + "srvDate":1564391740738, + "storage":{ + "storage":"mongodb", + "version":"3.6.12" + }, + "apiPermissions":{ + "devicestatus":"crud", + "entries":"crud", + "food":"crud", + "profile":"crud", + "settings":"crud", + "treatments":"crud" + } +} +``` +`"crud"` represents create + read + update + delete permissions for the collection. + + +--- +### SEARCH + +[SEARCH](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/SEARCH) operation filters, sorts, paginates and projects documents from the collection. + +Sample GET `/entries` client code (to retrieve last 3 BG values): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`, + (error, response, body) => console.log(body)); +``` +Sample result: +``` +[ + { + "dateString":"2019-07-30T02:24:50.434+0200", + "sgv":115, + "direction":"FortyFiveDown" + }, + { + "dateString":"2019-07-30T02:19:50.374+0200", + "sgv":121, + "direction":"FortyFiveDown" + }, + { + "dateString":"2019-07-30T02:14:50.450+0200", + "sgv":129, + "direction":"FortyFiveDown" + } +] +``` + + +--- +### CREATE + +[CREATE](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/generic/post__collection_) operation inserts a new document into the collection. + +Sample POST `/treatments` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const doc = { + date: 1564591511232, // (new Date()).getTime(), + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.3 +}; +request({ + method: 'post', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}` + }, + (error, response, body) => console.log(response.headers.location)); +``` +Sample result: +``` +/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895 +``` + + +--- +### READ + +[READ](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/generic/get__collection___identifier_) operation retrieves you a single document from the collection by its identifier. + +Sample GET `/treatments/{identifier}` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; + +request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +``` +{ + "date":1564591511232, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Correction Bolus", + "insulin":0.3, + "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "utcOffset":0, + "created_at":"2019-07-31T16:45:11.232Z", + "srvModified":1564591627732, + "srvCreated":1564591511711, + "subject":"test-admin" +} +``` + + +--- +### LAST MODIFIED + +[LAST MODIFIED](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/other/LAST-MODIFIED) operation finds the date of last modification for each collection. + +Sample GET `/lastModified` client code (to get latest modification dates): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "srvDate":1564591783202, + "collections":{ + "devicestatus":1564591490074, + "entries":1564591486801, + "profile":1548524042744, + "treatments":1564591627732 + } +} +``` + + +--- +### UPDATE + +[UPDATE](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/put__collection___identifier_) operation updates existing document in the collection. + +Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; +const doc = { + date: 1564591511232, + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.4 +}; + +request({ + method: 'put', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### PATCH + +[PATCH](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/patch__collection___identifier_) operation partially updates existing document in the collection. + +Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; +const doc = { + insulin: 0.5 +}; + +request({ + method: 'patch', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### DELETE + +[DELETE](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/delete__collection___identifier_) operation deletes existing document from the collection. + +Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; + +request({ + method: 'delete', + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### HISTORY + +[HISTORY](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/HISTORY2) operation queries all changes since the timestamp. + +Sample HISTORY `/treatments/history/{lastModified}` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const lastModified = 1564521267421; + +request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`, + (error, response, body) => console.log(response.body)); +``` +Sample result: +``` +[ + { + "date":1564521267421, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Correction Bolus", + "insulin":0.5, + "utcOffset":0, + "created_at":"2019-07-30T21:14:27.421Z", + "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "srvModified":1564592440416, + "srvCreated":1564592334853, + "subject":"test-admin", + "modifiedBy":"test-admin", + "isValid":false + }, + { + "date":1564592545299, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Snack Bolus", + "carbs":10, + "identifier":"267c43c2-f629-5191-a542-4f410c69e486", + "utcOffset":0, + "created_at":"2019-07-31T17:02:25.299Z", + "srvModified":1564592545781, + "srvCreated":1564592545781, + "subject":"test-admin" + } +] +``` +Notice the `"isValid":false` field marking the deletion of the document. \ No newline at end of file diff --git a/lib/api3/generic/collection.js b/lib/api3/generic/collection.js new file mode 100644 index 00000000000..0a1a29b3915 --- /dev/null +++ b/lib/api3/generic/collection.js @@ -0,0 +1,193 @@ +'use strict'; + +const apiConst = require('../const.json') + , _ = require('lodash') + , dateTools = require('../shared/dateTools') + , opTools = require('../shared/operationTools') + , stringTools = require('../shared/stringTools') + , CollectionStorage = require('../storage/mongoCollection') + , searchOperation = require('./search/operation') + , createOperation = require('./create/operation') + , readOperation = require('./read/operation') + , updateOperation = require('./update/operation') + , patchOperation = require('./patch/operation') + , deleteOperation = require('./delete/operation') + , historyOperation = require('./history/operation') + ; + +/** + * Generic collection (abstraction over each collection specifics) + * @param {string} colName - name of the collection inside the storage system + * @param {function} fallbackGetDate - function that tries to create srvModified virtually from other fields of document + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @param {function} fallbackHistoryFilter - function that creates storage filter for all newer records (than the timestamp from first function parameter) + */ +function Collection ({ ctx, env, app, colName, storageColName, fallbackGetDate, dedupFallbackFields, + fallbackDateField }) { + + const self = this; + + self.colName = colName; + self.fallbackGetDate = fallbackGetDate; + self.dedupFallbackFields = app.get('API3_DEDUP_FALLBACK_ENABLED') ? dedupFallbackFields : []; + self.autoPruneDays = app.setENVTruthy('API3_AUTOPRUNE_' + colName.toUpperCase()); + self.nextAutoPrune = new Date(); + self.storage = new CollectionStorage(ctx, env, storageColName); + self.fallbackDateField = fallbackDateField; + + self.mapRoutes = function mapRoutes () { + const prefix = '/' + colName + , prefixId = prefix + '/:identifier' + , prefixHistory = prefix + '/history' + ; + + + // GET /{collection} + app.get(prefix, searchOperation(ctx, env, app, self)); + + // POST /{collection} + app.post(prefix, createOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory, historyOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory + '/:lastModified', historyOperation(ctx, env, app, self)); + + // GET /{collection}/{identifier} + app.get(prefixId, readOperation(ctx, env, app, self)); + + // PUT /{collection}/{identifier} + app.put(prefixId, updateOperation(ctx, env, app, self)); + + // PATCH /{collection}/{identifier} + app.patch(prefixId, patchOperation(ctx, env, app, self)); + + // DELETE /{collection}/{identifier} + app.delete(prefixId, deleteOperation(ctx, env, app, self)); + }; + + + /** + * Parse limit (max document count) from query string + */ + self.parseLimit = function parseLimit (req, res) { + const maxLimit = app.get('API3_MAX_LIMIT'); + let limit = maxLimit; + + if (req.query.limit) { + if (!isNaN(req.query.limit) && req.query.limit > 0 && req.query.limit <= maxLimit) { + limit = parseInt(req.query.limit); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LIMIT); + return null; + } + } + + return limit; + }; + + + + /** + * Fetch modified date from document (with possible fallback and back-fill to srvModified/srvCreated) + * @param {Object} doc - document loaded from database + */ + self.resolveDates = function resolveDates (doc) { + let modifiedDate; + try { + if (doc.srvModified) { + modifiedDate = new Date(doc.srvModified); + } + else { + if (typeof (self.fallbackGetDate) === 'function') { + modifiedDate = self.fallbackGetDate(doc); + if (modifiedDate) { + doc.srvModified = modifiedDate.getTime(); + } + } + } + + if (doc.srvModified && !doc.srvCreated) { + doc.srvCreated = modifiedDate.getTime(); + } + } + catch (error) { + console.warn(error); + } + return modifiedDate; + }; + + + /** + * Deletes old documents from the collection if enabled (for this collection) + * in the background (asynchronously) + * */ + self.autoPrune = function autoPrune () { + + if (!stringTools.isNumberInString(self.autoPruneDays)) + return; + + const autoPruneDays = parseFloat(self.autoPruneDays); + if (autoPruneDays <= 0) + return; + + if (new Date() > self.nextAutoPrune) { + + const deleteBefore = new Date(new Date().getTime() - (autoPruneDays * 24 * 3600 * 1000)); + + const filter = [ + { field: 'srvCreated', operator: 'lt', value: deleteBefore.getTime() }, + { field: 'created_at', operator: 'lt', value: deleteBefore.toISOString() }, + { field: 'date', operator: 'lt', value: deleteBefore.getTime() } + ]; + + // let's autoprune asynchronously (we won't wait for the result) + self.storage.deleteManyOr(filter, function deleteDone (err, result) { + if (err || !result) { + console.error(err); + } + + if (result.deleted) { + console.info('Auto-pruned ' + result.deleted + ' documents from ' + self.colName + ' collection '); + } + }); + + self.nextAutoPrune = new Date(new Date().getTime() + (3600 * 1000)); + } + }; + + + /** + * Parse date and utcOffset + optional created_at fallback + * @param {Object} doc + */ + self.parseDate = function parseDate (doc) { + if (!_.isEmpty(doc)) { + + let values = app.get('API3_CREATED_AT_FALLBACK_ENABLED') + ? [doc.date, doc.created_at] + : [doc.date]; + + let m = dateTools.parseToMoment(values); + if (m && m.isValid()) { + doc.date = m.valueOf(); + + if (typeof doc.utcOffset === 'undefined') { + doc.utcOffset = m.utcOffset(); + } + + if (app.get('API3_CREATED_AT_FALLBACK_ENABLED')) { + doc.created_at = m.toISOString(); + } + else { + if (doc.created_at) + delete doc.created_at; + } + } + } + } +} + +module.exports = Collection; \ No newline at end of file diff --git a/lib/api3/generic/create/insert.js b/lib/api3/generic/create/insert.js new file mode 100644 index 00000000000..4ac80a37e94 --- /dev/null +++ b/lib/api3/generic/create/insert.js @@ -0,0 +1,45 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + ; + +/** + * Insert new document into the collection + * @param {Object} opCtx + * @param {Object} doc + */ +async function insert (opCtx, doc) { + + const { ctx, auth, col, req, res } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:create`); + + if (validate(opCtx, doc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const identifier = await col.storage.insertOne(doc); + + if (!identifier) + throw new Error('empty identifier'); + + res.setHeader('Last-Modified', now.toUTCString()); + res.setHeader('Location', `${req.baseUrl}${req.path}/${identifier}`); + res.status(apiConst.HTTP.CREATED).send({ }); + + ctx.bus.emit('storage-socket-create', { colName: col.colName, doc }); + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +module.exports = insert; \ No newline at end of file diff --git a/lib/api3/generic/create/operation.js b/lib/api3/generic/create/operation.js new file mode 100644 index 00000000000..39986a87ebd --- /dev/null +++ b/lib/api3/generic/create/operation.js @@ -0,0 +1,63 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('./insert') + , replace = require('../update/replace') + , opTools = require('../../shared/operationTools') + ; + + +/** + * CREATE: Inserts a new document into the collection + */ +async function create (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + const identifyingFilter = col.storage.identifyingFilter(doc.identifier, doc, col.dedupFallbackFields); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + if (result.length > 0) { + const storageDoc = result[0]; + await replace(opCtx, doc, storageDoc, { isDeduplication: true }); + } + else { + await insert(opCtx, doc); + } +} + + +function createOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await create(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = createOperation; \ No newline at end of file diff --git a/lib/api3/generic/create/validate.js b/lib/api3/generic/create/validate.js new file mode 100644 index 00000000000..e978a3955e5 --- /dev/null +++ b/lib/api3/generic/create/validate.js @@ -0,0 +1,26 @@ +'use strict'; + +const apiConst = require('../../const.json') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to create + * @param {Object} opCtx + * @param {Object} doc + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc) { + + const { res } = opCtx; + + if (typeof(doc.identifier) !== 'string' || stringTools.isNullOrWhitespace(doc.identifier)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_IDENTIFIER); + } + + return opTools.validateCommon(doc, res); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js new file mode 100644 index 00000000000..535ca1a7620 --- /dev/null +++ b/lib/api3/generic/delete/operation.js @@ -0,0 +1,93 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + ; + +/** + * DELETE: Deletes a document from the collection + */ +async function doDelete (opCtx) { + + const { col, req } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:delete`); + + if (req.query.permanent && req.query.permanent === "true") { + await deletePermanently(opCtx); + } else { + await markAsDeleted(opCtx); + } +} + + +async function deletePermanently (opCtx) { + + const { ctx, col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.deleteOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (!result.deleted) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + col.autoPrune(); + ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); + ctx.bus.emit('data-received'); + return res.status(apiConst.HTTP.NO_CONTENT).end(); +} + + +async function markAsDeleted (opCtx) { + + const { ctx, col, req, res, auth } = opCtx; + + const identifier = req.params.identifier; + const setFields = { 'isValid': false, 'srvModified': (new Date).getTime() }; + + if (auth && auth.subject && auth.subject.name) { + setFields.modifiedBy = auth.subject.name; + } + + const result = await col.storage.updateOne(identifier, setFields); + + if (!result) + throw new Error('empty result'); + + if (!result.updated) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); + col.autoPrune(); + ctx.bus.emit('data-received'); + return res.status(apiConst.HTTP.NO_CONTENT).end(); +} + + +function deleteOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await doDelete(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = deleteOperation; \ No newline at end of file diff --git a/lib/api3/generic/history/operation.js b/lib/api3/generic/history/operation.js new file mode 100644 index 00000000000..0929a09cc4f --- /dev/null +++ b/lib/api3/generic/history/operation.js @@ -0,0 +1,151 @@ +'use strict'; + +const dateTools = require('../../shared/dateTools') + , apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , FieldsProjector = require('../../shared/fieldsProjector') + , _ = require('lodash') + ; + +/** + * HISTORY: Retrieves incremental changes since timestamp + */ +async function history (opCtx, fieldsProjector) { + + const { req, res, col } = opCtx; + + let filter = parseFilter(opCtx) + , limit = col.parseLimit(req, res) + , projection = fieldsProjector.storageProjection() + , sort = prepareSort() + , skip = 0 + , onlyValid = false + , logicalOperator = 'or' + ; + + if (filter !== null && limit !== null && projection !== null) { + + const result = await col.storage.findMany(filter + , sort + , limit + , skip + , projection + , onlyValid + , logicalOperator); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NO_CONTENT).end(); + } + + _.each(result, col.resolveDates); + + const srvModifiedValues = _.map(result, function mapSrvModified (item) { + return item.srvModified; + }) + , maxSrvModified = _.max(srvModifiedValues); + + res.setHeader('Last-Modified', (new Date(maxSrvModified)).toUTCString()); + res.setHeader('ETag', 'W/"' + maxSrvModified + '"'); + + _.each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK).send(result); + } +} + + +/** + * Parse history filtering criteria from Last-Modified header + */ +function parseFilter (opCtx) { + + const { req, res } = opCtx; + + let lastModified = null + , lastModifiedParam = req.params.lastModified + , operator = null; + + if (lastModifiedParam) { + + // using param in URL as a source of timestamp + const m = dateTools.parseToMoment(lastModifiedParam); + + if (m === null || !m.isValid()) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + lastModified = m.toDate(); + operator = 'gt'; + } + else { + // using request HTTP header as a source of timestamp + const lastModifiedHeader = req.get('Last-Modified'); + if (!lastModifiedHeader) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + try { + lastModified = dateTools.floorSeconds(new Date(lastModifiedHeader)); + } catch (err) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + operator = 'gte'; + } + + return [ + { field: 'srvModified', operator: operator, value: lastModified.getTime() }, + { field: 'created_at', operator: operator, value: lastModified.toISOString() }, + { field: 'date', operator: operator, value: lastModified.getTime() } + ]; +} + + + +/** + * Prepare sorting for storage query + */ +function prepareSort () { + return { + srvModified: 1, + created_at: 1, + date: 1 + }; +} + + +function historyOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + await history(opCtx, fieldsProjector); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = historyOperation; \ No newline at end of file diff --git a/lib/api3/generic/patch/operation.js b/lib/api3/generic/patch/operation.js new file mode 100644 index 00000000000..d7bb5fc2b4d --- /dev/null +++ b/lib/api3/generic/patch/operation.js @@ -0,0 +1,118 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * PATCH: Partially updates document in the collection + */ +async function patch (opCtx) { + + const { req, res, col } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + col.parseDate(doc); + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('result empty'); + + if (result.length > 0) { + + const storageDoc = result[0]; + if (storageDoc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + } + + await applyPatch(opCtx, identifier, doc, storageDoc); + } + else { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } +} + + +/** + * Patch existing document in the collection + * @param {Object} opCtx + * @param {string} identifier + * @param {Object} doc - fields and values to patch + * @param {Object} storageDoc - original (database) version of document + */ +async function applyPatch (opCtx, identifier, doc, storageDoc) { + + const { ctx, res, col, auth } = opCtx; + + if (validate(opCtx, doc, storageDoc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + + if (auth && auth.subject && auth.subject.name) { + doc.modifiedBy = auth.subject.name; + } + + const matchedCount = await col.storage.updateOne(identifier, doc); + + if (!matchedCount) + throw new Error('matchedCount empty'); + + res.setHeader('Last-Modified', now.toUTCString()); + res.status(apiConst.HTTP.NO_CONTENT).send({ }); + + const fieldsProjector = new FieldsProjector('_all'); + const patchedDocs = await col.storage.findOne(identifier, fieldsProjector); + const patchedDoc = patchedDocs[0]; + fieldsProjector.applyProjection(patchedDoc); + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc: patchedDoc }); + + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +function patchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await patch(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = patchOperation; \ No newline at end of file diff --git a/lib/api3/generic/patch/validate.js b/lib/api3/generic/patch/validate.js new file mode 100644 index 00000000000..057bb5c39e8 --- /dev/null +++ b/lib/api3/generic/patch/validate.js @@ -0,0 +1,19 @@ +'use strict'; + +const updateValidate = require('../update/validate') + ; + + +/** + * Validate document to patch + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @returns string - null if validation fails + */ +function validate (opCtx, doc, storageDoc) { + + return updateValidate(opCtx, doc, storageDoc, { isPatching: true }); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/read/operation.js b/lib/api3/generic/read/operation.js new file mode 100644 index 00000000000..04d6f03bc70 --- /dev/null +++ b/lib/api3/generic/read/operation.js @@ -0,0 +1,75 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * READ: Retrieves a single document from the collection + */ +async function read (opCtx) { + + const { req, res, col } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:read`); + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const result = await col.storage.findOne(req.params.identifier + , fieldsProjector.storageProjection()); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + const doc = result[0]; + if (doc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + + const modifiedDate = col.resolveDates(doc); + if (modifiedDate) { + res.setHeader('Last-Modified', modifiedDate.toUTCString()); + + const ifModifiedSince = req.get('If-Modified-Since'); + + if (ifModifiedSince + && dateTools.floorSeconds(modifiedDate) <= dateTools.floorSeconds(new Date(ifModifiedSince))) { + return res.status(apiConst.HTTP.NOT_MODIFIED).end(); + } + } + + fieldsProjector.applyProjection(doc); + + res.status(apiConst.HTTP.OK).send(doc); +} + + +function readOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await read(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = readOperation; \ No newline at end of file diff --git a/lib/api3/generic/search/input.js b/lib/api3/generic/search/input.js new file mode 100644 index 00000000000..dbd37356760 --- /dev/null +++ b/lib/api3/generic/search/input.js @@ -0,0 +1,140 @@ +'use strict'; + +const apiConst = require('../../const.json') + , dateTools = require('../../shared/dateTools') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + +const filterRegex = /(.*)\$([a-zA-Z]+)/; + + +/** + * Parse value of the parameter (to the correct data type) + */ +function parseValue(param, value) { + + value = stringTools.isNumberInString(value) ? parseFloat(value) : value; // convert number from string + + // convert boolean from string + if (value === 'true') + value = true; + + if (value === 'false') + value = false; + + // unwrap string in single quotes + if (typeof(value) === 'string' && value.startsWith('\'') && value.endsWith('\'')) { + value = value.substr(1, value.length - 2); + } + + if (['date', 'srvModified', 'srvCreated'].includes(param)) { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.valueOf(); + } + } + + if (param === 'created_at') { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.toISOString(); + } + } + + return value; +} + + +/** + * Parse filtering criteria from query string + */ +function parseFilter (req, res) { + const filter = [] + , reservedParams = ['token', 'sort', 'sort$desc', 'limit', 'skip', 'fields', 'now'] + , operators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 're'] + ; + + for (let param in req.query) { + if (!Object.prototype.hasOwnProperty.call(req.query, param) + || reservedParams.includes(param)) continue; + + let field = param + , operator = 'eq' + ; + + const match = filterRegex.exec(param); + if (match != null) { + operator = match[2]; + field = match[1]; + + if (!operators.includes(operator)) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_UNSUPPORTED_FILTER_OPERATOR.replace('{0}', operator)); + return null; + } + } + const value = parseValue(field, req.query[param]); + + filter.push({ field, operator, value }); + } + + return filter; +} + + +/** + * Parse sorting from query string + */ +function parseSort (req, res) { + let sort = {} + , sortDirection = 1; + + if (req.query.sort && req.query.sort$desc) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_SORT_SORT_DESC); + return null; + } + + if (req.query.sort$desc) { + sortDirection = -1; + sort[req.query.sort$desc] = sortDirection; + } + else { + if (req.query.sort) { + sort[req.query.sort] = sortDirection; + } + } + + sort.identifier = sortDirection; + sort.created_at = sortDirection; + sort.date = sortDirection; + + return sort; +} + + +/** + * Parse skip (offset) from query string + */ +function parseSkip (req, res) { + let skip = 0; + + if (req.query.skip) { + if (!isNaN(req.query.skip) && req.query.skip >= 0) { + skip = parseInt(req.query.skip); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_SKIP); + return null; + } + } + + return skip; +} + + +module.exports = { + parseFilter, + parseSort, + parseSkip +}; \ No newline at end of file diff --git a/lib/api3/generic/search/operation.js b/lib/api3/generic/search/operation.js new file mode 100644 index 00000000000..074f864d58a --- /dev/null +++ b/lib/api3/generic/search/operation.js @@ -0,0 +1,77 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , input = require('./input') + , _each = require('lodash/each') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + + +/** + * SEARCH: Search documents from the collection + */ +async function search (opCtx) { + + const { req, res, col } = opCtx; + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const filter = input.parseFilter(req, res) + , sort = input.parseSort(req, res) + , limit = col.parseLimit(req, res) + , skip = input.parseSkip(req, res) + , projection = fieldsProjector.storageProjection() + , onlyValid = true + ; + + + if (filter !== null && sort !== null && limit !== null && skip !== null && projection !== null) { + + const result = await col.storage.findMany(filter + , sort + , limit + , skip + , projection + , onlyValid); + + if (!result) + throw new Error('empty result'); + + _each(result, col.resolveDates); + + _each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK).send(result); + } +} + + +function searchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await search(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = searchOperation; \ No newline at end of file diff --git a/lib/api3/generic/setup.js b/lib/api3/generic/setup.js new file mode 100644 index 00000000000..17e118658dd --- /dev/null +++ b/lib/api3/generic/setup.js @@ -0,0 +1,103 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../shared/dateTools') + , Collection = require('./collection') + ; + + +function fallbackDate (doc) { + const m = dateTools.parseToMoment(doc.date); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function fallbackCreatedAt (doc) { + const m = dateTools.parseToMoment(doc.created_at); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function setupGenericCollections (ctx, env, app) { + const cols = { } + , enabledCols = app.get('enabledCollections'); + + if (_.includes(enabledCols, 'devicestatus')) { + cols.devicestatus = new Collection({ + ctx, env, app, + colName: 'devicestatus', + storageColName: env.devicestatus_collection || 'devicestatus', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'device'], + fallbackDateField: 'created_at' + }); + } + + const entriesCollection = new Collection({ + ctx, env, app, + colName: 'entries', + storageColName: env.entries_collection || 'entries', + fallbackGetDate: fallbackDate, + dedupFallbackFields: ['date', 'type'], + fallbackDateField: 'date' + }); + app.set('entriesCollection', entriesCollection); + + if (_.includes(enabledCols, 'entries')) { + cols.entries = entriesCollection; + } + + if (_.includes(enabledCols, 'food')) { + cols.food = new Collection({ + ctx, env, app, + colName: 'food', + storageColName: env.food_collection || 'food', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'profile')) { + cols.profile = new Collection({ + ctx, env, app, + colName: 'profile', + storageColName: env.profile_collection || 'profile', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'settings')) { + cols.settings = new Collection({ + ctx, env, app, + colName: 'settings', + storageColName: env.settings_collection || 'settings' + }); + } + + if (_.includes(enabledCols, 'treatments')) { + cols.treatments = new Collection({ + ctx, env, app, + colName: 'treatments', + storageColName: env.treatments_collection || 'treatments', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'eventType'], + fallbackDateField: 'created_at' + }); + } + + _.forOwn(cols, function forMember (col) { + col.mapRoutes(); + }); + + app.set('collections', cols); +} + + +module.exports = setupGenericCollections; diff --git a/lib/api3/generic/update/operation.js b/lib/api3/generic/update/operation.js new file mode 100644 index 00000000000..3e517a32d11 --- /dev/null +++ b/lib/api3/generic/update/operation.js @@ -0,0 +1,86 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../../shared/dateTools') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('../create/insert') + , replace = require('./replace') + , opTools = require('../../shared/operationTools') + ; + +/** + * UPDATE: Updates a document in the collection + */ +async function update (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + doc.identifier = identifier; + + if (result.length > 0) { + await updateConditional(opCtx, doc, result[0]); + } + else { + await insert(opCtx, doc); + } +} + + +async function updateConditional (opCtx, doc, storageDoc) { + + const { col, req, res } = opCtx; + + if (storageDoc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + } + + await replace(opCtx, doc, storageDoc); +} + + +function updateOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await update(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = updateOperation; \ No newline at end of file diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js new file mode 100644 index 00000000000..ca490b31136 --- /dev/null +++ b/lib/api3/generic/update/replace.js @@ -0,0 +1,52 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + ; + +/** + * Replace existing document in the collection + * @param {Object} opCtx + * @param {any} doc - new version of document to set + * @param {any} storageDoc - old version of document (existing in the storage) + * @param {Object} options + */ +async function replace (opCtx, doc, storageDoc, options) { + + const { ctx, auth, col, req, res } = opCtx; + const { isDeduplication } = options || {}; + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + if (validate(opCtx, doc, storageDoc, { isDeduplication }) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = storageDoc.srvCreated || doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const matchedCount = await col.storage.replaceOne(storageDoc.identifier, doc); + + if (!matchedCount) + throw new Error('empty matchedCount'); + + res.setHeader('Last-Modified', now.toUTCString()); + + if (storageDoc.identifier !== doc.identifier || isDeduplication) { + res.setHeader('Location', `${req.baseUrl}${req.path}/${doc.identifier}`); + } + + res.status(apiConst.HTTP.NO_CONTENT).send({ }); + + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc }); + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +module.exports = replace; \ No newline at end of file diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js new file mode 100644 index 00000000000..e68eb2955f8 --- /dev/null +++ b/lib/api3/generic/update/validate.js @@ -0,0 +1,43 @@ +'use strict'; + +const apiConst = require('../../const.json') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to update + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @param {Object} options + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc, storageDoc, options) { + + const { res } = opCtx; + const { isPatching, isDeduplication } = options || {}; + + const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', + 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; + + for (const field of immutable) { + + // change of identifier is allowed in deduplication (for APIv1 documents) + if (field === 'identifier' && isDeduplication) + continue; + + // changing deleted document is without restrictions + if (storageDoc.isValid === false) + continue; + + if (typeof(doc[field]) !== 'undefined' && doc[field] !== storageDoc[field]) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_IMMUTABLE_FIELD.replace('{0}', field)); + } + } + + return opTools.validateCommon(doc, res, { isPatching }); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/index.js b/lib/api3/index.js new file mode 100644 index 00000000000..70fb6a4d59e --- /dev/null +++ b/lib/api3/index.js @@ -0,0 +1,106 @@ +'use strict'; + +const express = require('express') + , bodyParser = require('body-parser') + , StorageSocket = require('./storageSocket') + , apiConst = require('./const.json') + , security = require('./security') + , genericSetup = require('./generic/setup') + , swaggerSetup = require('./swagger') + ; + +function configure (env, ctx) { + + const self = { } + , app = express() + ; + + self.setENVTruthy = function setENVTruthy (varName, defaultValue) { + //for some reason Azure uses this prefix, maybe there is a good reason + let value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; + + value = value != null ? value : defaultValue; + + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + + app.set(varName, value); + return value; + }; + app.setENVTruthy = self.setENVTruthy; + + + self.setupApiEnvironment = function setupApiEnvironment () { + + app.use(bodyParser.json({ + limit: 1048576 * 50 + }), function errorHandler (err, req, res, next) { + console.error(err); + res.status(500).json({ + status: 500, + message: apiConst.MSG.HTTP_500_INTERNAL_ERROR + }); + if (next) { // we need 4th parameter next to behave like error handler, but we have to use it to prevent "unused variable" message + } + }); + + // we don't need these here + app.set('etag', false); + app.set('x-powered-by', false); // this seems to be unreliable + app.use(function (req, res, next) { + res.removeHeader('x-powered-by'); + next(); + }); + + app.set('name', env.name); + app.set('version', env.version); + app.set('apiVersion', apiConst.API3_VERSION); + app.set('units', env.DISPLAY_UNITS); + app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']); + + self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE); + self.setENVTruthy('API3_TIME_SKEW_TOLERANCE', apiConst.API3_TIME_SKEW_TOLERANCE); + self.setENVTruthy('API3_DEDUP_FALLBACK_ENABLED', apiConst.API3_DEDUP_FALLBACK_ENABLED); + self.setENVTruthy('API3_CREATED_AT_FALLBACK_ENABLED', apiConst.API3_CREATED_AT_FALLBACK_ENABLED); + self.setENVTruthy('API3_MAX_LIMIT', apiConst.API3_MAX_LIMIT); + }; + + + self.setupApiRoutes = function setupApiRoutes () { + + app.get('/version', require('./specific/version')(app, ctx, env)); + + if (app.get('env') === 'development') { // for development and testing purposes only + app.get('/test', async function test (req, res) { + + try { + const opCtx = {app, ctx, env, req, res}; + opCtx.auth = await security.authenticate(opCtx); + await security.demandPermission(opCtx, 'api:entries:read'); + res.status(200).end(); + } catch (error) { + console.error(error); + } + }); + } + + app.get('/lastModified', require('./specific/lastModified')(app, ctx, env)); + + app.get('/status', require('./specific/status')(app, ctx, env)); + }; + + + self.setupApiEnvironment(); + genericSetup(ctx, env, app); + self.setupApiRoutes(); + swaggerSetup(app); + + ctx.storageSocket = new StorageSocket(app, env, ctx); + + return app; +} + +module.exports = configure; diff --git a/lib/api3/security.js b/lib/api3/security.js new file mode 100644 index 00000000000..33099d88f12 --- /dev/null +++ b/lib/api3/security.js @@ -0,0 +1,122 @@ +'use strict'; + +const moment = require('moment') + , apiConst = require('./const.json') + , _ = require('lodash') + , shiroTrie = require('shiro-trie') + , dateTools = require('./shared/dateTools') + , opTools = require('./shared/operationTools') + ; + + +/** + * Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending) + */ +function checkDateHeader (opCtx) { + + const { app, req, res } = opCtx; + + let dateString = req.header('Date'); + if (!dateString) { + dateString = req.query.now; + } + + if (!dateString) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_DATE); + } + + let dateMoment = dateTools.parseToMoment(dateString); + if (!dateMoment) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_DATE); + } + + let nowMoment = moment(new Date()); + let diffMinutes = moment.duration(nowMoment.diff(dateMoment)).asMinutes(); + + if (Math.abs(diffMinutes) > app.get('API3_TIME_SKEW_TOLERANCE')) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + } + + return true; +} + + +function authenticate (opCtx) { + return new Promise(function promise (resolve, reject) { + + let { app, ctx, req, res } = opCtx; + + if (!app.get('API3_SECURITY_ENABLE')) { + const adminShiro = shiroTrie.new(); + adminShiro.add('*'); + return resolve({ shiros: [ adminShiro ] }); + } + + if (req.protocol !== 'https') { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_NOT_USING_HTTPS)); + } + + const checkDateResult = checkDateHeader(opCtx); + if (checkDateResult !== true) { + return checkDateResult; + } + + let token = ctx.authorization.extractToken(req); + if (!token) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN)); + } + + ctx.authorization.resolve({ token }, function resolveFinish (err, result) { + if (err) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_TOKEN)); + } + else { + return resolve(result); + } + }); + }); +} + + +/** + * Checks for the permission from the authorization without error response sending + * @param {any} auth + * @param {any} permission + */ +function checkPermission (auth, permission) { + + if (auth) { + const found = _.find(auth.shiros, function checkEach (shiro) { + return shiro && shiro.check(permission); + }); + return _.isObject(found); + } + else { + return false; + } +} + + + +function demandPermission (opCtx, permission) { + return new Promise(function promise (resolve, reject) { + const { auth, res } = opCtx; + + if (checkPermission(auth, permission)) { + return resolve(true); + } else { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', permission))); + } + }); +} + + +module.exports = { + authenticate, + checkPermission, + demandPermission +}; \ No newline at end of file diff --git a/lib/api3/shared/dateTools.js b/lib/api3/shared/dateTools.js new file mode 100644 index 00000000000..14b67f9e109 --- /dev/null +++ b/lib/api3/shared/dateTools.js @@ -0,0 +1,78 @@ +'use strict'; + +const moment = require('moment') + , stringTools = require('./stringTools') + , apiConst = require('../const.json') + ; + + +/** + * Floor date to whole seconds (cut off milliseconds) + * @param {Date} date + */ +function floorSeconds (date) { + let ms = date.getTime(); + ms -= ms % 1000; + return new Date(ms); +} + + +/** + * Parse date as moment object from value or array of values. + * @param {any} value + */ +function parseToMoment (value) +{ + if (!value) + return null; + + if (Array.isArray(value)) { + for (let item of value) { + let m = parseToMoment(item); + + if (m !== null) + return m; + } + } + else { + + if (typeof value === 'string' && stringTools.isNumberInString(value)) { + value = parseFloat(value); + } + + if (typeof value === 'number') { + let m = moment(value); + + if (!m.isValid()) + return null; + + if (m.valueOf() < apiConst.MIN_TIMESTAMP) + m = moment.unix(m); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + + if (typeof value === 'string') { + let m = moment.parseZone(value, moment.ISO_8601); + + if (!m.isValid()) + m = moment.parseZone(value, moment.RFC_2822); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + } + + // no parsing option succeeded => failure + return null; +} + +module.exports = { + floorSeconds, + parseToMoment +}; diff --git a/lib/api3/shared/fieldsProjector.js b/lib/api3/shared/fieldsProjector.js new file mode 100644 index 00000000000..921c7cc6df8 --- /dev/null +++ b/lib/api3/shared/fieldsProjector.js @@ -0,0 +1,82 @@ +'use strict'; + +const _each = require('lodash/each'); + +/** + * Decoder of 'fields' parameter providing storage projections + * @param {string} fieldsString - fields parameter from user + */ +function FieldsProjector (fieldsString) { + + const self = this + , exclude = []; + let specific = null; + + switch (fieldsString) + { + case '_all': + break; + + default: + if (fieldsString) { + specific = fieldsString.split(','); + } + } + + const systemFields = ['identifier', 'srvCreated', 'created_at', 'date']; + + /** + * Prepare projection definition for storage query + * */ + self.storageProjection = function storageProjection () { + const projection = { }; + + if (specific) { + _each(specific, function include (field) { + projection[field] = 1; + }); + + _each(systemFields, function include (field) { + projection[field] = 1; + }); + } + else { + _each(exclude, function exclude (field) { + projection[field] = 0; + }); + + _each(exclude, function exclude (field) { + if (systemFields.indexOf(field) >= 0) { + delete projection[field]; + } + }); + } + + return projection; + }; + + + /** + * Cut off unwanted fields from given document + * @param {Object} doc + */ + self.applyProjection = function applyProjection (doc) { + + if (specific) { + for(const field in doc) { + if (specific.indexOf(field) === -1) { + delete doc[field]; + } + } + } + else { + _each(exclude, function include (field) { + if (typeof(doc[field]) !== 'undefined') { + delete doc[field]; + } + }); + } + }; +} + +module.exports = FieldsProjector; \ No newline at end of file diff --git a/lib/api3/shared/operationTools.js b/lib/api3/shared/operationTools.js new file mode 100644 index 00000000000..1955b9c2068 --- /dev/null +++ b/lib/api3/shared/operationTools.js @@ -0,0 +1,111 @@ +'use strict'; + +const apiConst = require('../const.json') + , stringTools = require('./stringTools') + , uuidv5 = require('uuid/v5') + , uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-) + ; + +function sendJSONStatus (res, status, title, description, warning) { + + const json = { + status: status, + message: title, + description: description + }; + + // Add optional warning message. + if (warning) { json.warning = warning; } + + res.status(status).json(json); + + return title; +} + + +/** + * Validate document's common fields + * @param {Object} doc + * @param {any} res + * @param {Object} options + * @returns {any} - string with error message if validation fails, true in case of success + */ +function validateCommon (doc, res, options) { + + const { isPatching } = options || {}; + + + if ((!isPatching || typeof(doc.date) !== 'undefined') + + && (typeof(doc.date) !== 'number' + || doc.date <= apiConst.MIN_TIMESTAMP) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_DATE); + } + + + if ((!isPatching || typeof(doc.utcOffset) !== 'undefined') + + && (typeof(doc.utcOffset) !== 'number' + || doc.utcOffset < apiConst.MIN_UTC_OFFSET + || doc.utcOffset > apiConst.MAX_UTC_OFFSET) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_UTC); + } + + + if ((!isPatching || typeof(doc.app) !== 'undefined') + + && (typeof(doc.app) !== 'string' + || stringTools.isNullOrWhitespace(doc.app)) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_APP); + } + + return true; +} + + +/** + * Calculate identifier for the document + * @param {Object} doc + * @returns string + */ +function calculateIdentifier (doc) { + if (!doc) + return undefined; + + let key = doc.device + '_' + doc.date; + if (doc.eventType) { + key += '_' + doc.eventType; + } + + return uuidv5(key, uuidNamespace); +} + + +/** + * Validate identifier in the document + * @param {Object} doc + */ +function resolveIdentifier (doc) { + + let identifier = calculateIdentifier(doc); + if (doc.identifier) { + if (doc.identifier !== identifier) { + console.warn(`APIv3: Identifier mismatch (expected: ${identifier}, received: ${doc.identifier})`); + console.log(doc); + } + } + else { + doc.identifier = identifier; + } +} + + +module.exports = { + sendJSONStatus, + validateCommon, + calculateIdentifier, + resolveIdentifier +}; \ No newline at end of file diff --git a/lib/api3/shared/storageTools.js b/lib/api3/shared/storageTools.js new file mode 100644 index 00000000000..b7d9dca6776 --- /dev/null +++ b/lib/api3/shared/storageTools.js @@ -0,0 +1,63 @@ +'use strict'; + +function getStorageVersion (app) { + + return new Promise(function (resolve, reject) { + + try { + const storage = app.get('entriesCollection').storage; + let storageVersion = app.get('storageVersion'); + + if (storageVersion) { + process.nextTick(() => { + resolve(storageVersion); + }); + } else { + storage.version() + .then(storageVersion => { + + app.set('storageVersion', storageVersion); + resolve(storageVersion); + }, reject); + } + } catch (error) { + reject(error); + } + }); +} + + +function getVersionInfo(app) { + + return new Promise(function (resolve, reject) { + + try { + const srvDate = new Date() + , info = { version: app.get('version') + , apiVersion: app.get('apiVersion') + , srvDate: srvDate.getTime() + }; + + getStorageVersion(app) + .then(storageVersion => { + + if (!storageVersion) + throw new Error('empty storageVersion'); + + info.storage = storageVersion; + + resolve(info); + + }, reject); + + } catch(error) { + reject(error); + } + }); +} + + +module.exports = { + getStorageVersion, + getVersionInfo +}; diff --git a/lib/api3/shared/stringTools.js b/lib/api3/shared/stringTools.js new file mode 100644 index 00000000000..b71a4b4f1a6 --- /dev/null +++ b/lib/api3/shared/stringTools.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Check the string for strictly valid number (no other characters present) + * @param {any} str + */ +function isNumberInString (str) { + return !isNaN(parseFloat(str)) && isFinite(str); +} + + +/** + * Check the string for non-whitespace characters presence + * @param {any} input + */ +function isNullOrWhitespace (input) { + + if (typeof input === 'undefined' || input == null) return true; + + return input.replace(/\s/g, '').length < 1; +} + + + +module.exports = { + isNumberInString, + isNullOrWhitespace +}; diff --git a/lib/api3/specific/lastModified.js b/lib/api3/specific/lastModified.js new file mode 100644 index 00000000000..b27ecaca852 --- /dev/null +++ b/lib/api3/specific/lastModified.js @@ -0,0 +1,101 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/lastModified', async function getLastModified (req, res) { + + async function getLastModified (col) { + + let result; + const lastModified = await col.storage.getLastModified('srvModified'); + + if (lastModified) { + result = lastModified.srvModified ? lastModified.srvModified : null; + } + + if (col.fallbackDateField) { + + const lastModified = await col.storage.getLastModified(col.fallbackDateField); + + if (lastModified && lastModified[col.fallbackDateField]) { + let timestamp = lastModified[col.fallbackDateField]; + if (typeof(timestamp) === 'string') { + timestamp = (new Date(timestamp)).getTime(); + } + + if (result === null || result < timestamp) { + result = timestamp; + } + } + } + + return { colName: col.colName, lastModified: result }; + } + + + async function collectionsAsync (auth) { + + const cols = app.get('collections') + , promises = [] + , output = {} + ; + + for (const colName in cols) { + const col = cols[colName]; + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + promises.push(getLastModified(col)); + } + } + + const results = await Promise.all(promises); + + for (const result of results) { + if (result.lastModified) + output[result.colName] = result.lastModified; + } + + return output; + } + + + async function operation (opCtx) { + + const { res, auth } = opCtx; + const srvDate = new Date(); + + let info = { + srvDate: srvDate.getTime(), + collections: { } + }; + + info.collections = await collectionsAsync(auth); + + res.json(info); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/status.js b/lib/api3/specific/status.js new file mode 100644 index 00000000000..7b70b24ab71 --- /dev/null +++ b/lib/api3/specific/status.js @@ -0,0 +1,71 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/status', async function getStatus (req, res) { + + function permsForCol (col, auth) { + let colPerms = ''; + + if (security.checkPermission(auth, 'api:' + col.colName + ':create')) { + colPerms += 'c'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + colPerms += 'r'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':update')) { + colPerms += 'u'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':delete')) { + colPerms += 'd'; + } + + return colPerms; + } + + + async function operation (opCtx) { + const cols = app.get('collections'); + + let info = await storageTools.getVersionInfo(app); + + info.apiPermissions = {}; + for (let col in cols) { + const colPerms = permsForCol(col, opCtx.auth); + if (colPerms) { + info.apiPermissions[col] = colPerms; + } + } + + res.json(info); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/version.js b/lib/api3/specific/version.js new file mode 100644 index 00000000000..25392fe99d7 --- /dev/null +++ b/lib/api3/specific/version.js @@ -0,0 +1,28 @@ +'use strict'; + +function configure (app) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , opTools = require('../shared/operationTools') + ; + + api.get('/version', async function getVersion (req, res) { + + try { + const versionInfo = await storageTools.getVersionInfo(app); + + res.json(versionInfo); + + } catch(error) { + console.error(error); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/storage/mongoCollection/find.js b/lib/api3/storage/mongoCollection/find.js new file mode 100644 index 00000000000..bc399dbce98 --- /dev/null +++ b/lib/api3/storage/mongoCollection/find.js @@ -0,0 +1,93 @@ +'use strict'; + +const utils = require('./utils') + , _ = require('lodash') + ; + + +/** + * Find single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {Object} projection + */ +function findOne (col, identifier, projection) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +/** + * Find single document by query filter + * @param {Object} col + * @param {Object} filter specific filter + * @param {Object} projection + */ +function findOneFilter (col, filter, projection) { + + return new Promise(function (resolve, reject) { + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +/** + * Find many documents matching the filtering criteria + */ +function findMany (col, filterDef, sort, limit, skip, projection, onlyValid, logicalOperator = 'and') { + + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(filterDef, logicalOperator, onlyValid); + + col.find(filter) + .sort(sort) + .limit(limit) + .skip(skip) + .project(projection) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +module.exports = { + findOne, + findOneFilter, + findMany +}; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/index.js b/lib/api3/storage/mongoCollection/index.js new file mode 100644 index 00000000000..e6ad0a6cf8b --- /dev/null +++ b/lib/api3/storage/mongoCollection/index.js @@ -0,0 +1,90 @@ +'use strict'; + +/** + * Storage implementation using mongoDB + * @param {Object} ctx + * @param {Object} env + * @param {string} colName - name of the collection in mongo database + */ +function MongoCollection (ctx, env, colName) { + + const self = this + , utils = require('./utils') + , find = require('./find') + , modify = require('./modify') + ; + + self.colName = colName; + + self.col = ctx.store.collection(colName); + + ctx.store.ensureIndexes(self.col, [ 'identifier', + 'srvModified', + 'isValid' + ]); + + + self.identifyingFilter = utils.identifyingFilter; + + self.findOne = (...args) => find.findOne(self.col, ...args); + + self.findOneFilter = (...args) => find.findOneFilter(self.col, ...args); + + self.findMany = (...args) => find.findMany(self.col, ...args); + + self.insertOne = (...args) => modify.insertOne(self.col, ...args); + + self.replaceOne = (...args) => modify.replaceOne(self.col, ...args); + + self.updateOne = (...args) => modify.updateOne(self.col, ...args); + + self.deleteOne = (...args) => modify.deleteOne(self.col, ...args); + + self.deleteManyOr = (...args) => modify.deleteManyOr(self.col, ...args); + + + /** + * Get server version + */ + self.version = function version () { + + return new Promise(function (resolve, reject) { + + ctx.store.db.admin().buildInfo({}, function mongoDone (err, result) { + + err + ? reject(err) + : resolve({ + storage: 'mongodb', + version: result.version + }); + }); + }); + }; + + + /** + * Get timestamp (e.g. srvModified) of the last modified document + */ + self.getLastModified = function getLastModified (fieldName) { + + return new Promise(function (resolve, reject) { + + self.col.find() + + .sort({ [fieldName]: -1 }) + + .limit(1) + + .project({ [fieldName]: 1 }) + + .toArray(function mongoDone (err, [ result ]) { + err + ? reject(err) + : resolve(result); + }); + }); + } +} + +module.exports = MongoCollection; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/modify.js b/lib/api3/storage/mongoCollection/modify.js new file mode 100644 index 00000000000..6552fe40e8c --- /dev/null +++ b/lib/api3/storage/mongoCollection/modify.js @@ -0,0 +1,123 @@ +'use strict'; + +const utils = require('./utils'); + + +/** + * Insert single document + * @param {Object} col + * @param {Object} doc + */ +function insertOne (col, doc) { + + return new Promise(function (resolve, reject) { + + col.insertOne(doc, function mongoDone(err, result) { + + if (err) { + reject(err); + } else { + const identifier = doc.identifier || result.insertedId.toString(); + delete doc._id; + resolve(identifier); + } + }); + }); +} + + +/** + * Replace single document + * @param {Object} col + * @param {string} identifier + * @param {Object} doc + */ +function replaceOne (col, identifier, doc) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.replaceOne(filter, doc, { }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve(result.matchedCount); + } + }); + }); +} + + +/** + * Update single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {object} setFields + */ +function updateOne (col, identifier, setFields) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.updateOne(filter, { $set: setFields }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ updated: result.result.nModified }); + } + }); + }); +} + + +/** + * Permanently remove single document by identifier + * @param {Object} col + * @param {string} identifier + */ +function deleteOne (col, identifier) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.deleteOne(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.result.n }); + } + }); + }); +} + + +/** + * Permanently remove many documents matching any of filtering criteria + */ +function deleteManyOr (col, filterDef) { + + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(filterDef, 'or'); + + col.deleteMany(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.deletedCount }); + } + }); + }); +} + + +module.exports = { + insertOne, + replaceOne, + updateOne, + deleteOne, + deleteManyOr +}; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/utils.js b/lib/api3/storage/mongoCollection/utils.js new file mode 100644 index 00000000000..1b2ab5610d7 --- /dev/null +++ b/lib/api3/storage/mongoCollection/utils.js @@ -0,0 +1,178 @@ +'use strict'; + +const _ = require('lodash') + , checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$") + , ObjectID = require('mongodb').ObjectID +; + + +/** + * Normalize document (make it mongoDB independent) + * @param {Object} doc - document loaded from mongoDB + */ +function normalizeDoc (doc) { + if (!doc.identifier) { + doc.identifier = doc._id.toString(); + } + + delete doc._id; +} + + +/** + * Parse filter definition array into mongoDB filtering object + * @param {any} filterDef + * @param {string} logicalOperator + * @param {bool} onlyValid + */ +function parseFilter (filterDef, logicalOperator, onlyValid) { + + let filter = { }; + if (!filterDef) + return filter; + + if (!_.isArray(filterDef)) { + return filterDef; + } + + let clauses = []; + + for (const itemDef of filterDef) { + let item; + + switch (itemDef.operator) { + case 'eq': + item = itemDef.value; + break; + + case 'ne': + item = { $ne: itemDef.value }; + break; + + case 'gt': + item = { $gt: itemDef.value }; + break; + + case 'gte': + item = { $gte: itemDef.value }; + break; + + case 'lt': + item = { $lt: itemDef.value }; + break; + + case 'lte': + item = { $lte: itemDef.value }; + break; + + case 'in': + item = { $in: itemDef.value.toString().split('|') }; + break; + + case 'nin': + item = { $nin: itemDef.value.toString().split('|') }; + break; + + case 're': + item = { $regex: itemDef.value.toString() }; + break; + + default: + throw new Error('Unsupported or missing filter operator ' + itemDef.operator); + } + + if (logicalOperator === 'or') { + let clause = { }; + clause[itemDef.field] = item; + clauses.push(clause); + } + else { + filter[itemDef.field] = item; + } + } + + if (logicalOperator === 'or') { + filter = { $or: clauses }; + } + + if (onlyValid) { + filter.isValid = { $ne: false }; + } + + return filter; +} + + +/** + * Create query filter for single document with identifier fallback + * @param {string} identifier + */ +function filterForOne (identifier) { + + const filterOpts = [ { identifier } ]; + + // fallback to "identifier = _id" + if (checkForHexRegExp.test(identifier)) { + filterOpts.push({ _id: ObjectID(identifier) }); + } + + return { $or: filterOpts }; +} + + +/** + * Create query filter to check whether the document already exists in the storage. + * This function resolves eventual fallback deduplication. + * @param {string} identifier - identifier of document to check its existence in the storage + * @param {Object} doc - document to check its existence in the storage + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @returns {Object} - query filter for mongo or null in case of no identifying possibility + */ +function identifyingFilter (identifier, doc, dedupFallbackFields) { + + const filterItems = []; + + if (identifier) { + // standard identifier field (APIv3) + filterItems.push({ identifier: identifier }); + + // fallback to "identifier = _id" (APIv1) + if (checkForHexRegExp.test(identifier)) { + filterItems.push({ identifier: { $exists: false }, _id: ObjectID(identifier) }); + } + } + + // let's deal with eventual fallback deduplication + if (!_.isEmpty(doc) && _.isArray(dedupFallbackFields) && dedupFallbackFields.length > 0) { + let dedupFilterItems = []; + + _.each(dedupFallbackFields, function addDedupField (field) { + + if (doc[field] !== undefined) { + + let dedupFilterItem = { }; + dedupFilterItem[field] = doc[field]; + dedupFilterItems.push(dedupFilterItem); + } + }); + + if (dedupFilterItems.length === dedupFallbackFields.length) { // all dedup fields are present + + dedupFilterItems.push({ identifier: { $exists: false } }); // force not existing identifier for fallback deduplication + filterItems.push({ $and: dedupFilterItems }); + } + } + + if (filterItems.length > 0) + return { $or: filterItems }; + else + return null; // we don't have any filtering rule to identify the document in the storage +} + + +module.exports = { + normalizeDoc, + parseFilter, + filterForOne, + identifyingFilter +}; \ No newline at end of file diff --git a/lib/api3/storageSocket.js b/lib/api3/storageSocket.js new file mode 100644 index 00000000000..e8c08310d2b --- /dev/null +++ b/lib/api3/storageSocket.js @@ -0,0 +1,145 @@ +'use strict'; + +const apiConst = require('./const'); + +/** + * Socket.IO broadcaster of any storage change + */ +function StorageSocket (app, env, ctx) { + + const self = this; + + const LOG_GREEN = '\x1B[32m' + , LOG_MAGENTA = '\x1B[35m' + , LOG_RESET = '\x1B[0m' + , LOG = LOG_GREEN + 'STORAGE SOCKET: ' + LOG_RESET + , LOG_ERROR = LOG_MAGENTA + 'STORAGE SOCKET: ' + LOG_RESET + , NAMESPACE = '/storage' + ; + + + /** + * 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 = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; + 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('storage-socket-create', self.emitCreate); + ctx.bus.on('storage-socket-update', self.emitUpdate); + ctx.bus.on('storage-socket-delete', self.emitDelete); + }; + + + /** + * Authorize Socket.IO client and subscribe him to authorized rooms + * @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'; + + if (message && message.accessToken) { + return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinish (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 { + return self.subscribeAuthorized(socket, message, auth, returnCallback); + } + }); + } + + console.log(`${LOG_ERROR} Authorization failed for message:`, message); + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN}); + } + }; + + + /** + * Subscribe already authorized Socket.IO client to his rooms + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Object} auth authorization of the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribeAuthorized = function subscribeAuthorized (socket, message, auth, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + const enabledCols = app.get('enabledCollections'); + const cols = Array.isArray(message.collections) ? message.collections : enabledCols; + const subscribed = []; + + for (const col of cols) { + if (enabledCols.includes(col)) { + const permission = (col === 'settings') ? `api:${col}:admin` : `api:${col}:read`; + + if (ctx.authorization.checkMultiple(permission, auth.shiros)) { + socket.join(col); + subscribed.push(col); + } + } + } + + const doc = subscribed.length > 0 + ? { success: true, collections: subscribed } + : { success: false, message: apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY }; + if (shouldCallBack) { + returnCallback(doc); + } + return doc; + }; + + + /** + * Emit create event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitCreate = function emitCreate (event) { + self.namespace.to(event.colName) + .emit('create', event); + }; + + + /** + * Emit update event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitUpdate = function emitUpdate (event) { + self.namespace.to(event.colName) + .emit('update', event); + }; + + + /** + * Emit delete event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitDelete = function emitDelete (event) { + self.namespace.to(event.colName) + .emit('delete', event); + } +} + +module.exports = StorageSocket; \ No newline at end of file diff --git a/lib/api3/swagger.js b/lib/api3/swagger.js new file mode 100644 index 00000000000..2d434e97f53 --- /dev/null +++ b/lib/api3/swagger.js @@ -0,0 +1,41 @@ +'use strict'; + +const express = require('express') + , fs = require('fs') + ; + + +function setupSwaggerUI (app) { + + const serveSwaggerDef = function serveSwaggerDef (req, res) { + res.sendFile(__dirname + '/swagger.yaml'); + }; + app.get('/swagger.yaml', serveSwaggerDef); + + const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath(); + const swaggerFiles = express.static(swaggerUiAssetPath); + + const urlRegex = /url: "[^"]*",/; + + const patchIndex = function patchIndex (req, res) { + const indexContent = fs.readFileSync(`${swaggerUiAssetPath}/index.html`) + .toString() + .replace(urlRegex, 'url: "../swagger.yaml",'); + res.send(indexContent); + }; + + app.get('/swagger-ui-dist', function getSwaggerRoot (req, res) { + let targetUrl = req.originalUrl; + if (!targetUrl.endsWith('/')) { + targetUrl += '/'; + } + targetUrl += 'index.html'; + res.redirect(targetUrl); + }); + app.get('/swagger-ui-dist/index.html', patchIndex); + + app.use('/swagger-ui-dist', swaggerFiles); +} + + +module.exports = setupSwaggerUI; \ No newline at end of file diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml new file mode 100644 index 00000000000..32ac1815aee --- /dev/null +++ b/lib/api3/swagger.yaml @@ -0,0 +1,1592 @@ +openapi: 3.0.0 +servers: + - url: '/api/v3' +info: + version: '3.0.0' + title: Nightscout API + contact: + name: NS development discussion channel + url: https://gitter.im/nightscout/public + license: + name: AGPL 3 + url: 'https://www.gnu.org/licenses/agpl.txt' + description: + Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + + + API v3 uses these environment variables, among other things: + + - Security switch (optional, default = `true`) +

API3_SECURITY_ENABLE=true
+ You can turn the whole security mechanism off, e.g. for debugging or development purposes, + but this should never be set to false in production. + + + - Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5) +
API3_TIME_SKEW_TOLERANCE=5
+ This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header. + + + - Maximum limit count of documents retrieved from single query +
API3_MAX_LIMIT=1000
+ + + - Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60) +
API3_AUTOPRUNE_DEVICESTATUS=60
+
+      API3_AUTOPRUNE_ENTRIES=365
+
+      API3_AUTOPRUNE_TREATMENTS=120
+      
+ You can specify for which collections autopruning will be activated and length of retention period in days, e.g. "Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents." + + + - Fallback deduplication switch (optional, default = true) +
API3_DEDUP_FALLBACK_ENABLED=true
+ API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database. + + Documents not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. + When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets: +
`devicestatus` collection: equal combination of `created_at` and `device`
+
+      `entries` collection:      equal combination of `date` and `type`
+
+      `food` collection:         equal `created_at`
+
+      `profile` collection:      equal `created_at`
+
+      `treatments` collection:   equal combination of `created_at` and `eventType`
+      
+ + + - Fallback switch for adding `created_at` field along the `date` field (optional, default = true) +
API3_CREATED_AT_FALLBACK_ENABLED=true
+ Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working. + +tags: + - name: generic + description: Generic operations with each database collection (devicestatus, entries, food, profile, settings, treatments) + - name: other + description: All other various operations + + +paths: + /{collection}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + ###################################################################################### + get: + tags: + - generic + summary: 'SEARCH: Search documents from the collection' + operationId: SEARCH + description: General search operation through documents of one collection, matching the specified filtering criteria. You can apply: + + + 1) filtering - combining any number of filtering parameters + + + 2) ordering - using `sort` or `sort$desc` parameter + + + 3) paging - using `limit` and `skip` parameters + + + When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content. + + + This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`). + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/filterParams' + - $ref: '#/components/parameters/sortParam' + - $ref: '#/components/parameters/sortDescParam' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/skipParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/search200' + 204: + $ref: '#/components/responses/search204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + + + ###################################################################################### + post: + tags: + - generic + summary: 'CREATE: Inserts a new document into the collection' + description: + Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header. + + + When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`) + + requestBody: + description: JSON with new document to insert + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 201: + $ref: '#/components/responses/201CreatedLocation' + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + + + #return HTTP STATUS 400 for all other verbs (PUT, PATCH, DELETE,...) + + + /{collection}/{identifier}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + - in: path + name: identifier + description: Identifier of the document to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramIdentifier' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + ###################################################################################### + get: + tags: + - generic + summary: 'READ: Retrieves a single document from the collection' + description: + Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side. + With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned. + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + parameters: + - $ref: '#/components/parameters/ifModifiedSinceHeader' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/read200' + 304: + $ref: '#/components/responses/304NotModified' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 410: + $ref: '#/components/responses/410Gone' + + + ###################################################################################### + put: + tags: + - generic + summary: 'UPDATE: Updates a document in the collection' + description: + Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter). + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 201: + $ref: '#/components/responses/201Created' + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 410: + $ref: '#/components/responses/410Gone' + + + ###################################################################################### + patch: + tags: + - generic + summary: 'PATCH: Partially updates document in the collection' + description: + Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code. + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + `PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence. + + + While patching the document, the field `modifiedBy` is automatically set to the authorized subject's name. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 410: + $ref: '#/components/responses/410Gone' + + + ###################################################################################### + delete: + tags: + - generic + summary: 'DELETE: Deletes a document from the collection' + description: + If the document has already been deleted, the operation will succeed anyway. Normally, documents are not really deleted from the collection but they are only marked as deleted. For special cases the deletion can be irreversible using `permanent` parameter. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `delete` permission for the API and the collection (e.g. `api:treatments:delete`) + + + parameters: + - $ref: '#/components/parameters/permanentParam' + + security: + - apiKeyAuth: [] + + responses: + 204: + description: Successful operation - empty response + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + + + ###################################################################################### + /{collection}/history: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY + description: + HISTORY operation is intended for continuous data synchronization with other systems. + + Every insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter. + + + Deleted documents will appear with `isValid` = `false` field. + + + When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content. + + + HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections). + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/lastModifiedRequiredHeader' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 204: + $ref: '#/components/responses/history204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + + + ###################################################################################### + /{collection}/history/{lastModified}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - in: path + name: lastModified + description: Starting timestamp (in UNIX epoch format, defined with respect to server's clock) since which the changes in documents are to be listed. Query for modified documents is made using "greater than" operator (not including equal timestamps). + required: true + schema: + type: integer + format: int64 + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY2 + description: + This HISTORY operation variant is more precise than the previous one with `Last-Modified` request HTTP header), because it does not loose milliseconds precision. + + + Since this variant queries for changed documents by timestamp precisely and exclusively, the last modified document does not repeat itself in following calls. That is the reason why is this variant more suitable for continuous synchronization with other systems. + + + This variant behaves quite the same as the previous one in all other aspects. + + + parameters: + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 204: + $ref: '#/components/responses/history204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + + + ###################################################################################### + /version: + + get: + tags: + - other + summary: 'VERSION: Returns actual version information' + description: No authentication is needed for this commnad (it is public) + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + + + ###################################################################################### + /status: + + get: + tags: + - other + summary: 'STATUS: Returns actual version information and all permissions granted for API' + description: + This operation requires authorization in contrast with VERSION operation. + + security: + - apiKeyAuth: [] + + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + + ###################################################################################### + /lastModified: + parameters: + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - other + summary: 'LAST MODIFIED: Retrieves timestamp of the last modification of every collection' + operationId: LAST-MODIFIED + description: + LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion). + + Not only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API). + + + This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to. + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/lastModified200' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + +###################################################################################### +components: + + parameters: + + dateHeader: + in: header + name: Date + schema: + type: string + required: false + description: + Timestamp (defined by client's clock) when the HTTP request was constructed on client. + This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. + This can be set alternatively in `now` query parameter. + + Example: + + +
Date: Wed, 17 Oct 2018 05:13:00 GMT
+ + + nowParam: + in: query + name: now + schema: + type: integer + format: int64 + required: false + description: + Timestamp (defined by client's clock) when the HTTP request was constructed on client. + This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. + This can be set alternatively in `Date` header. + + + Example: + + +
now=1525383610088
+ + + tokenParam: + in: query + name: token + schema: + type: string + required: false + description: + An alternative way of authorization - passing accessToken in a query parameter. + + + Example: + + +
token=testadmin-bf2591231bd2c042
+ + + limitParam: + in: query + name: limit + schema: + type: integer + minimum: 1 + default: stored in API3_MAX_LIMIT environment variable (usually 1000) + example: 100 + description: Maximum number of documents to get in result array + + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + description: + Number of documents to skip from collection query before + loading them into result array (used for pagination) + + sortParam: + in: query + name: sort + schema: + type: string + required: false + description: + Field name by which the sorting of documents is performed. This parameter cannot be combined with `sort$desc` parameter. + + sortDescParam: + in: query + name: sort$desc + schema: + type: string + required: false + description: + Field name by which the descending (reverse) sorting of documents is performed. This parameter cannot be combined with `sort` parameter. + + permanentParam: + in: query + name: permanent + schema: + type: boolean + required: false + description: + If true, the deletion will be irreversible and it will not appear in `HISTORY` operation. Normally there is no reason for setting this flag. + + + fieldsParam: + in: query + name: fields + schema: + type: string + default: '_all' + required: false + examples: + all: + value: '_all' + summary: All fields will be returned (default behaviour) + customSet: + value: 'date,insulin' + summary: Only fields date and insulin will be returned + description: A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values: + + + _all: All fields will be returned (default value) + + + date,insulin: Only fields `date` and `insulin` will be returned + + + filterParams: + in: query + name: filter_parameters + schema: + type: string + description: + Any number of filtering operators. + + + Each filtering operator has name like `$`, e.g. `carbs$gt=2` which represents filtering rule "The field carbs must be present and greater than 2". + + + You can choose from operators: + + + `eq`=equals, `insulin$eq=1.5` + + + `ne`=not equals, `insulin$ne=1.5` + + + `gt`=greater than, `carbs$gt=30` + + + `gte`=greater than or equal, `carbs$gte=30` + + + `lt`=less than, `carbs$lt=30` + + + `lte`=less than or equal, `carbs$lte=30` + + + `in`=in specified set, `type$in=sgv|mbg|cal` + + + `nin`=not in specified set, `eventType$nin=Temp%20Basal|Temporary%20Target` + + + `re`=regex pattern, `eventType$re=Temp.%2A` + + + When filtering by field `date`, `created_at`, `srvModified` or `srvCreated`, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always queried in a normalized form - UTC with zero offset and with the correct format (1525383610088 for `date`, '2018-05-03T21:40:10.088Z' for `created_at`). + + lastModifiedRequiredHeader: + in: header + name: Last-Modified + schema: + type: string + required: true + description: + Starting timestamp (defined with respect to server's clock) since which the changes in documents are to be listed, formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + Example: + + +
Last-Modified: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifModifiedSinceHeader: + in: header + name: If-Modified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Modified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifUnmodifiedSinceHeader: + in: header + name: If-Unmodified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ###################################################################################### + responses: + + 201Created: + description: Successfully created a new document in collection + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 201CreatedLocation: + description: Successfully created a new document in collection + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + 'Location': + $ref: '#/components/schemas/headerLocation' + + 204NoContent: + description: Successfully finished operation + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 204NoContentLocation: + description: Successfully finished operation + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + 'Location': + $ref: '#/components/schemas/headerLocation' + + 304NotModified: + description: The document has not been modified on the server since timestamp specified in If-Modified-Since header + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 400BadRequest: + description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present. + + 401Unauthorized: + description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy. + + 403Forbidden: + description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation. + + 404NotFound: + description: The collection or document specified was not found. + + 412PreconditionFailed: + description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header). + + 410Gone: + description: The requested document has already been deleted. + + search200: + description: Successful operation returning array of documents matching the filtering criteria + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentArray' + + search204: + description: Successful operation - no documents matching the filtering criteria + + read200: + description: The document has been succesfully found and its JSON form returned in the response content. + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + history200: + description: + Changed documents since specified timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentArray' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModifiedMaximum' + 'ETag': + $ref: '#/components/schemas/headerEtagLastModifiedMaximum' + + history204: + description: No changes detected + + lastModified200: + description: Successful operation returning the timestamps + content: + application/json: + schema: + $ref: '#/components/schemas/LastModifiedResult' + + ###################################################################################### + schemas: + + headerLocation: + type: string + description: + Location of document - the relative part of URL. This can be used to parse the identifier + of just created document. + + Example=/api/v3/treatments/53409478-105f-11e9-ab14-d663bd873d93 + + headerLastModified: + type: string + description: + Timestamp of the last document modification on the server, formatted as + + ', :: GMT'. + + This field is relevant only for documents which were somehow modified by API v3 + (inserted, updated or deleted) and it was generated using server's clock. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents, formatted as + + ', :: GMT'. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerEtagLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents. + This header does not loose milliseconds from the date (unlike the `Last-Modified` header). + + Example='W/"1525383610088"' + + paramCollection: + type: string + enum: + - devicestatus + - entries + - food + - profile + - settings + - treatments + example: 'treatments' + + paramIdentifier: + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + + DocumentBase: + description: Shared base for all documents + properties: + identifier: + description: + Main addressing, required field that identifies document in the collection. + + + The client should not create the identifier, the server automatically assigns it when the document is inserted. + + + The server calculates the identifier in such a way that duplicate records are automatically merged (deduplicating is made by `date`, `device` and `eventType` fields). + + + The best practise for all applications is not to loose identifiers from received documents, but save them carefully for other consumer applications/systems. + + + API v3 has a fallback mechanism in place, for documents without `identifier` field the `identifier` is set to internal `_id`, when reading or addressing these documents. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + date: + type: integer + format: int64 + description: + Required timestamp when the record or event occured, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always stored in a normalized form - UTC with zero offset. If UTC offset was present, it is going to be set in the `utcOffset` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + utcOffset: + type: integer + description: + Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the `date` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 120 + + app: + type: string + description: + Application or system in which the record was entered by human or device for the first time. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: xdrip + + device: + type: string + description: + The device from which the data originated (including serial number of the device, if it is relevant and safe). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'dexcom G5' + + _id: + description: Internally assigned database id. This field is for internal server purposes only, clients communicate with API by using identifier field. + type: string + example: '58e9dfbc166d88cc18683aac' + + srvCreated: + type: integer + format: int64 + description: + The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + subject: + type: string + description: + Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'uploader' + + srvModified: + type: integer + format: int64 + description: + The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + modifiedBy: + type: string + description: + Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: admin + + isValid: + type: boolean + description: + A flag set by the server only for deleted documents. This field appears + only within history operation and for documents which were deleted by API v3 (and they always have a false value) + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: false + + + required: + - date + - app + + + DeviceStatus: + description: State of physical device, which is a technical part of the whole T1D compensation system + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Entry: + description: Blood glucose measurements and CGM calibrations + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + type: + type: string + description: 'sgv, mbg, cal, etc' + + sgv: + type: number + description: The glucose reading. (only available for sgv types) + + direction: + type: string + description: Direction of glucose trend reported by CGM. (only available for sgv types) + example: '"DoubleDown", "SingleDown", "FortyFiveDown", "Flat", "FortyFiveUp", "SingleUp", "DoubleUp", "NOT COMPUTABLE", "RATE OUT OF RANGE" for xdrip' + + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + example: 'xdrip: 0, 1, 2=high, 3=high_for_predict, 4=very high, 5=extreme' + + filtered: + type: number + description: The raw filtered value directly from CGM transmitter. (only available for sgv types) + + unfiltered: + type: number + description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + + rssi: + type: number + description: The signal strength from CGM transmitter. (only available for sgv types) + + units: + type: string + example: '"mg", "mmol"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + + + Food: + description: Nutritional values of food + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + food: + type: string + description: 'food, quickpick' + + category: + type: string + description: Name for a group of related records + + subcategory: + type: string + description: Name for a second level of groupping + + name: + type: string + description: Name of the food described + + portion: + type: number + description: Number of units (e.g. grams) of the whole portion described + + unit: + type: string + example: '"g", "ml", "oz"' + description: Unit for the portion + + carbs: + type: number + description: Amount of carbs in the portion in grams + + fat: + type: number + description: Amount of fat in the portion in grams + + protein: + type: number + description: Amount of proteins in the portion in grams + + energy: + type: number + description: Amount of energy in the portion in kJ + + gi: + type: number + description: 'Glycemic index (1=low, 2=medium, 3=high)' + + hideafteruse: + type: boolean + description: Flag used for quickpick + + hidden: + type: boolean + description: Flag used for quickpick + + position: + type: number + description: Ordering field for quickpick + + portions: + type: number + description: component multiplier if defined inside quickpick compound + + foods: + type: array + description: Neighbour documents (from food collection) that together make a quickpick compound + items: + $ref: '#/components/schemas/Food' + + + Profile: + description: Parameters describing body functioning relative to T1D + compensation parameters + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Settings: + description: + A document representing persisted settings of some application or system (it could by Nightscout itself as well). This pack of options serves as a backup or as a shared centralized storage for multiple client instances. It is a probably good idea to `PATCH` the document instead of `UPDATE` operation, e.g. when changing one settings option in a client application. + + + `identifier` represents a client application name here, e.g. `xdrip` or `aaps`. + + + `Settings` collection has a more specific authorization required. For the `SEARCH` operation within this collection, you need an `admin` permission, such as `api:settings:admin`. The goal is to isolate individual client application settings. + + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Treatment: + description: T1D compensation action + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + eventType: + type: string + example: '"BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard"' + description: The type of treatment event. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + # created_at: + # type: string + # description: The date of the event, might be set retroactively. + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + example: '"Sensor", "Finger", "Manual"' + description: Method used to obtain glucose, Finger or Sensor. + units: + type: string + example: '"mg/dl", "mmol/l"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field when `glucose` is entered. + carbs: + type: number + description: Amount of carbs given. + protein: + type: number + description: Amount of protein given. + fat: + type: number + description: Amount of fat given. + insulin: + type: number + description: Amount of insulin, if any. + duration: + type: number + description: Duration in minutes. + preBolus: + type: number + description: How many minutes the bolus was given before the meal started. + splitNow: + type: number + description: Immediate part of combo bolus (in percent). + splitExt: + type: number + description: Extended part of combo bolus (in percent). + percent: + type: number + description: Eventual basal change in percent. + absolute: + type: number + description: Eventual basal change in absolute value (insulin units per hour). + targetTop: + type: number + description: Top limit of temporary target. + targetBottom: + type: number + description: Bottom limit of temporary target. + profile: + type: string + description: Name of the profile to which the pump has been switched. + reason: + type: string + description: For example the reason why the profile has been switched or why the temporary target has been set. + notes: + type: string + description: Description/notes of treatment. + enteredBy: + type: string + description: Who entered the treatment. + + + DocumentToPost: + description: Single document + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'app': 'xdrip' + 'subject': 'uploader' + + + Document: + description: Single document + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'srvCreated': 1532936218000 + 'srvModified': 1532936218000 + 'app': 'xdrip' + 'subject': 'uploader' + 'modifiedBy': 'admin' + + + DeviceStatusArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/DeviceStatus' + + + EntryArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Entry' + + + FoodArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Food' + + + ProfileArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Profile' + + + SettingsArray: + description: Array of settings + type: array + items: + $ref: '#/components/schemas/Settings' + + + TreatmentArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Treatment' + + + DocumentArray: + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatusArray' + - $ref: '#/components/schemas/EntryArray' + - $ref: '#/components/schemas/FoodArray' + - $ref: '#/components/schemas/ProfileArray' + - $ref: '#/components/schemas/SettingsArray' + - $ref: '#/components/schemas/TreatmentArray' + + + Version: + description: Information about versions + type: object + properties: + + version: + description: The whole Nightscout instance version + type: string + example: '0.10.2-release-20171201' + + apiVersion: + description: API v3 subsystem version + type: string + example: '3.0.0' + + srvDate: + description: Actual server date and time in UNIX epoch format + type: number + example: 1532936118000 + + storage: + type: object + properties: + + type: + description: Type of storage engine used + type: string + example: 'mongodb' + + version: + description: Version of the storage engine + type: string + example: '4.0.6' + + + Status: + description: Information about versions and API permissions + allOf: + - $ref: '#/components/schemas/Version' + - type: object + properties: + + apiPermissions: + type: object + properties: + devicestatus: + type: string + example: 'crud' + entries: + type: string + example: 'r' + food: + type: string + example: 'crud' + profile: + type: string + example: 'r' + treatments: + type: string + example: 'crud' + + + LastModifiedResult: + description: Result of LAST MODIFIED operation + properties: + srvDate: + description: + Actual storage server date (Unix epoch in ms). + type: integer + format: int64 + example: 1556260878776 + + collections: + type: object + description: + Collections which the user have read access to. + properties: + devicestatus: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260760974 + treatments: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1553374184169 + entries: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260758768 + profile: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1548524042744 + + ###################################################################################### + securitySchemes: + + accessToken: + type: apiKey + name: token + in: query + description: >- + Add token as query item in the URL or as HTTP header. You can manage access token in + `/admin`. + + Each operation requires a specific permission that has to be granted (via security role) to the security subject, which was authenticated by `token` parameter/header or `JWT`. E.g. for creating new `devicestatus` document via API you need `api:devicestatus:create` permission. + + jwtoken: + type: http + scheme: bearer + description: Use this if you know the temporary json webtoken. + bearerFormat: JWT \ No newline at end of file diff --git a/lib/authorization/index.js b/lib/authorization/index.js index 0c0c6f5f2a7..388fa19dffc 100644 --- a/lib/authorization/index.js +++ b/lib/authorization/index.js @@ -55,6 +55,8 @@ function init (env, ctx) { return token; } + authorization.extractToken = extractToken; + function authorizeAccessToken (req) { var accessToken = req.query.token; @@ -152,9 +154,7 @@ function init (env, ctx) { if (err) { return callback(err, { shiros: [ ] }); } else { - var resolved = storage.resolveSubjectAndPermissions(verified.accessToken); - var shiros = resolved.shiros.concat(defaultShiros); - return callback(null, { shiros: shiros, subject: resolved.subject }); + authorization.resolveAccessToken (verified.accessToken, callback, defaultShiros); } }); } else { @@ -163,6 +163,21 @@ function init (env, ctx) { }; + authorization.resolveAccessToken = function resolveAccessToken (accessToken, callback, defaultShiros) { + + if (!defaultShiros) { + defaultShiros = storage.rolesToShiros(defaultRoles); + } + + let resolved = storage.resolveSubjectAndPermissions(accessToken); + if (!resolved || !resolved.subject) { + return callback('Subject not found', null); + } + + let shiros = resolved.shiros.concat(defaultShiros); + return callback(null, { shiros: shiros, subject: resolved.subject }); + }; + authorization.isPermitted = function isPermitted (permission, opts) { diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 9e53c4f77ac..84b6fd9c304 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -115,7 +115,7 @@ function boot (env, language) { } else { //TODO assume mongo for now, when there are more storage options add a lookup require('../storage/mongo-storage')(env, function ready(err, store) { - // FIXME, error is always null, if there is an error, the storage.js will throw an exception + // FIXME, error is always null, if there is an error, the index.js will throw an exception console.log('Mongo Storage system ready'); ctx.store = store; diff --git a/lib/server/websocket.js b/lib/server/websocket.js index fb0aa38b349..3946c129a5d 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -520,6 +520,10 @@ function init (env, ctx, server) { start( ); listeners( ); + if (ctx.storageSocket) { + ctx.storageSocket.init(io); + } + return websocket(); } diff --git a/package.json b/package.json index 121ff4d27c9..4a970a8c135 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "swagger-ui-express": "^4.1.2", "terser": "^3.17.0", "traverse": "^0.6.6", + "uuid": "^3.3.2", "webpack": "^4.39.2", "webpack-cli": "^3.3.7" }, diff --git a/tests/api.devicestatus.test.js b/tests/api.devicestatus.test.js index a618db49056..34a0908610e 100644 --- a/tests/api.devicestatus.test.js +++ b/tests/api.devicestatus.test.js @@ -56,6 +56,7 @@ describe('Devicestatus API', function ( ) { request(self.app) .get('/api/devicestatus/') .query('find[created_at][$gte]=2018-12-16') + .query('find[created_at][$lte]=2018-12-17') .set('api-secret', self.env.api_secret || '') .expect(200) .expect(function (response) { diff --git a/tests/api3.basic.test.js b/tests/api3.basic.test.js new file mode 100644 index 00000000000..8e51b343585 --- /dev/null +++ b/tests/api3.basic.test.js @@ -0,0 +1,49 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Basic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.server.close(); + }); + + + it('GET /swagger', async () => { + let res = await request(self.app) + .get('/api/v3/swagger.yaml') + .expect(200); + + res.header['content-length'].should.be.above(0); + }); + + + it('GET /version', async () => { + let res = await request(self.app) + .get('/api/v3/version') + .expect(200); + + const apiConst = require('../lib/api3/const.json') + , software = require('../package.json'); + + res.body.version.should.equal(software.version); + res.body.apiVersion.should.equal(apiConst.API3_VERSION); + res.body.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050); + }); + +}); + diff --git a/tests/api3.create.test.js b/tests/api3.create.test.js new file mode 100644 index 00000000000..cbd17a3e826 --- /dev/null +++ b/tests/api3.create.test.js @@ -0,0 +1,487 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 CREATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE, + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(20000); + + + /** + * Cleanup after successful creation + */ + self.delete = async function deletePermanent (identifier) { + await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + }; + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + /** + * Get document detail for futher processing + */ + self.search = async function search (date) { + let res = await self.instance.get(`${self.url}?date$eq=${date}&token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.create}`; + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.post(`${self.url}`) + .send(self.validDoc) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.post(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should require create permission', async () => { + let res = await self.instance.post(`${self.url}?token=${self.token.read}`) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should reject empty body', async () => { + await self.instance.post(self.urlToken) + .send({ }) + .expect(400); + }); + + + it('should accept valid document', async () => { + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${self.validDoc.identifier}`); + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiCreate.name); + + await self.delete(self.validDoc.identifier); + }); + + + it('should reject missing date', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.date; + + let res = await self.instance.post(self.urlToken) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date ABC', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date -1', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: -1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + + it('should reject invalid date 1 (too old)', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: 1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date - illegal format', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: '2019-20-60T50:90:90' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid utcOffset -5000', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: -5000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject invalid utcOffset ABC', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should accept valid utcOffset', async () => { + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: 120 })) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.utcOffset.should.equal(120); + await self.delete(self.validDoc.identifier); + }); + + + it('should reject invalid utcOffset null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject missing app', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.app; + + let res = await self.instance.post(self.urlToken) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject invalid app null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject empty app', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: '' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should normalize date and store utcOffset', async () => { + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: '2019-06-10T08:07:08,576+02:00' })) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.date.should.equal(1560146828576); + body.utcOffset.should.equal(120); + await self.delete(self.validDoc.identifier); + }); + + + it('should require update permission for deduplication', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.urlToken) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc); + let res = await self.instance.post(self.urlToken) + .send(doc2) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:update'); + await self.delete(doc.identifier); + }); + + + it('should deduplicate document by identifier', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.urlToken) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc, { + insulin: 0.5 + }); + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(204); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + + await self.delete(doc2.identifier); + }); + + + it('should deduplicate document by created_at+eventType', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + + const doc2 = Object.assign({}, doc, { + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(204); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + + await self.delete(doc2.identifier); + }); + }); + + + it('should not deduplicate treatment only by created_at', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + + let oldBody = await self.get(doc._id); + delete doc._id; // APIv1 updates input document, we must get rid of _id for the next round + oldBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc, { + eventType: 'Meal Bolus', + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(201); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + updatedBody.identifier.should.not.equal(oldBody.identifier); + + await self.delete(doc2.identifier); + await self.delete(oldBody.identifier); + }); + }); + + + it('should overwrite deleted document', async () => { + const date1 = new Date() + , identifier = utils.randomString('32', 'aA#'); + + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier, date: date1.toISOString() })) + .expect(201); + + await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + .expect(204); + + const date2 = new Date(); + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) + .expect(403); + + res.body.status.should.be.equal(403); + res.body.message.should.be.equal('Missing permission api:treatments:update'); + + res = await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(identifier); + body.date.should.equal(date2.getTime()); + body.identifier.should.equal(identifier); + await self.delete(identifier); + }); + + + it('should calculate the identifier', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + await self.delete(validIdentifier); + }); + + + it('should deduplicate by identifier calculation', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + + delete self.validDoc.identifier; + res = await self.instance.post(`${self.url}?token=${self.token.update}`) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + body = await self.search(self.validDoc.date); + body.length.should.equal(1); + + await self.delete(validIdentifier); + }); + +}); + diff --git a/tests/api3.delete.test.js b/tests/api3.delete.test.js new file mode 100644 index 00000000000..203d32edce8 --- /dev/null +++ b/tests/api3.delete.test.js @@ -0,0 +1,53 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.delete}`; + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.delete(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.delete(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + +}); + diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js new file mode 100644 index 00000000000..36c94a00f74 --- /dev/null +++ b/tests/api3.generic.workflow.test.js @@ -0,0 +1,257 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('Generic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , utils = require('./fixtures/api3/utils') + ; + + utils.randomString('32', 'aA#'); // let's have a brand new identifier for your testing document + self.urlLastModified = '/api/v3/lastModified'; + self.historyTimestamp = 0; + + self.docOriginal = { + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + }; + self.identifier = opTools.calculateIdentifier(self.docOriginal); + self.docOriginal.identifier = self.identifier; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.urlCol = '/api/v3/treatments'; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.create}`; + }); + + + after(() => { + self.instance.server.close(); + }); + + + self.checkHistoryExistence = async function checkHistoryExistence (assertions) { + + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`) + .expect(200); + + res.body.length.should.be.above(0); + res.body.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + value.srvModified.should.be.above(self.historyTimestamp); + + if (typeof(assertions) === 'function') { + assertions(value); + } + + self.historyTimestamp = value.srvModified; + }); + }; + + + it('LAST MODIFIED to get actual server timestamp', async () => { + let res = await self.instance.get(`${self.urlLastModified}?token=${self.token.read}`) + .expect(200); + + self.historyTimestamp = res.body.collections.treatments; + if (!self.historyTimestamp) { + self.historyTimestamp = res.body.srvDate - (10 * 60 * 1000); + } + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('STATUS to get actual server timestamp', async () => { + let res = await self.instance.get(`/api/v3/status?token=${self.token.read}`) + .expect(200); + + self.historyTimestamp = res.body.srvDate; + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('READ of not existing document is not found', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(404); + }); + + + it('SEARCH of not existing document (not found)', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.should.have.length(0); + }); + + + it('DELETE of not existing document is not found', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(404); + }); + + + it('CREATE new document', async () => { + await self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(self.docOriginal) + .expect(201); + }); + + + it('READ existing document', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(self.docOriginal); + self.docActual = res.body; + + if (self.historyTimestamp >= self.docActual.srvModified) { + self.historyTimestamp = self.docActual.srvModified - 1; + } + }); + + + it('SEARCH existing document (found)', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier$eq': self.identifier }) + .expect(200); + + res.body.length.should.be.above(0); + res.body.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + }); + }); + + + it('new document in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('UPDATE document', async () => { + self.docActual.insulin = 0.5; + + await self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(self.docActual) + .expect(204); + + self.docActual.subject = self.subject.apiUpdate.name; + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + delete self.docActual.srvModified; + res.body.should.containEql(self.docActual); + self.docActual = res.body; + }); + + + it('PATCH document', async () => { + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(204); + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + delete self.docActual.srvModified; + res.body.should.containEql(self.docActual); + self.docActual = res.body; + }); + + + it('soft DELETE', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(204); + }); + + + it('READ of deleted is gone', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(410); + }); + + + + it('SEARCH of deleted document missing it', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.should.have.length(0); + }); + + + it('document deleted in HISTORY', async () => { + await self.checkHistoryExistence(value => { + value.isValid.should.be.eql(false); + }); + }); + + + it('permanent DELETE', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(204); + }); + + + it('READ of permanently deleted is not found', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(404); + }); + + + it('document permanently deleted not in HISTORY', async () => { + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`); + + if (res.status === 200) { + res.body.should.matchEach(value => { + value.identifier.should.not.be.eql(self.identifier); + }); + } else { + res.status.should.equal(204); + } + }); + +}); + diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js new file mode 100644 index 00000000000..38850b46ad5 --- /dev/null +++ b/tests/api3.patch.test.js @@ -0,0 +1,219 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 PATCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE, + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}/${self.validDoc.identifier}?token=${self.token.update}`; + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.patch(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.patch(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should not found not existing document', async () => { + let res = await self.instance.patch(self.urlToken) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + + // now let's insert the document for further patching + res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + }); + + + it('should reject identifier alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED'})) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field identifier cannot be modified by the client'); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should patch document', async () => { + self.validDoc.carbs = 10; + + let res = await self.instance.patch(self.urlToken) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(self.validDoc.identifier); + body.carbs.should.equal(10); + body.insulin.should.equal(0.3); + body.subject.should.equal(self.subject.apiCreate.name); + body.modifiedBy.should.equal(self.subject.apiUpdate.name); + }); + +}); + diff --git a/tests/api3.read.test.js b/tests/api3.read.test.js new file mode 100644 index 00000000000..b18b0225bb9 --- /dev/null +++ b/tests/api3.read.test.js @@ -0,0 +1,180 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 READ', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE, + uploaderBattery: 58 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/devicestatus'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should not found not existing document', async () => { + await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(404); + }); + + + it('should read just created document', async () => { + let res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(self.validDoc); + res.body.should.have.property('srvCreated').which.is.a.Number(); + res.body.should.have.property('srvModified').which.is.a.Number(); + res.body.should.have.property('subject'); + self.validDoc.subject = res.body.subject; // let's store subject for later tests + }); + + + it('should contain only selected fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=date,device,subject&token=${self.token.read}`) + .expect(200); + + const correct = { + date: self.validDoc.date, + device: self.validDoc.device, + subject: self.validDoc.subject + }; + res.body.should.eql(correct); + }); + + + it('should contain all fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=_all&token=${self.token.read}`) + .expect(200); + + for (let fieldName of ['app', 'date', 'device', 'identifier', 'srvModified', 'uploaderBattery', 'subject']) { + res.body.should.have.property(fieldName); + } + }); + + + it('should not send unmodified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .set('If-Modified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .expect(304); + + res.body.should.be.empty(); + }); + + + it('should send modified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .set('If-Modified-Since', new Date(new Date(self.validDoc.date).getTime() - 1000).toUTCString()) + .expect(200); + + res.body.should.containEql(self.validDoc); + }); + + + it('should recognize softly deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(410); + + res.body.should.be.empty(); + }); + + + it('should not found permanently deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should found document created by APIv1', async () => { + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.devicestatus.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + const identifier = doc._id.toString(); + delete doc._id; + + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(doc); + + res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + }); + }); + + +}); + diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js new file mode 100644 index 00000000000..dae0ebaaf34 --- /dev/null +++ b/tests/api3.search.test.js @@ -0,0 +1,261 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 SEARCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.docs = testConst.SAMPLE_ENTRIES; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = function get (identifier, done) { + self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200) + .end((err, res) => { + should.not.exist(err); + done(res.body); + }); + }; + + + /** + * Create given document in a promise + */ + self.create = (doc) => new Promise((resolve) => { + doc.identifier = opTools.calculateIdentifier(doc); + self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc) + .end((err) => { + should.not.exist(err); + self.get(doc.identifier, resolve); + }); + }); + + + before(async () => { + self.testStarted = new Date(); + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/entries'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.read}`; + self.urlTest = `${self.urlToken}&srvModified$gte=${self.testStarted.getTime()}`; + + const promises = testConst.SAMPLE_ENTRIES.map(doc => self.create(doc)); + self.docs = await Promise.all(promises); + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(self.url) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should found at least 10 documents', async () => { + let res = await self.instance.get(self.urlToken) + .expect(200); + + res.body.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should found at least 10 documents from test start', async () => { + let res = await self.instance.get(self.urlTest) + .expect(200); + + res.body.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should reject invalid limit - not a number', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=INVALID`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should reject invalid limit - negative number', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=-1`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should reject invalid limit - zero', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=0`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should accept valid limit', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=3`) + .expect(200); + + res.body.length.should.be.equal(3); + }); + + + it('should reject invalid skip - not a number', async () => { + let res = await self.instance.get(`${self.urlToken}&skip=INVALID`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter skip out of tolerance'); + }); + + + it('should reject invalid skip - negative number', async () => { + let res = await self.instance.get(`${self.urlToken}&skip=-5`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter skip out of tolerance'); + }); + + + it('should reject both sort and sort$desc', async () => { + let res = await self.instance.get(`${self.urlToken}&sort=date&sort$desc=created_at`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameters sort and sort_desc cannot be combined'); + }); + + + it('should sort well by date field', async () => { + let res = await self.instance.get(`${self.urlTest}&sort=date`) + .expect(200); + + const ascending = res.body; + const length = ascending.length; + length.should.be.aboveOrEqual(self.docs.length); + + res = await self.instance.get(`${self.urlTest}&sort$desc=date`) + .expect(200); + + const descending = res.body; + descending.length.should.equal(length); + + for (let i in ascending) { + ascending[i].should.eql(descending[length - i - 1]); + + if (i > 0) { + ascending[i - 1].date.should.be.lessThanOrEqual(ascending[i].date); + } + } + }); + + + it('should skip documents', async () => { + let res = await self.instance.get(`${self.urlToken}&sort=date&limit=8`) + .expect(200); + + const fullDocs = res.body; + fullDocs.length.should.be.equal(8); + + res = await self.instance.get(`${self.urlToken}&sort=date&skip=3&limit=5`) + .expect(200); + + const skipDocs = res.body; + skipDocs.length.should.be.equal(5); + + for (let i = 0; i < 3; i++) { + skipDocs[i].should.be.eql(fullDocs[i + 3]); + } + }); + + + it('should project selected fields', async () => { + let res = await self.instance.get(`${self.urlToken}&fields=date,app,subject`) + .expect(200); + + res.body.forEach(doc => { + const docFields = Object.getOwnPropertyNames(doc); + docFields.sort().should.be.eql(['app', 'date', 'subject']); + }); + }); + + + it('should project all fields', async () => { + let res = await self.instance.get(`${self.urlToken}&fields=_all`) + .expect(200); + + res.body.forEach(doc => { + Object.getOwnPropertyNames(doc).length.should.be.aboveOrEqual(10); + Object.prototype.hasOwnProperty.call(doc, '_id').should.not.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'identifier').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvModified').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvCreated').should.be.true(); + }); + }); + + + it('should not exceed the limit of docs count', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.urlToken}&limit=10`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + + + it('should respect the ceiling (hard) limit of docs', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.urlToken}`) + .expect(200); + + res.body.length.should.be.equal(5); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + +}); + diff --git a/tests/api3.security.test.js b/tests/api3.security.test.js new file mode 100644 index 00000000000..4cdc8e22b21 --- /dev/null +++ b/tests/api3.security.test.js @@ -0,0 +1,189 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +const request = require('supertest') + , apiConst = require('../lib/api3/const.json') + , semver = require('semver') + , moment = require('moment') + ; +require('should'); + +describe('Security of REST API3', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + this.timeout(30000); + + + before(async () => { + self.http = await instance.create({ useHttps: false }); + self.https = await instance.create({ }); + + let authResult = await authSubject(self.https.ctx.authorization.storage); + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.http.server.close(); + self.https.server.close(); + }); + + + it('should require HTTPS', async () => { + if (semver.gte(process.version, '10.0.0')) { + let res = await request(self.http.baseUrl) // hangs on 8.x.x (no reason why) + .get('/api/v3/test') + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal(apiConst.MSG.HTTP_403_NOT_USING_HTTPS); + } + }); + + + it('should require Date header', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_DATE); + }); + + + it('should validate Date header syntax', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', 'invalid date header') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_BAD_DATE); + }); + + + it('should reject Date header out of tolerance', async () => { + const oldDate = new Date((new Date() * 1) - 2 * 3600 * 1000) + , futureDate = new Date((new Date() * 1) + 2 * 3600 * 1000); + + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', oldDate.toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + + res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date',futureDate.toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + }); + + + it('should reject invalid now ABC', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=ABC`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should reject invalid now -1', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=-1`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should reject invalid now - illegal format', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=2019-20-60T50:90:90`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should require token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', new Date().toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN); + }); + + + it('should require valid token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test?token=invalid_token') + .set('Date', new Date().toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN); + }); + + + it('should deny subject denied', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test?token=' + self.subject.denied.accessToken) + .set('Date', new Date().toUTCString()) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal(apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', 'api:entries:read')); + }); + + + it('should allow subject with read permission', async () => { + await request(self.https.baseUrl) + .get('/api/v3/test?token=' + self.token.read) + .set('Date', new Date().toUTCString()) + .expect(200); + }); + + + it('should accept valid now - epoch in ms', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().valueOf()}`) + .expect(200); + }); + + + it('should accept valid now - epoch in seconds', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().unix()}`) + .expect(200); + }); + + + it('should accept valid now - ISO 8601', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().toISOString()}`) + .expect(200); + }); + + + it('should accept valid now - RFC 2822', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().utc().format('ddd, DD MMM YYYY HH:mm:ss [GMT]')}`) + .expect(200); + }); + +}); \ No newline at end of file diff --git a/tests/api3.socket.test.js b/tests/api3.socket.test.js new file mode 100644 index 00000000000..5c2a5cf6461 --- /dev/null +++ b/tests/api3.socket.test.js @@ -0,0 +1,178 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('Socket.IO in REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , apiConst = require('../lib/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.identifier = utils.randomString('32', 'aA#'); // let's have a brand new identifier for your testing document + + self.docOriginal = { + identifier: self.identifier, + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP + }; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({ + storageSocket: true + }); + + self.app = self.instance.app; + self.env = self.instance.env; + self.colName = 'treatments'; + self.urlCol = `/api/v3/${self.colName}`; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.socket = self.instance.clientSocket; + }); + + + after(() => { + if(self.instance && self.instance.clientSocket && self.instance.clientSocket.connected) { + self.instance.clientSocket.disconnect(); + } + self.instance.server.close(); + }); + + + it('should not subscribe without accessToken', done => { + self.socket.emit('subscribe', { }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by invalid accessToken', done => { + self.socket.emit('subscribe', { accessToken: 'INVALID' }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by subject with no rights', done => { + self.socket.emit('subscribe', { accessToken: self.token.denied }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY); + done(); + }); + }); + + + it('should subscribe by valid accessToken', done => { + const cols = ['entries', 'treatments']; + + self.socket.emit('subscribe', { + accessToken: self.token.all, + collections: cols + }, function (data) { + data.success.should.equal(true); + should(data.collections.sort()).be.eql(cols); + done(); + }); + }); + + + it('should emit create event on CREATE', done => { + + self.socket.once('create', (event) => { + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docOriginal); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(self.docOriginal) + .expect(201) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit update event on UPDATE', done => { + + self.docActual.insulin = 0.5; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(self.docActual) + .expect(204) + .end((err) => { + should.not.exist(err); + self.docActual.subject = self.subject.apiUpdate.name; + }); + }); + + + it('should emit update event on PATCH', done => { + + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(204) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit delete event on DELETE', done => { + + self.socket.once('delete', (event) => { + event.colName.should.equal(self.colName); + event.identifier.should.equal(self.identifier); + done(); + }); + + self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(204) + .end((err) => { + should.not.exist(err); + }); + }); + +}); + diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js new file mode 100644 index 00000000000..403aadb022e --- /dev/null +++ b/tests/api3.update.test.js @@ -0,0 +1,289 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + identifier: utils.randomString('32', 'aA#'), + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + eventType: 'Correction Bolus', + insulin: 0.3 + }; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}/${self.validDoc.identifier}?token=${self.token.update}` + }); + + + after(() => { + self.instance.server.close(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.put(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.put(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should require update permission for upsert', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}?token=${self.token.update}`) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should upsert not existing document', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}?token=${self.token.all}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiAll.name); + }); + + + it('should update the document', async () => { + self.validDoc.carbs = 10; + delete self.validDoc.insulin; + + let res = await self.instance.put(self.urlToken) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + should.not.exist(body.insulin); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiUpdate.name); + }); + + + it('should update unmodified document since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 11 + }); + let res = await self.instance.put(self.urlToken) + .set('If-Unmodified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .send(doc) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(doc); + }); + + + it('should not update document modified since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 12 + }); + let body = await self.get(doc.identifier); + self.validDoc = body; + + let res = await self.instance.put(self.urlToken) + .set('If-Unmodified-Since', new Date(new Date(body.srvModified).getTime() - 1000).toUTCString()) + .send(doc) + .expect(412); + + res.body.should.be.empty(); + + body = await self.get(doc.identifier); + body.should.eql(self.validDoc); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should ignore identifier alteration in body', async () => { + self.validDoc = await self.get(self.validDoc.identifier); + + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED' })) + .expect(204); + + res.body.should.be.empty(); + }); + + + it('should not update deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.put(self.urlToken) + .send(self.validDoc) + .expect(410); + + res.body.should.be.empty(); + }); + +}); + diff --git a/tests/fixtures/api3/authSubject.js b/tests/fixtures/api3/authSubject.js new file mode 100644 index 00000000000..6036103b0e5 --- /dev/null +++ b/tests/fixtures/api3/authSubject.js @@ -0,0 +1,94 @@ +'use strict'; + +const _ = require('lodash'); + +function createRole (authStorage, name, permissions) { + + return new Promise((resolve, reject) => { + + let role = _.find(authStorage.roles, { name }); + + if (role) { + resolve(role); + } + else { + authStorage.createRole({ + "name": name, + "permissions": permissions, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + role = _.find(authStorage.roles, { name }); + resolve(role); + }); + } + }); +} + + +function createTestSubject (authStorage, subjectName, roles) { + + return new Promise((resolve, reject) => { + + const subjectDbName = 'test-' + subjectName; + let subject = _.find(authStorage.subjects, { name: subjectDbName }); + + if (subject) { + resolve(subject); + } + else { + authStorage.createSubject({ + "name": subjectDbName, + "roles": roles, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + subject = _.find(authStorage.subjects, { name: subjectDbName }); + resolve(subject); + }); + } + }); +} + + +async function authSubject (authStorage) { + + await createRole(authStorage, 'apiAll', 'api:*:*'); + await createRole(authStorage, 'apiAdmin', 'api:*:admin'); + await createRole(authStorage, 'apiCreate', 'api:*:create'); + await createRole(authStorage, 'apiRead', 'api:*:read'); + await createRole(authStorage, 'apiUpdate', 'api:*:update'); + await createRole(authStorage, 'apiDelete', 'api:*:delete'); + + const subject = { + apiAll: await createTestSubject(authStorage, 'apiAll', ['apiAll']), + apiAdmin: await createTestSubject(authStorage, 'apiAdmin', ['apiAdmin']), + apiCreate: await createTestSubject(authStorage, 'apiCreate', ['apiCreate']), + apiRead: await createTestSubject(authStorage, 'apiRead', ['apiRead']), + apiUpdate: await createTestSubject(authStorage, 'apiUpdate', ['apiUpdate']), + apiDelete: await createTestSubject(authStorage, 'apiDelete', ['apiDelete']), + admin: await createTestSubject(authStorage, 'admin', ['admin']), + readable: await createTestSubject(authStorage, 'readable', ['readable']), + denied: await createTestSubject(authStorage, 'denied', ['denied']) + }; + + const token = { + all: subject.apiAll.accessToken, + admin: subject.apiAdmin.accessToken, + create: subject.apiCreate.accessToken, + read: subject.apiRead.accessToken, + update: subject.apiUpdate.accessToken, + delete: subject.apiDelete.accessToken, + denied: subject.denied.accessToken + }; + + return {subject, token}; +} + +module.exports = authSubject; \ No newline at end of file diff --git a/tests/fixtures/api3/const.json b/tests/fixtures/api3/const.json new file mode 100644 index 00000000000..a0acf37cfee --- /dev/null +++ b/tests/fixtures/api3/const.json @@ -0,0 +1,138 @@ +{ + "YEAR_2019": 1546304400000, + "YEAR_2050": 2524611600000, + "TEST_APP": "cgm-remote-monitor.test", + "TEST_DEVICE": "Samsung XCover 4-123456735643809", + + "SAMPLE_ENTRIES": [ + { + "date": 1491717830000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 167584, + "noise": 2, + "rssi": 183, + "sgv": 149, + "type": "sgv", + "unfiltered": 171584, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718130000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 170656, + "noise": 2, + "rssi": 181, + "sgv": 152, + "type": "sgv", + "unfiltered": 175776, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718430000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 173536, + "noise": 2, + "rssi": 185, + "sgv": 155, + "type": "sgv", + "unfiltered": 180864, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718730000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 177120, + "noise": 2, + "rssi": 186, + "sgv": 159, + "type": "sgv", + "unfiltered": 182080, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719030000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 181088, + "noise": 2, + "rssi": 165, + "sgv": 163, + "type": "sgv", + "unfiltered": 186912, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719330000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 184736, + "noise": 1, + "rssi": 162, + "sgv": 170, + "type": "sgv", + "unfiltered": 188512, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719630000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 187776, + "noise": 1, + "rssi": 175, + "sgv": 175, + "type": "sgv", + "unfiltered": 192608, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719930000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 190816, + "noise": 1, + "rssi": 181, + "sgv": 179, + "type": "sgv", + "unfiltered": 196640, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720230000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 194016, + "noise": 1, + "rssi": 203, + "sgv": 181, + "type": "sgv", + "unfiltered": 199008, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720530000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 197536, + "noise": 1, + "rssi": 184, + "sgv": 186, + "type": "sgv", + "unfiltered": 203296, + "app": "cgm-remote-monitor.test" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/api3/instance.js b/tests/fixtures/api3/instance.js new file mode 100644 index 00000000000..a7693ab3c40 --- /dev/null +++ b/tests/fixtures/api3/instance.js @@ -0,0 +1,163 @@ +'use strict'; + +var fs = require('fs') + , language = require('../../../lib/language')() + , api = require('../../../lib/api3/') + , http = require('http') + , https = require('https') + , request = require('supertest') + , websocket = require('../../../lib/server/websocket') + , io = require('socket.io-client') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(__dirname + '/localhost.key'), + cert: fs.readFileSync(__dirname + '/localhost.crt') + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + self.addSecuredOperations = function addSecuredOperations (instance) { + + instance.get = (url) => request(instance.baseUrl).get(url).set('Date', new Date().toUTCString()); + + instance.post = (url) => request(instance.baseUrl).post(url).set('Date', new Date().toUTCString()); + + instance.put = (url) => request(instance.baseUrl).put(url).set('Date', new Date().toUTCString()); + + instance.patch = (url) => request(instance.baseUrl).patch(url).set('Date', new Date().toUTCString()); + + instance.delete = (url) => request(instance.baseUrl).delete(url).set('Date', new Date().toUTCString()); + }; + + + self.bindSocket = function bindSocket (storageSocket, instance) { + + return new Promise(function (resolve, reject) { + if (!storageSocket) { + resolve(); + } + else { + let socket = io(`${instance.baseUrl}/storage`, { + origins:"*", + transports: ['websocket', 'flashsocket', 'polling'], + rejectUnauthorized: false + }); + + socket.on('connect', function () { + resolve(socket); + }); + socket.on('connect_error', function (error) { + console.error(error); + reject(error); + }); + } + }); + }; + + + self.unbindSocket = function unbindSocket (instance) { + if (instance.clientSocket.connected) { + instance.clientSocket.disconnect(); + } + }; + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + disableSecurity = false, + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'], + storageSocket = null + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiApp = api(instance.env, ctx); + + if (disableSecurity) { + instance.ctx.apiApp.set('API3_SECURITY_ENABLE', false); + } + + instance.app.use('/api/v3', instance.ctx.apiApp); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + self.addSecuredOperations(instance); + + websocket(instance.env, instance.ctx, instance.server); + + self.bindSocket(storageSocket, instance) + .then((socket) => { + instance.clientSocket = socket; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }) + .catch((reason) => { + console.error(reason); + reject(reason); + }); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + return self; +} + +module.exports = configure(); \ No newline at end of file diff --git a/tests/fixtures/api3/localhost.crt b/tests/fixtures/api3/localhost.crt new file mode 100644 index 00000000000..21a2a39b0a4 --- /dev/null +++ b/tests/fixtures/api3/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAIx0y57dTqDpMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xOTAyMDQxOTM1MDhaFw0yOTAyMDExOTM1MDhaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKCeBaqAJU+nrzNUZMsD1jYQpmcw8+6tG69KQY2XmqMsaPupo2ArwUlYD3pm +F1HTf9Lkq8u07rlUyMaSSRYrY56vPrMWGSK5Elm4kF8DNS4b/55KwZC+YQM0ZuJK +wSM6WX4G7JwV936HKJAT+Ec+8Ofq3GQzA9+Z4x2zMwNGC8AghtPjsCk68ORCmr+5 +fdCdC1Rz9hE92Nmofi8e1hUTeZmFROx8hcYRhxYXLIWVxALc/t8yY3MZfsRuZXcP +/3PageAn0ecxhqlWBY23GDQx7OSEZxSEPgqxnAHQfQXIrPRjMkFNHeMM7HTvITAG +VCc99zEG3Jy5hatm+RAajdWBH4sCAwEAAaNQME4wHQYDVR0OBBYEFJJVZn5Y91O7 +JUKeHW4La8eseKKwMB8GA1UdIwQYMBaAFJJVZn5Y91O7JUKeHW4La8eseKKwMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAFOU19t9h6C1Hakkik/93kun +pwG7v8VvDPjKECR5KlNPKNZUOQaiMAVHgNwPWV8q+qvfydzIpDrTd/O5eOaOduLx +gDVDj078Q05j17RUC+ct5yQ6lPgEHlnkI0Zr/hgFyNC+mtK7oIm6BT8wSSRbv7AG +3wQzCA5UvW/BQ8rtNZSC42Jyr0BR0ZS9Fo3Gc4v/nZJlgkiBvU2gKVQ7VRKxybCn +0hDghVwTfBPq7PKmupLX82ktwhYpDJZXCsOVfq9mF6nbQ6b0MieXFD+7cBlEXb1e +3VgtVzKYyqh/Oex4HfMThzAJZSWa0E4FShr5XdTdIc3nB4Vgbsis5l9Yrcp3Xo4= +-----END CERTIFICATE----- diff --git a/tests/fixtures/api3/localhost.key b/tests/fixtures/api3/localhost.key new file mode 100644 index 00000000000..2486c15fefe --- /dev/null +++ b/tests/fixtures/api3/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAoJ4FqoAlT6evM1RkywPWNhCmZzDz7q0br0pBjZeaoyxo+6mj +YCvBSVgPemYXUdN/0uSry7TuuVTIxpJJFitjnq8+sxYZIrkSWbiQXwM1Lhv/nkrB +kL5hAzRm4krBIzpZfgbsnBX3focokBP4Rz7w5+rcZDMD35njHbMzA0YLwCCG0+Ow +KTrw5EKav7l90J0LVHP2ET3Y2ah+Lx7WFRN5mYVE7HyFxhGHFhcshZXEAtz+3zJj +cxl+xG5ldw//c9qB4CfR5zGGqVYFjbcYNDHs5IRnFIQ+CrGcAdB9Bcis9GMyQU0d +4wzsdO8hMAZUJz33MQbcnLmFq2b5EBqN1YEfiwIDAQABAoIBADoh95sGVnrGDjtd +yD1SXi2jSRcAOMmiDesbzS4aOPXmFPlBJMiiDYsmPDPoz3fmPNVvvl40VlLtxN1a +BOnpOl0swFzBGsfehC3FBzvcRVsy9wmrtPNWdHZceQBeXhkJ/WoHx4uWx8Ub1iqP +j8T5mufVsX7yl+xOHk2ZllUQ/R/EEz9x00pkiH8Vsn8DhFI5KNqgi4n4c36T3vrn +MjTp+1o7bJ/cEnvXLi+IG2CO5y5hVbu3iKb+71YOGc6f/AJVzZ3MegC3KMFho9lh +DbDzumMuW8fZNyBfslXXoOr6oDqNq92n/jC/2hR8Xlth/aafisJiIVGydeVdDXhM +gDjdroECgYEAy3hXuo/Q1acncInGhIJvHjS/sVShP2epHz9zp8XuWl4NCuGP5V2c +jLT0hDW+ZKTUFweK9sQJNta81gs4pYc+2HGI8RP65XW4vgesNoKbBcE9xhEq0HMX +KN3/MJiwkNkM95T3nWqulhzNszhgNbZDMAU3Ule+o4n8udwOlFCTeXMCgYEAyhV4 +PoL3wp05BY0ssyKEqld3EqHNlPdQeJe1Dg9LSBy+3Z9sNngRD1/FuTo7RX6UY0FH +MaSI1JwhHSQ+2GNkqdMvVAilTXIDRw8vU9B77bYiHjny8+vMU06I9V3cJ57bNfmR +NUJtPmGO9xQ5UYxhP9rFOcI4MIecSzu1tvqiG4kCgYB01NoS7sdsFrnnvcS2i6rA +PmufqEeaf6w1nBqNyHJPg1eb2t7kRfdBOBp6291CLv71Zkhd3zynN3BguzrAmUL1 +x2Npgh57qTf2LbOt7RqUmFwfIfZikONIfQgt4E7qLSdr9iakRgCPg2R9ty5PSSOV +LDmS131IrE/obLoWYZn8jwKBgQDIaAxMahONA+CFueCHcgcA6yah6qZ3QeCjB0g9 +vjsZM7CxFqX5So8YoRDzxWT8YTCFUjppZ9NujbtlLAnLDJ7KsC2yd7R/Hj9T3CJC +S3JrZoFlWnCvJ7wFLdAzDTcEb8zTNUGlANBX2eYu7/Z8Aex7p9iJlCunLQV5sqhd +4yaaiQKBgQCERrz1XcJpM8S93nXdAv3Nn1bwA1V/ylx42DRxNEBl2JZQ1sQeqN36 +JvXPXhVZ3vTQDhVUqcVgqJIAb2xMviIVBnssOq3+pi/hOs13rakJf4AOulZ/3Si7 +HSLdymfQAMEKczU2261kw4pjPwiurkjAFWbQG2C8RGE/rR2y38PkDg== +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/api3/utils.js b/tests/fixtures/api3/utils.js new file mode 100644 index 00000000000..942f948c10e --- /dev/null +++ b/tests/fixtures/api3/utils.js @@ -0,0 +1,21 @@ +'use strict'; + +function randomString (length, chars) { + let mask = ''; + if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; + if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (chars.indexOf('#') > -1) mask += '0123456789'; + if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; + + let result = ''; + + for (let i = length; i > 0; --i) + result += mask[Math.floor(Math.random() * mask.length)]; + + return result; +} + + +module.exports = { + randomString +}; \ No newline at end of file From 7a0204842a8982e3975b67e5a8dacfadd26ab85b Mon Sep 17 00:00:00 2001 From: John Weston Date: Wed, 9 Oct 2019 12:56:54 -0700 Subject: [PATCH 048/134] Add BrowserStack badge to the Readme (#5001) * Create robots.txt * Update robots.txt * Delete robots.txt * add browserstack logo image * add BrowserStack badge/link to readme * add resized BrowserStack logo * Delete browserstack-logo-600x315.png --- README.md | 6 ++++++ static/images/browserstack-logo.png | Bin 0 -> 14657 bytes static/robots.txt | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 static/images/browserstack-logo.png diff --git a/README.md b/README.md index 917be293303..0118853ff3d 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,12 @@ Feel free to [post an issue][issues], but read the [wiki][wiki] first. [issues]: https://github.com/nightscout/cgm-remote-monitor/issues [wiki]: https://github.com/nightscout/cgm-remote-monitor/wiki +### Browser testing suite provided by +[![BrowserStack][browserstack-img]][browserstack-url] + +[browserstack-img]: /static/images/browserstack-logo.png +[browserstack-url]: https://www.browserstack.com/ + License --------------- diff --git a/static/images/browserstack-logo.png b/static/images/browserstack-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..765fea8fe1f615f2fafd8448bc4e630f408278bc GIT binary patch literal 14657 zcmdVAWmFv7)-FsScyM=@;BG;Jy9DT*D8n0BmoR@e zq6$qS& zd2n*@To~;Wpau#;in{SuCzcRwLr=95iger*1Q-2F8KnkOe-36vwyGB<0q)kOl{way zw^cK}>+g-&Wraj1$+P493auQul8QOO>%OOC6`h zZ=ulL$Zmr7If)^}+C=uovA5N|M;b#(CJBT|T`j6SZSBLRchIJSjk-xDy^3kk6Pebx z0Daa+n0t@(9M#m->x4+|_|SW?F6Pigx}b3UUK}B2h9dW&??R1p?cA{92N+vh!K3@A zc$(bZ^GC$nH#o#USV7y}VaHm*EKd7mG`Ssf+o%=Fg{UNC`V4G-XhijH2Y@S2%vMjc zU4vwOvwWm*onf`BL1aDGm&t@^Z94`XXHL)<1s}}^ORv>oG7L^p$^mh&$OC5r4%@@? z-SdM`QG&v$v#1rXy)x&_E1Zskn}o;ysZqLgrwdc3`lT77>oimh#KBrwM4vPh{(R3 z#Ajkm#3K5=O!{#0-l!x1F>raW$H3%|!^CkB@SD8jUhfDkBIqgMbFyd@aacw2vT&n= zATp75xmmEZ{I@gjb|ETJi-iy};da$7sT?tmL_TK8v=5wZ~}W<3*-#rSB|lE$xY0D?Ed{y|`Vt z#eXDx{@}qsO00^Q49zX@jwqqbU+yC%ks?wk3LWAx@;wxRzfdOjnD?&OB}F%~LV#qz zK|q85RVTVGs%YF?3QDd8h!{esZ*MyKwl(;c)Yad$F`$mU6ne*h9%slAu0^ zP2}@8=fHE?bDMJp;Q(DBt6Xn6?`X2P}T9Ksexm(F0Jj@|#iGglkS#?nXw` z_X?$*%3*gpg&Ap;>;|Qq;A7Zos-yuK!?^O13dpO<%f~ByM>NMH$BujQ`-ppo`>Xpf zxO@m)FH)~!h++sY-+JGQcX3$L*t`sxm{!5^!PPibSS~DVjG=7WCW}Uk7MbSl%zdl{ ztPcA93}4x`jMRaSJ;6rZjFQX?0J{lX)*5zMMtb%Z9S?O5Reu^gHCrhXDVs#{RAtWM ziAeLls=*&cRfj+1D`3j|D~u~0d6q047cYk<7INKcs%vCwmJOdqMY_ma$wzYMwu_7F zid(A}uS4-VhG)!kw4#=hu+W*24G&Yt03(%YfR^dzspllGEU(FoVV%~oQO0GfJUt4( zF6gbI9ZhRu4hODcTduv!xw$sZg**R?&vdbLKsrdxd&9&*spL8fgVMrg>XoYYs)djv z^poofjhcqk4=YXMPX&G+exq6a8=N!2N&P&%E=Rzl_gCDUDOAO^B)Kn?&nQF6JlZhm9?db&V}CPUywl8Q)x=SKKU&FB5Ok3eFT>`)35 z)gHPURvU)LTf+w|pd_FxU?fU2DlqCkN-L^{6d}7dFFLys*&(1`$U^=};U?Qv3RZj} zjV%&CMLUgu*nfnBNEs;CnTV^9| zK=r^s9kmgwka_|9G?|$d6oV0j@mWK+S$`>Vr>?oKg{F%hjmAtXx5o6UJaubv3ynG) z4VqeA1-oeQ^IT28h0>Mr{(j}u%tg^f;Kg!jfc8XkX-W%K%MD};fiWy9Og4CmiPrS- z9nwqIZqM~sy92wT2W?84MtOZLMfuCGO<$g3(o9{co`Cv-TBj) zY`*@mwlcRmiB;3Ut}d@ZH+N|#V!dq}ic4#bn15_Pnn2jHwT|!-$v?5*=GO3WfI9n-lU2u~>(Z72? zrGO)=AiI->&l%-pkS{!bzHfRyw>%AAj4;xb++gE+eRmD(fkCHr*<@c5vS7Czd5{wo zmqMRb+g>+rKecAu!TMm*Q@Ze(Z~1t1*|OtIWI{SGX$X(`gUpI`%h&7bo1>s__TNgD z;hGn$=+CuPb{2<^>2y?Ho-Q9O@He?JU2FG|);_K2<@AurR;DUGn_oM*)Ck@b;Gd+L z$abXh9->_~Kaw1TmAc%GV2((n9`Kg#)g1A0u70q*=t{ZHp>Sn5yi-j-_Bj=S{$Ao< z{eI|BFUxP!se(s(^#rHqJO5 zrQku7XhE{F)K970VQ7MX$X$X!brH9N$$j|X@Tj0BZdbjbT`6M9@*WGz!EeZyl-b=P zg@mLdgoI?}dzk?NNToAo+K_0D*ORRO0B+7fwrJWa-z5SzBlx9Pr@6%I`W4zzOw9%a z1QzAz`wd89GUn@QmIm3*&$Ij&?4Q&Bl>lH5{4aWc&iq%{tCRn;MtdF0{|O{5{XfIZ%>JX9Hnzfc zua5gY5dUK09|~<09V`L#@&Fr)uhx11VLO1i?O)M&bvE};KXUz?H~soEAWG&yTV5va zU+(?q@L%!y4W;@QjF+8>;q^t&`1A93^xvod36-@5zMhHBPmR2czsvu9?4R;#|Ff#U zPyY@2XPmiSZ}NV>$Kw?O+E|+EIB?Vd$Jzgg7PK(6u$H#a2kZ>GwbiLzYYHr@k^K5U%FUd{rQ)w zzfJrN_?o9&(ywK&uVbqtq+<)l@W?2E#QTb&(+BEv zvFfogaB>3lUd?0ErDb7bXQ0(#WMiY%;b3NDWYp(0VAW^;rQtu*{F^EY%C0Hzv);5 z_zhnT;^qH&MIe;>qcak|KK6KxIx8Wiw2mIo{Aas-&rScIfPWeAr;gthd@};P=JqSe ze~J8WY`>)bI|5u9{Xfk7ee|C&Zu;L` z{{K`c^o(@O4FUSR^nV!l$G|_W`zt;EZ$*z;k4cwPpOt|Yps&kB%c8@Z@PweVj{S^VGg@_%R9zrp_s z!SB?6YcjvB!t^clUeo@s3zC7ZK9l~>9>c-L0-$ANWapsec=akRCp)LE9st0|!NKzC z!rv7AjqGM59^{1?17bIVfRH?j3-K%3zd1^QOjeqz zI-TLH8D8A;$zZHZO{51N*PsLHMmG6Hv# zBYfjyC>SUFCdcVW@0*R2srM!s@elQeGS%(fQq<;Cs{9q|@bwbK@DsF4xvCLD(&i+( z^+Qj{Rmy5)$5SN|GbVy3ZuJKzQ=zyixRAVkxVNLAh5?wxe921V%d{v`jJ)*7Y1Bkn z^Tsv-Q=908CSS7QO6SbpE{@dpJBQs-+$+ZhhfHC|@t{ADGSkjfzjDw(LXe}WO%~VL zXgT>FH%%fQ1nH*`A!YS)xrgHk&3gI~ipK)5bvuz%o^!m`m^(^{Zyay3J>wpkC7$K% z*PfEzQHk~R`Pkrc%yU{o?d>+0RIQ{i4*EB^deJ1q`O4O7A+H=GN%d(9=sUpeCGN%! z7nnnb+KOi<%aeacVzjQ(ST78ceBRDSEeZ%MWHwjv?+$v%#bDnWqIu#2$9OV`(4n-0 zx5mBQS#5O(H=k=~UPMtRHdNPL44B0&tQVE;u}^_CVd@I5T?rqxjH{Hr5$mxs4?xFn zh5-X(DkGv2J`w6{;h{QaBdyDo8?_y&r(Uc9H&}V8QG~0L3k4IUhlHnxyEGekyl`>N za!CcMzDQKcH<+;6lP<3!2jq%*LpEdst5vutFvPPhy@hfF)QMezFm>6mjhGY^+F1%P zT5$5~B^_OzUfnjW{(+lE{^ETmI`8fIh*S5Ssb3ZYr44)x`PUp;92VOK?l|Fbi*d!o z2Z$WYc0N=zlgEB#r*sH3V9lJf%bLS=mv~?aJA5+EdE0~`-pQk7{9&oQnvB?Njpb@* zbt4HmTGv6m`&Okt+p}!5K_NB^J0hplJ7>Kh-JSwXtS!fjQ82{h!f>U5qo!0+PnXVq z(rPsvw5O4Me`lm#PU_%|qw@Eriut*UBcWMh9%0&O%>gVD{SzNU$ugtdLxg&L7fD6z zdM5&w@4E%61@bmj*J8z}>ZM{;ZiR}F#PZu+Mr5BejL^7{GpLi*i5)8X;r%7esp4H$ zB2yO^S8yRidEq&18b)Gg+?=Lq2EzHWmZ(hMl9D$sQ7CJ)!*?#Y>4Y5e+`uCD;ShgK zi7f&KiW#!x6leOY3Ubp8oyEW9OsND9Rlk5!ppsVTmyWB=kpzX(DmbeSh7kg2@@p=x z?k<v}M+IzW>A^V-ZkTMF;}m-5V+*Ltw~oH8^=lgNwPbyQ z#3f{fqg;2h8I}qWyQfkMiNzzqtS-RavYafwn^Mw|U@w~pF>?7m8#P)TW=7%JZT6_Y znE0d4nOc&*tp}Mx;QG2mWy$4L**i6$SX-A@*Sp%pB7XrE?xQ<+Wlv|`^P`gpX z#>3vyTR^|F!(mxzNl3;Iq}?u|9q}I+ig&vv3NTO`(ln;HHv7J`PuHO7snl^OUzNfL zgua6MG>cyC&mzPbu~^4{uI+}dq(T89061A4HJDD%K<2u_-Ai@O)Z!IMC|(Uh3g^DS zeP52xzdKpP0zgibLDL#|SA*I-O|%>Uj@`sDg7Sd!>^Aq-+RYl5LxN*8ueFZ$0;1E< zv>ZXGNLfY{dDM3P9Wcqc2ycRCuDUvZcLT}Caj+LG#Z3P8JZoL++FSLgdHTtU@nzT+vUJhw zFZ37>R*KkL9vZFYOXZw?#$b2#BYl+ojX7nq!`@Jh05|V+9O*|IzR%Zb1z}Q->5Q0h z*J5Fn1)tOwDrI5fp8f2fA7ou%aX9e9M02H+$VqO@Vls1H=#Hn!lj7y)FwZQ#$Fn#E zd-p?!QyrCt@RGM8M(^!#;s$Q4Qp}cS79tkYvLeAc{18TPKG+@Bj%A4w__+=GBa!%~ zT6JT&Pr^cf$5Tn$0=%Q8dq-sV()on83Z*ml< z?`rqa=g8N>RiW8(E2i9dOQP;-T05G_R zjhb8;H+lQI(UkzOsi`Tl@O$**&$>V2kGqz}j3%w?G&52@d}CuU7>TOZ?+L|gIOY4q zU@#bu0=y`Vr>0vpZm&6BZc6eB@Zk4t<>}AbR4sCaSl^OW_2*GkTt&8?;&HAm-oiaE zj)-oZVrTb1naaDLGF3O|pDd^@Q&Lv$ueI99uB}y7uYP@wdh<-RM3MR)Y4U)-P>$x> zqEY&X^b{O!da^RoL~&N-mtE!Z&znCg%%MfAak_!wF6}_s>aen{j(8L?f~b_8PTKvq z!(A=-H!AaA;RI?tD&Mhj+;YAM+O&Gt^pI#uW-t(?!02rX^)Nwj<*o>ynv3qbiqGOh zzWvH(uPxl^_VwZVs2Ka1!=pOpQ;kc!7)L+o(TB);W~u%O$P?-;>Zn~Uk$TcW@qwdA zIVCiNaD}vRs!k;&G%MvLF}5 z@rf96F+1|q@UJ1vWXmy&80ZPHtR3pr40>z3@274)$LGLt$8Q_CS0po($FfF2|9Fc+ zQRR1(q)2xK#R(#r*SheM9Zzc->~k!jyvi5yg50c*#eG`gQAe4#5g93V`@Vap&-;K= zsO(c%DjyaKtC8?PldiHM`%2)|<=lECm!^Dj#F0#|cDw@0%|5$4GdmbH8soV4RozZ% z)b_T)hlxcMY7vz@|Cy0D61m1>Tg7C9WE{mITR7Z!6{cQM`ew6znk^4!Q#9N(>1VDX z4>4Z7t5~irZf{Z6FtYdhzsq?QSh77Ue~5j!xLI|FUZM9L-F(Z4cS|j%5fc}O;j;ok zhfr~D{|LSbr)%o!pT?Ad3?61cFSz97eC*bSo%0VIxQ+3Hq&rF0!J~{K&{qI*J1xlBhlcEV1P8zyct}NBpNoxfDN`j z22U;t?g#}7p-0DCqlGFHl_o8wD?^0b;V!5p4blRGi&46(s*2%2Nd_=Yg*=%d6Em}r zpSgd`&2$c+Bfo#G{zo`CI8TM-Y~$h1$DZy=yh4>yI~y@1jb>*Dx2HaHZ#5;Q7{92S z6}K0$1@ky1&r1!rAqzM>u1N&hJQTaZEYXXLi_cmut~`=J%qm_Phm4LD<&IFVo0opCI)$~kXS7%>;>WJ*+tx?_xyC-(NN@WyG zdUi<69xo^Cuc4Wtpwc}SWX&n7*7jweCdymg=~2WUXr2;4&5`tw$jsP*^b+I|Wf*&FKKqM&|gbm~v9>&v?JAwod5=+yC zD1h>MIT`OLjqWbGtm}LTyd`}K6$VwgwU%#zVN7)V5)JF#(DlXf^A{Er)_l=^a{0;= z0R@PLga??nm|j|KAlkC?ipeFdd703L8wUnz8kv>}Foe%}SlJuBGdz7?l5rB@LqF~q zFRH&?)Ag>11MMJ04G~zvN1^b4`EwZs$=x)RfefsDmG^way zpWB0?!}J`^wj`kmq@bWW8Vf*ai(Y{p<|bba0s_7x1sVo~b?o8rf;k&%wg_k*V+v49bOL#lTs^WtjHQ)PcWZ`0bg0>o zg7mW+@`9LY{BaUqjjnT{S4TsnwXohb!@FQ6$KC#Y?!)fI(B)l?cNDhnW^V+ByBh(J z%Yd%ko@>Qadnj+jO_#q3zisU71Opj%*JhHP+v5^3XeRO!v=2U;;u^*&8G_I6A?=t^QO|Ewtxo`p!6Q>duX`ThEvEjd~0lp#$ydDiv_(n8049FZ4_@VTpw z&NT*muEB&@OA0~|!?x_MRhduD?HZVyEr_em?+)rbT0O>0>bUhVm+)?F*s)n08MP*1z3$3_)kl;ru5gyL|P+pc~ z!JGuBqM2WrKZ7d3qI4;HU4SW{-8>LgBt70i4Jq&1TPaYk1b2`3UGVHmNk)J8c#9$B z&)z#To$C3(C_~YyI&R+iLPtKUwPty%-pe;Sv43kQmne zA+&ndrDp}cxW={|`8j!;8c|zFE2#GXq{aO@n4A|<{nKE|0wcBAn{>W3aFrWX7-NWK zh-2gVTrkE&*X2+*rmfouzdPx-FAE_dSktA`Q)0_EXDY^N8Jzgo4x@5II22Dv{Ub!q z5HkD@89`pg;^y8(2K|Rw#Tu(yULGG;9hNru4}{03*GeIJQY&s@^AEJfjbjj}{S)2! z2%n6TjT7){&37I4c7(=VeHE7B1?_o#T4aU{Z|?}akD@-Fe)iiAOIJfEIX@FPFX?*0 zdbX(@6c_gtkd~GPii-A>Qrdui(xy@m>o~X@%&@~DI+FEm4VclxiZEIX>{c9)`eM(S zEjo~(=_;CJ7l>pgnnkwbu}K8S3NQy2B@lIOm}GyoakF6+XJQV=2!Gu^6rr1@v90X8 z<;By$d=ao$g`-QZ-R!b1qbA0N|8A_0i_jelobbCOD6o&gV~j)E@+}jOEX5Qa!+Akv zC#y_+W8J8=eGTe_@DQKh@PQQ66wM2}hZ{Dv8JSb{kyHs<_8#eKpSqOKA=tR0UUKlj z@q+2N;%Yq3Hp}i@l+SIU6e`{-$PuOV!32y{FQ}fDmQ}Rci$uqKDvK<1Q zsgDvBfu~ltak`{Gq>zE<3+ZwEX>2WE&Gn|LVbe1%OVWuRrJfW}3jvd? zskI_s5%?hAoQ#TrPJgj_)byDX@4ZGcU~j)BwB^lm7*+2hzA)?7F80(8KP=mh2Y)td z%%Y|lQ>HkD2bkulE5FCwWCu)tDaDL)m0o^0Y7sB9vY@(< z%V^hy9&VDM@`H~l^g|c3d6+;B034%K+3U0y(S8r}|Fk~1N2=2!B~E^c;ZAhWyRf&} zK{>?>o-IpMG%qkD!fI3E4jjO1^d3PVqM==ZokV@hw>0SpPAWQU>I|_CPQDQ4zuQUY z6)RKr#IeLa)Cs(QFV#mQWK*W=PYCz-I)l@JjDAJPQv2nE)~0*KcI1)*2?C zGk>G-!EmbzY#vhU$7GQbzY4ztyW}j(+;p;CdKh2ydw@q{9G9sMm~ai0%2*HSOGOX> zMpIn7_nqH@-cp2=hn^S_tw@k$Kh8$^eyfJOntd|rof1~|7tGu(HRfFL<~ts>AGh#( zyk+z;b1M0j^7V9e>LP=;m71T>T3btEs*3ApZ+w|@GXsuPr=eaX%Fk&FN2W+&2SVGE zSD4h?b~dfl4Zoc4nxHPx(-Lb@OcSoFFrBUFe#BM0nrhrrs8utA#rilU!R9`*>y4Mz zHWrhOXDAC{37ROXW$~#7K2oL{m?4*s`{0JUCeAVBmi$QUNo85j5Uk5;PUs9HvjzfN z+*D3-$?t6r4WI*)?NMRvt}&&l#8{JAd{fzDeE$tLN>77$WQ{60Hfg+{wA`$sz4~2x z@sZ3ms|O+p&n_WBcNmF~F+>3~g-kN0JA2ou9@Le#_k+s#SE8}54TuudXKE*UA#Hlh z*G1d;4thT>EbUPkOf0QsC2d0!m3P0w2Z9#3Z$cmM#79$-m3w99!8=bRBnO}jppxC} zbyQSLR3xS9zjpP3WWDv+cs4n0&cD6AKiGZ>8?frw4(L&e_fUyAiGUqRCSvWOkH9#2 z$~km?U=28r^!B~PG{zN7G!Mhn%kzJLof+^!>FG@%cX+xR$mj~ZoDiI+eIj|Gd*VMz#NLsZsB^8_;r(4?na18ov0>ecP+YHK!1yyUj*yAhp5ZO}f)5uD$^t5|7qVa?iVTQ#X3yM}1 zDD$w@bWl7iGAJlLN1NOkQbK(veR8n#@^aMU5+S&dzK(FEx^$O8qmXMR*9MOAQs=KU zoY}<|ui08)8}k8;{3Q*W#5}W?yl)_QCx0uLkFC#(@MNyf4yH- zsqeRs*W{6r5K$ZuYYAFu`9UB2ZNSdxtM5(j)HKSZ3FyPj%Hfh^jbA5^xe-0CV*&M+ zD0ugPJpbeyd7V=tXP4pc%~b1k>Nub?(y3;6a?4n^q+9}99=RPfc%)qzOIc+-_C9Un zPf62G@0+=NZm=4wlHVQYHkPytuWEUgk*VO16A&&XsH%$~wxSvNMmh7YFjfj9@F;S8 zT~Ga}DNV>{o13|mhvFJx$-D>fuF|R*^EEtuc#9rIxn!Z|)zz)x{atm$GHUeEr{HU>rzB|iJestj75-SsDNj=P{LsfAOfxQI2j$l=f zIh;P6G0B<+ege_`(IMdt7AQsy_t0^F`%-s_$gdN?%13ihXo^XzY)<*5^U5Ncg^ zr3&Qm^|;b;R`5C)LWuga`Nj(Xd5Ny}AMll11UUsZB*9#{2buEnWQcN$Ih0W*`kjeB-cbuZ2VX5!)`ANcn z3NU%qcI10Oc^$8qek`OF-k=$UVkzmG=qp`&I9sQRrdf9*epFh#<8V6hSP6!(hvCCp zG&cEiHjI5ivpXDKmmsy#U;uQ~Ze?oe7{`F8yF{sl6C*!Fe zLsUTnGSLrd{Gqo}{YvGltd~MgPfMbK_~G2(soIh>bHD2eQ|eHsh1Xq zq%m1kU|c%tYVEf9?bZhElPwghArJH~JVbLyCN(PFSI1r1&e5NS{wAPuopzRfJG1zzFB1KqL`AW^N#v+W`;$=9hX|$+P zfwIcu$J6*ce_H>xFwdhFfdVCxgjBg?R0%geTNjJc_YBD4a!+sTA+o3xcPGeA>y27(!dMSwp&uHG7``BnEQ3)! z-^2_oYxWk!=r>}h7SiOV&F93*1P(M+pB7?DDqA*&z7>GLE}^Ux>E2ss7Nlw(maROa z2X+>L6m}+}QqUcjahwFPyX+L9*b3|74sOlD+UQSakHX0?${^fk12>hp4vw|y)L~6f zC@)BP9-sN`of4#0FQF%S;COd=Mfs(#_i9(7a#<3AS?6Ly+OcXl(KoX4E!729_ex@Wh%8<&uo!t>;h z>=9Kd(bU<=ONQfa{EZQfrjnl7_;Rp*;HwXn;;cucN3a0beyx>n6b}uYRmEKTMtV9MlFk(HL`=RR0+2_7PnTRXk`GLswPI8%PSQAs4{sF1o z5%Ux?Slb=C;zDfii(`=};kLrDjwYuJ#Xhi>^rIUd*dv-$971G30Ej?s@S6qUO6xG73m6xC|AE2yK z$Qgj_6_K9>R|tr|!}_*U_UB*Af|`vtZPRLw5={0C^#ymlo!#K|1Ud9vrcED~5xhad zk3^sOg82>O@m9G0I(ONL(t=*|_WRaV^yu_7)U5294;^`4M4q$Ck+-2&Gq;-PB9a(| z*6vFb)Im*eOO!L(Ez?&Z;Og=#W93UC&7`b5W3iySg}yTZm@r21NClZgYMJ``YKUVp z%kG35TiZsfan0|UboSB?VmhG@DSHv`9oBwHHJ+HEbg+4~)`M6dr#f{hy?(Vh(#Cmp?AKi( zmC5IBlO9%nueazW)xVH0OQ95A%@>enPE|neh{?rePGnR}Nl(90iqMWnxF~gNr@YW* zlx|}5;&Az?lvB}*ve;leRfrNJN7i6`V`1)MD4ucS*5%>a1#eMOe1cbD5(??FbRA@y zgAWT16%`e+COx<8GMy!tWnoIADW4VJ4cFcd#h|3iYEj(Sce9l0BhsRcJ1AIpUuJje ze$q0z*)1CE9q~VHikePWSx!X^XJzHp*kw8oBg`xn@75KO z*@x*H{hA^#hk?vILg5khy=(C1I+%S~Pr6E7#Uh^`?0BFUaRfoH@^nFvnN zD%ag?;2jALySXV*GEwK#RFNMsu4;S_zLV*UUf5H|^9XNBt+4<>!CeUe^?tpenlsFA zjnTV+AY@G8cH{Zr^`Dz2JbJBfB>VfrtFaIymBl0AoJziQqIU=%uDdTbw-VmWeFi4o z7d*c4zGNvUM~oW7;mR>ph^xKjh-jl9yfF3+Ff1F6GcDT(<(<)=>Yd}PvkwJJQ-se4 zj#HdU9#dpmZBV^k*SUHY`L%Sm`SfVBZf7ZH-Zqdv_Id$sCF9T=Pr`%wAx@ox zt~kwhie#`SnB)^?r4L#3Y>Oe?&rNRYUo`go4kocVS?rq_IZKMQLe-|;4>_Z(eMyz% zx{zYSbr#2JV`e0*J&ok{F7{U#)ou3K*=dN)eWGmgt6r|U>w!Z)5gW#v%?G?GH{r&( zen{(+7!kI?z3sF~pD~rO_;IzXsUoU#2R3+DsMz4cQKfhh8S;f|-o4!}K*+3m1_62@ zIGIb4Tabao2{vhZs$bT2_}yTg#g#kNFwxfQAGqtSMBp+Dz~axlNBK)PG(9JVNX#Tx z#mMX7#VZp72y>iZaJ>~wXJMttDr)I`R6~@#2j_+>aaK2z1lDvsJsz^yknq%&^Q+ew zJ9uF0FM2DsncW+BL;;7wr^ZQLFv2W*Bpz6&0j2eeyts^wPwI8F-15yiU#^=TU)Ehx zrChSvHeq%+POU7zd<4yd+jBI3KCOM-wfyBgava7czC7!bK*Q}J=n5gHJj0XrSx6!3 z2~?8__q$W-!`3>b#anvr^FVk(uDFzq)}AhpH?YZU`43wrBHg;%#4srX=rj(Dxw5U9 z(vA4Q<8_i^qJWFjgwG*2$;SJxVNLOg!S@7?N!nqxFfP?|yL4NwpJYGXgTb@EZ|c~j zIKHz`ep*(IN2z{RjgjQz;;<9~&C}vKZD$ocr@c^j)w*h7JaptU4?eG=b@3|ryi{I_ zbZILD9{L@TYr?B9f6c?4sPqghK_hUu>7;DL7(#%~FKVOp#RC4F<@Sxa!NJ7}M<1XT zJ&GA^=A^suTVF@cE>`~7@C%Kl%5%xAY>3Ip%WhI18h}EjzYn%@21Z&c7AG@fF z?RRl2XG6q#KU*I8(OKak=ZEkxvaAPx%DG&qZPzQclBv{ z;QV@h{EXY1I|hLl-|JK<>xqi9?1nt~{Ysuat@*aG7zq0VQLA|F_LH kf9|>o-)J4{^LTlK<>zsobc50VbFa9#u#8ZNz!%T|1D&J2rvLx| literal 0 HcmV?d00001 diff --git a/static/robots.txt b/static/robots.txt index 1f53798bb4f..68ad19bb9f2 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -1,2 +1,2 @@ -User-agent: * -Disallow: / +User-agent: Browsershots +Disallow: From adb138525d5773bcb393b85cd39c6a85072fcdc9 Mon Sep 17 00:00:00 2001 From: p5nbTgip0r Date: Wed, 9 Oct 2019 13:02:16 -0700 Subject: [PATCH 049/134] Fix black OpenAPS COB forecast dots (#5057) * Removed alpha from COB hex colors * Updated README for default COB/ACOB hex values * Added link to W3C CSS color specifications --- README.md | 6 +++--- lib/plugins/openaps.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0118853ff3d..614cbe94e7b 100644 --- a/README.md +++ b/README.md @@ -481,9 +481,9 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. - * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in either `#RRGGBB` or `#RRGGBBAA` format. - * `OPENAPS_PRED_COB_COLOR` (`#FB8C00FF`) - The color to use for COB prediction lines. Same format as above. - * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C0080`) - The color to use for ACOB prediction lines. Same format as above. + * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in `#RRGGBB` format, but [other CSS color units](https://www.w3.org/TR/css-color-3/#colorunits) may be used as well. + * `OPENAPS_PRED_COB_COLOR` (`#FB8C00`) - The color to use for COB prediction lines. Same format as above. + * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C00`) - The color to use for ACOB prediction lines. Same format as above. * `OPENAPS_PRED_ZT_COLOR` (`#00d2d2`) - The color to use for ZT prediction lines. Same format as above. * `OPENAPS_PRED_UAM_COLOR` (`#c9bd60`) - The color to use for UAM prediction lines. Same format as above. * `OPENAPS_COLOR_PREDICTION_LINES` (`true`) - Enables / disables the colored lines vs the classic purple color. diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index a9c45db7e1b..cda93141481 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -54,8 +54,8 @@ function init (ctx) { , urgent: settings.urgent ? settings.urgent : 60 , enableAlerts: settings.enableAlerts , predIOBColor: settings.predIobColor ? settings.predIobColor : '#1e88e5' - , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00FF' - , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C0080' + , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00' + , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C00' , predZTColor: settings.predZtColor ? settings.predZtColor : '#00d2d2' , predUAMColor: settings.predUamColor ? settings.predUamColor : '#c9bd60' , colorPredictionLines: settings.colorPredictionLines From 112bfff0d0efb643811b71eb5cfa8cba7733d5c6 Mon Sep 17 00:00:00 2001 From: Erin879 <53097574+Erin879@users.noreply.github.com> Date: Wed, 9 Oct 2019 21:07:31 +0100 Subject: [PATCH 050/134] Append token parameter in URL to side menu links (#5020) --- lib/client/browser-settings.js | 17 +++++++++++++++++ views/index.html | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index 4f990eb57f3..2119b39da40 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -135,7 +135,24 @@ function init (client, serverSettings, $) { }); $('#editprofilelink').toggle(settings.isEnabled('iob') || settings.isEnabled('cob') || settings.isEnabled('bwp') || settings.isEnabled('basal')); + + //fetches token from url + var parts = (location.search || '?').substring(1).split('&'); + var token = ''; + parts.forEach(function (val) { + if (val.startsWith('token=')) { + token = val.substring('token='.length); + } + }); + //if there is a token, append it to each of the links in the hamburger menu + if (token != '') { + token = '?token=' + token; + $('#reportlink').attr('href', 'report' + token); + $('#editprofilelink').attr('href', 'profile' + token); + $('#admintoolslink').attr('href', 'admin' + token); + $('#editfoodlink').attr('href', 'food' + token); + } } function wireForm () { diff --git a/views/index.html b/views/index.html index 3a156008903..2531fbba743 100644 --- a/views/index.html +++ b/views/index.html @@ -168,7 +168,7 @@
From b8f28b05866272fa12063bfa01b7ee1308bfc9fa Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Mon, 21 Oct 2019 07:06:05 -0500 Subject: [PATCH 062/134] fix(d3): D3 libarry upgrade (lib for visualizing data such as main view, graphs, etc.) (#5081) * initial commit for d3 upgrade * d3 v5 mostly working except brush * more work on the brush * brush and layout mostly working now * make open-right and open-left lines visible * make brush selection hidden * fix open-right, open-top, and now-line in retro mode * fix setting brush to now * fix updateBrushToNow to set start correctly * add extent to updateBrushToNow * fix inRetro to use dataExtent instead of domain * move a debug log message to better location * debug for brush movements * cleanup adjusted range vs. brushed range * fix syntax error * remove transitions for brush movements * log message for dataUpdate * fix updating brush range when new data arrives * fix keeping up with data when not in retro * keep brush range at focusRangeMS during update * fix variable name error * keep chart in sync with current time when not in retro * use short transition time * test no opacity changes * remove one more highlight and use small transition duration * fix race condition * fix for uncommanded going to retro * fix syntax error * update chart scales when new data arrives in retro mode * Use consistent transition for scroll * clean chart.updateContext * comment out debug messages * remove unused variable * update renderer to account for no structure arg for d3 attr * fix syntax error * one more syntax error * use requestAnimationFrame (cherry picked from commit b610485597b2e9ecf46471994980136d3c17deec) * forgot to update current scroll data (cherry picked from commit 1ab26522cd4abd3fb77a76964469200fc50dc555) * remove extra treatment circles * try using class for selection (cherry picked from commit 92f678c6fe8e2bb700dc61ab56a71d483c5a9988) * reduce update required for focus circles (cherry picked from commit 46aab643125c2a083807627b9fb6052faef2134f) * reduce update required for focus treatments (cherry picked from commit 23ba5c4bea4bf2ff75f2296e1edc606f311ef7fe) * fix update prepare treat circles (cherry picked from commit afcbbab40fe0c93e26e97152513845cf2c5ecf6f) * use _id for treatments key * do not use opacity for past entry circles * replace scale.linear with scaleLinear see https://github.com/d3/d3/blob/master/CHANGES.md#scales-d3-scale (cherry picked from commit 3445eeeedd1c0d863ca5210d4cd32994a9da4d49) * d3 upgrade axis (cherry picked from commit 06a8a9283100a980ccb5bed7920bce96a5b138cc) * d3 upgrade axis (cherry picked from commit 1ac583ce2b1d0796f8d8f0d5f9b5ee29a051af1e) * fix tooltip location * fix plugin tooltip location * update reports for d3 v5 * fix inner radius default for insulin distribution pie * fix dynamic scaling issues * fix single click scroll * Fix font size on axis labels * Render ticks on top of everything * fix click to scroll jankiness * move loading finished to the bottom of updateHeader --- lib/client/chart.js | 444 ++++++++++++++++++----------- lib/client/index.js | 49 ++-- lib/client/renderer.js | 262 +++++++++-------- lib/plugins/pluginbase.js | 2 +- lib/report_plugins/calibrations.js | 14 +- lib/report_plugins/daytoday.js | 47 ++- lib/report_plugins/weektoweek.js | 18 +- npm-shrinkwrap.json | 333 +++++++++++++++++++--- package.json | 2 +- 9 files changed, 775 insertions(+), 396 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index bb9ef950977..7b053cd83d0 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -3,7 +3,19 @@ // var _ = require('lodash'); var times = require('../times'); var d3locales = require('./d3locales'); -var padding = { bottom: 30 }; +var scrolling = false + , scrollNow = 0 + , scrollBrushExtent = null + , scrollRange = null +; + +var PADDING_BOTTOM = 30 + , CONTEXT_MAX = 420 + , CONTEXT_MIN = 36 + , FOCUS_MAX = 510 + , FOCUS_MIN = 30 + , DEFAULT_TRANS_MS = 100 +; function init (client, d3, $) { var chart = { }; @@ -31,20 +43,37 @@ function init (client, d3, $) { // arrow head defs.append('marker') - .attr({ - 'id': 'arrow', - 'viewBox': '0 -5 10 10', - 'refX': 5, - 'refY': 0, - 'markerWidth': 8, - 'markerHeight': 8, - 'orient': 'auto' - }) + .attr('id', 'arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 5) + .attr('refY', 0) + .attr('markerWidth', 8) + .attr('markerHeight', 8) + .attr('orient', 'auto') .append('path') .attr('d', 'M0,-5L10,0L0,5') .attr('class', 'arrowHead'); - var localeFormatter = d3.locale(d3locales.locale(client.settings.language)); + var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language)); + + function beforeBrushStarted ( ) { + // go ahead and move the brush because + // a single click will not execute the brush event + var now = new Date(); + var dx = chart.xScale2(now) - chart.xScale2(new Date(now.getTime() - client.focusRangeMS)); + + var cx = d3.mouse(this)[0]; + var x0 = cx - dx / 2; + var x1 = cx + dx / 2; + + var range = chart.xScale2.range(); + var X0 = range[0]; + var X1 = range[1]; + + var brush = x0 < X0 ? [X0, X0 + dx] : x1 > X1 ? [X1 - dx, X1] : [x0, x1]; + + chart.theBrush.call(chart.brush.move, brush); + } function brushStarted ( ) { // update the opacity of the context data points to brush extent @@ -64,13 +93,13 @@ function init (client, d3, $) { var yScaleType; if (client.settings.scaleY === 'linear') { - yScaleType = d3.scale.linear; + yScaleType = d3.scaleLinear; } else { - yScaleType = d3.scale.log; + yScaleType = d3.scaleLog; } - var focusYDomain = [utils.scaleMgdl(30), utils.scaleMgdl(510)]; - var contextYDomain = [utils.scaleMgdl(36), utils.scaleMgdl(420)]; + var focusYDomain = [utils.scaleMgdl(FOCUS_MIN), utils.scaleMgdl(FOCUS_MAX)]; + var contextYDomain = [utils.scaleMgdl(CONTEXT_MIN), utils.scaleMgdl(CONTEXT_MAX)]; function dynamicDomain() { // allow y-axis to extend all the way to the top of the basal area, but leave room to display highest value @@ -84,7 +113,7 @@ function init (client, d3, $) { //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); return [ - utils.scaleMgdl(30) + utils.scaleMgdl(FOCUS_MIN) , Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult)) ]; } @@ -98,71 +127,92 @@ function init (client, d3, $) { } // define the parts of the axis that aren't dependent on width or height - var xScale = chart.xScale = d3.time.scale().domain(extent); + var xScale = chart.xScale = d3.scaleTime().domain(extent); + focusYDomain = dynamicDomainOrElse(focusYDomain); var yScale = chart.yScale = yScaleType() - .domain(dynamicDomainOrElse(focusYDomain)); + .domain(focusYDomain); + + var xScale2 = chart.xScale2 = d3.scaleTime().domain(extent); - var xScale2 = chart.xScale2 = d3.time.scale().domain(extent); + contextYDomain = dynamicDomainOrElse(contextYDomain); var yScale2 = chart.yScale2 = yScaleType() - .domain(dynamicDomainOrElse(contextYDomain)); + .domain(contextYDomain); - chart.xScaleBasals = d3.time.scale().domain(extent); + chart.xScaleBasals = d3.scaleTime().domain(extent); - chart.yScaleBasals = d3.scale.linear() + chart.yScaleBasals = d3.scaleLinear() .domain([0, 5]); - var tickFormat = localeFormatter.timeFormat.multi( [ - ['.%L', function(d) { return d.getMilliseconds(); }], - [':%S', function(d) { return d.getSeconds(); }], - [client.settings.timeFormat === 24 ? '%H:%M' : '%I:%M', function(d) { return d.getMinutes(); }], - [client.settings.timeFormat === 24 ? '%H:%M' : '%-I %p', function(d) { return d.getHours(); }], - ['%a %d', function(d) { return d.getDay() && d.getDate() !== 1; }], - ['%b %d', function(d) { return d.getDate() !== 1; }], - ['%B', function(d) { return d.getMonth(); }], - ['%Y', function() { return true; }] - ]); + var formatMillisecond = localeFormatter.format('.%L'), + formatSecond = localeFormatter.format(':%S'), + formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%I:%M'), + formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I %p'), + formatDay = localeFormatter.format('%a %d'), + formatWeek = localeFormatter.format('%b %d'), + formatMonth = localeFormatter.format('%B'), + formatYear = localeFormatter.format('%Y'); + + var tickFormat = function (date) { + return (d3.timeSecond(date) < date ? formatMillisecond + : d3.timeMinute(date) < date ? formatSecond + : d3.timeHour(date) < date ? formatMinute + : d3.timeDay(date) < date ? formatHour + : d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) + : d3.timeYear(date) < date ? formatMonth + : formatYear)(date); + }; var tickValues = client.ticks(client); - chart.xAxis = d3.svg.axis() - .scale(xScale) + chart.xAxis = d3.axisBottom(xScale) + + chart.xAxis = d3.axisBottom(xScale) .tickFormat(tickFormat) - .ticks(4) - .orient('bottom'); + .ticks(6); - chart.yAxis = d3.svg.axis() - .scale(yScale) + chart.yAxis = d3.axisLeft(yScale) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); - chart.xAxis2 = d3.svg.axis() - .scale(xScale2) + chart.xAxis2 = d3.axisBottom(xScale2) .tickFormat(tickFormat) - .ticks(6) - .orient('bottom'); + .ticks(6); - chart.yAxis2 = d3.svg.axis() - .scale(yScale2) + chart.yAxis2 = d3.axisRight(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('right'); + .tickValues(tickValues); + + d3.select('tick') + .style('z-index', '10000'); // setup a brush - chart.brush = d3.svg.brush() - .x(xScale2) - .on('brushstart', brushStarted) + chart.brush = d3.brushX() + .on('start', brushStarted) .on('brush', function brush (time) { client.loadRetroIfNeeded(); client.brushed(time); }) - .on('brushend', brushEnded); + .on('end', brushEnded); + + chart.theBrush = null; - chart.futureOpacity = d3.scale.linear( ) - .domain([times.mins(25).msecs, times.mins(60).msecs]) - .range([0.8, 0.1]); + chart.futureOpacity = (function() { + var scale = d3.scaleLinear( ) + .domain([times.mins(25).msecs, times.mins(60).msecs]) + .range([0.8, 0.1]); + + return function (delta) { + if (delta < 0) { + return null; + } else { + return scale(delta); + } + }; + })(); // create svg and g to contain the chart contents chart.charts = d3.select('#chartContainer').append('svg') @@ -176,42 +226,66 @@ function init (client, d3, $) { // create the x axis container chart.focus.append('g') - .attr('class', 'x axis'); - + .attr('class', 'x axis') + .style("font-size", "16px"); + // create the y axis container chart.focus.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); - chart.context = chart.charts.append('g').attr('class', 'chart-context'); + chart.context = chart.charts.append('g') + .attr('class', 'chart-context'); // create the x axis container chart.context.append('g') - .attr('class', 'x axis'); + .attr('class', 'x axis') + .style("font-size", "16px"); // create the y axis container chart.context.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); + + chart.createBrushedRange = function () { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; + var range = brushedRange && brushedRange.map(chart.xScale2.invert); + var dataExtent = client.dataExtent(); - function createAdjustedRange() { - var range = chart.brush.extent().slice(); + if (!brushedRange) { + // console.log('No current brushed range. Setting range to last focusRangeMS amount of available data'); + range = dataExtent; + + range[0] = new Date(range[1].getTime() - client.focusRangeMS); + } - var end = range[1].getTime() + client.forecastTime; + var end = range[1].getTime() if (!chart.inRetroMode()) { - var lastSGVMills = client.latestSGV ? client.latestSGV.mills : client.now; - end += (client.now - lastSGVMills); + end = client.now > dataExtent[1].getTime() ? client.now : dataExtent[1].getTime(); } range[1] = new Date(end); + range[0] = new Date(end - client.focusRangeMS); return range; } + chart.createAdjustedRange = function () { + var adjustedRange = chart.createBrushedRange(); + + adjustedRange[1] = new Date(adjustedRange[1].getTime() + client.forecastTime); + + return adjustedRange; + } + chart.inRetroMode = function inRetroMode() { - if (!chart.brush || !chart.xScale2) { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; + + if (!brushedRange || !chart.xScale2) { return false; } - var brushTime = chart.brush.extent()[1].getTime(); var maxTime = chart.xScale2.domain()[1].getTime(); + var brushTime = chart.xScale2.invert(brushedRange[1]).getTime(); return brushTime < maxTime; }; @@ -235,15 +309,16 @@ function init (client, d3, $) { var dataRange = client.dataExtent(); var chartContainerRect = chartContainer[0].getBoundingClientRect(); var chartWidth = chartContainerRect.width; - var chartHeight = chartContainerRect.height - padding.bottom; + var chartHeight = chartContainerRect.height - PADDING_BOTTOM; // get the height of each chart based on its container size ratio var focusHeight = chart.focusHeight = chartHeight * .7; - var contextHeight = chart.contextHeight = chartHeight * .2; + var contextHeight = chart.contextHeight = chartHeight * .3; chart.basalsHeight = focusHeight / 4; // get current brush extent - var currentBrushExtent = createAdjustedRange(); + var currentRange = chart.createAdjustedRange(); + var currentBrushExtent = chart.createBrushedRange(); // only redraw chart if chart size has changed var widthChanged = (chart.prevChartWidth !== chartWidth); @@ -259,14 +334,14 @@ function init (client, d3, $) { //set the width and height of the SVG element chart.charts.attr('width', chartWidth) - .attr('height', chartHeight + padding.bottom); + .attr('height', chartHeight + PADDING_BOTTOM); // ranges are based on the width and height available so reset chart.xScale.range([0, chartWidth]); chart.xScale2.range([0, chartWidth]); chart.xScaleBasals.range([0, chartWidth]); chart.yScale.range([focusHeight, 0]); - chart.yScale2.range([chartHeight, chartHeight - contextHeight]); + chart.yScale2.range([contextHeight, 0]); chart.yScaleBasals.range([0, focusHeight / 4]); if (init) { @@ -281,33 +356,39 @@ function init (client, d3, $) { .call(chart.yAxis); // if first run then just display axis with no transition + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') + chart.context.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); -// chart.basals.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); - - chart.context.append('g') + chart.theBrush = chart.context.append('g') .attr('class', 'x brush') - .call(d3.svg.brush().x(chart.xScale2).on('brush', client.brushed)) - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + .call(chart.brush) + .call(g => g.select(".overlay") + .datum({type: 'selection'}) + .on('mousedown touchstart', beforeBrushStarted)); + + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight); // disable resizing of brush - d3.select('.x.brush').select('.background').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.e').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.w').style('cursor', 'move'); + chart.context.select('.x.brush').select('.overlay').style('cursor', 'move'); + chart.context.select('.x.brush').selectAll('.handle') + .style('cursor', 'move'); + + chart.context.select('.x.brush').select('.selection') + .style('visibility', 'hidden'); // add a line that marks the current time chart.focus.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale(utils.scaleMgdl(420))) + .attr('y2', chart.yScale(focusYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -371,9 +452,9 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) + .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))) + .attr('y2', chart.yScale2(contextYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -400,7 +481,7 @@ function init (client, d3, $) { } else { // for subsequent updates use a transition to animate the axis to the new position - var focusTransition = chart.focus.transition(); + var focusTransition = chart.focus.transition().duration(DEFAULT_TRANS_MS); focusTransition.select('.x') .attr('transform', 'translate(0,' + focusHeight + ')') @@ -410,86 +491,83 @@ function init (client, d3, $) { .attr('transform', 'translate(' + chartWidth + ', 0)') .call(chart.yAxis); - var contextTransition = chart.context.transition(); + var contextTransition = chart.context.transition().duration(DEFAULT_TRANS_MS); + + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') contextTransition.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); - chart.basals.transition(); - -// basalsTransition.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); + chart.basals.transition().duration(DEFAULT_TRANS_MS); // reset brush location - chart.context.select('.x.brush') - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight); - // clear current brushs - d3.select('.brush').call(chart.brush.clear()); + // console.log('Redrawing old brush with new dimensions: ', currentBrushExtent); // redraw old brush with new dimensions - d3.select('.brush').transition().call(chart.brush.extent(currentBrushExtent)); + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); // transition lines to correct location chart.focus.select('.high-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))); chart.focus.select('.target-top-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))); chart.focus.select('.target-bottom-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))); chart.focus.select('.low-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))); // transition open-top line to correct location chart.context.select('.open-top') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX))) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX))); // transition open-left line to correct location chart.context.select('.open-left') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[0])) - .attr('y2', chartHeight); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[1])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chartHeight); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition high line to correct location chart.context.select('.high-line') - .transition() + .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .attr('x2', chart.xScale2(dataRange[1])) @@ -497,7 +575,7 @@ function init (client, d3, $) { // transition low line to correct location chart.context.select('.low-line') - .transition() + .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .attr('x2', chart.xScale2(dataRange[1])) @@ -505,64 +583,88 @@ function init (client, d3, $) { } } - // update domain - chart.xScale2.domain(dataRange); + chart.updateContext(dataRange); + chart.xScaleBasals.domain(dataRange); - var updateBrush = d3.select('.brush').transition(); - updateBrush - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]])); - client.brushed(true); + // console.log('Redrawing brush due to update: ', currentBrushExtent); + + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); + }; + + chart.updateContext = function (dataRange_) { + if (client.documentHidden) { + console.info('Document Hidden, not updating - ' + (new Date())); + return; + } + + // get current data range + var dataRange = dataRange_ || client.dataExtent(); + + // update domain + chart.xScale2.domain(dataRange); renderer.addContextCircles(); // update x axis domain chart.context.select('.x').call(chart.xAxis2); - }; - chart.scroll = function scroll (nowDate) { - chart.xScale.domain(createAdjustedRange()); - chart.yScale.domain(dynamicDomainOrElse(focusYDomain)); - chart.xScaleBasals.domain(createAdjustedRange()); + function scrollUpdate() { + scrolling = false; + + var nowDate = scrollNow; + + var currentBrushExtent = scrollBrushExtent; + var currentRange = scrollRange; + + chart.xScale.domain(currentRange); + + focusYDomain = dynamicDomainOrElse(focusYDomain); + + chart.yScale.domain(focusYDomain); + chart.xScaleBasals.domain(currentRange); // remove all insulin/carb treatment bubbles so that they can be redrawn to correct location d3.selectAll('.path').remove(); // transition open-top line to correct location chart.context.select('.open-top') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[1])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-left line to correct location chart.context.select('.open-left') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(chart.brush.extent()[0])) - .attr('y2', chart.prevChartHeight); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .attr('x1', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.prevChartHeight); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); chart.focus.select('.now-line') - .transition() + .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(nowDate)) - .attr('y1', chart.yScale(utils.scaleMgdl(36))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(nowDate)) - .attr('y2', chart.yScale(utils.scaleMgdl(420))); + .attr('y2', chart.yScale(focusYDomain[1])); chart.context.select('.now-line') - .transition() - .attr('x1', chart.xScale2(chart.brush.extent()[1])) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) - .attr('x2', chart.xScale2(chart.brush.extent()[1])) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))); + .transition().duration(DEFAULT_TRANS_MS) + .attr('x1', chart.xScale2(currentBrushExtent[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentBrushExtent[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // update x,y axis chart.focus.select('.x.axis').call(chart.xAxis); @@ -575,6 +677,18 @@ function init (client, d3, $) { renderer.addTreatmentProfiles(client); renderer.drawTreatments(client); + } + + chart.scroll = function scroll (nowDate) { + scrollNow = nowDate; + scrollBrushExtent = chart.createBrushedRange(); + scrollRange = chart.createAdjustedRange(); + + if (!scrolling) { + requestAnimationFrame(scrollUpdate); + } + + scrolling = true; }; return chart; diff --git a/lib/client/index.js b/lib/client/index.js index 979348c0cb5..dd7fba1a063 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -276,7 +276,7 @@ client.load = function load (serverSettings, callback) { function formatTime (time, compact) { var timeFormat = getTimeFormat(false, compact); - time = d3.time.format(timeFormat)(time); + time = d3.timeFormat(timeFormat)(time); if (client.settings.timeFormat !== 24) { time = time.toLowerCase(); } @@ -375,14 +375,14 @@ client.load = function load (serverSettings, callback) { // clears the current user brush and resets to the current real time data function updateBrushToNow (skipBrushing) { - // get current time range - var dataRange = client.dataExtent(); - // update brush and focus chart with recent data - d3.select('.brush') - .transition() - .duration(UPDATE_TRANS_MS) - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]])); + var brushExtent = client.dataExtent(); + + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); + + // console.log('Resetting brush in updateBrushToNow: ', brushExtent); + + chart.theBrush && chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); if (!skipBrushing) { brushed(); @@ -398,21 +398,34 @@ client.load = function load (serverSettings, callback) { } function brushed () { + // Brush not initialized + if (!chart.theBrush) { + return; + } + + // default to most recent focus period + var brushExtent = client.dataExtent(); + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); - var brushExtent = chart.brush.extent(); + var brushedRange = d3.brushSelection(chart.theBrush.node()); - // ensure that brush extent is fixed at 3.5 hours - if (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS) { + if (brushedRange) { + brushExtent = brushedRange.map(chart.xScale2.invert); + } + + // console.log('Brushed to: ', brushExtent); + + if (!brushedRange || (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS)) { // ensure that brush updating is with the time range if (brushExtent[0].getTime() + client.focusRangeMS > client.dataExtent()[1].getTime()) { brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); } else { brushExtent[1] = new Date(brushExtent[0].getTime() + client.focusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); } + + // console.log('Updating brushed to: ', brushExtent); + + chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); } function adjustCurrentSGVClasses (value, isCurrent) { @@ -428,7 +441,6 @@ client.load = function load (serverSettings, callback) { currentBG.toggleClass('icon-hourglass', value === 9); currentBG.toggleClass('error-code', value < 39); currentBG.toggleClass('bg-limit', value === 39 || value > 400); - container.removeClass('loading'); } function updateCurrentSGV (entry) { @@ -546,6 +558,8 @@ client.load = function load (serverSettings, callback) { var top = (client.bottomOfPills() + 5); $('#chartContainer').css({ top: top + 'px', height: $(window).height() - top - 10 }); + + container.removeClass('loading'); } function sgvToColor (sgv) { @@ -1130,6 +1144,7 @@ client.load = function load (serverSettings, callback) { } function dataUpdate (received) { + console.info('got dataUpdate', new Date(client.now)); var lastUpdated = Date.now(); receiveDData(received, client.ddata, client.settings); @@ -1171,6 +1186,8 @@ client.load = function load (serverSettings, callback) { chart.update(false); client.plugins.updateVisualisations(client.nowSBX); brushed(); + } else { + chart.updateContext(); } } }; diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 84f8574ed47..944d66da964 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -7,6 +7,7 @@ var DEFAULT_FOCUS = times.hours(3).msecs , WIDTH_SMALL_DOTS = 420 , WIDTH_BIG_DOTS = 800 , TOOLTIP_TRANS_MS = 100 // milliseconds + , DEFAULT_TRANS_MS = 10 // milliseconds , TOOLTIP_WIDTH = 150 //min-width + padding ; @@ -40,20 +41,23 @@ function init (client, d3) { }; function tooltipLeft () { - var windowWidth = $(client.tooltip).parent().parent().width(); + var windowWidth = $(client.tooltip.node()).parent().parent().width(); var left = d3.event.pageX + TOOLTIP_WIDTH < windowWidth ? d3.event.pageX : windowWidth - TOOLTIP_WIDTH - 10; return left + 'px'; } function hideTooltip () { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) + client.tooltip.transition().duration(TOOLTIP_TRANS_MS) .style('opacity', 0); } // get the desired opacity for context chart based on the brush extent renderer.highlightBrushPoints = function highlightBrushPoints (data) { - if (client.latestSGV && data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) { + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + + if (client.latestSGV && data.mills >= from && data.mills <= to) { return chart().futureOpacity(data.mills - client.latestSGV.mills); } else { return 0.5; @@ -75,7 +79,10 @@ function init (client, d3) { }); var maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); // limit lookahead to the same as lookback - var focusHoursAheadMills = chart().brush.extent()[1].getTime() + client.focusRangeMS; + var selectedRange = chart().createAdjustedRange(); + var to = selectedRange[1].getTime(); + + var focusHoursAheadMills = to + client.focusRangeMS; maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; focusData = focusData.concat(shownForecastPoints); @@ -85,7 +92,7 @@ function init (client, d3) { // selects all our data into data and uses date function to get current max date var focusCircles = chart().focus.selectAll('circle').data(focusData, client.entryToDate); - function prepareFocusCircles (sel) { + function updateFocusCircles (sel) { var badData = []; sel.attr('cx', function(d) { if (!d) { @@ -107,17 +114,12 @@ function init (client, d3) { return chart().yScale(scaled); } }) - .attr('fill', function(d) { - return d.type === 'forecast' ? 'none' : d.color; - }) .attr('opacity', function(d) { - return d.noFade || !client.latestSGV ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills); - }) - .attr('stroke-width', function(d) { - return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; - }) - .attr('stroke', function(d) { - return (d.type === 'mbg' ? 'white' : d.color); + if (d.noFade) { + return null; + } else { + return !client.latestSGV ? 1 : chart().futureOpacity(d.mills - client.latestSGV.mills); + } }) .attr('r', function(d) { return dotRadius(d.type); @@ -130,6 +132,21 @@ function init (client, d3) { return sel; } + function prepareFocusCircles (sel) { + updateFocusCircles(sel) + .attr('fill', function(d) { + return d.type === 'forecast' ? 'none' : d.color; + }) + .attr('stroke-width', function(d) { + return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; + }) + .attr('stroke', function(d) { + return (d.type === 'mbg' ? 'white' : d.color); + }); + + return sel; + } + function focusCircleTooltip (d) { if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') { return; @@ -161,7 +178,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareFocusCircles(focusCircles.transition()); + updateFocusCircles(focusCircles.transition().duration(DEFAULT_TRANS_MS)); // if new circle then just display prepareFocusCircles(focusCircles.enter().append('circle')) @@ -240,7 +257,7 @@ function init (client, d3) { //NOTE: treatments with insulin or carbs are drawn by drawTreatment() // bind up the focus chart data to an array of circles - var treatCircles = chart().focus.selectAll('treatment-dot').data(client.ddata.treatments.filter(function(treatment) { + var treatCircles = chart().focus.selectAll('.treatment-dot').data(client.ddata.treatments.filter(function(treatment) { var notCarbsOrInsulin = !treatment.carbs && !treatment.insulin; var notTempOrProfile = !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); @@ -253,7 +270,22 @@ function init (client, d3) { })); return notCarbsOrInsulin && !treatment.duration && treatment.durationType !== 'indefinite' && notTempOrProfile && notOpenAPSSpam; - })); + }), function (d) { return d._id; }); + + function updateTreatCircles (sel) { + + sel.attr('cx', function(d) { + return chart().xScale(new Date(d.mills)); + }) + .attr('cy', function(d) { + return chart().yScale(client.sbx.scaleEntry(d)); + }) + .attr('r', function() { + return dotRadius('mbg'); + }); + + return sel; + } function prepareTreatCircles (sel) { function strokeColor (d) { @@ -276,15 +308,7 @@ function init (client, d3) { return color; } - sel.attr('cx', function(d) { - return chart().xScale(new Date(d.mills)); - }) - .attr('cy', function(d) { - return chart().yScale(client.sbx.scaleEntry(d)); - }) - .attr('r', function() { - return dotRadius('mbg'); - }) + updateTreatCircles(sel) .attr('stroke-width', 2) .attr('stroke', strokeColor) .attr('fill', fillColor); @@ -293,10 +317,11 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareTreatCircles(treatCircles.transition()); + updateTreatCircles(treatCircles.transition().duration(DEFAULT_TRANS_MS)); // if new circle then just display prepareTreatCircles(treatCircles.enter().append('circle')) + .attr('class', 'treatment-dot') .on('mouseover', function(d) { client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) @@ -305,6 +330,8 @@ function init (client, d3) { }) .on('mouseout', hideTooltip); + treatCircles.exit().remove(); + var durationTreatments = client.ddata.treatments.filter(function(treatment) { return !treatment.carbs && !treatment.insulin && (treatment.duration || treatment.durationType !== undefined) && !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); @@ -377,8 +404,8 @@ function init (client, d3) { } // if transitioning, update rect text, position, and width - var rectUpdates = treatRects.transition() - rectUpdates.attr('transform', rectTranslate) + var rectUpdates = treatRects.transition().duration(DEFAULT_TRANS_MS); + rectUpdates.attr('transform', rectTranslate); rectUpdates.select('text') .text(treatmentText) @@ -441,7 +468,7 @@ function init (client, d3) { } }) .attr('fill', function(d) { return d.color; }) - .style('opacity', function(d) { return renderer.highlightBrushPoints(d) }) + //.style('opacity', function(d) { return renderer.highlightBrushPoints(d) }) .attr('stroke-width', function(d) { return d.type === 'mbg' ? 2 : 0; }) .attr('stroke', function() { return 'white'; }) .attr('r', function(d) { return d.type === 'mbg' ? 4 : 2; }); @@ -454,7 +481,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareContextCircles(contextCircles.transition()); + prepareContextCircles(contextCircles.transition().duration(DEFAULT_TRANS_MS)); // if new circle then just display prepareContextCircles(contextCircles.enter().append('circle')); @@ -538,7 +565,7 @@ function init (client, d3) { arc_data[4].element = translate(treatment.status); } - var arc = d3.svg.arc() + var arc = d3.arc() .innerRadius(function(d) { return 5 * d.inner; }) @@ -617,10 +644,10 @@ function init (client, d3) { var insulinRect = { x: 0, y: 0, width: 0, height: 0 }; var carbsRect = { x: 0, y: 0, width: 0, height: 0 }; var operation; - renderer.drag = d3.behavior.drag() - .on('dragstart', function() { + renderer.drag = d3.drag() + .on('start', function() { //console.log(treatment); - var windowWidth = $(client.tooltip).parent().parent().width(); + var windowWidth = $(client.tooltip.node()).parent().parent().width(); var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10; client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9) .style('left', left + 'px') @@ -633,29 +660,25 @@ function init (client, d3) { , height: chart().yScale(chart().yScale.domain()[0]) }; chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: deleteRect.x - , y: deleteRect.y - , width: deleteRect.width - , height: deleteRect.height - , fill: 'red' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x) + .attr('y', deleteRect.y) + .attr('width', deleteRect.width) + .attr('height', deleteRect.height) + .attr('fill', 'red') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: deleteRect.x + deleteRect.width / 2 - , y: deleteRect.y + deleteRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: 'red' - , 'text-anchor': 'middle' - , dy: '.35em' - , transform: 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')' - }) + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x + deleteRect.width / 2) + .attr('y', deleteRect.y + deleteRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'red') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('transform', 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')') .text(translate('Remove')); if (treatment.insulin && treatment.carbs) { @@ -672,52 +695,44 @@ function init (client, d3) { , height: 50 }; chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: carbsRect.x - , y: carbsRect.y - , width: carbsRect.width - , height: carbsRect.height - , fill: 'white' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x) + .attr('y', carbsRect.y) + .attr('width', carbsRect.width) + .attr('height', carbsRect.height) + .attr('fill', 'white') + .attr('opacitys', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: carbsRect.x + carbsRect.width / 2 - , y: carbsRect.y + carbsRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: 'white' - , 'text-anchor': 'middle' - , dy: '.35em' - }) + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x + carbsRect.width / 2) + .attr('y', carbsRect.y + carbsRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'white') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') .text(translate('Move carbs')); chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: insulinRect.x - , y: insulinRect.y - , width: insulinRect.width - , height: insulinRect.height - , fill: '#0099ff' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x) + .attr('y', insulinRect.y) + .attr('width', insulinRect.width) + .attr('height', insulinRect.height) + .attr('fill', '#0099ff') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: insulinRect.x + insulinRect.width / 2 - , y: insulinRect.y + insulinRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: '#0099ff' - , 'text-anchor': 'middle' - , dy: '.35em' - }) + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x + insulinRect.width / 2) + .attr('y', insulinRect.y + insulinRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', '#0099ff') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') .text(translate('Move insulin')); } @@ -727,7 +742,7 @@ function init (client, d3) { }) .on('drag', function() { //console.log(d3.event); - client.tooltip.transition().style('opacity', .9); + client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width')); var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight); @@ -754,19 +769,16 @@ function init (client, d3) { chart().drag.selectAll('.arrow').remove(); chart().drag.append('line') - .attr({ - 'class': 'arrow' - , 'marker-end': 'url(#arrow)' - , 'x1': chart().xScale(new Date(treatment.mills)) - , 'y1': chart().yScale(client.sbx.scaleEntry(treatment)) - , 'x2': x - , 'y2': y - , 'stroke-width': 2 - , 'stroke': 'white' - }); - + .attr('class', 'arrow') + .attr('marker-end', 'url(#arrow)') + .attr('x1', chart().xScale(new Date(treatment.mills))) + .attr('y1', chart().yScale(client.sbx.scaleEntry(treatment))) + .attr('x2', x) + .attr('y2', y) + .attr('stroke-width', 2) + .attr('stroke', 'white'); }) - .on('dragend', function() { + .on('end', function() { var newTreatment; chart().drag.selectAll('.drag-droparea').remove(); hideTooltip(); @@ -1009,8 +1021,9 @@ function init (client, d3) { var basalareadata = []; var tempbasalareadata = []; var comboareadata = []; - var from = chart().brush.extent()[0].getTime(); - var to = Math.max(chart().brush.extent()[1].getTime(), client.sbx.time) + client.forecastTime; + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = Math.max(selectedRange[1].getTime(), client.sbx.time) + client.forecastTime; var date = from; var lastbasal = 0; @@ -1069,16 +1082,16 @@ function init (client, d3) { chart().basals.selectAll('.tempbasalarea').remove().data(tempbasalareadata); chart().basals.selectAll('.comboarea').remove().data(comboareadata); - var valueline = d3.svg.line() - .interpolate('step-after') + var valueline = d3.line() .x(function(d) { return chart().xScaleBasals(d.d); }) - .y(function(d) { return chart().yScaleBasals(d.b); }); + .y(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() .x(function(d) { return chart().xScaleBasals(d.d); }) .y0(chart().yScaleBasals(0)) - .y1(function(d) { return chart().yScaleBasals(d.b); }); + .y1(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); var g = chart().basals.append('g'); @@ -1159,8 +1172,9 @@ function init (client, d3) { } // calculate position of profile on left side - var from = chart().brush.extent()[0].getTime(); - var to = chart().brush.extent()[1].getTime(); + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); var mult = (to - from) / times.hours(24).msecs; from += times.mins(20 * mult).msecs; diff --git a/lib/plugins/pluginbase.js b/lib/plugins/pluginbase.js index a6f0e77e455..4ae28f67ca2 100644 --- a/lib/plugins/pluginbase.js +++ b/lib/plugins/pluginbase.js @@ -86,7 +86,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { pill.mouseover(function pillMouseover (event) { tooltip.transition().duration(200).style('opacity', .9); - var windowWidth = $(tooltip).parent().parent().width(); + var windowWidth = $(tooltip.node()).parent().parent().width(); var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH - 10; tooltip.html(html) .style('left', left + 'px') diff --git a/lib/report_plugins/calibrations.js b/lib/report_plugins/calibrations.js index 958dd06c906..baf16a47a27 100644 --- a/lib/report_plugins/calibrations.js +++ b/lib/report_plugins/calibrations.js @@ -146,20 +146,16 @@ calibrations.report = function report_calibrations (datastorage, sorteddaystosho calibration_context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.scale.linear() + xScale2 = d3.scaleLinear() .domain([0, maxBG]); - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([0, 400000]); - var xAxis2 = d3.svg.axis() - .scale(xScale2) - .ticks(10) - .orient('bottom'); + var xAxis2 = d3.axisBottom(xScale2) + .ticks(10); - var yAxis2 = d3.svg.axis() - .scale(yScale2) - .orient('left'); + var yAxis2 = d3.axisLeft(yScale2); // get current data range var dataRange = [0, maxBG]; diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index b4b1d65167b..4eadc45ea90 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -170,37 +170,33 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.time.scale() + xScale2 = d3.scaleTime() .domain(d3.extent(data.sgv, dateFn)); if (options.scale === report_plugins.consts.SCALE_LOG) { - yScale2 = d3.scale.log() + yScale2 = d3.scaleLog() .domain([client.utils.scaleMgdl(options.basal ? 30 : 36), client.utils.scaleMgdl(420)]); } else { - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([client.utils.scaleMgdl(options.basal ? -40 : 36), client.utils.scaleMgdl(420)]); } // allow insulin to be negative (when plotting negative IOB) - yInsulinScale = d3.scale.linear() + yInsulinScale = d3.scaleLinear() .domain([-2 * options.maxInsulinValue, 2 * options.maxInsulinValue]); - yCarbsScale = d3.scale.linear() + yCarbsScale = d3.scaleLinear() .domain([0, options.maxCarbsValue * 1.25]); - yScaleBasals = d3.scale.linear(); + yScaleBasals = d3.scaleLinear(); - xAxis2 = d3.svg.axis() - .scale(xScale2) + xAxis2 = d3.axisBottom(xScale2) .tickFormat(timeTicks) - .ticks(24) - .orient('bottom'); + .ticks(24); - yAxis2 = d3.svg.axis() - .scale(yScale2) + yAxis2 = d3.axisLeft(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); // get current data range var dataRange = d3.extent(data.sgv, dateFn); @@ -602,13 +598,13 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio yScaleBasals.domain([basalMax, 0]); - var valueline = d3.svg.line() - .interpolate('step-after') + var valueline = d3.line() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y(function(d) { return yScaleBasals(d.b) + padding.top; }); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y0(yScaleBasals(0) + padding.top) .y1(function(d) { return yScaleBasals(d.b) + padding.top; }); @@ -931,9 +927,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var height = 120; var radius = Math.min(width, height) / 2; - var color = d3.scale.ordinal().range([basalcolor, boluscolor]); + var color = d3.scaleOrdinal().range([basalcolor, boluscolor]); - var labelArc = d3.svg.arc() + var labelArc = d3.arc() .outerRadius(radius / 2) .innerRadius(radius / 2); @@ -945,10 +941,11 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); - var arc = d3.svg.arc() + var arc = d3.arc() + .innerRadius(0) .outerRadius(radius); - var pie = d3.layout.pie() + var pie = d3.pie() .value(function(d) { return d.count; }) @@ -980,7 +977,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio // Carbs pie chart - var carbscolor = d3.scale.ordinal().range(['red']); + var carbscolor = d3.scaleOrdinal().range(['red']); var carbsData = [ { label: translate('Carbs'), count: data.dailyCarbs } @@ -994,10 +991,10 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); - var carbsarc = d3.svg.arc() + var carbsarc = d3.arc() .outerRadius(radius * data.dailyCarbs / options.maxDailyCarbsValue); - var carbspie = d3.layout.pie() + var carbspie = d3.pie() .value(function(d) { return d.count; }) diff --git a/lib/report_plugins/weektoweek.js b/lib/report_plugins/weektoweek.js index 3ff84a78639..449546bbbe2 100644 --- a/lib/report_plugins/weektoweek.js +++ b/lib/report_plugins/weektoweek.js @@ -196,28 +196,24 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.time.scale() + xScale2 = d3.scaleTime() .domain(d3.extent(sgvData, dateFn)); if (options.weekscale === report_plugins.consts.SCALE_LOG) { - yScale2 = d3.scale.log() + yScale2 = d3.scaleLog() .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); } else { - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); } - xAxis2 = d3.svg.axis() - .scale(xScale2) + xAxis2 = d3.axisBottom(xScale2) .tickFormat(timeTicks) - .ticks(24) - .orient('bottom'); + .ticks(24); - yAxis2 = d3.svg.axis() - .scale(yScale2) + yAxis2 = d3.axisLeft(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); // get current data range var dataRange = d3.extent(sgvData, dateFn); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index af216bf5250..b63bd4dbc14 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2272,7 +2272,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -2306,7 +2306,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", @@ -2352,7 +2352,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { "base64-js": "^1.0.2", @@ -3056,7 +3056,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -3068,7 +3068,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -3185,9 +3185,268 @@ "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" }, "d3": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", - "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.12.0.tgz", + "integrity": "sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } }, "dashdash": { "version": "1.14.1", @@ -3354,7 +3613,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -4434,8 +4693,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4453,13 +4711,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4472,18 +4728,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4586,8 +4839,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4597,7 +4849,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4610,20 +4861,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4640,7 +4888,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4713,8 +4960,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4724,7 +4970,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4800,8 +5045,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -4831,7 +5075,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4849,7 +5092,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4888,13 +5130,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -6115,7 +6355,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" @@ -6599,7 +6839,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "^1.1.7" } @@ -9162,7 +9402,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9515,6 +9755,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", @@ -9665,7 +9910,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -10541,7 +10786,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" diff --git a/package.json b/package.json index 98ea20cda1c..2a12a51f6c2 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "compression": "^1.7.4", "css-loader": "^1.0.1", "cssmin": "^0.4.3", - "d3": "^3.5.17", + "d3": "^5.12.0", "ejs": "^2.6.2", "errorhandler": "^1.5.1", "event-stream": "3.3.4", From 30b88f67650afa5482fad3f1024f995a2cf6e2d6 Mon Sep 17 00:00:00 2001 From: Caleb Date: Mon, 21 Oct 2019 05:09:49 -0700 Subject: [PATCH 063/134] fix/feat: Update Alexa integration and add Google Home support (#4980) * Copied work done by mdomox * Updates and fixes to CONTRIBUTING.md * Spacing unification * One more fix for CONTRIBUTING.md * Minor code formatting improvements * One more time... * Renamed Alexa stuff to virtAsst for generic-ness * Corrected missed translate() text * Updated googlehome plugin to mimic the alexa plugin * Changed order of operations * Fixed parameter referencing in googlehome * Yet another CONTRIBUTING fix * Removed extra google stuff * Migrated standalone intents to MetricNow intent * Simplified route handling * Added logging * Added forgotten path selector * Separated instructions for adding virtual assistant support in a plugin * A few typo fixes * Improved logging * Updated Google Home plugin instructions * Attempt to trigger download of template file * Small wording tweaks * Updated Alexa plugin documentation * Updated test files * Re-added handler count tests so devs are prompted to write tests for new handlers * Updated Alexa documentation * Small typo fix * Clarification * Further clarifications and typos * Added language info to Google Home plugin doc * URL correction * URL fix v2 * Wording clarification * Ugh... * Minor instruction fix * Sub steps fix * Fixed Alexa references in Google Home * Added a couple steps for improved user experience * One more forgotten step * Updated pump reservoir handler to handle undefined values * Updated titles and unknown-value responses * Modified forecast responses to use translate() * Updated tests * Improved training phrases * Wording improvements * Google Home setup instruction corrections * Corrected how metric selection is found --- CONTRIBUTING.md | 12 +- README.md | 4 + ...add-virtual-assistant-support-to-plugin.md | 52 +++ docs/plugins/alexa-plugin.md | 307 +++------------ docs/plugins/alexa-templates/en-us.json | 218 +++++++++++ docs/plugins/google-home-templates/en-us.zip | Bin 0 -> 13230 bytes docs/plugins/googlehome-plugin.md | 109 ++++++ .../interacting-with-virtual-assistants.md | 56 +++ lib/api/alexa/index.js | 279 +++++++------- lib/api/googlehome/index.js | 123 ++++++ lib/api/index.js | 4 + lib/language.js | 356 ++++++++++++------ lib/plugins/alexa.js | 57 ++- lib/plugins/ar2.js | 24 +- lib/plugins/basalprofile.js | 28 +- lib/plugins/cob.js | 9 +- lib/plugins/googlehome.js | 97 +++++ lib/plugins/iob.js | 23 +- lib/plugins/loop.js | 42 ++- lib/plugins/openaps.js | 35 +- lib/plugins/pump.js | 53 ++- lib/plugins/rawbg.js | 17 +- lib/plugins/upbat.js | 28 +- lib/server/bootevent.js | 4 + tests/ar2.test.js | 8 +- tests/basalprofileplugin.test.js | 10 +- tests/cob.test.js | 6 +- tests/iob.test.js | 10 +- tests/loop.test.js | 10 +- tests/openaps.test.js | 10 +- tests/pump.test.js | 26 +- tests/rawbg.test.js | 6 +- tests/upbat.test.js | 18 +- 33 files changed, 1342 insertions(+), 699 deletions(-) create mode 100644 docs/plugins/add-virtual-assistant-support-to-plugin.md create mode 100644 docs/plugins/alexa-templates/en-us.json create mode 100644 docs/plugins/google-home-templates/en-us.zip create mode 100644 docs/plugins/googlehome-plugin.md create mode 100644 docs/plugins/interacting-with-virtual-assistants.md create mode 100644 lib/api/googlehome/index.js create mode 100644 lib/plugins/googlehome.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c77a0df1c6a..f80a79f2a1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -202,13 +202,13 @@ Also if you can't code, it's possible to contribute by improving the documentati | Release coordination 0.11.x: | [@PieterGit] | | Issue/Pull request coordination: | Please volunteer | | Cleaning up git fork spam: | Please volunteer | -| Documentation writers: | [@andrew-warrington][@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | +| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | ### Plugin contributors | Contribution area | List of developers | List of testers | ------------------------------------- | -------------------- | -------------------- | -| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| Please volunteer | Please volunteer | +| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| [@inventor96] | Please volunteer | | [`ar2` (AR2 Forecasting)](README.md#ar2-ar2-forecasting)| Please volunteer | Please volunteer | | [`basal` (Basal Profile)](README.md#basal-basal-profile)| Please volunteer | Please volunteer | | [`boluscalc` (Bolus Wizard)](README.md#boluscalc-bolus-wizard)| Please volunteer | Please volunteer | @@ -223,7 +223,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | | [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | | [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | -| [`googlehome` (Google Home)](README.md#google-home) |[@mdomox] [@rickfriele] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`googlehome` (Google Home/DialogFlow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | | [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | | [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | | [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | @@ -232,9 +232,9 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`profile` (Treatment Profile)](README.md#profile-treatment-profile)| Please volunteer | Please volunteer | | [`pump` (Pump Monitoring)](README.md#pump-pump-monitoring)| Please volunteer | Please volunteer | | [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | -| [`sage` (Sensor Age)](README.md#sage-sensor-age)| @jpcunningh | Please volunteer | +| [`sage` (Sensor Age)](README.md#sage-sensor-age)| [@jpcunningh] | Please volunteer | | [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | -| [`speech` (Speech)](README.md#speech-speech) | [@sulkaharo] | Please volunteer | +| [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | | [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | | [`treatmentnotify` (Treatment Notifications)](README.md#treatmentnotify-treatment-notifications)| Please volunteer | Please volunteer | | [`upbat` (Uploader Battery)](README.md#upbat-uploader-battery)| [@jpcunningh] | Please volunteer | @@ -251,7 +251,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Čeština (`cs`) |Please volunteer|OK | | Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK | | Dansk (`dk`) | [@janrpn] |OK | -| Ελληνικά `(el`)|Please volunteer|Needs attention: 68.5%| +| Ελληνικά (`el`)|Please volunteer|Needs attention: 68.5%| | English (`en`)|Please volunteer|OK| | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| diff --git a/README.md b/README.md index 8ad79a39d72..91a0b72dbc4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Community maintained fork of the - [`override` (Override Mode)](#override-override-mode) - [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) @@ -518,6 +519,9 @@ For remote overrides, the following extended settings must be configured: ##### `alexa` (Amazon Alexa) Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) +##### `googlehome` (Google Home/DialogFLow) + Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md) + ##### `speech` (Speech) Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms. diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md new file mode 100644 index 00000000000..764cfc7c4ea --- /dev/null +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -0,0 +1,52 @@ +Adding Virtual Assistant Support to a Plugin +========================================= + +To add virtual assistant support to a plugin, the `init` method of the plugin should return an object that contains a `virtAsst` key. Here is an example: + +```javascript +iob.virtAsst = { + intentHandlers: [{ + intent: "MetricNow" + , metrics: ["iob"] + , intentHandler: virtAsstIOBIntentHandler + }] + , rollupHandlers: [{ + rollupGroup: "Status" + , rollupName: "current iob" + , rollupHandler: virtAsstIOBRollupHandler + }] +}; +``` + +There are 2 types of handlers that you will need to supply: +* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### Intent Handlers + +A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). Each intent handler should be structured as follows: ++ `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) ++ `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Make sure to add the metric name and its synonyms to the list of metrics used by the virtual assistant(s). + - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! + - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. We keep this value as an array for backwards compatibility. ++ `intenthandler` - This is a callback function that receives 3 arguments: + - `callback` Call this at the end of your function. It requires 2 arguments: + - `title` - Title of the handler. This is the value that will be displayed on the Alexa card (for devices with a screen). The Google Home response doesn't currently display a card, so it doesn't use this value. + - `text` - This is text that the virtual assistant should speak (and show, for devices with a screen). + - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected (e.g. `pwd` as seen in the templates is a slot/parameter. `metric` is technically a slot, too). + - `sandbox` - This is the Nightscout sandbox that allows access to various functions. + +### Rollup handlers + +A plugin can also expose multiple rollup handlers ++ `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked ++ `rollupName` - This is the name of the handler. Primarily used for debugging ++ `rollupHandler` - This is a callback function that receives 3 arguments + - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot + - `sandbox` - This is the nightscout sandbox that allows access to various functions. + - `callback` - + - `error` - This would be an error message + - `response` - A simple object that expects a `results` string and a `priority` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: + ```javascript + callback(null, {results: "Hello world", priority: 1}); + ``` diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index a5dcb886e9c..8ba8188143e 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -41,9 +41,9 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p ### Get an Amazon Developer account -- Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. -- [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. -- Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). +1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. +1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. +1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). ### Create a new Alexa skill @@ -58,164 +58,11 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p Your Alexa skill's "interaction model" defines how your spoken questions get translated into requests to your Nightscout site, and how your Nightscout site's responses get translated into the audio responses that Alexa says back to you. -To get up and running with a basic interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code below. - -```json -{ - "interactionModel": { - "languageModel": { - "invocationName": "nightscout", - "intents": [ - { - "name": "NSStatus", - "slots": [], - "samples": [ - "How am I doing" - ] - }, - { - "name": "UploaderBattery", - "slots": [], - "samples": [ - "How is my uploader battery" - ] - }, - { - "name": "PumpBattery", - "slots": [], - "samples": [ - "How is my pump battery" - ] - }, - { - "name": "LastLoop", - "slots": [], - "samples": [ - "When was my last loop" - ] - }, - { - "name": "MetricNow", - "slots": [ - { - "name": "metric", - "type": "LIST_OF_METRICS" - }, - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "What is my {metric}", - "What my {metric} is", - "What is {pwd} {metric}" - ] - }, - { - "name": "InsulinRemaining", - "slots": [ - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "How much insulin do I have left", - "How much insulin do I have remaining", - "How much insulin does {pwd} have left", - "How much insulin does {pwd} have remaining" - ] - } - ], - "types": [ - { - "name": "LIST_OF_METRICS", - "values": [ - { - "name": { - "value": "bg" - } - }, - { - "name": { - "value": "blood glucose" - } - }, - { - "name": { - "value": "number" - } - }, - { - "name": { - "value": "iob" - } - }, - { - "name": { - "value": "insulin on board" - } - }, - { - "name": { - "value": "current basal" - } - }, - { - "name": { - "value": "basal" - } - }, - { - "name": { - "value": "cob" - } - }, - { - "name": { - "value": "carbs on board" - } - }, - { - "name": { - "value": "carbohydrates on board" - } - }, - { - "name": { - "value": "loop forecast" - } - }, - { - "name": { - "value": "ar2 forecast" - } - }, - { - "name": { - "value": "forecast" - } - }, - { - "name": { - "value": "raw bg" - } - }, - { - "name": { - "value": "raw blood glucose" - } - } - ] - } - ] - } - } -} -``` - -Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. +To get up and running with an interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code for your language from [the list of templates](alexa-templates/). + +- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. + +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". @@ -242,108 +89,50 @@ After you enable testing, you can also use the Alexa Simulator in the left colum ##### What questions can you ask it? -*Forecast:* - -- "Alexa, ask Nightscout how am I doing" -- "Alexa, ask Nightscout how I'm doing" - -*Uploader Battery:* - -- "Alexa, ask Nightscout how is my uploader battery" - -*Pump Battery:* - -- "Alexa, ask Nightscout how is my pump battery" - -*Metrics:* - -- "Alexa, ask Nightscout what my bg is" -- "Alexa, ask Nightscout what my blood glucose is" -- "Alexa, ask Nightscout what my number is" -- "Alexa, ask Nightscout what is my insulin on board" -- "Alexa, ask Nightscout what is my basal" -- "Alexa, ask Nightscout what is my current basal" -- "Alexa, ask Nightscout what is my cob" -- "Alexa, ask Nightscout what is Charlie's carbs on board" -- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" -- "Alexa, ask Nightscout what is Harper's loop forecast" -- "Alexa, ask Nightscout what is Alicia's ar2 forecast" -- "Alexa, ask Nightscout what is Peter's forecast" -- "Alexa, ask Nightscout what is Arden's raw bg" -- "Alexa, ask Nightscout what is Dana's raw blood glucose" - -*Insulin Remaining:* - -- "Alexa, ask Nightscout how much insulin do I have left" -- "Alexa, ask Nightscout how much insulin do I have remaining" -- "Alexa, ask Nightscout how much insulin does Dana have left? -- "Alexa, ask Nightscout how much insulin does Arden have remaining? - -*Last Loop:* - -- "Alexa, ask Nightscout when was my last loop" - -(Note: all the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name.) +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. ### Activate the skill on your Echo or other device If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. + +1. Open the Build tab of your Alexa Skill. + - Get to your list of Alexa Skills at https://developer.amazon.com/alexa/console/ask and click on the name of the skill. +1. Click on the language drop-down box in the upper right corner of the window. +1. Click "Language settings". +1. Add your desired language. +1. Click the "Save" button. +1. Navigate to "CUSTOM" in the left navigation pane. +1. Select your new language in the language drop-down box. +1. Go to "JSON Editor" (just above "Interfaces" in the left navigation pane). +1. Remove the existing contents in the text box, and copy and paste the configuration code from a familiar language in [the list of templates](alexa-templates/). +1. Click "Save Model". +1. Click the "Add" button next to the "Slot Types" section in the left pane. +1. Click the radio button for "Use an existing slot type from Alexa's built-in library" +1. In the search box just below that option, search for "first name" +1. If your language has an option, click the "Add Slot Type" button for that option. + - If your language doesn't have an option, you won't be able to ask Nightscout a question that includes a name. +1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): + 1. Click on the Intent name. + 1. Scroll down to the "Slots" section + 1. If there's a slot with the name "pwd", change the Slot Type to the one found above. + - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. + 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. + 1. Set the "Alexa speech prompts" in your language, and remove the old ones. + 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. + 1. Click on the Intent name (just to the left of "metric") to return to the previous screen. + 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. +1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. +1. For each metric listed, add synonyms in your language, and delete the old synonyms. + - What ever you do, **DO NOT** change the text in the "VALUE" column! Nightscout will be looking for these exact values. Only change the synonyms. +1. Click "Save Model" at the top, and then click on "Build Model". +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. + ## Adding Alexa support to a plugin -This document assumes some familiarity with the Alexa interface. You can find more information [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide). - -To add alexa support to a plugin the ``init`` should return an object that contains an "alexa" key. Here is an example: - -```javascript -var iob = { - name: 'iob' - , label: 'Insulin-on-Board' - , pluginType: 'pill-major' - , alexa : { - rollupHandlers: [{ - rollupGroup: "Status" - , rollupName: "current iob" - , rollupHandler: alexaIOBRollupHandler - }] - , intentHandlers: [{ - intent: "MetricNow" - , routableSlot: "metric" - , slots: ["iob", "insulin on board"] - , intentHandler: alexaIOBIntentHandler - }] - } -}; -``` - -There are 2 types of handlers that you will need to supply: -* Intent handler - enables you to "teach" Alexa how to respond to a user's question. -* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the Alexa "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. - -### Intent Handlers - -A plugin can expose multiple intent handlers. -+ ``intent`` - this is the intent in the "intent schema" above -+ ``routeableSlot`` - This enables routing by a slot name to the appropriate intent handler for overloaded intents e.g. "What is my " - iob, bg, cob, etc. This value should match the slot named in the "intent schema" -+ ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot -+ ``intenthandler`` - this is a callback function that receives 3 arguments - - ``callback`` Call this at the end of your function. It requires 2 arguments - - ``title`` - Title of the handler. This is the value that will be displayed on the Alexa card - - ``text`` - This is text that Alexa should speak. - - ``slots`` - these are the slots that Alexa detected - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - -### Rollup handlers - -A plugin can also expose multiple rollup handlers -+ ``rollupGroup`` - This is the key that is used to aggregate the responses when the intent is invoked -+ ``rollupName`` - This is the name of the handler. Primarily used for debugging -+ ``rollupHandler`` - this is a callback function that receives 3 arguments - - ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - - ``callback`` - - - ``error`` - This would be an error message - - ``response`` - A simple object that expects a ``results`` string and a ``priority`` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: - ```javascript - callback(null, {results: "Hello world", priority: 1}); - ``` +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json new file mode 100644 index 00000000000..cf90a710b88 --- /dev/null +++ b/docs/plugins/alexa-templates/en-us.json @@ -0,0 +1,218 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "nightscout", + "intents": [ + { + "name": "NSStatus", + "slots": [], + "samples": [ + "How am I doing" + ] + }, + { + "name": "LastLoop", + "slots": [], + "samples": [ + "When was my last loop" + ] + }, + { + "name": "MetricNow", + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "samples": [ + "what {pwd} {metric} is", + "what my {metric} is", + "how {pwd} {metric} is", + "how my {metric} is", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "how much {metric}", + "{pwd} {metric}", + "{metric}", + "my {metric}" + ] + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "how much {metric} does {pwd} have left", + "what's {metric}", + "what's my {metric}", + "how much {metric} is left", + "what's {pwd} {metric}", + "how much {metric}", + "how is {metric}", + "how is my {metric}", + "how is {pwd} {metric}", + "how my {metric} is", + "what is {metric}", + "how much {metric} do I have", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "what is my {metric}", + "what my {metric} is", + "what is {pwd} {metric}" + ] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + } + ], + "types": [ + { + "name": "LIST_OF_METRICS", + "values": [ + { + "name": { + "value": "uploader battery", + "synonyms": [ + "uploader battery remaining", + "uploader battery power" + ] + } + }, + { + "name": { + "value": "pump reservoir", + "synonyms": [ + "remaining insulin", + "insulin remaining", + "insulin is left", + "insulin left", + "insulin in my pump", + "insulin" + ] + } + }, + { + "name": { + "value": "pump battery", + "synonyms": [ + "pump battery remaining", + "pump battery power" + ] + } + }, + { + "name": { + "value": "bg", + "synonyms": [ + "number", + "blood sugar", + "blood glucose" + ] + } + }, + { + "name": { + "value": "iob", + "synonyms": [ + "insulin on board" + ] + } + }, + { + "name": { + "value": "basal", + "synonyms": [ + "current basil", + "basil", + "current basal" + ] + } + }, + { + "name": { + "value": "cob", + "synonyms": [ + "carbs", + "carbs on board", + "carboydrates", + "carbohydrates on board" + ] + } + }, + { + "name": { + "value": "forecast", + "synonyms": [ + "ar2 forecast", + "loop forecast" + ] + } + }, + { + "name": { + "value": "raw bg", + "synonyms": [ + "raw number", + "raw blood sugar", + "raw blood glucose" + ] + } + } + ] + } + ] + }, + "dialog": { + "intents": [ + { + "name": "MetricNow", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.1421281086569.34001419564" + } + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME", + "confirmationRequired": false, + "elicitationRequired": false, + "prompts": {} + } + ] + } + ], + "delegationStrategy": "ALWAYS" + }, + "prompts": [ + { + "id": "Elicit.Slot.1421281086569.34001419564", + "variations": [ + { + "type": "PlainText", + "value": "What metric are you looking for?" + }, + { + "type": "PlainText", + "value": "What value are you looking for?" + }, + { + "type": "PlainText", + "value": "What metric do you want to know?" + }, + { + "type": "PlainText", + "value": "What value do you want to know?" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip new file mode 100644 index 0000000000000000000000000000000000000000..6a8498b0b19e0c1822efaf4652f737cf282b350d GIT binary patch literal 13230 zcmdT~cRUq(*f)>8S4KtID?6jCNYX*(kvP_|M;Y1U7{>_7jAR~?%n~8lB70TMv4Zp<#BR2 zqX{BJTfp(r>qLC7pi`o$BF<2v;UYfN`_7Q;o#B;J2dg{U*+X^@&;D_E{vN+=jWntJ z57AfLPH&HJHjmrl{bkMXs zt>dJzNXHx4Oz{}ZC@F?OQ$go%nU~BDt$XAS(51GEg_cHb0J}qlsW;ONyDn^9!DI3# zopfuVO0rw0l@0Q4ERbXt?|v{O3hsEv%+?j$=J-LRfyz-4M|}J5_h}Go$xu6yTkO;y zUid`6kIIYAR}fYqvN9FJ4}MrjfCH^LLkM1^kDI>Y zYJY`v=&LUf>eDlB939QksrdA!kpko35(HI=3L^k;L45ydE@=AzAl)+;LoSd&OXx>}>on=T~havwF_XHn>6F3u_k{ z%padxvB-!Yh!-=nZFgT^Czc0NDiKB1h>bcY`pGuR)X~;~E~#n=R7AdR@iGg$69H#% znw6rMm2P9UXkzaswNE7Q!@o2!A=?ljiIG=m^F&CxSEG{{FxQ#tKq~FH8zzc4qq8i$iJ6V41bl<9ecJLKlLD0IHSVXI={Y8-lue#UD zmkSh|`M=ruEv|b&7=nMr zH-v1u#hN-RdvnQW;_~a2TM!eflU&AXp2HtKDqdFH%zvNU=J#%SP2@)0()e}=oLESR~hvhEGilUgNVy*?QfL0*&7w#G%IrRa_}ev6&1M^HHmRy z*%fGYB7}r#xK05X!ScbbEb*?72xkZAjbuWSE{zyrbq1^f0MxMFOb%)Q;61fzZyT-r z_tHnb06?a4Y-V0wSzdS>pp2Ee&9*F-x1piTwxOW`77J*AwV7rTZU>sF^QO`Th#*6V zjtay?!Oyvjd@s=4Ff_J8Ag~u|au8wWz3xuPef(ZcRh3hThqJAvUHyrgsz8rmhoI_Y zHug5B^s{*`bO=s}kd6}4_H#OZ801GWJp%MojDXMie5BY67i4`?u0({>N?ZG7`jav7 zCL|_5jsvMiL}hD(53aTJh@v?*OH;i=W}nLuPzFWi^|HrLe0CWKS5j*2RwddS7(*Y43EuS3Fn5!-9RLdG*r zmm|XaDZ?o%iIsV}@rth=51zrN^j?h00pm2zFO{?|>J!~y7r&#Yo9mqYK43-fbCTOk zM)zBxGnjqNQ*j;G%BFJQlP5IYhjll?hs&8>b~jA-%wHrsC5Zn0*+7Lzz!>^>v-09u zi84>Rm*>VzafqG$FqeAuJ3n`>(I5>3q*WW^g>W=pMC3tc^~38Enu9K|-%<|NDf=x& z=+{Je!GFVO{>AQfxQnnIa1mv7bw@)-CkL>T1H|6J(A5D9u|3p5gv0qseECQNoW1HZ zo&qnYvbPbJm6O4`PGsUyp0W|*-*O(h25WxzWpsVi3A>7o5RE?}5o-Rn>`_L_DoWb|6RxWJSATp*+5~SgnwX0*qBBILBk+DJB?zZnMa#N z@g*NfXU~Fm5y0E1eC%|6@2%+KtmE?CvLL}+Qm;<&TcQA?Dv@hCxQ*ztc@txbK9ARf zeJ|5@VZ~5m^I^SoQ>N{K(K$0=E!9~S#KxvuPK6la=+oIT$A_oFvzI?f%aaU$pTG#v z()WnA#;oFOKRad<311K{9K6>YYK@zoZG4Ga8mLNv8Ler;^%|e2n}>YrLfWz`HIap# zr{lNU)Rf#hy|H=~#lnuvuUnt;($r?vWTk>mjKLUcfh4b*ykl%d!^j44Ux?20dqqiqGp?J&mL5lf6uqAQH-Bf47B0!e+RRQu5N`RO7S`VOWO!SudtnP4n9( zibFsetpEFB_17-q9EY;j?OB|*`S6De)qK;hLhK{#z{nb8FB1I+9NE;FC6#e`cq|R=Yj7QL1#OH)Z6c zPLD#R=a~M(DG;+G1}(&p{M92}Q|`nIlkDvSCgKm?@n2NDF<8>Qnma+KbEhBO*kEF* z_-m;P_`3;V*tcN1+7r@3Cp-jUz1K9K9slCm*2IM$0hz^qS5?hd7bgswRKXI8%(Vtf@mg|PiVoH=2%XpYSt zB`3VtynL{x?(4F`{6~)(#B)g){5ciYo2+gw-K8(LtHbPS9=hZXG>BDxNb8RWBq*W0 zdGA8Be^KL8(fh5F@+T_#_5H%Xji*$$^d`n^phKq1EsNS`h}<7FdHHTVgfHBB6|B92 z5e5#L|3Xc)QK|Z79&S2p&!T$+H=gcpp6>TARuu9#waC(oMw9Ncq%umxvj7S$XZ25a z2$xR28>E}LL@{C;JPUg|PUCTmx&zn!sr$)1Y|vSj?AWopnyQ?d_`|@-2dkGkWp5-GU2p~1ve~D6Q*sXL zB4%`48Sq{t4#vLO?Y*wWrXsanmAWr9Y-k{y((R{KXMeqbOq=_LBD>L)7ksqg{^vaN zV2N5d?aC&m-ldvx_2TL z5+83@+=?dYh1Q>?gPEfSI=OWDvpSgidL+B6-gHRwQp!@I$(_puwNj~_sqICd2gR=5 zT(*baD(@y#DdMZBPH{4bg#bUB4|AXA6$%v$_=+Bu!R$5lN$rh!zECdU_?FZ|GU0Dx}$9j=KyX`t*BP7c# zL|pPUqE5iW15$J=hh7*Iw~?pQ4witC{_s>BjFBaYS$Yxtps&rT4y0;YO;6wtdQm5M zBJi;Zb-Ay$0p(=4Yrk&RL?0SY|R5fCDCX7Vrpw5L9 zrTI{~DW>S_@7j)MjYD;%bQD=Q*(5J#0xl7}Dj5S(0^_IzvsNHA#oX_*A$5e(^|F&|8=}&B!)%1$%dACnSMoH_5c{^*%gx^z|26Rs7CZ|_^ zoDf~h?;?KK%Q6$mtEDUVX=#~I5$dug5J6-%G(j|@5?sWQ!R^_-R){SUiKn8XE5R>M zLcReKaG~}zE*&NTH{Nty$3zyhBCvoZ&Hr7caHQh?bB4)zpisTy+vnvi>JQz_6eUZ~8Y4{}$WgTS-HU2p zRi=#%F$}((%Rf?`ZrU{*Szyu~(H~3M`^duAdqB89UsIa9w8mTk-4e~m)1`!IvOeN& zs=W-W96!XO$eGdU>X)1z2^-GiME>0A(Td!opJ>yt2Q$%F*8%NKV#A?xp*+}tzJN35 zl0szy+@>lz3>e3O)v_nTl3>?mqHR^a;;o*>@k+gf5#hvzkJB{J~S?n6y z+STu|A-VDj3-7dPxIDYCN(-^LmmGpgls`_7U8npmhrWDrWvkuw>)W}4i*Gf5cuv2B z&2kOedVDOK(P1)cc)umSKyu#dxm#{*pT$sNK^LgWi9*Tf2UbzqO7FdqI}M}HN$#w6 z^=m_1IAS7Gi2xUSfdRJcp%>9NEqpo|rzNo8G`q0oSsC*{ewYGGot!&fC%R6OJr#5hvBkroEJKn(nI5O@Kybt)UI!8A({E zE?35V5SQ-GlsPW?T(r&LnAx>xIY~*$(E-Ii&p4Gn;F`GW81Onf{4Te799d51iADk6 zDeHvO8bq6Km-|94b43c9-G3+{Mc*7!nG3M%zJ1^JC2zNFxry=hGv8-Umv~jzhKHFd zjna81ZVkOj)YkU=K0?y!c1_Yc5)6w1N^aPcRj$uiZc5#EUh$1{e_rSNeq)@9*KWq| z19+5o%yT~972TWHbMSnoW9vxh(2Uf=lkbva@5{kn@4vqb)$;gOlAC%xhsMwQnWsJ& zUr3~_r|GNoH)+@#O^6bP%{9zfHQylLq8X*zVsb|GcSub(?KgT%;1TI&ieV8xt-==s zD5YZ(Id$vi2DyN(OIXo4Y1U6La;JP&#+mSAWLayKXR8Y5NIULxI%oy=0LD%buk9G|SiGIQnW> zu~7lm95OX;PenD?@F?7i?}HC0)N7}kKgCm9E(~G~Wq)z42~#afvOepDvO;ivoa#m_ z#(UPTC&GD*M(WEM+E`6#HvOUinp;**wG8&SrLL?3D~dO3@jJDcx^u|i%91e%NMv8c z9;KQKOszYSd_|$&1y|9S4>!l$?3CEP*Wo>^P50fM@*hKGf>g@tNk0yeI< zk>#*ledzZkHB&5p(rI|b2IBoTN?ny-y$e8>0NuKbo;@JVNhj8u)j!7<@hP{5SUB-b z8k(a^rJ}WoxzU-H3?7$vozx;4FX+-GDzEnSH(t8N^(GC6;;P&;OIf{A>=g4Lhbpb+ zyhc&P`)jrA1@~nT%gL8+hEfUwidJ`_{B@yuL41S~L6zn9=ObRsvB23rNTDNhYc;6g z(F~&4S7$gXk_a9ZNq*n8{`GNW|LFbLLAiOsf3!Kf@9eun_?`aIH zYK;VW9c(iD^K#kPIWuGxGv8(tFL#*C-w!B(#+1@T7dOYoi3o9f}({zDt z0=>)%B326b4JL_(79a6Q=v8hzD2GQDEjJ?7L$Hr}0yxA__w=%BsDJ$iz<1Ex_o(oK ze^0;vVmCN^{*PSH*qH|J$+{!yUXwSDV!oO!&B%T}V8Syw&BUh4lIYR;b1Dx%3$tix zUKW~Msxdvkkh0A!F^9)92^J{ZAS}u51`tWZ(X?s^67he=_YzSkiBB~?_|QS}Ok=8+$9>)&U9`rd)cU>=%4vH#k; zGvogw!%3h#LV|z93w193v*FyO`js$$O9ek0jug#r2=JHmQAd}YKN$`uV$p9WC+r5C z-QafkRQvagUl@f49C{V8Q20yl{ip0uXDI9_?B=0L^!r8HGu3}=Re~rS;L!Ugju7bY zGeefis7`q7CuUJ64=_U+hwPLVebaUZoAW<>U#vm-Ja~=EM@=Q|AYp$ z6GSZu5h?NX0gQK`+4I%i26ulUg4#-jPC{MIpJ@675hzR!OQ#*oZn34UHM`aY5vGMmFSs{{zrEGOqvt literal 0 HcmV?d00001 diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md new file mode 100644 index 00000000000..c5dff113ab1 --- /dev/null +++ b/docs/plugins/googlehome-plugin.md @@ -0,0 +1,109 @@ +Nightscout Google Home/DialogFlow Plugin +======================================== + +## Overview + +To add Google Home support for your Nightscout site, here's what you need to do: + +1. [Activate the `googlehome` plugin](#activate-the-nightscout-google-home-plugin) on your Nightscout site, so your site will respond correctly to Google's requests. +1. [Create a custom DialogFlow agent](#create-your-dialogflow-agent) that points at your site and defines certain questions you want to be able to ask. + +## Activate the Nightscout Google Home Plugin + +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 0.13 (VERSION_NAME)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/0.13) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) + +## Create Your DialogFlow Agent + +1. Download the agent template in your language for Google Home [here](google-home-templates/). + - If you're language doesn't have a template, please consider starting with [the en-us template](google-home-templates/en-us.zip), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. +1. [Sign in to Google's Action Console](https://console.actions.google.com) + - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. +1. Click on the "New Project" button. +1. If prompted, agree to the Terms of Service. +1. Give your project a name (e.g. "Nightscout") and then click "Create project". +1. For the "development experience", select "Conversational" at the bottom of the list. +1. Click on the "Develop" tab at the top of the sreen. +1. Click on "Invocation" in the left navigation pane. +1. Set the display name (e.g. "Night Scout") of your Action and set your Google Assistant voice. + - Unfortunately, the Action name needs to be two words, and is required to be unique across all of Google, even though you won't be publishing this for everyone on Google to use. So you'll have to be creative with the name since "Night Scout" is already taken. +1. Click "Save" in the upper right corner. +1. Navigate to "Actions" in the left nagivation pane, then click on the "Add your first action" button. +1. Make sure you're on "Cutom intent" and then click "Build" to open DialogFlow in a new tab. +1. Sign in with the same Google account you used to sign in to the Actions Console. + - You'll have to go through the account setup steps if this is your first time using DialogFlow. +1. Verify the name for your agent (e.g. "Nightscout") and click "CREATE". +1. In the navigation pane on the left, click the gear icon next to your agent name. +1. Click on the "Export and Import" tab in the main area of the page. +1. Click the "IMPORT FROM ZIP" button. +1. Select the template file downloaded in step 1. +1. Type "IMPORT" where requested and then click the "IMPORT" button. +1. After the import finishes, click the "DONE" button followed by the "SAVE" button. +1. In the navigation pane on the left, click on "Fulfillment". +1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` +1. Scroll down to the bottom of the page and click the "SAVE" button. +1. Click on "Integrations" in the navigation pane. +1. Click on "INTEGRATION SETTINGS" for "Google Assistant". +1. Under "Implicit invocation", add every intent listed. +1. Turn on the toggle for "Auto-preview changes". +1. Click "CLOSE". + +That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I doing?" + +### What questions can you ask it? + +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". + +1. Open your DialogFlow agent. + - Get to your list of agents at https://console.dialogflow.com/api-client/#/agents and click on the name of your Nightscout agent. +1. Click on the "Languages" tab. +1. Click the "Add Additional Language" drop-down box. +1. Select your desired language. +1. Click the "SAVE" button. + - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". +1. Click on "Intents" in the left navigation pane. +1. For each intent in the list (NOT including those that start with "Default" in the name): + 1. Click on the intent name. + 1. Note the phrases used in the "Training phrases" section. + - If the phrase has a colored block (e.g. `metric` or `pwd`), click the phrase (but NOT the colored block) and note the "PARAMETER NAME" of the item with the same-colored "ENTITY". + 1. Click on the new language code (beneath the agent name near the top of the navigation pane). + 1. Add equivalent or similar training phrases as those you noted a couple steps ago. + - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: + 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. + 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. + 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the one you determined just a few steps ago. + 1. Press the Enter key to add the phrase. + 1. Click the "SAVE" button. + 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. + 1. Scroll down to the "Action and parameters" section. + 1. If any of the items in that list have the "REQUIRED" option checked: + 1. Click the "Define prompts..." link on the right side of that item. + 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). + 1. Click "CLOSE". + 1. Scroll down to the "Responses" section. + 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. + 1. Click the "SAVE" button at the top of the window. +1. Click on the "Entities" section in the navigation pane. +1. For each entity listed: + 1. Click the entity name. + 1. Switch to the starting language (beneath the agent name near the top of the left navigation pane). + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. Select all the text in the text box and copy it. + 1. Switch back to your new language. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. In the text box, paste the text you just copied. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to editor mode". + 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. + - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. + 1. Click the "SAVE" button. +1. You should be good to go! Feel free to try it out by click the "See how it works in Google Assistant" link in the right navigation pane, or start asking your Google-Home-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +## Adding Google Home support to a plugin + +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md new file mode 100644 index 00000000000..a9f2541d8d8 --- /dev/null +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -0,0 +1,56 @@ +Interacting with Virtual Assistants +=================================== + +# Alexa vs. Google Home + +Although these example phrases reference Alexa, the exact same questions could be asked of Google. +Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's name* ..." + +# What questions can you ask it? + +This list is not meant to be comprehensive, nor does it include every way you can ask the questions. To get the full picture, in the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. + +*Forecast:* + +- "Alexa, ask Nightscout how am I doing" +- "Alexa, ask Nightscout how I'm doing" + +*Uploader Battery:* + +- "Alexa, ask Nightscout how is my uploader battery" + +*Pump Battery:* + +- "Alexa, ask Nightscout how is my pump battery" + +*Metrics:* + +- "Alexa, ask Nightscout what my bg is" +- "Alexa, ask Nightscout what my blood glucose is" +- "Alexa, ask Nightscout what my number is" +- "Alexa, ask Nightscout what is my insulin on board" +- "Alexa, ask Nightscout what is my basal" +- "Alexa, ask Nightscout what is my current basal" +- "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is Charlie's carbs on board" +- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" +- "Alexa, ask Nightscout what is Harper's loop forecast" +- "Alexa, ask Nightscout what is Alicia's ar2 forecast" +- "Alexa, ask Nightscout what is Peter's forecast" +- "Alexa, ask Nightscout what is Arden's raw bg" +- "Alexa, ask Nightscout what is Dana's raw blood glucose" + +*Insulin Remaining:* + +- "Alexa, ask Nightscout how much insulin do I have left" +- "Alexa, ask Nightscout how much insulin do I have remaining" +- "Alexa, ask Nightscout how much insulin does Dana have left? +- "Alexa, ask Nightscout how much insulin does Arden have remaining? + +*Last Loop:* + +- "Alexa, ask Nightscout when was my last loop" + +## A note about names + +All the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name. \ No newline at end of file diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 65f477ad85d..f5a55c214de 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -4,156 +4,163 @@ var moment = require('moment'); var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; - var express = require('express') - , api = express.Router( ); - var translate = ctx.language.translate; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.alexa) { - if (plugin.alexa.intentHandlers) { - console.log(plugin.name + ' is Alexa enabled'); - _each(plugin.alexa.intentHandlers, function (route) { - if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); - } - }); - } - if (plugin.alexa.rollupHandlers) { - console.log(plugin.name + ' is Alexa rollup enabled'); - _each(plugin.alexa.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); - } - }); - - api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Alexa'); - var locale = req.body.request.locale; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - switch (req.body.request.type) { - case 'IntentRequest': - onIntent(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'LaunchRequest': - onLaunch(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'SessionEndedRequest': - onSessionEnded(req.body.request.intent, function (alexaResponse) { - res.json(alexaResponse); - next( ); - }); - break; - } - }); - - ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)) + '.'; - callback(null, {results: status, priority: -1}); + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.plugins.eachEnabledPlugin(function each(plugin){ + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Alexa: Plugin ' + plugin.name + ' supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.metrics); + } }); - // console.log('BG results called'); - // callback(null, 'BG results'); - }, 'BG Status'); - - ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); - callback('Current blood glucose', status); + } + if (plugin.virtAsst.rollupHandlers) { + console.log('Alexa: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + console.log('Route'); + console.log(route); + if (route) { + ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } }); - }, 'metric', ['bg', 'blood glucose', 'number']); + } + } else { + console.log('Alexa: Plugin ' + plugin.name + ' does not support Virtual Assistants'); + } + }); + + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = req.body.request.locale; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } - ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + switch (req.body.request.type) { + case 'IntentRequest': + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'LaunchRequest': + onLaunch(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'SessionEndedRequest': + onSessionEnded(req.body.request.intent, function (alexaResponse) { + res.json(alexaResponse); + next( ); }); + break; + } + }); + + ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + ctx.alexa.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback('Current blood glucose', status); }); + }, ['bg', 'blood glucose', 'number']); + ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { + callback('Full status', status); + }); + }); - function onLaunch() { - console.log('Session launched'); - } - function onIntent(intent, next) { - console.log('Received intent request'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); - } + function onLaunch() { + console.log('Session launched'); + } + + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } - function onSessionEnded() { - console.log('Session ended'); + function onSessionEnded() { + console.log('Session ended'); + } + + function handleIntent(intentName, slots, next) { + if (slots.metric.resolutions.resolutionsPerAuthority[0].status.code != "ER_SUCCESS_MATCH"){ + next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); } - function handleIntent(intentName, slots, next) { - var handler = ctx.alexa.getIntentHandler(intentName, slots); - if (handler){ - var sbx = initializeSandbox(); - handler(next, slots, sbx); - } else { - next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); - } + var metricValues = slots.metric.resolutions.resolutionsPerAuthority[0].values; + if (metricValues.length == 0){ + next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); } - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; + var handler = ctx.alexa.getIntentHandler(intentName, metricValues[0].value.name); + if (handler){ + var sbx = initializeSandbox(); + handler(next, slots, sbx); + } else { + next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); } + } + + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - return api; + return api; } -module.exports = configure; +module.exports = configure; \ No newline at end of file diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js new file mode 100644 index 00000000000..b03ac42bfe3 --- /dev/null +++ b/lib/api/googlehome/index.js @@ -0,0 +1,123 @@ +'use strict'; + +var moment = require('moment'); +var _each = require('lodash/each'); + +function configure (app, wares, ctx, env) { + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.plugins.eachEnabledPlugin(function each(plugin){ + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Google Home: Plugin ' + plugin.name + ' supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.metrics); + } + }); + } + if (plugin.virtAsst.rollupHandlers) { + console.log('Google Home: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + console.log('Route'); + console.log(route); + if (route) { + ctx.googleHome.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } + }); + } + } else { + console.log('Google Home: Plugin ' + plugin.name + ' does not support Virtual Assistants'); + } + }); + + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = req.body.queryResult.languageCode; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric); + if (handler){ + var sbx = initializeSandbox(); + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); + next( ); + } + }); + + ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + ctx.googleHome.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback('Current blood glucose', status); + }); + }, ['bg', 'blood glucose', 'number']); + + ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { + callback('Full status', status); + }); + }); + + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } + + return api; +} + +module.exports = configure; \ No newline at end of file diff --git a/lib/api/index.js b/lib/api/index.js index f92dda9cfbd..4b3d6a4fcb6 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -65,6 +65,10 @@ function create (env, ctx) { app.all('/alexa*', require('./alexa/')(app, wares, ctx, env)); } + if (ctx.googleHome) { + app.all('/googlehome*', require('./googlehome/')(app, wares, ctx, env)); + } + return app; } diff --git a/lib/language.js b/lib/language.js index cb56d3f500f..3f8e8a846e4 100644 --- a/lib/language.js +++ b/lib/language.js @@ -667,6 +667,81 @@ function init() { ,tr: 'Son 3 ay' ,zh_cn: '过去3个月' } + , 'between': { + cs: 'between' + ,de: 'between' + ,es: 'between' + ,fr: 'between' + ,el: 'between' + ,pt: 'between' + ,sv: 'between' + ,ro: 'between' + ,bg: 'between' + ,hr: 'between' + ,it: 'between' + ,ja: 'between' + ,dk: 'between' + ,fi: 'between' + ,nb: 'between' + ,he: 'between' + ,pl: 'between' + ,ru: 'between' + ,sk: 'between' + ,nl: 'between' + ,ko: 'between' + ,tr: 'between' + ,zh_cn: 'between' + } + , 'around': { + cs: 'around' + ,de: 'around' + ,es: 'around' + ,fr: 'around' + ,el: 'around' + ,pt: 'around' + ,sv: 'around' + ,ro: 'around' + ,bg: 'around' + ,hr: 'around' + ,it: 'around' + ,ja: 'around' + ,dk: 'around' + ,fi: 'around' + ,nb: 'around' + ,he: 'around' + ,pl: 'around' + ,ru: 'around' + ,sk: 'around' + ,nl: 'around' + ,ko: 'around' + ,tr: 'around' + ,zh_cn: 'around' + } + , 'and': { + cs: 'and' + ,de: 'and' + ,es: 'and' + ,fr: 'and' + ,el: 'and' + ,pt: 'and' + ,sv: 'and' + ,ro: 'and' + ,bg: 'and' + ,hr: 'and' + ,it: 'and' + ,ja: 'and' + ,dk: 'and' + ,fi: 'and' + ,nb: 'and' + ,he: 'and' + ,pl: 'and' + ,ru: 'and' + ,sk: 'and' + ,nl: 'and' + ,ko: 'and' + ,tr: 'and' + ,zh_cn: 'and' + } ,'From' : { cs: 'Od' ,de: 'Von' @@ -13255,7 +13330,33 @@ function init() { , zh_cn: '快速上升' , zh_tw: 'rapidly rising' }, - 'alexaStatus': { + 'virtAsstUnknown': { + bg: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , cs: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , de: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , dk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , el: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , en: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , es: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fi: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , he: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , hr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , it: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ko: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nb: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pt: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ro: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ru: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , sk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , sv: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , tr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_cn: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_tw: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + }, + 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' , de: '%1 und bis %3 %2.' @@ -13281,7 +13382,7 @@ function init() { , zh_cn: '%1 和 %2 到 %3.' , zh_tw: '%1 and %2 as of %3.' }, - 'alexaBasal': { + 'virtAsstBasal': { bg: '%1 současný bazál je %2 jednotek za hodinu' , cs: '%1 current basal is %2 units per hour' , de: '%1 aktuelle Basalrate ist %2 Einheiten je Stunde' @@ -13307,7 +13408,7 @@ function init() { , zh_cn: '%1 当前基础率是 %2 U/小时' , zh_tw: '%1 current basal is %2 units per hour' }, - 'alexaBasalTemp': { + 'virtAsstBasalTemp': { bg: '%1 dočasný bazál %2 jednotek za hodinu skončí %3' , cs: '%1 temp basal of %2 units per hour will end %3' , de: '%1 temporäre Basalrate von %2 Einheiten endet %3' @@ -13333,7 +13434,7 @@ function init() { , zh_cn: '%1 临时基础率 %2 U/小时将会在 %3结束' , zh_tw: '%1 temp basal of %2 units per hour will end %3' }, - 'alexaIob': { + 'virtAsstIob': { bg: 'a máte %1 jednotek aktivního inzulínu.' , cs: 'and you have %1 insulin on board.' , de: 'und du hast %1 Insulin wirkend.' @@ -13359,33 +13460,33 @@ function init() { , zh_cn: '并且你有 %1 的活性胰岛素.' , zh_tw: 'and you have %1 insulin on board.' }, - 'alexaIobIntent': { - bg: 'Máte %1 jednotek aktivního inzulínu' - , cs: 'You have %1 insulin on board' - , de: 'Du hast noch %1 Insulin wirkend' - , dk: 'Du har %1 insulin i kroppen' - , el: 'You have %1 insulin on board' - , en: 'You have %1 insulin on board' - , es: 'Tienes %1 insulina activa' - , fi: 'Sinulla on %1 aktiivista insuliinia' - , fr: 'You have %1 insulin on board' - , he: 'You have %1 insulin on board' - , hr: 'You have %1 insulin on board' - , it: 'Tu hai %1 insulina attiva' - , ko: 'You have %1 insulin on board' - , nb: 'You have %1 insulin on board' - , pl: 'Masz %1 aktywnej insuliny' - , pt: 'You have %1 insulin on board' - , ro: 'Aveți %1 insulină activă' - , ru: 'вы имеете %1 инсулина в организме' - , sk: 'You have %1 insulin on board' - , sv: 'You have %1 insulin on board' - , nl: 'You have %1 insulin on board' - , tr: 'Sizde %1 aktif insülin var' - , zh_cn: '你有 %1 的活性胰岛素' - , zh_tw: 'You have %1 insulin on board' - }, - 'alexaIobUnits': { + 'virtAsstIobIntent': { + bg: 'Máte %1 jednotek aktivního inzulínu' + , cs: 'You have %1 insulin on board' + , de: 'Du hast noch %1 Insulin wirkend' + , dk: 'Du har %1 insulin i kroppen' + , el: 'You have %1 insulin on board' + , en: 'You have %1 insulin on board' + , es: 'Tienes %1 insulina activa' + , fi: 'Sinulla on %1 aktiivista insuliinia' + , fr: 'You have %1 insulin on board' + , he: 'You have %1 insulin on board' + , hr: 'You have %1 insulin on board' + , it: 'Tu hai %1 insulina attiva' + , ko: 'You have %1 insulin on board' + , nb: 'You have %1 insulin on board' + , pl: 'Masz %1 aktywnej insuliny' + , pt: 'You have %1 insulin on board' + , ro: 'Aveți %1 insulină activă' + , ru: 'вы имеете %1 инсулина в организме' + , sk: 'You have %1 insulin on board' + , sv: 'You have %1 insulin on board' + , nl: 'You have %1 insulin on board' + , tr: 'Sizde %1 aktif insülin var' + , zh_cn: '你有 %1 的活性胰岛素' + , zh_tw: 'You have %1 insulin on board' + }, + 'virtAsstIobUnits': { bg: '%1 units of' , cs: '%1 jednotek' , de: 'noch %1 Einheiten' @@ -13411,7 +13512,7 @@ function init() { , zh_cn: '%1 单位' , zh_tw: '%1 units of' }, - 'alexaPreamble': { + 'virtAsstPreamble': { bg: 'Your' , cs: 'Vaše' , de: 'Deine' @@ -13437,7 +13538,7 @@ function init() { , zh_cn: '你的' , zh_tw: 'Your' }, - 'alexaPreamble3person': { + 'virtAsstPreamble3person': { bg: '%1 has a ' , cs: '%1 má ' , de: '%1 hat eine' @@ -13463,7 +13564,7 @@ function init() { , zh_cn: '%1 有一个 ' , zh_tw: '%1 has a ' }, - 'alexaNoInsulin': { + 'virtAsstNoInsulin': { bg: 'no' , cs: 'žádný' , de: 'kein' @@ -13489,75 +13590,75 @@ function init() { , zh_cn: '否' , zh_tw: 'no' }, - 'alexaUploadBattery': { - bg: 'Your uploader battery is at %1' - ,cs: 'Baterie mobilu má %1' - , en: 'Your uploader battery is at %1' - , hr: 'Your uploader battery is at %1' - , de: 'Der Akku deines Uploader Handys ist bei %1' - , dk: 'Din uploaders batteri er %1' - , ko: 'Your uploader battery is at %1' - , nl: 'De batterij van je mobiel is bij %l' - ,zh_cn: '你的手机电池电量是 %1 ' - , sv: 'Din uppladdares batteri är %1' - , fi: 'Lähettimen paristoa jäljellä %1' - , ro: 'Bateria uploaderului este la %1' - , pl: 'Twoja bateria ma %1' - , ru: 'батарея загрузчика %1' - , tr: 'Yükleyici piliniz %1' - }, - 'alexaReservoir': { - bg: 'You have %1 units remaining' - , cs: 'V zásobníku zbývá %1 jednotek' - , en: 'You have %1 units remaining' - , hr: 'You have %1 units remaining' - , de: 'Du hast %1 Einheiten übrig' - , dk: 'Du har %1 enheder tilbage' - , ko: 'You have %1 units remaining' - , nl: 'Je hebt nog %l eenheden in je reservoir' - ,zh_cn: '你剩余%1 U的胰岛素' - , sv: 'Du har %1 enheter kvar' - , fi: '%1 yksikköä insuliinia jäljellä' - , ro: 'Mai aveți %1 unități rămase' - , pl: 'W zbiorniku pozostało %1 jednostek' - , ru: 'остается %1 ед' - , tr: '%1 birim kaldı' - }, - 'alexaPumpBattery': { - bg: 'Your pump battery is at %1 %2' - , cs: 'Baterie v pumpě má %1 %2' - , en: 'Your pump battery is at %1 %2' - , hr: 'Your pump battery is at %1 %2' - , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' - , dk: 'Din pumpes batteri er %1 %2' - , ko: 'Your pump battery is at %1 %2' - , nl: 'Je pomp batterij is bij %1 %2' - ,zh_cn: '你的泵电池电量是%1 %2' - , sv: 'Din pumps batteri är %1 %2' - , fi: 'Pumppu on %1 %2' - , ro: 'Bateria pompei este la %1 %2' - , pl: 'Bateria pompy jest w %1 %2' - , ru: 'батарея помпы %1 %2' - , tr: 'Pompa piliniz %1 %2' - }, - 'alexaLastLoop': { - bg: 'The last successful loop was %1' - , cs: 'Poslední úšpěšné provedení smyčky %1' - , en: 'The last successful loop was %1' - , hr: 'The last successful loop was %1' - , de: 'Der letzte erfolgreiche Loop war %1' - , dk: 'Seneste successfulde loop var %1' - , ko: 'The last successful loop was %1' - , nl: 'De meest recente goede loop was %1' - ,zh_cn: '最后一次成功闭环的是在%1' - , sv: 'Senaste lyckade loop var %1' - , fi: 'Viimeisin onnistunut loop oli %1' - , ro: 'Ultima decizie loop implementată cu succes a fost %1' - , pl: 'Ostatnia pomyślna pętla była %1' - , ru: 'недавний успешный цикл был %1' - , tr: 'Son başarılı döngü %1 oldu' - }, - 'alexaLoopNotAvailable': { + 'virtAsstUploadBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Baterie mobilu má %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Der Akku deines Uploader Handys ist bei %1' + , dk: 'Din uploaders batteri er %1' + , ko: 'Your uploader battery is at %1' + , nl: 'De batterij van je mobiel is bij %l' + , zh_cn: '你的手机电池电量是 %1 ' + , sv: 'Din uppladdares batteri är %1' + , fi: 'Lähettimen paristoa jäljellä %1' + , ro: 'Bateria uploaderului este la %1' + , pl: 'Twoja bateria ma %1' + , ru: 'батарея загрузчика %1' + , tr: 'Yükleyici piliniz %1' + }, + 'virtAsstReservoir': { + bg: 'You have %1 units remaining' + , cs: 'V zásobníku zbývá %1 jednotek' + , en: 'You have %1 units remaining' + , hr: 'You have %1 units remaining' + , de: 'Du hast %1 Einheiten übrig' + , dk: 'Du har %1 enheder tilbage' + , ko: 'You have %1 units remaining' + , nl: 'Je hebt nog %l eenheden in je reservoir' + , zh_cn: '你剩余%1 U的胰岛素' + , sv: 'Du har %1 enheter kvar' + , fi: '%1 yksikköä insuliinia jäljellä' + , ro: 'Mai aveți %1 unități rămase' + , pl: 'W zbiorniku pozostało %1 jednostek' + , ru: 'остается %1 ед' + , tr: '%1 birim kaldı' + }, + 'virtAsstPumpBattery': { + bg: 'Your pump battery is at %1 %2' + , cs: 'Baterie v pumpě má %1 %2' + , en: 'Your pump battery is at %1 %2' + , hr: 'Your pump battery is at %1 %2' + , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' + , dk: 'Din pumpes batteri er %1 %2' + , ko: 'Your pump battery is at %1 %2' + , nl: 'Je pomp batterij is bij %1 %2' + , zh_cn: '你的泵电池电量是%1 %2' + , sv: 'Din pumps batteri är %1 %2' + , fi: 'Pumppu on %1 %2' + , ro: 'Bateria pompei este la %1 %2' + , pl: 'Bateria pompy jest w %1 %2' + , ru: 'батарея помпы %1 %2' + , tr: 'Pompa piliniz %1 %2' + }, + 'virtAsstLastLoop': { + bg: 'The last successful loop was %1' + , cs: 'Poslední úšpěšné provedení smyčky %1' + , en: 'The last successful loop was %1' + , hr: 'The last successful loop was %1' + , de: 'Der letzte erfolgreiche Loop war %1' + , dk: 'Seneste successfulde loop var %1' + , ko: 'The last successful loop was %1' + , nl: 'De meest recente goede loop was %1' + , zh_cn: '最后一次成功闭环的是在%1' + , sv: 'Senaste lyckade loop var %1' + , fi: 'Viimeisin onnistunut loop oli %1' + , ro: 'Ultima decizie loop implementată cu succes a fost %1' + , pl: 'Ostatnia pomyślna pętla była %1' + , ru: 'недавний успешный цикл был %1' + , tr: 'Son başarılı döngü %1 oldu' + }, + 'virtAsstLoopNotAvailable': { bg: 'Loop plugin does not seem to be enabled' , cs: 'Plugin smyčka není patrně povolený' , en: 'Loop plugin does not seem to be enabled' @@ -13566,7 +13667,7 @@ function init() { , dk: 'Loop plugin lader ikke til at være slået til' , ko: 'Loop plugin does not seem to be enabled' , nl: 'De Loop plugin is niet geactiveerd' - ,zh_cn: 'Loop插件看起来没有被启用' + , zh_cn: 'Loop插件看起来没有被启用' , sv: 'Loop plugin verkar inte vara aktiverad' , fi: 'Loop plugin ei ole aktivoitu' , ro: 'Extensia loop pare a fi dezactivată' @@ -13574,7 +13675,7 @@ function init() { , ru: 'плагин ЗЦ Loop не активирован ' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'alexaLoopForecast': { + 'virtAsstLoopForecast': { bg: 'According to the loop forecast you are expected to be %1 over the next %2' , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' , en: 'According to the loop forecast you are expected to be %1 over the next %2' @@ -13583,7 +13684,7 @@ function init() { , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' , ko: 'According to the loop forecast you are expected to be %1 over the next %2' , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - ,zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' + , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' @@ -13591,7 +13692,24 @@ function init() { , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' }, - 'alexaForecastUnavailable': { + 'virtAsstAR2Forecast': { + bg: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , cs: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , en: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , hr: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , de: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , dk: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ko: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , nl: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , zh_cn: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , sv: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , fi: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ro: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , pl: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , ru: 'According to the AR2 forecast you are expected to be %1 over the next %2' + , tr: 'According to the AR2 forecast you are expected to be %1 over the next %2' + }, + 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' , en: 'Unable to forecast with the data that is available' @@ -13600,7 +13718,7 @@ function init() { , dk: 'Det er ikke muligt at forudsige md de tilgængelige data' , ko: 'Unable to forecast with the data that is available' , nl: 'Niet mogelijk om een voorspelling te doen met de data die beschikbaar is' - ,zh_cn: '血糖数据不可用,无法预测未来走势' + , zh_cn: '血糖数据不可用,无法预测未来走势' , sv: 'Förutsägelse ej möjlig med tillgänlig data' , fi: 'Ennusteet eivät ole toiminnassa puuttuvan tiedon vuoksi' , ro: 'Estimarea este imposibilă pe baza datelor disponibile' @@ -13608,14 +13726,14 @@ function init() { , ru: 'прогноз при таких данных невозможен' , tr: 'Mevcut verilerle tahmin edilemedi' }, - 'alexaRawBG': { - en: 'Your raw bg is %1' + 'virtAsstRawBG': { + en: 'Your raw bg is %1' , cs: 'Raw glykémie je %1' , de: 'Dein Rohblutzucker ist %1' , dk: 'Dit raw blodsukker er %1' , ko: 'Your raw bg is %1' , nl: 'Je raw bloedwaarde is %1' - ,zh_cn: '你的血糖是 %1' + , zh_cn: '你的血糖是 %1' , sv: 'Ditt raw blodsocker är %1' , fi: 'Suodattamaton verensokeriarvo on %1' , ro: 'Glicemia brută este %1' @@ -13625,36 +13743,36 @@ function init() { , ru: 'ваши необработанные данные RAW %1' , tr: 'Ham kan şekeriniz %1' }, - 'alexaOpenAPSForecast': { + 'virtAsstOpenAPSForecast': { en: 'The OpenAPS Eventual BG is %1' , cs: 'OpenAPS Eventual BG je %1' , de: 'Der von OpenAPS vorhergesagte Blutzucker ist %1' , dk: 'OpenAPS forventet blodsukker er %1' , ko: 'The OpenAPS Eventual BG is %1' , nl: 'OpenAPS uiteindelijke bloedglucose van %1' - ,zh_cn: 'OpenAPS 预测最终血糖是 %1' + , zh_cn: 'OpenAPS 预测最终血糖是 %1' , sv: 'OpenAPS slutgiltigt blodsocker är %1' , fi: 'OpenAPS verensokeriarvio on %1' , ro: 'Glicemia estimată de OpenAPS este %1' - ,bg: 'The OpenAPS Eventual BG is %1' - ,hr: 'The OpenAPS Eventual BG is %1' + , bg: 'The OpenAPS Eventual BG is %1' + , hr: 'The OpenAPS Eventual BG is %1' , pl: 'Glikemia prognozowana przez OpenAPS wynosi %1' , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' }, - 'alexaCOB': { + 'virtAsstCOB': { en: '%1 %2 carbohydrates on board' , cs: '%1 %2 aktivních sachridů' , de: '%1 %2 Gramm Kohlenhydrate wirkend.' , dk: '%1 %2 gram aktive kulhydrater' , ko: '%1 %2 carbohydrates on board' , nl: '%1 %2 actieve koolhydraten' - ,zh_cn: '%1 %2 活性碳水化合物' + , zh_cn: '%1 %2 活性碳水化合物' , sv: '%1 %2 gram aktiva kolhydrater' , fi: '%1 %2 aktiivista hiilihydraattia' , ro: '%1 %2 carbohidrați activi în corp' - ,bg: '%1 %2 carbohydrates on board' - ,hr: '%1 %2 carbohydrates on board' + , bg: '%1 %2 carbohydrates on board' + , hr: '%1 %2 carbohydrates on board' , pl: '%1 %2 aktywnych węglowodanów' , ru: '%1 $2 активных углеводов' , tr: '%1 %2 aktif karbonhidrat' diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index 38ae449c249..d41aa567885 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -1,60 +1,50 @@ var _ = require('lodash'); var async = require('async'); -function init(env, ctx) { - console.log('Configuring Alexa.'); +function init (env, ctx) { + console.log('Configuring Alexa...'); function alexa() { return alexa; } var intentHandlers = {}; var rollup = {}; - // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues - // are the values that determine the routing. This allows for specific intent handlers based on the value of a - // specific slot. Routing is only supported on one slot for now. - // There is no protection for a previously configured handler - one plugin can overwrite the handler of another - // plugin. - alexa.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (! intentHandlers[intent]) { + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + alexa.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { intentHandlers[intent] = {}; } - if (routableSlot && slotValues) { - for (var i = 0, len = slotValues.length; i < len; i++) { - if (! intentHandlers[intent][routableSlot]) { - intentHandlers[intent][routableSlot] = {}; + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; } - if (!intentHandlers[intent][routableSlot][slotValues[i]]) { - intentHandlers[intent][routableSlot][slotValues[i]] = {}; - } - intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; } } else { + console.log('Storing handler for intent \'' + intent + '\''); intentHandlers[intent].handler = handler; } }; - // This function retrieves a handler based on the intent name and slots requested. - alexa.getIntentHandler = function getIntentHandler(intentName, slots) { + // This function retrieves a handler based on the intent name and metric requested. + alexa.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); if (intentName && intentHandlers[intentName]) { - if (slots) { - var slotKeys = Object.keys(slots); - for (var i = 0, len = slotKeys.length; i < len; i++) { - if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { - - return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; - } - } - } - if (intentHandlers[intentName].handler) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { + console.log('Found!'); return intentHandlers[intentName].handler; } + console.log('Not found!'); return null; } else { + console.log('Not found!'); return null; } - }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { @@ -63,7 +53,6 @@ function init(env, ctx) { rollup[rollupGroup] = []; } rollup[rollupGroup].push({handler: handler, name: rollupName}); - // status = _.orderBy(status, ['priority'], ['asc']) }; alexa.getRollup = function(rollupGroup, sbx, slots, locale, callback) { @@ -110,4 +99,4 @@ function init(env, ctx) { return alexa; } -module.exports = init; +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index e25a2b36229..cab989c8150 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -146,7 +146,7 @@ function init (ctx) { return result.points; }; - function alexaAr2Handler (next, slots, sbx) { + function virtAsstAr2Handler (next, slots, sbx) { if (sbx.properties.ar2.forecast.predicted) { var forecast = sbx.properties.ar2.forecast.predicted; var max = forecast[0].mgdl; @@ -163,19 +163,29 @@ function init (ctx) { maxForecastMills = forecast[i].mills; } } - var response = 'You are expected to be between ' + min + ' and ' + max + ' over the ' + moment(maxForecastMills).from(moment(sbx.time)); + var value = ''; + if (min === max) { + value = translate('around') + ' ' + max; + } else { + value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; + } + var response = translate('virtAsstAR2Forecast', { + params: [ + value + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); next('AR2 Forecast', response); } else { - next('AR2 Forecast', 'AR2 plugin does not seem to be enabled'); + next('AR2 Forecast', translate('virtAsstUnknown')); } } - ar2.alexa = { + ar2.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['ar2 forecast', 'forecast'] - , intentHandler: alexaAr2Handler + , metrics: ['ar2 forecast', 'forecast'] + , intentHandler: virtAsstAr2Handler }] }; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 73c9492ea44..c7c54da75c7 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -112,16 +112,16 @@ function init (ctx) { function basalMessage(slots, sbx) { var basalValue = sbx.data.profile.getTempBasal(sbx.time); - var response = 'Unable to determine current basal'; + var response = translate('virtAsstUnknown'); var preamble = ''; if (basalValue.treatment) { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); + }) : translate('virtAsstPreamble'); var minutesLeft = moment(basalValue.treatment.endmills).from(moment(sbx.time)); - response = translate('alexaBasalTemp', { + response = translate('virtAsstBasalTemp', { params: [ preamble, basalValue.totalbasal, @@ -129,12 +129,12 @@ function init (ctx) { ] }); } else { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); - response = translate('alexaBasal', { + }) : translate('virtAsstPreamble'); + response = translate('virtAsstBasal', { params: [ preamble, basalValue.totalbasal @@ -144,30 +144,28 @@ function init (ctx) { return response; } - function alexaRollupCurrentBasalHandler (slots, sbx, callback) { + function virtAsstRollupCurrentBasalHandler (slots, sbx, callback) { callback(null, {results: basalMessage(slots, sbx), priority: 1}); } - function alexaCurrentBasalhandler (next, slots, sbx) { + function virtAsstCurrentBasalhandler (next, slots, sbx) { next('Current Basal', basalMessage(slots, sbx)); } - basal.alexa = { + basal.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current basal' - , rollupHandler: alexaRollupCurrentBasalHandler + , rollupHandler: virtAsstRollupCurrentBasalHandler }], intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['basal', 'current basal'] - , intentHandler: alexaCurrentBasalhandler + , metrics: ['basal', 'current basal'] + , intentHandler: virtAsstCurrentBasalhandler }] }; return basal; } - module.exports = init; diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index f3bb1902ad6..ef9b4252be5 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -292,7 +292,7 @@ function init (ctx) { }); }; - function alexaCOBHandler (next, slots, sbx) { + function virtAsstCOBHandler (next, slots, sbx) { var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; var value = 'no'; if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { @@ -302,12 +302,11 @@ function init (ctx) { next('Current COB', response); } - cob.alexa = { + cob.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['cob', 'carbs on board', 'carbohydrates on board'] - , intentHandler: alexaCOBHandler + , metrics: ['cob', 'carbs on board', 'carbohydrates on board'] + , intentHandler: virtAsstCOBHandler }] }; diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js new file mode 100644 index 00000000000..8e8181512c8 --- /dev/null +++ b/lib/plugins/googlehome.js @@ -0,0 +1,97 @@ +var _ = require('lodash'); +var async = require('async'); + +function init (env, ctx) { + console.log('Configuring Google Home...'); + function googleHome() { + return googleHome; + } + var intentHandlers = {}; + var rollup = {}; + + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; + } + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; + } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; + } + } else { + console.log('Storing handler for intent \'' + intent + '\''); + intentHandlers[intent].handler = handler; + } + }; + + // This function retrieves a handler based on the intent name and metric requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName && intentHandlers[intentName]) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { + console.log('Found!'); + return intentHandlers[intentName].handler; + } + console.log('Not found!'); + return null; + } else { + console.log('Not found!'); + return null; + } + }; + + googleHome.addToRollup = function(rollupGroup, handler, rollupName) { + if (!rollup[rollupGroup]) { + console.log('Creating the rollup group: ', rollupGroup); + rollup[rollupGroup] = []; + } + rollup[rollupGroup].push({handler: handler, name: rollupName}); + }; + + googleHome.getRollup = function(rollupGroup, sbx, slots, locale, callback) { + var handlers = _.map(rollup[rollupGroup], 'handler'); + console.log('Rollup array for ', rollupGroup); + console.log(rollup[rollupGroup]); + var nHandlers = []; + _.each(handlers, function (handler) { + nHandlers.push(handler.bind(null, slots, sbx)); + }); + async.parallelLimit(nHandlers, 10, function(err, results) { + if (err) { + console.error('Error: ', err); + } + callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); + }); + }; + + // This creates the expected Google Home response + googleHome.buildSpeechletResponse = function buildSpeechletResponse(output, expectUserResponse) { + return { + payload: { + google: { + expectUserResponse: expectUserResponse, + richResponse: { + items: [ + { + simpleResponse: { + textToSpeech: output + } + } + ] + } + } + } + }; + }; + + return googleHome; +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index f9bf082d0f4..614cc56981b 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -243,21 +243,19 @@ function init(ctx) { }; - function alexaIOBIntentHandler (callback, slots, sbx) { + function virtAsstIOBIntentHandler (callback, slots, sbx) { - var message = translate('alexaIobIntent', { + var message = translate('virtAsstIobIntent', { params: [ - //preamble, getIob(sbx) ] }); - //preamble + + ' insulin on board'; callback('Current IOB', message); } - function alexaIOBRollupHandler (slots, sbx, callback) { + function virtAsstIOBRollupHandler (slots, sbx, callback) { var iob = getIob(sbx); - var message = translate('alexaIob', { + var message = translate('virtAsstIob', { params: [iob] }); callback(null, {results: message, priority: 2}); @@ -265,26 +263,25 @@ function init(ctx) { function getIob(sbx) { if (sbx.properties.iob && sbx.properties.iob.iob !== 0) { - return translate('alexaIobUnits', { + return translate('virtAsstIobUnits', { params: [ utils.toFixed(sbx.properties.iob.iob) ] }); } - return translate('alexaNoInsulin'); + return translate('virtAsstNoInsulin'); } - iob.alexa = { + iob.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current iob' - , rollupHandler: alexaIOBRollupHandler + , rollupHandler: virtAsstIOBRollupHandler }] , intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['iob', 'insulin on board'] - , intentHandler: alexaIOBIntentHandler + , metrics: ['iob', 'insulin on board'] + , intentHandler: virtAsstIOBIntentHandler }] }; diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index 82ed74ecb3c..02a37b1f16a 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -506,7 +506,7 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.loop.lastLoop.predicted) { var forecast = sbx.properties.loop.lastLoop.predicted.values; var max = forecast[0]; @@ -516,7 +516,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', 'Unable to forecast with the data that is available'); + next('Loop Forecast', translate('virtAsstForecastUnavailable')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -528,33 +528,45 @@ function init (ctx) { } var value = ''; if (min === max) { - value = 'around ' + max; + value = translate('around') + ' ' + max; } else { - value = 'between ' + min + ' and ' + max; + value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; } - var response = 'According to the loop forecast you are expected to be ' + value + ' over the next ' + moment(endPrediction).from(moment(sbx.time)); + var response = translate('virtAsstLoopForecast', { + params: [ + value + , moment(endPrediction).from(moment(sbx.time)) + ] + }); next('Loop Forecast', response); } } else { - next('Loop forecast', 'Loop plugin does not seem to be enabled'); + next('Loop Forecast', translate('virtAsstUnknown')); } } - function alexaLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.loop.lastLoop)); - var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); - next('Last loop', response); + function virtAsstLastLoopHandler (next, slots, sbx) { + if (sbx.properties.loop.lastLoop) { + console.log(JSON.stringify(sbx.properties.loop.lastLoop)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) + ] + }); + next('Last Loop', response); + } else { + next('Last Loop', translate('virtAsstUnknown')); + } } - loop.alexa = { + loop.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['loop forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , metrics: ['loop forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 77f27288b14..64b8dccdd12 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -516,36 +516,41 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { - var response = translate('alexaOpenAPSForecast', { + var response = translate('virtAsstOpenAPSForecast', { params: [ sbx.properties.openaps.lastEventualBG ] }); next('Loop Forecast', response); + } else { + next('Loop Forecast', translate('virtAsstUnknown')); } } - function alexaLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); - var response = translate('alexaLastLoop', { - params: [ - moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) - ] - }); - next('Last loop', response); + function virtAsstLastLoopHandler (next, slots, sbx) { + if (sbx.properties.openaps.lastLoopMoment) { + console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) + ] + }); + next('Last Loop', response); + } else { + next('Last Loop', translate('virtAsstUnknown')); + } } - openaps.alexa = { + openaps.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['openaps forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , metrics: ['openaps forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 842d8536b6b..840afa8aeae 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -135,38 +135,57 @@ function init (ctx) { }); }; - function alexaReservoirHandler (next, slots, sbx) { - var response = translate('alexaReservoir', { + function virtAsstReservoirHandler (next, slots, sbx) { + if (sbx.properties.pump.pump.reservoir) { + var response = translate('virtAsstReservoir', { params: [ - sbx.properties.pump.pump.reservoir + reservoir ] - }); - next('Remaining insulin', response); + }); + next('Remaining Insulin', response); + } else { + next('Remaining Insulin', translate('virtAsstUnknown')); + } } - function alexaBatteryHandler (next, slots, sbx) { + function virtAsstBatteryHandler (next, slots, sbx) { var battery = _.get(sbx, 'properties.pump.data.battery'); if (battery) { - var response = translate('alexaPumpBattery', { + var response = translate('virtAsstPumpBattery', { params: [ battery.value, battery.unit ] }); - next('Pump battery', response); + next('Pump Battery', response); } else { - next(); + next('Pump Battery', translate('virtAsstUnknown')); } } - pump.alexa = { - intentHandlers:[{ - intent: 'InsulinRemaining', - intentHandler: alexaReservoirHandler - }, { - intent: 'PumpBattery', - intentHandler: alexaBatteryHandler - }] + pump.virtAsst = { + intentHandlers:[ + { + // backwards compatibility + intent: 'InsulinRemaining', + intentHandler: virtAsstReservoirHandler + } + , { + // backwards compatibility + intent: 'PumpBattery', + intentHandler: virtAsstBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump reservoir'] + , intentHandler: virtAsstReservoirHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump battery'] + , intentHandler: virtAsstBatteryHandler + } + ] }; function statusClass (level) { diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index f19e669f63b..997cf7c55a3 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -106,17 +106,20 @@ function init (ctx) { return display; }; - function alexaRawBGHandler (next, slots, sbx) { - var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; - next('Current Raw BG', response); + function virtAsstRawBGHandler (next, slots, sbx) { + if (sbx.properties.rawbg.mgdl) { + var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; + next('Current Raw BG', response); + } else { + next('Current Raw BG', translate('virtAsstUnknown')); + } } - rawbg.alexa = { + rawbg.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['raw bg', 'raw blood glucose'] - , intentHandler: alexaRawBGHandler + , metrics:['raw bg', 'raw blood glucose'] + , intentHandler: virtAsstRawBGHandler }] }; diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index eda42a3901f..1bd8795ef7c 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -221,16 +221,28 @@ function init() { }); }; - function alexaUploaderBatteryHandler (next, slots, sbx) { - var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; - next('Uploader battery', response); + function virtAsstUploaderBatteryHandler (next, slots, sbx) { + if (sbx.properties.upbat.display) { + var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; + next('Uploader Battery', response); + } else { + next('Uploader Battery', translate('virtAsstUnknown')); + } } - upbat.alexa = { - intentHandlers: [{ - intent: 'UploaderBattery' - , intentHandler: alexaUploaderBatteryHandler - }] + upbat.virtAsst = { + intentHandlers: [ + { + // for backwards compatibility + intent: 'UploaderBattery' + , intentHandler: virtAsstUploaderBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['uploader battery'] + , intentHandler: virtAsstUploaderBatteryHandler + } + ] }; return upbat; diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index af5f954dd1b..2bb63ad78f0 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -183,6 +183,10 @@ function boot (env, language) { ctx.alexa = require('../plugins/alexa')(env, ctx); } + if (env.settings.isEnabled('googlehome')) { + ctx.googleHome = require('../plugins/googlehome')(env, ctx); + } + next( ); } diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 9dbf6de14cd..01f4f3d41a1 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -147,18 +147,18 @@ describe('ar2', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var now = Date.now(); var before = now - FIVE_MINS; ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); - ar2.alexa.intentHandlers.length.should.equal(1); + ar2.virtAsst.intentHandlers.length.should.equal(1); - ar2.alexa.intentHandlers[0].intentHandler(function next(title, response) { + ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); - response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); + response.should.equal('According to the AR2 forecast you are expected to be between 109 and 120 over the next in 30 minutes'); done(); }, [], sbx); }); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index 0bcfd3bc268..fa97f84274e 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -77,7 +77,7 @@ describe('basalprofile', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = {}; var ctx = { @@ -92,14 +92,14 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; - basal.alexa.intentHandlers.length.should.equal(1); - basal.alexa.rollupHandlers.length.should.equal(1); + basal.virtAsst.intentHandlers.length.should.equal(1); + basal.virtAsst.rollupHandlers.length.should.equal(1); - basal.alexa.intentHandlers[0].intentHandler(function next(title, response) { + basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); - basal.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + basal.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('Your current basal is 0.175 units per hour'); response.priority.should.equal(1); diff --git a/tests/cob.test.js b/tests/cob.test.js index dbbecda0b67..54fbcb6c50d 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -97,7 +97,7 @@ describe('COB', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = { treatments: [{ carbs: '8' @@ -110,9 +110,9 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); - cob.alexa.intentHandlers.length.should.equal(1); + cob.virtAsst.intentHandlers.length.should.equal(1); - cob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); done(); diff --git a/tests/iob.test.js b/tests/iob.test.js index 30872e4fb4d..b6c5c2430ec 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -10,7 +10,7 @@ describe('IOB', function() { var iob = require('../lib/plugins/iob')(ctx); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sbx = { properties: { @@ -20,14 +20,14 @@ describe('IOB', function() { } }; - iob.alexa.intentHandlers.length.should.equal(1); - iob.alexa.rollupHandlers.length.should.equal(1); + iob.virtAsst.intentHandlers.length.should.equal(1); + iob.virtAsst.rollupHandlers.length.should.equal(1); - iob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); - iob.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + iob.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('and you have 1.50 units of insulin on board.'); response.priority.should.equal(2); diff --git a/tests/loop.test.js b/tests/loop.test.js index 9c65ff9bdd1..71bc0860bda 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -243,7 +243,7 @@ describe('loop', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -255,14 +255,14 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); - loop.alexa.intentHandlers.length.should.equal(2); + loop.virtAsst.intentHandlers.length.should.equal(2); - loop.alexa.intentHandlers[0].intentHandler(function next(title, response) { + loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); - loop.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); response.should.equal('The last successful loop was a few seconds ago'); done(); }, [], sbx); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index ed3dd6d3b9f..5c76deeaaf3 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -370,7 +370,7 @@ describe('openaps', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -382,14 +382,14 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); - openaps.alexa.intentHandlers.length.should.equal(2); + openaps.virtAsst.intentHandlers.length.should.equal(2); - openaps.alexa.intentHandlers[0].intentHandler(function next(title, response) { + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); - openaps.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); response.should.equal('The last successful loop was 2 minutes ago'); done(); }, [], sbx); diff --git a/tests/pump.test.js b/tests/pump.test.js index c6def822058..374ba03f06c 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -254,7 +254,7 @@ describe('pump', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -266,16 +266,28 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); - pump.alexa.intentHandlers.length.should.equal(2); + pump.virtAsst.intentHandlers.length.should.equal(4); - pump.alexa.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Remaining insulin'); + pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Remaining Insulin'); response.should.equal('You have 86.4 units remaining'); - pump.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Pump battery'); + pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); response.should.equal('Your pump battery is at 1.52 volts'); - done(); + + pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { + title.should.equal('Remaining Insulin'); + response.should.equal('You have 86.4 units remaining'); + + pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); + response.should.equal('Your pump battery is at 1.52 volts'); + done(); + }, [], sbx); + + }, [], sbx); + }, [], sbx); }, [], sbx); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index ab91d2bf722..48c21186cc5 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -35,16 +35,16 @@ describe('Raw BG', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sandbox = require('../lib/sandbox')(); var sbx = sandbox.clientInit(ctx, Date.now(), data); rawbg.setProperties(sbx); - rawbg.alexa.intentHandlers.length.should.equal(1); + rawbg.virtAsst.intentHandlers.length.should.equal(1); - rawbg.alexa.intentHandlers[0].intentHandler(function next(title, response) { + rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 9b48c3b845e..42d18bb0854 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -93,7 +93,7 @@ describe('Uploader Battery', function ( ) { upbat.updateVisualisation(sbx); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: {} @@ -106,13 +106,19 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); - upbat.alexa.intentHandlers.length.should.equal(1); + upbat.virtAsst.intentHandlers.length.should.equal(2); - upbat.alexa.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Uploader battery'); + upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); response.should.equal('Your uploader battery is at 20%'); - - done(); + + upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); + response.should.equal('Your uploader battery is at 20%'); + + done(); + }, [], sbx); + }, [], sbx); }); From 36de82bffd3387d9a7fdc9a3a1d02c01bf472af4 Mon Sep 17 00:00:00 2001 From: PieterGit <6500826+PieterGit@users.noreply.github.com> Date: Mon, 21 Oct 2019 16:23:32 +0200 Subject: [PATCH 064/134] Revert "fix/feat: Update Alexa integration and add Google Home support (#4980)" (#5131) This reverts commit 30b88f67650afa5482fad3f1024f995a2cf6e2d6. --- CONTRIBUTING.md | 12 +- README.md | 4 - ...add-virtual-assistant-support-to-plugin.md | 52 --- docs/plugins/alexa-plugin.md | 307 ++++++++++++--- docs/plugins/alexa-templates/en-us.json | 218 ----------- docs/plugins/google-home-templates/en-us.zip | Bin 13230 -> 0 bytes docs/plugins/googlehome-plugin.md | 109 ------ .../interacting-with-virtual-assistants.md | 56 --- lib/api/alexa/index.js | 279 +++++++------- lib/api/googlehome/index.js | 123 ------ lib/api/index.js | 4 - lib/language.js | 356 ++++++------------ lib/plugins/alexa.js | 57 +-- lib/plugins/ar2.js | 24 +- lib/plugins/basalprofile.js | 28 +- lib/plugins/cob.js | 9 +- lib/plugins/googlehome.js | 97 ----- lib/plugins/iob.js | 23 +- lib/plugins/loop.js | 42 +-- lib/plugins/openaps.js | 35 +- lib/plugins/pump.js | 53 +-- lib/plugins/rawbg.js | 17 +- lib/plugins/upbat.js | 28 +- lib/server/bootevent.js | 4 - tests/ar2.test.js | 8 +- tests/basalprofileplugin.test.js | 10 +- tests/cob.test.js | 6 +- tests/iob.test.js | 10 +- tests/loop.test.js | 10 +- tests/openaps.test.js | 10 +- tests/pump.test.js | 26 +- tests/rawbg.test.js | 6 +- tests/upbat.test.js | 18 +- 33 files changed, 699 insertions(+), 1342 deletions(-) delete mode 100644 docs/plugins/add-virtual-assistant-support-to-plugin.md delete mode 100644 docs/plugins/alexa-templates/en-us.json delete mode 100644 docs/plugins/google-home-templates/en-us.zip delete mode 100644 docs/plugins/googlehome-plugin.md delete mode 100644 docs/plugins/interacting-with-virtual-assistants.md delete mode 100644 lib/api/googlehome/index.js delete mode 100644 lib/plugins/googlehome.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f80a79f2a1d..c77a0df1c6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -202,13 +202,13 @@ Also if you can't code, it's possible to contribute by improving the documentati | Release coordination 0.11.x: | [@PieterGit] | | Issue/Pull request coordination: | Please volunteer | | Cleaning up git fork spam: | Please volunteer | -| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | +| Documentation writers: | [@andrew-warrington][@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | ### Plugin contributors | Contribution area | List of developers | List of testers | ------------------------------------- | -------------------- | -------------------- | -| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| [@inventor96] | Please volunteer | +| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| Please volunteer | Please volunteer | | [`ar2` (AR2 Forecasting)](README.md#ar2-ar2-forecasting)| Please volunteer | Please volunteer | | [`basal` (Basal Profile)](README.md#basal-basal-profile)| Please volunteer | Please volunteer | | [`boluscalc` (Bolus Wizard)](README.md#boluscalc-bolus-wizard)| Please volunteer | Please volunteer | @@ -223,7 +223,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | | [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | | [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | -| [`googlehome` (Google Home/DialogFlow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`googlehome` (Google Home)](README.md#google-home) |[@mdomox] [@rickfriele] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | | [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | | [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | | [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | @@ -232,9 +232,9 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`profile` (Treatment Profile)](README.md#profile-treatment-profile)| Please volunteer | Please volunteer | | [`pump` (Pump Monitoring)](README.md#pump-pump-monitoring)| Please volunteer | Please volunteer | | [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | -| [`sage` (Sensor Age)](README.md#sage-sensor-age)| [@jpcunningh] | Please volunteer | +| [`sage` (Sensor Age)](README.md#sage-sensor-age)| @jpcunningh | Please volunteer | | [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | -| [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | +| [`speech` (Speech)](README.md#speech-speech) | [@sulkaharo] | Please volunteer | | [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | | [`treatmentnotify` (Treatment Notifications)](README.md#treatmentnotify-treatment-notifications)| Please volunteer | Please volunteer | | [`upbat` (Uploader Battery)](README.md#upbat-uploader-battery)| [@jpcunningh] | Please volunteer | @@ -251,7 +251,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Čeština (`cs`) |Please volunteer|OK | | Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK | | Dansk (`dk`) | [@janrpn] |OK | -| Ελληνικά (`el`)|Please volunteer|Needs attention: 68.5%| +| Ελληνικά `(el`)|Please volunteer|Needs attention: 68.5%| | English (`en`)|Please volunteer|OK| | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| diff --git a/README.md b/README.md index 91a0b72dbc4..8ad79a39d72 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,6 @@ Community maintained fork of the - [`override` (Override Mode)](#override-override-mode) - [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) - - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) @@ -519,9 +518,6 @@ For remote overrides, the following extended settings must be configured: ##### `alexa` (Amazon Alexa) Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) -##### `googlehome` (Google Home/DialogFLow) - Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md) - ##### `speech` (Speech) Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms. diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md deleted file mode 100644 index 764cfc7c4ea..00000000000 --- a/docs/plugins/add-virtual-assistant-support-to-plugin.md +++ /dev/null @@ -1,52 +0,0 @@ -Adding Virtual Assistant Support to a Plugin -========================================= - -To add virtual assistant support to a plugin, the `init` method of the plugin should return an object that contains a `virtAsst` key. Here is an example: - -```javascript -iob.virtAsst = { - intentHandlers: [{ - intent: "MetricNow" - , metrics: ["iob"] - , intentHandler: virtAsstIOBIntentHandler - }] - , rollupHandlers: [{ - rollupGroup: "Status" - , rollupName: "current iob" - , rollupHandler: virtAsstIOBRollupHandler - }] -}; -``` - -There are 2 types of handlers that you will need to supply: -* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. -* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. - -### Intent Handlers - -A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). Each intent handler should be structured as follows: -+ `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) -+ `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Make sure to add the metric name and its synonyms to the list of metrics used by the virtual assistant(s). - - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! - - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. We keep this value as an array for backwards compatibility. -+ `intenthandler` - This is a callback function that receives 3 arguments: - - `callback` Call this at the end of your function. It requires 2 arguments: - - `title` - Title of the handler. This is the value that will be displayed on the Alexa card (for devices with a screen). The Google Home response doesn't currently display a card, so it doesn't use this value. - - `text` - This is text that the virtual assistant should speak (and show, for devices with a screen). - - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected (e.g. `pwd` as seen in the templates is a slot/parameter. `metric` is technically a slot, too). - - `sandbox` - This is the Nightscout sandbox that allows access to various functions. - -### Rollup handlers - -A plugin can also expose multiple rollup handlers -+ `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked -+ `rollupName` - This is the name of the handler. Primarily used for debugging -+ `rollupHandler` - This is a callback function that receives 3 arguments - - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot - - `sandbox` - This is the nightscout sandbox that allows access to various functions. - - `callback` - - - `error` - This would be an error message - - `response` - A simple object that expects a `results` string and a `priority` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: - ```javascript - callback(null, {results: "Hello world", priority: 1}); - ``` diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 8ba8188143e..a5dcb886e9c 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -41,9 +41,9 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p ### Get an Amazon Developer account -1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. -1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. -1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). +- Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. +- [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. +- Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). ### Create a new Alexa skill @@ -58,11 +58,164 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p Your Alexa skill's "interaction model" defines how your spoken questions get translated into requests to your Nightscout site, and how your Nightscout site's responses get translated into the audio responses that Alexa says back to you. -To get up and running with an interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code for your language from [the list of templates](alexa-templates/). - -- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. - -Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. +To get up and running with a basic interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code below. + +```json +{ + "interactionModel": { + "languageModel": { + "invocationName": "nightscout", + "intents": [ + { + "name": "NSStatus", + "slots": [], + "samples": [ + "How am I doing" + ] + }, + { + "name": "UploaderBattery", + "slots": [], + "samples": [ + "How is my uploader battery" + ] + }, + { + "name": "PumpBattery", + "slots": [], + "samples": [ + "How is my pump battery" + ] + }, + { + "name": "LastLoop", + "slots": [], + "samples": [ + "When was my last loop" + ] + }, + { + "name": "MetricNow", + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS" + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "What is my {metric}", + "What my {metric} is", + "What is {pwd} {metric}" + ] + }, + { + "name": "InsulinRemaining", + "slots": [ + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "How much insulin do I have left", + "How much insulin do I have remaining", + "How much insulin does {pwd} have left", + "How much insulin does {pwd} have remaining" + ] + } + ], + "types": [ + { + "name": "LIST_OF_METRICS", + "values": [ + { + "name": { + "value": "bg" + } + }, + { + "name": { + "value": "blood glucose" + } + }, + { + "name": { + "value": "number" + } + }, + { + "name": { + "value": "iob" + } + }, + { + "name": { + "value": "insulin on board" + } + }, + { + "name": { + "value": "current basal" + } + }, + { + "name": { + "value": "basal" + } + }, + { + "name": { + "value": "cob" + } + }, + { + "name": { + "value": "carbs on board" + } + }, + { + "name": { + "value": "carbohydrates on board" + } + }, + { + "name": { + "value": "loop forecast" + } + }, + { + "name": { + "value": "ar2 forecast" + } + }, + { + "name": { + "value": "forecast" + } + }, + { + "name": { + "value": "raw bg" + } + }, + { + "name": { + "value": "raw blood glucose" + } + } + ] + } + ] + } + } +} +``` + +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". @@ -89,50 +242,108 @@ After you enable testing, you can also use the Alexa Simulator in the left colum ##### What questions can you ask it? -See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. +*Forecast:* + +- "Alexa, ask Nightscout how am I doing" +- "Alexa, ask Nightscout how I'm doing" + +*Uploader Battery:* + +- "Alexa, ask Nightscout how is my uploader battery" + +*Pump Battery:* + +- "Alexa, ask Nightscout how is my pump battery" + +*Metrics:* + +- "Alexa, ask Nightscout what my bg is" +- "Alexa, ask Nightscout what my blood glucose is" +- "Alexa, ask Nightscout what my number is" +- "Alexa, ask Nightscout what is my insulin on board" +- "Alexa, ask Nightscout what is my basal" +- "Alexa, ask Nightscout what is my current basal" +- "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is Charlie's carbs on board" +- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" +- "Alexa, ask Nightscout what is Harper's loop forecast" +- "Alexa, ask Nightscout what is Alicia's ar2 forecast" +- "Alexa, ask Nightscout what is Peter's forecast" +- "Alexa, ask Nightscout what is Arden's raw bg" +- "Alexa, ask Nightscout what is Dana's raw blood glucose" + +*Insulin Remaining:* + +- "Alexa, ask Nightscout how much insulin do I have left" +- "Alexa, ask Nightscout how much insulin do I have remaining" +- "Alexa, ask Nightscout how much insulin does Dana have left? +- "Alexa, ask Nightscout how much insulin does Arden have remaining? + +*Last Loop:* + +- "Alexa, ask Nightscout when was my last loop" + +(Note: all the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name.) ### Activate the skill on your Echo or other device If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. -## Adding support for additional languages - -If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. - -If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. - -1. Open the Build tab of your Alexa Skill. - - Get to your list of Alexa Skills at https://developer.amazon.com/alexa/console/ask and click on the name of the skill. -1. Click on the language drop-down box in the upper right corner of the window. -1. Click "Language settings". -1. Add your desired language. -1. Click the "Save" button. -1. Navigate to "CUSTOM" in the left navigation pane. -1. Select your new language in the language drop-down box. -1. Go to "JSON Editor" (just above "Interfaces" in the left navigation pane). -1. Remove the existing contents in the text box, and copy and paste the configuration code from a familiar language in [the list of templates](alexa-templates/). -1. Click "Save Model". -1. Click the "Add" button next to the "Slot Types" section in the left pane. -1. Click the radio button for "Use an existing slot type from Alexa's built-in library" -1. In the search box just below that option, search for "first name" -1. If your language has an option, click the "Add Slot Type" button for that option. - - If your language doesn't have an option, you won't be able to ask Nightscout a question that includes a name. -1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): - 1. Click on the Intent name. - 1. Scroll down to the "Slots" section - 1. If there's a slot with the name "pwd", change the Slot Type to the one found above. - - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. - 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. - 1. Set the "Alexa speech prompts" in your language, and remove the old ones. - 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. - 1. Click on the Intent name (just to the left of "metric") to return to the previous screen. - 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. -1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. -1. For each metric listed, add synonyms in your language, and delete the old synonyms. - - What ever you do, **DO NOT** change the text in the "VALUE" column! Nightscout will be looking for these exact values. Only change the synonyms. -1. Click "Save Model" at the top, and then click on "Build Model". -1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. - ## Adding Alexa support to a plugin -See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file +This document assumes some familiarity with the Alexa interface. You can find more information [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide). + +To add alexa support to a plugin the ``init`` should return an object that contains an "alexa" key. Here is an example: + +```javascript +var iob = { + name: 'iob' + , label: 'Insulin-on-Board' + , pluginType: 'pill-major' + , alexa : { + rollupHandlers: [{ + rollupGroup: "Status" + , rollupName: "current iob" + , rollupHandler: alexaIOBRollupHandler + }] + , intentHandlers: [{ + intent: "MetricNow" + , routableSlot: "metric" + , slots: ["iob", "insulin on board"] + , intentHandler: alexaIOBIntentHandler + }] + } +}; +``` + +There are 2 types of handlers that you will need to supply: +* Intent handler - enables you to "teach" Alexa how to respond to a user's question. +* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the Alexa "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### Intent Handlers + +A plugin can expose multiple intent handlers. ++ ``intent`` - this is the intent in the "intent schema" above ++ ``routeableSlot`` - This enables routing by a slot name to the appropriate intent handler for overloaded intents e.g. "What is my " - iob, bg, cob, etc. This value should match the slot named in the "intent schema" ++ ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot ++ ``intenthandler`` - this is a callback function that receives 3 arguments + - ``callback`` Call this at the end of your function. It requires 2 arguments + - ``title`` - Title of the handler. This is the value that will be displayed on the Alexa card + - ``text`` - This is text that Alexa should speak. + - ``slots`` - these are the slots that Alexa detected + - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. + +### Rollup handlers + +A plugin can also expose multiple rollup handlers ++ ``rollupGroup`` - This is the key that is used to aggregate the responses when the intent is invoked ++ ``rollupName`` - This is the name of the handler. Primarily used for debugging ++ ``rollupHandler`` - this is a callback function that receives 3 arguments + - ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot + - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. + - ``callback`` - + - ``error`` - This would be an error message + - ``response`` - A simple object that expects a ``results`` string and a ``priority`` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: + ```javascript + callback(null, {results: "Hello world", priority: 1}); + ``` diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json deleted file mode 100644 index cf90a710b88..00000000000 --- a/docs/plugins/alexa-templates/en-us.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "interactionModel": { - "languageModel": { - "invocationName": "nightscout", - "intents": [ - { - "name": "NSStatus", - "slots": [], - "samples": [ - "How am I doing" - ] - }, - { - "name": "LastLoop", - "slots": [], - "samples": [ - "When was my last loop" - ] - }, - { - "name": "MetricNow", - "slots": [ - { - "name": "metric", - "type": "LIST_OF_METRICS", - "samples": [ - "what {pwd} {metric} is", - "what my {metric} is", - "how {pwd} {metric} is", - "how my {metric} is", - "how much {metric} does {pwd} have", - "how much {metric} I have", - "how much {metric}", - "{pwd} {metric}", - "{metric}", - "my {metric}" - ] - }, - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "how much {metric} does {pwd} have left", - "what's {metric}", - "what's my {metric}", - "how much {metric} is left", - "what's {pwd} {metric}", - "how much {metric}", - "how is {metric}", - "how is my {metric}", - "how is {pwd} {metric}", - "how my {metric} is", - "what is {metric}", - "how much {metric} do I have", - "how much {metric} does {pwd} have", - "how much {metric} I have", - "what is my {metric}", - "what my {metric} is", - "what is {pwd} {metric}" - ] - }, - { - "name": "AMAZON.NavigateHomeIntent", - "samples": [] - } - ], - "types": [ - { - "name": "LIST_OF_METRICS", - "values": [ - { - "name": { - "value": "uploader battery", - "synonyms": [ - "uploader battery remaining", - "uploader battery power" - ] - } - }, - { - "name": { - "value": "pump reservoir", - "synonyms": [ - "remaining insulin", - "insulin remaining", - "insulin is left", - "insulin left", - "insulin in my pump", - "insulin" - ] - } - }, - { - "name": { - "value": "pump battery", - "synonyms": [ - "pump battery remaining", - "pump battery power" - ] - } - }, - { - "name": { - "value": "bg", - "synonyms": [ - "number", - "blood sugar", - "blood glucose" - ] - } - }, - { - "name": { - "value": "iob", - "synonyms": [ - "insulin on board" - ] - } - }, - { - "name": { - "value": "basal", - "synonyms": [ - "current basil", - "basil", - "current basal" - ] - } - }, - { - "name": { - "value": "cob", - "synonyms": [ - "carbs", - "carbs on board", - "carboydrates", - "carbohydrates on board" - ] - } - }, - { - "name": { - "value": "forecast", - "synonyms": [ - "ar2 forecast", - "loop forecast" - ] - } - }, - { - "name": { - "value": "raw bg", - "synonyms": [ - "raw number", - "raw blood sugar", - "raw blood glucose" - ] - } - } - ] - } - ] - }, - "dialog": { - "intents": [ - { - "name": "MetricNow", - "confirmationRequired": false, - "prompts": {}, - "slots": [ - { - "name": "metric", - "type": "LIST_OF_METRICS", - "confirmationRequired": false, - "elicitationRequired": true, - "prompts": { - "elicitation": "Elicit.Slot.1421281086569.34001419564" - } - }, - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME", - "confirmationRequired": false, - "elicitationRequired": false, - "prompts": {} - } - ] - } - ], - "delegationStrategy": "ALWAYS" - }, - "prompts": [ - { - "id": "Elicit.Slot.1421281086569.34001419564", - "variations": [ - { - "type": "PlainText", - "value": "What metric are you looking for?" - }, - { - "type": "PlainText", - "value": "What value are you looking for?" - }, - { - "type": "PlainText", - "value": "What metric do you want to know?" - }, - { - "type": "PlainText", - "value": "What value do you want to know?" - } - ] - } - ] - } -} \ No newline at end of file diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip deleted file mode 100644 index 6a8498b0b19e0c1822efaf4652f737cf282b350d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13230 zcmdT~cRUq(*f)>8S4KtID?6jCNYX*(kvP_|M;Y1U7{>_7jAR~?%n~8lB70TMv4Zp<#BR2 zqX{BJTfp(r>qLC7pi`o$BF<2v;UYfN`_7Q;o#B;J2dg{U*+X^@&;D_E{vN+=jWntJ z57AfLPH&HJHjmrl{bkMXs zt>dJzNXHx4Oz{}ZC@F?OQ$go%nU~BDt$XAS(51GEg_cHb0J}qlsW;ONyDn^9!DI3# zopfuVO0rw0l@0Q4ERbXt?|v{O3hsEv%+?j$=J-LRfyz-4M|}J5_h}Go$xu6yTkO;y zUid`6kIIYAR}fYqvN9FJ4}MrjfCH^LLkM1^kDI>Y zYJY`v=&LUf>eDlB939QksrdA!kpko35(HI=3L^k;L45ydE@=AzAl)+;LoSd&OXx>}>on=T~havwF_XHn>6F3u_k{ z%padxvB-!Yh!-=nZFgT^Czc0NDiKB1h>bcY`pGuR)X~;~E~#n=R7AdR@iGg$69H#% znw6rMm2P9UXkzaswNE7Q!@o2!A=?ljiIG=m^F&CxSEG{{FxQ#tKq~FH8zzc4qq8i$iJ6V41bl<9ecJLKlLD0IHSVXI={Y8-lue#UD zmkSh|`M=ruEv|b&7=nMr zH-v1u#hN-RdvnQW;_~a2TM!eflU&AXp2HtKDqdFH%zvNU=J#%SP2@)0()e}=oLESR~hvhEGilUgNVy*?QfL0*&7w#G%IrRa_}ev6&1M^HHmRy z*%fGYB7}r#xK05X!ScbbEb*?72xkZAjbuWSE{zyrbq1^f0MxMFOb%)Q;61fzZyT-r z_tHnb06?a4Y-V0wSzdS>pp2Ee&9*F-x1piTwxOW`77J*AwV7rTZU>sF^QO`Th#*6V zjtay?!Oyvjd@s=4Ff_J8Ag~u|au8wWz3xuPef(ZcRh3hThqJAvUHyrgsz8rmhoI_Y zHug5B^s{*`bO=s}kd6}4_H#OZ801GWJp%MojDXMie5BY67i4`?u0({>N?ZG7`jav7 zCL|_5jsvMiL}hD(53aTJh@v?*OH;i=W}nLuPzFWi^|HrLe0CWKS5j*2RwddS7(*Y43EuS3Fn5!-9RLdG*r zmm|XaDZ?o%iIsV}@rth=51zrN^j?h00pm2zFO{?|>J!~y7r&#Yo9mqYK43-fbCTOk zM)zBxGnjqNQ*j;G%BFJQlP5IYhjll?hs&8>b~jA-%wHrsC5Zn0*+7Lzz!>^>v-09u zi84>Rm*>VzafqG$FqeAuJ3n`>(I5>3q*WW^g>W=pMC3tc^~38Enu9K|-%<|NDf=x& z=+{Je!GFVO{>AQfxQnnIa1mv7bw@)-CkL>T1H|6J(A5D9u|3p5gv0qseECQNoW1HZ zo&qnYvbPbJm6O4`PGsUyp0W|*-*O(h25WxzWpsVi3A>7o5RE?}5o-Rn>`_L_DoWb|6RxWJSATp*+5~SgnwX0*qBBILBk+DJB?zZnMa#N z@g*NfXU~Fm5y0E1eC%|6@2%+KtmE?CvLL}+Qm;<&TcQA?Dv@hCxQ*ztc@txbK9ARf zeJ|5@VZ~5m^I^SoQ>N{K(K$0=E!9~S#KxvuPK6la=+oIT$A_oFvzI?f%aaU$pTG#v z()WnA#;oFOKRad<311K{9K6>YYK@zoZG4Ga8mLNv8Ler;^%|e2n}>YrLfWz`HIap# zr{lNU)Rf#hy|H=~#lnuvuUnt;($r?vWTk>mjKLUcfh4b*ykl%d!^j44Ux?20dqqiqGp?J&mL5lf6uqAQH-Bf47B0!e+RRQu5N`RO7S`VOWO!SudtnP4n9( zibFsetpEFB_17-q9EY;j?OB|*`S6De)qK;hLhK{#z{nb8FB1I+9NE;FC6#e`cq|R=Yj7QL1#OH)Z6c zPLD#R=a~M(DG;+G1}(&p{M92}Q|`nIlkDvSCgKm?@n2NDF<8>Qnma+KbEhBO*kEF* z_-m;P_`3;V*tcN1+7r@3Cp-jUz1K9K9slCm*2IM$0hz^qS5?hd7bgswRKXI8%(Vtf@mg|PiVoH=2%XpYSt zB`3VtynL{x?(4F`{6~)(#B)g){5ciYo2+gw-K8(LtHbPS9=hZXG>BDxNb8RWBq*W0 zdGA8Be^KL8(fh5F@+T_#_5H%Xji*$$^d`n^phKq1EsNS`h}<7FdHHTVgfHBB6|B92 z5e5#L|3Xc)QK|Z79&S2p&!T$+H=gcpp6>TARuu9#waC(oMw9Ncq%umxvj7S$XZ25a z2$xR28>E}LL@{C;JPUg|PUCTmx&zn!sr$)1Y|vSj?AWopnyQ?d_`|@-2dkGkWp5-GU2p~1ve~D6Q*sXL zB4%`48Sq{t4#vLO?Y*wWrXsanmAWr9Y-k{y((R{KXMeqbOq=_LBD>L)7ksqg{^vaN zV2N5d?aC&m-ldvx_2TL z5+83@+=?dYh1Q>?gPEfSI=OWDvpSgidL+B6-gHRwQp!@I$(_puwNj~_sqICd2gR=5 zT(*baD(@y#DdMZBPH{4bg#bUB4|AXA6$%v$_=+Bu!R$5lN$rh!zECdU_?FZ|GU0Dx}$9j=KyX`t*BP7c# zL|pPUqE5iW15$J=hh7*Iw~?pQ4witC{_s>BjFBaYS$Yxtps&rT4y0;YO;6wtdQm5M zBJi;Zb-Ay$0p(=4Yrk&RL?0SY|R5fCDCX7Vrpw5L9 zrTI{~DW>S_@7j)MjYD;%bQD=Q*(5J#0xl7}Dj5S(0^_IzvsNHA#oX_*A$5e(^|F&|8=}&B!)%1$%dACnSMoH_5c{^*%gx^z|26Rs7CZ|_^ zoDf~h?;?KK%Q6$mtEDUVX=#~I5$dug5J6-%G(j|@5?sWQ!R^_-R){SUiKn8XE5R>M zLcReKaG~}zE*&NTH{Nty$3zyhBCvoZ&Hr7caHQh?bB4)zpisTy+vnvi>JQz_6eUZ~8Y4{}$WgTS-HU2p zRi=#%F$}((%Rf?`ZrU{*Szyu~(H~3M`^duAdqB89UsIa9w8mTk-4e~m)1`!IvOeN& zs=W-W96!XO$eGdU>X)1z2^-GiME>0A(Td!opJ>yt2Q$%F*8%NKV#A?xp*+}tzJN35 zl0szy+@>lz3>e3O)v_nTl3>?mqHR^a;;o*>@k+gf5#hvzkJB{J~S?n6y z+STu|A-VDj3-7dPxIDYCN(-^LmmGpgls`_7U8npmhrWDrWvkuw>)W}4i*Gf5cuv2B z&2kOedVDOK(P1)cc)umSKyu#dxm#{*pT$sNK^LgWi9*Tf2UbzqO7FdqI}M}HN$#w6 z^=m_1IAS7Gi2xUSfdRJcp%>9NEqpo|rzNo8G`q0oSsC*{ewYGGot!&fC%R6OJr#5hvBkroEJKn(nI5O@Kybt)UI!8A({E zE?35V5SQ-GlsPW?T(r&LnAx>xIY~*$(E-Ii&p4Gn;F`GW81Onf{4Te799d51iADk6 zDeHvO8bq6Km-|94b43c9-G3+{Mc*7!nG3M%zJ1^JC2zNFxry=hGv8-Umv~jzhKHFd zjna81ZVkOj)YkU=K0?y!c1_Yc5)6w1N^aPcRj$uiZc5#EUh$1{e_rSNeq)@9*KWq| z19+5o%yT~972TWHbMSnoW9vxh(2Uf=lkbva@5{kn@4vqb)$;gOlAC%xhsMwQnWsJ& zUr3~_r|GNoH)+@#O^6bP%{9zfHQylLq8X*zVsb|GcSub(?KgT%;1TI&ieV8xt-==s zD5YZ(Id$vi2DyN(OIXo4Y1U6La;JP&#+mSAWLayKXR8Y5NIULxI%oy=0LD%buk9G|SiGIQnW> zu~7lm95OX;PenD?@F?7i?}HC0)N7}kKgCm9E(~G~Wq)z42~#afvOepDvO;ivoa#m_ z#(UPTC&GD*M(WEM+E`6#HvOUinp;**wG8&SrLL?3D~dO3@jJDcx^u|i%91e%NMv8c z9;KQKOszYSd_|$&1y|9S4>!l$?3CEP*Wo>^P50fM@*hKGf>g@tNk0yeI< zk>#*ledzZkHB&5p(rI|b2IBoTN?ny-y$e8>0NuKbo;@JVNhj8u)j!7<@hP{5SUB-b z8k(a^rJ}WoxzU-H3?7$vozx;4FX+-GDzEnSH(t8N^(GC6;;P&;OIf{A>=g4Lhbpb+ zyhc&P`)jrA1@~nT%gL8+hEfUwidJ`_{B@yuL41S~L6zn9=ObRsvB23rNTDNhYc;6g z(F~&4S7$gXk_a9ZNq*n8{`GNW|LFbLLAiOsf3!Kf@9eun_?`aIH zYK;VW9c(iD^K#kPIWuGxGv8(tFL#*C-w!B(#+1@T7dOYoi3o9f}({zDt z0=>)%B326b4JL_(79a6Q=v8hzD2GQDEjJ?7L$Hr}0yxA__w=%BsDJ$iz<1Ex_o(oK ze^0;vVmCN^{*PSH*qH|J$+{!yUXwSDV!oO!&B%T}V8Syw&BUh4lIYR;b1Dx%3$tix zUKW~Msxdvkkh0A!F^9)92^J{ZAS}u51`tWZ(X?s^67he=_YzSkiBB~?_|QS}Ok=8+$9>)&U9`rd)cU>=%4vH#k; zGvogw!%3h#LV|z93w193v*FyO`js$$O9ek0jug#r2=JHmQAd}YKN$`uV$p9WC+r5C z-QafkRQvagUl@f49C{V8Q20yl{ip0uXDI9_?B=0L^!r8HGu3}=Re~rS;L!Ugju7bY zGeefis7`q7CuUJ64=_U+hwPLVebaUZoAW<>U#vm-Ja~=EM@=Q|AYp$ z6GSZu5h?NX0gQK`+4I%i26ulUg4#-jPC{MIpJ@675hzR!OQ#*oZn34UHM`aY5vGMmFSs{{zrEGOqvt diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md deleted file mode 100644 index c5dff113ab1..00000000000 --- a/docs/plugins/googlehome-plugin.md +++ /dev/null @@ -1,109 +0,0 @@ -Nightscout Google Home/DialogFlow Plugin -======================================== - -## Overview - -To add Google Home support for your Nightscout site, here's what you need to do: - -1. [Activate the `googlehome` plugin](#activate-the-nightscout-google-home-plugin) on your Nightscout site, so your site will respond correctly to Google's requests. -1. [Create a custom DialogFlow agent](#create-your-dialogflow-agent) that points at your site and defines certain questions you want to be able to ask. - -## Activate the Nightscout Google Home Plugin - -1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 0.13 (VERSION_NAME)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/0.13) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. -1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) - -## Create Your DialogFlow Agent - -1. Download the agent template in your language for Google Home [here](google-home-templates/). - - If you're language doesn't have a template, please consider starting with [the en-us template](google-home-templates/en-us.zip), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. -1. [Sign in to Google's Action Console](https://console.actions.google.com) - - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. -1. Click on the "New Project" button. -1. If prompted, agree to the Terms of Service. -1. Give your project a name (e.g. "Nightscout") and then click "Create project". -1. For the "development experience", select "Conversational" at the bottom of the list. -1. Click on the "Develop" tab at the top of the sreen. -1. Click on "Invocation" in the left navigation pane. -1. Set the display name (e.g. "Night Scout") of your Action and set your Google Assistant voice. - - Unfortunately, the Action name needs to be two words, and is required to be unique across all of Google, even though you won't be publishing this for everyone on Google to use. So you'll have to be creative with the name since "Night Scout" is already taken. -1. Click "Save" in the upper right corner. -1. Navigate to "Actions" in the left nagivation pane, then click on the "Add your first action" button. -1. Make sure you're on "Cutom intent" and then click "Build" to open DialogFlow in a new tab. -1. Sign in with the same Google account you used to sign in to the Actions Console. - - You'll have to go through the account setup steps if this is your first time using DialogFlow. -1. Verify the name for your agent (e.g. "Nightscout") and click "CREATE". -1. In the navigation pane on the left, click the gear icon next to your agent name. -1. Click on the "Export and Import" tab in the main area of the page. -1. Click the "IMPORT FROM ZIP" button. -1. Select the template file downloaded in step 1. -1. Type "IMPORT" where requested and then click the "IMPORT" button. -1. After the import finishes, click the "DONE" button followed by the "SAVE" button. -1. In the navigation pane on the left, click on "Fulfillment". -1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` -1. Scroll down to the bottom of the page and click the "SAVE" button. -1. Click on "Integrations" in the navigation pane. -1. Click on "INTEGRATION SETTINGS" for "Google Assistant". -1. Under "Implicit invocation", add every intent listed. -1. Turn on the toggle for "Auto-preview changes". -1. Click "CLOSE". - -That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I doing?" - -### What questions can you ask it? - -See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. - -## Adding support for additional languages - -If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. - -If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". - -1. Open your DialogFlow agent. - - Get to your list of agents at https://console.dialogflow.com/api-client/#/agents and click on the name of your Nightscout agent. -1. Click on the "Languages" tab. -1. Click the "Add Additional Language" drop-down box. -1. Select your desired language. -1. Click the "SAVE" button. - - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". -1. Click on "Intents" in the left navigation pane. -1. For each intent in the list (NOT including those that start with "Default" in the name): - 1. Click on the intent name. - 1. Note the phrases used in the "Training phrases" section. - - If the phrase has a colored block (e.g. `metric` or `pwd`), click the phrase (but NOT the colored block) and note the "PARAMETER NAME" of the item with the same-colored "ENTITY". - 1. Click on the new language code (beneath the agent name near the top of the navigation pane). - 1. Add equivalent or similar training phrases as those you noted a couple steps ago. - - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: - 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. - 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. - 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the one you determined just a few steps ago. - 1. Press the Enter key to add the phrase. - 1. Click the "SAVE" button. - 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. - 1. Scroll down to the "Action and parameters" section. - 1. If any of the items in that list have the "REQUIRED" option checked: - 1. Click the "Define prompts..." link on the right side of that item. - 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). - 1. Click "CLOSE". - 1. Scroll down to the "Responses" section. - 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. - 1. Click the "SAVE" button at the top of the window. -1. Click on the "Entities" section in the navigation pane. -1. For each entity listed: - 1. Click the entity name. - 1. Switch to the starting language (beneath the agent name near the top of the left navigation pane). - 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". - 1. Select all the text in the text box and copy it. - 1. Switch back to your new language. - 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". - 1. In the text box, paste the text you just copied. - 1. Click the menu icon to the right of the "SAVE" button and click "Switch to editor mode". - 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. - - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. - 1. Click the "SAVE" button. -1. You should be good to go! Feel free to try it out by click the "See how it works in Google Assistant" link in the right navigation pane, or start asking your Google-Home-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. - -## Adding Google Home support to a plugin - -See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md deleted file mode 100644 index a9f2541d8d8..00000000000 --- a/docs/plugins/interacting-with-virtual-assistants.md +++ /dev/null @@ -1,56 +0,0 @@ -Interacting with Virtual Assistants -=================================== - -# Alexa vs. Google Home - -Although these example phrases reference Alexa, the exact same questions could be asked of Google. -Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's name* ..." - -# What questions can you ask it? - -This list is not meant to be comprehensive, nor does it include every way you can ask the questions. To get the full picture, in the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. - -*Forecast:* - -- "Alexa, ask Nightscout how am I doing" -- "Alexa, ask Nightscout how I'm doing" - -*Uploader Battery:* - -- "Alexa, ask Nightscout how is my uploader battery" - -*Pump Battery:* - -- "Alexa, ask Nightscout how is my pump battery" - -*Metrics:* - -- "Alexa, ask Nightscout what my bg is" -- "Alexa, ask Nightscout what my blood glucose is" -- "Alexa, ask Nightscout what my number is" -- "Alexa, ask Nightscout what is my insulin on board" -- "Alexa, ask Nightscout what is my basal" -- "Alexa, ask Nightscout what is my current basal" -- "Alexa, ask Nightscout what is my cob" -- "Alexa, ask Nightscout what is Charlie's carbs on board" -- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" -- "Alexa, ask Nightscout what is Harper's loop forecast" -- "Alexa, ask Nightscout what is Alicia's ar2 forecast" -- "Alexa, ask Nightscout what is Peter's forecast" -- "Alexa, ask Nightscout what is Arden's raw bg" -- "Alexa, ask Nightscout what is Dana's raw blood glucose" - -*Insulin Remaining:* - -- "Alexa, ask Nightscout how much insulin do I have left" -- "Alexa, ask Nightscout how much insulin do I have remaining" -- "Alexa, ask Nightscout how much insulin does Dana have left? -- "Alexa, ask Nightscout how much insulin does Arden have remaining? - -*Last Loop:* - -- "Alexa, ask Nightscout when was my last loop" - -## A note about names - -All the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name. \ No newline at end of file diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index f5a55c214de..65f477ad85d 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -4,163 +4,156 @@ var moment = require('moment'); var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; - var express = require('express') - , api = express.Router( ); - var translate = ctx.language.translate; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.virtAsst) { - if (plugin.virtAsst.intentHandlers) { - console.log('Alexa: Plugin ' + plugin.name + ' supports Virtual Assistants'); - _each(plugin.virtAsst.intentHandlers, function (route) { - if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.metrics); - } - }); - } - if (plugin.virtAsst.rollupHandlers) { - console.log('Alexa: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); - _each(plugin.virtAsst.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Alexa: Plugin ' + plugin.name + ' does not support Virtual Assistants'); - } - }); - - api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Alexa'); - var locale = req.body.request.locale; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - switch (req.body.request.type) { - case 'IntentRequest': - onIntent(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'LaunchRequest': - onLaunch(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'SessionEndedRequest': - onSessionEnded(req.body.request.intent, function (alexaResponse) { - res.json(alexaResponse); - next( ); - }); - break; - } - }); - - ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - - callback(null, {results: status, priority: -1}); - }); - }, 'BG Status'); - - ctx.alexa.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - - callback('Current blood glucose', status); + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.plugins.eachEnabledPlugin(function each(plugin){ + if (plugin.alexa) { + if (plugin.alexa.intentHandlers) { + console.log(plugin.name + ' is Alexa enabled'); + _each(plugin.alexa.intentHandlers, function (route) { + if (route) { + ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); + } + }); + } + if (plugin.alexa.rollupHandlers) { + console.log(plugin.name + ' is Alexa rollup enabled'); + _each(plugin.alexa.rollupHandlers, function (route) { + console.log('Route'); + console.log(route); + if (route) { + ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } + }); + } + } else { + console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); + } }); - }, ['bg', 'blood glucose', 'number']); - ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = req.body.request.locale; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + switch (req.body.request.type) { + case 'IntentRequest': + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'LaunchRequest': + onLaunch(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'SessionEndedRequest': + onSessionEnded(req.body.request.intent, function (alexaResponse) { + res.json(alexaResponse); + next( ); + }); + break; + } }); - }); + ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('alexaStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)) + '.'; + callback(null, {results: status, priority: -1}); + }); + // console.log('BG results called'); + // callback(null, 'BG results'); + }, 'BG Status'); + + ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('alexaStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); + callback('Current blood glucose', status); + }); + }, 'metric', ['bg', 'blood glucose', 'number']); - function onLaunch() { - console.log('Session launched'); - } + ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { + ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { + callback('Full status', status); + }); + }); - function onIntent(intent, next) { - console.log('Received intent request'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); - } - function onSessionEnded() { - console.log('Session ended'); - } + function onLaunch() { + console.log('Session launched'); + } - function handleIntent(intentName, slots, next) { - if (slots.metric.resolutions.resolutionsPerAuthority[0].status.code != "ER_SUCCESS_MATCH"){ - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); } - var metricValues = slots.metric.resolutions.resolutionsPerAuthority[0].values; - if (metricValues.length == 0){ - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); + function onSessionEnded() { + console.log('Session ended'); } - var handler = ctx.alexa.getIntentHandler(intentName, metricValues[0].value.name); - if (handler){ - var sbx = initializeSandbox(); - handler(next, slots, sbx); - } else { - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); + function handleIntent(intentName, slots, next) { + var handler = ctx.alexa.getIntentHandler(intentName, slots); + if (handler){ + var sbx = initializeSandbox(); + handler(next, slots, sbx); + } else { + next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); + } } - } - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - return api; + return api; } -module.exports = configure; \ No newline at end of file +module.exports = configure; diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js deleted file mode 100644 index b03ac42bfe3..00000000000 --- a/lib/api/googlehome/index.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -var moment = require('moment'); -var _each = require('lodash/each'); - -function configure (app, wares, ctx, env) { - var entries = ctx.entries; - var express = require('express') - , api = express.Router( ); - var translate = ctx.language.translate; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.virtAsst) { - if (plugin.virtAsst.intentHandlers) { - console.log('Google Home: Plugin ' + plugin.name + ' supports Virtual Assistants'); - _each(plugin.virtAsst.intentHandlers, function (route) { - if (route) { - ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.metrics); - } - }); - } - if (plugin.virtAsst.rollupHandlers) { - console.log('Google Home: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); - _each(plugin.virtAsst.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.googleHome.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Google Home: Plugin ' + plugin.name + ' does not support Virtual Assistants'); - } - }); - - api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Google Home'); - var locale = req.body.queryResult.languageCode; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric); - if (handler){ - var sbx = initializeSandbox(); - handler(function (title, response) { - res.json(ctx.googleHome.buildSpeechletResponse(response, false)); - next( ); - }, req.body.queryResult.parameters, sbx); - } else { - res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); - next( ); - } - }); - - ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - - callback(null, {results: status, priority: -1}); - }); - }, 'BG Status'); - - ctx.googleHome.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - - callback('Current blood glucose', status); - }); - }, ['bg', 'blood glucose', 'number']); - - ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { - ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); - }); - }); - - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } - - return api; -} - -module.exports = configure; \ No newline at end of file diff --git a/lib/api/index.js b/lib/api/index.js index 4b3d6a4fcb6..f92dda9cfbd 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -65,10 +65,6 @@ function create (env, ctx) { app.all('/alexa*', require('./alexa/')(app, wares, ctx, env)); } - if (ctx.googleHome) { - app.all('/googlehome*', require('./googlehome/')(app, wares, ctx, env)); - } - return app; } diff --git a/lib/language.js b/lib/language.js index 3f8e8a846e4..cb56d3f500f 100644 --- a/lib/language.js +++ b/lib/language.js @@ -667,81 +667,6 @@ function init() { ,tr: 'Son 3 ay' ,zh_cn: '过去3个月' } - , 'between': { - cs: 'between' - ,de: 'between' - ,es: 'between' - ,fr: 'between' - ,el: 'between' - ,pt: 'between' - ,sv: 'between' - ,ro: 'between' - ,bg: 'between' - ,hr: 'between' - ,it: 'between' - ,ja: 'between' - ,dk: 'between' - ,fi: 'between' - ,nb: 'between' - ,he: 'between' - ,pl: 'between' - ,ru: 'between' - ,sk: 'between' - ,nl: 'between' - ,ko: 'between' - ,tr: 'between' - ,zh_cn: 'between' - } - , 'around': { - cs: 'around' - ,de: 'around' - ,es: 'around' - ,fr: 'around' - ,el: 'around' - ,pt: 'around' - ,sv: 'around' - ,ro: 'around' - ,bg: 'around' - ,hr: 'around' - ,it: 'around' - ,ja: 'around' - ,dk: 'around' - ,fi: 'around' - ,nb: 'around' - ,he: 'around' - ,pl: 'around' - ,ru: 'around' - ,sk: 'around' - ,nl: 'around' - ,ko: 'around' - ,tr: 'around' - ,zh_cn: 'around' - } - , 'and': { - cs: 'and' - ,de: 'and' - ,es: 'and' - ,fr: 'and' - ,el: 'and' - ,pt: 'and' - ,sv: 'and' - ,ro: 'and' - ,bg: 'and' - ,hr: 'and' - ,it: 'and' - ,ja: 'and' - ,dk: 'and' - ,fi: 'and' - ,nb: 'and' - ,he: 'and' - ,pl: 'and' - ,ru: 'and' - ,sk: 'and' - ,nl: 'and' - ,ko: 'and' - ,tr: 'and' - ,zh_cn: 'and' - } ,'From' : { cs: 'Od' ,de: 'Von' @@ -13330,33 +13255,7 @@ function init() { , zh_cn: '快速上升' , zh_tw: 'rapidly rising' }, - 'virtAsstUnknown': { - bg: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , cs: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , de: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , dk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , el: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , en: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , es: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , fi: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , fr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , he: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , hr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , it: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , ko: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , nb: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , pl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , pt: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , ro: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , nl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , ru: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , sk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , sv: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , tr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , zh_cn: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - , zh_tw: 'That value is unknown at the moment. Please see your Nightscout site for more details.' - }, - 'virtAsstStatus': { + 'alexaStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' , de: '%1 und bis %3 %2.' @@ -13382,7 +13281,7 @@ function init() { , zh_cn: '%1 和 %2 到 %3.' , zh_tw: '%1 and %2 as of %3.' }, - 'virtAsstBasal': { + 'alexaBasal': { bg: '%1 současný bazál je %2 jednotek za hodinu' , cs: '%1 current basal is %2 units per hour' , de: '%1 aktuelle Basalrate ist %2 Einheiten je Stunde' @@ -13408,7 +13307,7 @@ function init() { , zh_cn: '%1 当前基础率是 %2 U/小时' , zh_tw: '%1 current basal is %2 units per hour' }, - 'virtAsstBasalTemp': { + 'alexaBasalTemp': { bg: '%1 dočasný bazál %2 jednotek za hodinu skončí %3' , cs: '%1 temp basal of %2 units per hour will end %3' , de: '%1 temporäre Basalrate von %2 Einheiten endet %3' @@ -13434,7 +13333,7 @@ function init() { , zh_cn: '%1 临时基础率 %2 U/小时将会在 %3结束' , zh_tw: '%1 temp basal of %2 units per hour will end %3' }, - 'virtAsstIob': { + 'alexaIob': { bg: 'a máte %1 jednotek aktivního inzulínu.' , cs: 'and you have %1 insulin on board.' , de: 'und du hast %1 Insulin wirkend.' @@ -13460,33 +13359,33 @@ function init() { , zh_cn: '并且你有 %1 的活性胰岛素.' , zh_tw: 'and you have %1 insulin on board.' }, - 'virtAsstIobIntent': { - bg: 'Máte %1 jednotek aktivního inzulínu' - , cs: 'You have %1 insulin on board' - , de: 'Du hast noch %1 Insulin wirkend' - , dk: 'Du har %1 insulin i kroppen' - , el: 'You have %1 insulin on board' - , en: 'You have %1 insulin on board' - , es: 'Tienes %1 insulina activa' - , fi: 'Sinulla on %1 aktiivista insuliinia' - , fr: 'You have %1 insulin on board' - , he: 'You have %1 insulin on board' - , hr: 'You have %1 insulin on board' - , it: 'Tu hai %1 insulina attiva' - , ko: 'You have %1 insulin on board' - , nb: 'You have %1 insulin on board' - , pl: 'Masz %1 aktywnej insuliny' - , pt: 'You have %1 insulin on board' - , ro: 'Aveți %1 insulină activă' - , ru: 'вы имеете %1 инсулина в организме' - , sk: 'You have %1 insulin on board' - , sv: 'You have %1 insulin on board' - , nl: 'You have %1 insulin on board' - , tr: 'Sizde %1 aktif insülin var' - , zh_cn: '你有 %1 的活性胰岛素' - , zh_tw: 'You have %1 insulin on board' - }, - 'virtAsstIobUnits': { + 'alexaIobIntent': { + bg: 'Máte %1 jednotek aktivního inzulínu' + , cs: 'You have %1 insulin on board' + , de: 'Du hast noch %1 Insulin wirkend' + , dk: 'Du har %1 insulin i kroppen' + , el: 'You have %1 insulin on board' + , en: 'You have %1 insulin on board' + , es: 'Tienes %1 insulina activa' + , fi: 'Sinulla on %1 aktiivista insuliinia' + , fr: 'You have %1 insulin on board' + , he: 'You have %1 insulin on board' + , hr: 'You have %1 insulin on board' + , it: 'Tu hai %1 insulina attiva' + , ko: 'You have %1 insulin on board' + , nb: 'You have %1 insulin on board' + , pl: 'Masz %1 aktywnej insuliny' + , pt: 'You have %1 insulin on board' + , ro: 'Aveți %1 insulină activă' + , ru: 'вы имеете %1 инсулина в организме' + , sk: 'You have %1 insulin on board' + , sv: 'You have %1 insulin on board' + , nl: 'You have %1 insulin on board' + , tr: 'Sizde %1 aktif insülin var' + , zh_cn: '你有 %1 的活性胰岛素' + , zh_tw: 'You have %1 insulin on board' + }, + 'alexaIobUnits': { bg: '%1 units of' , cs: '%1 jednotek' , de: 'noch %1 Einheiten' @@ -13512,7 +13411,7 @@ function init() { , zh_cn: '%1 单位' , zh_tw: '%1 units of' }, - 'virtAsstPreamble': { + 'alexaPreamble': { bg: 'Your' , cs: 'Vaše' , de: 'Deine' @@ -13538,7 +13437,7 @@ function init() { , zh_cn: '你的' , zh_tw: 'Your' }, - 'virtAsstPreamble3person': { + 'alexaPreamble3person': { bg: '%1 has a ' , cs: '%1 má ' , de: '%1 hat eine' @@ -13564,7 +13463,7 @@ function init() { , zh_cn: '%1 有一个 ' , zh_tw: '%1 has a ' }, - 'virtAsstNoInsulin': { + 'alexaNoInsulin': { bg: 'no' , cs: 'žádný' , de: 'kein' @@ -13590,75 +13489,75 @@ function init() { , zh_cn: '否' , zh_tw: 'no' }, - 'virtAsstUploadBattery': { - bg: 'Your uploader battery is at %1' - , cs: 'Baterie mobilu má %1' - , en: 'Your uploader battery is at %1' - , hr: 'Your uploader battery is at %1' - , de: 'Der Akku deines Uploader Handys ist bei %1' - , dk: 'Din uploaders batteri er %1' - , ko: 'Your uploader battery is at %1' - , nl: 'De batterij van je mobiel is bij %l' - , zh_cn: '你的手机电池电量是 %1 ' - , sv: 'Din uppladdares batteri är %1' - , fi: 'Lähettimen paristoa jäljellä %1' - , ro: 'Bateria uploaderului este la %1' - , pl: 'Twoja bateria ma %1' - , ru: 'батарея загрузчика %1' - , tr: 'Yükleyici piliniz %1' - }, - 'virtAsstReservoir': { - bg: 'You have %1 units remaining' - , cs: 'V zásobníku zbývá %1 jednotek' - , en: 'You have %1 units remaining' - , hr: 'You have %1 units remaining' - , de: 'Du hast %1 Einheiten übrig' - , dk: 'Du har %1 enheder tilbage' - , ko: 'You have %1 units remaining' - , nl: 'Je hebt nog %l eenheden in je reservoir' - , zh_cn: '你剩余%1 U的胰岛素' - , sv: 'Du har %1 enheter kvar' - , fi: '%1 yksikköä insuliinia jäljellä' - , ro: 'Mai aveți %1 unități rămase' - , pl: 'W zbiorniku pozostało %1 jednostek' - , ru: 'остается %1 ед' - , tr: '%1 birim kaldı' - }, - 'virtAsstPumpBattery': { - bg: 'Your pump battery is at %1 %2' - , cs: 'Baterie v pumpě má %1 %2' - , en: 'Your pump battery is at %1 %2' - , hr: 'Your pump battery is at %1 %2' - , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' - , dk: 'Din pumpes batteri er %1 %2' - , ko: 'Your pump battery is at %1 %2' - , nl: 'Je pomp batterij is bij %1 %2' - , zh_cn: '你的泵电池电量是%1 %2' - , sv: 'Din pumps batteri är %1 %2' - , fi: 'Pumppu on %1 %2' - , ro: 'Bateria pompei este la %1 %2' - , pl: 'Bateria pompy jest w %1 %2' - , ru: 'батарея помпы %1 %2' - , tr: 'Pompa piliniz %1 %2' - }, - 'virtAsstLastLoop': { - bg: 'The last successful loop was %1' - , cs: 'Poslední úšpěšné provedení smyčky %1' - , en: 'The last successful loop was %1' - , hr: 'The last successful loop was %1' - , de: 'Der letzte erfolgreiche Loop war %1' - , dk: 'Seneste successfulde loop var %1' - , ko: 'The last successful loop was %1' - , nl: 'De meest recente goede loop was %1' - , zh_cn: '最后一次成功闭环的是在%1' - , sv: 'Senaste lyckade loop var %1' - , fi: 'Viimeisin onnistunut loop oli %1' - , ro: 'Ultima decizie loop implementată cu succes a fost %1' - , pl: 'Ostatnia pomyślna pętla była %1' - , ru: 'недавний успешный цикл был %1' - , tr: 'Son başarılı döngü %1 oldu' - }, - 'virtAsstLoopNotAvailable': { + 'alexaUploadBattery': { + bg: 'Your uploader battery is at %1' + ,cs: 'Baterie mobilu má %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Der Akku deines Uploader Handys ist bei %1' + , dk: 'Din uploaders batteri er %1' + , ko: 'Your uploader battery is at %1' + , nl: 'De batterij van je mobiel is bij %l' + ,zh_cn: '你的手机电池电量是 %1 ' + , sv: 'Din uppladdares batteri är %1' + , fi: 'Lähettimen paristoa jäljellä %1' + , ro: 'Bateria uploaderului este la %1' + , pl: 'Twoja bateria ma %1' + , ru: 'батарея загрузчика %1' + , tr: 'Yükleyici piliniz %1' + }, + 'alexaReservoir': { + bg: 'You have %1 units remaining' + , cs: 'V zásobníku zbývá %1 jednotek' + , en: 'You have %1 units remaining' + , hr: 'You have %1 units remaining' + , de: 'Du hast %1 Einheiten übrig' + , dk: 'Du har %1 enheder tilbage' + , ko: 'You have %1 units remaining' + , nl: 'Je hebt nog %l eenheden in je reservoir' + ,zh_cn: '你剩余%1 U的胰岛素' + , sv: 'Du har %1 enheter kvar' + , fi: '%1 yksikköä insuliinia jäljellä' + , ro: 'Mai aveți %1 unități rămase' + , pl: 'W zbiorniku pozostało %1 jednostek' + , ru: 'остается %1 ед' + , tr: '%1 birim kaldı' + }, + 'alexaPumpBattery': { + bg: 'Your pump battery is at %1 %2' + , cs: 'Baterie v pumpě má %1 %2' + , en: 'Your pump battery is at %1 %2' + , hr: 'Your pump battery is at %1 %2' + , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' + , dk: 'Din pumpes batteri er %1 %2' + , ko: 'Your pump battery is at %1 %2' + , nl: 'Je pomp batterij is bij %1 %2' + ,zh_cn: '你的泵电池电量是%1 %2' + , sv: 'Din pumps batteri är %1 %2' + , fi: 'Pumppu on %1 %2' + , ro: 'Bateria pompei este la %1 %2' + , pl: 'Bateria pompy jest w %1 %2' + , ru: 'батарея помпы %1 %2' + , tr: 'Pompa piliniz %1 %2' + }, + 'alexaLastLoop': { + bg: 'The last successful loop was %1' + , cs: 'Poslední úšpěšné provedení smyčky %1' + , en: 'The last successful loop was %1' + , hr: 'The last successful loop was %1' + , de: 'Der letzte erfolgreiche Loop war %1' + , dk: 'Seneste successfulde loop var %1' + , ko: 'The last successful loop was %1' + , nl: 'De meest recente goede loop was %1' + ,zh_cn: '最后一次成功闭环的是在%1' + , sv: 'Senaste lyckade loop var %1' + , fi: 'Viimeisin onnistunut loop oli %1' + , ro: 'Ultima decizie loop implementată cu succes a fost %1' + , pl: 'Ostatnia pomyślna pętla była %1' + , ru: 'недавний успешный цикл был %1' + , tr: 'Son başarılı döngü %1 oldu' + }, + 'alexaLoopNotAvailable': { bg: 'Loop plugin does not seem to be enabled' , cs: 'Plugin smyčka není patrně povolený' , en: 'Loop plugin does not seem to be enabled' @@ -13667,7 +13566,7 @@ function init() { , dk: 'Loop plugin lader ikke til at være slået til' , ko: 'Loop plugin does not seem to be enabled' , nl: 'De Loop plugin is niet geactiveerd' - , zh_cn: 'Loop插件看起来没有被启用' + ,zh_cn: 'Loop插件看起来没有被启用' , sv: 'Loop plugin verkar inte vara aktiverad' , fi: 'Loop plugin ei ole aktivoitu' , ro: 'Extensia loop pare a fi dezactivată' @@ -13675,7 +13574,7 @@ function init() { , ru: 'плагин ЗЦ Loop не активирован ' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'virtAsstLoopForecast': { + 'alexaLoopForecast': { bg: 'According to the loop forecast you are expected to be %1 over the next %2' , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' , en: 'According to the loop forecast you are expected to be %1 over the next %2' @@ -13684,7 +13583,7 @@ function init() { , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' , ko: 'According to the loop forecast you are expected to be %1 over the next %2' , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' + ,zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' @@ -13692,24 +13591,7 @@ function init() { , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' }, - 'virtAsstAR2Forecast': { - bg: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , cs: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , en: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , hr: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , de: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , dk: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ko: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , nl: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , zh_cn: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , sv: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , fi: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ro: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , pl: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ru: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , tr: 'According to the AR2 forecast you are expected to be %1 over the next %2' - }, - 'virtAsstForecastUnavailable': { + 'alexaForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' , en: 'Unable to forecast with the data that is available' @@ -13718,7 +13600,7 @@ function init() { , dk: 'Det er ikke muligt at forudsige md de tilgængelige data' , ko: 'Unable to forecast with the data that is available' , nl: 'Niet mogelijk om een voorspelling te doen met de data die beschikbaar is' - , zh_cn: '血糖数据不可用,无法预测未来走势' + ,zh_cn: '血糖数据不可用,无法预测未来走势' , sv: 'Förutsägelse ej möjlig med tillgänlig data' , fi: 'Ennusteet eivät ole toiminnassa puuttuvan tiedon vuoksi' , ro: 'Estimarea este imposibilă pe baza datelor disponibile' @@ -13726,14 +13608,14 @@ function init() { , ru: 'прогноз при таких данных невозможен' , tr: 'Mevcut verilerle tahmin edilemedi' }, - 'virtAsstRawBG': { - en: 'Your raw bg is %1' + 'alexaRawBG': { + en: 'Your raw bg is %1' , cs: 'Raw glykémie je %1' , de: 'Dein Rohblutzucker ist %1' , dk: 'Dit raw blodsukker er %1' , ko: 'Your raw bg is %1' , nl: 'Je raw bloedwaarde is %1' - , zh_cn: '你的血糖是 %1' + ,zh_cn: '你的血糖是 %1' , sv: 'Ditt raw blodsocker är %1' , fi: 'Suodattamaton verensokeriarvo on %1' , ro: 'Glicemia brută este %1' @@ -13743,36 +13625,36 @@ function init() { , ru: 'ваши необработанные данные RAW %1' , tr: 'Ham kan şekeriniz %1' }, - 'virtAsstOpenAPSForecast': { + 'alexaOpenAPSForecast': { en: 'The OpenAPS Eventual BG is %1' , cs: 'OpenAPS Eventual BG je %1' , de: 'Der von OpenAPS vorhergesagte Blutzucker ist %1' , dk: 'OpenAPS forventet blodsukker er %1' , ko: 'The OpenAPS Eventual BG is %1' , nl: 'OpenAPS uiteindelijke bloedglucose van %1' - , zh_cn: 'OpenAPS 预测最终血糖是 %1' + ,zh_cn: 'OpenAPS 预测最终血糖是 %1' , sv: 'OpenAPS slutgiltigt blodsocker är %1' , fi: 'OpenAPS verensokeriarvio on %1' , ro: 'Glicemia estimată de OpenAPS este %1' - , bg: 'The OpenAPS Eventual BG is %1' - , hr: 'The OpenAPS Eventual BG is %1' + ,bg: 'The OpenAPS Eventual BG is %1' + ,hr: 'The OpenAPS Eventual BG is %1' , pl: 'Glikemia prognozowana przez OpenAPS wynosi %1' , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' }, - 'virtAsstCOB': { + 'alexaCOB': { en: '%1 %2 carbohydrates on board' , cs: '%1 %2 aktivních sachridů' , de: '%1 %2 Gramm Kohlenhydrate wirkend.' , dk: '%1 %2 gram aktive kulhydrater' , ko: '%1 %2 carbohydrates on board' , nl: '%1 %2 actieve koolhydraten' - , zh_cn: '%1 %2 活性碳水化合物' + ,zh_cn: '%1 %2 活性碳水化合物' , sv: '%1 %2 gram aktiva kolhydrater' , fi: '%1 %2 aktiivista hiilihydraattia' , ro: '%1 %2 carbohidrați activi în corp' - , bg: '%1 %2 carbohydrates on board' - , hr: '%1 %2 carbohydrates on board' + ,bg: '%1 %2 carbohydrates on board' + ,hr: '%1 %2 carbohydrates on board' , pl: '%1 %2 aktywnych węglowodanów' , ru: '%1 $2 активных углеводов' , tr: '%1 %2 aktif karbonhidrat' diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index d41aa567885..38ae449c249 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -1,50 +1,60 @@ var _ = require('lodash'); var async = require('async'); -function init (env, ctx) { - console.log('Configuring Alexa...'); +function init(env, ctx) { + console.log('Configuring Alexa.'); function alexa() { return alexa; } var intentHandlers = {}; var rollup = {}; - // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. - alexa.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { - if (!intentHandlers[intent]) { + // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues + // are the values that determine the routing. This allows for specific intent handlers based on the value of a + // specific slot. Routing is only supported on one slot for now. + // There is no protection for a previously configured handler - one plugin can overwrite the handler of another + // plugin. + alexa.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { + if (! intentHandlers[intent]) { intentHandlers[intent] = {}; } - if (metrics) { - for (var i = 0, len = metrics.length; i < len; i++) { - if (!intentHandlers[intent][metrics[i]]) { - intentHandlers[intent][metrics[i]] = {}; + if (routableSlot && slotValues) { + for (var i = 0, len = slotValues.length; i < len; i++) { + if (! intentHandlers[intent][routableSlot]) { + intentHandlers[intent][routableSlot] = {}; } - console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); - intentHandlers[intent][metrics[i]].handler = handler; + if (!intentHandlers[intent][routableSlot][slotValues[i]]) { + intentHandlers[intent][routableSlot][slotValues[i]] = {}; + } + intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; } } else { - console.log('Storing handler for intent \'' + intent + '\''); intentHandlers[intent].handler = handler; } }; - // This function retrieves a handler based on the intent name and metric requested. - alexa.getIntentHandler = function getIntentHandler(intentName, metric) { - console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + // This function retrieves a handler based on the intent name and slots requested. + alexa.getIntentHandler = function getIntentHandler(intentName, slots) { if (intentName && intentHandlers[intentName]) { - if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { - console.log('Found!'); - return intentHandlers[intentName][metric].handler - } else if (intentHandlers[intentName].handler) { - console.log('Found!'); + if (slots) { + var slotKeys = Object.keys(slots); + for (var i = 0, len = slotKeys.length; i < len; i++) { + if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && + intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && + intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { + + return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; + } + } + } + if (intentHandlers[intentName].handler) { return intentHandlers[intentName].handler; } - console.log('Not found!'); return null; } else { - console.log('Not found!'); return null; } + }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { @@ -53,6 +63,7 @@ function init (env, ctx) { rollup[rollupGroup] = []; } rollup[rollupGroup].push({handler: handler, name: rollupName}); + // status = _.orderBy(status, ['priority'], ['asc']) }; alexa.getRollup = function(rollupGroup, sbx, slots, locale, callback) { @@ -99,4 +110,4 @@ function init (env, ctx) { return alexa; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index cab989c8150..e25a2b36229 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -146,7 +146,7 @@ function init (ctx) { return result.points; }; - function virtAsstAr2Handler (next, slots, sbx) { + function alexaAr2Handler (next, slots, sbx) { if (sbx.properties.ar2.forecast.predicted) { var forecast = sbx.properties.ar2.forecast.predicted; var max = forecast[0].mgdl; @@ -163,29 +163,19 @@ function init (ctx) { maxForecastMills = forecast[i].mills; } } - var value = ''; - if (min === max) { - value = translate('around') + ' ' + max; - } else { - value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; - } - var response = translate('virtAsstAR2Forecast', { - params: [ - value - , moment(maxForecastMills).from(moment(sbx.time)) - ] - }); + var response = 'You are expected to be between ' + min + ' and ' + max + ' over the ' + moment(maxForecastMills).from(moment(sbx.time)); next('AR2 Forecast', response); } else { - next('AR2 Forecast', translate('virtAsstUnknown')); + next('AR2 Forecast', 'AR2 plugin does not seem to be enabled'); } } - ar2.virtAsst = { + ar2.alexa = { intentHandlers: [{ intent: 'MetricNow' - , metrics: ['ar2 forecast', 'forecast'] - , intentHandler: virtAsstAr2Handler + , routableSlot: 'metric' + , slots: ['ar2 forecast', 'forecast'] + , intentHandler: alexaAr2Handler }] }; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index c7c54da75c7..73c9492ea44 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -112,16 +112,16 @@ function init (ctx) { function basalMessage(slots, sbx) { var basalValue = sbx.data.profile.getTempBasal(sbx.time); - var response = translate('virtAsstUnknown'); + var response = 'Unable to determine current basal'; var preamble = ''; if (basalValue.treatment) { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { params: [ slots.pwd.value ] - }) : translate('virtAsstPreamble'); + }) : translate('alexaPreamble'); var minutesLeft = moment(basalValue.treatment.endmills).from(moment(sbx.time)); - response = translate('virtAsstBasalTemp', { + response = translate('alexaBasalTemp', { params: [ preamble, basalValue.totalbasal, @@ -129,12 +129,12 @@ function init (ctx) { ] }); } else { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { params: [ slots.pwd.value ] - }) : translate('virtAsstPreamble'); - response = translate('virtAsstBasal', { + }) : translate('alexaPreamble'); + response = translate('alexaBasal', { params: [ preamble, basalValue.totalbasal @@ -144,28 +144,30 @@ function init (ctx) { return response; } - function virtAsstRollupCurrentBasalHandler (slots, sbx, callback) { + function alexaRollupCurrentBasalHandler (slots, sbx, callback) { callback(null, {results: basalMessage(slots, sbx), priority: 1}); } - function virtAsstCurrentBasalhandler (next, slots, sbx) { + function alexaCurrentBasalhandler (next, slots, sbx) { next('Current Basal', basalMessage(slots, sbx)); } - basal.virtAsst = { + basal.alexa = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current basal' - , rollupHandler: virtAsstRollupCurrentBasalHandler + , rollupHandler: alexaRollupCurrentBasalHandler }], intentHandlers: [{ intent: 'MetricNow' - , metrics: ['basal', 'current basal'] - , intentHandler: virtAsstCurrentBasalhandler + , routableSlot:'metric' + , slots:['basal', 'current basal'] + , intentHandler: alexaCurrentBasalhandler }] }; return basal; } + module.exports = init; diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index ef9b4252be5..f3bb1902ad6 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -292,7 +292,7 @@ function init (ctx) { }); }; - function virtAsstCOBHandler (next, slots, sbx) { + function alexaCOBHandler (next, slots, sbx) { var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; var value = 'no'; if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { @@ -302,11 +302,12 @@ function init (ctx) { next('Current COB', response); } - cob.virtAsst = { + cob.alexa = { intentHandlers: [{ intent: 'MetricNow' - , metrics: ['cob', 'carbs on board', 'carbohydrates on board'] - , intentHandler: virtAsstCOBHandler + , routableSlot: 'metric' + , slots: ['cob', 'carbs on board', 'carbohydrates on board'] + , intentHandler: alexaCOBHandler }] }; diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js deleted file mode 100644 index 8e8181512c8..00000000000 --- a/lib/plugins/googlehome.js +++ /dev/null @@ -1,97 +0,0 @@ -var _ = require('lodash'); -var async = require('async'); - -function init (env, ctx) { - console.log('Configuring Google Home...'); - function googleHome() { - return googleHome; - } - var intentHandlers = {}; - var rollup = {}; - - // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. - googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { - if (!intentHandlers[intent]) { - intentHandlers[intent] = {}; - } - if (metrics) { - for (var i = 0, len = metrics.length; i < len; i++) { - if (!intentHandlers[intent][metrics[i]]) { - intentHandlers[intent][metrics[i]] = {}; - } - console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); - intentHandlers[intent][metrics[i]].handler = handler; - } - } else { - console.log('Storing handler for intent \'' + intent + '\''); - intentHandlers[intent].handler = handler; - } - }; - - // This function retrieves a handler based on the intent name and metric requested. - googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { - console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); - if (intentName && intentHandlers[intentName]) { - if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { - console.log('Found!'); - return intentHandlers[intentName][metric].handler - } else if (intentHandlers[intentName].handler) { - console.log('Found!'); - return intentHandlers[intentName].handler; - } - console.log('Not found!'); - return null; - } else { - console.log('Not found!'); - return null; - } - }; - - googleHome.addToRollup = function(rollupGroup, handler, rollupName) { - if (!rollup[rollupGroup]) { - console.log('Creating the rollup group: ', rollupGroup); - rollup[rollupGroup] = []; - } - rollup[rollupGroup].push({handler: handler, name: rollupName}); - }; - - googleHome.getRollup = function(rollupGroup, sbx, slots, locale, callback) { - var handlers = _.map(rollup[rollupGroup], 'handler'); - console.log('Rollup array for ', rollupGroup); - console.log(rollup[rollupGroup]); - var nHandlers = []; - _.each(handlers, function (handler) { - nHandlers.push(handler.bind(null, slots, sbx)); - }); - async.parallelLimit(nHandlers, 10, function(err, results) { - if (err) { - console.error('Error: ', err); - } - callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); - }); - }; - - // This creates the expected Google Home response - googleHome.buildSpeechletResponse = function buildSpeechletResponse(output, expectUserResponse) { - return { - payload: { - google: { - expectUserResponse: expectUserResponse, - richResponse: { - items: [ - { - simpleResponse: { - textToSpeech: output - } - } - ] - } - } - } - }; - }; - - return googleHome; -} - -module.exports = init; \ No newline at end of file diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 614cc56981b..f9bf082d0f4 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -243,19 +243,21 @@ function init(ctx) { }; - function virtAsstIOBIntentHandler (callback, slots, sbx) { + function alexaIOBIntentHandler (callback, slots, sbx) { - var message = translate('virtAsstIobIntent', { + var message = translate('alexaIobIntent', { params: [ + //preamble, getIob(sbx) ] }); + //preamble + + ' insulin on board'; callback('Current IOB', message); } - function virtAsstIOBRollupHandler (slots, sbx, callback) { + function alexaIOBRollupHandler (slots, sbx, callback) { var iob = getIob(sbx); - var message = translate('virtAsstIob', { + var message = translate('alexaIob', { params: [iob] }); callback(null, {results: message, priority: 2}); @@ -263,25 +265,26 @@ function init(ctx) { function getIob(sbx) { if (sbx.properties.iob && sbx.properties.iob.iob !== 0) { - return translate('virtAsstIobUnits', { + return translate('alexaIobUnits', { params: [ utils.toFixed(sbx.properties.iob.iob) ] }); } - return translate('virtAsstNoInsulin'); + return translate('alexaNoInsulin'); } - iob.virtAsst = { + iob.alexa = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current iob' - , rollupHandler: virtAsstIOBRollupHandler + , rollupHandler: alexaIOBRollupHandler }] , intentHandlers: [{ intent: 'MetricNow' - , metrics: ['iob', 'insulin on board'] - , intentHandler: virtAsstIOBIntentHandler + , routableSlot: 'metric' + , slots: ['iob', 'insulin on board'] + , intentHandler: alexaIOBIntentHandler }] }; diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index 02a37b1f16a..82ed74ecb3c 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -506,7 +506,7 @@ function init (ctx) { } }; - function virtAsstForecastHandler (next, slots, sbx) { + function alexaForecastHandler (next, slots, sbx) { if (sbx.properties.loop.lastLoop.predicted) { var forecast = sbx.properties.loop.lastLoop.predicted.values; var max = forecast[0]; @@ -516,7 +516,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', translate('virtAsstForecastUnavailable')); + next('Loop Forecast', 'Unable to forecast with the data that is available'); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -528,45 +528,33 @@ function init (ctx) { } var value = ''; if (min === max) { - value = translate('around') + ' ' + max; + value = 'around ' + max; } else { - value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; + value = 'between ' + min + ' and ' + max; } - var response = translate('virtAsstLoopForecast', { - params: [ - value - , moment(endPrediction).from(moment(sbx.time)) - ] - }); + var response = 'According to the loop forecast you are expected to be ' + value + ' over the next ' + moment(endPrediction).from(moment(sbx.time)); next('Loop Forecast', response); } } else { - next('Loop Forecast', translate('virtAsstUnknown')); + next('Loop forecast', 'Loop plugin does not seem to be enabled'); } } - function virtAsstLastLoopHandler (next, slots, sbx) { - if (sbx.properties.loop.lastLoop) { - console.log(JSON.stringify(sbx.properties.loop.lastLoop)); - var response = translate('virtAsstLastLoop', { - params: [ - moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) - ] - }); - next('Last Loop', response); - } else { - next('Last Loop', translate('virtAsstUnknown')); - } + function alexaLastLoopHandler (next, slots, sbx) { + console.log(JSON.stringify(sbx.properties.loop.lastLoop)); + var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); + next('Last loop', response); } - loop.virtAsst = { + loop.alexa = { intentHandlers: [{ intent: 'MetricNow' - , metrics: ['loop forecast', 'forecast'] - , intentHandler: virtAsstForecastHandler + , routableSlot: 'metric' + , slots: ['loop forecast', 'forecast'] + , intentHandler: alexaForecastHandler }, { intent: 'LastLoop' - , intentHandler: virtAsstLastLoopHandler + , intentHandler: alexaLastLoopHandler }] }; diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 64b8dccdd12..77f27288b14 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -516,41 +516,36 @@ function init (ctx) { } }; - function virtAsstForecastHandler (next, slots, sbx) { + function alexaForecastHandler (next, slots, sbx) { if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { - var response = translate('virtAsstOpenAPSForecast', { + var response = translate('alexaOpenAPSForecast', { params: [ sbx.properties.openaps.lastEventualBG ] }); next('Loop Forecast', response); - } else { - next('Loop Forecast', translate('virtAsstUnknown')); } } - function virtAsstLastLoopHandler (next, slots, sbx) { - if (sbx.properties.openaps.lastLoopMoment) { - console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); - var response = translate('virtAsstLastLoop', { - params: [ - moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) - ] - }); - next('Last Loop', response); - } else { - next('Last Loop', translate('virtAsstUnknown')); - } + function alexaLastLoopHandler (next, slots, sbx) { + console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); + var response = translate('alexaLastLoop', { + params: [ + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) + ] + }); + next('Last loop', response); } - openaps.virtAsst = { + openaps.alexa = { intentHandlers: [{ intent: 'MetricNow' - , metrics: ['openaps forecast', 'forecast'] - , intentHandler: virtAsstForecastHandler + , routableSlot: 'metric' + , slots: ['openaps forecast', 'forecast'] + , intentHandler: alexaForecastHandler }, { intent: 'LastLoop' - , intentHandler: virtAsstLastLoopHandler + , intentHandler: alexaLastLoopHandler }] }; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 840afa8aeae..842d8536b6b 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -135,57 +135,38 @@ function init (ctx) { }); }; - function virtAsstReservoirHandler (next, slots, sbx) { - if (sbx.properties.pump.pump.reservoir) { - var response = translate('virtAsstReservoir', { + function alexaReservoirHandler (next, slots, sbx) { + var response = translate('alexaReservoir', { params: [ - reservoir + sbx.properties.pump.pump.reservoir ] - }); - next('Remaining Insulin', response); - } else { - next('Remaining Insulin', translate('virtAsstUnknown')); - } + }); + next('Remaining insulin', response); } - function virtAsstBatteryHandler (next, slots, sbx) { + function alexaBatteryHandler (next, slots, sbx) { var battery = _.get(sbx, 'properties.pump.data.battery'); if (battery) { - var response = translate('virtAsstPumpBattery', { + var response = translate('alexaPumpBattery', { params: [ battery.value, battery.unit ] }); - next('Pump Battery', response); + next('Pump battery', response); } else { - next('Pump Battery', translate('virtAsstUnknown')); + next(); } } - pump.virtAsst = { - intentHandlers:[ - { - // backwards compatibility - intent: 'InsulinRemaining', - intentHandler: virtAsstReservoirHandler - } - , { - // backwards compatibility - intent: 'PumpBattery', - intentHandler: virtAsstBatteryHandler - } - , { - intent: 'MetricNow' - , metrics: ['pump reservoir'] - , intentHandler: virtAsstReservoirHandler - } - , { - intent: 'MetricNow' - , metrics: ['pump battery'] - , intentHandler: virtAsstBatteryHandler - } - ] + pump.alexa = { + intentHandlers:[{ + intent: 'InsulinRemaining', + intentHandler: alexaReservoirHandler + }, { + intent: 'PumpBattery', + intentHandler: alexaBatteryHandler + }] }; function statusClass (level) { diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 997cf7c55a3..f19e669f63b 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -106,20 +106,17 @@ function init (ctx) { return display; }; - function virtAsstRawBGHandler (next, slots, sbx) { - if (sbx.properties.rawbg.mgdl) { - var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; - next('Current Raw BG', response); - } else { - next('Current Raw BG', translate('virtAsstUnknown')); - } + function alexaRawBGHandler (next, slots, sbx) { + var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; + next('Current Raw BG', response); } - rawbg.virtAsst = { + rawbg.alexa = { intentHandlers: [{ intent: 'MetricNow' - , metrics:['raw bg', 'raw blood glucose'] - , intentHandler: virtAsstRawBGHandler + , routableSlot:'metric' + , slots:['raw bg', 'raw blood glucose'] + , intentHandler: alexaRawBGHandler }] }; diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 1bd8795ef7c..eda42a3901f 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -221,28 +221,16 @@ function init() { }); }; - function virtAsstUploaderBatteryHandler (next, slots, sbx) { - if (sbx.properties.upbat.display) { - var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; - next('Uploader Battery', response); - } else { - next('Uploader Battery', translate('virtAsstUnknown')); - } + function alexaUploaderBatteryHandler (next, slots, sbx) { + var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; + next('Uploader battery', response); } - upbat.virtAsst = { - intentHandlers: [ - { - // for backwards compatibility - intent: 'UploaderBattery' - , intentHandler: virtAsstUploaderBatteryHandler - } - , { - intent: 'MetricNow' - , metrics: ['uploader battery'] - , intentHandler: virtAsstUploaderBatteryHandler - } - ] + upbat.alexa = { + intentHandlers: [{ + intent: 'UploaderBattery' + , intentHandler: alexaUploaderBatteryHandler + }] }; return upbat; diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 2bb63ad78f0..af5f954dd1b 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -183,10 +183,6 @@ function boot (env, language) { ctx.alexa = require('../plugins/alexa')(env, ctx); } - if (env.settings.isEnabled('googlehome')) { - ctx.googleHome = require('../plugins/googlehome')(env, ctx); - } - next( ); } diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 01f4f3d41a1..9dbf6de14cd 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -147,18 +147,18 @@ describe('ar2', function ( ) { done(); }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var now = Date.now(); var before = now - FIVE_MINS; ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); - ar2.virtAsst.intentHandlers.length.should.equal(1); + ar2.alexa.intentHandlers.length.should.equal(1); - ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + ar2.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); - response.should.equal('According to the AR2 forecast you are expected to be between 109 and 120 over the next in 30 minutes'); + response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); done(); }, [], sbx); }); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index fa97f84274e..0bcfd3bc268 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -77,7 +77,7 @@ describe('basalprofile', function ( ) { }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var data = {}; var ctx = { @@ -92,14 +92,14 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; - basal.virtAsst.intentHandlers.length.should.equal(1); - basal.virtAsst.rollupHandlers.length.should.equal(1); + basal.alexa.intentHandlers.length.should.equal(1); + basal.alexa.rollupHandlers.length.should.equal(1); - basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + basal.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); - basal.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + basal.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('Your current basal is 0.175 units per hour'); response.priority.should.equal(1); diff --git a/tests/cob.test.js b/tests/cob.test.js index 54fbcb6c50d..dbbecda0b67 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -97,7 +97,7 @@ describe('COB', function ( ) { }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var data = { treatments: [{ carbs: '8' @@ -110,9 +110,9 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); - cob.virtAsst.intentHandlers.length.should.equal(1); + cob.alexa.intentHandlers.length.should.equal(1); - cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + cob.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); done(); diff --git a/tests/iob.test.js b/tests/iob.test.js index b6c5c2430ec..30872e4fb4d 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -10,7 +10,7 @@ describe('IOB', function() { var iob = require('../lib/plugins/iob')(ctx); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var sbx = { properties: { @@ -20,14 +20,14 @@ describe('IOB', function() { } }; - iob.virtAsst.intentHandlers.length.should.equal(1); - iob.virtAsst.rollupHandlers.length.should.equal(1); + iob.alexa.intentHandlers.length.should.equal(1); + iob.alexa.rollupHandlers.length.should.equal(1); - iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + iob.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); - iob.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + iob.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('and you have 1.50 units of insulin on board.'); response.priority.should.equal(2); diff --git a/tests/loop.test.js b/tests/loop.test.js index 71bc0860bda..9c65ff9bdd1 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -243,7 +243,7 @@ describe('loop', function ( ) { done(); }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -255,14 +255,14 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); - loop.virtAsst.intentHandlers.length.should.equal(2); + loop.alexa.intentHandlers.length.should.equal(2); - loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + loop.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); - loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last Loop'); + loop.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last loop'); response.should.equal('The last successful loop was a few seconds ago'); done(); }, [], sbx); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index 5c76deeaaf3..ed3dd6d3b9f 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -370,7 +370,7 @@ describe('openaps', function ( ) { done(); }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -382,14 +382,14 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); - openaps.virtAsst.intentHandlers.length.should.equal(2); + openaps.alexa.intentHandlers.length.should.equal(2); - openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + openaps.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); - openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last Loop'); + openaps.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last loop'); response.should.equal('The last successful loop was 2 minutes ago'); done(); }, [], sbx); diff --git a/tests/pump.test.js b/tests/pump.test.js index 374ba03f06c..c6def822058 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -254,7 +254,7 @@ describe('pump', function ( ) { done(); }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -266,28 +266,16 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); - pump.virtAsst.intentHandlers.length.should.equal(4); + pump.alexa.intentHandlers.length.should.equal(2); - pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Remaining Insulin'); + pump.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Remaining insulin'); response.should.equal('You have 86.4 units remaining'); - pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Pump Battery'); + pump.alexa.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Pump battery'); response.should.equal('Your pump battery is at 1.52 volts'); - - pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { - title.should.equal('Remaining Insulin'); - response.should.equal('You have 86.4 units remaining'); - - pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { - title.should.equal('Pump Battery'); - response.should.equal('Your pump battery is at 1.52 volts'); - done(); - }, [], sbx); - - }, [], sbx); - + done(); }, [], sbx); }, [], sbx); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index 48c21186cc5..ab91d2bf722 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -35,16 +35,16 @@ describe('Raw BG', function ( ) { }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var sandbox = require('../lib/sandbox')(); var sbx = sandbox.clientInit(ctx, Date.now(), data); rawbg.setProperties(sbx); - rawbg.virtAsst.intentHandlers.length.should.equal(1); + rawbg.alexa.intentHandlers.length.should.equal(1); - rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + rawbg.alexa.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 42d18bb0854..9b48c3b845e 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -93,7 +93,7 @@ describe('Uploader Battery', function ( ) { upbat.updateVisualisation(sbx); }); - it('should handle virtAsst requests', function (done) { + it('should handle alexa requests', function (done) { var ctx = { settings: {} @@ -106,19 +106,13 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); - upbat.virtAsst.intentHandlers.length.should.equal(2); + upbat.alexa.intentHandlers.length.should.equal(1); - upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Uploader Battery'); + upbat.alexa.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Uploader battery'); response.should.equal('Your uploader battery is at 20%'); - - upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Uploader Battery'); - response.should.equal('Your uploader battery is at 20%'); - - done(); - }, [], sbx); - + + done(); }, [], sbx); }); From 7770ff91b43f289f77163956e661b949d90456a5 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 21 Oct 2019 13:21:40 -0700 Subject: [PATCH 065/134] Improved logic to handle Alexa intents --- lib/api/alexa/index.js | 33 +++++++++++++++++++++++---------- lib/language.js | 34 ++++++++++++++++++++++++++++++++++ lib/plugins/alexa.js | 28 ++++++++++++++++++---------- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index f5a55c214de..1cce2764011 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -120,8 +120,10 @@ function configure (app, wares, ctx, env) { }); - function onLaunch() { + function onLaunch(intent, next) { console.log('Session launched'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); } function onIntent(intent, next) { @@ -135,21 +137,32 @@ function configure (app, wares, ctx, env) { } function handleIntent(intentName, slots, next) { - if (slots.metric.resolutions.resolutionsPerAuthority[0].status.code != "ER_SUCCESS_MATCH"){ - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); - } - - var metricValues = slots.metric.resolutions.resolutionsPerAuthority[0].values; - if (metricValues.length == 0){ - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); + var metric; + if (slots) { + if (slots.metric + && slots.metric.resolutions + && slots.metric.resolutions.resolutionsPerAuthority + && slots.metric.resolutions.resolutionsPerAuthority.length + && slots.metric.resolutions.resolutionsPerAuthority[0].status + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code == "ER_SUCCESS_MATCH" + && slots.metric.resolutions.resolutionsPerAuthority[0].values + && slots.metric.resolutions.resolutionsPerAuthority[0].values.length + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name + ){ + metric = slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + } } - var handler = ctx.alexa.getIntentHandler(intentName, metricValues[0].value.name); + var handler = ctx.alexa.getIntentHandler(intentName, metric); if (handler){ var sbx = initializeSandbox(); handler(next, slots, sbx); } else { - next('Unknown Intent', 'I\'m sorry. I don\'t know what you\'re asking for.'); + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); } } diff --git a/lib/language.js b/lib/language.js index 9227f4ae33d..97a9632e5e9 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13796,6 +13796,40 @@ function init() { , ru: '%1 $2 активных углеводов' , tr: '%1 %2 aktif karbonhidrat' }, + 'virtAsstUnknownIntentTitle': { + en: 'Unknown Intent' + , cs: 'Unknown Intent' + , de: 'Unknown Intent' + , dk: 'Unknown Intent' + , ko: 'Unknown Intent' + , nl: 'Unknown Intent' + , zh_cn: 'Unknown Intent' + , sv: 'Unknown Intent' + , fi: 'Unknown Intent' + , ro: 'Unknown Intent' + , bg: 'Unknown Intent' + , hr: 'Unknown Intent' + , pl: 'Unknown Intent' + , ru: 'Unknown Intent' + , tr: 'Unknown Intent' + }, + 'virtAsstUnknownIntentText': { + en: 'I\'m sorry, I don\'t know what you\'re asking for.' + , cs: 'I\'m sorry, I don\'t know what you\'re asking for.' + , de: 'I\'m sorry, I don\'t know what you\'re asking for.' + , dk: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ko: 'I\'m sorry, I don\'t know what you\'re asking for.' + , nl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , zh_cn: 'I\'m sorry, I don\'t know what you\'re asking for.' + , sv: 'I\'m sorry, I don\'t know what you\'re asking for.' + , fi: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ro: 'I\'m sorry, I don\'t know what you\'re asking for.' + , bg: 'I\'m sorry, I don\'t know what you\'re asking for.' + , hr: 'I\'m sorry, I don\'t know what you\'re asking for.' + , pl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ru: 'I\'m sorry, I don\'t know what you\'re asking for.' + , tr: 'I\'m sorry, I don\'t know what you\'re asking for.' + }, 'Fat [g]': { cs: 'Tuk [g]' ,de: 'Fett [g]' diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index d41aa567885..f85bc685eec 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -30,21 +30,29 @@ function init (env, ctx) { // This function retrieves a handler based on the intent name and metric requested. alexa.getIntentHandler = function getIntentHandler(intentName, metric) { - console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); - if (intentName && intentHandlers[intentName]) { - if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { - console.log('Found!'); - return intentHandlers[intentName][metric].handler - } else if (intentHandlers[intentName].handler) { + if (metric === null) { + console.log('Looking for handler for intent \'' + intentName + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName].handler + ) { console.log('Found!'); return intentHandlers[intentName].handler; } - console.log('Not found!'); - return null; } else { - console.log('Not found!'); - return null; + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName][metric] + && intentHandlers[intentName][metric].handler + ) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } } + + console.log('Not found!'); + return null; }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { From 8b1bbb564967073c4c9be61b5f223d54ef78d771 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 21 Oct 2019 13:27:34 -0700 Subject: [PATCH 066/134] Clarification --- docs/plugins/add-virtual-assistant-support-to-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md index 764cfc7c4ea..4ca0c0542fa 100644 --- a/docs/plugins/add-virtual-assistant-support-to-plugin.md +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -18,7 +18,7 @@ iob.virtAsst = { }; ``` -There are 2 types of handlers that you will need to supply: +There are 2 types of handlers that you can supply: * Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. * A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. From 376a71200faaa4797cdc6e8ef0e3179128bb4c64 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 21 Oct 2019 13:29:37 -0700 Subject: [PATCH 067/134] Logic correction --- lib/plugins/alexa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index f85bc685eec..da7bbdca2f1 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -30,7 +30,7 @@ function init (env, ctx) { // This function retrieves a handler based on the intent name and metric requested. alexa.getIntentHandler = function getIntentHandler(intentName, metric) { - if (metric === null) { + if (metric === undefined) { console.log('Looking for handler for intent \'' + intentName + '\''); if (intentName && intentHandlers[intentName] From e10d4838a8f9572e4646741715e5db624e98b45f Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Tue, 22 Oct 2019 00:10:06 +0300 Subject: [PATCH 068/134] Fix tests failing in dev (#5127) * * Fix reporting test to match the new output * Fix number formatter to return the expected 0 when data is NaN * Remove excessive logging during test runs from cob plugin * Run all tests in one go * Fixes D3 invocations for tests. Note this also removes transition timers (most of which seemed to be timed so fast they actually had no effect as the timer was faster than what it takes to draw a frame in Nightscout) * Run tests independently * Try not running tap-set-exit * Remove tap-set-exit --- Makefile | 2 +- lib/client/chart.js | 21 ++------ lib/client/index.js | 21 ++++---- lib/client/renderer.js | 42 +++++++-------- lib/plugins/cob.js | 1 - lib/plugins/pluginbase.js | 6 +-- lib/report_plugins/daytoday.js | 8 +-- lib/report_plugins/weektoweek.js | 6 +-- lib/utils.js | 3 +- npm-shrinkwrap.json | 90 +++++++++++++++----------------- package.json | 1 - tests/admintools.test.js | 4 +- tests/careportal.test.js | 2 +- tests/client.renderer.test.js | 4 ++ tests/reports.test.js | 8 +-- 15 files changed, 96 insertions(+), 123 deletions(-) diff --git a/Makefile b/Makefile index e4b4912ac89..1ca626ab88c 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ travis: python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' # NODE_ENV=test ${MONGO_SETTINGS} \ # ${ISTANBUL} cover ${MOCHA} --report lcovonly -- --timeout 5000 -R tap ${TESTS} - for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done | tap-set-exit + for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done docker_release: # Get the version from the package.json file diff --git a/lib/client/chart.js b/lib/client/chart.js index 7b053cd83d0..c0aa098fa45 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -14,7 +14,6 @@ var PADDING_BOTTOM = 30 , CONTEXT_MIN = 36 , FOCUS_MAX = 510 , FOCUS_MIN = 30 - , DEFAULT_TRANS_MS = 100 ; function init (client, d3, $) { @@ -481,7 +480,7 @@ function init (client, d3, $) { } else { // for subsequent updates use a transition to animate the axis to the new position - var focusTransition = chart.focus.transition().duration(DEFAULT_TRANS_MS); + var focusTransition = chart.focus; focusTransition.select('.x') .attr('transform', 'translate(0,' + focusHeight + ')') @@ -491,7 +490,7 @@ function init (client, d3, $) { .attr('transform', 'translate(' + chartWidth + ', 0)') .call(chart.yAxis); - var contextTransition = chart.context.transition().duration(DEFAULT_TRANS_MS); + var contextTransition = chart.context; chart.context .attr('transform', 'translate(0,' + focusHeight + ')') @@ -500,7 +499,7 @@ function init (client, d3, $) { .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); - chart.basals.transition().duration(DEFAULT_TRANS_MS); + chart.basals; // reset brush location chart.theBrush.selectAll('rect') @@ -514,28 +513,24 @@ function init (client, d3, $) { // transition lines to correct location chart.focus.select('.high-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))); chart.focus.select('.target-top-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))); chart.focus.select('.target-bottom-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .attr('x2', chart.xScale(currentRange[1])) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))); chart.focus.select('.low-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(currentRange[0])) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) .attr('x2', chart.xScale(currentRange[1])) @@ -543,7 +538,6 @@ function init (client, d3, $) { // transition open-top line to correct location chart.context.select('.open-top') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX))) .attr('x2', chart.xScale2(currentRange[1])) @@ -551,7 +545,6 @@ function init (client, d3, $) { // transition open-left line to correct location chart.context.select('.open-left') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[0])) .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale2(currentRange[0])) @@ -559,7 +552,6 @@ function init (client, d3, $) { // transition open-right line to correct location chart.context.select('.open-right') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[1])) .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale2(currentRange[1])) @@ -567,7 +559,6 @@ function init (client, d3, $) { // transition high line to correct location chart.context.select('.high-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .attr('x2', chart.xScale2(dataRange[1])) @@ -575,7 +566,6 @@ function init (client, d3, $) { // transition low line to correct location chart.context.select('.low-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .attr('x2', chart.xScale2(dataRange[1])) @@ -630,7 +620,6 @@ function init (client, d3, $) { // transition open-top line to correct location chart.context.select('.open-top') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[0])) .attr('y1', chart.yScale2(contextYDomain[1])) .attr('x2', chart.xScale2(currentRange[1])) @@ -638,7 +627,6 @@ function init (client, d3, $) { // transition open-left line to correct location chart.context.select('.open-left') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[0])) .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale2(currentRange[0])) @@ -646,21 +634,18 @@ function init (client, d3, $) { // transition open-right line to correct location chart.context.select('.open-right') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentRange[1])) .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale2(currentRange[1])) .attr('y2', chart.yScale2(contextYDomain[1])); chart.focus.select('.now-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale(nowDate)) .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(nowDate)) .attr('y2', chart.yScale(focusYDomain[1])); chart.context.select('.now-line') - .transition().duration(DEFAULT_TRANS_MS) .attr('x1', chart.xScale2(currentBrushExtent[1])) .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale2(currentBrushExtent[1])) diff --git a/lib/client/index.js b/lib/client/index.js index dd7fba1a063..daccbb17783 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -102,8 +102,7 @@ client.init = function init (callback) { client.load = function load (serverSettings, callback) { - var UPDATE_TRANS_MS = 750 // milliseconds - , FORMAT_TIME_12 = '%-I:%M %p' + var FORMAT_TIME_12 = '%-I:%M %p' , FORMAT_TIME_12_COMPACT = '%-I:%M' , FORMAT_TIME_24 = '%H:%M%' , FORMAT_TIME_12_SCALE = '%-I %p' @@ -1143,7 +1142,7 @@ client.load = function load (serverSettings, callback) { }); } - function dataUpdate (received) { + function dataUpdate (received, headless) { console.info('got dataUpdate', new Date(client.now)); var lastUpdated = Date.now(); @@ -1177,17 +1176,21 @@ client.load = function load (serverSettings, callback) { prepareEntries(); updateTitle(); + // Don't invoke D3 in headless mode + if (!isInitialData) { isInitialData = true; - chart = client.chart = require('./chart')(client, d3, $); - brushed(); - chart.update(true); + if (!headless) { + chart = client.chart = require('./chart')(client, d3, $); + brushed(); + chart.update(true); + } } else if (!inRetroMode()) { - chart.update(false); + if (!headless) chart.update(false); client.plugins.updateVisualisations(client.nowSBX); - brushed(); + if (!headless) brushed(); } else { - chart.updateContext(); + if (!headless) chart.updateContext(); } } }; diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 944d66da964..2c455f16ecd 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -6,8 +6,6 @@ var times = require('../times'); var DEFAULT_FOCUS = times.hours(3).msecs , WIDTH_SMALL_DOTS = 420 , WIDTH_BIG_DOTS = 800 - , TOOLTIP_TRANS_MS = 100 // milliseconds - , DEFAULT_TRANS_MS = 10 // milliseconds , TOOLTIP_WIDTH = 150 //min-width + padding ; @@ -47,8 +45,7 @@ function init (client, d3) { } function hideTooltip () { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } // get the desired opacity for context chart based on the brush extent @@ -166,7 +163,7 @@ function init (client, d3) { var rawbgInfo = getRawbgInfo(); - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html('' + translate('BG') + ': ' + client.sbx.scaleEntry(d) + (d.type === 'mbg' ? '
' + translate('Device') + ': ' + d.device : '') + (d.type === 'forecast' && d.forecastType ? '
' + translate('Forecast Type') + ': ' + d.forecastType : '') + @@ -178,7 +175,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - updateFocusCircles(focusCircles.transition().duration(DEFAULT_TRANS_MS)); + updateFocusCircles(focusCircles); // if new circle then just display prepareFocusCircles(focusCircles.enter().append('circle')) @@ -317,13 +314,13 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - updateTreatCircles(treatCircles.transition().duration(DEFAULT_TRANS_MS)); + updateTreatCircles(treatCircles); // if new circle then just display prepareTreatCircles(treatCircles.enter().append('circle')) .attr('class', 'treatment-dot') .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); @@ -404,7 +401,7 @@ function init (client, d3) { } // if transitioning, update rect text, position, and width - var rectUpdates = treatRects.transition().duration(DEFAULT_TRANS_MS); + var rectUpdates = treatRects; rectUpdates.attr('transform', rectTranslate); rectUpdates.select('text') @@ -420,7 +417,7 @@ function init (client, d3) { .attr('class', 'g-duration') .attr('transform', rectTranslate) .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); @@ -481,7 +478,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareContextCircles(contextCircles.transition().duration(DEFAULT_TRANS_MS)); + prepareContextCircles(contextCircles); // if new circle then just display prepareContextCircles(contextCircles.enter().append('circle')); @@ -622,7 +619,7 @@ function init (client, d3) { glucose = Math.round(glucose * decimals) / decimals; } - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(new Date(treatment.mills)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
' : '') + (treatment.protein ? '' + translate('Protein') + ': ' + treatment.protein + '
' : '') + @@ -649,7 +646,7 @@ function init (client, d3) { //console.log(treatment); var windowWidth = $(client.tooltip.node()).parent().parent().width(); var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10; - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9) + client.tooltip.style('opacity', .9) .style('left', left + 'px') .style('top', (d3.event.pageY ? d3.event.pageY + 15 : 40) + 'px'); @@ -742,7 +739,7 @@ function init (client, d3) { }) .on('drag', function() { //console.log(d3.event); - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width')); var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight); @@ -793,7 +790,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -810,7 +807,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -827,7 +824,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -843,7 +840,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -871,7 +868,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -899,7 +896,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -1213,8 +1210,7 @@ function init (client, d3) { return ret; }; - treatProfiles.transition().duration(0) - .attr('transform', function(t) { + treatProfiles.attr('transform', function(t) { // change text of record on left side return 'rotate(-90,' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' + 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')'; @@ -1234,7 +1230,7 @@ function init (client, d3) { }) .text(generateText) .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(profileTooltip(d)) .style('left', (d3.event.pageX) + 'px') .style('top', (d3.event.pageY + 15) + 'px'); diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index f3bb1902ad6..5020f2e6458 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -47,7 +47,6 @@ function init (ctx) { if (_.isEmpty(result) || _.isNil(result.cob) || (Date.now() - result.mills) > TEN_MINUTES) { - console.log('Calculating COB'); var treatmentCOB = (treatments !== undefined && treatments.length) ? cob.fromTreatments(treatments, devicestatus, profile, time, spec_profile) : {}; result = treatmentCOB; diff --git a/lib/plugins/pluginbase.js b/lib/plugins/pluginbase.js index 4ae28f67ca2..87102d1831d 100644 --- a/lib/plugins/pluginbase.js +++ b/lib/plugins/pluginbase.js @@ -84,7 +84,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }).join('
\n'); pill.mouseover(function pillMouseover (event) { - tooltip.transition().duration(200).style('opacity', .9); + tooltip.style('opacity', .9); var windowWidth = $(tooltip.node()).parent().parent().width(); var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH - 10; @@ -94,9 +94,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }); pill.mouseout(function pillMouseout ( ) { - tooltip.transition() - .duration(200) - .style('opacity', 0); + tooltip.style('opacity', 0); }); } else { pill.off('mouseover'); diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 4eadc45ea90..4fde5658b19 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -82,8 +82,6 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var report_plugins = Nightscout.report_plugins; var scaledTreatmentBG = report_plugins.utils.scaledTreatmentBG; - var TOOLTIP_TRANS_MS = 300; - var padding = { top: 15, right: 22, bottom: 30, left: 35 }; var tddSum = 0; @@ -290,7 +288,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio }) .on('mouseover', function(d) { if (options.openAps && d.openaps) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); var text = 'BG: ' + d.openaps.suggested.bg + ', ' + d.openaps.suggested.reason + (d.openaps.suggested.mealAssist ? ' Meal Assist: ' + d.openaps.suggested.mealAssist : ''); @@ -1063,8 +1061,6 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } function hideTooltip () { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } }; diff --git a/lib/report_plugins/weektoweek.js b/lib/report_plugins/weektoweek.js index 449546bbbe2..8719af81542 100644 --- a/lib/report_plugins/weektoweek.js +++ b/lib/report_plugins/weektoweek.js @@ -79,8 +79,6 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op var client = Nightscout.client; var report_plugins = Nightscout.report_plugins; - var TOOLTIP_TRANS_MS = 300; - var padding = { top: 15, right: 22, bottom: 30, left: 35 }; var weekstoshow = [ ]; @@ -322,8 +320,6 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op } function hideTooltip ( ) { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } }; diff --git a/lib/utils.js b/lib/utils.js index fe1778f8120..083c4284846 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -39,7 +39,8 @@ function init(ctx) { return '0'; } var mult = Math.pow(10,digits); - var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult + var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult; + if (isNaN(fixed)) return '0'; return String(fixed); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b63bd4dbc14..554719ebdd2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2272,7 +2272,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -2306,7 +2306,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", @@ -2352,7 +2352,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { "base64-js": "^1.0.2", @@ -3056,7 +3056,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -3068,7 +3068,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -3613,7 +3613,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -4162,12 +4162,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" }, - "events-to-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", - "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", - "dev": true - }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4693,7 +4687,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4711,11 +4706,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4728,15 +4725,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4839,7 +4839,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4849,6 +4850,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4861,17 +4863,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4888,6 +4893,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4960,7 +4966,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4970,6 +4977,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5045,7 +5053,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5075,6 +5084,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5092,6 +5102,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5130,11 +5141,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -6355,7 +6368,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" @@ -6839,7 +6852,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } @@ -9402,7 +9415,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9910,7 +9923,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -10786,7 +10799,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -10964,27 +10977,6 @@ } } }, - "tap-parser": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-1.3.2.tgz", - "integrity": "sha1-EgxQiciMPIp5PvKIhn3jIeGPjCI=", - "dev": true, - "requires": { - "events-to-array": "^1.0.1", - "inherits": "~2.0.1", - "js-yaml": "^3.2.7", - "readable-stream": "^2" - } - }, - "tap-set-exit": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tap-set-exit/-/tap-set-exit-1.1.1.tgz", - "integrity": "sha1-nGejf03FcOSZlCBGT45sGYaLVWE=", - "dev": true, - "requires": { - "tap-parser": "^1.0.4" - } - }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", diff --git a/package.json b/package.json index 2a12a51f6c2..bf3c4c7f802 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,6 @@ "nyc": "^14.1.1", "should": "^13.2.3", "supertest": "^3.4.2", - "tap-set-exit": "^1.1.1", "terser-webpack-plugin": "^1.4.1", "webpack-bundle-analyzer": "^3.4.1", "webpack-dev-middleware": "^3.7.2", diff --git a/tests/admintools.test.js b/tests/admintools.test.js index bbede54156a..9f867a543c3 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -153,7 +153,9 @@ describe('admintools', function ( ) { var d3 = require('d3'); //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; + //d3.timer = function mockTimer() { }; + let timer = d3.timer(function mockTimer() { }); + timer.stop(); var cookieStorageType = self.localStorage._type diff --git a/tests/careportal.test.js b/tests/careportal.test.js index 782bc4fa566..36f48d3a5a4 100644 --- a/tests/careportal.test.js +++ b/tests/careportal.test.js @@ -49,7 +49,7 @@ describe('client', function ( ) { client.init(); - client.dataUpdate(nowData); + client.dataUpdate(nowData, true); client.careportal.prepareEvents(); diff --git a/tests/client.renderer.test.js b/tests/client.renderer.test.js index 569691cd717..ca81e7d99e8 100644 --- a/tests/client.renderer.test.js +++ b/tests/client.renderer.test.js @@ -54,6 +54,10 @@ describe('renderer', () => { } } , futureOpacity: (millsDifference) => { return 1; } + , createAdjustedRange: () => { return [ + { getTime: () => { return extent.times[0]}}, + { getTime: () => { return extent.times[1]}} + ] } } , latestSGV: { mills: 120 } }; diff --git a/tests/reports.test.js b/tests/reports.test.js index 3c79e3b096a..7d5a0eb7009 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -261,10 +261,12 @@ describe('reports', function ( ) { var result = $('body').html(); //var filesys = require('fs'); //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - //logfile.write($('body').html()); - + //logfile.write(result); + //console.log('RESULT', result); + result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday - result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday + result.indexOf('50 g').should.be.greaterThan(-1); // daytoday + result.indexOf('TDD average: 2.9U').should.be.greaterThan(-1); // daytoday result.indexOf('0%100%0%2').should.be.greaterThan(-1); //dailystats //TODO FIXME result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: 64.7%6').should.be.greaterThan(-1); // distribution result.indexOf('16 (100%)').should.be.greaterThan(-1); // hourlystats From 2d07c5488a797b517d96d665932160c8ffe6627b Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Tue, 22 Oct 2019 08:37:47 +0300 Subject: [PATCH 069/134] Performance fixes (#5134) * Further fixes to profile data fetches, change the client to not create excessive amount of date objects * Sort entries once on load and then rely on the sorting to find out the largest and smallest value * Make the renderer reuse Date objects instead of instantiating a huge amount of dates all the time --- lib/client/index.js | 20 +++++++++++++----- lib/client/renderer.js | 46 ++++++++++++++++++++++++----------------- lib/profilefunctions.js | 31 +++++++++++++++++++-------- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/lib/client/index.js b/lib/client/index.js index daccbb17783..53458d625f5 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -123,7 +123,11 @@ client.load = function load (serverSettings, callback) { , urgentAlarmSound = 'alarm2.mp3' , previousNotifyTimestamp; - client.entryToDate = function entryToDate (entry) { return new Date(entry.mills); }; + client.entryToDate = function entryToDate (entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + }; client.now = Date.now(); client.ddata = require('../data/ddata')(); @@ -260,10 +264,12 @@ client.load = function load (serverSettings, callback) { //client.ctx.bus.uptime( ); client.dataExtent = function dataExtent () { - return client.entries.length > 0 ? - d3.extent(client.entries, client.entryToDate) : - d3.extent([new Date(client.now - times.hours(history).msecs), new Date(client.now)]); - }; + if (client.entries.length > 0) { + return[client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length-1])]; + } else { + return [new Date(client.now - times.hours(history).msecs), new Date(client.now)]; + } + }; client.bottomOfPills = function bottomOfPills () { //the offset's might not exist for some tests @@ -1140,6 +1146,10 @@ client.load = function load (serverSettings, callback) { point.color = 'transparent'; } }); + + client.entries.sort(function sorter(a,b) { + return a.mills - b.mills; + }); } function dataUpdate (received, headless) { diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 2c455f16ecd..adf6e584759 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -9,6 +9,8 @@ var DEFAULT_FOCUS = times.hours(3).msecs , TOOLTIP_WIDTH = 150 //min-width + padding ; +const zeroDate = new Date(0); + function init (client, d3) { var renderer = {}; @@ -16,6 +18,12 @@ function init (client, d3) { var utils = client.utils; var translate = client.translate; + function getOrAddDate(entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + } + //chart isn't created till the client gets data, so can grab the var at init function chart () { return client.chart; @@ -94,12 +102,12 @@ function init (client, d3) { sel.attr('cx', function(d) { if (!d) { console.error('Bad data', d); - return chart().xScale(new Date(0)); + return chart().xScale(zeroDate); } else if (!d.mills) { console.error('Bad data, no mills', d); - return chart().xScale(new Date(0)); + return chart().xScale(zeroDate); } else { - return chart().xScale(new Date(d.mills)); + return chart().xScale(getOrAddDate(d)); } }) .attr('cy', function(d) { @@ -169,7 +177,7 @@ function init (client, d3) { (d.type === 'forecast' && d.forecastType ? '
' + translate('Forecast Type') + ': ' + d.forecastType : '') + (rawbgInfo.value ? '
' + translate('Raw BG') + ': ' + rawbgInfo.value : '') + (rawbgInfo.noise ? '
' + translate('Noise') + ': ' + rawbgInfo.noise : '') + - '
' + translate('Time') + ': ' + client.formatTime(new Date(d.mills))) + '
' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d))) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); } @@ -229,7 +237,7 @@ function init (client, d3) { } } - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
' : '') + (d.reason ? '' + translate('Reason') + ': ' + translate(d.reason) + '
' : '') + (d.glucose ? '' + translate('BG') + ': ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')' : '') + '
' : '') + @@ -243,7 +251,7 @@ function init (client, d3) { } function announcementTooltip (d) { - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Announcement') + '
' : '') + (d.notes && d.notes.length > 1 ? '' + translate('Message') + ': ' + d.notes + '
' : '') + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
' : ''); @@ -272,7 +280,7 @@ function init (client, d3) { function updateTreatCircles (sel) { sel.attr('cx', function(d) { - return chart().xScale(new Date(d.mills)); + return chart().xScale(getOrAddDate(d)); }) .attr('cy', function(d) { return chart().yScale(client.sbx.scaleEntry(d)); @@ -366,26 +374,26 @@ function init (client, d3) { if (d.eventType === 'Temporary Target') { top = d.targetTop === d.targetBottom ? d.targetTop + rectHeight(d) : d.targetTop; } - return 'translate(' + chart().xScale(new Date(d.mills)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')'; + return 'translate(' + chart().xScale(getOrAddDate(d)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')'; } function treatmentRectWidth (d) { if (d.durationType === "indefinite") { - return chart().xScale(chart().xScale.domain()[1].getTime()) - chart().xScale(new Date(d.mills)); + return chart().xScale(chart().xScale.domain()[1].getTime()) - chart().xScale(getOrAddDate(d)); } else { - return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)); + return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d)); } } function treatmentTextTransform (d) { if (d.durationType === "indefinite") { var offset = 0; - if (chart().xScale(new Date(d.mills)) < chart().xScale(chart().xScale.domain()[0].getTime())) { - offset = chart().xScale(nowDate) - chart().xScale(new Date(d.mills)); + if (chart().xScale(getOrAddDate(d)) < chart().xScale(chart().xScale.domain()[0].getTime())) { + offset = chart().xScale(nowDate) - chart().xScale(getOrAddDate(d)); } return 'translate(' + offset + ',' + 10 + ')'; } else { - return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills))) / 2 + ',' + 10 + ')'; + return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d))) / 2 + ',' + 10 + ')'; } } @@ -454,7 +462,7 @@ function init (client, d3) { function prepareContextCircles (sel) { var badData = []; - sel.attr('cx', function(d) { return chart().xScale2(new Date(d.mills)); }) + sel.attr('cx', function(d) { return chart().xScale2(getOrAddDate(d)); }) .attr('cy', function(d) { var scaled = client.sbx.scaleEntry(d); if (isNaN(scaled)) { @@ -620,7 +628,7 @@ function init (client, d3) { } client.tooltip.style('opacity', .9); - client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(new Date(treatment.mills)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + + client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(getOrAddDate(treatment)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
' : '') + (treatment.protein ? '' + translate('Protein') + ': ' + treatment.protein + '
' : '') + (treatment.fat ? '' + translate('Fat') + ': ' + treatment.fat + '
' : '') + @@ -768,7 +776,7 @@ function init (client, d3) { chart().drag.append('line') .attr('class', 'arrow') .attr('marker-end', 'url(#arrow)') - .attr('x1', chart().xScale(new Date(treatment.mills))) + .attr('x1', chart().xScale(getOrAddDate(treatment))) .attr('y1', chart().yScale(client.sbx.scaleEntry(treatment))) .attr('x2', x) .attr('y2', y) @@ -912,7 +920,7 @@ function init (client, d3) { .enter() .append('g') .attr('class', 'draggable-treatment') - .attr('transform', 'translate(' + chart().xScale(new Date(treatment.mills)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') + .attr('transform', 'translate(' + chart().xScale(getOrAddDate(treatment)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') .on('mouseover', treatmentTooltip) .on('mouseout', hideTooltip); if (client.editMode) { @@ -994,7 +1002,7 @@ function init (client, d3) { //when the tests are run window isn't available var innerWidth = window && window.innerWidth || -1; // don't render the treatment if it's not visible - if (Math.abs(chart().xScale(new Date(treatment.mills))) > innerWidth) { + if (Math.abs(chart().xScale(getOrAddDate(treatment))) > innerWidth) { return; } @@ -1159,7 +1167,7 @@ function init (client, d3) { } function profileTooltip (d) { - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
' : '') + (d.endprofile ? '' + translate('End of profile') + ': ' + d.endprofile + '
' : '') + (d.profile ? '' + translate('Profile') + ': ' + d.profile + '
' : '') + diff --git a/lib/profilefunctions.js b/lib/profilefunctions.js index ec9e29fded5..40ce9629030 100644 --- a/lib/profilefunctions.js +++ b/lib/profilefunctions.js @@ -6,14 +6,13 @@ var c = require('memory-cache'); var times = require('./times'); var crypto = require('crypto'); -var cacheTTL = 600; - +var cacheTTL = 5000; var prevBasalTreatment = null; +var cache = new c.Cache(); function init (profileData) { var profile = {}; - var cache = new c.Cache(); profile.loadData = function loadData (profileData) { if (profileData && profileData.length) { @@ -139,10 +138,22 @@ function init (profileData) { }; profile.getCurrentProfile = function getCurrentProfile (time, spec_profile) { - time = time || new Date().getTime(); + + time = time || Date.now(); + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = ("profile" + minuteTime + spec_profile); + var returnValue = cache.get(cacheKey); + + if (returnValue) { + return returnValue; + } + var data = profile.hasData() ? profile.data[0] : null; var timeprofile = spec_profile || profile.activeProfileToTime(time); - return data && data.store[timeprofile] ? data.store[timeprofile] : {}; + returnValue = data && data.store[timeprofile] ? data.store[timeprofile] : {}; + + cache.put(cacheKey, returnValue, cacheTTL); + return returnValue; }; profile.getUnits = function getUnits (spec_profile) { @@ -223,9 +234,10 @@ function init (profileData) { }; profile.activeProfileTreatmentToTime = function activeProfileTreatmentToTime (time) { - var cacheKey = 'profile' + time + profile.profiletreatments_hash; - //var returnValue = profile.timeValueCache[cacheKey]; - var returnValue; + + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'profile' + minuteTime + profile.profiletreatments_hash; + var returnValue = cache.get(cacheKey); if (returnValue) { return returnValue; @@ -312,7 +324,8 @@ function init (profileData) { profile.getTempBasal = function getTempBasal (time, spec_profile) { - var cacheKey = 'basal' + time + profile.tempbasaltreatments_hash + profile.combobolustreatments_hash + profile.profiletreatments_hash + spec_profile; + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'basal' + minuteTime + profile.tempbasaltreatments_hash + profile.combobolustreatments_hash + profile.profiletreatments_hash + spec_profile; var returnValue = cache.get(cacheKey); if (returnValue) { From fab24536ddaa55b02950a9f3340106762661c84d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Tue, 22 Oct 2019 10:12:58 +0300 Subject: [PATCH 070/134] Don't load retro data if not needed. Merge retro data faster if it's present. (#5141) --- lib/client/chart.js | 6 +++++- lib/client/index.js | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index c0aa098fa45..bc0116796ed 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -16,6 +16,8 @@ var PADDING_BOTTOM = 30 , FOCUS_MIN = 30 ; +var loadTime = Date.now(); + function init (client, d3, $) { var chart = { }; @@ -192,7 +194,9 @@ function init (client, d3, $) { chart.brush = d3.brushX() .on('start', brushStarted) .on('brush', function brush (time) { - client.loadRetroIfNeeded(); + // layouting the graph causes a brushed event + // ignore retro data load the first two seconds + if (Date.now() - loadTime > 2000) client.loadRetroIfNeeded(); client.brushed(time); }) .on('end', brushEnded); diff --git a/lib/client/index.js b/lib/client/index.js index 53458d625f5..3f4a2ae6630 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -475,10 +475,22 @@ client.load = function load (serverSettings, callback) { client.ddata.inRetroMode = inRetroMode(); client.ddata.profile = profile; + // retro data only ever contains device statuses + // Cleate a clone of the data for the sandbox given to plugins + + var mergedStatuses = client.ddata.devicestatus; + + if (client.retro.data) { + mergedStatuses = _.merge({}, client.retro.data.devicestatus, client.ddata.devicestatus); + } + + var clonedData = _.clone(client.ddata); + clonedData.devicestatus = mergedStatuses; + client.sbx = sandbox.clientInit( client.ctx , new Date(time).getTime() //make sure we send a timestamp - , _.merge({}, client.retro.data || {}, client.ddata) + , clonedData ); //all enabled plugins get a chance to set properties, even if they aren't shown From bda879304cf2382b83b519ddb30a4e41d6a63a19 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 22 Oct 2019 10:46:37 -0700 Subject: [PATCH 071/134] Updated Alexa template to comply with new requirements --- docs/plugins/alexa-templates/en-us.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json index cf90a710b88..4cb10aa0643 100644 --- a/docs/plugins/alexa-templates/en-us.json +++ b/docs/plugins/alexa-templates/en-us.json @@ -1,7 +1,7 @@ { "interactionModel": { "languageModel": { - "invocationName": "nightscout", + "invocationName": "night scout", "intents": [ { "name": "NSStatus", @@ -64,6 +64,10 @@ { "name": "AMAZON.NavigateHomeIntent", "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] } ], "types": [ From 243ff2e28e432b53e3a846cbf7829407b5c7ba90 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Wed, 23 Oct 2019 15:18:54 +0300 Subject: [PATCH 072/134] Load data in one pass (#5142) * Fix layouting and load data in one go. Bonus points if you can fix the rendering for this * fix focus target lines and high and low lines * Remove debug logging. Change profile functions to clear the cache on new data, instead of calculating heavy hashes * Remove unused code * Fix tests * Have Travis run tests on Node 10 and 12, not 13 --- .travis.yml | 2 +- lib/client/chart.js | 32 ++++++++++++------------- lib/client/index.js | 6 +++-- lib/data/ddata.js | 53 +++++++++-------------------------------- lib/profilefunctions.js | 19 ++++++++------- lib/server/websocket.js | 19 ++++++--------- tests/ddata.test.js | 13 ---------- tests/profile.test.js | 6 +++-- 8 files changed, 54 insertions(+), 96 deletions(-) diff --git a/.travis.yml b/.travis.yml index 90331521284..78b056b8dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,5 +26,5 @@ matrix: include: - node_js: "10" <<: *node_js-steps - - node_js: "node" # Latest Node is not supported, and recommend, but we'll test it to know incompatibility issues + - node_js: "12" # Latest Node is not supported, and recommend, but we'll test it to know incompatibility issues <<: *node_js-steps diff --git a/lib/client/chart.js b/lib/client/chart.js index bc0116796ed..fd250d7b15d 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -398,9 +398,9 @@ function init (client, d3, $) { // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'high-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -408,9 +408,9 @@ function init (client, d3, $) { // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'target-top-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -418,9 +418,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'target-bottom-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -428,9 +428,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'low-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -517,27 +517,27 @@ function init (client, d3, $) { // transition lines to correct location chart.focus.select('.high-line') - .attr('x1', chart.xScale(currentRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(currentRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))); chart.focus.select('.target-top-line') - .attr('x1', chart.xScale(currentRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(currentRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))); chart.focus.select('.target-bottom-line') - .attr('x1', chart.xScale(currentRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(currentRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))); chart.focus.select('.low-line') - .attr('x1', chart.xScale(currentRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(currentRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))); // transition open-top line to correct location diff --git a/lib/client/index.js b/lib/client/index.js index 3f4a2ae6630..5e9874f2ea6 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -404,6 +404,7 @@ client.load = function load (serverSettings, callback) { function brushed () { // Brush not initialized + console.log("brushed"); if (!chart.theBrush) { return; } @@ -575,7 +576,6 @@ client.load = function load (serverSettings, callback) { var top = (client.bottomOfPills() + 5); $('#chartContainer').css({ top: top + 'px', height: $(window).height() - top - 10 }); - container.removeClass('loading'); } @@ -1204,8 +1204,10 @@ client.load = function load (serverSettings, callback) { isInitialData = true; if (!headless) { chart = client.chart = require('./chart')(client, d3, $); - brushed(); chart.update(true); + brushed(); + chart.update(false); +// brushed(); } } else if (!inRetroMode()) { if (!headless) chart.update(false); diff --git a/lib/data/ddata.js b/lib/data/ddata.js index 1912ca4a6ae..0251ec17d99 100644 --- a/lib/data/ddata.js +++ b/lib/data/ddata.js @@ -32,39 +32,11 @@ function init () { }); }; - ddata.splitRecent = function splitRecent (time, cutoff, max, treatmentsToo) { - var result = { - first: {} - , rest: {} - }; - - function recent (item) { - return item.mills >= time - cutoff; - } - - function filterMax (item) { - return item.mills >= time - max; - } - - function partition (field, filter) { - var data; - if (filter) { - data = ddata[field].filter(filterMax); - } else { - data = ddata[field]; - } - - var parts = _.partition(data, recent); - result.first[field] = parts[0]; - result.rest[field] = parts[1]; - } - - partition('treatments', treatmentsToo ? filterMax : false); - - result.first.devicestatus = ddata.recentDeviceStatus(time); - - result.first.sgvs = ddata.sgvs.filter(filterMax); - result.first.cals = ddata.cals; + ddata.dataWithRecentStatuses = function dataWithRecentStatuses() { + var results = {}; + results.devicestatus = ddata.recentDeviceStatus(Date.now()); + results.sgvs = ddata.sgvs; + results.cals = ddata.cals; var profiles = _.cloneDeep(ddata.profiles); if (profiles && profiles[0]) { @@ -74,17 +46,14 @@ function init () { } }) } - result.first.profiles = profiles; - - result.rest.mbgs = ddata.mbgs.filter(filterMax); - result.rest.food = ddata.food; - result.rest.activity = ddata.activity; + results.profiles = profiles; + results.mbgs = ddata.mbgs; + results.food = ddata.food; + results.treatments = ddata.treatments; - console.log('results.first size', JSON.stringify(result.first).length, 'bytes'); - console.log('results.rest size', JSON.stringify(result.rest).length, 'bytes'); + return results; - return result; - }; + } ddata.recentDeviceStatus = function recentDeviceStatus (time) { diff --git a/lib/profilefunctions.js b/lib/profilefunctions.js index 40ce9629030..5826e6108b4 100644 --- a/lib/profilefunctions.js +++ b/lib/profilefunctions.js @@ -4,7 +4,6 @@ var _ = require('lodash'); var moment = require('moment-timezone'); var c = require('memory-cache'); var times = require('./times'); -var crypto = require('crypto'); var cacheTTL = 5000; var prevBasalTreatment = null; @@ -14,6 +13,12 @@ function init (profileData) { var profile = {}; + profile.clear = function clear() { + cache.clear(); + profile.data = null; + prevBasalTreatment = null; + } + profile.loadData = function loadData (profileData) { if (profileData && profileData.length) { profile.data = profile.convertToProfileStore(profileData); @@ -72,8 +77,7 @@ function init (profileData) { //round to the minute for better caching var minuteTime = Math.round(time / 60000) * 60000; - - var cacheKey = (minuteTime + valueType + spec_profile + profile.profiletreatments_hash); + var cacheKey = (minuteTime + valueType + spec_profile); var returnValue = cache.get(cacheKey); if (returnValue) { @@ -215,9 +219,8 @@ function init (profileData) { }); profile.combobolustreatments = combobolustreatments || []; - profile.profiletreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.profiletreatments)).digest('hex'); - profile.tempbasaltreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.tempbasaltreatments)).digest('hex'); - profile.combobolustreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.combobolustreatments)).digest('hex'); + + cache.clear(); }; profile.activeProfileToTime = function activeProfileToTime (time) { @@ -236,7 +239,7 @@ function init (profileData) { profile.activeProfileTreatmentToTime = function activeProfileTreatmentToTime (time) { var minuteTime = Math.round(time / 60000) * 60000; - var cacheKey = 'profile' + minuteTime + profile.profiletreatments_hash; + var cacheKey = 'profileCache' + minuteTime; var returnValue = cache.get(cacheKey); if (returnValue) { @@ -325,7 +328,7 @@ function init (profileData) { profile.getTempBasal = function getTempBasal (time, spec_profile) { var minuteTime = Math.round(time / 60000) * 60000; - var cacheKey = 'basal' + minuteTime + profile.tempbasaltreatments_hash + profile.combobolustreatments_hash + profile.profiletreatments_hash + spec_profile; + var cacheKey = 'basalCache' + minuteTime + spec_profile; var returnValue = cache.get(cacheKey); if (returnValue) { diff --git a/lib/server/websocket.js b/lib/server/websocket.js index 3946c129a5d..76276edffb2 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -447,20 +447,15 @@ function init (env, ctx, server) { filterTreatments = true; msecHistory = Math.min(new Date().getTime() - from, msecHistory); } - // send all data upon new connection - if (lastData && lastData.splitRecent) { - var split = lastData.splitRecent(Date.now(), times.hours(3).msecs, msecHistory, filterTreatments); + + if (lastData && lastData.dataWithRecentStatuses) { + let data = lastData.dataWithRecentStatuses(); + if (message.status) { - split.first.status = status(split.first.profiles); + data.status = status(data.profiles); } - //send out first chunk - socket.emit('dataUpdate', split.first); - - //then send out the rest - setTimeout(function sendTheRest() { - split.rest.delta = true; - socket.emit('dataUpdate', split.rest); - }, 500); + + socket.emit('dataUpdate', data); } } console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history); diff --git a/tests/ddata.test.js b/tests/ddata.test.js index f3757348c53..ceb163b7c4f 100644 --- a/tests/ddata.test.js +++ b/tests/ddata.test.js @@ -41,19 +41,6 @@ describe('ddata', function ( ) { done( ); }); - it('has #split( )', function (done) { - var date = new Date( ); - var time = date.getTime( ); - var cutoff = 1000 * 60 * 5; - var max = 1000 * 60 * 60 * 24 * 2; - var pieces = ctx.ddata.splitRecent(time, cutoff, max); - should.exist(pieces); - should.exist(pieces.first); - should.exist(pieces.rest); - - done( ); - }); - // TODO: ensure partition function gets called via: // Properties // * ddata.devicestatus diff --git a/tests/profile.test.js b/tests/profile.test.js index 8171f459e3d..373f0479d9d 100644 --- a/tests/profile.test.js +++ b/tests/profile.test.js @@ -5,6 +5,10 @@ describe('Profile', function ( ) { var profile_empty = require('../lib/profilefunctions')(); + beforeEach(function() { + profile_empty.clear(); + }); + it('should say it does not have data before it has data', function() { var hasData = profile_empty.hasData(); hasData.should.equal(false); @@ -30,8 +34,6 @@ describe('Profile', function ( ) { }; var profile = require('../lib/profilefunctions')([profileData]); -// console.log(profile); - var now = Date.now(); it('should know what the DIA is with old style profiles', function() { From 4fd28434c129af2736b8fceb4cf69ac7e53b72d6 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Wed, 23 Oct 2019 07:21:25 -0500 Subject: [PATCH 073/134] D3 Upgrade Residual Cleanup (#5140) * fix forecast limit logic (cherry picked from commit e88ce7e5d1f5fb12c71ae67da4131ff1a50940c9) * fix minimum focus forecast time * make min forecast 30 min --- lib/client/chart.js | 12 ++++++------ lib/client/renderer.js | 13 +++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index fd250d7b15d..ba35ed52f6a 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -102,7 +102,7 @@ function init (client, d3, $) { var focusYDomain = [utils.scaleMgdl(FOCUS_MIN), utils.scaleMgdl(FOCUS_MAX)]; var contextYDomain = [utils.scaleMgdl(CONTEXT_MIN), utils.scaleMgdl(CONTEXT_MAX)]; - function dynamicDomain() { + function dynamicDomain () { // allow y-axis to extend all the way to the top of the basal area, but leave room to display highest value var mult = 1.15 , targetTop = client.settings.thresholds.bgTargetTop @@ -119,7 +119,7 @@ function init (client, d3, $) { ]; } - function dynamicDomainOrElse(defaultDomain) { + function dynamicDomainOrElse (defaultDomain) { if (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic') { return dynamicDomain(); } else { @@ -203,7 +203,7 @@ function init (client, d3, $) { chart.theBrush = null; - chart.futureOpacity = (function() { + chart.futureOpacity = (function () { var scale = d3.scaleLinear( ) .domain([times.mins(25).msecs, times.mins(60).msecs]) .range([0.8, 0.1]); @@ -280,7 +280,7 @@ function init (client, d3, $) { return adjustedRange; } - chart.inRetroMode = function inRetroMode() { + chart.inRetroMode = function inRetroMode () { var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; if (!brushedRange || !chart.xScale2) { @@ -294,7 +294,7 @@ function init (client, d3, $) { }; // called for initial update and updates for resize - chart.update = function update(init) { + chart.update = function update (init) { if (client.documentHidden && !init) { console.info('Document Hidden, not updating - ' + (new Date())); @@ -604,7 +604,7 @@ function init (client, d3, $) { chart.context.select('.x').call(chart.xAxis2); }; - function scrollUpdate() { + function scrollUpdate () { scrolling = false; var nowDate = scrollNow; diff --git a/lib/client/renderer.js b/lib/client/renderer.js index adf6e584759..45f66d0b30b 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -82,12 +82,17 @@ function init (client, d3) { var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { return client.settings.showForecast.indexOf(point.info.type) > -1; }); - var maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); // limit lookahead to the same as lookback - var selectedRange = chart().createAdjustedRange(); + var selectedRange = chart().createBrushedRange(); var to = selectedRange[1].getTime(); - var focusHoursAheadMills = to + client.focusRangeMS; + var focusHoursAheadMills = to + times.mins(30).msecs; + var maxForecastMills = focusHoursAheadMills; + + if (shownForecastPoints.length > 0) { + maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); + } + maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; focusData = focusData.concat(shownForecastPoints); @@ -1028,7 +1033,7 @@ function init (client, d3) { var comboareadata = []; var selectedRange = chart().createAdjustedRange(); var from = selectedRange[0].getTime(); - var to = Math.max(selectedRange[1].getTime(), client.sbx.time) + client.forecastTime; + var to = selectedRange[1].getTime(); var date = from; var lastbasal = 0; From 41310808b2982cc9c34d60909a9d79b39c1ad22e Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Wed, 23 Oct 2019 08:02:32 -0500 Subject: [PATCH 074/134] fix forecast time max to client focusRangeMS instead of 30 min (#5143) --- lib/client/renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 45f66d0b30b..c526798e76c 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -86,7 +86,7 @@ function init (client, d3) { var selectedRange = chart().createBrushedRange(); var to = selectedRange[1].getTime(); - var focusHoursAheadMills = to + times.mins(30).msecs; + var focusHoursAheadMills = to + client.focusRangeMS; var maxForecastMills = focusHoursAheadMills; if (shownForecastPoints.length > 0) { From afd12fd7f0ada29907ef21798b8968b4c75243a1 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Wed, 23 Oct 2019 23:50:41 +0300 Subject: [PATCH 075/134] Fix initial forecast point display --- lib/client/chart.js | 41 ++++++++++++++++++++++++++++++++--------- lib/client/index.js | 17 ++++++++--------- lib/client/renderer.js | 14 +------------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index ba35ed52f6a..6f27a3bea1d 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -1,6 +1,6 @@ 'use strict'; -// var _ = require('lodash'); +var _ = require('lodash'); var times = require('../times'); var d3locales = require('./d3locales'); var scrolling = false @@ -258,11 +258,10 @@ function init (client, d3, $) { if (!brushedRange) { // console.log('No current brushed range. Setting range to last focusRangeMS amount of available data'); range = dataExtent; - range[0] = new Date(range[1].getTime() - client.focusRangeMS); } - var end = range[1].getTime() + var end = range[1].getTime(); if (!chart.inRetroMode()) { end = client.now > dataExtent[1].getTime() ? client.now : dataExtent[1].getTime(); } @@ -301,6 +300,8 @@ function init (client, d3, $) { return; } + chart.setForecastTime(); + var chartContainer = $('#chartContainer'); if (chartContainer.length < 1) { @@ -484,22 +485,19 @@ function init (client, d3, $) { } else { // for subsequent updates use a transition to animate the axis to the new position - var focusTransition = chart.focus; - focusTransition.select('.x') + chart.focus.select('.x') .attr('transform', 'translate(0,' + focusHeight + ')') .call(chart.xAxis); - focusTransition.select('.y') + chart.focus.select('.y') .attr('transform', 'translate(' + chartWidth + ', 0)') .call(chart.yAxis); - var contextTransition = chart.context; - chart.context .attr('transform', 'translate(0,' + focusHeight + ')') - contextTransition.select('.x') + chart.context.select('.x') .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); @@ -679,6 +677,31 @@ function init (client, d3, $) { scrolling = true; }; + + chart.setForecastTime = function setForecastTime () { + if (client.sbx.pluginBase.forecastPoints) { + var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { + return client.settings.showForecast.indexOf(point.info.type) > -1; + }); + // limit lookahead to the same as lookback + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + + var focusHoursAheadMills = to + client.focusRangeMS; + var maxForecastMills = focusHoursAheadMills; + + if (shownForecastPoints.length > 0) { + maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); + } else { + maxForecastMills = to + times.mins(30).msecs; + } + + console.log("shownForecastPoints",shownForecastPoints); + + maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); + client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; + } + } return chart; } diff --git a/lib/client/index.js b/lib/client/index.js index 5e9874f2ea6..30a72ae9106 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -127,7 +127,7 @@ client.load = function load (serverSettings, callback) { if (entry.date) return entry.date; entry.date = new Date(entry.mills); return entry.date; - }; + }; client.now = Date.now(); client.ddata = require('../data/ddata')(); @@ -265,11 +265,11 @@ client.load = function load (serverSettings, callback) { client.dataExtent = function dataExtent () { if (client.entries.length > 0) { - return[client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length-1])]; + return [client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length - 1])]; } else { - return [new Date(client.now - times.hours(history).msecs), new Date(client.now)]; - } - }; + return [new Date(client.now - times.hours(history).msecs), new Date(client.now)]; + } + }; client.bottomOfPills = function bottomOfPills () { //the offset's might not exist for some tests @@ -478,13 +478,13 @@ client.load = function load (serverSettings, callback) { // retro data only ever contains device statuses // Cleate a clone of the data for the sandbox given to plugins - + var mergedStatuses = client.ddata.devicestatus; if (client.retro.data) { mergedStatuses = _.merge({}, client.retro.data.devicestatus, client.ddata.devicestatus); } - + var clonedData = _.clone(client.ddata); clonedData.devicestatus = mergedStatuses; @@ -1159,7 +1159,7 @@ client.load = function load (serverSettings, callback) { } }); - client.entries.sort(function sorter(a,b) { + client.entries.sort(function sorter (a, b) { return a.mills - b.mills; }); } @@ -1207,7 +1207,6 @@ client.load = function load (serverSettings, callback) { chart.update(true); brushed(); chart.update(false); -// brushed(); } } else if (!inRetroMode()) { if (!headless) chart.update(false); diff --git a/lib/client/renderer.js b/lib/client/renderer.js index c526798e76c..19e8e3ae557 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -82,19 +82,7 @@ function init (client, d3) { var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { return client.settings.showForecast.indexOf(point.info.type) > -1; }); - // limit lookahead to the same as lookback - var selectedRange = chart().createBrushedRange(); - var to = selectedRange[1].getTime(); - - var focusHoursAheadMills = to + client.focusRangeMS; - var maxForecastMills = focusHoursAheadMills; - - if (shownForecastPoints.length > 0) { - maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); - } - - maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); - client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; + focusData = focusData.concat(shownForecastPoints); } From 8d33c6eb382cb2d95cc2c4daf3f1906800cf1a41 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Thu, 24 Oct 2019 00:07:16 +0300 Subject: [PATCH 076/134] Fix forecast time randomly resetting to 30 minutes for openaps users --- lib/client/chart.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index 6f27a3bea1d..b3395613c34 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -679,6 +679,7 @@ function init (client, d3, $) { }; chart.setForecastTime = function setForecastTime () { + if (client.sbx.pluginBase.forecastPoints) { var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { return client.settings.showForecast.indexOf(point.info.type) > -1; @@ -692,12 +693,12 @@ function init (client, d3, $) { if (shownForecastPoints.length > 0) { maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); - } else { + } + + if (!client.settings.showForecast || client.settings.showForecast == "") { maxForecastMills = to + times.mins(30).msecs; } - console.log("shownForecastPoints",shownForecastPoints); - maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; } From c1244eae12ce171e73a8fa5ce0b08b159d5ab73e Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Fri, 25 Oct 2019 12:08:03 -0500 Subject: [PATCH 077/134] Stop Focus Circles from Eating Entries (#5145) * stop focus circles from eating entries * fix forecastCircles remove * resolve possible key collision * Fixes a major bug where plugins were ran against a sandbox twice, causing issues with predictions. Fixes the look ahead timings. --- lib/client/chart.js | 133 ++++++++++++++++++++++------------------- lib/client/index.js | 19 +++--- lib/client/renderer.js | 39 +++++++----- 3 files changed, 107 insertions(+), 84 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index b3395613c34..8d0aa754e09 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -6,20 +6,18 @@ var d3locales = require('./d3locales'); var scrolling = false , scrollNow = 0 , scrollBrushExtent = null - , scrollRange = null -; + , scrollRange = null; var PADDING_BOTTOM = 30 , CONTEXT_MAX = 420 , CONTEXT_MIN = 36 , FOCUS_MAX = 510 - , FOCUS_MIN = 30 -; + , FOCUS_MIN = 30; var loadTime = Date.now(); function init (client, d3, $) { - var chart = { }; + var chart = {}; var utils = client.utils; var renderer = client.renderer; @@ -36,9 +34,9 @@ function init (client, d3, $) { .attr('x', 0) .attr('y', 0) .append('g') - .style('fill', 'none') - .style('stroke', '#0099ff') - .style('stroke-width', 2) + .style('fill', 'none') + .style('stroke', '#0099ff') + .style('stroke-width', 2) .append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth) .append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth); @@ -52,12 +50,12 @@ function init (client, d3, $) { .attr('markerHeight', 8) .attr('orient', 'auto') .append('path') - .attr('d', 'M0,-5L10,0L0,5') - .attr('class', 'arrowHead'); + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrowHead'); var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language)); - function beforeBrushStarted ( ) { + function beforeBrushStarted () { // go ahead and move the brush because // a single click will not execute the brush event var now = new Date(); @@ -76,18 +74,18 @@ function init (client, d3, $) { chart.theBrush.call(chart.brush.move, brush); } - function brushStarted ( ) { + function brushStarted () { // update the opacity of the context data points to brush extent chart.context.selectAll('circle') .data(client.entries) .style('opacity', 1); } - function brushEnded ( ) { + function brushEnded () { // update the opacity of the context data points to brush extent chart.context.selectAll('circle') .data(client.entries) - .style('opacity', function (d) { return renderer.highlightBrushPoints(d) }); + .style('opacity', function(d) { return renderer.highlightBrushPoints(d) }); } var extent = client.dataExtent(); @@ -108,13 +106,16 @@ function init (client, d3, $) { , targetTop = client.settings.thresholds.bgTargetTop // filter to only use actual SGV's (not rawbg's) to set the view window. // can switch to Logarithmic (non-dynamic) to see anything that doesn't fit in the dynamicDomain - , mgdlMax = d3.max(client.entries, function (d) { if ( d.type === 'sgv') { return d.mgdl; } }); - // use the 99th percentile instead of max to avoid rescaling for 1 flukey data point - // need to sort client.entries by mgdl first - //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); + , mgdlMax = d3.max(client.entries, function(d) { if (d.type === 'sgv') { return d.mgdl; } }); + // use the 99th percentile instead of max to avoid rescaling for 1 flukey data point + // need to sort client.entries by mgdl first + //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); return [ utils.scaleMgdl(FOCUS_MIN) + + + , Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult)) ]; } @@ -136,7 +137,7 @@ function init (client, d3, $) { var xScale2 = chart.xScale2 = d3.scaleTime().domain(extent); - contextYDomain = dynamicDomainOrElse(contextYDomain); + contextYDomain = dynamicDomainOrElse(contextYDomain); var yScale2 = chart.yScale2 = yScaleType() .domain(contextYDomain); @@ -146,25 +147,25 @@ function init (client, d3, $) { chart.yScaleBasals = d3.scaleLinear() .domain([0, 5]); - var formatMillisecond = localeFormatter.format('.%L'), - formatSecond = localeFormatter.format(':%S'), - formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : - localeFormatter.format('%I:%M'), - formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : - localeFormatter.format('%-I %p'), - formatDay = localeFormatter.format('%a %d'), - formatWeek = localeFormatter.format('%b %d'), - formatMonth = localeFormatter.format('%B'), - formatYear = localeFormatter.format('%Y'); - - var tickFormat = function (date) { - return (d3.timeSecond(date) < date ? formatMillisecond - : d3.timeMinute(date) < date ? formatSecond - : d3.timeHour(date) < date ? formatMinute - : d3.timeDay(date) < date ? formatHour - : d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) - : d3.timeYear(date) < date ? formatMonth - : formatYear)(date); + var formatMillisecond = localeFormatter.format('.%L') + , formatSecond = localeFormatter.format(':%S') + , formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%I:%M') + , formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I %p') + , formatDay = localeFormatter.format('%a %d') + , formatWeek = localeFormatter.format('%b %d') + , formatMonth = localeFormatter.format('%B') + , formatYear = localeFormatter.format('%Y'); + + var tickFormat = function(date) { + return (d3.timeSecond(date) < date ? formatMillisecond : + d3.timeMinute(date) < date ? formatSecond : + d3.timeHour(date) < date ? formatMinute : + d3.timeDay(date) < date ? formatHour : + d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) : + d3.timeYear(date) < date ? formatMonth : + formatYear)(date); }; var tickValues = client.ticks(client); @@ -203,12 +204,12 @@ function init (client, d3, $) { chart.theBrush = null; - chart.futureOpacity = (function () { - var scale = d3.scaleLinear( ) + chart.futureOpacity = (function() { + var scale = d3.scaleLinear() .domain([times.mins(25).msecs, times.mins(60).msecs]) .range([0.8, 0.1]); - return function (delta) { + return function(delta) { if (delta < 0) { return null; } else { @@ -231,7 +232,7 @@ function init (client, d3, $) { chart.focus.append('g') .attr('class', 'x axis') .style("font-size", "16px"); - + // create the y axis container chart.focus.append('g') .attr('class', 'y axis') @@ -250,7 +251,7 @@ function init (client, d3, $) { .attr('class', 'y axis') .style("font-size", "16px"); - chart.createBrushedRange = function () { + chart.createBrushedRange = function() { var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; var range = brushedRange && brushedRange.map(chart.xScale2.invert); var dataExtent = client.dataExtent(); @@ -271,7 +272,7 @@ function init (client, d3, $) { return range; } - chart.createAdjustedRange = function () { + chart.createAdjustedRange = function() { var adjustedRange = chart.createBrushedRange(); adjustedRange[1] = new Date(adjustedRange[1].getTime() + client.forecastTime); @@ -371,7 +372,7 @@ function init (client, d3, $) { .attr('class', 'x brush') .call(chart.brush) .call(g => g.select(".overlay") - .datum({type: 'selection'}) + .datum({ type: 'selection' }) .on('mousedown touchstart', beforeBrushStarted)); chart.theBrush.selectAll('rect') @@ -584,7 +585,7 @@ function init (client, d3, $) { chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); }; - chart.updateContext = function (dataRange_) { + chart.updateContext = function(dataRange_) { if (client.documentHidden) { console.info('Document Hidden, not updating - ' + (new Date())); return; @@ -677,32 +678,44 @@ function init (client, d3, $) { scrolling = true; }; - - chart.setForecastTime = function setForecastTime () { + + chart.getMaxForecastMills = function getMaxForecastMills () { + // limit lookahead to the same as lookback + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + return to + client.focusRangeMS; + }; + + chart.getForecastData = function getForecastData () { + + var maxForecastAge = chart.getMaxForecastMills(); if (client.sbx.pluginBase.forecastPoints) { - var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { - return client.settings.showForecast.indexOf(point.info.type) > -1; + return _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { + return point.mills < maxForecastAge && client.settings.showForecast.indexOf(point.info.type) > -1; }); - // limit lookahead to the same as lookback - var selectedRange = chart.createBrushedRange(); - var to = selectedRange[1].getTime(); + } else return []; + }; + + chart.setForecastTime = function setForecastTime () { - var focusHoursAheadMills = to + client.focusRangeMS; - var maxForecastMills = focusHoursAheadMills; + if (client.sbx.pluginBase.forecastPoints) { + var shownForecastPoints = chart.getForecastData(); + + var focusHoursAheadMills = chart.getMaxForecastMills(); + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + var maxForecastMills = to + times.mins(30).msecs; + if (shownForecastPoints.length > 0) { maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); } - - if (!client.settings.showForecast || client.settings.showForecast == "") { - maxForecastMills = to + times.mins(30).msecs; - } maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; } - } + }; return chart; } diff --git a/lib/client/index.js b/lib/client/index.js index 30a72ae9106..01b1dce60ea 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1200,20 +1200,19 @@ client.load = function load (serverSettings, callback) { // Don't invoke D3 in headless mode + if (headless) return; + if (!isInitialData) { isInitialData = true; - if (!headless) { - chart = client.chart = require('./chart')(client, d3, $); - chart.update(true); - brushed(); - chart.update(false); - } + chart = client.chart = require('./chart')(client, d3, $); + chart.update(true); + brushed(); + chart.update(false); } else if (!inRetroMode()) { - if (!headless) chart.update(false); - client.plugins.updateVisualisations(client.nowSBX); - if (!headless) brushed(); + brushed(); + chart.update(false); } else { - if (!headless) chart.updateContext(); + chart.updateContext(); } } }; diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 19e8e3ae557..4da2119731d 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -75,20 +75,6 @@ function init (client, d3) { }; renderer.addFocusCircles = function addFocusCircles () { - // get slice of data so that concatenation of predictions do not interfere with subsequent updates - var focusData = client.entries.slice(); - - if (client.sbx.pluginBase.forecastPoints) { - var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { - return client.settings.showForecast.indexOf(point.info.type) > -1; - }); - - focusData = focusData.concat(shownForecastPoints); - } - - // bind up the focus chart data to an array of circles - // selects all our data into data and uses date function to get current max date - var focusCircles = chart().focus.selectAll('circle').data(focusData, client.entryToDate); function updateFocusCircles (sel) { var badData = []; @@ -175,15 +161,40 @@ function init (client, d3) { .style('top', (d3.event.pageY + 15) + 'px'); } + var focusData = client.entries; + var shownForecastPoints = client.chart.getForecastData(); + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + var focusCircles = chart().focus.selectAll('circle.entry-dot').data(focusData, function genKey (d) { + return d.forecastType + d.mills; + }); + // if already existing then transition each circle to its new position updateFocusCircles(focusCircles); // if new circle then just display prepareFocusCircles(focusCircles.enter().append('circle')) + .attr('class', 'entry-dot') .on('mouseover', focusCircleTooltip) .on('mouseout', hideTooltip); focusCircles.exit().remove(); + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + var forecastCircles = chart().focus.selectAll('circle.forecast-dot').data(shownForecastPoints, client.entryToDate); + + // if already existing then transition each circle to its new position + updateFocusCircles(forecastCircles); + + // if new circle then just display + prepareFocusCircles(forecastCircles.enter().append('circle')) + .attr('class', 'forecast-dot') + .on('mouseover', focusCircleTooltip) + .on('mouseout', hideTooltip); + + forecastCircles.exit().remove(); }; renderer.addTreatmentCircles = function addTreatmentCircles (nowDate) { From 28b892e46c596b01f44899dbcb49281eaffc216e Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Mon, 28 Oct 2019 10:38:02 +0200 Subject: [PATCH 078/134] Fix forecast behaviour (#5160) * stop focus circles from eating entries * fix forecastCircles remove * resolve possible key collision * Fixes a major bug where plugins were ran against a sandbox twice, causing issues with predictions. Fixes the look ahead timings. * Refactor of how prediction data is collected --- lib/client/chart.js | 19 ++++++++++--------- lib/client/renderer.js | 27 +++++++++++++++++---------- lib/plugins/pluginbase.js | 4 ++-- lib/sandbox.js | 2 +- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index 8d0aa754e09..a40aa8ff542 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -113,9 +113,6 @@ function init (client, d3, $) { return [ utils.scaleMgdl(FOCUS_MIN) - - - , Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult)) ]; } @@ -689,12 +686,16 @@ function init (client, d3, $) { chart.getForecastData = function getForecastData () { var maxForecastAge = chart.getMaxForecastMills(); + var pointTypes = client.settings.showForecast.split(' '); + + var points = pointTypes.reduce( function (points, type) { + return points.concat(client.sbx.pluginBase.forecastPoints[type] || []); + }, [] ); + + return _.filter(points, function isShown (point) { + return point.mills < maxForecastAge; + }); - if (client.sbx.pluginBase.forecastPoints) { - return _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { - return point.mills < maxForecastAge && client.settings.showForecast.indexOf(point.info.type) > -1; - }); - } else return []; }; chart.setForecastTime = function setForecastTime () { @@ -707,7 +708,7 @@ function init (client, d3, $) { var selectedRange = chart.createBrushedRange(); var to = selectedRange[1].getTime(); var maxForecastMills = to + times.mins(30).msecs; - + if (shownForecastPoints.length > 0) { maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); } diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 4da2119731d..688b046b7b2 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -161,13 +161,14 @@ function init (client, d3) { .style('top', (d3.event.pageY + 15) + 'px'); } + // CGM data + var focusData = client.entries; - var shownForecastPoints = client.chart.getForecastData(); // bind up the focus chart data to an array of circles // selects all our data into data and uses date function to get current max date var focusCircles = chart().focus.selectAll('circle.entry-dot').data(focusData, function genKey (d) { - return d.forecastType + d.mills; + return "cgmreading." + d.mills; }); // if already existing then transition each circle to its new position @@ -181,20 +182,26 @@ function init (client, d3) { focusCircles.exit().remove(); + // Forecasts + + var shownForecastPoints = client.chart.getForecastData(); + // bind up the focus chart data to an array of circles // selects all our data into data and uses date function to get current max date - var forecastCircles = chart().focus.selectAll('circle.forecast-dot').data(shownForecastPoints, client.entryToDate); - // if already existing then transition each circle to its new position - updateFocusCircles(forecastCircles); + var forecastCircles = chart().focus.selectAll('circle.forecast-dot').data(shownForecastPoints, function genKey (d) { + return d.forecastType + d.mills; + }); + + forecastCircles.exit().remove(); - // if new circle then just display prepareFocusCircles(forecastCircles.enter().append('circle')) - .attr('class', 'forecast-dot') - .on('mouseover', focusCircleTooltip) - .on('mouseout', hideTooltip); + .attr('class', 'forecast-dot') + .on('mouseover', focusCircleTooltip) + .on('mouseout', hideTooltip); + + updateFocusCircles(forecastCircles); - forecastCircles.exit().remove(); }; renderer.addTreatmentCircles = function addTreatmentCircles (nowDate) { diff --git a/lib/plugins/pluginbase.js b/lib/plugins/pluginbase.js index 87102d1831d..0e2adad3586 100644 --- a/lib/plugins/pluginbase.js +++ b/lib/plugins/pluginbase.js @@ -10,7 +10,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { var pluginBase = { }; pluginBase.forecastInfos = []; - pluginBase.forecastPoints = []; + pluginBase.forecastPoints = {}; function findOrCreatePill (plugin) { var container = null; @@ -111,7 +111,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }); pluginBase.forecastInfos.push(info); - pluginBase.forecastPoints = pluginBase.forecastPoints.concat(points); + pluginBase.forecastPoints[info.type] = points; }; return pluginBase; diff --git a/lib/sandbox.js b/lib/sandbox.js index ceac9a3fe29..3acb9a19428 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -96,7 +96,7 @@ function init () { if (sbx.pluginBase) { sbx.pluginBase.forecastInfos = []; - sbx.pluginBase.forecastPoints = []; + sbx.pluginBase.forecastPoints = {}; } sbx.extendedSettings = { empty: true }; From 9c42fed941587c42586de6ab980ac0e8387cd07d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Wed, 30 Oct 2019 09:26:05 +0200 Subject: [PATCH 079/134] Update node pushover library to fix crash in case Pushover service is down --- npm-shrinkwrap.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 554719ebdd2..e74737626e6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9297,9 +9297,9 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pushover-notifications": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.0.tgz", - "integrity": "sha512-Da2XgHDDq9ZU4idbIx5Y9N4kCsHVgeeHViHK2wxdtdkdP58OvrsKCqpLZnr5nS+I4/PphjTORGSVzwMV2UaPLg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.1.tgz", + "integrity": "sha512-FEPbbEhKPDw4PP/e4irEEv1gRmHvt2rulpsvj9OaWTBLWuTf0qBEuaydOsYnQdXS7zq0fAX/ptsj5/BqbKrcUw==" }, "qs": { "version": "6.5.2", diff --git a/package.json b/package.json index bf3c4c7f802..346e67c6251 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "node-cache": "^4.2.1", "parse-duration": "^0.1.1", "pem": "^1.14.3", - "pushover-notifications": "^1.2.0", + "pushover-notifications": "^1.2.1", "random-token": "0.0.8", "request": "^2.88.0", "semver": "^6.3.0", From 5c5821a90dc6ca1e8effb06b5cda93e93ce9465a Mon Sep 17 00:00:00 2001 From: PetrOndrusek Date: Wed, 30 Oct 2019 22:55:23 +0100 Subject: [PATCH 080/134] adding list of supported APIs versions (GET /api/versions) --- app.js | 9 +++- lib/api/const.json | 4 ++ lib/api/root.js | 23 ++++++++ tests/api.root.test.js | 42 +++++++++++++++ tests/fixtures/api/instance.js | 98 ++++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 lib/api/const.json create mode 100644 lib/api/root.js create mode 100644 tests/api.root.test.js create mode 100644 tests/fixtures/api/instance.js diff --git a/app.js b/app.js index 75cfdd0bf5a..daa34aa7651 100644 --- a/app.js +++ b/app.js @@ -117,10 +117,11 @@ function create (env, ctx) { /////////////////////////////////////////////////// // api and json object variables /////////////////////////////////////////////////// + const apiRoot = require('./lib/api/root')(env, ctx); var api = require('./lib/api/')(env, ctx); var api3 = require('./lib/api3/')(env, ctx); var ddata = require('./lib/data/endpoints')(env, ctx); - var notificationsV2 = require('./lib/api/notifications-v2')(app, ctx) + var notificationsV2 = require('./lib/api/notifications-v2')(app, ctx); app.use(compression({ filter: function shouldCompress (req, res) { @@ -166,6 +167,10 @@ function create (env, ctx) { }); }); + app.use('/api', bodyParser({ + limit: 1048576 * 50 + }), apiRoot); + app.use('/api/v1', bodyParser({ limit: 1048576 * 50 }), api); @@ -233,7 +238,7 @@ function create (env, ctx) { app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - app.use('/swagger-ui-dist', (req, res, next) => { + app.use('/swagger-ui-dist', (req, res) => { res.redirect(307, '/api-docs'); }); diff --git a/lib/api/const.json b/lib/api/const.json new file mode 100644 index 00000000000..cb1421d8520 --- /dev/null +++ b/lib/api/const.json @@ -0,0 +1,4 @@ +{ + "API1_VERSION": "1.0.0", + "API2_VERSION": "2.0.0" +} \ No newline at end of file diff --git a/lib/api/root.js b/lib/api/root.js new file mode 100644 index 00000000000..275660eeae8 --- /dev/null +++ b/lib/api/root.js @@ -0,0 +1,23 @@ +'use strict'; + +function configure () { + const express = require('express') + , api = express.Router( ) + , apiConst = require('./const') + , api3Const = require('../api3/const') + ; + + api.get('/versions', function getVersion (req, res) { + + const versions = [ + { version: apiConst.API1_VERSION, url: '/api/v1' }, + { version: apiConst.API2_VERSION, url: '/api/v2' }, + { version: api3Const.API3_VERSION, url: '/api/v3' } + ]; + + res.json(versions); + }); + + return api; +} +module.exports = configure; diff --git a/tests/api.root.test.js b/tests/api.root.test.js new file mode 100644 index 00000000000..d2c3609a117 --- /dev/null +++ b/tests/api.root.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Root REST API', function() { + const self = this + , instance = require('./fixtures/api/instance') + , semver = require('semver') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.server.close(); + }); + + + it('GET /api/versions', async () => { + let res = await request(self.app) + .get('/api/versions') + .expect(200); + + res.body.length.should.be.aboveOrEqual(3); + res.body.forEach(obj => { + const fields = Object.getOwnPropertyNames(obj); + fields.sort().should.be.eql(['url', 'version']); + + semver.valid(obj.version).should.be.ok(); + obj.url.should.startWith('/api'); + }); + }); + +}); + diff --git a/tests/fixtures/api/instance.js b/tests/fixtures/api/instance.js new file mode 100644 index 00000000000..ed5b28474c9 --- /dev/null +++ b/tests/fixtures/api/instance.js @@ -0,0 +1,98 @@ +'use strict'; + +const fs = require('fs') + , path = require('path') + , language = require('../../../lib/language')() + , apiRoot = require('../../../lib/api/root') + , http = require('http') + , https = require('https') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(path.join(__dirname, '../api3/localhost.key')), + cert: fs.readFileSync(path.join(__dirname, '../api3/localhost.crt')) + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'] + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiRootApp = apiRoot(instance.env, ctx); + + instance.app.use('/api', instance.ctx.apiRootApp); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + + return self; +} + +module.exports = configure(); \ No newline at end of file From 13dd0fa77e1063a55cfbd1d38bf8a85e8ef97964 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Fri, 1 Nov 2019 15:06:49 -0600 Subject: [PATCH 081/134] Fixed a few missing definitions --- lib/plugins/ar2.js | 1 + lib/plugins/loop.js | 1 + lib/plugins/pump.js | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index cab989c8150..490d00dbf83 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -17,6 +17,7 @@ var AR2_COLOR = 'cyan'; // eslint-disable-next-line no-unused-vars function init (ctx) { + var translate = ctx.language.translate; var ar2 = { name: 'ar2' diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index 02a37b1f16a..5e7aa22ea7c 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -9,6 +9,7 @@ var levels = require('../levels'); function init (ctx) { var utils = require('../utils')(ctx); + var translate = ctx.language.translate; var loop = { name: 'loop' diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 840afa8aeae..69e0cd4fcf2 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -136,7 +136,8 @@ function init (ctx) { }; function virtAsstReservoirHandler (next, slots, sbx) { - if (sbx.properties.pump.pump.reservoir) { + var reservoir = sbx.properties.pump.pump.reservoir; + if (reservoir) { var response = translate('virtAsstReservoir', { params: [ reservoir From 164ea062dcea6c18db0f990cfcb67a0fed9885fb Mon Sep 17 00:00:00 2001 From: PetrOndrusek Date: Sat, 2 Nov 2019 18:19:01 +0100 Subject: [PATCH 082/134] API3: add locking documents using isReadOnly flag --- lib/api3/const.json | 2 ++ lib/api3/generic/delete/operation.js | 29 ++++++++++++++++++++++++++++ lib/api3/generic/update/validate.js | 5 +++++ lib/api3/swagger.yaml | 24 ++++++++++++++++++++++- 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/api3/const.json b/lib/api3/const.json index f041134480d..1c3dfd873ee 100644 --- a/lib/api3/const.json +++ b/lib/api3/const.json @@ -17,6 +17,7 @@ "NOT_FOUND": 404, "GONE": 410, "PRECONDITION_FAILED": 412, + "UNPROCESSABLE_ENTITY": 422, "INTERNAL_ERROR": 500 }, @@ -39,6 +40,7 @@ "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", + "HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document", "HTTP_500_INTERNAL_ERROR": "Internal Server Error", "STORAGE_ERROR": "Database error", "SOCKET_MISSING_OR_BAD_ACCESS_TOKEN": "Missing or bad accessToken", diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js index 535ca1a7620..344226206b9 100644 --- a/lib/api3/generic/delete/operation.js +++ b/lib/api3/generic/delete/operation.js @@ -14,6 +14,9 @@ async function doDelete (opCtx) { await security.demandPermission(opCtx, `api:${col.colName}:delete`); + if (await validateDelete(opCtx) !== true) + return; + if (req.query.permanent && req.query.permanent === "true") { await deletePermanently(opCtx); } else { @@ -22,6 +25,32 @@ async function doDelete (opCtx) { } +async function validateDelete (opCtx) { + + const { col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.findOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + else { + const storageDoc = result[0]; + + if (storageDoc.isReadOnly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + } + + return true; +} + + async function deletePermanently (opCtx) { const { ctx, col, req, res } = opCtx; diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js index e68eb2955f8..e138a16aba7 100644 --- a/lib/api3/generic/update/validate.js +++ b/lib/api3/generic/update/validate.js @@ -21,6 +21,11 @@ function validate (opCtx, doc, storageDoc, options) { const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; + if (storageDoc.isReadOnly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + for (const field of immutable) { // change of identifier is allowed in deduplication (for APIv1 documents) diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml index 32ac1815aee..17db893e0ef 100644 --- a/lib/api3/swagger.yaml +++ b/lib/api3/swagger.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 servers: - url: '/api/v3' info: - version: '3.0.0' + version: '3.0.1' title: Nightscout API contact: name: NS development discussion channel @@ -178,6 +178,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' #return HTTP STATUS 400 for all other verbs (PUT, PATCH, DELETE,...) @@ -296,6 +298,8 @@ paths: $ref: '#/components/responses/412PreconditionFailed' 410: $ref: '#/components/responses/410Gone' + 422: + $ref: '#/components/responses/422UnprocessableEntity' ###################################################################################### @@ -356,6 +360,8 @@ paths: $ref: '#/components/responses/412PreconditionFailed' 410: $ref: '#/components/responses/410Gone' + 422: + $ref: '#/components/responses/422UnprocessableEntity' ###################################################################################### @@ -388,6 +394,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' ###################################################################################### @@ -886,6 +894,9 @@ components: 410Gone: description: The requested document has already been deleted. + 422UnprocessableEntity: + description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`). + search200: description: Successful operation returning array of documents matching the filtering criteria content: @@ -1129,6 +1140,17 @@ components: example: false + isReadOnly: + type: boolean + description: + A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted. + + + Any attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY. + + + example: true + required: - date - app From c6e2b58b520369423fdcd88e5e8a166af2a88eba Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 5 Nov 2019 00:57:34 -0700 Subject: [PATCH 083/134] Translation improvements --- lib/language.js | 182 ++++++++++++++++++++++++++++++----------- lib/plugins/ar2.js | 23 ++++-- lib/plugins/cob.js | 20 +++-- lib/plugins/loop.js | 23 ++++-- lib/plugins/openaps.js | 4 +- lib/plugins/rawbg.js | 6 +- lib/plugins/upbat.js | 6 +- 7 files changed, 189 insertions(+), 75 deletions(-) diff --git a/lib/language.js b/lib/language.js index 2641feb7add..2adb6168243 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13641,6 +13641,23 @@ function init() { , ru: 'батарея помпы %1 %2' , tr: 'Pompa piliniz %1 %2' }, + 'virtAsstUploaderBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Your uploader battery is at %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Your uploader battery is at %1' + , dk: 'Your uploader battery is at %1' + , ko: 'Your uploader battery is at %1' + , nl: 'Your uploader battery is at %1' + , zh_cn: 'Your uploader battery is at %1' + , sv: 'Your uploader battery is at %1' + , fi: 'Your uploader battery is at %1' + , ro: 'Your uploader battery is at %1' + , pl: 'Your uploader battery is at %1' + , ru: 'Your uploader battery is at %1' + , tr: 'Your uploader battery is at %1' + }, 'virtAsstLastLoop': { bg: 'The last successful loop was %1' , cs: 'Poslední úšpěšné provedení smyčky %1' @@ -13675,39 +13692,73 @@ function init() { , ru: 'плагин ЗЦ Loop не активирован ' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'virtAsstLoopForecast': { - bg: 'According to the loop forecast you are expected to be %1 over the next %2' - , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' - , en: 'According to the loop forecast you are expected to be %1 over the next %2' - , hr: 'According to the loop forecast you are expected to be %1 over the next %2' - , de: 'Entsprechend der Loop Vorhersage landest du bei %1 während der nächsten %2' - , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' - , ko: 'According to the loop forecast you are expected to be %1 over the next %2' - , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' - , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' - , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' - , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' - , pl: 'Zgodnie z prognozą pętli, glikemia %1 będzie podczas następnego %2' - , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' - , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' + 'virtAsstLoopForecastAround': { + bg: 'According to the loop forecast you are expected to be around %1 over the next %2' + , cs: 'Podle přepovědi smyčky je očekávána glykémie around %1 během následujících %2' + , en: 'According to the loop forecast you are expected to be around %1 over the next %2' + , hr: 'According to the loop forecast you are expected to be around %1 over the next %2' + , de: 'Entsprechend der Loop Vorhersage landest du bei around %1 während der nächsten %2' + , dk: 'Ifølge Loops forudsigelse forventes du at blive around %1 i den næste %2' + , ko: 'According to the loop forecast you are expected to be around %1 over the next %2' + , nl: 'Volgens de Loop voorspelling is je waarde around %1 over de volgnede %2' + , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是around %1' + , sv: 'Enligt Loops förutsägelse förväntas du bli around %1 inom %2' + , fi: 'Ennusteen mukaan olet around %1 seuraavan %2 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază around %1 pentru următoarele %2' + , pl: 'Zgodnie z prognozą pętli, glikemia around %1 będzie podczas następnego %2' + , ru: 'по прогнозу алгоритма ЗЦ ожидается around %1 за последующие %2' + , tr: 'Döngü tahminine göre sonraki %2 ye göre around %1 olması bekleniyor' + }, + 'virtAsstLoopForecastBetween': { + bg: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'Podle přepovědi smyčky je očekávána glykémie between %1 and %2 během následujících %3' + , en: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , de: 'Entsprechend der Loop Vorhersage landest du bei between %1 and %2 während der nächsten %3' + , dk: 'Ifølge Loops forudsigelse forventes du at blive between %1 and %2 i den næste %3' + , ko: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'Volgens de Loop voorspelling is je waarde between %1 and %2 over de volgnede %3' + , zh_cn: '根据loop的预测,在接下来的%3你的血糖将会是between %1 and %2' + , sv: 'Enligt Loops förutsägelse förväntas du bli between %1 and %2 inom %3' + , fi: 'Ennusteen mukaan olet between %1 and %2 seuraavan %3 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază between %1 and %2 pentru următoarele %3' + , pl: 'Zgodnie z prognozą pętli, glikemia between %1 and %2 będzie podczas następnego %3' + , ru: 'по прогнозу алгоритма ЗЦ ожидается between %1 and %2 за последующие %3' + , tr: 'Döngü tahminine göre sonraki %3 ye göre between %1 and %2 olması bekleniyor' }, - 'virtAsstAR2Forecast': { - bg: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , cs: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , en: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , hr: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , de: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , dk: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ko: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , nl: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , zh_cn: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , sv: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , fi: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ro: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , pl: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , ru: 'According to the AR2 forecast you are expected to be %1 over the next %2' - , tr: 'According to the AR2 forecast you are expected to be %1 over the next %2' + 'virtAsstAR2ForecastAround': { + bg: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , cs: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , en: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , hr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , de: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , dk: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ko: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , nl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , zh_cn: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , sv: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , fi: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ro: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , pl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ru: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , tr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + }, + 'virtAsstAR2ForecastBetween': { + bg: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , en: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , de: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , dk: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ko: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , zh_cn: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , sv: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , fi: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ro: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , pl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ru: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , tr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' }, 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' @@ -13760,22 +13811,57 @@ function init() { , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' }, - 'virtAsstCOB': { - en: '%1 %2 carbohydrates on board' - , cs: '%1 %2 aktivních sachridů' - , de: '%1 %2 Gramm Kohlenhydrate wirkend.' - , dk: '%1 %2 gram aktive kulhydrater' - , ko: '%1 %2 carbohydrates on board' - , nl: '%1 %2 actieve koolhydraten' - , zh_cn: '%1 %2 活性碳水化合物' - , sv: '%1 %2 gram aktiva kolhydrater' - , fi: '%1 %2 aktiivista hiilihydraattia' - , ro: '%1 %2 carbohidrați activi în corp' - , bg: '%1 %2 carbohydrates on board' - , hr: '%1 %2 carbohydrates on board' - , pl: '%1 %2 aktywnych węglowodanów' - , ru: '%1 $2 активных углеводов' - , tr: '%1 %2 aktif karbonhidrat' + 'virtAsstCob3person': { + bg: '%1 has $2 carbohydrates on board' + , cs: '%1 has $2 carbohydrates on board' + , de: '%1 has $2 carbohydrates on board' + , dk: '%1 has $2 carbohydrates on board' + , el: '%1 has $2 carbohydrates on board' + , en: '%1 has $2 carbohydrates on board' + , es: '%1 has $2 carbohydrates on board' + , fi: '%1 has $2 carbohydrates on board' + , fr: '%1 has $2 carbohydrates on board' + , he: '%1 has $2 carbohydrates on board' + , hr: '%1 has $2 carbohydrates on board' + , it: '%1 has $2 carbohydrates on board' + , ko: '%1 has $2 carbohydrates on board' + , nb: '%1 has $2 carbohydrates on board' + , nl: '%1 has $2 carbohydrates on board' + , pl: '%1 has $2 carbohydrates on board' + , pt: '%1 has $2 carbohydrates on board' + , ro: '%1 has $2 carbohydrates on board' + , ru: '%1 has $2 carbohydrates on board' + , sk: '%1 has $2 carbohydrates on board' + , sv: '%1 has $2 carbohydrates on board' + , tr: '%1 has $2 carbohydrates on board' + , zh_cn: '%1 has $2 carbohydrates on board' + , zh_tw: '%1 has $2 carbohydrates on board' + }, + 'virtAsstCob': { + bg: 'You have %1 carbohydrates on board' + , cs: 'You have %1 carbohydrates on board' + , de: 'You have %1 carbohydrates on board' + , dk: 'You have %1 carbohydrates on board' + , el: 'You have %1 carbohydrates on board' + , en: 'You have %1 carbohydrates on board' + , es: 'You have %1 carbohydrates on board' + , fi: 'You have %1 carbohydrates on board' + , fr: 'You have %1 carbohydrates on board' + , he: 'You have %1 carbohydrates on board' + , hr: 'You have %1 carbohydrates on board' + , it: 'You have %1 carbohydrates on board' + , ko: 'You have %1 carbohydrates on board' + , nb: 'You have %1 carbohydrates on board' + , nl: 'You have %1 carbohydrates on board' + , pl: 'You have %1 carbohydrates on board' + , pt: 'You have %1 carbohydrates on board' + , ro: 'You have %1 carbohydrates on board' + , ru: 'You have %1 carbohydrates on board' + , sk: 'You have %1 carbohydrates on board' + , sv: 'You have %1 carbohydrates on board' + , tr: 'You have %1 carbohydrates on board' + , zh_cn: 'You have %1 carbohydrates on board' + , zh_tw: 'You have %1 carbohydrates on board' }, 'virtAsstUnknownIntentTitle': { en: 'Unknown Intent' diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 490d00dbf83..9b3978ef0c0 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -164,18 +164,23 @@ function init (ctx) { maxForecastMills = forecast[i].mills; } } - var value = ''; + var response = ''; if (min === max) { - value = translate('around') + ' ' + max; + response = translate('virtAsstAR2ForecastAround', { + params: [ + max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); } else { - value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; + response = translate('virtAsstAR2ForecastBetween', { + params: [ + min + , max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); } - var response = translate('virtAsstAR2Forecast', { - params: [ - value - , moment(maxForecastMills).from(moment(sbx.time)) - ] - }); next('AR2 Forecast', response); } else { next('AR2 Forecast', translate('virtAsstUnknown')); diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index ef9b4252be5..32b6f583b58 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -293,12 +293,22 @@ function init (ctx) { }; function virtAsstCOBHandler (next, slots, sbx) { - var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; - var value = 'no'; - if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { - value = sbx.properties.cob.cob; + var response = ''; + var value = (sbx.properties.cob && sbx.properties.cob.cob) ? sbx.properties.cob.cob : 0; + if (slots && slots.pwd && slots.pwd.value) { + response = translate('virtAsstCob3person', { + params: [ + slots.pwd.value.replace('\'s', '') + , value + ] + }); + } else { + response = translate('virtAsstCob', { + params: [ + value + ] + }); } - var response = preamble + ' ' + value + ' carbohydrates on board'; next('Current COB', response); } diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index 5e7aa22ea7c..c290fb0776d 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -527,18 +527,23 @@ function init (ctx) { min = forecast[i]; } } - var value = ''; + var response = ''; if (min === max) { - value = translate('around') + ' ' + max; + response = translate('virtAsstLoopForecastAround', { + params: [ + max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); } else { - value = translate('between') + ' ' + min + ' ' + translate('and') + ' ' + max; + response = translate('virtAsstLoopForecastBetween', { + params: [ + min + , max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); } - var response = translate('virtAsstLoopForecast', { - params: [ - value - , moment(endPrediction).from(moment(sbx.time)) - ] - }); next('Loop Forecast', response); } } else { diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 64b8dccdd12..0e249cdfabb 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -523,9 +523,9 @@ function init (ctx) { sbx.properties.openaps.lastEventualBG ] }); - next('Loop Forecast', response); + next('OpenAPS Forecast', response); } else { - next('Loop Forecast', translate('virtAsstUnknown')); + next('OpenAPS Forecast', translate('virtAsstUnknown')); } } diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 997cf7c55a3..5ac28fc8a8e 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -108,7 +108,11 @@ function init (ctx) { function virtAsstRawBGHandler (next, slots, sbx) { if (sbx.properties.rawbg.mgdl) { - var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; + var response = translate('virtAsstRawBG', { + params: [ + sbx.properties.rawbg.mgdl + ] + }); next('Current Raw BG', response); } else { next('Current Raw BG', translate('virtAsstUnknown')); diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 1bd8795ef7c..9aad634be89 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -223,7 +223,11 @@ function init() { function virtAsstUploaderBatteryHandler (next, slots, sbx) { if (sbx.properties.upbat.display) { - var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; + var response = translate('virtAsstUploaderBattery', { + params: [ + sbx.properties.upbat.display + ] + }); next('Uploader Battery', response); } else { next('Uploader Battery', translate('virtAsstUnknown')); From 98b222c0e7403b76aca3596308ac215b16fa116d Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 5 Nov 2019 12:46:26 -0700 Subject: [PATCH 084/134] Defined `translate()` in upbat plugin --- lib/plugins/upbat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 9aad634be89..d332e9c78ae 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -4,7 +4,8 @@ var _ = require('lodash'); var times = require('../times'); var levels = require('../levels'); -function init() { +function init(ctx) { + var translate = ctx.language.translate; var upbat = { name: 'upbat' From b9dc376b19194fa30e805de937b61112af9c52cc Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 5 Nov 2019 14:11:55 -0700 Subject: [PATCH 085/134] Migrated titles to use translate() --- lib/language.js | 286 ++++++++++++++++++++++++++++++++++++ lib/plugins/ar2.js | 4 +- lib/plugins/basalprofile.js | 2 +- lib/plugins/cob.js | 2 +- lib/plugins/iob.js | 2 +- lib/plugins/loop.js | 10 +- lib/plugins/openaps.js | 8 +- lib/plugins/pump.js | 8 +- lib/plugins/rawbg.js | 4 +- lib/plugins/upbat.js | 4 +- 10 files changed, 308 insertions(+), 22 deletions(-) diff --git a/lib/language.js b/lib/language.js index 2adb6168243..f48b4c2db7b 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13356,6 +13356,292 @@ function init() { , zh_cn: 'That value is unknown at the moment. Please see your Nightscout site for more details.' , zh_tw: 'That value is unknown at the moment. Please see your Nightscout site for more details.' }, + 'virtAsstTitleAR2Forecast': { + bg: 'AR2 Forecast' + , cs: 'AR2 Forecast' + , de: 'AR2 Forecast' + , dk: 'AR2 Forecast' + , el: 'AR2 Forecast' + , en: 'AR2 Forecast' + , es: 'AR2 Forecast' + , fi: 'AR2 Forecast' + , fr: 'AR2 Forecast' + , he: 'AR2 Forecast' + , hr: 'AR2 Forecast' + , it: 'AR2 Forecast' + , ko: 'AR2 Forecast' + , nb: 'AR2 Forecast' + , pl: 'AR2 Forecast' + , pt: 'AR2 Forecast' + , ro: 'AR2 Forecast' + , nl: 'AR2 Forecast' + , ru: 'AR2 Forecast' + , sk: 'AR2 Forecast' + , sv: 'AR2 Forecast' + , tr: 'AR2 Forecast' + , zh_cn: 'AR2 Forecast' + , zh_tw: 'AR2 Forecast' + }, + 'virtAsstTitleCurrentBasal': { + bg: 'Current Basal' + , cs: 'Current Basal' + , de: 'Current Basal' + , dk: 'Current Basal' + , el: 'Current Basal' + , en: 'Current Basal' + , es: 'Current Basal' + , fi: 'Current Basal' + , fr: 'Current Basal' + , he: 'Current Basal' + , hr: 'Current Basal' + , it: 'Current Basal' + , ko: 'Current Basal' + , nb: 'Current Basal' + , pl: 'Current Basal' + , pt: 'Current Basal' + , ro: 'Current Basal' + , nl: 'Current Basal' + , ru: 'Current Basal' + , sk: 'Current Basal' + , sv: 'Current Basal' + , tr: 'Current Basal' + , zh_cn: 'Current Basal' + , zh_tw: 'Current Basal' + }, + 'virtAsstTitleCurrentCOB': { + bg: 'Current COB' + , cs: 'Current COB' + , de: 'Current COB' + , dk: 'Current COB' + , el: 'Current COB' + , en: 'Current COB' + , es: 'Current COB' + , fi: 'Current COB' + , fr: 'Current COB' + , he: 'Current COB' + , hr: 'Current COB' + , it: 'Current COB' + , ko: 'Current COB' + , nb: 'Current COB' + , pl: 'Current COB' + , pt: 'Current COB' + , ro: 'Current COB' + , nl: 'Current COB' + , ru: 'Current COB' + , sk: 'Current COB' + , sv: 'Current COB' + , tr: 'Current COB' + , zh_cn: 'Current COB' + , zh_tw: 'Current COB' + }, + 'virtAsstTitleCurrentIOB': { + bg: 'Current IOB' + , cs: 'Current IOB' + , de: 'Current IOB' + , dk: 'Current IOB' + , el: 'Current IOB' + , en: 'Current IOB' + , es: 'Current IOB' + , fi: 'Current IOB' + , fr: 'Current IOB' + , he: 'Current IOB' + , hr: 'Current IOB' + , it: 'Current IOB' + , ko: 'Current IOB' + , nb: 'Current IOB' + , pl: 'Current IOB' + , pt: 'Current IOB' + , ro: 'Current IOB' + , nl: 'Current IOB' + , ru: 'Current IOB' + , sk: 'Current IOB' + , sv: 'Current IOB' + , tr: 'Current IOB' + , zh_cn: 'Current IOB' + , zh_tw: 'Current IOB' + }, + 'virtAsstTitleLoopForecast': { + bg: 'Loop Forecast' + , cs: 'Loop Forecast' + , de: 'Loop Forecast' + , dk: 'Loop Forecast' + , el: 'Loop Forecast' + , en: 'Loop Forecast' + , es: 'Loop Forecast' + , fi: 'Loop Forecast' + , fr: 'Loop Forecast' + , he: 'Loop Forecast' + , hr: 'Loop Forecast' + , it: 'Loop Forecast' + , ko: 'Loop Forecast' + , nb: 'Loop Forecast' + , pl: 'Loop Forecast' + , pt: 'Loop Forecast' + , ro: 'Loop Forecast' + , nl: 'Loop Forecast' + , ru: 'Loop Forecast' + , sk: 'Loop Forecast' + , sv: 'Loop Forecast' + , tr: 'Loop Forecast' + , zh_cn: 'Loop Forecast' + , zh_tw: 'Loop Forecast' + }, + 'virtAsstTitleLastLoop': { + bg: 'Last Loop' + , cs: 'Last Loop' + , de: 'Last Loop' + , dk: 'Last Loop' + , el: 'Last Loop' + , en: 'Last Loop' + , es: 'Last Loop' + , fi: 'Last Loop' + , fr: 'Last Loop' + , he: 'Last Loop' + , hr: 'Last Loop' + , it: 'Last Loop' + , ko: 'Last Loop' + , nb: 'Last Loop' + , pl: 'Last Loop' + , pt: 'Last Loop' + , ro: 'Last Loop' + , nl: 'Last Loop' + , ru: 'Last Loop' + , sk: 'Last Loop' + , sv: 'Last Loop' + , tr: 'Last Loop' + , zh_cn: 'Last Loop' + , zh_tw: 'Last Loop' + }, + 'virtAsstTitleOpenAPSForecast': { + bg: 'OpenAPS Forecast' + , cs: 'OpenAPS Forecast' + , de: 'OpenAPS Forecast' + , dk: 'OpenAPS Forecast' + , el: 'OpenAPS Forecast' + , en: 'OpenAPS Forecast' + , es: 'OpenAPS Forecast' + , fi: 'OpenAPS Forecast' + , fr: 'OpenAPS Forecast' + , he: 'OpenAPS Forecast' + , hr: 'OpenAPS Forecast' + , it: 'OpenAPS Forecast' + , ko: 'OpenAPS Forecast' + , nb: 'OpenAPS Forecast' + , pl: 'OpenAPS Forecast' + , pt: 'OpenAPS Forecast' + , ro: 'OpenAPS Forecast' + , nl: 'OpenAPS Forecast' + , ru: 'OpenAPS Forecast' + , sk: 'OpenAPS Forecast' + , sv: 'OpenAPS Forecast' + , tr: 'OpenAPS Forecast' + , zh_cn: 'OpenAPS Forecast' + , zh_tw: 'OpenAPS Forecast' + }, + 'virtAsstTitlePumpReservoir': { + bg: 'Insulin Remaining' + , cs: 'Insulin Remaining' + , de: 'Insulin Remaining' + , dk: 'Insulin Remaining' + , el: 'Insulin Remaining' + , en: 'Insulin Remaining' + , es: 'Insulin Remaining' + , fi: 'Insulin Remaining' + , fr: 'Insulin Remaining' + , he: 'Insulin Remaining' + , hr: 'Insulin Remaining' + , it: 'Insulin Remaining' + , ko: 'Insulin Remaining' + , nb: 'Insulin Remaining' + , pl: 'Insulin Remaining' + , pt: 'Insulin Remaining' + , ro: 'Insulin Remaining' + , nl: 'Insulin Remaining' + , ru: 'Insulin Remaining' + , sk: 'Insulin Remaining' + , sv: 'Insulin Remaining' + , tr: 'Insulin Remaining' + , zh_cn: 'Insulin Remaining' + , zh_tw: 'Insulin Remaining' + }, + 'virtAsstTitlePumpBattery': { + bg: 'Pump Battery' + , cs: 'Pump Battery' + , de: 'Pump Battery' + , dk: 'Pump Battery' + , el: 'Pump Battery' + , en: 'Pump Battery' + , es: 'Pump Battery' + , fi: 'Pump Battery' + , fr: 'Pump Battery' + , he: 'Pump Battery' + , hr: 'Pump Battery' + , it: 'Pump Battery' + , ko: 'Pump Battery' + , nb: 'Pump Battery' + , pl: 'Pump Battery' + , pt: 'Pump Battery' + , ro: 'Pump Battery' + , nl: 'Pump Battery' + , ru: 'Pump Battery' + , sk: 'Pump Battery' + , sv: 'Pump Battery' + , tr: 'Pump Battery' + , zh_cn: 'Pump Battery' + , zh_tw: 'Pump Battery' + }, + 'virtAsstTitleRawBG': { + bg: 'Current Raw BG' + , cs: 'Current Raw BG' + , de: 'Current Raw BG' + , dk: 'Current Raw BG' + , el: 'Current Raw BG' + , en: 'Current Raw BG' + , es: 'Current Raw BG' + , fi: 'Current Raw BG' + , fr: 'Current Raw BG' + , he: 'Current Raw BG' + , hr: 'Current Raw BG' + , it: 'Current Raw BG' + , ko: 'Current Raw BG' + , nb: 'Current Raw BG' + , pl: 'Current Raw BG' + , pt: 'Current Raw BG' + , ro: 'Current Raw BG' + , nl: 'Current Raw BG' + , ru: 'Current Raw BG' + , sk: 'Current Raw BG' + , sv: 'Current Raw BG' + , tr: 'Current Raw BG' + , zh_cn: 'Current Raw BG' + , zh_tw: 'Current Raw BG' + }, + 'virtAsstTitleUploaderBattery': { + bg: 'Uploader Battery' + , cs: 'Uploader Battery' + , de: 'Uploader Battery' + , dk: 'Uploader Battery' + , el: 'Uploader Battery' + , en: 'Uploader Battery' + , es: 'Uploader Battery' + , fi: 'Uploader Battery' + , fr: 'Uploader Battery' + , he: 'Uploader Battery' + , hr: 'Uploader Battery' + , it: 'Uploader Battery' + , ko: 'Uploader Battery' + , nb: 'Uploader Battery' + , pl: 'Uploader Battery' + , pt: 'Uploader Battery' + , ro: 'Uploader Battery' + , nl: 'Uploader Battery' + , ru: 'Uploader Battery' + , sk: 'Uploader Battery' + , sv: 'Uploader Battery' + , tr: 'Uploader Battery' + , zh_cn: 'Uploader Battery' + , zh_tw: 'Uploader Battery' + }, 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 9b3978ef0c0..52aec073759 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -181,9 +181,9 @@ function init (ctx) { ] }); } - next('AR2 Forecast', response); + next(translate('virtAsstTitleAR2Forecast'), response); } else { - next('AR2 Forecast', translate('virtAsstUnknown')); + next(translate('virtAsstTitleAR2Forecast'), translate('virtAsstUnknown')); } } diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index c7c54da75c7..06aa53c7bd7 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -149,7 +149,7 @@ function init (ctx) { } function virtAsstCurrentBasalhandler (next, slots, sbx) { - next('Current Basal', basalMessage(slots, sbx)); + next(translate('virtAsstTitleCurrentBasal'), basalMessage(slots, sbx)); } basal.virtAsst = { diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 32b6f583b58..c066dec9988 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -309,7 +309,7 @@ function init (ctx) { ] }); } - next('Current COB', response); + next(translate('virtAsstTitleCurrentCOB'), response); } cob.virtAsst = { diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 614cc56981b..96bea03b3ff 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -250,7 +250,7 @@ function init(ctx) { getIob(sbx) ] }); - callback('Current IOB', message); + callback(translate('virtAsstTitleCurrentIOB'), message); } function virtAsstIOBRollupHandler (slots, sbx, callback) { diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index c290fb0776d..ff6e153d796 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -517,7 +517,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', translate('virtAsstForecastUnavailable')); + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstForecastUnavailable')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -544,10 +544,10 @@ function init (ctx) { ] }); } - next('Loop Forecast', response); + next(translate('virtAsstTitleLoopForecast'), response); } } else { - next('Loop Forecast', translate('virtAsstUnknown')); + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstUnknown')); } } @@ -559,9 +559,9 @@ function init (ctx) { moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) ] }); - next('Last Loop', response); + next(translate('virtAsstTitleLastLoop'), response); } else { - next('Last Loop', translate('virtAsstUnknown')); + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); } } diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 0e249cdfabb..5704575547a 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -523,9 +523,9 @@ function init (ctx) { sbx.properties.openaps.lastEventualBG ] }); - next('OpenAPS Forecast', response); + next(translate('virtAsstTitleOpenAPSForecast'), response); } else { - next('OpenAPS Forecast', translate('virtAsstUnknown')); + next(translate('virtAsstTitleOpenAPSForecast'), translate('virtAsstUnknown')); } } @@ -537,9 +537,9 @@ function init (ctx) { moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) ] }); - next('Last Loop', response); + next(translate('virtAsstTitleLastLoop'), response); } else { - next('Last Loop', translate('virtAsstUnknown')); + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); } } diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 69e0cd4fcf2..f23dd70b9cb 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -143,9 +143,9 @@ function init (ctx) { reservoir ] }); - next('Remaining Insulin', response); + next(translate('virtAsstTitlePumpReservoir'), response); } else { - next('Remaining Insulin', translate('virtAsstUnknown')); + next(translate('virtAsstTitlePumpReservoir'), translate('virtAsstUnknown')); } } @@ -158,9 +158,9 @@ function init (ctx) { battery.unit ] }); - next('Pump Battery', response); + next(translate('virtAsstTitlePumpBattery'), response); } else { - next('Pump Battery', translate('virtAsstUnknown')); + next(translate('virtAsstTitlePumpBattery'), translate('virtAsstUnknown')); } } diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 5ac28fc8a8e..3248126b046 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -113,9 +113,9 @@ function init (ctx) { sbx.properties.rawbg.mgdl ] }); - next('Current Raw BG', response); + next(translate('virtAsstTitleRawBG'), response); } else { - next('Current Raw BG', translate('virtAsstUnknown')); + next(translate('virtAsstTitleRawBG'), translate('virtAsstUnknown')); } } diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index d332e9c78ae..dc603054ecb 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -229,9 +229,9 @@ function init(ctx) { sbx.properties.upbat.display ] }); - next('Uploader Battery', response); + next(translate('virtAsstTitleUploaderBattery'), response); } else { - next('Uploader Battery', translate('virtAsstUnknown')); + next(translate('virtAsstTitleUploaderBattery'), translate('virtAsstUnknown')); } } From c33aebbbd5705647121c49266682d503272cbaef Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 5 Nov 2019 14:29:51 -0700 Subject: [PATCH 086/134] Migrated static intent titles to use translate() --- lib/api/alexa/index.js | 4 +-- lib/api/googlehome/index.js | 4 +-- lib/language.js | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 1cce2764011..337ec00f732 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -109,13 +109,13 @@ function configure (app, wares, ctx, env) { moment(records[0].date).from(moment(sbx.time))] }); - callback('Current blood glucose', status); + callback(translate('virtAsstTitleCurrentBG'), status); }); }, ['bg', 'blood glucose', 'number']); ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + callback(translate('virtAsstTitleFullStatus'), status); }); }); diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index b03ac42bfe3..2b2caa2a378 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -100,13 +100,13 @@ function configure (app, wares, ctx, env) { moment(records[0].date).from(moment(sbx.time))] }); - callback('Current blood glucose', status); + callback(translate('virtAsstTitleCurrentBG'), status); }); }, ['bg', 'blood glucose', 'number']); ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + callback(translate('virtAsstTitleFullStatus'), status); }); }); diff --git a/lib/language.js b/lib/language.js index f48b4c2db7b..9329c24208a 100644 --- a/lib/language.js +++ b/lib/language.js @@ -13642,6 +13642,58 @@ function init() { , zh_cn: 'Uploader Battery' , zh_tw: 'Uploader Battery' }, + 'virtAsstTitleCurrentBG': { + bg: 'Current BG' + , cs: 'Current BG' + , de: 'Current BG' + , dk: 'Current BG' + , el: 'Current BG' + , en: 'Current BG' + , es: 'Current BG' + , fi: 'Current BG' + , fr: 'Current BG' + , he: 'Current BG' + , hr: 'Current BG' + , it: 'Current BG' + , ko: 'Current BG' + , nb: 'Current BG' + , pl: 'Current BG' + , pt: 'Current BG' + , ro: 'Current BG' + , nl: 'Current BG' + , ru: 'Current BG' + , sk: 'Current BG' + , sv: 'Current BG' + , tr: 'Current BG' + , zh_cn: 'Current BG' + , zh_tw: 'Current BG' + }, + 'virtAsstTitleFullStatus': { + bg: 'Full Status' + , cs: 'Full Status' + , de: 'Full Status' + , dk: 'Full Status' + , el: 'Full Status' + , en: 'Full Status' + , es: 'Full Status' + , fi: 'Full Status' + , fr: 'Full Status' + , he: 'Full Status' + , hr: 'Full Status' + , it: 'Full Status' + , ko: 'Full Status' + , nb: 'Full Status' + , pl: 'Full Status' + , pt: 'Full Status' + , ro: 'Full Status' + , nl: 'Full Status' + , ru: 'Full Status' + , sk: 'Full Status' + , sv: 'Full Status' + , tr: 'Full Status' + , zh_cn: 'Full Status' + , zh_tw: 'Full Status' + }, 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' From 43a32b34fb47a2db2644d8ae798cc831eaf2eb56 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 5 Nov 2019 14:52:00 -0700 Subject: [PATCH 087/134] =?UTF-8?q?Misc=20fixes=20for=20tests=20(even=20th?= =?UTF-8?q?ough=20they=20didn't=20fail=20=F0=9F=A4=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/plugins/loop.js | 4 ++-- tests/openaps.test.js | 2 +- tests/pump.test.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index ff6e153d796..9b099dd188c 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -532,7 +532,7 @@ function init (ctx) { response = translate('virtAsstLoopForecastAround', { params: [ max - , moment(maxForecastMills).from(moment(sbx.time)) + , moment(endPrediction).from(moment(sbx.time)) ] }); } else { @@ -540,7 +540,7 @@ function init (ctx) { params: [ min , max - , moment(maxForecastMills).from(moment(sbx.time)) + , moment(endPrediction).from(moment(sbx.time)) ] }); } diff --git a/tests/openaps.test.js b/tests/openaps.test.js index 5c76deeaaf3..660770b9274 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -385,7 +385,7 @@ describe('openaps', function ( ) { openaps.virtAsst.intentHandlers.length.should.equal(2); openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Loop Forecast'); + title.should.equal('OpenAPS Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { diff --git a/tests/pump.test.js b/tests/pump.test.js index 374ba03f06c..87ab615d322 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -269,7 +269,7 @@ describe('pump', function ( ) { pump.virtAsst.intentHandlers.length.should.equal(4); pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Remaining Insulin'); + title.should.equal('Insulin Remaining'); response.should.equal('You have 86.4 units remaining'); pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { @@ -277,7 +277,7 @@ describe('pump', function ( ) { response.should.equal('Your pump battery is at 1.52 volts'); pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { - title.should.equal('Remaining Insulin'); + title.should.equal('Insulin Remaining'); response.should.equal('You have 86.4 units remaining'); pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { From 51a7c12ba3f6e3bf005ea24f4e310c187f9c349d Mon Sep 17 00:00:00 2001 From: PetrOndrusek Date: Thu, 7 Nov 2019 23:46:34 +0100 Subject: [PATCH 088/134] API3: testing isReadOnly feature --- lib/api3/generic/update/validate.js | 2 +- tests/api3.generic.workflow.test.js | 38 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js index e138a16aba7..187df3dbb06 100644 --- a/lib/api3/generic/update/validate.js +++ b/lib/api3/generic/update/validate.js @@ -21,7 +21,7 @@ function validate (opCtx, doc, storageDoc, options) { const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; - if (storageDoc.isReadOnly === true) { + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true) { return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, apiConst.MSG.HTTP_422_READONLY_MODIFICATION); } diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js index 36c94a00f74..acebe39555a 100644 --- a/tests/api3.generic.workflow.test.js +++ b/tests/api3.generic.workflow.test.js @@ -253,5 +253,43 @@ describe('Generic REST API3', function() { } }); + + it('should not modify read-only document', async () => { + await self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(Object.assign({}, self.docOriginal, { isReadOnly: true })) + .expect(201); + + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + self.docActual = res.body; + delete self.docActual.srvModified; + const readOnlyMessage = 'Trying to modify read-only document'; + + res = await self.instance.post(`${self.urlCol}?token=${self.token.update}`) + .send(Object.assign({}, self.docActual, { insulin: 0.41 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(Object.assign({}, self.docActual, { insulin: 0.42 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ insulin: 0.43 }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + res.body.should.containEql(self.docOriginal); + }); + }); From 2050ddc0564a4e32d4fb67e8716734c50ae00e0a Mon Sep 17 00:00:00 2001 From: PetrOndrusek Date: Thu, 7 Nov 2019 23:56:04 +0100 Subject: [PATCH 089/134] API3: more readonly field name variants --- lib/api3/generic/delete/operation.js | 2 +- lib/api3/generic/update/validate.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js index 344226206b9..8f9463f3ef9 100644 --- a/lib/api3/generic/delete/operation.js +++ b/lib/api3/generic/delete/operation.js @@ -41,7 +41,7 @@ async function validateDelete (opCtx) { else { const storageDoc = result[0]; - if (storageDoc.isReadOnly === true) { + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, apiConst.MSG.HTTP_422_READONLY_MODIFICATION); } diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js index 187df3dbb06..b36e1410067 100644 --- a/lib/api3/generic/update/validate.js +++ b/lib/api3/generic/update/validate.js @@ -21,7 +21,7 @@ function validate (opCtx, doc, storageDoc, options) { const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; - if (storageDoc.isReadOnly === true || storageDoc.readOnly === true) { + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, apiConst.MSG.HTTP_422_READONLY_MODIFICATION); } From f0fb480910345ac5845b86384ced4bd1006c7429 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 9 Nov 2019 21:12:39 -0700 Subject: [PATCH 090/134] Added instructions on how to update skills/agents --- docs/plugins/alexa-plugin.md | 18 +++++++++++++++++- docs/plugins/googlehome-plugin.md | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 8ba8188143e..22d0701bf25 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -43,7 +43,7 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p 1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. 1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. -1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). +1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). ### Create a new Alexa skill @@ -95,6 +95,22 @@ See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. +## Updating your skill with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Alexa may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your Alexa skill. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Open [the latest skill template](alexa-templates/) in your language. You'll be copying the contents of the file later. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). +1. Open your Nightscout skill. +1. Open the "JSON Editor" in the left navigation pane. +1. Select everything in the text box (Ctrl + A on Windows, Cmd + A on Mac) and delete it. +1. Copy the contents of the updated template and paste it in the text box in the JSON Editor page. +1. Click the "Save Model" button near the top of the page, and then click the "Build Model" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. Enjoy the new features! + ## Adding support for additional languages If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index c5dff113ab1..305fa608e14 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -54,6 +54,22 @@ That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. +## Updating your agent with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Google Home may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your DialogFlow agent. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Download [the latest skill template](google-home-templates/) in your language. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [DialogFlow developer portal](https://dialogflow.cloud.google.com/). +1. Make sure you're viewing your Nightscout agent (there's a drop-down box immediately below the DialogFlow logo where you can select your agent). +1. Click on the gear icon next to your agent name, then click on the "Export and Import" tab. +1. Click the "RESTORE FROM ZIP" button. +1. Select the template file you downloaded earlier, then type "RESTORE" in the text box as requested, and click the "RESTORE" button. +1. After the import is completed, click the "DONE" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. Enjoy the new features! + ## Adding support for additional languages If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. From c4ee62b49505a743ab002984af6f3575b2594346 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Sun, 10 Nov 2019 13:18:05 -0600 Subject: [PATCH 091/134] fix devicestatus retro merge (#5193) --- lib/client/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/client/index.js b/lib/client/index.js index 01b1dce60ea..8c5d22f5c82 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -468,6 +468,16 @@ client.load = function load (serverSettings, callback) { adjustCurrentSGVClasses(value, isCurrent); } + function mergeDeviceStatus (retro, ddata) { + var result = retro.map(x => Object.assign(x, ddata.find(y => y._id == x._id))); + + var missingInRetro = ddata.filter(y => !retro.find(x => x._id == y._id)); + + result.push(...missingInRetro); + + return result; + } + function updatePlugins (time) { //TODO: doing a clone was slow, but ok to let plugins muck with data? @@ -482,7 +492,7 @@ client.load = function load (serverSettings, callback) { var mergedStatuses = client.ddata.devicestatus; if (client.retro.data) { - mergedStatuses = _.merge({}, client.retro.data.devicestatus, client.ddata.devicestatus); + mergedStatuses = mergeDeviceStatus(client.retro.data.devicestatus, client.ddata.devicestatus); } var clonedData = _.clone(client.ddata); From fcfc209f22df048d493aa430aa0d8cf2edb0cc92 Mon Sep 17 00:00:00 2001 From: Diabetlum <44998140+diabetlum@users.noreply.github.com> Date: Sun, 10 Nov 2019 22:04:48 +0100 Subject: [PATCH 092/134] Turkish language updates (#5192) * Turkish language updates * in to Languages Part Turkish added * in to Languages Part Turkish added --- README.md | 2 +- lib/language.js | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8ad79a39d72..f81f9265a04 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used - * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), zh_cn (中文(简体)), zh_tw (中文(繁體)) + * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), tr (Turkish), zh_cn (中文(简体)), zh_tw (中文(繁體)) * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. * The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted. diff --git a/lib/language.js b/lib/language.js index cb56d3f500f..1ad709af080 100644 --- a/lib/language.js +++ b/lib/language.js @@ -14047,37 +14047,44 @@ function init() { }, 'Protein': { fi: 'Proteiini' - , de: 'Protein' + ,de: 'Protein' + ,tr: 'Protein' ,hr: 'Proteini' }, 'Fat': { fi: 'Rasva' - , de: 'Fett' + ,de: 'Fett' + ,tr: 'Yağ' ,hr: 'Masti' }, 'Protein average': { fi: 'Proteiini keskiarvo' - , de: 'Proteine Durchschnitt' + ,de: 'Proteine Durchschnitt' + ,tr: 'Protein Ortalaması' ,hr: 'Prosjek proteina' }, 'Fat average': { fi: 'Rasva keskiarvo' - , de: 'Fett Durchschnitt' + ,de: 'Fett Durchschnitt' + ,tr: 'Yağ Ortalaması' ,hr: 'Prosjek masti' }, 'Total carbs': { fi: 'Hiilihydraatit yhteensä' , de: 'Kohlenhydrate gesamt' + ,tr: 'Toplam Karbonhidrat' ,hr: 'Ukupno ugh' }, 'Total protein': { fi: 'Proteiini yhteensä' , de: 'Protein gesamt' + ,tr: 'Toplam Protein' ,hr: 'Ukupno proteini' }, 'Total fat': { fi: 'Rasva yhteensä' , de: 'Fett gesamt' + ,tr: 'Toplam Yağ' ,hr: 'Ukupno masti' } }; From c6e763521c6aac3fe5c83e26dab46723a2ee2e1d Mon Sep 17 00:00:00 2001 From: Willem Fibbe Date: Sun, 10 Nov 2019 22:07:01 +0100 Subject: [PATCH 093/134] Fix falsely suspended timeago alarms (#5170) * Fix continuous suspension of the timeago alerts The default hearbeat-setting is 60 seconds, so the delta between two timeago-checks will always be >15 seconds and the timeago-alarms will always be suspended (that's what Papertrail also shows). To fix this, make the delta-check heartbeat-setting-dependant and also simplify the code by using just 1 variable. * Add test that verifies the hibernation detection behaviour The real-world test would be to actually wait for a couple of minutes (with 2 * heartbeat of default settings) in the unit test, but this is not feasible, so just modify the heartbeat-setting to a lower value. I tested it by only running tests inside `tests/timeago.test.js` and by actually deploying this code and testing the alarms with Pushover and reading the logs in Papertrail. Before this change, I saw a 'Hibernation detected' log every minute. After this change, I didn't see it anymore, probably because the app wasn't actually hibernated (yet). * Take sulkaharo's feedback into account and differentiate between client and server (by introducing it in sandbox.js). On the client the behaviour is different from the server: > On client, the issue is browsers stop the execution of JS if the window is not > visible and the alarm is falsely triggered immediately when the execution is > resumed, so we need to suspend the alarm for ~10 seconds after the execution has > resumed to give the client time to update the data to prevent false alarms. While on the server, the default heartbeat from 60s needs to be taken into account to prevent the timeago alarm from falsely triggering. So detect hibernation there if the last check was more than 2 heartbeats ago. * Fix the tests by adding settings to the context, which is now required by timeago. Also, change the timeago test a bit so that it both succeeds when testing in isolation as when testing it along with the other tests. --- lib/plugins/timeago.js | 22 ++++++++++++++++------ lib/sandbox.js | 2 ++ tests/bgnow.test.js | 1 + tests/iob.test.js | 1 + tests/loop.test.js | 1 + tests/openaps.test.js | 1 + tests/pump.test.js | 1 + tests/timeago.test.js | 30 ++++++++++++++++++++++++++++++ 8 files changed, 53 insertions(+), 6 deletions(-) diff --git a/lib/plugins/timeago.js b/lib/plugins/timeago.js index 6f8989a4876..4b176764d45 100644 --- a/lib/plugins/timeago.js +++ b/lib/plugins/timeago.js @@ -7,6 +7,7 @@ var lastSuspendTime = new Date("1900-01-01"); function init(ctx) { var translate = ctx.language.translate; + var heartbeatMs = ctx.settings.heartbeat * 1000; var timeago = { name: 'timeago', @@ -64,20 +65,29 @@ function init(ctx) { }; timeago.checkStatus = function checkStatus(sbx) { - // Check if the app has been suspended; if yes, snooze data missing alarmn for 15 seconds var now = new Date(); var delta = now.getTime() - lastChecked.getTime(); lastChecked = now; - if (delta > 15 * 1000) { // Looks like we've been hibernating - lastSuspendTime = now; - } + function isHibernationDetected() { + if (sbx.runtimeEnvironment === 'client') { + if (delta > 15 * 1000) { // Looks like we've been hibernating + lastSuspendTime = now; + } - var timeSinceLastSuspended = now.getTime() - lastSuspendTime.getTime(); + var timeSinceLastSuspended = now.getTime() - lastSuspendTime.getTime(); - if (timeSinceLastSuspended < (10 * 1000)) { + return timeSinceLastSuspended < (10 * 1000); + } else if (sbx.runtimeEnvironment === 'server') { + return delta > 2 * heartbeatMs; + } else { + console.error('Cannot detect hibernation, because runtimeEnvironment is not detected from sbx.runtimeEnvironment:', sbx.runtimeEnvironment); + return false; + } + } + if (isHibernationDetected()) { console.log('Hibernation detected, suspending timeago alarm'); return 'current'; } diff --git a/lib/sandbox.js b/lib/sandbox.js index 3acb9a19428..4bcee3b40e4 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -44,6 +44,7 @@ function init () { sbx.serverInit = function serverInit (env, ctx) { reset(); + sbx.runtimeEnvironment = 'server'; sbx.time = Date.now(); sbx.settings = env.settings; sbx.data = ctx.ddata.clone(); @@ -83,6 +84,7 @@ function init () { sbx.clientInit = function clientInit (ctx, time, data) { reset(); + sbx.runtimeEnvironment = 'client'; sbx.settings = ctx.settings; sbx.showPlugins = ctx.settings.showPlugins; sbx.time = time; diff --git a/tests/bgnow.test.js b/tests/bgnow.test.js index 819f3dafbfc..c87e513c48d 100644 --- a/tests/bgnow.test.js +++ b/tests/bgnow.test.js @@ -9,6 +9,7 @@ var SIX_MINS = 360000; describe('BG Now', function ( ) { var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.levels = require('../lib/levels'); diff --git a/tests/iob.test.js b/tests/iob.test.js index 30872e4fb4d..1ef86d82aef 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -7,6 +7,7 @@ describe('IOB', function() { var ctx = {}; ctx.language = require('../lib/language')(); ctx.language.set('en'); + ctx.settings = require('../lib/settings')(); var iob = require('../lib/plugins/iob')(ctx); diff --git a/tests/loop.test.js b/tests/loop.test.js index 9c65ff9bdd1..b62b39c15eb 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index ed3dd6d3b9f..ce221c8f396 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); diff --git a/tests/pump.test.js b/tests/pump.test.js index c6def822058..94e2de18974 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); diff --git a/tests/timeago.test.js b/tests/timeago.test.js index 7b4a718ccd0..c0c88b8a1b1 100644 --- a/tests/timeago.test.js +++ b/tests/timeago.test.js @@ -7,6 +7,8 @@ describe('timeago', function() { ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); ctx.language = require('../lib/language')(); + ctx.settings = require('../lib/settings')(); + ctx.settings.heartbeat = 0.5; // short heartbeat to speedup tests var timeago = require('../lib/plugins/timeago')(ctx); @@ -41,6 +43,34 @@ describe('timeago', function() { done(); }); + it('should suspend alarms due to hibernation when 2 heartbeats are skipped on server', function() { + ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; + + var sbx = freshSBX() + var status = timeago.checkStatus(sbx); + // By default (no hibernation detected) a warning should be given + // we force no hibernation by checking status twice + status = timeago.checkStatus(sbx); + should.equal(status, 'warn'); + + // 10ms more than suspend-threshold to prevent flapping tests + var timeoutMs = 2 * ctx.settings.heartbeat * 1000 + 100; + return new Promise(function(resolve, reject) { + setTimeout(function() { + status = timeago.checkStatus(sbx); + // Because hibernation should now be detected, no warning should be given + should.equal(status, 'current'); + + // We immediately ask status again, so hibernation should not be detected anymore, + // and we should receive a warning again + status = timeago.checkStatus(sbx); + should.equal(status, 'warn'); + + resolve() + }, timeoutMs) + }) + }); + it('should trigger a warning when data older than 15m', function(done) { ctx.notifications.initRequests(); ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; From 1b8dc27c195d0db10fa03603317ee8a41b49a5e7 Mon Sep 17 00:00:00 2001 From: John Weston Date: Sun, 10 Nov 2019 13:48:56 -0800 Subject: [PATCH 094/134] Adding to #5121 (#5126) * changed gitter shield to discord * changed discord shield text to discord chat instead of just chat * changed gitter shield to discord * need to fix my md error * Update CONTRIBUTING.md Few more tweaks * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update README.md Change Discord URL to be an invite link instead of a direct link to the channel (which doesn't work if you're not already in it). --- CONTRIBUTING.md | 35 ++++++++++++----------------------- README.md | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c77a0df1c6a..d9aa862457d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,9 +25,7 @@ [![Build Status][build-img]][build-url] [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] -[![Gitter chat][gitter-img]][gitter-url] -[![Stories in Ready][ready-img]][waffle] -[![Stories in Progress][progress-img]][waffle] +[![Discord chat][discord-img]][discord-url] [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor @@ -35,11 +33,8 @@ [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor [coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg [coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master -[gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg -[gitter-url]: https://gitter.im/nightscout/public -[ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready -[waffle]: https://waffle.io/nightscout/cgm-remote-monitor -[progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress +[discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat +[discord-url]: https://discordapp.com/channels/629952586895851530/629952669967974410 ## Installation for development @@ -67,13 +62,9 @@ If you want to additionaly test the site in production mode, create a file calle ## REST API -Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation -for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml -files locally). +Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml files locally). -Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and -be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL -encoded. +Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL encoded. ## Design & new features @@ -104,7 +95,7 @@ If in doubt, format your code with `js-beautify --indent-size 2 --comma-first - ## Create a prototype -Fork cgm-remote-monitor and create a branch. You can create a branch using `git checkout -b wip/add-my-widget`. This creates a new branch called `wip/add-my-widget`. The `wip` stands for work in progress and is a common prefix so that when know what to expect when reviewing many branches. +Fork cgm-remote-monitor and create a branch. You can create a branch using `git checkout -b wip/add-my-widget`. This creates a new branch called `wip/add-my-widget`. The "`wip`" stands for work-in-progress and is a common prefix so that we know what to expect when reviewing many branches. ## Submit a pull request @@ -114,11 +105,9 @@ This can be done by checking your code `git commit -avm 'my improvements are her Now that the commits are available on github, you can click on the compare buttons on your fork to create a pull request. Make sure to select [Nightscout's `dev` branch](https://github.com/nightscout/cgm-remote-monitor/tree/dev). -We assume all new Pull Requests are at least smoke tested by the author and all code in the PR actually works. -Please include a description of what the features do and rationalize why the changes are needed. +We assume all new Pull Requests are at least smoke tested by the author and all code in the PR actually works. Please include a description of what the features do and rationalize why the changes are needed. -If you add any new NPM module dependencies, you have to rationalize why they are needed - we prefer pull requests that reduce dependencies, not add them. -Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. +If you add any new NPM module dependencies, you have to rationalize why they are needed - we prefer pull requests that reduce dependencies, not add them. Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. When adding new features that add configuration options, please ensure the `README` document is amended with information on the new configuration. @@ -141,7 +130,7 @@ We encourage liberal use of the comments, including images where appropriate. ## Co-ordination -Most cgm-remote-monitor hackers use github's ticketing system, along with Facebook cgm-in-the-cloud, and gitter. +We primarily use GitHub's ticketing system for discussing PRs and bugs, and [Discord][discord-url] for general development chatter. We use git-flow, with `master` as our production, stable branch, and `dev` is used to queue up for upcoming releases. Everything else is done on branches, hopefully with names that indicate what to expect. @@ -151,7 +140,7 @@ Every commit is tested by travis. We encourage adding tests to validate your de ## Other Dev Tips -* Join the [Gitter chat][gitter-url] +* Join the [Discord chat][discord-url]. * Get a local dev environment setup if you haven't already. * Try breaking up big features/improvements into small parts. It's much easier to accept small PR's. * Create tests for your new code as well as the old code. We are aiming for a full test coverage. @@ -202,7 +191,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | Release coordination 0.11.x: | [@PieterGit] | | Issue/Pull request coordination: | Please volunteer | | Cleaning up git fork spam: | Please volunteer | -| Documentation writers: | [@andrew-warrington][@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | +| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | ### Plugin contributors @@ -278,7 +267,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver ### List of all contributors | Contribution area | List of contributors | | ------------------------------------- | -------------------- | -| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo] +| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo] [@unsoluble] | All active testers/documentors: | [@danamlewis] [@jamieowendexcom] [@mcdafydd] [@oteroos] [@rarneson] [@tynbendad] [@unsoluble] | All active translators: | [@apanasef] [@jizhongwen] [@viderehh] [@herzogmedia] [@LuminaryXion] [@OpossumGit] diff --git a/README.md b/README.md index f81f9265a04..443b194b7d6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Community maintained fork of the [codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b [codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor [discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat -[discord-url]: https://discordapp.com/channels/629952586895851530/629952669967974410 +[discord-url]: https://discord.gg/rTKhrqz [heroku-img]: https://www.herokucdn.com/deploy/button.png [heroku-url]: https://heroku.com/deploy [update-img]: update.png From d3bd77b51485b1fdc87e025bbfe3661562845c80 Mon Sep 17 00:00:00 2001 From: ireneusz-ptak <31506973+ireneusz-ptak@users.noreply.github.com> Date: Sun, 10 Nov 2019 22:51:19 +0100 Subject: [PATCH 095/134] Extra info (delta and last measurement time) on clock view. (#5151) * Parametrized clock view * Parametrized clock view --- README.md | 2 +- lib/client/clock-client.js | 52 +++++++++++++++++++++++++++---- lib/settings.js | 4 +++ views/clockviews/clock-color.css | 14 +++++++++ views/clockviews/clock-shared.css | 4 +++ views/clockviews/shared.html | 5 ++- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 443b194b7d6..8721cfede6f 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or There are a few alternate web views available from the main menu that display a simplified BG stream. (If you launch one of these in a fullscreen view in iOS, you can use a left-to-right swipe gesture to exit the view.) * `Clock` - Shows current BG, trend arrow, and time of day. Grey text on a black background. - * `Color` - Shows current BG and trend arrow. White text on a background that changes color to indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + * `Color` - Shows current BG and trend arrow. White text on a background that changes color to indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). Set `SHOW_CLOCK_DELTA` to `true` to show BG change in the last 5 minutes, set `SHOW_CLOCK_LAST_TIME` to `true` to always show BG age. * `Simple` - Shows current BG. Grey text on a black background. ### Plugins diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index 5ff3a787e28..62c204a696d 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -37,11 +37,14 @@ client.render = function render (xhr) { console.log('got data', xhr); let rec; + let delta; - xhr.some(element => { - if (element.sgv) { + xhr.forEach(element => { + if (element.sgv && !rec && !delta) { rec = element; - return true; + } + else if (element.sgv && rec && !delta) { + delta = (rec.sgv - element.sgv)/((rec.date - element.date)/(5*60*1000)); } }); @@ -67,8 +70,14 @@ client.render = function render (xhr) { // Convert BG to mmol/L if necessary. if (window.serverSettings.settings.units === 'mmol') { var displayValue = window.Nightscout.units.mgdlToMMOL(rec.sgv); + var deltaDisplayValue = window.Nightscout.units.mgdlToMMOL(delta); } else { displayValue = rec.sgv; + deltaDisplayValue = Math.round(delta); + } + + if (deltaDisplayValue > 0) { + deltaDisplayValue = '+' + deltaDisplayValue; } // Insert the BG value text. @@ -118,7 +127,30 @@ client.render = function render (xhr) { var elapsedMins = Math.round(((now - last) / 1000) / 60); // Insert the BG stale time text. - $('#staleTime').text(elapsedMins + ' minutes ago'); + let staleTimeText; + if (elapsedMins == 0) { + staleTimeText = 'Just now'; + } + else if (elapsedMins == 1) { + staleTimeText = '1 minute ago'; + } + else { + staleTimeText = elapsedMins + ' minutes ago'; + } + $('#staleTime').text(staleTimeText); + + // Force NS to always show 'x minutes ago' + if (window.serverSettings.settings.showClockLastTime) { + $('#staleTime').css('display', 'block'); + } + + // Insert the delta value text. + $('#delta').html(deltaDisplayValue); + + // Show delta + if (window.serverSettings.settings.showClockDelta) { + $('#delta').css('display', 'inline-block'); + } // Threshold background coloring. if (bgNum < bgLow) { @@ -141,12 +173,20 @@ client.render = function render (xhr) { if (now - last > threshold) { $('body').css('background-color', 'grey'); $('body').css('color', 'black'); - $('#staleTime').css('display', 'block'); $('#arrow').css('filter', 'brightness(0%)'); + + if (!window.serverSettings.settings.showClockLastTime) { + $('#staleTime').css('display', 'block'); + } + } else { - $('#staleTime').css('display', 'none'); $('body').css('color', 'white'); $('#arrow').css('filter', 'brightness(100%)'); + + if (!window.serverSettings.settings.showClockLastTime) { + $('#staleTime').css('display', 'none'); + } + } } }; diff --git a/lib/settings.js b/lib/settings.js index cd2aa3dc928..5024157e70d 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -48,6 +48,8 @@ function init () { , secureHstsHeaderPreload: false , secureCsp: false , deNormalizeDates: false + , showClockDelta: false + , showClockLastTime: false }; var valueMappers = { @@ -69,6 +71,8 @@ function init () { , secureHstsHeader: mapTruthy , secureCsp: mapTruthy , deNormalizeDates: mapTruthy + , showClockDelta: mapTruthy + , showClockLastTime: mapTruthy }; function mapNumberArray (value) { diff --git a/views/clockviews/clock-color.css b/views/clockviews/clock-color.css index 5a8c41d2451..6a6796ef823 100644 --- a/views/clockviews/clock-color.css +++ b/views/clockviews/clock-color.css @@ -8,6 +8,20 @@ body { flex-direction: column; } +#bgnow { + display: inline-block; + vertical-align: middle; +} + +#delta { + font-size: 16vmin; + vertical-align: middle; +} + +#innerTrend { + word-spacing: 2em; +} + #arrowDiv { flex-grow: 1; text-align: center; diff --git a/views/clockviews/clock-shared.css b/views/clockviews/clock-shared.css index 9919a1a11c1..83328fe4114 100644 --- a/views/clockviews/clock-shared.css +++ b/views/clockviews/clock-shared.css @@ -52,6 +52,10 @@ main { display: none; } +#delta { + display: none; +} + .close { color: white; font: 4em 'Open Sans'; diff --git a/views/clockviews/shared.html b/views/clockviews/shared.html index 1be330dee66..cb27b137dfb 100644 --- a/views/clockviews/shared.html +++ b/views/clockviews/shared.html @@ -30,7 +30,10 @@
-
+
+ + +
arrow
From 9877a5133edab6706a9b39c3e3edc0ea508e4a65 Mon Sep 17 00:00:00 2001 From: John Weston Date: Sun, 10 Nov 2019 13:53:46 -0800 Subject: [PATCH 096/134] Wording tweaks to go with #4810 (#4866) * mmol/L wording tweak in Readme * Various wording & clarity tweaks in the Readme * Heroku template update to reflect mmol allowance tweak * Update README.md * Update minimum device requirements Ran some proper browser test suites, determined actual minimum specs for the main site. --- README.md | 57 ++++++++++++++++++++++++------------------------------- app.json | 2 +- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8721cfede6f..b84532d825d 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,15 @@ If you plan to use Nightscout, we recommend using [Heroku](http://www.nightscout Older versions of the browsers might work, but are untested. - Android 4 -- Chrome 68 +- iOS 6 +- Chrome 35 - Edge 17 - Firefox 61 +- Opera 12.1 +- Safari 6 (macOS 10.7) - Internet Explorer: not supported -- iOS 11 -- Opera 54 -- Safari 10 (macOS 10.12) + +Some features may not work with devices/browsers on the older end of these requirements. ## Windows installation software requirements: @@ -167,7 +169,7 @@ SCM_COMMAND_IDLE_TIMEOUT=300 # Development -Wanna help with development, or just see how Nigthscout works? Great! See [CONTRIBUTING.md](CONTRIBUTING.md) for development related documentation. +Want to help with development, or just see how Nightscout works? Great! See [CONTRIBUTING.md](CONTRIBUTING.md) for development-related documentation. # Usage @@ -179,15 +181,9 @@ MongoDB server such as [mLab][mLab]. [mongostring]: https://nightscout.github.io/pages/mongostring/ ## Updating my version? -The easiest way to update your version of cgm-remote-monitor to our latest -recommended version is to use the [update my fork tool][update-fork]. It even -gives out stars if you are up to date. - -## What is my mongo string? -Try the [what is my mongo string tool][mongostring] to get a good idea of your -mongo string. You can copy and paste the text in the gray box into your -`MONGO_CONNECTION` environment variable. +The easiest way to update your version of cgm-remote-monitor to the latest version is to use the [update tool][update-fork]. A step-by-step guide is available [here][http://www.nightscout.info/wiki/welcome/how-to-update-to-latest-cgm-remote-monitor-aka-cookie]. +To downgrade to an older version, follow [this guide][http://www.nightscout.info/wiki/welcome/how-to-deploy-an-older-version-of-nightscout]. ## Configure my uploader to match @@ -195,7 +191,7 @@ Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. ## Nightscout API -The Nightscout API enables direct access to your DData without the need for direct Mongo access. +The Nightscout API enables direct access to your data without the need for Mongo access. You can find CGM data in `/api/v1/entries`, Care Portal Treatments in `/api/v1/treatments`, and Treatment Profiles in `/api/v1/profile`. The server status and settings are available from `/api/v1/status.json`. @@ -206,7 +202,7 @@ Once you've installed Nightscout, you can access API documentation by loading `/ #### Example Queries -(replace `http://localhost:1337` with your base url, YOUR-SITE) +(replace `http://localhost:1337` with your own URL) * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` * Count of 100's in a month: `http://localhost:1337/api/v1/count/entries/where?find[dateString][$gte]=2016-09&find[dateString][$lte]=2016-10&find[sgv]=100` @@ -223,15 +219,16 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or ### Required - * `MONGO_CONNECTION` - Your mongo uri, for example: `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout` - * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. - * `BASE_URL` - Used for building links to your sites api, ie pushover callbacks, usually the URL of your Nightscout site you may want https instead of http + * `MONGODB_URI` - The connection string for your Mongo database. Something like `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout`. + * `API_SECRET` - A secret passphrase that must be at least 12 characters long. + * `MONGODB_COLLECTION` (`entries`) - The Mongo collection where CGM entries are stored. + * `DISPLAY_UNITS` (`mg/dl`) - Options are `mg/dl` or `mmol/L` (or just `mmol`). Setting to `mmol/L` puts the entire server into `mmol/L` mode by default, no further settings needed. -### Features/Labs +### Features * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below - * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal + * `BASE_URL` - Used for building links to your site's API, i.e. Pushover callbacks, usually the URL of your Nightscout site. * `AUTH_DEFAULT_ROLES` (`readable`) - possible values `readable`, `denied`, or any valid role name. When `readable`, anyone can view Nightscout without a token. Setting it to `denied` will require a token from every visit, using `status-only` will enable api-secret based login. @@ -240,7 +237,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or ### Alarms - These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) + These alarm setting affect all delivery methods (browser, Pushover, IFTTT, etc.). Values and settings entered here will be the defaults for new browser views, but will be overridden if different choices are made in the settings UI. * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent @@ -258,10 +255,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `ALARM_URGENT_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover * `ALARM_WARN_MINS` (`30 60 90 120`) - Number of minutes to snooze warning alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover - ### Core - * `MONGO_COLLECTION` (`entries`) - The collection used to store SGV, MBG, and CAL records from your CGM device * `MONGO_TREATMENTS_COLLECTION` (`treatments`) -The collection used to store treatments entered in the Care Portal, see the `ENABLE` env var above * `MONGO_DEVICESTATUS_COLLECTION`(`devicestatus`) - The collection used to store device status information such as uploader battery * `MONGO_PROFILE_COLLECTION`(`profile`) - The collection used to store your profiles @@ -276,13 +271,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `DEBUG_MINIFY` (`true`) - Debug option, setting to `false` will disable bundle minification to help tracking down error and speed up development * `DE_NORMALIZE_DATES`(`true`) - The Nightscout REST API normalizes all entered dates to UTC zone. Some Nightscout clients have broken date deserialization logic and expect to received back dates in zoned formats. Setting this variable to `true` causes the REST API to serialize dates sent to Nightscout in zoned format back to zoned format when served to clients over REST. - ### Predefined values for your browser settings (optional) + * `TIME_FORMAT` (`12`)- possible values `12` or `24` * `NIGHT_MODE` (`off`) - possible values `on` or `off` * `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise` - * `CUSTOM_TITLE` (`Nightscout`) - Usually name of T1 - * `THEME` (`default`) - possible values `default`, `colors`, or `colorblindfriendly` + * `CUSTOM_TITLE` (`Nightscout`) - Title for the main view + * `THEME` (`colors`) - possible values `default`, `colors`, or `colorblindfriendly` * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` @@ -293,9 +288,9 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), tr (Turkish), zh_cn (中文(简体)), zh_tw (中文(繁體)) * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. - * The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted. + * The `linear` option has equidistant tick marks; the range used is dynamic so that space at the top of chart isn't wasted. * The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale. - * `EDIT_MODE` (`on`) - possible values `on` or `off`. Enable or disable icon allowing enter treatments edit mode + * `EDIT_MODE` (`on`) - possible values `on` or `off`. Enables the icon allowing for editing of treatments in the main view. ### Predefined values for your server settings (optional) * `INSECURE_USE_HTTP` (`false`) - Redirect unsafe http traffic to https. Possible values `false`, or `true`. Your site redirects to `https` by default. If you don't want that from Nightscout, but want to implement that with a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`. Note: This will allow (unsafe) http traffic to your Nightscout instance and is not recommended. @@ -320,7 +315,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or #### Default Plugins - These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` + These can be disabled by adding them to the `DISABLE` variable, for example `DISABLE="direction upbat"` ##### `delta` (BG Delta) Calculates and displays the change between the last 2 BG values. @@ -342,7 +337,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm - ##### `devicestatus` (Device Status) Used by `upbat` and other plugins to display device status info. Supports the `DEVICESTATUS_ADVANCED="true"` [extended setting](#extended-settings) to send all device statuses to the client for retrospective use and to support other plugins. @@ -440,7 +434,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `BRIDGE_USER_NAME` - Your user name for the Share service. * `BRIDGE_PASSWORD` - Your password for the Share service. * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time to wait between each update. - * `BRIDGE_MAX_COUNT` (`1`) - The maximum number of records to fetch per update. + * `BRIDGE_MAX_COUNT` (`1`) - The number of records to attempt to fetch per update. * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). @@ -487,7 +481,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `OPENAPS_PRED_UAM_COLOR` (`#c9bd60`) - The color to use for UAM prediction lines. Same format as above. * `OPENAPS_COLOR_PREDICTION_LINES` (`true`) - Enables / disables the colored lines vs the classic purple color. - Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). ##### `loop` (Loop) diff --git a/app.json b/app.json index 8fea5103f88..65d07d318a1 100644 --- a/app.json +++ b/app.json @@ -93,7 +93,7 @@ "required": false }, "DISPLAY_UNITS": { - "description": "Preferred BG units for the site:'mg/dl' or 'mmol'. (Note that it is *not* 'mmol/L')", + "description": "Preferred BG units for the site: 'mg/dl' or 'mmol/L' (or just 'mmol').", "value": "mg/dl", "required": true }, From 5bc6799e48596bb89fdad86d1f98d62c182f2d2d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Sun, 10 Nov 2019 21:57:49 +0000 Subject: [PATCH 097/134] GitHub actions build (#5168) * Run tests using GitHub Actions * Node 10 * Test CI fails * Use npm to run tests * Fix package json to run tests * Enable v3 TEST api for both development env and CI * Run CI on both Node 10 and 12 * Allow downgrading Mongo --- .github/workflows/main.yml | 32 ++++++++++++++++++++++++++++++++ .gitignore | 2 +- ci.test.env | 7 +++++++ lib/api3/index.js | 3 ++- package.json | 3 ++- tests/fail.test.js | 14 ++++++++++++++ 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 ci.test.env create mode 100644 tests/fail.test.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000000..23cc61e5ef7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,32 @@ +name: CI test + +on: [push] + +jobs: + build: + + runs-on: ubuntu-16.04 + + strategy: + matrix: + node-version: [10.x, 12.x] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Install MongoDB + run: | + wget -qO - https://www.mongodb.org/static/pgp/server-3.6.asc | sudo apt-key add - + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list + sudo apt-get update + sudo apt-get install -y mongodb-org + sudo apt-get install -y --allow-downgrades mongodb-org=3.6.14 mongodb-org-server=3.6.14 mongodb-org-shell=3.6.14 mongodb-org-mongos=3.6.14 mongodb-org-tools=3.6.14 + - name: Start MongoDB + run: sudo systemctl start mongod + - name: Run tests + run: npm run-script test-ci diff --git a/.gitignore b/.gitignore index 5f82d7929f7..1cf7ab06f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ bundle/bundle.out.js .idea/ *.iml my.env +my.*.env -*.env static/bower_components/ .*.sw? .DS_Store diff --git a/ci.test.env b/ci.test.env new file mode 100644 index 00000000000..c57e5eeb0c4 --- /dev/null +++ b/ci.test.env @@ -0,0 +1,7 @@ +CUSTOMCONNSTR_mongo=mongodb://127.0.0.1:27017/testdb +API_SECRET=abcdefghij123 +HOSTNAME=localhost +INSECURE_USE_HTTP=true +PORT=1337 +NODE_ENV=production +CI=true \ No newline at end of file diff --git a/lib/api3/index.js b/lib/api3/index.js index 70fb6a4d59e..d188a5ee537 100644 --- a/lib/api3/index.js +++ b/lib/api3/index.js @@ -59,6 +59,7 @@ function configure (env, ctx) { app.set('version', env.version); app.set('apiVersion', apiConst.API3_VERSION); app.set('units', env.DISPLAY_UNITS); + app.set('ci', process.env['CI'] ? true: false); app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']); self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE); @@ -73,7 +74,7 @@ function configure (env, ctx) { app.get('/version', require('./specific/version')(app, ctx, env)); - if (app.get('env') === 'development') { // for development and testing purposes only + if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only app.get('/test', async function test (req, res) { try { diff --git a/package.json b/package.json index 346e67c6251..25a96043401 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "scripts": { "start": "node server.js", - "test": "env-cmd ./test.env mocha --exit tests/*.test.js", + "test": "env-cmd ./my.test.env mocha --exit tests/*.test.js", + "test-ci": "env-cmd ./ci.test.env mocha --exit tests/*.test.js", "env": "env", "postinstall": "webpack --mode production --config webpack.config.js && npm run-script update-buster", "bundle": "webpack --mode production --config webpack.config.js && npm run-script update-buster", diff --git a/tests/fail.test.js b/tests/fail.test.js new file mode 100644 index 00000000000..eefda445b3d --- /dev/null +++ b/tests/fail.test.js @@ -0,0 +1,14 @@ +'use strict'; + +require('should'); + +// This test is included just so we have an easy to template to intentionally cause +// builds to fail + +describe('fail', function ( ) { + + it('should not fail', function () { + true.should.equal(true); + }); + +}); From d55b2d0ed0b06622e9d24657fa091d14d2fc711d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Mon, 11 Nov 2019 00:24:23 +0200 Subject: [PATCH 098/134] Move the units configuration detection to a place where it's guaranteed to catch all invocations --- env.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/env.js b/env.js index e8aa36dc89f..352aab4b8c6 100644 --- a/env.js +++ b/env.js @@ -22,14 +22,6 @@ function config ( ) { */ env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); - // be lenient at accepting the mmol input - if (env.DISPLAY_UNITS.toLowerCase().includes('mmol')) { - env.DISPLAY_UNITS = 'mmol'; - } else { - // also ensure the mg/dl is set with expected case - env.DISPLAY_UNITS = 'mg/dl'; - } - console.log('Units set to', env.DISPLAY_UNITS ); env.PORT = readENV('PORT', 1337); @@ -145,8 +137,6 @@ function updateSettings() { env.settings.authDefaultRoles = env.settings.authDefaultRoles || ""; env.settings.authDefaultRoles += ' careportal'; } - - } function readENV(varName, defaultValue) { @@ -156,6 +146,13 @@ function readENV(varName, defaultValue) { || process.env[varName] || process.env[varName.toLowerCase()]; + if (varName == 'DISPLAY_UNITS') { + if (value.toLowerCase().includes('mmol')) { + value = 'mmol'; + } else { + value = 'mg/dl'; + } + } return value != null ? value : defaultValue; } From 2e3681311c90cf1d1a5163ce011d2b76fbedbfe5 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Mon, 11 Nov 2019 00:30:49 +0200 Subject: [PATCH 099/134] Fix the check for environments without the units setting --- env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.js b/env.js index 352aab4b8c6..0d8d41409b0 100644 --- a/env.js +++ b/env.js @@ -146,7 +146,7 @@ function readENV(varName, defaultValue) { || process.env[varName] || process.env[varName.toLowerCase()]; - if (varName == 'DISPLAY_UNITS') { + if (varName == 'DISPLAY_UNITS' && value) { if (value.toLowerCase().includes('mmol')) { value = 'mmol'; } else { From f7750b3534eba39860383ddf93518742cf7b22b7 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Sun, 10 Nov 2019 16:32:46 -0600 Subject: [PATCH 100/134] Rename xdrip-js to xdripjs (#4959) * rename xdrip-js to xdripjs * update readme for xdrip-js name change * update from CGM to XDRIPJS label * change label from all caps to mixed case for xDripJS * change pill label back to CGM --- README.md | 10 +++++----- lib/plugins/index.js | 4 ++-- lib/plugins/{xdrip-js.js => xdripjs.js} | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) rename lib/plugins/{xdrip-js.js => xdripjs.js} (95%) diff --git a/README.md b/README.md index b84532d825d..b283e415e72 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Community maintained fork of the - [`openaps` (OpenAPS)](#openaps-openaps) - [`loop` (Loop)](#loop-loop) - [`override` (Override Mode)](#override-override-mode) - - [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js) + - [`xdripjs` (xDrip-js)](#xdripjs-xdripjs) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) @@ -501,12 +501,12 @@ For remote overrides, the following extended settings must be configured: Additional monitoring for DIY automated insulin delivery systems to display real-time overrides such as Eating Soon or Exercise Mode: * Requires `DEVICESTATUS_ADVANCED="true"` to be set -##### `xdrip-js` (xDrip-js) +##### `xdripjs` (xDrip-js) Integrated xDrip-js monitoring, uses these extended settings: * Requires `DEVICESTATUS_ADVANCED="true"` to be set - * `XDRIP-JS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when CGM state is not OK or battery voltages fall below threshold. - * `XDRIP-JS_STATE_NOTIFY_INTRVL` (`0.5`) - Set to number of hours between CGM state notifications - * `XDRIP-JS_WARN_BAT_V` (`300`) - The voltage of either transmitter battery, a warning will be triggered when dropping below this threshold. + * `XDRIPJS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when CGM state is not OK or battery voltages fall below threshold. + * `XDRIPJS_STATE_NOTIFY_INTRVL` (`0.5`) - Set to number of hours between CGM state notifications + * `XDRIPJS_WARN_BAT_V` (`300`) - The voltage of either transmitter battery, a warning will be triggered when dropping below this threshold. ##### `alexa` (Amazon Alexa) Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 5970c16836c..2568b634afd 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -37,7 +37,7 @@ function init (ctx) { , require('./careportal')(ctx) , require('./pump')(ctx) , require('./openaps')(ctx) - , require('./xdrip-js')(ctx) + , require('./xdripjs')(ctx) , require('./loop')(ctx) , require('./override')(ctx) , require('./boluswizardpreview')(ctx) @@ -63,7 +63,7 @@ function init (ctx) { , require('./cob')(ctx) , require('./pump')(ctx) , require('./openaps')(ctx) - , require('./xdrip-js')(ctx) + , require('./xdripjs')(ctx) , require('./loop')(ctx) , require('./boluswizardpreview')(ctx) , require('./cannulaage')(ctx) diff --git a/lib/plugins/xdrip-js.js b/lib/plugins/xdripjs.js similarity index 95% rename from lib/plugins/xdrip-js.js rename to lib/plugins/xdripjs.js index d66f112cfcf..a36a42de2e0 100644 --- a/lib/plugins/xdrip-js.js +++ b/lib/plugins/xdripjs.js @@ -11,7 +11,7 @@ function init(ctx) { var lastStateNotification = null; var sensorState = { - name: 'xdrip-js' + name: 'xdripjs' , label: 'CGM Status' , pluginType: 'pill-status' }; @@ -25,7 +25,7 @@ function init(ctx) { if (firstPrefs) { firstPrefs = false; - console.info('xdrip-js Prefs:', prefs); + console.info('xdripjs Prefs:', prefs); } return prefs; @@ -154,8 +154,8 @@ function init(ctx) { }; } - message = 'CGM state: ' + sensorInfo.xdripjs.stateString; - title = 'CGM state: ' + sensorInfo.xdripjs.stateString; + message = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; + title = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; if (sensorInfo.xdripjs.state == 0x7) { // If it is a calibration request, only use INFO @@ -167,15 +167,15 @@ function init(ctx) { if (sensorInfo.xdripjs.voltagea && (sensorInfo.xdripjs.voltagea < prefs.warnBatV)) { sendNotification = true; - message = 'CGM Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; - title = 'CGM Battery Low'; + message = 'CGM Transmitter Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; + title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } if (sensorInfo.xdripjs.voltageb && (sensorInfo.xdripjs.voltageb < (prefs.warnBatV - 10))) { sendNotification = true; - message = 'CGM Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; - title = 'CGM Battery Low'; + message = 'CGM Transmitter Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; + title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } From 013300a17f1b32b6166f9e74396f18a7ae4e2bdd Mon Sep 17 00:00:00 2001 From: inventor96 Date: Mon, 11 Nov 2019 00:50:09 -0700 Subject: [PATCH 101/134] Added customization step for Alexa skill updates --- docs/plugins/alexa-plugin.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 22d0701bf25..63f89d8e871 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -109,6 +109,7 @@ As more work is done on Nightscout, new ways to interact with Nighscout via Alex 1. Copy the contents of the updated template and paste it in the text box in the JSON Editor page. 1. Click the "Save Model" button near the top of the page, and then click the "Build Model" button. 1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you gave your skill name something other than "night scout," you will need to go to the "Invocation" page in the left navigation pane and change the Skill Invocation Name back to your preferred name. Make sure to click the "Save Model" button followed by the "Build Model" button after you change the name. 1. Enjoy the new features! ## Adding support for additional languages From 0c6929fb1c4b4c6609a7c3c3867a2085ad61fb0d Mon Sep 17 00:00:00 2001 From: Lukas Herzog Date: Mon, 11 Nov 2019 10:27:10 +0100 Subject: [PATCH 102/134] Language Update: added missing German Translations (#5172) * language Update devicestatus collection * language Update Admin-Tools * changed wording for admin authorization * typo corrected (admin auth) * language Updates for Admin Tools database cleanups --- lib/language.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/language.js b/lib/language.js index 1ad709af080..6800ff50517 100644 --- a/lib/language.js +++ b/lib/language.js @@ -7698,6 +7698,7 @@ function init() { } ,'%1 records deleted' : { hr: 'obrisano %1 zapisa' + ,de: '%1 Einträge gelöscht' } ,'Clean Mongo status database' : { cs: 'Vyčištění Mongo databáze statusů' @@ -7869,52 +7870,67 @@ function init() { ,'Delete all documents from devicestatus collection older than 30 days' : { hr: 'Obriši sve statuse starije od 30 dana' ,ru: 'Удалить все записи коллекции devicestatus' + ,de: 'Alle Dokumente der Gerätestatus-Sammlung löschen, die älter als 30 Tage sind' } ,'Number of Days to Keep:' : { hr: 'Broj dana za sačuvati:' ,ru: 'Оставить дней' + ,de: 'Daten löschen, die älter sind (in Tagen) als:' } ,'This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo uklanja sve statuse starije od 30 dana. Korisno kada se status baterije uploadera ne osvježava ispravno.' ,ru: 'Это удалит все документы коллекции devicestatus которым более 30 дней. Полезно, когда статус батареи не обновляется или обновляется неверно.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Gerätestatus-Sammlung, die älter sind als 30 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' } ,'Delete old documents from devicestatus collection?' : { hr: 'Obriši stare statuse' + ,de: 'Alte Dokumente aus der Gerätestatus-Sammlung entfernen?' } ,'Clean Mongo entries (glucose entries) database' : { hr: 'Obriši GUK zapise iz baze' + ,de: 'Mongo-Einträge (Glukose-Einträge) Datenbank bereinigen' } ,'Delete all documents from entries collection older than 180 days' : { hr: 'Obriši sve zapise starije od 180 dana' + ,de: 'Alle Dokumente aus der Einträge-Sammlung löschen, die älter sind als 180 Tage' } ,'This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve zapise starije od 180 dana. Korisno kada se status baterije uploadera ne osvježava.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Einträge-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' } ,'Delete old documents' : { hr: 'Obriši stare zapise' + ,de: 'Alte Dokumente löschen' } ,'Delete old documents from entries collection?' : { hr: 'Obriši stare zapise?' + ,de: 'Alte Dokumente aus der Einträge-Sammlung entfernen?' } ,'%1 is not a valid number' : { hr: '%1 nije valjan broj' + ,de: '%1 ist keine gültige Zahl' } ,'%1 is not a valid number - must be more than 2' : { hr: '%1 nije valjan broj - mora biti veći od 2' + ,de: '%1 ist keine gültige Zahl - Eingabe muss größer als 2 sein' } ,'Clean Mongo treatments database' : { hr: 'Obriši tretmane iz baze' + ,de: 'Mongo-Behandlungsdatenbank bereinigen' } ,'Delete all documents from treatments collection older than 180 days' : { hr: 'Obriši tretmane starije od 180 dana iz baze' + ,de: 'Alle Dokumente aus der Behandlungs-Sammlung löschen, die älter sind als 180 Tage' } ,'This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve tretmane starije od 180 dana iz baze. Korisno kada se status baterije uploadera ne osvježava.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Behandlungs-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' } ,'Delete old documents from treatments collection?' : { hr: 'Obriši stare tretmane?' ,ru: 'Удалить старые документы из коллекции лечения?' + ,de: 'Alte Dokumente aus der Behandlungs-Sammlung entfernen?' } ,'Admin Tools' : { cs: 'Nástroje pro správu' @@ -10412,7 +10428,7 @@ function init() { ,sv: 'Administratorgodkänt' ,nb: 'Administratorgodkjent' ,fi: 'Ylläpitäjä valtuutettu' - ,de: 'Admin Authorisierung' + ,de: 'als Administrator autorisiert' ,dk: 'Administrator godkendt' ,pt: 'Administrador autorizado' ,sk: 'Admin autorizovaný' From 534c4435a4dbee1331085dddd8584dbf4b3c5c8e Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Date: Tue, 12 Nov 2019 00:42:30 -0600 Subject: [PATCH 103/134] Don't use dynamic scale unless entries in database (#5195) * don't use dynamic scale unless have entries * fix report crash when no entries * set forecastTime correctly when no entries present --- lib/client/chart.js | 7 +++++-- lib/client/index.js | 2 +- lib/report_plugins/glucosedistribution.js | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index a40aa8ff542..d37f9047e97 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -118,7 +118,7 @@ function init (client, d3, $) { } function dynamicDomainOrElse (defaultDomain) { - if (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic') { + if (client.entries && (client.entries.length > 0) && (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic')) { return dynamicDomain(); } else { return defaultDomain; @@ -714,7 +714,10 @@ function init (client, d3, $) { } maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); - client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; + + var lastSGVMills = client.sbx.lastSGVMills(); + + client.forecastTime = ((maxForecastMills > 0) && lastSGVMills) ? maxForecastMills - lastSGVMills : client.defaultForecastTime; } }; diff --git a/lib/client/index.js b/lib/client/index.js index 8c5d22f5c82..84f0dff70fb 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -131,7 +131,7 @@ client.load = function load (serverSettings, callback) { client.now = Date.now(); client.ddata = require('../data/ddata')(); - client.forecastTime = times.mins(30).msecs; + client.forecastTime = client.defaultForecastTime = times.mins(30).msecs; client.entries = []; client.ticks = require('./ticks'); diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index 4a5e7dd31fb..c2709cd28e5 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -146,6 +146,11 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var glucose_data = [data[0]]; + if (data.length === 0) { + $('#glucosedistribution-days').text(translate('Result is empty')); + return; + } + // data cleaning pass 1 - add interpolated missing points for (i = 0; i <= data.length - 2; i++) { var entry = data[i]; From 4342eaaef0bcd5ba54c05e79d5dbd46ae8d0535d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Wed, 13 Nov 2019 13:41:55 +0200 Subject: [PATCH 104/134] Language Matters: change "Normal" to "In Range" in glucose distribution reports --- lib/report_plugins/glucosedistribution.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index c2709cd28e5..3d8d109610b 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -297,7 +297,10 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s rangeExp = ' (>=' + options.targetHigh + ')'; } - $('' + translate(range) + rangeExp + ': ').appendTo(tr); + var rangeLabel = range; + if (rangeLabel == 'Normal') rangeLabel = 'In Range'; + + $('' + translate(rangeLabel) + rangeExp + ': ').appendTo(tr); $('' + r.readingspct + '%').appendTo(tr); $('' + r.rangeRecords.length + '').appendTo(tr); if (r.rangeRecords.length > 0) { From 78c0778c9dea93aa3d6cb663133acc066e777d6c Mon Sep 17 00:00:00 2001 From: inventor96 Date: Thu, 14 Nov 2019 23:52:17 -0700 Subject: [PATCH 105/134] Added/updated TOC's --- .../add-virtual-assistant-support-to-plugin.md | 10 ++++++++++ docs/plugins/alexa-plugin.md | 4 ++-- docs/plugins/googlehome-plugin.md | 15 +++++++++++++++ .../interacting-with-virtual-assistants.md | 11 +++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md index 4ca0c0542fa..60ac1d1957b 100644 --- a/docs/plugins/add-virtual-assistant-support-to-plugin.md +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -1,3 +1,13 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Adding Virtual Assistant Support to a Plugin](#adding-virtual-assistant-support-to-a-plugin) + - [Intent Handlers](#intent-handlers) + - [Rollup handlers](#rollup-handlers) + + + Adding Virtual Assistant Support to a Plugin ========================================= diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 63f89d8e871..0eefb465718 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -13,9 +13,9 @@ - [Test your skill out with the test tool](#test-your-skill-out-with-the-test-tool) - [What questions can you ask it?](#what-questions-can-you-ask-it) - [Activate the skill on your Echo or other device](#activate-the-skill-on-your-echo-or-other-device) + - [Updating your skill with new features](#updating-your-skill-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) - [Adding Alexa support to a plugin](#adding-alexa-support-to-a-plugin) - - [Intent Handlers](#intent-handlers) - - [Rollup handlers](#rollup-handlers) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 305fa608e14..83acea5055d 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -1,3 +1,18 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout Google Home/DialogFlow Plugin](#nightscout-google-homedialogflow-plugin) + - [Overview](#overview) + - [Activate the Nightscout Google Home Plugin](#activate-the-nightscout-google-home-plugin) + - [Create Your DialogFlow Agent](#create-your-dialogflow-agent) + - [What questions can you ask it?](#what-questions-can-you-ask-it) + - [Updating your agent with new features](#updating-your-agent-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) + - [Adding Google Home support to a plugin](#adding-google-home-support-to-a-plugin) + + + Nightscout Google Home/DialogFlow Plugin ======================================== diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md index a9f2541d8d8..984a876f21c 100644 --- a/docs/plugins/interacting-with-virtual-assistants.md +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -1,3 +1,14 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Interacting with Virtual Assistants](#interacting-with-virtual-assistants) +- [Alexa vs. Google Home](#alexa-vs-google-home) +- [What questions can you ask it?](#what-questions-can-you-ask-it) + - [A note about names](#a-note-about-names) + + + Interacting with Virtual Assistants =================================== From beda457f0337c0218892ecab60ccb719a2e2ad08 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Fri, 15 Nov 2019 17:30:18 -0600 Subject: [PATCH 106/134] set auth dialog width to client width if smaller than default width --- lib/hashauth.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/hashauth.js b/lib/hashauth.js index 0840381a24c..0c95c77ad2e 100644 --- a/lib/hashauth.js +++ b/lib/hashauth.js @@ -78,8 +78,10 @@ hashauth.init = function init(client, $) { hashauth.requestAuthentication = function requestAuthentication (eventOrNext) { var translate = client.translate; hashauth.injectHtml(); + var clientWidth = Math.min(400, $( '#container')[0].clientWidth); + $( '#requestauthenticationdialog' ).dialog({ - width: 400 + width: clientWidth , height: 270 , closeText: '' , buttons: [ From 4e48952d789e1fbb26c3cb73f269e5d647a09157 Mon Sep 17 00:00:00 2001 From: Jake Bloom Date: Sat, 16 Nov 2019 17:37:14 -0800 Subject: [PATCH 107/134] More hebrew translations --- CONTRIBUTING.md | 2 +- lib/language.js | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9aa862457d..650728cd477 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -245,7 +245,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| | Français (`fr`)|Please volunteer|OK| -| עברית (`he`)|Please volunteer|OK| +| עברית (`he`)| [@jakebloom] |OK| | Hrvatski (`hr`)|[@OpossumGit]|OK| | Italiano (`it`)|Please volunteer|OK| | 日本語 (`ja`)|[@LuminaryXion]|Working on this| diff --git a/lib/language.js b/lib/language.js index 6800ff50517..c005bfdd489 100644 --- a/lib/language.js +++ b/lib/language.js @@ -4877,6 +4877,7 @@ function init() { ,es: '2h' ,fr: '2hr' ,el: '2 ώρες' + ,he: 'שעתיים' ,pt: '2h' ,sv: '2tim' ,ro: '2h' @@ -6778,6 +6779,7 @@ function init() { ,tr: 'Ağır' ,zh_cn: '重度' ,zh_tw: '嚴重' + ,he: 'כבד' } ,'Treatment type' : { cs: 'Typ ošetření' @@ -6802,6 +6804,7 @@ function init() { ,ko: 'Treatment 타입' ,tr: 'Tedavi tipi' ,zh_cn: '操作类型' + ,he: 'סוג הטיפול' } ,'Raw BG' : { cs: 'Glykémie z RAW dat' @@ -6849,6 +6852,7 @@ function init() { ,ko: '기기' ,tr: 'Cihaz' ,zh_cn: '设备' + ,he: 'התקן' } ,'Noise' : { cs: 'Šum' @@ -7910,6 +7914,7 @@ function init() { ,'%1 is not a valid number' : { hr: '%1 nije valjan broj' ,de: '%1 ist keine gültige Zahl' + ,he: 'זה לא מיספר %1' } ,'%1 is not a valid number - must be more than 2' : { hr: '%1 nije valjan broj - mora biti veći od 2' @@ -9903,7 +9908,7 @@ function init() { } ,'Eating soon' : { cs: 'Následuje jídlo' - ,he: 'אוכל בקרוב ' + ,he: 'אוכל בקרוב' ,sk: 'Jesť čoskoro' ,fr: 'Repas sous peu' ,sv: 'Snart matdags' @@ -13693,6 +13698,7 @@ function init() { ,hr: 'Masnoće [g]' ,pl: 'Tłuszcz [g]' ,tr: 'Yağ [g]' + ,he: '[g] שמן' }, 'Protein [g]': { cs: 'Proteiny [g]' @@ -13712,6 +13718,7 @@ function init() { ,hr: 'Proteini [g]' ,pl: 'Białko [g]' ,tr: 'Protein [g]' + ,he: '[g] חלבון' }, 'Energy [kJ]': { cs: 'Energie [kJ]' @@ -13731,6 +13738,7 @@ function init() { ,hr: 'Energija [kJ]' ,pl: 'Energia [kJ}' ,tr: 'Enerji [kJ]' + ,he: '[kJ] אנרגיה' }, 'Clock Views:': { cs: 'Hodiny:' @@ -13750,6 +13758,7 @@ function init() { ,hr: 'Satovi:' ,pl: 'Widoki zegarów' ,tr: 'Saat Görünümü' + ,he: 'צגים השעון' }, 'Clock': { cs: 'Hodiny' @@ -13768,6 +13777,7 @@ function init() { ,pl: 'Zegar' ,ru: 'часы' ,tr: 'Saat' + ,he: 'שעון' }, 'Color': { cs: 'Barva' @@ -13786,6 +13796,7 @@ function init() { ,pl: 'Kolor' ,ru: 'цвет' ,tr: 'Renk' + ,he: 'צבע' }, 'Simple': { cs: 'Jednoduchý' @@ -13804,6 +13815,7 @@ function init() { ,pl: 'Prosty' ,ru: 'простой' ,tr: 'Basit' + ,he: 'פשוט' }, 'TDD average': { cs: 'Průměrná denní dávka' @@ -13838,6 +13850,7 @@ function init() { , pl: 'Średnia ilość węglowodanów' , ru: 'среднее кол-во углеводов за сутки' , tr: 'Günde ortalama karbonhidrat' + ,he: 'פחמימות ממוצע' }, 'Eating Soon': { cs: 'Blížící se jídlo' @@ -13855,6 +13868,7 @@ function init() { , pl: 'Przed jedzeniem' , ru: 'скоро прием пищи' , tr: 'Yakında Yenecek' + , he: 'אוכל בקרוב' }, 'Last entry {0} minutes ago': { cs: 'Poslední hodnota {0} minut zpět' @@ -13889,6 +13903,7 @@ function init() { , pl: 'zmiana' , ru: 'замена' , tr: 'değişiklik' + , he: 'שינוי' }, 'Speech': { cs: 'Hlas' @@ -13906,6 +13921,7 @@ function init() { , pl: 'Głos' , ru: 'речь' , tr: 'Konuş' + , he: 'דיבור' }, 'Target Top': { cs: 'Horní cíl' @@ -13923,6 +13939,7 @@ function init() { , ru: 'верхняя граница цели' , de: 'Oberes Ziel' , tr: 'Hedef Üst' + , he: 'ראש היעד' }, 'Target Bottom': { cs: 'Dolní cíl' @@ -13940,6 +13957,7 @@ function init() { , ru: 'нижняя граница цели' , de: 'Unteres Ziel' , tr: 'Hedef Alt' + , he: 'תחתית היעד' }, 'Canceled': { cs: 'Zrušený' @@ -13957,6 +13975,7 @@ function init() { , ru: 'отменено' , de: 'Abgebrochen' , tr: 'İptal edildi' + , he: 'מבוטל' }, 'Meter BG': { cs: 'Hodnota z glukoměru' @@ -13974,6 +13993,7 @@ function init() { , ru: 'СК по глюкометру' , de: 'Wert Blutzuckermessgerät' , tr: 'Glikometre KŞ' + , he: 'סוכר הדם של מד' }, 'predicted': { cs: 'přepověď' @@ -13991,6 +14011,7 @@ function init() { , ru: 'прогноз' , de: 'vorhergesagt' , tr: 'tahmin' + , he: 'חזה' }, 'future': { cs: 'budoucnost' @@ -14008,6 +14029,7 @@ function init() { , ru: 'будущее' , de: 'Zukunft' , tr: 'gelecek' + , he: 'עתיד' }, 'ago': { cs: 'zpět' @@ -14024,6 +14046,7 @@ function init() { , ru: 'назад' , de: 'vor' , tr: 'önce' + , he: 'לפני' }, 'Last data received': { cs: 'Poslední data přiajata' @@ -14041,6 +14064,7 @@ function init() { , ru: 'недавние данные получены' , de: 'Zuletzt Daten empfangen' , tr: 'Son veri alındı' + , he: 'הנתונים המקבל אחרונים' }, 'Clock View': { cs: 'Hodiny' @@ -14060,48 +14084,56 @@ function init() { ,de: 'Uhr-Anzeigen' ,pl: 'Widok zegara' ,tr: 'Saat Görünümü' + ,he: 'צג השעון' }, 'Protein': { fi: 'Proteiini' ,de: 'Protein' ,tr: 'Protein' ,hr: 'Proteini' + ,he: 'חלבון' }, 'Fat': { fi: 'Rasva' ,de: 'Fett' ,tr: 'Yağ' ,hr: 'Masti' + ,he: 'שמן' }, 'Protein average': { fi: 'Proteiini keskiarvo' ,de: 'Proteine Durchschnitt' ,tr: 'Protein Ortalaması' ,hr: 'Prosjek proteina' + ,he: 'חלבון ממוצע' }, 'Fat average': { fi: 'Rasva keskiarvo' ,de: 'Fett Durchschnitt' ,tr: 'Yağ Ortalaması' ,hr: 'Prosjek masti' + ,he: 'שמן ממוצע' }, 'Total carbs': { fi: 'Hiilihydraatit yhteensä' , de: 'Kohlenhydrate gesamt' ,tr: 'Toplam Karbonhidrat' ,hr: 'Ukupno ugh' + ,he: 'כל פחמימות' }, 'Total protein': { fi: 'Proteiini yhteensä' , de: 'Protein gesamt' ,tr: 'Toplam Protein' ,hr: 'Ukupno proteini' + ,he: 'כל חלבונים' }, 'Total fat': { fi: 'Rasva yhteensä' , de: 'Fett gesamt' ,tr: 'Toplam Yağ' ,hr: 'Ukupno masti' + ,he: 'כל שומנים' } }; From ba012f9bc65e7ee506ab579a4470a4c4a2f1f0a3 Mon Sep 17 00:00:00 2001 From: inventor96 Date: Tue, 19 Nov 2019 16:03:09 -0700 Subject: [PATCH 108/134] Added note regarding the use of the Alexa online simulator --- docs/plugins/alexa-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 0eefb465718..4e298df4b74 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -85,7 +85,7 @@ Click on the "Test" tab on the top menu. This will take you to the page where yo Enable testing for your skill (click the toggle). As indicated on this page, when testing is enabled, you can interact with the development version of your skill in the Alexa simulator and on all devices linked to your Alexa developer account. (Your skill will always be a development version. There's no need to publish it to the public.) -After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. +After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. When typing your test question, only type what you would verbally say to an Alexa device after the wake word. (e.g. you would verbally say "Alexa, ask Nightscout how am I doing", so you would type only "ask Nightscout how am I doing") You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. ##### What questions can you ask it? From 22c889da7fbb93188eb830d6f41d14a8fba7040a Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Fri, 22 Nov 2019 09:06:11 -0600 Subject: [PATCH 109/134] fix focus scale hour format --- lib/client/chart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index d37f9047e97..a4ceb25c925 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -147,7 +147,7 @@ function init (client, d3, $) { var formatMillisecond = localeFormatter.format('.%L') , formatSecond = localeFormatter.format(':%S') , formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : - localeFormatter.format('%I:%M') + localeFormatter.format('%-I:%M') , formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : localeFormatter.format('%-I %p') , formatDay = localeFormatter.format('%a %d') From ace12ca86ba6f9ed94755a1218e10f6b7f70bb24 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Fri, 22 Nov 2019 23:38:52 -0600 Subject: [PATCH 110/134] trip open-top size to fit --- lib/client/chart.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/client/chart.js b/lib/client/chart.js index d37f9047e97..2dbf1080fa1 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -9,6 +9,7 @@ var scrolling = false , scrollRange = null; var PADDING_BOTTOM = 30 + , OPEN_TOP_HEIGHT = 8 , CONTEXT_MAX = 420 , CONTEXT_MIN = 36 , FOCUS_MAX = 510 @@ -438,7 +439,7 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'open-top') .attr('stroke', '#111') - .attr('stroke-width', 12); + .attr('stroke-width', OPEN_TOP_HEIGHT); // add a x-axis line that closes the the brush container on left side chart.context.append('line') @@ -539,9 +540,9 @@ function init (client, d3, $) { // transition open-top line to correct location chart.context.select('.open-top') .attr('x1', chart.xScale2(currentRange[0])) - .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX))) + .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1) .attr('x2', chart.xScale2(currentRange[1])) - .attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX))); + .attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') @@ -621,9 +622,9 @@ function init (client, d3, $) { // transition open-top line to correct location chart.context.select('.open-top') .attr('x1', chart.xScale2(currentRange[0])) - .attr('y1', chart.yScale2(contextYDomain[1])) + .attr('y1', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1) .attr('x2', chart.xScale2(currentRange[1])) - .attr('y2', chart.yScale2(contextYDomain[1])); + .attr('y2', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') From b5b2f2a201bb6bc98562b3776d3fa2b01d17c276 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Fri, 22 Nov 2019 23:39:27 -0600 Subject: [PATCH 111/134] fix clearing alarming when clearning CurrentSGV --- lib/client/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/client/index.js b/lib/client/index.js index 84f0dff70fb..76e543f7278 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -469,6 +469,10 @@ client.load = function load (serverSettings, callback) { } function mergeDeviceStatus (retro, ddata) { + if (!retro) { + return ddata; + } + var result = retro.map(x => Object.assign(x, ddata.find(y => y._id == x._id))); var missingInRetro = ddata.filter(y => !retro.find(x => x._id == y._id)); @@ -545,7 +549,7 @@ client.load = function load (serverSettings, callback) { function clearCurrentSGV () { currentBG.text('---'); - container.removeClass('urgent warning inrange'); + container.removeClass('alarming urgent warning inrange'); } var nowDate = null; @@ -839,6 +843,12 @@ client.load = function load (serverSettings, callback) { container.toggleClass('alarming-timeago', status !== 'current'); + if (status === 'warn') { + container.addClass('warn'); + } else if (status === 'urgent') { + container.addClass('urgent'); + } + if (alarmingNow() && status === 'current' && isTimeAgoAlarmType()) { stopAlarm(true, times.min().msecs); } From 73667f92b74846581027e4617c56ad1321a77426 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Fri, 22 Nov 2019 23:39:58 -0600 Subject: [PATCH 112/134] do not set direction if not current --- lib/plugins/direction.js | 4 ++-- npm-shrinkwrap.json | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/plugins/direction.js b/lib/plugins/direction.js index 12a3511368b..6274fc9e537 100644 --- a/lib/plugins/direction.js +++ b/lib/plugins/direction.js @@ -10,7 +10,7 @@ function init() { direction.setProperties = function setProperties (sbx) { sbx.offerProperty('direction', function setDirection ( ) { - if (sbx.data.inRetroMode && !sbx.isCurrent(sbx.lastSGVEntry())) { + if (!sbx.isCurrent(sbx.lastSGVEntry())) { return undefined; } else { return direction.info(sbx.lastSGVEntry()); @@ -77,4 +77,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e74737626e6..bd9ada67189 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2272,7 +2272,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -2306,7 +2306,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", @@ -2352,7 +2352,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { "base64-js": "^1.0.2", @@ -3056,7 +3056,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -3068,7 +3068,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -3613,7 +3613,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -6368,7 +6368,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" @@ -6852,7 +6852,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "^1.1.7" } @@ -9415,7 +9415,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9923,7 +9923,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -10799,7 +10799,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" From c449d3957bd19e404134364e6c4f963de0c1ad6d Mon Sep 17 00:00:00 2001 From: inventor96 Date: Sat, 23 Nov 2019 13:25:49 -0700 Subject: [PATCH 113/134] Handled 0 for pump reservoir --- lib/plugins/pump.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index f23dd70b9cb..7e71c21e1b1 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -137,7 +137,7 @@ function init (ctx) { function virtAsstReservoirHandler (next, slots, sbx) { var reservoir = sbx.properties.pump.pump.reservoir; - if (reservoir) { + if (reservoir || reservoir === 0) { var response = translate('virtAsstReservoir', { params: [ reservoir From 72a89b606293f2203521f56bb388e246b726fd17 Mon Sep 17 00:00:00 2001 From: Jeremy Cunningham Date: Wed, 27 Nov 2019 22:35:39 -0600 Subject: [PATCH 114/134] rollback npm-shrinkwrap.json --- npm-shrinkwrap.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index bd9ada67189..e74737626e6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2272,7 +2272,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -2306,7 +2306,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", @@ -2352,7 +2352,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { "base64-js": "^1.0.2", @@ -3056,7 +3056,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -3068,7 +3068,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -3613,7 +3613,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -6368,7 +6368,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" @@ -6852,7 +6852,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } @@ -9415,7 +9415,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9923,7 +9923,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -10799,7 +10799,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" From b7ab2ad4735d3a60ce47d4db78b7810e423de7f7 Mon Sep 17 00:00:00 2001 From: David Jansson Date: Fri, 29 Nov 2019 10:42:20 +0100 Subject: [PATCH 115/134] Fix for distributionpage bug that caused hour 23 to always be active even after uncheck --- lib/report_plugins/glucosedistribution.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index 3d8d109610b..33b78a3ca8a 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -122,7 +122,7 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s $('#glucosedistribution-days').text(days + ' ' + translate('days total')); - for (var i = 0; i < 23; i++) { + for (var i = 0; i < 24; i++) { $('#glucosedistribution-' + i).unbind('click').click(onClick); enabledHours[i] = $('#glucosedistribution-' + i).is(':checked'); } From 1e2e94876382e86b25fb6827024a58577de97e3c Mon Sep 17 00:00:00 2001 From: fedor apanasenko Date: Wed, 4 Dec 2019 09:32:59 +0300 Subject: [PATCH 116/134] Russian translation update edit --- lib/language.js | 113 +++++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/lib/language.js b/lib/language.js index 6800ff50517..f33caa0277b 100644 --- a/lib/language.js +++ b/lib/language.js @@ -8546,7 +8546,7 @@ function init() { ,hr: 'Pohranjeni profili' ,pl: 'Zachowane profile' ,pt: 'Perfis guardados' - ,ru: 'Запомненные профили' + ,ru: 'Сохраненные профили' ,sk: 'Uložené profily' ,nl: 'Opgeslagen profielen' ,ko: '저장된 프로파일' @@ -8930,7 +8930,7 @@ function init() { ,hr: 'Iscrtaj bazale' ,pl: 'Zmiana dawki bazowej' ,pt: 'Renderizar basal' - ,ru: 'показывать базал' + ,ru: 'отрисовать базал' ,sk: 'Zobrazenie bazálu' ,nl: 'Toon basaal' ,ko: 'Basal 사용하기' @@ -8978,7 +8978,7 @@ function init() { ,hr: 'Izračun je u ciljanom rasponu.' ,pl: 'Obliczenie mieści się w zakresie docelowym' ,pt: 'O cálculo está dentro da meta' - ,ru: 'Расчет в целевых пределах ' + ,ru: 'Расчет в целевом диапазоне ' ,sk: 'Výpočet je v cieľovom rozsahu.' ,nl: 'Berekening valt binnen doelwaards' ,ko: '계산은 목표 범위 안에 있습니다.' @@ -9122,7 +9122,7 @@ function init() { ,hr: 'Vrijedi od:' ,pl: 'Ważne od:' ,pt: 'Válido desde:' - ,ru: 'Действует с' + ,ru: 'Действительно с' ,sk: 'Platné od:' ,nl: 'Geldig van:' ,ko: '유효' @@ -9360,7 +9360,7 @@ function init() { ,bg: 'Когато е активно ,иконката за редактиране ще се вижда' ,hr: 'Kada je omogućeno, mod uređivanje je omogućen' ,fi: 'Muokkausmoodin ikoni tulee näkyviin kun laitat tämän päälle' - ,ru: 'При активации видна икона начать режим редактирования' + ,ru: 'При активации видна пиктограмма начать режим редактирования' ,sk: 'Keď je povolený, je zobrazená ikona editačného módu' ,pl: 'Po aktywacji, widoczne ikony, aby uruchomić tryb edycji' ,pt: 'Quando ativado, o ícone iniciar modo de edição estará visível' @@ -9554,7 +9554,7 @@ function init() { ,bg: 'Да променя ли времето на събитието с %1?' ,hr: 'Promijeni vrijeme tretmana na %1?' ,fi: 'Muuta hoidon aika? Uusi: %1' - ,ru: 'Изменить время события на %1?' + ,ru: 'Изменить время события на %1 ?' ,sk: 'Zmeniť čas ošetrenia na %1 ?' ,pl: 'Zmień czas zdarzenia na %1 ?' ,pt: 'Alterar horário do tratamento para %1 ?' @@ -9578,7 +9578,7 @@ function init() { ,bg: 'Да променя ли времето на ВХ с %1?' ,hr: 'Promijeni vrijeme UGH na %1?' ,fi: 'Muuta hiilihydraattien aika? Uusi: %1' - ,ru: 'Изменить время подачи углеводов на %?' + ,ru: 'Изменить время подачи углеводов на % ?' ,sk: 'Zmeniť čas sacharidov na %1 ?' ,pl: 'Zmień czas węglowodanów na %1 ?' ,pt: 'Alterar horário do carboidrato para %1 ?' @@ -9602,7 +9602,7 @@ function init() { ,bg: 'Да променя ли времето на инсулина с %1?' ,hr: 'Promijeni vrijeme inzulina na %1?' ,fi: 'Muuta insuliinin aika? Uusi: %1' - ,ru: 'Изменить время подачи инсулина на %?' + ,ru: 'Изменить время подачи инсулина на % ?' ,sk: 'Zmeniť čas inzulínu na %1 ?' ,pl: 'Zmień czas insuliny na %1 ?' ,pt: 'Alterar horário da insulina para %1 ?' @@ -9626,7 +9626,7 @@ function init() { ,bg: 'Изтрий събитието' ,hr: 'Obriši tretman?' ,fi: 'Poista hoito?' - ,ru: 'Удалить событие?' + ,ru: 'Удалить событие ?' ,sk: 'Odstrániť ošetrenie?' ,pl: 'Usunąć wydarzenie?' ,pt: 'Remover tratamento?' @@ -9650,7 +9650,7 @@ function init() { ,bg: 'Да изтрия ли инсулина от събитието?' ,hr: 'Obriši inzulin iz tretmana?' ,fi: 'Poista insuliini hoidosta?' - ,ru: 'Удалить инсулин из событий?' + ,ru: 'Удалить инсулин из событий ?' ,sk: 'Odstrániť inzulín z ošetrenia?' ,pl: 'Usunąć insulinę z wydarzenia?' ,pt: 'Remover insulina do tratamento?' @@ -9674,7 +9674,7 @@ function init() { ,bg: 'Да изтрия ли ВХ от събитието?' ,hr: 'Obriši UGH iz tretmana?' ,fi: 'Poista hiilihydraatit hoidosta?' - ,ru: 'Удалить углеводы из событий?' + ,ru: 'Удалить углеводы из событий ?' ,sk: 'Odstrániť sacharidy z ošetrenia?' ,pl: 'Usunąć węglowodany z wydarzenia?' ,pt: 'Remover carboidratos do tratamento?' @@ -9818,7 +9818,7 @@ function init() { ,bg: 'Възраст на сензора (ВС)' ,hr: 'Starost senzora' ,ro: 'Vechimea senzorului' - ,ru: 'Сенсор проработал' + ,ru: 'Сенсор отработал' ,nl: 'Sensor leeftijd' ,ko: '센서 사용 기간' ,fi: 'Sensorin ikä' @@ -9843,7 +9843,7 @@ function init() { ,bg: 'Възраст на инсулина (ВИ)' ,hr: 'Starost inzulina' ,ro: 'Vechimea insulinei' - ,ru: 'инсулин проработал' + ,ru: 'инсулин отработал' ,ko: '인슐린 사용 기간' ,fi: 'Insuliinin ikä' ,pt: 'Idade da insulina' @@ -9914,7 +9914,7 @@ function init() { ,bg: 'Ядене скоро' ,hr: 'Uskoro jelo' ,ro: 'Mâncare în curând' - ,ru: 'Скоро еда' + ,ru: 'Приближается прием пищи' ,nl: 'Binnenkort eten' ,ko: '편집 중' ,fi: 'Syödään pian' @@ -10028,7 +10028,7 @@ function init() { ,ro: 'Insulină bolusată:' ,el: 'Ινσουλίνη' ,es: 'Bolo de Insulina' - ,ru: 'Болюс инсулин' + ,ru: 'Болюсный инсулин' ,sv: 'Bolusinsulin:' ,nb: 'Bolusinsulin:' ,hr: 'Bolus:' @@ -10052,7 +10052,7 @@ function init() { ,ro: 'Bazala obișnuită:' ,el: 'Βασική Ινσουλίνη' ,es: 'Insulina basal básica' - ,ru: 'Основной базал инсулин' + ,ru: 'Основной базальный инсулин' ,sv: 'Basalinsulin:' ,nb: 'Basalinsulin:' ,hr: 'Osnovni bazal:' @@ -10172,7 +10172,7 @@ function init() { ,fr: 'Incapable de %1 rôle' ,ro: 'Imposibil de %1 Rolul' ,es: 'Incapaz de %1 Rol' - ,ru: 'Невозможно %1 Роль' + ,ru: 'Невозможно назначить %1 Роль' ,sv: 'Kan inte ta bort roll %1' ,nb: 'Kan ikke %1 rolle' ,fi: '%1 operaatio roolille opäonnistui' @@ -10309,7 +10309,7 @@ function init() { ,hr: 'Sigurno želite obrisati?' ,ro: 'Confirmați ștergerea: ' ,fr: 'Êtes-vous sûr de vouloir effacer:' - ,ru: 'Вы уверены, что хотите удалить' + ,ru: 'Подтвердите удаление' ,sv: 'Är du säker att du vill ta bort:' ,nb: 'Er du sikker på at du vil slette:' ,fi: 'Oletko varmat että haluat tuhota: ' @@ -10516,7 +10516,7 @@ function init() { ,hr: 'Ne mogu %1 subjekt' ,ro: 'Imposibil de %1 Subiectul' ,fr: 'Impossible de créer l\'Utilisateur %1' - ,ru: 'Невозможно %1 субъекта' + ,ru: 'Невозможно создать %1 субъект' ,sv: 'Kan ej %1 ämne' ,nb: 'Kan ikke %1 ressurs' ,fi: '%1 operaatio käyttäjälle epäonnistui' @@ -10539,7 +10539,7 @@ function init() { ,hr: 'Ne mogu obrisati subjekt' ,ro: 'Imposibil de șters Subiectul' ,fr: 'Impossible d\'effacer l\'Utilisateur' - ,ru: 'Невозможно удалить ' + ,ru: 'Невозможно удалить Субъект ' ,sv: 'Kan ej ta bort ämne' ,nb: 'Kan ikke slette ressurs' ,fi: 'Käyttäjän poistaminen epäonnistui' @@ -10586,7 +10586,7 @@ function init() { ,ro: 'Editează Subiectul' ,es: 'Editar sujeto' ,fr: 'Éditer l\'Utilisateur' - ,ru: 'Редактировать субъекта' + ,ru: 'Редактировать Субъект' ,sv: 'Editera ämne' ,nb: 'Editer ressurs' ,fi: 'Muokkaa käyttäjää' @@ -10654,7 +10654,7 @@ function init() { ,hr: 'Uredi ovaj subjekt' ,ro: 'Editează acest subiect' ,fr: 'Éditer cet utilisateur' - ,ru: 'Редактировать этого субъекта' + ,ru: 'Редактировать этот субъект' ,sv: 'Editera ämnet' ,es: 'Editar este sujeto' ,nb: 'Editer ressurs' @@ -10677,7 +10677,7 @@ function init() { ,hr: 'Obriši ovaj subjekt' ,ro: 'Șterge acest subiect' ,fr: 'Effacer cet utilisateur:' - ,ru: 'Удалить этого субъекта' + ,ru: 'Удалить этот субъект' ,sv: 'Ta bort ämnet' ,nb: 'Slett ressurs' ,fi: 'Poista tämä käyttäjä' @@ -10886,7 +10886,7 @@ function init() { ,dk: 'Insulinfølsomhed (ISF)' ,ro: 'Sensibilitate la insulină (ISF)' ,fr: 'Sensibilité à l\'insuline (ISF)' - ,ru: 'Чуствительность к инсулину' + ,ru: 'Чуствительность к инсулину ISF' ,sk: 'Citlivosť (ISF)' ,sv: 'Insulinkönslighet (ISF)' ,nb: 'Insulinsensitivitet (ISF)' @@ -10909,7 +10909,7 @@ function init() { ,dk: 'Nuværende kulhydratratio' ,fr: 'Rapport Insuline-glucides actuel (I:C)' ,ro: 'Raport Insulină:Carbohidrați (ICR)' - ,ru: 'Актуальное соотношение инсулин:углеводы' + ,ru: 'Актуальное соотношение инсулин:углеводы I:C' ,sk: 'Aktuálny sacharidový pomer (I"C)' ,sv: 'Gällande kolhydratkvot' ,nb: 'Gjeldende karbohydratforhold' @@ -11069,7 +11069,7 @@ function init() { ,de: 'Basal-Profil Wert' ,dk: 'Basalprofil værdi' ,ro: 'Valoarea profilului bazalei' - ,ru: 'значение профиля базала' + ,ru: 'Величина профильного базала' ,fr: 'Valeur du débit basal' ,sv: 'Basalprofil värde' ,es: 'Valor perfil Basal' @@ -11280,7 +11280,7 @@ function init() { ,de: 'BWP' ,dk: 'Bolusberegner (BWP)' ,ro: 'Ajutor bolusare (BWP)' - ,ru: 'Предпросмотр калькулятора болюса' + ,ru: 'Калькулятор болюса' ,fr: 'Calculateur de bolus (BWP)' ,sv: 'Boluskalkylator' ,es: 'VistaPreviaCalculoBolo (BWP)' @@ -11427,7 +11427,7 @@ function init() { ,fr: 'Vérifier la glycémie, bolus nécessaire ?' ,bg: 'Провери КЗ, не е ли време за болус?' ,hr: 'Provjeri GUK, vrijeme je za bolus?' - ,ru: 'Проверьте СК, не пора ли ввести болюс?' + ,ru: 'Проверьте СК, дать болюс?' ,sv: 'Kontrollera BS, dags att ge bolus?' ,nb: 'Sjekk blodsukker, på tide med bolus?' ,fi: 'Tarkista VS, aika bolustaa?' @@ -11612,7 +11612,7 @@ function init() { ,fr: 'Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, sans prendre en compte les glucides' ,bg: 'Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ВХ не се вземат под внимание' ,hr: 'Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, ne uzevši u obzir UGH' - ,ru: 'Избыток инсулина равного %1U, необходимого для достижения нижнего целевого значения, углеводы не учитываются' + ,ru: 'Избыток инсулина равного %1U, необходимого для достижения нижнего целевого значения, углеводы не приняты в расчет' ,sv: 'Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, kolhydrater ej medräknade' ,nb: 'Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, karbohydrater ikke medregnet' ,nl: 'Insulineoverschot van %1U om laag doel te behalen (excl. koolhydraten)' @@ -11724,7 +11724,7 @@ function init() { ,dk: 'over højt grænse' ,ro: 'peste ținta superioară' ,fr: 'plus haut que la limite supérieure' - ,ru: 'Выше верхнего' + ,ru: 'Выше верхней границы' ,bg: 'над горната' ,hr: 'iznad gornje granice' ,sv: 'över hög nivå' @@ -11750,7 +11750,7 @@ function init() { ,fr: 'plus bas que la limite inférieure' ,bg: 'под долната' ,hr: 'ispod donje granice' - ,ru: 'Ниже нижнего' + ,ru: 'Ниже нижней границы' ,sv: 'under låg nivå' ,nb: 'under lav grense' ,fi: 'alle matalan' @@ -11866,7 +11866,7 @@ function init() { ,fr: 'Vérifier la glycémie avec un glucomètre avant de corriger!' ,bg: 'Провери КЗ с глюкомер, преди кореция!' ,hr: 'Provjeri GUK glukometrom prije korekcije!' - ,ru: 'Перед корректировкой сверьте СК с глюкометром' + ,ru: 'Перед корректировкой сверьте ГК с глюкометром' ,sv: 'Kontrollera blodglukos med fingerstick före korrigering!' ,nb: 'Sjekk blodsukker før korrigering!' ,fi: 'Tarkista VS mittarilla ennen korjaamista!' @@ -11889,7 +11889,7 @@ function init() { ,fr: 'Réduction du débit basal pour obtenir l\'effet d\' %1 unité' ,bg: 'Намаляне на базала с %1 единици' ,hr: 'Smanjeni bazal da uračuna %1 jedinica:' - ,ru: 'Снижение базы на %1 единиц' + ,ru: 'Снижение базы из-за %1 единиц болюса' ,sv: 'Basalsänkning för att nå %1 enheter' ,nb: 'Basalredusering for å nå %1 enheter' ,fi: 'Basaalin vähennys saadaksesi %1 yksikön vaikutuksen:' @@ -11958,7 +11958,7 @@ function init() { ,bg: 'Времето за смяна на сет просрочено' ,hr: 'Prošao rok za zamjenu kanile!' ,fr: 'Dépassement de date de changement de canule!' - ,ru: 'Срок работы катеттера истек' + ,ru: 'Срок замены катетера истек' ,sv: 'Infusionsset, bytestid överskriden' ,nb: 'Byttetid for infusjonssett overskredet' ,fi: 'Kanyylin ikä yli määräajan!' @@ -11981,7 +11981,7 @@ function init() { ,ro: 'Este vremea să schimbați canula' ,bg: 'Време за смяна на сет' ,hr: 'Vrijeme za zamjenu kanile' - ,ru: 'Пора заменить катеттер' + ,ru: 'Пора заменить катетер' ,sv: 'Dags att byta infusionsset' ,nb: 'På tide å bytte infusjonssett' ,fi: 'Aika vaihtaa kanyyli' @@ -12004,7 +12004,7 @@ function init() { ,fr: 'Changement de canule bientòt' ,bg: 'Смени сета скоро' ,hr: 'Zamijena kanile uskoro' - ,ru: 'Подходит время замены катеттера' + ,ru: 'Приближается время замены катетера' ,sv: 'Byt infusionsset snart' ,nb: 'Bytt infusjonssett snart' ,fi: 'Vaihda kanyyli pian' @@ -12026,7 +12026,7 @@ function init() { ,ro: 'Vechimea canulei în ore: %1' ,bg: 'Сетът е на %1 часове' ,hr: 'Staros kanile %1 sati' - ,ru: 'Возраст катеттера %1 час' + ,ru: 'Катетер отработал %1 час' ,sv: 'Infusionsset tid %1 timmar' ,nb: 'infusjonssett alder %1 timer' ,fi: 'Kanyylin ikä %1 tuntia' @@ -12049,7 +12049,7 @@ function init() { ,fr: 'Insérée' ,bg: 'Поставен' ,hr: 'Postavljanje' - ,ru: 'Введено' + ,ru: 'Установлен' ,sv: 'Applicerad' ,nb: 'Satt inn' ,fi: 'Asetettu' @@ -12074,7 +12074,7 @@ function init() { ,bg: 'ВС' ,hr: 'Starost kanile' ,fr: 'CAGE' - ,ru: 'ВозрКат' + ,ru: 'ОтрабКат' ,sv: 'Nål' ,nb: 'Nål alder' ,fi: 'KIKÄ' @@ -12098,7 +12098,7 @@ function init() { ,bg: 'АВХ' ,hr: 'Aktivni UGH' ,fr: 'COB' - ,ru: 'Активн углеводы' + ,ru: 'Активн углеводы COB' ,sv: 'COB' ,nb: 'Aktive karbohydrater' ,fi: 'AH' @@ -12142,9 +12142,9 @@ function init() { ,dk: 'Insulinalder' ,ro: 'VI' ,fr: 'IAGE' - ,bg: 'ВИнс' + ,bg: 'ИнсСрок' ,hr: 'Starost inzulina' - ,ru: 'ВозрИнс' + ,ru: 'ИнсСрок' ,sv: 'Insulinålder' ,nb: 'Insulinalder' ,fi: 'IIKÄ' @@ -12510,7 +12510,7 @@ function init() { ,de: 'RETRO' ,dk: 'RETRO' ,ro: 'VECHI' - ,ru: 'РЕТРО' + ,ru: 'ПРОШЛОЕ' ,bg: 'РЕТРО' ,hr: 'RETRO' ,fr: 'RETRO' @@ -12558,7 +12558,7 @@ function init() { ,dk: 'Sensor skift/genstart overskredet!' ,ro: 'Depășire termen schimbare/restart senzor!' ,fr: 'Changement/Redémarrage du senseur dépassé!' - ,ru: 'Рестарт сенсора просрочен' + ,ru: 'Рестарт сенсора пропущен' ,bg: 'Смяната/рестартът на сензора са пресрочени' ,hr: 'Prošao rok za zamjenu/restart senzora!' ,sv: 'Sensor byte/omstart överskriden!' @@ -12627,7 +12627,7 @@ function init() { ,dk: 'Sensoralder %1 dage %2 timer' ,ro: 'Senzori vechi de %1 zile și %2 ore' ,fr: 'Âge su senseur %1 jours et %2 heures' - ,ru: 'Сенсор проработал % дн % час' + ,ru: 'Сенсор отработал % дн % час' ,bg: 'Сензорът е на %1 дни %2 часа ' ,hr: 'Starost senzora %1 dana i %2 sati' ,sv: 'Sensorålder %1 dagar %2 timmar' @@ -12672,7 +12672,7 @@ function init() { ,de: 'Sensorstart' ,dk: 'Sensor start' ,ro: 'Pornirea senzorului' - ,ru: 'Запуск сенсора' + ,ru: 'Старт сенсора' ,fr: 'Démarrage du senseur' ,bg: 'Стартиране на сензора' ,hr: 'Pokretanje senzora' @@ -13037,7 +13037,7 @@ function init() { ,es: '">here.' ,fr: '">ici.' ,ro: '">aici.' - ,ru: '">here.' + ,ru: '">здесь.' ,nl: '">is hier te vinden.' ,zh_cn: '">here.' ,sv: '">här.' @@ -13587,7 +13587,7 @@ function init() { , fi: 'Loop plugin ei ole aktivoitu' , ro: 'Extensia loop pare a fi dezactivată' , pl: 'Plugin Loop prawdopodobnie nie jest włączona' - , ru: 'плагин ЗЦ Loop не активирован ' + , ru: 'плагин ЗЦ Loop не активирован' , tr: 'Döngü eklentisi etkin görünmüyor' }, 'alexaLoopForecast': { @@ -13604,7 +13604,7 @@ function init() { , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' , pl: 'Zgodnie z prognozą pętli, glikemia %1 będzie podczas następnego %2' - , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' + , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 в последующие %2' , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' }, 'alexaForecastUnavailable': { @@ -13721,7 +13721,7 @@ function init() { ,es: 'Energía [Kj]' ,fr: 'Énergie [kJ]' ,ro: 'Energie [g]' - ,ru: 'энергия [kJ' + ,ru: 'энергия [kJ]' ,it: 'Energia [kJ]' ,zh_cn: '能量 [kJ]' ,ko: 'Energy [kJ]' @@ -13971,7 +13971,7 @@ function init() { , bg: 'Измерена КЗ' , hr: 'GUK iz krvi' , pl: 'Glikemia z krwi' - , ru: 'СК по глюкометру' + , ru: 'ГК по глюкометру' , de: 'Wert Blutzuckermessgerät' , tr: 'Glikometre KŞ' }, @@ -14021,7 +14021,7 @@ function init() { , bg: 'преди' , hr: 'prije' , pl: 'temu' - , ru: 'назад' + , ru: 'в прошлом' , de: 'vor' , tr: 'önce' }, @@ -14038,7 +14038,7 @@ function init() { , bg: 'Последни данни преди' , hr: 'Podaci posljednji puta primljeni' , pl: 'Ostatnie otrzymane dane' - , ru: 'недавние данные получены' + , ru: 'последние данные получены' , de: 'Zuletzt Daten empfangen' , tr: 'Son veri alındı' }, @@ -14066,42 +14066,49 @@ function init() { ,de: 'Protein' ,tr: 'Protein' ,hr: 'Proteini' + ,ru: 'Белки' }, 'Fat': { fi: 'Rasva' ,de: 'Fett' ,tr: 'Yağ' ,hr: 'Masti' + ,ru: 'Жиры' }, 'Protein average': { fi: 'Proteiini keskiarvo' ,de: 'Proteine Durchschnitt' ,tr: 'Protein Ortalaması' ,hr: 'Prosjek proteina' + ,ru: 'Средний белок' }, 'Fat average': { fi: 'Rasva keskiarvo' ,de: 'Fett Durchschnitt' ,tr: 'Yağ Ortalaması' ,hr: 'Prosjek masti' + ,ru: 'Средний жир' }, 'Total carbs': { fi: 'Hiilihydraatit yhteensä' , de: 'Kohlenhydrate gesamt' ,tr: 'Toplam Karbonhidrat' ,hr: 'Ukupno ugh' + ,ru: 'Всего углеводов' }, 'Total protein': { fi: 'Proteiini yhteensä' , de: 'Protein gesamt' ,tr: 'Toplam Protein' ,hr: 'Ukupno proteini' + ,ru: 'Всего белков' }, 'Total fat': { fi: 'Rasva yhteensä' , de: 'Fett gesamt' ,tr: 'Toplam Yağ' ,hr: 'Ukupno masti' + ,ru: 'Всего жиров' } }; From c4f5ffaf8cccef0a7f4e5fadddd8a42b783fec4a Mon Sep 17 00:00:00 2001 From: Jakob Sandberg Date: Thu, 5 Dec 2019 14:58:58 -0800 Subject: [PATCH 117/134] Use default entries count instead of distributed number literals --- lib/api/entries/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index 0c8b8fc1ef7..faf922ff0e8 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -408,7 +408,7 @@ function configure (app, wares, ctx, env) { // If "?count=" is present, use that number to decided how many to return. if (!query.count) { - query.count = 10; + query.count = consts.ENTRIES_DEFAULT_COUNT; } // bias towards entries, but allow expressing preference of storage layer var storage = req.params.echo || 'entries'; @@ -434,7 +434,7 @@ function configure (app, wares, ctx, env) { // If "?count=" is present, use that number to decided how many to return. if (!query.count) { - query.count = 10; + query.count = consts.ENTRIES_DEFAULT_COUNT; } // bias to entries, but allow expressing a preference @@ -476,7 +476,7 @@ function configure (app, wares, ctx, env) { } var query = req.query; if (!query.count) { - query.count = 10 + query.count = consts.ENTRIES_DEFAULT_COUNT; } // remove using the query req.model.remove(query, function(err, stat) { From 3170345731b099275923e0ca733a911292f9eec2 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 6 Dec 2019 01:42:16 -0800 Subject: [PATCH 118/134] Handle mmol to mgdl conversions with a constant reference (#5274) --- lib/client/careportal.js | 21 +++++++++++---------- lib/client/renderer.js | 7 ++++--- lib/constants.json | 3 ++- lib/data/ddata.js | 9 +++++---- lib/data/treatmenttocurve.js | 3 ++- lib/plugins/basalprofile.js | 3 ++- lib/plugins/openaps.js | 3 ++- lib/report_plugins/glucosedistribution.js | 12 +++++++----- lib/report_plugins/utils.js | 4 +++- lib/units.js | 6 ++++-- 10 files changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/client/careportal.js b/lib/client/careportal.js index 0d1220d55a7..a5c86232d8e 100644 --- a/lib/client/careportal.js +++ b/lib/client/careportal.js @@ -4,6 +4,7 @@ var moment = require('moment-timezone'); var _ = require('lodash'); var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration var times = require('../times'); +var consts = require('../constants'); var Storages = require('js-storage'); function init (client, $) { @@ -240,8 +241,8 @@ function init (client, $) { } if (units == "mmol") { - data.targetTop = data.targetTop * 18; - data.targetBottom = data.targetBottom * 18; + data.targetTop = data.targetTop * consts.MMOL_TO_MGDL; + data.targetBottom = data.targetBottom * consts.MMOL_TO_MGDL; } //special handling for absolute to support temp to 0 @@ -298,14 +299,14 @@ function init (client, $) { let targetTop = parseInt(data.targetTop); let targetBottom = parseInt(data.targetBottom); - let minTarget = 4 * 18; - let maxTarget = 18 * 18; + let minTarget = 4 * consts.MMOL_TO_MGDL; + let maxTarget = 18 * consts.MMOL_TO_MGDL; if (units == "mmol") { - targetTop = Math.round(targetTop / 18.0 * 10) / 10; - targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; - minTarget = Math.round(minTarget / 18.0 * 10) / 10; - maxTarget = Math.round(maxTarget / 18.0 * 10) / 10; + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + minTarget = Math.round(minTarget / consts.MMOL_TO_MGDL * 10) / 10; + maxTarget = Math.round(maxTarget / consts.MMOL_TO_MGDL * 10) / 10; } if (targetTop > maxTarget) { @@ -358,8 +359,8 @@ function init (client, $) { var targetBottom = data.targetBottom; if (units == "mmol") { - targetTop = Math.round(data.targetTop / 18.0 * 10) / 10; - targetBottom = Math.round(data.targetBottom / 18.0 * 10) / 10; + targetTop = Math.round(data.targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(data.targetBottom / consts.MMOL_TO_MGDL * 10) / 10; } pushIf(data.targetTop, translate('Target Top') + ': ' + targetTop); diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 688b046b7b2..5a0354afc51 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var times = require('../times'); +var consts = require('../constants'); var DEFAULT_FOCUS = times.hours(3).msecs , WIDTH_SMALL_DOTS = 420 @@ -210,8 +211,8 @@ function init (client, d3) { var targetTop = d.targetTop; if (client.settings.units === 'mmol') { - targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; - targetTop = Math.round(targetTop / 18.0 * 10) / 10; + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; } var correctionRangeText; @@ -632,7 +633,7 @@ function init (client, d3) { function treatmentTooltip () { var glucose = treatment.glucose; if (client.settings.units != client.ddata.profile.getUnits()) { - glucose *= (client.settings.units === 'mmol' ? 0.055 : 18); + glucose *= (client.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL); var decimals = (client.settings.units === 'mmol' ? 10 : 1); glucose = Math.round(glucose * decimals) / decimals; diff --git a/lib/constants.json b/lib/constants.json index 31a4524b4d3..c2736f7e6e9 100644 --- a/lib/constants.json +++ b/lib/constants.json @@ -4,5 +4,6 @@ "HTTP_UNAUTHORIZED" : 401, "HTTP_VALIDATION_ERROR" : 422, "HTTP_INTERNAL_ERROR" : 500, - "ENTRIES_DEFAULT_COUNT" : 10 + "ENTRIES_DEFAULT_COUNT" : 10, + "MMOL_TO_MGDL": 18 } diff --git a/lib/data/ddata.js b/lib/data/ddata.js index 0251ec17d99..9fa470e3f16 100644 --- a/lib/data/ddata.js +++ b/lib/data/ddata.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var times = require('../times'); +var consts = require('../constants'); var DEVICE_TYPE_FIELDS = ['uploader', 'pump', 'openaps', 'loop', 'xdripjs']; @@ -213,18 +214,18 @@ function init () { if (t.units) { if (t.units == 'mmol') { //convert to mgdl - t.targetTop = t.targetTop * 18; - t.targetBottom = t.targetBottom * 18; + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } } //if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety. if (t.targetTop < 20) { - t.targetTop = t.targetTop * 18; + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } if (t.targetBottom < 20) { - t.targetBottom = t.targetBottom * 18; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } return t.eventType && t.eventType.indexOf('Temporary Target') > -1; diff --git a/lib/data/treatmenttocurve.js b/lib/data/treatmenttocurve.js index 8bca564d091..afa17b397ec 100644 --- a/lib/data/treatmenttocurve.js +++ b/lib/data/treatmenttocurve.js @@ -1,9 +1,10 @@ 'use strict'; var _ = require('lodash'); +var consts = require('../constants'); const MAX_BG_MMOL = 22; -const MAX_BG_MGDL = MAX_BG_MMOL * 18; +const MAX_BG_MGDL = MAX_BG_MMOL * consts.MMOL_TO_MGDL; module.exports = function fitTreatmentsToBGCurve (ddata, env, ctx) { diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 73c9492ea44..a766f9ab0a8 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -1,6 +1,7 @@ 'use strict'; var times = require('../times'); var moment = require('moment'); +var consts = require('../constants'); function init (ctx) { @@ -65,7 +66,7 @@ function init (ctx) { var units = profile.getUnits(); if (sbx.settings.units != units) { - sensitivity *= (sbx.settings.units === 'mmol' ? 0.055 : 18); + sensitivity *= (sbx.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL); var decimals = (sbx.settings.units === 'mmol' ? 10 : 1); sensitivity = Math.round(sensitivity * decimals) / decimals; diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index 77f27288b14..4d2535a3b76 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -4,6 +4,7 @@ var _ = require('lodash'); var moment = require('moment'); var times = require('../times'); var levels = require('../levels'); +var consts = require('../constants'); // var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable @@ -352,7 +353,7 @@ function init (ctx) { var units = sbx.data.profile.getUnits(); if (units === 'mmol') { - bg = Math.round(bg / 18 * 10) / 10; + bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10; } var valueParts = [ diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index 3d8d109610b..2d034d2bba8 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + var glucosedistribution = { name: 'glucosedistribution' , label: 'Distribution' @@ -174,7 +176,7 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var bg = Math.floor(entry.bgValue + bgDelta * j); var t = new Date(entry.displayTime.getTime() + j * timePatch); var newEntry = { - sgv: displayUnits === 'mmol' ? bg / 18 : bg + sgv: displayUnits === 'mmol' ? bg / consts.MMOL_TO_MGDL : bg , bgValue: bg , displayTime: t }; @@ -217,7 +219,7 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s const interpolatedValue = prevEntry.bgValue + d; let newEntry = { - sgv: displayUnits === 'mmol' ? interpolatedValue / 18 : interpolatedValue + sgv: displayUnits === 'mmol' ? interpolatedValue / consts.MMOL_TO_MGDL : interpolatedValue , bgValue: interpolatedValue , displayTime: entry.displayTime }; @@ -433,11 +435,11 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var unitString = ' mg/dl'; if (displayUnits == 'mmol') { - TDC = TDC / 18.0; - TDCHourly = TDCHourly / 18.0; + TDC = TDC / consts.MMOL_TO_MGDL; + TDCHourly = TDCHourly / consts.MMOL_TO_MGDL; unitString = ' mmol/L'; - RMS = Math.sqrt(RMSTotal / events) / 18; + RMS = Math.sqrt(RMSTotal / events) / consts.MMOL_TO_MGDL; } TDC = Math.round(TDC * 100) / 100; diff --git a/lib/report_plugins/utils.js b/lib/report_plugins/utils.js index 10daec32304..be6ba940003 100644 --- a/lib/report_plugins/utils.js +++ b/lib/report_plugins/utils.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + var moment = window.moment; var utils = { }; @@ -69,7 +71,7 @@ utils.scaledTreatmentBG = function scaledTreatmentBG(treatment,data) { console.info('found mismatched glucose units, converting ' + treatment.units + ' into ' + client.settings.units, treatment); if (treatment.units === 'mmol') { //BG is in mmol and display in mg/dl - treatmentGlucose = Math.round(treatment.glucose * 18); + treatmentGlucose = Math.round(treatment.glucose * consts.MMOL_TO_MGDL); } else { //BG is in mg/dl and display in mmol treatmentGlucose = client.utils.scaleMgdl(treatment.glucose); diff --git a/lib/units.js b/lib/units.js index f548d55744e..5eb36c10950 100644 --- a/lib/units.js +++ b/lib/units.js @@ -1,11 +1,13 @@ 'use strict'; +var consts = require('./constants'); + function mgdlToMMOL(mgdl) { - return (Math.round((mgdl / 18) * 10) / 10).toFixed(1); + return (Math.round((mgdl / consts.MMOL_TO_MGDL) * 10) / 10).toFixed(1); } function mmolToMgdl(mgdl) { - return Math.round(mgdl * 18); + return Math.round(mgdl * consts.MMOL_TO_MGDL); } function configure() { From 6abd6bed2712989f4f389e115f60d719809db569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Aarrestad?= Date: Fri, 6 Dec 2019 11:27:00 +0100 Subject: [PATCH 119/134] Added sanity check for the bridge interval value (#4717) * Added sanity check for the bridge interval value, setting minimum allowed value to 30 seconds and maximum to 5 minutes. This is to avoid input of very low values which might overload the dexcom servers. * Added missin ';' * Fixed typoe in comment * Added test for default interval (not set in config) * Set lower limit to 1 second --- lib/plugins/bridge.js | 10 +++++++++- tests/bridge.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/plugins/bridge.js b/lib/plugins/bridge.js index dc47f13aa9b..bf3c67e88da 100644 --- a/lib/plugins/bridge.js +++ b/lib/plugins/bridge.js @@ -46,9 +46,17 @@ function options (env) { , minutes: env.extendedSettings.bridge.minutes || 1440 }; + var interval = env.extendedSettings.bridge.interval || 60000 * 2.5; // Default: 2.5 minutes + + if (interval < 1000 || interval > 300000) { + // Invalid interval range. Revert to default + console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.5 minutes.") + interval = 60000 * 2.5 // 2.5 minutes + } + return { login: config - , interval: env.extendedSettings.bridge.interval || 60000 * 2.5 + , interval: interval , fetch: fetch_config , nightscout: { } , maxFailures: env.extendedSettings.bridge.maxFailures || 3 diff --git a/tests/bridge.test.js b/tests/bridge.test.js index 99c1587fab4..66b69f64c3a 100644 --- a/tests/bridge.test.js +++ b/tests/bridge.test.js @@ -10,6 +10,7 @@ describe('bridge', function ( ) { bridge: { userName: 'nightscout' , password: 'wearenotwaiting' + , interval: 60000 } } }; @@ -27,6 +28,7 @@ describe('bridge', function ( ) { opts.login.accountName.should.equal('nightscout'); opts.login.password.should.equal('wearenotwaiting'); + opts.interval.should.equal(60000); }); it('store entries from share', function (done) { @@ -39,4 +41,43 @@ describe('bridge', function ( ) { bridge.bridged(mockEntries)(null); }); + it('set too low bridge interval option from env', function () { + var tooLowInterval = { + extendedSettings: { + bridge: { interval: 900 } + } + }; + + var opts = bridge.options(tooLowInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + + it('set too high bridge interval option from env', function () { + var tooHighInterval = { + extendedSettings: { + bridge: { interval: 500000 } + } + }; + + var opts = bridge.options(tooHighInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + + it('set no bridge interval option from env', function () { + var noInterval = { + extendedSettings: { + bridge: { } + } + }; + + var opts = bridge.options(noInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + }); From 7b58a2251f797572bce0601aef12bdaade9273bb Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Fri, 6 Dec 2019 12:27:54 +0200 Subject: [PATCH 120/134] Use auth tokens in api-secret (#5214) * Allow auth tokens to be passed in the api-secret header and used in the client in place of the API secret * Fix unit test * Fix admin test * Reload page when token is used as a secret --- lib/api/status.js | 5 +++- lib/api/verifyauth.js | 8 ++++-- lib/authorization/index.js | 29 +++++++++++++++++-- lib/authorization/storage.js | 22 ++++++++++++-- lib/client/index.js | 40 ++++++++++++++++++++------ lib/hashauth.js | 49 ++++++++++++++++++++++--------- lib/language.js | 56 ++++-------------------------------- lib/server/websocket.js | 3 +- tests/admintools.test.js | 2 +- tests/api.verifyauth.test.js | 4 +-- 10 files changed, 131 insertions(+), 87 deletions(-) diff --git a/lib/api/status.js b/lib/api/status.js index 9d18b524ec3..a6ab2e82750 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -14,6 +14,9 @@ function configure (app, wares, env, ctx) { // Status badge/text/json api.get('/status', function (req, res) { + + var authToken = req.query.token || req.query.secret || ''; + var date = new Date(); var info = { status: 'ok' , name: app.get('name') @@ -25,7 +28,7 @@ function configure (app, wares, env, ctx) { , boluscalcEnabled: app.enabled('api') && env.settings.enable.indexOf('boluscalc') > -1 , settings: env.settings , extendedSettings: app.extendedClientSettings - , authorized: ctx.authorization.authorize(req.query.token || '') + , authorized: ctx.authorization.authorize(authToken) }; var badge = 'http://img.shields.io/badge/Nightscout-OK-green'; diff --git a/lib/api/verifyauth.js b/lib/api/verifyauth.js index 3b1bf74c844..a4eb2edf4ee 100644 --- a/lib/api/verifyauth.js +++ b/lib/api/verifyauth.js @@ -10,11 +10,15 @@ function configure (ctx) { ctx.authorization.resolveWithRequest(req, function resolved (err, result) { // this is used to see if req has api-secret equivalent authorization - var authorized = !err && + var authorized = !err && ctx.authorization.checkMultiple('*:*:create,update,delete', result.shiros) && //can write to everything ctx.authorization.checkMultiple('admin:*:*:*', result.shiros); //full admin permissions too + var response = { + message: authorized ? 'OK' : 'UNAUTHORIZED', + rolefound: result.subject ? 'FOUND' : 'NOTFOUND' + } - res.sendJSONStatus(res, consts.HTTP_OK, authorized ? 'OK' : 'UNAUTHORIZED'); + res.sendJSONStatus(res, consts.HTTP_OK, response); }); }); diff --git a/lib/authorization/index.js b/lib/authorization/index.js index 388fa19dffc..feaed739b42 100644 --- a/lib/authorization/index.js +++ b/lib/authorization/index.js @@ -141,14 +141,19 @@ function init (env, ctx) { authorization.resolve = function resolve (data, callback) { + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (storage.doesAccessTokenExist(data.api_secret)) { + authorization.resolveAccessToken (data.api_secret, callback, defaultShiros); + return; + } + if (authorizeAdminSecret(data.api_secret)) { var admin = shiroTrie.new(); admin.add(['*']); return callback(null, { shiros: [ admin ] }); } - var defaultShiros = storage.rolesToShiros(defaultRoles); - if (data.token) { jwt.verify(data.token, env.api_secret, function result(err, verified) { if (err) { @@ -192,6 +197,25 @@ function init (env, ctx) { var remoteIP = getRemoteIP(req); + var secret = adminSecretFromRequest(req); + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (storage.doesAccessTokenExist(secret)) { + var resolved = storage.resolveSubjectAndPermissions (secret); + + if (authorization.checkMultiple(permission, resolved.shiros)) { + console.log(LOG_GRANTED, remoteIP, resolved.accessToken , permission); + next(); + } else if (authorization.checkMultiple(permission, defaultShiros)) { + console.log(LOG_GRANTED, remoteIP, resolved.accessToken, permission, 'default'); + next( ); + } else { + console.log(LOG_DENIED, remoteIP, resolved.accessToken, permission); + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + return; + } + if (authorizeAdminSecretWithRequest(req)) { console.log(LOG_GRANTED, remoteIP, 'api-secret', permission); next( ); @@ -199,7 +223,6 @@ function init (env, ctx) { } var token = extractToken(req); - var defaultShiros = storage.rolesToShiros(defaultRoles); if (token) { jwt.verify(token, env.api_secret, function result(err, verified) { diff --git a/lib/authorization/storage.js b/lib/authorization/storage.js index 9999b9f3178..3a4c4490876 100644 --- a/lib/authorization/storage.js +++ b/lib/authorization/storage.js @@ -122,6 +122,12 @@ function init (env, ctx) { , { name: 'activity', permissions: [ 'api:activity:create' ] } ]; + storage.getSHA1 = function getSHA1 (message) { + var shasum = crypto.createHash('sha1'); + shasum.update(message); + return shasum.digest('hex'); + } + storage.reload = function reload (callback) { storage.listRoles({sort: {name: 1}}, function listResults (err, results) { @@ -152,6 +158,7 @@ function init (env, ctx) { var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10); subject.digest = shasum.digest('hex'); subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16); + subject.accessTokenDigest = storage.getSHA1(subject.accessToken); } return subject; @@ -200,17 +207,28 @@ function init (env, ctx) { }; storage.findSubject = function findSubject (accessToken) { - var prefix = _.last(accessToken.split('-')); + + if (!accessToken) return null; + + var split_token = accessToken.split('-'); + var prefix = split_token ? _.last(split_token) : ''; if (prefix.length < 16) { return null; } return _.find(storage.subjects, function matches (subject) { - return subject.digest.indexOf(prefix) === 0; + return subject.accessTokenDigest.indexOf(accessToken) === 0 || subject.digest.indexOf(prefix) === 0; }); }; + storage.doesAccessTokenExist = function doesAccessTokenExist(accessToken) { + if (storage.findSubject(accessToken)) { + return true; + } + return false; + } + storage.resolveSubjectAndPermissions = function resolveSubjectAndPermissions (accessToken) { var shiros = []; diff --git a/lib/client/index.js b/lib/client/index.js index 76e543f7278..eb6c4a9fd93 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -35,6 +35,11 @@ client.headers = function headers () { } }; +client.crashed = function crashed () { + $('#centerMessagePanel').show(); + $('#loadingMessageText').html('It appears the server has crashed. Please go to Heroku or Azure and reboot the server.'); +} + client.init = function init (callback) { client.browserUtils = require('./browser-utils')($); @@ -1003,18 +1008,30 @@ client.load = function load (serverSettings, callback) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Alarms and Text handling //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - socket.on('connect', function() { - console.log('Client connected to server.'); + + + client.authorizeSocket = function authorizeSocket() { + + console.log('Authorizing socket'); + var auth_data = { + client: 'web' + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token + , history: history + }; + socket.emit( 'authorize' - , { - client: 'web' - , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() - , token: client.authorized && client.authorized.token - , history: history - } + , auth_data , function authCallback (data) { - console.log('Client rights: ', data); + + console.log('Socket auth response', data); + + if (!data) { + console.log('Crashed!'); + client.crashed(); + } + if (!data.read || !hasRequiredPermission()) { client.hashauth.requestAuthentication(function afterRequest () { client.hashauth.updateSocketAuth(); @@ -1027,6 +1044,11 @@ client.load = function load (serverSettings, callback) { } } ); + } + + socket.on('connect', function() { + console.log('Client connected to server.'); + client.authorizeSocket(); }); function hasRequiredPermission () { diff --git a/lib/hashauth.js b/lib/hashauth.js index 0c95c77ad2e..1421c076535 100644 --- a/lib/hashauth.js +++ b/lib/hashauth.js @@ -9,6 +9,7 @@ var hashauth = { , apisecrethash: null , authenticated: false , initialized: false + , tokenauthenticated: false }; hashauth.init = function init(client, $) { @@ -24,15 +25,27 @@ hashauth.init = function init(client, $) { , url: '/api/v1/verifyauth?t=' + Date.now() //cache buster , headers: client.headers() }).done(function verifysuccess (response) { - if (response.message === 'OK') { + + if (response.message.rolefound == 'FOUND') { + hashauth.tokenauthenticated = true; + console.log('Token Authentication passed.'); + client.authorizeSocket(); + next(true); + return; + } + + if (response.message.message === 'OK') { hashauth.authenticated = true; console.log('Authentication passed.'); next(true); - } else { - console.log('Authentication failed.', response); + return; + } + + console.log('Authentication failed.', response); hashauth.removeAuthentication(); next(false); - } + return; + }).fail(function verifyfail (err) { console.log('Authentication failed.', err); hashauth.removeAuthentication(); @@ -60,7 +73,7 @@ hashauth.init = function init(client, $) { Storages.localStorage.remove('apisecrethash'); - if (hashauth.authenticated) { + if (hashauth.authenticated || hashauth.tokenauthenticated) { client.browserUtils.reload(); } @@ -141,6 +154,8 @@ hashauth.init = function init(client, $) { if (isok) { if (hashauth.storeapisecret) { Storages.localStorage.set('apisecrethash',hashauth.apisecrethash); + // TODO show dialog first, then reload + if (hashauth.tokenauthenticated) client.browserUtils.reload(); } $('#authentication_placeholder').html(hashauth.inlineCode()); if (callback) { @@ -161,9 +176,18 @@ hashauth.init = function init(client, $) { var status = null; - if (client.authorized) { - status = translate('Authorized by token') + ' (' + translate('view without token') + ')' + - '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + if (client.authorized || hashauth.tokenauthenticated) { + status = translate('Authorized by token'); + if (client.authorized && client.authorized.sub) { + status += '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + } + if (hashauth.apisecrethash) + { + status += '
(' + translate('Remove stored token') + ')'; + } else { + status += '
(' + translate('view without token') + ')'; + } + } else if (hashauth.isAuthenticated()) { console.info('status isAuthenticated', hashauth); status = translate('Admin authorized') + ' (' + translate('Remove') + ')'; @@ -173,13 +197,10 @@ hashauth.init = function init(client, $) { var html = ''+ '
' + status + '
'; @@ -208,7 +229,7 @@ hashauth.init = function init(client, $) { }; hashauth.isAuthenticated = function isAuthenticated() { - return hashauth.authenticated; + return hashauth.authenticated || hashauth.tokenauthenticated; }; hashauth.initialized = true; diff --git a/lib/language.js b/lib/language.js index 23571dcd3a1..bba7871b0ce 100644 --- a/lib/language.js +++ b/lib/language.js @@ -3021,57 +3021,11 @@ function init() { ,tr: 'Gizli göster' ,zh_cn: '显示隐藏值' } - ,'Your API secret' : { - cs: 'Vaše API heslo' - ,he: 'הסיסמא הסודית שלך' - ,de: 'Deine API-Prüfsumme' - ,es: 'Su API secreto' - ,fr: 'Votre secret API' - ,el: 'Το συνθηματικό σας' - ,pt: 'Seu segredo de API' - ,sv: 'Din API-nyckel' - ,ro: 'Cheia API' - ,bg: 'Твоята API парола' - ,hr: 'Vaš tajni API' - ,it: 'Il tuo API secreto' - ,ja: 'あなたのAPI Secret' - ,dk: 'Din API-nøgle' - ,fi: 'Sinun API-avaimesi' - ,nb: 'Din API nøkkel' - ,pl: 'Twoje poufne hasło API' - ,ru: 'Ваш пароль API' - ,sk: 'Vaše API heslo' - ,nl: 'Uw API wachtwoord' - ,ko: 'API secret' - ,tr: 'API secret parolanız' - ,zh_cn: 'API密钥' - ,zh_tw: 'API密鑰' - } - ,'Remember the API Secret on this device. (Do not enable this on public computers.)' : { - cs: 'Ulož hash na tomto počítači (používejte pouze na soukromých počítačích)' - ,he: 'אחסן את הסיסמא הסודית שלך על מחשב זה.מומלץ לעשות כן רק אם המחשב בשימושך הפרטי' - ,de: 'Speichere Prüfsumme auf diesem Computer (nur auf privaten Computern verwenden)' - ,es: 'Guardar hash en este ordenador (Usar solo en ordenadores privados)' - ,fr: 'Sauvegarder le hash sur cet ordinateur (privé uniquement)' - ,el: 'Αποθήκευση συνθηματικού σε αυτό τον υπολογιστή (μόνο για υπολογιστές προσωπικής χρήσης)' - ,pt: 'Salvar hash nesse computador (Somente em computadores privados)' - ,ro: 'Salvează cheia pe acest PC (Folosiți doar PC de încredere)' - ,bg: 'Запамети данните на този компютър. ( Използвай само на собствен компютър)' - ,hr: 'Pohrani hash na ovom računalu (Koristiti samo na osobnom računalu)' - ,sv: 'Lagra hashvärde på denna dator (använd endast på privat dator)' - ,it: 'Conservare hash su questo computer (utilizzare solo su computer privati)' - ,ja: 'このコンピューターにハッシュ値を保存する(このコンピューターでのみ使用する)' - ,dk: 'Gemme hash på denne computer (brug kun på privat computer)' - ,fi: 'Tallenna avain tälle tietokoneelle (käytä vain omalla tietokoneellasi)' - ,nb: 'Lagre hash på denne pc (bruk kun på privat pc)' - ,pl: 'Zapisz na tym komputerze (korzystaj tylko na komputerach prywatnych)' - ,ru: 'Сохранить хеш на этом ПК (только для личных компьютеров)' - ,sk: 'Uložiť hash na tomto počítači (Používajte iba na súkromných počítačoch)' - ,nl: 'Sla wachtwoord op. (gebruik dit alleen op prive computers)' - ,ko: '이 컴퓨터에 hash를 저장하세요.(단, 개인 컴퓨터를 사용하세요.)' - ,tr: 'Bu bilgisayarda hash parolasını sakla (Yalnızca özel bilgisayarlarda kullanın)' - ,zh_cn: '在本机存储API密钥\n(请勿在公用电脑上使用本功能)' - ,zh_tw: '在本機存儲API密鑰\n(請勿在公用電腦上使用本功能)' + ,'Your API secret or token' : { + fi: 'API salaisuus tai avain' + } + ,'Remember this device. (Do not enable this on public computers.)' : { + fi: 'Muista tämä laite (Älä valitse julkisilla tietokoneilla)' } ,'Treatments' : { cs: 'Ošetření' diff --git a/lib/server/websocket.js b/lib/server/websocket.js index 76276edffb2..afced8bfb37 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -83,6 +83,7 @@ function init (env, ctx, server) { read: false , write: false , write_treatment: false + , error: true }); } @@ -440,11 +441,9 @@ function init (env, ctx, server) { if (socketAuthorization.read) { socket.join('DataReceivers'); - var filterTreatments = false; var msecHistory = times.hours(history).msecs; // if `from` is received, it's a reconnection and full data is not needed if (from && from > 0) { - filterTreatments = true; msecHistory = Math.min(new Date().getTime() - from, msecHistory); } diff --git a/tests/admintools.test.js b/tests/admintools.test.js index 9f867a543c3..0b95acdf6c1 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -138,7 +138,7 @@ describe('admintools', function ( ) { if (url.indexOf('status.json') > -1) { fn(serverSettings); } else { - fn({message: 'OK'}); + fn({message: {message: 'OK'}}); } return self.$.ajax(); }, diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js index a9fd681da7b..48a05f2d9c4 100644 --- a/tests/api.verifyauth.test.js +++ b/tests/api.verifyauth.test.js @@ -26,7 +26,7 @@ describe('Verifyauth REST api', function ( ) { .get('/api/verifyauth') .expect(200) .end(function(err, res) { - res.body.message.should.equal('UNAUTHORIZED'); + res.body.message.message.should.equal('UNAUTHORIZED'); done(); }); }); @@ -37,7 +37,7 @@ describe('Verifyauth REST api', function ( ) { .set('api-secret', self.env.api_secret || '') .expect(200) .end(function(err, res) { - res.body.message.should.equal('OK'); + res.body.message.message.should.equal('OK'); done(); }); }); From d05c1603d5c148b2c3f7928654ba645e938c15c1 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Fri, 6 Dec 2019 12:31:09 +0200 Subject: [PATCH 121/134] Fix dependency vulnerabilities --- npm-shrinkwrap.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e74737626e6..f9281c2e088 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5304,9 +5304,9 @@ } }, "handlebars": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", - "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", From 149ca1a9ab8104198e1137236d2328975f4e312e Mon Sep 17 00:00:00 2001 From: lixgbg Date: Fri, 6 Dec 2019 11:39:59 +0100 Subject: [PATCH 122/134] Added basal and bolus averages besides TDD and carbs (#4216) --- lib/report_plugins/daytoday.js | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 4fde5658b19..f4b5da7cf45 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -85,6 +85,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var padding = { top: 15, right: 22, bottom: 30, left: 35 }; var tddSum = 0; + var basalSum = 0; + var baseBasalSum = 0; + var bolusSum = 0; var carbsSum = 0; var proteinSum = 0; var fatSum = 0; @@ -95,22 +98,28 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio }); var tddAverage = tddSum / datastorage.alldays; + var basalAveragePercent = Math.round( (basalSum / datastorage.alldays) / tddAverage * 100); + var baseBasalAveragePercent = Math.round( (baseBasalSum / datastorage.alldays) / tddAverage * 100); + var bolusAveragePercent = Math.round( (bolusSum / datastorage.alldays) / tddAverage * 100); var carbsAverage = carbsSum / datastorage.alldays; var proteinAverage = proteinSum / datastorage.alldays; var fatAverage = fatSum / datastorage.alldays; - if (options.insulindistribution) - $('#daytodaycharts').append('

' + - '' + translate('TDD average') + ': ' + tddAverage.toFixed(1) + 'U ' + - '' + translate('Carbs average') + ': ' + carbsAverage.toFixed(0) + 'g ' + - '' + translate('Protein average') + ': ' + proteinAverage.toFixed(0) + 'g ' + - '' + translate('Fat average') + ': ' + fatAverage.toFixed(0) + 'g' - ); - - function timeTicks (n, i) { - var t12 = [ - '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '' - , '12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am' + if (options.insulindistribution) { + var html = '

' + translate('TDD average') + ': ' + tddAverage.toFixed(1) + 'U  '; + html += '' + translate('Bolus average') + ': ' + bolusAveragePercent + '%  '; + html += '' + translate('Basal average') + ': ' + basalAveragePercent + '%  '; + html += '(' + translate('Base basal average:') + ' ' + baseBasalAveragePercent + '%)  '; + html += '' + translate('Carbs average') + ': ' + carbsAverage.toFixed(0) + 'g'; + html += '' + translate('Protein average') + ': ' + proteinAverage.toFixed(0) + 'g'; + html += '' + translate('Fat average') + ': ' + fatAverage.toFixed(0) + 'g'; + $('#daytodaycharts').append(html); + } + + function timeTicks(n,i) { + var t12 = [ + '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '', + '12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am' ]; if (Nightscout.client.settings.timeFormat === 24) { return ('00' + i).slice(-2); @@ -1024,6 +1033,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } tddSum += totalDailyInsulin; + basalSum += totalBasalInsulin; + baseBasalSum += baseBasalInsulin; + bolusSum += bolusInsulin; carbsSum += data.dailyCarbs; proteinSum += data.dailyProtein; fatSum += data.dailyFat; From 8194a872edf8a972564c919aad681111f8261d09 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Fri, 6 Dec 2019 12:44:14 +0200 Subject: [PATCH 123/134] Remove the leading 0 from the version, so we can comply to how version numbering should really work --- npm-shrinkwrap.json | 2 +- package.json | 2 +- swagger.json | 2 +- swagger.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f9281c2e088..4b6cfdcf9d8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.6-dev", + "version": "13.0.0-dev", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 25a96043401..73401ff8743 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.6-dev", + "version": "13.0.0-dev", "description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.", "license": "AGPL-3.0", "author": "Nightscout Team", diff --git a/swagger.json b/swagger.json index 726e79faee6..c960341d62b 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "0.12.6-dev", + "version": "13.0.0-dev", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" diff --git a/swagger.yaml b/swagger.yaml index f77aeb1ceef..e22809e4570 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ servers: info: title: Nightscout API description: Own your DData with the Nightscout API - version: 0.12.6-dev + version: 13.0.0-dev license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt' From 0ea36abe0a945f529d7fafb6abdbee0e0b9fec1d Mon Sep 17 00:00:00 2001 From: Harmjan Greving Date: Sat, 14 Dec 2019 15:09:56 +0100 Subject: [PATCH 124/134] Removed unsused var ic (#5288) From de70aa29fd008b0d633050f8bc4128189fd499f6 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sat, 14 Dec 2019 06:14:13 -0800 Subject: [PATCH 125/134] Use constants instead of number literals for http status codes (#5276) --- lib/api/notifications-api.js | 8 +++++--- lib/api/notifications-v2.js | 6 ++++-- lib/api3/index.js | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/api/notifications-api.js b/lib/api/notifications-api.js index f9810256ad8..d08a03a7ead 100644 --- a/lib/api/notifications-api.js +++ b/lib/api/notifications-api.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + function configure (app, wares, ctx) { var express = require('express') , api = express.Router( ) @@ -7,9 +9,9 @@ function configure (app, wares, ctx) { api.post('/notifications/pushovercallback', function (req, res) { if (ctx.pushnotify.pushoverAck(req.body)) { - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); } else { - res.sendStatus(500); + res.sendStatus(consts.HTTP_INTERNAL_ERROR); } }); @@ -21,7 +23,7 @@ function configure (app, wares, ctx) { var time = req.query.time && Number(req.query.time); console.info('got api ack, level: ', level, ', time: ', time, ', query: ', req.query); ctx.notifications.ack(level, group, time, true); - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); }); } diff --git a/lib/api/notifications-v2.js b/lib/api/notifications-v2.js index 38bc73e4a78..16eac1de975 100644 --- a/lib/api/notifications-v2.js +++ b/lib/api/notifications-v2.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + function configure (app, ctx) { var express = require('express') , api = express.Router( ) @@ -8,10 +10,10 @@ function configure (app, ctx) { api.post('/loop', ctx.authorization.isPermitted('notifications:loop:push'), function (req, res) { ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) { if (error) { - res.status(500).send(error) + res.status(consts.HTTP_INTERNAL_ERROR).send(error) console.log("error sending notification to Loop: ", error); } else { - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); } }); }); diff --git a/lib/api3/index.js b/lib/api3/index.js index d188a5ee537..5b1799c78fd 100644 --- a/lib/api3/index.js +++ b/lib/api3/index.js @@ -39,8 +39,8 @@ function configure (env, ctx) { limit: 1048576 * 50 }), function errorHandler (err, req, res, next) { console.error(err); - res.status(500).json({ - status: 500, + res.status(apiConst.HTTP.INTERNAL_ERROR).json({ + status: apiConst.HTTP.INTERNAL_ERROR, message: apiConst.MSG.HTTP_500_INTERNAL_ERROR }); if (next) { // we need 4th parameter next to behave like error handler, but we have to use it to prevent "unused variable" message @@ -81,7 +81,7 @@ function configure (env, ctx) { const opCtx = {app, ctx, env, req, res}; opCtx.auth = await security.authenticate(opCtx); await security.demandPermission(opCtx, 'api:entries:read'); - res.status(200).end(); + res.status(apiConst.HTTP.OK).end(); } catch (error) { console.error(error); } From a764412a88da30f28181a65848ee0535614ddc0b Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 14 Dec 2019 07:14:31 -0700 Subject: [PATCH 126/134] Updated release name and number (#5280) --- docs/plugins/googlehome-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index 83acea5055d..f4bddb4d9cb 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -25,7 +25,7 @@ To add Google Home support for your Nightscout site, here's what you need to do: ## Activate the Nightscout Google Home Plugin -1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 0.13 (VERSION_NAME)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/0.13) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 13.0 (Ketchup)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/13.0) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. 1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) ## Create Your DialogFlow Agent From 5cc19ae163349f0390e524dab53bd37124c4df93 Mon Sep 17 00:00:00 2001 From: Bluefox Date: Sat, 14 Dec 2019 15:29:32 +0100 Subject: [PATCH 127/134] Allow use cgm-remote-monitor as npm package. (#4847) To use cgm-remote-monitor as npm package, the main attribute is required. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 73401ff8743..b7b2dfa4106 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dev": "env-cmd ./my.env nodemon server.js 0.0.0.0", "prod": "env-cmd ./my.prod.env node server.js 0.0.0.0" }, + "main": "server.js", "config": { "blanket": { "pattern": [ From 04b47f6109d29b2ab5bf9d8c913e2ff1f1e88aa2 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Sat, 14 Dec 2019 16:07:47 +0000 Subject: [PATCH 128/134] Add 4 hour option to view (#5289) * Revert "change default back to 3HR" This reverts commit e60ff3aa9d6c6cb18fe8b414d95cef09e76350b6. * shorten HR to H to avoid line-wrapping on mobile * try larger label font: 35 instead of 25 (vs. 40) * add comma * deviceInfo.recent null check * default back to 3, w/ 4 as an option * re-add H to single-digit hours * * Don't use cache manifest all all for development * Add 4 hours option to view and change the labelling to remove language issues * Have page scale the minor pill widths more gracefully --- app.js | 3 + lib/language.js | 154 +++++++------------------------------------- static/css/main.css | 4 +- views/index.html | 19 ++++-- 4 files changed, 42 insertions(+), 138 deletions(-) diff --git a/app.js b/app.js index daa34aa7651..7c6f67b1ee1 100644 --- a/app.js +++ b/app.js @@ -247,10 +247,13 @@ function create (env, ctx) { app.locals.bundle = '/bundle'; + app.locals.mode = 'production'; + if (process.env.NODE_ENV === 'development') { console.log('Development mode'); + app.locals.mode = 'development'; app.locals.bundle = '/devbundle'; const webpack = require('webpack'); diff --git a/lib/language.js b/lib/language.js index bba7871b0ce..87fb871e1d1 100644 --- a/lib/language.js +++ b/lib/language.js @@ -4900,136 +4900,6 @@ function init() { ,zh_cn: '静音2小时' ,zh_tw: '靜音2小時' } - ,'2HR' : { - cs: '2hod' - ,de: '2h' - ,es: '2h' - ,fr: '2hr' - ,el: '2 ώρες' - ,he: 'שעתיים' - ,pt: '2h' - ,sv: '2tim' - ,ro: '2h' - ,bg: '2часа' - ,hr: '2h' - ,it: '2ORE' - ,ja: '2時間' - ,dk: '2t' - ,fi: '2h' - ,nb: '2t' - ,pl: '2h' - ,ru: '2ч' - ,sk: '2 hod' - ,nl: '2uur' - ,ko: '2시간' - ,tr: '2sa.' - ,zh_cn: '2小时' - ,zh_tw: '2小時' - } - ,'3HR' : { - cs: '3hod' - ,he: 'שלוש שעות' - ,de: '3h' - ,es: '3h' - ,fr: '3hr' - ,el: '3 ώρες' - ,pt: '3h' - ,sv: '3tim' - ,ro: '3h' - ,bg: '3часа' - ,hr: '3h' - ,it: '3ORE' - ,ja: '3時間' - ,dk: '3t' - ,fi: '3h' - ,nb: '3t' - ,pl: '3h' - ,ru: '3ч' - ,sk: '3 hod' - ,nl: '3uur' - ,ko: '3시간' - ,tr: '3sa.' - ,zh_cn: '3小时' - ,zh_tw: '3小時' - } - ,'6HR' : { - cs: '6hod' - ,he: 'שש שעות' - ,de: '6h' - ,es: '6h' - ,fr: '6hr' - ,el: '6 ώρες' - ,pt: '6h' - ,sv: '6tim' - ,ro: '6h' - ,bg: '6часа' - ,hr: '6h' - ,it: '6ORE' - ,ja: '6時間' - ,dk: '6t' - ,fi: '6h' - ,nb: '6t' - ,pl: '6h' - ,ru: '6ч' - ,sk: '6 hod' - ,nl: '6uur' - ,ko: '6시간' - ,tr: '6sa.' - ,zh_cn: '6小时' - ,zh_tw: '6小時' - } - ,'12HR' : { - cs: '12hod' - ,he: 'שתים עשרה שעות' - ,de: '12h' - ,es: '12h' - ,fr: '12hr' - ,el: '12 ώρες' - ,pt: '12h' - ,sv: '12t' - ,ro: '12h' - ,bg: '12часа' - ,hr: '12h' - ,it: '12ORE' - ,ja: '12時間' - ,dk: '12t' - ,fi: '12h' - ,nb: '12t' - ,pl: '12h' - ,ru: '12ч' - ,sk: '12 hod' - ,nl: '12uur' - ,ko: '12시간' - ,tr: '12sa.' - ,zh_cn: '12小时' - ,zh_tw: '12小時' - } - ,'24HR' : { - cs: '24hod' - ,he: 'עשרים וארבע שעות' - ,de: '24h' - ,es: '24h' - ,fr: '24hr' - ,el: '24 ώρες' - ,pt: '24h' - ,sv: '24tim' - ,ro: '24h' - ,bg: '24часа' - ,hr: '24h' - ,it: '24ORE' - ,ja: '24時間' - ,dk: '24t' - ,fi: '24h' - ,nb: '24t' - ,pl: '24h' - ,ru: '24ч' - ,sk: '24 hod' - ,nl: '24uur' - ,ko: '24시간' - ,tr: '24sa.' - ,zh_cn: '24小时' - ,zh_tw: '24小時' - } ,'Settings' : { cs: 'Nastavení' ,he: 'הגדרות' @@ -8684,6 +8554,30 @@ function init() { ,tr: 'İnsülin/Karbonhidrat oranı (I:C)' ,zh_cn: '碳水化合物系数(ICR)' } + ,'Hours:' : { + cs: 'Hodin:' + ,he: 'שעות:' + ,ro: 'Ore:' + ,el: 'ώρες:' + ,fr: 'Heures:' + ,de: 'Stunden:' + ,es: 'Horas:' + ,dk: 'Timer:' + ,sv: 'Timmar:' + ,nb: 'Timer:' + ,fi: 'Tunnit:' + ,bg: 'часове:' + ,hr: 'Sati:' + ,pl: 'Godziny:' + ,pt: 'Horas:' + ,ru: 'час:' + ,sk: 'Hodiny:' + ,nl: 'Uren:' + ,ko: '시간:' + ,it: 'Ore:' + ,tr: 'Saat:' + ,zh_cn: '小时:' + } ,'hours' : { cs: 'hodin' ,he: 'שעות ' diff --git a/static/css/main.css b/static/css/main.css index 78cf7ae4c97..7c2e25837cb 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -436,9 +436,9 @@ a, a:visited, a:link { font-size: 12px; } -@media (max-width: 800px) { +@media (max-width: 1000px) { .bgStatus { - width: 300px; + width: 450px; } .bgButton { diff --git a/views/index.html b/views/index.html index 7690dcc848c..b80e11ddd42 100644 --- a/views/index.html +++ b/views/index.html @@ -1,5 +1,7 @@ - + + manifest="appcache/nightscout-<%= locals.cachebuster %>.appcache" + <% } %>> @@ -150,14 +152,19 @@
+
+
    -
  • 2HR
  • -
  • 3HR
  • -
  • 6HR
  • -
  • 12HR
  • -
  • 24HR
  • +
  • Hours:
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 6
  • +
  • 12
  • +
  • 24
  • ...
+
From 51781f79218de6c0f011fafd9a5593bf1e7865c5 Mon Sep 17 00:00:00 2001 From: ireneusz-ptak <31506973+ireneusz-ptak@users.noreply.github.com> Date: Sat, 14 Dec 2019 17:28:06 +0100 Subject: [PATCH 129/134] [Clock] Fix incorrect delta calculation (#5286) * Fix !delta check for delta == 0 * Fixed delta comparison --- lib/client/clock-client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index 62c204a696d..324a5168693 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -40,10 +40,10 @@ client.render = function render (xhr) { let delta; xhr.forEach(element => { - if (element.sgv && !rec && !delta) { + if (element.sgv && !rec) { rec = element; } - else if (element.sgv && rec && !delta) { + else if (element.sgv && rec && delta==null) { delta = (rec.sgv - element.sgv)/((rec.date - element.date)/(5*60*1000)); } }); From 1d005f9204e4b9ebfb3f26e02a0b7d4141e4a58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cas=20Eli=C3=ABns?= Date: Sat, 14 Dec 2019 17:29:59 +0100 Subject: [PATCH 130/134] Hide unit suffix for SMBs issued by AndroidAPS (#4771) --- lib/client/renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 5a0354afc51..f058d3de860 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -569,7 +569,7 @@ function init (client, d3) { var unit_of_measurement = ' U'; // One international unit of insulin (1 IU) is shown as '1 U' var enteredBy = '' + treatment.enteredBy; - if (treatment.insulin < 1 && !treatment.carbs && enteredBy.indexOf('openaps') > -1) { // don't show the unit of measurement for insulin boluses < 1 without carbs (e.g. oref0 SMB's). Otherwise lot's of small insulin only dosages are often unreadable + if ((treatment.insulin < 1 && !treatment.carbs && enteredBy.indexOf('openaps') > -1) || treatment.isSMB) { // don't show the unit of measurement for insulin boluses < 1 without carbs (e.g. oref0 SMB's). Otherwise lot's of small insulin only dosages are often unreadable unit_of_measurement = ''; // remove leading zeros to avoid overlap with adjacent boluses dosage_units = (dosage_units + "").replace(/^0/, ""); From 13758709a142e3c071465d6d33cb7c5ed46b56fb Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Sat, 14 Dec 2019 16:31:54 +0000 Subject: [PATCH 131/134] Fix issues from `npm audit` --- npm-shrinkwrap.json | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4b6cfdcf9d8..cdbfe3aa901 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2415,9 +2415,9 @@ }, "dependencies": { "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2436,9 +2436,9 @@ } }, "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -2678,9 +2678,9 @@ } }, "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" }, "chrome-trace-event": { "version": "1.0.2", @@ -3180,9 +3180,9 @@ } }, "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, "d3": { "version": "5.12.0", @@ -8890,11 +8890,11 @@ "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "requires": { - "cyclist": "~0.2.2", + "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } @@ -9870,9 +9870,9 @@ } }, "serialize-javascript": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.8.0.tgz", - "integrity": "sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" }, "serve-static": { "version": "1.14.1", @@ -10765,9 +10765,9 @@ } }, "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "string-width": { "version": "4.1.0", @@ -11002,15 +11002,15 @@ } }, "terser-webpack-plugin": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", - "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", + "serialize-javascript": "^2.1.2", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -11063,9 +11063,9 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "terser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.0.tgz", - "integrity": "sha512-6lPt7lZdZ/13icQJp8XasFOwZjFJkxFFIb/N1fhYEQNoNI3Ilo3KABZ9OocZvZoB39r6SiIk/0+v/bt8nZoSeA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.2.tgz", + "integrity": "sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", From 83c22f92ab2268396e0348dc398986f6890a562d Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Sun, 15 Dec 2019 16:13:53 +0200 Subject: [PATCH 132/134] Read BG targets in using mmol when server is in mmol mode (#5291) * Read BG targets in using mmol when server is in mmol mode * Change the Heroku template to match the new logic * Deal with mmol targets being set using commas --- README.md | 8 ++++---- app.json | 8 ++++---- lib/settings.js | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 88ad92ba017..f4a0452dd9d 100644 --- a/README.md +++ b/README.md @@ -241,10 +241,10 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or These alarm setting affect all delivery methods (browser, Pushover, IFTTT, etc.). Values and settings entered here will be the defaults for new browser views, but will be overridden if different choices are made in the settings UI. * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's - * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent - * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart - * `BG_TARGET_BOTTOM` (`80`) - must be set using mg/dl units; the bottom of the target range, also used to draw the line on the chart - * `BG_LOW` (`55`) - must be set using mg/dl units; the low BG outside the target range that is considered urgent + * `BG_HIGH` (`260`) - the high BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_TOP` (`180`) - the top of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_BOTTOM` (`80`) - the bottom of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_LOW` (`55`) - the low BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) * `ALARM_URGENT_HIGH` (`on`) - possible values `on` or `off` * `ALARM_URGENT_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent high alarms, space separated for options in browser, first used for pushover * `ALARM_HIGH` (`on`) - possible values `on` or `off` diff --git a/app.json b/app.json index 65d07d318a1..3d86d59bfa2 100644 --- a/app.json +++ b/app.json @@ -53,22 +53,22 @@ "required": true }, "BG_HIGH": { - "description": "Urgent High BG threshold, triggers the ALARM_URGENT_HIGH alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Urgent High BG threshold, triggers the ALARM_URGENT_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "260", "required": false }, "BG_LOW": { - "description": "Urgent Low BG threshold, triggers the ALARM_URGENT_LOW alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Urgent Low BG threshold, triggers the ALARM_URGENT_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "55", "required": false }, "BG_TARGET_BOTTOM": { - "description": "Low BG threshold, triggers the ALARM_LOW alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Low BG threshold, triggers the ALARM_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "80", "required": false }, "BG_TARGET_TOP": { - "description": "High BG threshold, triggers the ALARM_HIGH alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "High BG threshold, triggers the ALARM_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "180", "required": false }, diff --git a/lib/settings.js b/lib/settings.js index 5024157e70d..c2497ed66d5 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -2,11 +2,12 @@ var _ = require('lodash'); var levels = require('./levels'); +var constants = require('./constants.json'); function init () { var settings = { - units: 'mg/dL' + units: 'mg/dl' , timeFormat: 12 , nightMode: false , editMode: true @@ -73,6 +74,10 @@ function init () { , deNormalizeDates: mapTruthy , showClockDelta: mapTruthy , showClockLastTime: mapTruthy + , bgHigh: mapNumber + , bgLow: mapNumber + , bgTargetTop: mapNumber + , bgTargetBottom: mapNumber }; function mapNumberArray (value) { @@ -95,6 +100,11 @@ function init () { return value; } + if (typeof value === 'string' && isNaN(value)) { + const decommaed = value.replace(',','.'); + if (!isNaN(decommaed)) { value = decommaed; } + } + if (isNaN(value)) { return value; } else { @@ -210,6 +220,14 @@ function init () { thresholds.bgTargetBottom = Number(thresholds.bgTargetBottom); thresholds.bgLow = Number(thresholds.bgLow); + // Do not convert for old installs that have these set in mg/dl + if (settings.units.toLowerCase().includes('mmol') && thresholds.bgHigh < 50) { + thresholds.bgHigh = Math.round(thresholds.bgHigh * constants.MMOL_TO_MGDL); + thresholds.bgTargetTop = Math.round(thresholds.bgTargetTop * constants.MMOL_TO_MGDL); + thresholds.bgTargetBottom = Math.round(thresholds.bgTargetBottom * constants.MMOL_TO_MGDL); + thresholds.bgLow = Math.round(thresholds.bgLow * constants.MMOL_TO_MGDL); + } + verifyThresholds(); adjustShownPlugins(); } From 1312256673f03245fb4c9cb88ae83d74a177c246 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Mon, 16 Dec 2019 19:22:26 +0200 Subject: [PATCH 133/134] Unbreak JSON serialization failing on the /properties API due to circular data reference --- lib/plugins/cob.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 198780c2926..c6d4c4fdf8f 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -49,9 +49,9 @@ function init (ctx) { var treatmentCOB = (treatments !== undefined && treatments.length) ? cob.fromTreatments(treatments, devicestatus, profile, time, spec_profile) : {}; - result = treatmentCOB; + result = _.cloneDeep(treatmentCOB); result.source = 'Care Portal'; - result.treatmentCOB = treatmentCOB; + result.treatmentCOB = _.cloneDeep(treatmentCOB); } return addDisplay(result); From 07b528cf30a81007cb5035bd852d71e9f90c7bd1 Mon Sep 17 00:00:00 2001 From: Sulka Haro Date: Mon, 16 Dec 2019 20:28:41 +0200 Subject: [PATCH 134/134] Bump version to 13.0.0 --- npm-shrinkwrap.json | 2 +- package.json | 2 +- swagger.json | 2 +- swagger.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cdbfe3aa901..99dbaa768e3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "13.0.0-dev", + "version": "13.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b7b2dfa4106..88e92a6f4ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "13.0.0-dev", + "version": "13.0.0", "description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.", "license": "AGPL-3.0", "author": "Nightscout Team", diff --git a/swagger.json b/swagger.json index c960341d62b..385f73c068f 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "13.0.0-dev", + "version": "13.0.0", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" diff --git a/swagger.yaml b/swagger.yaml index e22809e4570..bdb74b652b9 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ servers: info: title: Nightscout API description: Own your DData with the Nightscout API - version: 13.0.0-dev + version: 13.0.0 license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt'