diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9aa8624..be636f64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -197,7 +197,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | 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 | @@ -212,7 +212,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 | @@ -221,9 +221,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 | @@ -240,7 +240,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 b283e415..88ad92ba 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Community maintained fork of the - [`override` (Override Mode)](#override-override-mode) - [`xdripjs` (xDrip-js)](#xdripjs-xdripjs) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) @@ -511,6 +512,9 @@ 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 00000000..60ac1d19 --- /dev/null +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -0,0 +1,62 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Adding Virtual Assistant Support to a Plugin](#adding-virtual-assistant-support-to-a-plugin) + - [Intent Handlers](#intent-handlers) + - [Rollup handlers](#rollup-handlers) + + + +Adding Virtual Assistant Support to a Plugin +========================================= + +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 can supply: +* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### 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 a5dcb886..4e298df4 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -13,9 +13,9 @@ - [Test your skill out with the test tool](#test-your-skill-out-with-the-test-tool) - [What questions can you ask it?](#what-questions-can-you-ask-it) - [Activate the skill on your Echo or other device](#activate-the-skill-on-your-echo-or-other-device) + - [Updating your skill with new features](#updating-your-skill-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) - [Adding Alexa support to a plugin](#adding-alexa-support-to-a-plugin) - - [Intent Handlers](#intent-handlers) - - [Rollup handlers](#rollup-handlers) @@ -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/console/ask). ### 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. +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/). -```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" - } - } - ] - } - ] - } - } -} -``` +- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. -Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". @@ -238,112 +85,71 @@ Click on the "Test" tab on the top menu. This will take you to the page where yo Enable testing for your skill (click the toggle). As indicated on this page, when testing is enabled, you can interact with the development version of your skill in the Alexa simulator and on all devices linked to your Alexa developer account. (Your skill will always be a development version. There's no need to publish it to the public.) -After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. +After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. When typing your test question, only type what you would verbally say to an Alexa device after the wake word. (e.g. you would verbally say "Alexa, ask Nightscout how am I doing", so you would type only "ask Nightscout how am I doing") You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. ##### What questions can you ask it? -*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. +## Updating your skill with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Alexa may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your Alexa skill. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Open [the latest skill template](alexa-templates/) in your language. You'll be copying the contents of the file later. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). +1. Open your Nightscout skill. +1. Open the "JSON Editor" in the left navigation pane. +1. Select everything in the text box (Ctrl + A on Windows, Cmd + A on Mac) and delete it. +1. Copy the contents of the updated template and paste it in the text box in the JSON Editor page. +1. Click the "Save Model" button near the top of the page, and then click the "Build Model" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you gave your skill name something other than "night scout," you will need to go to the "Invocation" page in the left navigation pane and change the Skill Invocation Name back to your preferred name. Make sure to click the "Save Model" button followed by the "Build Model" button after you change the name. +1. Enjoy the new features! + +## Adding support for additional languages + +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 00000000..4cb10aa0 --- /dev/null +++ b/docs/plugins/alexa-templates/en-us.json @@ -0,0 +1,222 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "night scout", + "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": [] + }, + { + "name": "AMAZON.StopIntent", + "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 00000000..6a8498b0 Binary files /dev/null and b/docs/plugins/google-home-templates/en-us.zip differ diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md new file mode 100644 index 00000000..83acea50 --- /dev/null +++ b/docs/plugins/googlehome-plugin.md @@ -0,0 +1,140 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout Google Home/DialogFlow Plugin](#nightscout-google-homedialogflow-plugin) + - [Overview](#overview) + - [Activate the Nightscout Google Home Plugin](#activate-the-nightscout-google-home-plugin) + - [Create Your DialogFlow Agent](#create-your-dialogflow-agent) + - [What questions can you ask it?](#what-questions-can-you-ask-it) + - [Updating your agent with new features](#updating-your-agent-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) + - [Adding Google Home support to a plugin](#adding-google-home-support-to-a-plugin) + + + +Nightscout Google Home/DialogFlow Plugin +======================================== + +## 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. + +## Updating your agent with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Google Home may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your DialogFlow agent. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Download [the latest skill template](google-home-templates/) in your language. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [DialogFlow developer portal](https://dialogflow.cloud.google.com/). +1. Make sure you're viewing your Nightscout agent (there's a drop-down box immediately below the DialogFlow logo where you can select your agent). +1. Click on the gear icon next to your agent name, then click on the "Export and Import" tab. +1. Click the "RESTORE FROM ZIP" button. +1. Select the template file you downloaded earlier, then type "RESTORE" in the text box as requested, and click the "RESTORE" button. +1. After the import is completed, click the "DONE" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. Enjoy the new features! + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +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 00000000..984a876f --- /dev/null +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -0,0 +1,67 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Interacting with Virtual Assistants](#interacting-with-virtual-assistants) +- [Alexa vs. Google Home](#alexa-vs-google-home) +- [What questions can you ask it?](#what-questions-can-you-ask-it) + - [A note about names](#a-note-about-names) + + + +Interacting with Virtual Assistants +=================================== + +# 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 65f477ad..337ec00f 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -4,156 +4,176 @@ 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; + var entries = ctx.entries; + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.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}); + 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'); + } + }); - ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = req.body.request.locale; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + switch (req.body.request.type) { + case 'IntentRequest': + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); }); + break; + case 'LaunchRequest': + onLaunch(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + next( ); + }); + break; + case 'SessionEndedRequest': + onSessionEnded(req.body.request.intent, function (alexaResponse) { + res.json(alexaResponse); + next( ); + }); + break; + } + }); + + ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('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(translate('virtAsstTitleCurrentBG'), status); + }); + }, ['bg', 'blood glucose', 'number']); + + ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { + callback(translate('virtAsstTitleFullStatus'), status); + }); + }); - function onLaunch() { - console.log('Session launched'); + function onLaunch(intent, next) { + console.log('Session launched'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } + + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } + + function onSessionEnded() { + console.log('Session ended'); + } + + function handleIntent(intentName, slots, next) { + var metric; + if (slots) { + if (slots.metric + && slots.metric.resolutions + && slots.metric.resolutions.resolutionsPerAuthority + && slots.metric.resolutions.resolutionsPerAuthority.length + && slots.metric.resolutions.resolutionsPerAuthority[0].status + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code == "ER_SUCCESS_MATCH" + && slots.metric.resolutions.resolutionsPerAuthority[0].values + && slots.metric.resolutions.resolutionsPerAuthority[0].values.length + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name + ){ + metric = slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + } } - function onIntent(intent, next) { - console.log('Received intent request'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); + var handler = ctx.alexa.getIntentHandler(intentName, metric); + if (handler){ + var sbx = initializeSandbox(); + handler(next, slots, sbx); + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); } + } - function onSessionEnded() { - console.log('Session ended'); - } + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - function handleIntent(intentName, slots, next) { - var handler = ctx.alexa.getIntentHandler(intentName, slots); - if (handler){ - var sbx = initializeSandbox(); - handler(next, slots, sbx); - } else { - next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); - } - } - - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } - - 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 00000000..2b2caa2a --- /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(translate('virtAsstTitleCurrentBG'), status); + }); + }, ['bg', 'blood glucose', 'number']); + + ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { + callback(translate('virtAsstTitleFullStatus'), 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 f92dda9c..4b3d6a4f 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 6800ff50..9a3e5843 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' @@ -13271,7 +13346,371 @@ 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.' + }, + 'virtAsstTitleAR2Forecast': { + bg: 'AR2 Forecast' + , cs: 'AR2 Forecast' + , de: 'AR2 Forecast' + , dk: 'AR2 Forecast' + , el: 'AR2 Forecast' + , en: 'AR2 Forecast' + , es: 'AR2 Forecast' + , fi: 'AR2 Forecast' + , fr: 'AR2 Forecast' + , he: 'AR2 Forecast' + , hr: 'AR2 Forecast' + , it: 'AR2 Forecast' + , ko: 'AR2 Forecast' + , nb: 'AR2 Forecast' + , pl: 'AR2 Forecast' + , pt: 'AR2 Forecast' + , ro: 'AR2 Forecast' + , nl: 'AR2 Forecast' + , ru: 'AR2 Forecast' + , sk: 'AR2 Forecast' + , sv: 'AR2 Forecast' + , tr: 'AR2 Forecast' + , zh_cn: 'AR2 Forecast' + , zh_tw: 'AR2 Forecast' + }, + 'virtAsstTitleCurrentBasal': { + bg: 'Current Basal' + , cs: 'Current Basal' + , de: 'Current Basal' + , dk: 'Current Basal' + , el: 'Current Basal' + , en: 'Current Basal' + , es: 'Current Basal' + , fi: 'Current Basal' + , fr: 'Current Basal' + , he: 'Current Basal' + , hr: 'Current Basal' + , it: 'Current Basal' + , ko: 'Current Basal' + , nb: 'Current Basal' + , pl: 'Current Basal' + , pt: 'Current Basal' + , ro: 'Current Basal' + , nl: 'Current Basal' + , ru: 'Current Basal' + , sk: 'Current Basal' + , sv: 'Current Basal' + , tr: 'Current Basal' + , zh_cn: 'Current Basal' + , zh_tw: 'Current Basal' + }, + 'virtAsstTitleCurrentCOB': { + bg: 'Current COB' + , cs: 'Current COB' + , de: 'Current COB' + , dk: 'Current COB' + , el: 'Current COB' + , en: 'Current COB' + , es: 'Current COB' + , fi: 'Current COB' + , fr: 'Current COB' + , he: 'Current COB' + , hr: 'Current COB' + , it: 'Current COB' + , ko: 'Current COB' + , nb: 'Current COB' + , pl: 'Current COB' + , pt: 'Current COB' + , ro: 'Current COB' + , nl: 'Current COB' + , ru: 'Current COB' + , sk: 'Current COB' + , sv: 'Current COB' + , tr: 'Current COB' + , zh_cn: 'Current COB' + , zh_tw: 'Current COB' + }, + 'virtAsstTitleCurrentIOB': { + bg: 'Current IOB' + , cs: 'Current IOB' + , de: 'Current IOB' + , dk: 'Current IOB' + , el: 'Current IOB' + , en: 'Current IOB' + , es: 'Current IOB' + , fi: 'Current IOB' + , fr: 'Current IOB' + , he: 'Current IOB' + , hr: 'Current IOB' + , it: 'Current IOB' + , ko: 'Current IOB' + , nb: 'Current IOB' + , pl: 'Current IOB' + , pt: 'Current IOB' + , ro: 'Current IOB' + , nl: 'Current IOB' + , ru: 'Current IOB' + , sk: 'Current IOB' + , sv: 'Current IOB' + , tr: 'Current IOB' + , zh_cn: 'Current IOB' + , zh_tw: 'Current IOB' + }, + 'virtAsstTitleLoopForecast': { + bg: 'Loop Forecast' + , cs: 'Loop Forecast' + , de: 'Loop Forecast' + , dk: 'Loop Forecast' + , el: 'Loop Forecast' + , en: 'Loop Forecast' + , es: 'Loop Forecast' + , fi: 'Loop Forecast' + , fr: 'Loop Forecast' + , he: 'Loop Forecast' + , hr: 'Loop Forecast' + , it: 'Loop Forecast' + , ko: 'Loop Forecast' + , nb: 'Loop Forecast' + , pl: 'Loop Forecast' + , pt: 'Loop Forecast' + , ro: 'Loop Forecast' + , nl: 'Loop Forecast' + , ru: 'Loop Forecast' + , sk: 'Loop Forecast' + , sv: 'Loop Forecast' + , tr: 'Loop Forecast' + , zh_cn: 'Loop Forecast' + , zh_tw: 'Loop Forecast' + }, + 'virtAsstTitleLastLoop': { + bg: 'Last Loop' + , cs: 'Last Loop' + , de: 'Last Loop' + , dk: 'Last Loop' + , el: 'Last Loop' + , en: 'Last Loop' + , es: 'Last Loop' + , fi: 'Last Loop' + , fr: 'Last Loop' + , he: 'Last Loop' + , hr: 'Last Loop' + , it: 'Last Loop' + , ko: 'Last Loop' + , nb: 'Last Loop' + , pl: 'Last Loop' + , pt: 'Last Loop' + , ro: 'Last Loop' + , nl: 'Last Loop' + , ru: 'Last Loop' + , sk: 'Last Loop' + , sv: 'Last Loop' + , tr: 'Last Loop' + , zh_cn: 'Last Loop' + , zh_tw: 'Last Loop' + }, + 'virtAsstTitleOpenAPSForecast': { + bg: 'OpenAPS Forecast' + , cs: 'OpenAPS Forecast' + , de: 'OpenAPS Forecast' + , dk: 'OpenAPS Forecast' + , el: 'OpenAPS Forecast' + , en: 'OpenAPS Forecast' + , es: 'OpenAPS Forecast' + , fi: 'OpenAPS Forecast' + , fr: 'OpenAPS Forecast' + , he: 'OpenAPS Forecast' + , hr: 'OpenAPS Forecast' + , it: 'OpenAPS Forecast' + , ko: 'OpenAPS Forecast' + , nb: 'OpenAPS Forecast' + , pl: 'OpenAPS Forecast' + , pt: 'OpenAPS Forecast' + , ro: 'OpenAPS Forecast' + , nl: 'OpenAPS Forecast' + , ru: 'OpenAPS Forecast' + , sk: 'OpenAPS Forecast' + , sv: 'OpenAPS Forecast' + , tr: 'OpenAPS Forecast' + , zh_cn: 'OpenAPS Forecast' + , zh_tw: 'OpenAPS Forecast' + }, + 'virtAsstTitlePumpReservoir': { + bg: 'Insulin Remaining' + , cs: 'Insulin Remaining' + , de: 'Insulin Remaining' + , dk: 'Insulin Remaining' + , el: 'Insulin Remaining' + , en: 'Insulin Remaining' + , es: 'Insulin Remaining' + , fi: 'Insulin Remaining' + , fr: 'Insulin Remaining' + , he: 'Insulin Remaining' + , hr: 'Insulin Remaining' + , it: 'Insulin Remaining' + , ko: 'Insulin Remaining' + , nb: 'Insulin Remaining' + , pl: 'Insulin Remaining' + , pt: 'Insulin Remaining' + , ro: 'Insulin Remaining' + , nl: 'Insulin Remaining' + , ru: 'Insulin Remaining' + , sk: 'Insulin Remaining' + , sv: 'Insulin Remaining' + , tr: 'Insulin Remaining' + , zh_cn: 'Insulin Remaining' + , zh_tw: 'Insulin Remaining' + }, + 'virtAsstTitlePumpBattery': { + bg: 'Pump Battery' + , cs: 'Pump Battery' + , de: 'Pump Battery' + , dk: 'Pump Battery' + , el: 'Pump Battery' + , en: 'Pump Battery' + , es: 'Pump Battery' + , fi: 'Pump Battery' + , fr: 'Pump Battery' + , he: 'Pump Battery' + , hr: 'Pump Battery' + , it: 'Pump Battery' + , ko: 'Pump Battery' + , nb: 'Pump Battery' + , pl: 'Pump Battery' + , pt: 'Pump Battery' + , ro: 'Pump Battery' + , nl: 'Pump Battery' + , ru: 'Pump Battery' + , sk: 'Pump Battery' + , sv: 'Pump Battery' + , tr: 'Pump Battery' + , zh_cn: 'Pump Battery' + , zh_tw: 'Pump Battery' + }, + 'virtAsstTitleRawBG': { + bg: 'Current Raw BG' + , cs: 'Current Raw BG' + , de: 'Current Raw BG' + , dk: 'Current Raw BG' + , el: 'Current Raw BG' + , en: 'Current Raw BG' + , es: 'Current Raw BG' + , fi: 'Current Raw BG' + , fr: 'Current Raw BG' + , he: 'Current Raw BG' + , hr: 'Current Raw BG' + , it: 'Current Raw BG' + , ko: 'Current Raw BG' + , nb: 'Current Raw BG' + , pl: 'Current Raw BG' + , pt: 'Current Raw BG' + , ro: 'Current Raw BG' + , nl: 'Current Raw BG' + , ru: 'Current Raw BG' + , sk: 'Current Raw BG' + , sv: 'Current Raw BG' + , tr: 'Current Raw BG' + , zh_cn: 'Current Raw BG' + , zh_tw: 'Current Raw BG' + }, + 'virtAsstTitleUploaderBattery': { + bg: 'Uploader Battery' + , cs: 'Uploader Battery' + , de: 'Uploader Battery' + , dk: 'Uploader Battery' + , el: 'Uploader Battery' + , en: 'Uploader Battery' + , es: 'Uploader Battery' + , fi: 'Uploader Battery' + , fr: 'Uploader Battery' + , he: 'Uploader Battery' + , hr: 'Uploader Battery' + , it: 'Uploader Battery' + , ko: 'Uploader Battery' + , nb: 'Uploader Battery' + , pl: 'Uploader Battery' + , pt: 'Uploader Battery' + , ro: 'Uploader Battery' + , nl: 'Uploader Battery' + , ru: 'Uploader Battery' + , sk: 'Uploader Battery' + , sv: 'Uploader Battery' + , tr: 'Uploader Battery' + , zh_cn: 'Uploader Battery' + , zh_tw: 'Uploader Battery' + }, + 'virtAsstTitleCurrentBG': { + bg: 'Current BG' + , cs: 'Current BG' + , de: 'Current BG' + , dk: 'Current BG' + , el: 'Current BG' + , en: 'Current BG' + , es: 'Current BG' + , fi: 'Current BG' + , fr: 'Current BG' + , he: 'Current BG' + , hr: 'Current BG' + , it: 'Current BG' + , ko: 'Current BG' + , nb: 'Current BG' + , pl: 'Current BG' + , pt: 'Current BG' + , ro: 'Current BG' + , nl: 'Current BG' + , ru: 'Current BG' + , sk: 'Current BG' + , sv: 'Current BG' + , tr: 'Current BG' + , zh_cn: 'Current BG' + , zh_tw: 'Current BG' + }, + 'virtAsstTitleFullStatus': { + bg: 'Full Status' + , cs: 'Full Status' + , de: 'Full Status' + , dk: 'Full Status' + , el: 'Full Status' + , en: 'Full Status' + , es: 'Full Status' + , fi: 'Full Status' + , fr: 'Full Status' + , he: 'Full Status' + , hr: 'Full Status' + , it: 'Full Status' + , ko: 'Full Status' + , nb: 'Full Status' + , pl: 'Full Status' + , pt: 'Full Status' + , ro: 'Full Status' + , nl: 'Full Status' + , ru: 'Full Status' + , sk: 'Full Status' + , sv: 'Full Status' + , tr: 'Full Status' + , zh_cn: 'Full Status' + , zh_tw: 'Full Status' + }, + 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' , de: '%1 und bis %3 %2.' @@ -13297,7 +13736,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' @@ -13323,7 +13762,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' @@ -13349,7 +13788,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.' @@ -13375,33 +13814,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' @@ -13427,7 +13866,7 @@ function init() { , zh_cn: '%1 单位' , zh_tw: '%1 units of' }, - 'alexaPreamble': { + 'virtAsstPreamble': { bg: 'Your' , cs: 'Vaše' , de: 'Deine' @@ -13453,7 +13892,7 @@ function init() { , zh_cn: '你的' , zh_tw: 'Your' }, - 'alexaPreamble3person': { + 'virtAsstPreamble3person': { bg: '%1 has a ' , cs: '%1 má ' , de: '%1 hat eine' @@ -13479,7 +13918,7 @@ function init() { , zh_cn: '%1 有一个 ' , zh_tw: '%1 has a ' }, - 'alexaNoInsulin': { + 'virtAsstNoInsulin': { bg: 'no' , cs: 'žádný' , de: 'kein' @@ -13505,75 +13944,92 @@ 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' + }, + 'virtAsstUploaderBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Your uploader battery is at %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Your uploader battery is at %1' + , dk: 'Your uploader battery is at %1' + , ko: 'Your uploader battery is at %1' + , nl: 'Your uploader battery is at %1' + , zh_cn: 'Your uploader battery is at %1' + , sv: 'Your uploader battery is at %1' + , fi: 'Your uploader battery is at %1' + , ro: 'Your uploader battery is at %1' + , pl: 'Your uploader battery is at %1' + , ru: 'Your uploader battery is at %1' + , tr: 'Your uploader battery is at %1' + }, + 'virtAsstLastLoop': { + bg: 'The last successful loop was %1' + , cs: 'Poslední úšpěšné provedení smyčky %1' + , 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' @@ -13582,7 +14038,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ă' @@ -13590,24 +14046,75 @@ function init() { , ru: 'плагин ЗЦ Loop не активирован ' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'alexaLoopForecast': { - bg: 'According to the loop forecast you are expected to be %1 over the next %2' - , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' - , en: 'According to the loop forecast you are expected to be %1 over the next %2' - , hr: 'According to the loop forecast you are expected to be %1 over the next %2' - , de: 'Entsprechend der Loop Vorhersage landest du bei %1 während der nächsten %2' - , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' - , ko: 'According to the loop forecast you are expected to be %1 over the next %2' - , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - ,zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' - , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' - , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' - , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' - , pl: 'Zgodnie z prognozą pętli, glikemia %1 będzie podczas następnego %2' - , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' - , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' + 'virtAsstLoopForecastAround': { + bg: 'According to the loop forecast you are expected to be around %1 over the next %2' + , cs: 'Podle přepovědi smyčky je očekávána glykémie around %1 během následujících %2' + , en: 'According to the loop forecast you are expected to be around %1 over the next %2' + , hr: 'According to the loop forecast you are expected to be around %1 over the next %2' + , de: 'Entsprechend der Loop Vorhersage landest du bei around %1 während der nächsten %2' + , dk: 'Ifølge Loops forudsigelse forventes du at blive around %1 i den næste %2' + , ko: 'According to the loop forecast you are expected to be around %1 over the next %2' + , nl: 'Volgens de Loop voorspelling is je waarde around %1 over de volgnede %2' + , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是around %1' + , sv: 'Enligt Loops förutsägelse förväntas du bli around %1 inom %2' + , fi: 'Ennusteen mukaan olet around %1 seuraavan %2 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază around %1 pentru următoarele %2' + , pl: 'Zgodnie z prognozą pętli, glikemia around %1 będzie podczas następnego %2' + , ru: 'по прогнозу алгоритма ЗЦ ожидается around %1 за последующие %2' + , tr: 'Döngü tahminine göre sonraki %2 ye göre around %1 olması bekleniyor' }, - 'alexaForecastUnavailable': { + 'virtAsstLoopForecastBetween': { + bg: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'Podle přepovědi smyčky je očekávána glykémie between %1 and %2 během následujících %3' + , en: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , de: 'Entsprechend der Loop Vorhersage landest du bei between %1 and %2 während der nächsten %3' + , dk: 'Ifølge Loops forudsigelse forventes du at blive between %1 and %2 i den næste %3' + , ko: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'Volgens de Loop voorspelling is je waarde between %1 and %2 over de volgnede %3' + , zh_cn: '根据loop的预测,在接下来的%3你的血糖将会是between %1 and %2' + , sv: 'Enligt Loops förutsägelse förväntas du bli between %1 and %2 inom %3' + , fi: 'Ennusteen mukaan olet between %1 and %2 seuraavan %3 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază between %1 and %2 pentru următoarele %3' + , pl: 'Zgodnie z prognozą pętli, glikemia between %1 and %2 będzie podczas następnego %3' + , ru: 'по прогнозу алгоритма ЗЦ ожидается between %1 and %2 за последующие %3' + , tr: 'Döngü tahminine göre sonraki %3 ye göre between %1 and %2 olması bekleniyor' + }, + 'virtAsstAR2ForecastAround': { + bg: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , cs: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , en: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , hr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , de: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , dk: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ko: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , nl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , zh_cn: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , sv: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , fi: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ro: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , pl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ru: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , tr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + }, + 'virtAsstAR2ForecastBetween': { + bg: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , en: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , de: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , dk: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ko: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , zh_cn: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , sv: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , fi: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ro: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , pl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ru: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , tr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + }, + 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' , en: 'Unable to forecast with the data that is available' @@ -13616,7 +14123,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' @@ -13624,14 +14131,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' @@ -13641,39 +14148,108 @@ 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': { - en: '%1 %2 carbohydrates on board' - , cs: '%1 %2 aktivních sachridů' - , de: '%1 %2 Gramm Kohlenhydrate wirkend.' - , dk: '%1 %2 gram aktive kulhydrater' - , ko: '%1 %2 carbohydrates on board' - , nl: '%1 %2 actieve koolhydraten' - ,zh_cn: '%1 %2 活性碳水化合物' - , sv: '%1 %2 gram aktiva kolhydrater' - , fi: '%1 %2 aktiivista hiilihydraattia' - , ro: '%1 %2 carbohidrați activi în corp' - ,bg: '%1 %2 carbohydrates on board' - ,hr: '%1 %2 carbohydrates on board' - , pl: '%1 %2 aktywnych węglowodanów' - , ru: '%1 $2 активных углеводов' - , tr: '%1 %2 aktif karbonhidrat' + 'virtAsstCob3person': { + bg: '%1 has $2 carbohydrates on board' + , cs: '%1 has $2 carbohydrates on board' + , de: '%1 has $2 carbohydrates on board' + , dk: '%1 has $2 carbohydrates on board' + , el: '%1 has $2 carbohydrates on board' + , en: '%1 has $2 carbohydrates on board' + , es: '%1 has $2 carbohydrates on board' + , fi: '%1 has $2 carbohydrates on board' + , fr: '%1 has $2 carbohydrates on board' + , he: '%1 has $2 carbohydrates on board' + , hr: '%1 has $2 carbohydrates on board' + , it: '%1 has $2 carbohydrates on board' + , ko: '%1 has $2 carbohydrates on board' + , nb: '%1 has $2 carbohydrates on board' + , nl: '%1 has $2 carbohydrates on board' + , pl: '%1 has $2 carbohydrates on board' + , pt: '%1 has $2 carbohydrates on board' + , ro: '%1 has $2 carbohydrates on board' + , ru: '%1 has $2 carbohydrates on board' + , sk: '%1 has $2 carbohydrates on board' + , sv: '%1 has $2 carbohydrates on board' + , tr: '%1 has $2 carbohydrates on board' + , zh_cn: '%1 has $2 carbohydrates on board' + , zh_tw: '%1 has $2 carbohydrates on board' + }, + 'virtAsstCob': { + bg: 'You have %1 carbohydrates on board' + , cs: 'You have %1 carbohydrates on board' + , de: 'You have %1 carbohydrates on board' + , dk: 'You have %1 carbohydrates on board' + , el: 'You have %1 carbohydrates on board' + , en: 'You have %1 carbohydrates on board' + , es: 'You have %1 carbohydrates on board' + , fi: 'You have %1 carbohydrates on board' + , fr: 'You have %1 carbohydrates on board' + , he: 'You have %1 carbohydrates on board' + , hr: 'You have %1 carbohydrates on board' + , it: 'You have %1 carbohydrates on board' + , ko: 'You have %1 carbohydrates on board' + , nb: 'You have %1 carbohydrates on board' + , nl: 'You have %1 carbohydrates on board' + , pl: 'You have %1 carbohydrates on board' + , pt: 'You have %1 carbohydrates on board' + , ro: 'You have %1 carbohydrates on board' + , ru: 'You have %1 carbohydrates on board' + , sk: 'You have %1 carbohydrates on board' + , sv: 'You have %1 carbohydrates on board' + , tr: 'You have %1 carbohydrates on board' + , zh_cn: 'You have %1 carbohydrates on board' + , zh_tw: 'You have %1 carbohydrates on board' + }, + 'virtAsstUnknownIntentTitle': { + en: 'Unknown Intent' + , cs: 'Unknown Intent' + , de: 'Unknown Intent' + , dk: 'Unknown Intent' + , ko: 'Unknown Intent' + , nl: 'Unknown Intent' + , zh_cn: 'Unknown Intent' + , sv: 'Unknown Intent' + , fi: 'Unknown Intent' + , ro: 'Unknown Intent' + , bg: 'Unknown Intent' + , hr: 'Unknown Intent' + , pl: 'Unknown Intent' + , ru: 'Unknown Intent' + , tr: 'Unknown Intent' + }, + 'virtAsstUnknownIntentText': { + en: 'I\'m sorry, I don\'t know what you\'re asking for.' + , cs: 'I\'m sorry, I don\'t know what you\'re asking for.' + , de: 'I\'m sorry, I don\'t know what you\'re asking for.' + , dk: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ko: 'I\'m sorry, I don\'t know what you\'re asking for.' + , nl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , zh_cn: 'I\'m sorry, I don\'t know what you\'re asking for.' + , sv: 'I\'m sorry, I don\'t know what you\'re asking for.' + , fi: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ro: 'I\'m sorry, I don\'t know what you\'re asking for.' + , bg: 'I\'m sorry, I don\'t know what you\'re asking for.' + , hr: 'I\'m sorry, I don\'t know what you\'re asking for.' + , pl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ru: 'I\'m sorry, I don\'t know what you\'re asking for.' + , tr: 'I\'m sorry, I don\'t know what you\'re asking for.' }, 'Fat [g]': { cs: 'Tuk [g]' diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index 38ae449c..da7bbdca 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -1,60 +1,58 @@ 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) { - 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) { + // This function retrieves a handler based on the intent name and metric requested. + alexa.getIntentHandler = function getIntentHandler(intentName, metric) { + if (metric === undefined) { + console.log('Looking for handler for intent \'' + intentName + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName].handler + ) { + console.log('Found!'); return intentHandlers[intentName].handler; } - return null; } else { - return null; + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName][metric] + && intentHandlers[intentName][metric].handler + ) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } } + console.log('Not found!'); + return null; }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { @@ -63,7 +61,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 +107,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 e25a2b36..52aec073 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -17,6 +17,7 @@ var AR2_COLOR = 'cyan'; // eslint-disable-next-line no-unused-vars function init (ctx) { + var translate = ctx.language.translate; var ar2 = { name: 'ar2' @@ -146,7 +147,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 +164,34 @@ 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)); - next('AR2 Forecast', response); + var response = ''; + if (min === max) { + response = translate('virtAsstAR2ForecastAround', { + params: [ + max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstAR2ForecastBetween', { + params: [ + min + , max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } + next(translate('virtAsstTitleAR2Forecast'), response); } else { - next('AR2 Forecast', 'AR2 plugin does not seem to be enabled'); + next(translate('virtAsstTitleAR2Forecast'), 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 a766f9ab..4347a700 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -113,16 +113,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, @@ -130,12 +130,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 @@ -145,30 +145,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) { - next('Current Basal', basalMessage(slots, sbx)); + function virtAsstCurrentBasalhandler (next, slots, sbx) { + next(translate('virtAsstTitleCurrentBasal'), 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 5020f2e6..198780c2 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -291,22 +291,31 @@ function init (ctx) { }); }; - function alexaCOBHandler (next, slots, sbx) { - var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; - var value = 'no'; - if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { - value = sbx.properties.cob.cob; + function virtAsstCOBHandler (next, slots, sbx) { + var response = ''; + var value = (sbx.properties.cob && sbx.properties.cob.cob) ? sbx.properties.cob.cob : 0; + if (slots && slots.pwd && slots.pwd.value) { + response = translate('virtAsstCob3person', { + params: [ + slots.pwd.value.replace('\'s', '') + , value + ] + }); + } else { + response = translate('virtAsstCob', { + params: [ + value + ] + }); } - var response = preamble + ' ' + value + ' carbohydrates on board'; - next('Current COB', response); + next(translate('virtAsstTitleCurrentCOB'), 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 00000000..8e818151 --- /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 f9bf082d..96bea03b 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); + callback(translate('virtAsstTitleCurrentIOB'), 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 82ed74ec..9b099dd1 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -9,6 +9,7 @@ var levels = require('../levels'); function init (ctx) { var utils = require('../utils')(ctx); + var translate = ctx.language.translate; var loop = { name: 'loop' @@ -506,7 +507,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 +517,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', 'Unable to forecast with the data that is available'); + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstForecastUnavailable')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -526,35 +527,52 @@ function init (ctx) { min = forecast[i]; } } - var value = ''; + var response = ''; if (min === max) { - value = 'around ' + max; + response = translate('virtAsstLoopForecastAround', { + params: [ + max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); } else { - value = 'between ' + min + ' and ' + max; + response = translate('virtAsstLoopForecastBetween', { + params: [ + min + , max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); } - var response = 'According to the loop forecast you are expected to be ' + value + ' over the next ' + moment(endPrediction).from(moment(sbx.time)); - next('Loop Forecast', response); + next(translate('virtAsstTitleLoopForecast'), response); } } else { - next('Loop forecast', 'Loop plugin does not seem to be enabled'); + next(translate('virtAsstTitleLoopForecast'), 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(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), 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 4d2535a3..59af42c0 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -517,36 +517,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); + next(translate('virtAsstTitleOpenAPSForecast'), response); + } else { + next(translate('virtAsstTitleOpenAPSForecast'), 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(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), 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 842d8536..7e71c21e 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -135,38 +135,58 @@ function init (ctx) { }); }; - function alexaReservoirHandler (next, slots, sbx) { - var response = translate('alexaReservoir', { + function virtAsstReservoirHandler (next, slots, sbx) { + var reservoir = sbx.properties.pump.pump.reservoir; + if (reservoir || reservoir === 0) { + var response = translate('virtAsstReservoir', { params: [ - sbx.properties.pump.pump.reservoir + reservoir ] - }); - next('Remaining insulin', response); + }); + next(translate('virtAsstTitlePumpReservoir'), response); + } else { + next(translate('virtAsstTitlePumpReservoir'), 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(translate('virtAsstTitlePumpBattery'), response); } else { - next(); + next(translate('virtAsstTitlePumpBattery'), 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 f19e669f..3248126b 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -106,17 +106,24 @@ 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 = translate('virtAsstRawBG', { + params: [ + sbx.properties.rawbg.mgdl + ] + }); + next(translate('virtAsstTitleRawBG'), response); + } else { + next(translate('virtAsstTitleRawBG'), 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 eda42a39..dc603054 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -4,7 +4,8 @@ var _ = require('lodash'); var times = require('../times'); var levels = require('../levels'); -function init() { +function init(ctx) { + var translate = ctx.language.translate; var upbat = { name: 'upbat' @@ -221,16 +222,32 @@ 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 = translate('virtAsstUploaderBattery', { + params: [ + sbx.properties.upbat.display + ] + }); + next(translate('virtAsstTitleUploaderBattery'), response); + } else { + next(translate('virtAsstTitleUploaderBattery'), 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 af5f954d..2bb63ad7 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 9dbf6de1..01f4f3d4 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 0bcfd3bc..fa97f842 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 dbbecda0..54fbcb6c 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 1ef86d82..3b92fb8d 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -11,7 +11,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: { @@ -21,14 +21,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 b62b39c1..bfe11d50 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -244,7 +244,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' @@ -256,14 +256,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 ce221c8f..b2e767c8 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -371,7 +371,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' @@ -383,14 +383,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) { - title.should.equal('Loop Forecast'); + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('OpenAPS 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 94e2de18..d051cbe7 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -255,7 +255,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' @@ -267,16 +267,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('Insulin Remaining'); 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('Insulin Remaining'); + 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 ab91d2bf..48c21186 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 9b48c3b8..42d18bb0 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%'); + + upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); + response.should.equal('Your uploader battery is at 20%'); - done(); + done(); + }, [], sbx); + }, [], sbx); });