From 30b88f67650afa5482fad3f1024f995a2cf6e2d6 Mon Sep 17 00:00:00 2001 From: Caleb Date: Mon, 21 Oct 2019 05:09:49 -0700 Subject: [PATCH] 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); });