-
Notifications
You must be signed in to change notification settings - Fork 71.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DialogFlow APIv2 Integration (GoogleHome) - dev #4035
Changes from all commits
7f3ab74
1b7f56a
051af65
1b09851
5da1217
736fa70
3d15c6b
7c272ec
b3f7d65
b632833
ca3f518
5523622
8a23db9
fa769b5
b2b8aad
39382aa
16a55c6
6e8a9b9
6d87b33
7ab3c27
f590a92
0bd9731
542e2e9
dd6c0b6
f0f601b
2a94037
be26a15
c4fa77d
a8e7d26
6ecb5a2
5a8b509
b435fae
bed31ef
cf22127
c046e86
8029ddf
577e18f
78d0188
2935268
04dae21
025561e
3f8f7a1
2e4f279
8a88010
5019a98
8f827cf
742e914
9cc12f9
163fff0
0ce7964
1f8d09a
38581d3
b544022
c72c944
50d3f29
73b2bd5
a353446
4ccb7b6
8a40bc6
10c4a0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
Nightscout GoogleHome - DialogFlow Plugin | ||
====================================== | ||
|
||
## Overview | ||
|
||
To add GoogleHome support for your Nightscout site, here's what you need to do: | ||
|
||
1. Activate the `googlehome` plugin on your Nightscout site, so your site will respond correctly to Google's requests. | ||
2. Create a custom DialogFlow agent that points at your site and defines certain questions you want to be able to ask. (You'll copy and paste a basic template for this, to keep things simple.) | ||
3. Create desired integrations with DialogFlow | ||
|
||
|
||
## Activate the Nightscout GoogleHome Plugin | ||
|
||
1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. . | ||
2. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) | ||
|
||
## Create Your DialogFlow Agent | ||
|
||
### Signin to DialogFlow | ||
|
||
- Sign in to DialogFlow with your Google account (https://console.dialogflow.com/api-client/#/login). If you don't already have one, signup with Google. | ||
|
||
### Create a new custom DialogFlow agent | ||
|
||
1. Select "Create new agent" in the main menu bar. | ||
2. Input a custom name for your agent and click "CREATE". | ||
3. Download the simple agent template : ( https://drive.google.com/drive/folders/18z2kQSEInvH4O_jfjB4Qh8z9508P9Oao?usp=sharing ) | ||
4. Select IMPORT FROM ZIP , in order to import the template. | ||
5. SAVE | ||
6. Go to "Fullfillment" menu and enter details about your webhook. | ||
7. SAVE | ||
8. Go to "Integration" menu and select your desired integration. | ||
9. Follow instructions for each desired integration. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
'use strict'; | ||
|
||
var _ = require('lodash'); | ||
var moment = require('moment'); | ||
|
||
function configure (app, wares, ctx, env) { | ||
var express = require('express'); | ||
var api = express.Router(); | ||
var entries = ctx.entries; | ||
var translate = ctx.language.translate; | ||
|
||
// invoke common middleware | ||
api.use(wares.sendJSONStatus); | ||
// text body types get handled as raw buffer stream | ||
api.use(wares.bodyParser.raw()); | ||
// json body types get handled as parsed json | ||
api.use(wares.bodyParser.json()); | ||
|
||
ctx.plugins.eachEnabledPlugin(function each(plugin) { | ||
if (plugin.googleHome) { | ||
if (plugin.googleHome.intentHandlers) { | ||
console.log('Plugin ' + plugin.name + ' is Google Home enabled'); | ||
_.each(plugin.googleHome.intentHandlers, function (handler) { | ||
if (handler) { | ||
ctx.googleHome.configureIntentHandler(handler.intent, handler.intentHandler, handler.routableSlot, handler.slots); | ||
} | ||
}); | ||
} | ||
} else { | ||
console.log('Plugin ' + plugin.name + ' is not Google Home enabled'); | ||
} | ||
}); | ||
|
||
ctx.googleHome.configureIntentHandler('CurrentMetric', function (result, next, sbx) { | ||
entries.list({count: 1}, function(err, records) { | ||
var response = ''; | ||
if (records && records.length > 0) { | ||
var direction = ''; | ||
if (records[0].direction === 'FortyFiveDown') { | ||
direction = ' and slightly dropping'; | ||
} else if (records[0].direction === 'FortyFiveUp') { | ||
direction = ' and slightly rising'; | ||
} else if (records[0].direction === 'Flat') { | ||
direction = ' and holding'; | ||
} else if (records[0].direction === 'SingleUp') { | ||
direction = ' and rising'; | ||
} else if (records[0].direction === 'SingleDown') { | ||
direction = ' and dropping'; | ||
} else if (records[0].direction === 'DoubleDown') { | ||
direction = ' and rapidly dropping'; | ||
} else if (records[0].direction === 'DoubleUp') { | ||
direction = ' and rapidly rising'; | ||
} | ||
response = buildPreamble(result.parameters); | ||
response += sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); | ||
} else { | ||
response = buildPreamble(result.parameters) + 'unknown'; | ||
} | ||
next(response); | ||
}); | ||
}, 'metric', ['bg', 'blood glucose', 'blood sugar', 'number']); | ||
|
||
api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { | ||
console.log('Incoming request from Google Home'); | ||
onIntent(req.body, function (response) { | ||
res.json(ctx.googleHome.buildResponse(response)); | ||
next(); | ||
}); | ||
}); | ||
|
||
function buildPreamble(parameters) { | ||
var preamble = ''; | ||
if (parameters && parameters.givenName) { | ||
preamble = parameters.givenName + '\'s current '; | ||
} else { | ||
preamble = 'Your current '; | ||
} | ||
if (parameters && parameters.readingType) { | ||
preamble += parameters.readingType + ' is '; | ||
} else { | ||
preamble += 'blood glucose is '; | ||
} | ||
return preamble; | ||
} | ||
|
||
function onIntent(body, next) { | ||
console.log('Received intent request'); | ||
console.log(JSON.stringify(body)); | ||
handleIntent(body, next); | ||
} | ||
|
||
// https://docs.api.ai/docs/webhook#section-format-of-request-to-the-service | ||
function handleIntent(body, next) { | ||
var displayName = body.queryResult.intent.displayName; | ||
var metric = body.queryResult.parameters ? body.queryResult.parameters.metric : null; | ||
var handler = ctx.googleHome.getIntentHandler(displayName, metric); | ||
if (handler) { | ||
var sbx = initializeSandbox(); | ||
handler(body.queryResult, next, sbx); | ||
} else { | ||
next('I\'m sorry I don\'t know what you\'re asking for'); | ||
} | ||
} | ||
|
||
function initializeSandbox() { | ||
var sbx = require('../../sandbox')(); | ||
sbx.serverInit(env, ctx); | ||
ctx.plugins.setProperties(sbx); | ||
return sbx; | ||
} | ||
|
||
return api; | ||
} | ||
|
||
module.exports = configure; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -310,6 +310,25 @@ function init(ctx) { | |
, intentHandler: alexaCOBHandler | ||
}] | ||
}; | ||
|
||
function googleHomeCOBHandler(result, next, sbx) { | ||
var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has' : 'You have'; | ||
var value = 'no'; | ||
if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { | ||
value = Math.round(sbx.properties.cob.cob); | ||
} | ||
var response = preamble + ' ' + value + ' carbohydrates on board'; | ||
next(response); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder how we could change this code (and the Alexa implementation) so that COB plugin would really only have the COB features and any other plugin that needs COB data would contain the COB processing in the plugin itself? I actually failed the Alexa code review - we should aim for clean separation of concerns and cob.js containing any code that has to do with Alexa and Home is architecturally nasty. Alternatively we could have a single method call here that outputs a speech synthesis friendly string, that gets reused across Home and Alexa |
||
|
||
cob.googleHome = { | ||
intentHandlers: [{ | ||
intent: 'CurrentMetric' | ||
, routableSlot:'metric' | ||
, slots:['cob', 'carbs on board', 'carbohydrates on board', 'carbohydrates'] | ||
, intentHandler: googleHomeCOBHandler | ||
}] | ||
}; | ||
|
||
return cob; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
|
||
function init(env, ctx) { | ||
|
||
console.log('Configuring Google Home...'); | ||
|
||
function googleHome() { | ||
return googleHome; | ||
} | ||
|
||
var intentHandlers = {}; | ||
|
||
googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { | ||
if (!intentHandlers[intent]) { | ||
intentHandlers[intent] = {}; | ||
} | ||
if (routableSlot && slotValues) { | ||
for (var i = 0, len = slotValues.length; i < len; i++) { | ||
if (!intentHandlers[intent][routableSlot]) { | ||
intentHandlers[intent][routableSlot] = {}; | ||
} | ||
if (!intentHandlers[intent][routableSlot][slotValues[i]]) { | ||
intentHandlers[intent][routableSlot][slotValues[i]] = {}; | ||
} | ||
intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; | ||
} | ||
} else { | ||
intentHandlers[intent].handler = handler; | ||
} | ||
}; | ||
|
||
googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { | ||
if (intentName && intentHandlers[intentName]) { | ||
if (metric && intentHandlers[intentName]['metric'] && | ||
intentHandlers[intentName]['metric'][metric] && | ||
intentHandlers[intentName]['metric'][metric].handler) { | ||
return intentHandlers[intentName]['metric'][metric].handler; | ||
} else if (intentHandlers[intentName].handler) { | ||
return intentHandlers[intentName].handler; | ||
} else { | ||
return null; | ||
} | ||
} else { | ||
return null; | ||
} | ||
|
||
}; | ||
|
||
googleHome.buildResponse = function buildResponse(output) { | ||
return { | ||
fulfillmentText: output | ||
// , fulfillmentMessages: [output] | ||
, source: 'Nightscout' | ||
}; | ||
}; | ||
|
||
return googleHome; | ||
} | ||
|
||
module.exports = init; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -287,6 +287,21 @@ function init(ctx) { | |
, intentHandler: alexaIOBIntentHandler | ||
}] | ||
}; | ||
|
||
function googleHomeIOBIntentHandler(result, next, sbx) { | ||
var preamble = result && result.parameters && result.parameters.givenName ? result.parameters.givenName + ' has ' : 'You have '; | ||
var message = preamble + getIob(sbx) + ' insulin on board'; | ||
next(message); | ||
} | ||
|
||
iob.googleHome = { | ||
intentHandlers: [{ | ||
intent: 'CurrentMetric' | ||
, routableSlot: 'metric' | ||
, slots: ['iob', 'insulin on board', 'insulin'] | ||
, intentHandler: googleHomeIOBIntentHandler | ||
}] | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here let's aim to restructure both Home and Alexa so the IOB reporting code is in the plugin, not in iob.js There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, would it be possible to add a single method that output a string for speech and reuse that in both Home and Alexa? That'd maybe be the cleanest solution? |
||
|
||
return iob; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -491,6 +491,30 @@ function init(ctx) { | |
, intentHandler: alexaLastLoopHandler | ||
}] | ||
}; | ||
|
||
function googleHomeForecastHandler(response, next, sbx) { | ||
if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { | ||
var response = 'The Open APS eventual BG is ' + sbx.properties.openaps.lastEventualBG; | ||
next(response); | ||
} | ||
} | ||
|
||
function googleHomeLastLoopHandler (response, next, sbx) { | ||
var response = 'The last successful loop was ' + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)); | ||
next(response); | ||
} | ||
|
||
openaps.googleHome = { | ||
intentHandlers: [{ | ||
intent: 'CurrentMetric' | ||
, routableSlot: 'metric' | ||
, slots: ['openaps', 'openaps forecast', 'forecast'] | ||
, intentHandler: googleHomeForecastHandler | ||
}, { | ||
intent: 'LastLoop' | ||
, intentHandler: googleHomeLastLoopHandler | ||
}] | ||
}; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And same here - Home and Alexa code for creating the speech user interface should be in the plugin itself. What'd be acceptable for this file is a generic API method that returns a string for speech, which is reused in both Home and Alexa speech implementations, but custom method for both adds a lot of maintenance burden |
||
function statusClass (prop, prefs, sbx) { | ||
var level = statusLevel(prop, prefs, sbx); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -137,6 +137,10 @@ function boot (env, language) { | |
if (env.settings.isEnabled('alexa')) { | ||
ctx.alexa = require('../plugins/alexa')(env, ctx); | ||
} | ||
|
||
if (env.settings.isEnabled('googlehome')) { | ||
ctx.googleHome = require('../plugins/googlehome')(env, ctx); | ||
} | ||
|
||
next( ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @PieterGit let's try to figure out if there's a way to reduce plugin-specific code in this section |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What API call changes are needed to have this live in the Home plugin? basalprofile.js should only contain basalprofile code.