Skip to content

Instantly share code, notes, and snippets.

@i8beef
Last active October 14, 2018 10:46
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save i8beef/8f6252d80af34183f5ca554d1e0af8ab to your computer and use it in GitHub Desktop.
Save i8beef/8f6252d80af34183f5ca554d1e0af8ab to your computer and use it in GitHub Desktop.
[{"id":"41819097.56452","type":"debug","z":"46839377.4d6c0c","name":"","active":false,"console":"false","complete":"true","x":470,"y":420,"wires":[]},{"id":"62a080b1.65f88","type":"http in","z":"46839377.4d6c0c","name":"","url":"/google/home","method":"post","swaggerDoc":"","x":290,"y":460,"wires":[["41819097.56452","97181f8b.08539"]]},{"id":"fd3b0cd7.2f07a","type":"switch","z":"46839377.4d6c0c","name":"Sort by Intent Type","property":"payload.inputs[0].intent","propertyType":"msg","rules":[{"t":"eq","v":"action.devices.SYNC","vt":"str"},{"t":"eq","v":"action.devices.QUERY","vt":"str"},{"t":"eq","v":"action.devices.EXECUTE","vt":"str"},{"t":"else"}],"checkall":"false","outputs":4,"x":790,"y":600,"wires":[["48964de7.af0da4"],["be74c96a.f70178"],["8c5e6842.b912a8"],["4b28322c.99bb2c"]]},{"id":"8f49fcb9.99dbd","type":"switch","z":"46839377.4d6c0c","name":"Check inputs","property":"payload.inputs","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":570,"y":540,"wires":[["6258647e.516dac"],["fd3b0cd7.2f07a"]]},{"id":"6258647e.516dac","type":"function","z":"46839377.4d6c0c","name":"Error: Missing Inputs","func":"msg.statusCode = 400;\nmsg.payload = { \n requestId: msg.req.body.requestId,\n payload: {\n errorCode: \"protocolError\"\n }\n};\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":500,"wires":[["1ec86c58.938694"]]},{"id":"1ec86c58.938694","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":500,"wires":[]},{"id":"8c5e6842.b912a8","type":"function","z":"46839377.4d6c0c","name":"action.devices.EXECUTE","func":"msg.requestId = msg.payload.requestId;\nmsg.payload = msg.payload.inputs[0].payload;\n\nreturn msg;","outputs":"1","noerr":0,"x":1070,"y":620,"wires":[["1332d581.826c8a"]]},{"id":"13f2f440.702bfc","type":"inject","z":"46839377.4d6c0c","name":"Test","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":760,"wires":[["d1b5a190.3d73a"]]},{"id":"d1b5a190.3d73a","type":"function","z":"46839377.4d6c0c","name":"Inject test payload","func":"return {\n requestId: \"ff36a3cc-ec34-11e6-b1a0-64510650abcf\",\n payload: {\n \"commands\": [{\n \"devices\": [{\n \"id\": \"zway/familyRoom/familyRoomLight\",\n \"customData\": {\n \"fooValue\": 74,\n \"barValue\": false\n }\n }, {\n \"id\": \"zway/kitchen/kitchenLight\",\n \"customData\": {\n \"fooValue\": 12,\n \"barValue\": true\n }\n }, {\n \"id\": \"zway/diningRoom/diningRoomLight\",\n \"customData\": {\n \"fooValue\": 35,\n \"barValue\": false,\n \"bazValue\": \"sheep dip\"\n }\n }],\n \"execution\": [{\n \"command\": \"action.devices.commands.OnOff\",\n \"params\": {\n \"on\": true\n }\n }]\n }]\n }\n};","outputs":1,"noerr":0,"x":270,"y":760,"wires":[["492089c0.bf8898"]]},{"id":"cd98ce34.d79af","type":"link in","z":"46839377.4d6c0c","name":"Google Home EXECUTE","links":["1332d581.826c8a"],"x":55,"y":820,"wires":[["492089c0.bf8898"]]},{"id":"7b107473.9f68fc","type":"comment","z":"46839377.4d6c0c","name":"Process Google Home EXECUTE","info":"","x":180,"y":720,"wires":[]},{"id":"38ef9781.fbddd8","type":"comment","z":"46839377.4d6c0c","name":"Google Home Endpoint","info":"","x":140,"y":420,"wires":[]},{"id":"5d0362f3.f667bc","type":"http response","z":"46839377.4d6c0c","name":"","x":330,"y":940,"wires":[]},{"id":"938b6a26.1045a8","type":"function","z":"46839377.4d6c0c","name":"Add Headers","func":"msg.headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization'\n};\nreturn msg;","outputs":1,"noerr":0,"x":170,"y":940,"wires":[["5d0362f3.f667bc","17093049.5a43b"]]},{"id":"c7959523.ec2808","type":"link in","z":"46839377.4d6c0c","name":"Google Home Smart Home Response","links":["1ec86c58.938694","8601c640.0382d8"],"x":55,"y":940,"wires":[["938b6a26.1045a8"]]},{"id":"bdb53516.9356f8","type":"comment","z":"46839377.4d6c0c","name":"Google Home Response","info":"","x":150,"y":900,"wires":[]},{"id":"1332d581.826c8a","type":"link out","z":"46839377.4d6c0c","name":"","links":["cd98ce34.d79af"],"x":1255,"y":620,"wires":[]},{"id":"23fc38ec.a0e948","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":540,"wires":[]},{"id":"2cead14a.b0da7e","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":580,"wires":[]},{"id":"4b28322c.99bb2c","type":"function","z":"46839377.4d6c0c","name":"Error: Unknown intent","func":"msg.statusCode = 400;\nmsg.payload = { \n requestId: msg.req.body.requestId,\n payload: {\n errorCode: \"protocolError\"\n }\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":660,"wires":[["5955a067.309f1"]]},{"id":"5955a067.309f1","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":660,"wires":[]},{"id":"17093049.5a43b","type":"debug","z":"46839377.4d6c0c","name":"","active":false,"console":"false","complete":"true","x":330,"y":980,"wires":[]},{"id":"8601c640.0382d8","type":"link out","z":"46839377.4d6c0c","name":"","links":["c7959523.ec2808"],"x":1135,"y":800,"wires":[]},{"id":"469336eb.5eb988","type":"comment","z":"46839377.4d6c0c","name":"Google Device States","info":"","x":140,"y":100,"wires":[]},{"id":"e83f150b.b81c08","type":"inject","z":"46839377.4d6c0c","name":"Print","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":180,"wires":[["a93049d8.fb24f8"]]},{"id":"a93049d8.fb24f8","type":"function","z":"46839377.4d6c0c","name":"Get current device state","func":"let devices = flow.get(\"googleDevices\") || {};\n\nif (devices[\"undefined\"])\n delete devices[\"undefined\"];\n\nreturn { payload: devices };","outputs":1,"noerr":0,"x":290,"y":180,"wires":[["632a92a4.31c5ec"]]},{"id":"632a92a4.31c5ec","type":"debug","z":"46839377.4d6c0c","name":"","active":true,"console":"false","complete":"false","x":1030,"y":180,"wires":[]},{"id":"e538faf8.ed4f68","type":"inject","z":"46839377.4d6c0c","name":"SYNC","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":500,"wires":[["b40cf88e.440898"]]},{"id":"5ed8eade.6f5d94","type":"inject","z":"46839377.4d6c0c","name":"QUERY","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":540,"wires":[["267c6952.970496"]]},{"id":"99692839.a4f958","type":"inject","z":"46839377.4d6c0c","name":"EXECUTE","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":580,"wires":[["a65b8270.08bbe"]]},{"id":"b40cf88e.440898","type":"function","z":"46839377.4d6c0c","name":"Inject test payload","func":"return {\n payload: {\n \"requestId\": \"ff36a3cc-ec34-11e6-b1a0-64510650abcf\",\n \"inputs\": [{\n \"intent\": \"action.devices.SYNC\",\n }]\n }\n};","outputs":1,"noerr":0,"x":290,"y":500,"wires":[["8f49fcb9.99dbd"]]},{"id":"267c6952.970496","type":"function","z":"46839377.4d6c0c","name":"Inject test payload","func":"return {\n payload: {\n \"requestId\": \"ff36a3cc-ec34-11e6-b1a0-64510650abcf\",\n \"inputs\": [{\n \"intent\": \"action.devices.QUERY\",\n \"payload\": {\n \"devices\": [{\n \"id\": \"zway/familyRoom/familyRoomLight\",\n \"customData\": {\n \"fooValue\": 12,\n \"barValue\": true,\n \"bazValue\": \"alpaca sauce\"\n }\n }, {\n \"id\": \"zway/kitchen/kitchenLight\",\n \"customData\": {\n \"fooValue\": 74,\n \"barValue\": false,\n \"bazValue\": \"sheep dip\"\n }\n }]\n }\n }]\n }\n};","outputs":1,"noerr":0,"x":290,"y":540,"wires":[["8f49fcb9.99dbd"]]},{"id":"a65b8270.08bbe","type":"function","z":"46839377.4d6c0c","name":"Inject test payload","func":"return {\n payload: {\n \"requestId\": \"ff36a3cc-ec34-11e6-b1a0-64510650abcf\",\n \"uid\": \"213456\",\n \"auth\": \"bearer xxx\",\n \"inputs\": [{\n \"intent\": \"action.devices.EXECUTE\",\n \"payload\": {\n \"commands\": [{\n \"devices\": [{\n \"id\": \"zway/familyRoom/familyRoomLight\",\n \"customData\": {\n \"fooValue\": 74,\n \"barValue\": false\n }\n }, {\n \"id\": \"zway/kitchen/kitchenLight\",\n \"customData\": {\n \"fooValue\": 12,\n \"barValue\": true\n }\n }, {\n \"id\": \"zway/diningRoom/diningRoomLight\",\n \"customData\": {\n \"fooValue\": 35,\n \"barValue\": false,\n \"bazValue\": \"sheep dip\"\n }\n }],\n \"execution\": [{\n \"command\": \"action.devices.commands.OnOff\",\n \"params\": {\n \"on\": true\n }\n }]\n }]\n }\n }]\n }\n};","outputs":1,"noerr":0,"x":290,"y":580,"wires":[["8f49fcb9.99dbd"]]},{"id":"1a651aa0.0bf6a5","type":"inject","z":"46839377.4d6c0c","name":"Reset","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":140,"wires":[["63ea1dc5.939784"]]},{"id":"63ea1dc5.939784","type":"function","z":"46839377.4d6c0c","name":"Reset current device state","func":"let devices = flow.get(\"googleDevices\") || {};\nflow.set(\"googleDevices\", {});\nreturn { payload: devices };","outputs":1,"noerr":0,"x":290,"y":140,"wires":[[]]},{"id":"b3799825.8b6e48","type":"http request","z":"46839377.4d6c0c","name":"REQUEST_SYNC ","method":"POST","ret":"obj","url":"","tls":"","x":630,"y":360,"wires":[["f6c424d2.e72a08"]]},{"id":"f6c424d2.e72a08","type":"debug","z":"46839377.4d6c0c","name":"","active":false,"console":"false","complete":"true","x":1010,"y":360,"wires":[]},{"id":"97181f8b.08539","type":"function","z":"46839377.4d6c0c","name":"Get bearer token","func":"let accessTokenStore = global.get(\"googleAccessTokenStore\") || {};\nlet accessTokenId = msg.req.headers.authorization ? msg.req.headers.authorization.split(' ')[1] : null;\nlet accessToken = accessTokenStore[accessTokenId];\n\nif (accessToken !== undefined) {\n if (new Date(accessToken.expiresAt) < Date.now()) {\n delete accessTokenStore[accessTokenId];\n msg.accessToken = \"EXPIRED\";\n } else {\n msg.accessToken = accessToken;\n }\n} else {\n msg.accessToken = null;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":510,"y":460,"wires":[["9faf29d2.cc8468"]]},{"id":"9faf29d2.cc8468","type":"switch","z":"46839377.4d6c0c","name":"Check bearer token","property":"accessToken","propertyType":"msg","rules":[{"t":"null"},{"t":"eq","v":"EXPIRED","vt":"str"},{"t":"else"}],"checkall":"false","outputs":3,"x":740,"y":460,"wires":[["cd90e7eb.b1a048"],["729ff13d.024c2"],["8f49fcb9.99dbd"]]},{"id":"cd90e7eb.b1a048","type":"function","z":"46839377.4d6c0c","name":"Error: Bearer token incorrect","func":"msg.statusCode = 401;\nmsg.payload = { \n requestId: msg.req.body.requestId,\n payload: {\n errorCode: \"authFailure\"\n }\n};\nreturn msg;","outputs":1,"noerr":0,"x":1080,"y":420,"wires":[["741ac462.48099c"]]},{"id":"729ff13d.024c2","type":"function","z":"46839377.4d6c0c","name":"Error: Bearer token expired","func":"msg.statusCode = 401;\nmsg.payload = { \n requestId: msg.req.body.requestId,\n payload: {\n errorCode: \"authExpired\"\n }\n};\nreturn msg;","outputs":1,"noerr":0,"x":1080,"y":460,"wires":[["1306ca8f.11a835"]]},{"id":"1306ca8f.11a835","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":460,"wires":[]},{"id":"741ac462.48099c","type":"link out","z":"46839377.4d6c0c","name":"","links":["dfa15993.ece3d8","c7959523.ec2808"],"x":1255,"y":420,"wires":[]},{"id":"109b127e.38625e","type":"inject","z":"46839377.4d6c0c","name":"On Startup","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"x":110,"y":220,"wires":[["6b16ca61.a48174","679a40cc.51a2a"]]},{"id":"a90446b0.5cf458","type":"function","z":"46839377.4d6c0c","name":"Build request","func":"let agentUserId = flow.get(\"agentUserId\");\nlet homeGraphApiKey = flow.get(\"homeGraphApiKey\");\n\nmsg.url = 'https://homegraph.googleapis.com/v1/devices:requestSync?key=' + homeGraphApiKey;\nmsg.headers = {\n 'Content-Type': 'application/json'\n};\n\nmsg.payload = {\n 'agent_user_id': agentUserId\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":430,"y":360,"wires":[["b3799825.8b6e48"]]},{"id":"825f14df.c0cd28","type":"inject","z":"46839377.4d6c0c","name":"Trigger Google REQUEST_SYNC","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":180,"y":360,"wires":[["a90446b0.5cf458"]]},{"id":"487515ef.bfa52c","type":"comment","z":"46839377.4d6c0c","name":"Google Device Sync Request","info":"","x":160,"y":320,"wires":[]},{"id":"6b16ca61.a48174","type":"credentials","z":"46839377.4d6c0c","name":"","props":[{"value":"agentUserId","type":"flow"},{"value":"homeGraphApiKey","type":"flow"}],"x":290,"y":220,"wires":[[]]},{"id":"48964de7.af0da4","type":"function","z":"46839377.4d6c0c","name":"action.devices.SYNC","func":"let allDevices = flow.get(\"googleDevices\");\nlet devices = [];\nfor (let device in allDevices) {\n var traits = allDevices[device].traits.map(x => x.trait);\n devices.push(Object.assign({}, allDevices[device], { traits: traits }));\n}\n\nlet requestId = msg.payload.requestId;\nlet agentUserId = flow.get(\"agentUserId\");\nmsg.payload = {\n requestId: requestId,\n payload: {\n agentUserId: agentUserId,\n devices: devices\n }\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":540,"wires":[["23fc38ec.a0e948"]]},{"id":"be74c96a.f70178","type":"function","z":"46839377.4d6c0c","name":"action.devices.QUERY","func":"let allDevices = flow.get(\"googleDevices\");\nlet deviceStates = global.get(\"deviceStates\");\nlet devices = {};\nfor (let device in msg.payload.inputs[0].payload.devices) {\n let deviceId = msg.payload.inputs[0].payload.devices[device].id;\n\n let queryDeviceBase = { \"online\": true, \"on\": true };\n \n let currentStates = [];\n for (let trait in allDevices[deviceId].traits) {\n let stateObj = JSON.parse(JSON.stringify(allDevices[deviceId].traits[trait].state));\n \n // Set states according to trait type definition\n switch (allDevices[deviceId].traits[trait].trait) {\n case \"action.devices.traits.OnOff\":\n // Translate on and off to true and false\n if (\"on\" in stateObj && stateObj[\"on\"] in deviceStates) {\n if (isNaN(deviceStates[stateObj[\"on\"]])) {\n stateObj[\"on\"] = deviceStates[stateObj[\"on\"]] === \"on\";\n } else {\n stateObj[\"on\"] = deviceStates[stateObj[\"on\"]] > 0;\n }\n }\n break;\n default:\n // Direct pass through of value\n for (let state in stateObj) {\n if (stateObj[state] in deviceStates) {\n stateObj[state] = deviceStates[stateObj[state]];\n }\n }\n break;\n }\n \n currentStates.push(stateObj);\n }\n \n devices[deviceId] = Object.assign({}, queryDeviceBase, ...currentStates);\n}\n\nlet requestId = msg.payload.requestId;\nmsg.payload = {\n requestId: requestId,\n payload: {\n devices: devices\n }\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":580,"wires":[["2cead14a.b0da7e"]]},{"id":"492089c0.bf8898","type":"function","z":"46839377.4d6c0c","name":"Split command and response","func":"return [ \n msg, \n msg.payload.commands.map(x => { \n return { \"payload\": x };\n })\n];","outputs":2,"noerr":0,"x":240,"y":820,"wires":[["26e176d0.96326a"],["786573c2.7fb3bc"]]},{"id":"26e176d0.96326a","type":"function","z":"46839377.4d6c0c","name":"Build response","func":"let defaultState = { on: true, online: true };\nlet commandResponses = msg.payload.commands.map(x => {\n let translatedParams = Object.assign({}, defaultState, x.execution.params);\n \n if (translatedParams[\"updateModeSettings\"]) {\n translatedParams[\"currentModeSettings\"] = translatedParams[\"updateModeSettings\"];\n delete translatedParams[\"updateModeSettings\"];\n }\n\n if (translatedParams[\"updateToggleSettings\"]) {\n translatedParams[\"currentToggleSettings\"] = translatedParams[\"updateToggleSettings\"];\n delete translatedParams[\"updateToggleSettings\"];\n }\n \n return {\n ids: x.devices.map(y => y.id),\n status: true ? \"SUCCESS\" : \"OFFLINE\",\n states: translatedParams\n };\n});\n\nmsg.payload = {\n requestId: msg.requestId,\n payload: {\n commands: commandResponses\n }\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":480,"y":800,"wires":[["8601c640.0382d8"]]},{"id":"786573c2.7fb3bc","type":"function","z":"46839377.4d6c0c","name":"Parse commands","func":"let commands = [];\nfor (let execution in msg.payload.execution) {\n for (let device in msg.payload.devices) {\n commands.push({\n topic: msg.payload.devices[device].id,\n payload: msg.payload.execution[execution]\n });\n }\n}\n\nreturn [ commands ];","outputs":1,"noerr":0,"x":490,"y":840,"wires":[["b6e1614b.b30d2"]]},{"id":"b6e1614b.b30d2","type":"function","z":"46839377.4d6c0c","name":"Generate MQTT messages","func":"let allDevices = flow.get(\"googleDevices\");\n\n// { topic: device id, payload: execution }\n\nlet commandMsgs = [];\nif (msg.topic in allDevices) {\n // Find traits matching the requested command\n let applicableTraits = allDevices[msg.topic].traits\n .filter(x => msg.payload.command in x.commands);\n\n // A device should have only one handler for a given command\n if (applicableTraits.length == 1) {\n let commandParamToMqttTopicMap = applicableTraits[0].commands[msg.payload.command];\n for (let param in msg.payload.params) {\n if (typeof msg.payload.params[param] === 'object') {\n let commandTopic = \"\";\n // Support one level drill down\n for (let subParam in msg.payload.params[param]) {\n commandMsgs.push({\n topic: commandParamToMqttTopicMap[param][subParam],\n payload: msg.payload.params[param][subParam]\n });\n }\n } else {\n // Set translate states according to trait type\n let payload = \"\";\n switch (applicableTraits[0].trait) {\n case \"action.devices.traits.OnOff\":\n // Translate true and false to on and off\n payload = msg.payload.params[param] === true ? \"on\" : \"off\";\n break;\n default:\n payload = msg.payload.params[param]\n break;\n }\n\n commandMsgs.push({\n topic: commandParamToMqttTopicMap[param],\n payload: payload\n });\n }\n }\n }\n}\n\nreturn [ commandMsgs] ;","outputs":1,"noerr":0,"x":740,"y":840,"wires":[["8d5ba57c.d5d658"]]},{"id":"d8212114.3d38e","type":"link out","z":"46839377.4d6c0c","name":"","links":["d0d9d6b7.697a88"],"x":1255,"y":960,"wires":[]},{"id":"679a40cc.51a2a","type":"file in","z":"46839377.4d6c0c","name":"Read file","filename":"/data/projects/HomeAutio/config/googleDevices.json","format":"utf8","sendError":true,"x":280,"y":260,"wires":[["42e373a7.84018c"]]},{"id":"42e373a7.84018c","type":"json","z":"46839377.4d6c0c","name":"","x":430,"y":260,"wires":[["56b3abb.7086554"]]},{"id":"56b3abb.7086554","type":"function","z":"46839377.4d6c0c","name":"Update googleDevices","func":"if (msg.payload !== null) {\n flow.set(\"googleDevices\", msg.payload);\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":620,"y":260,"wires":[["fded4397.e7b94"]]},{"id":"fded4397.e7b94","type":"debug","z":"46839377.4d6c0c","name":"","active":false,"console":"false","complete":"true","x":1010,"y":260,"wires":[]},{"id":"8d5ba57c.d5d658","type":"switch","z":"46839377.4d6c0c","name":"Custom handlers","property":"topic","propertyType":"msg","rules":[{"t":"regex","v":"^denon.*$","vt":"str","case":false},{"t":"eq","v":"harmony/default/activity/set","vt":"str"},{"t":"eq","v":"harmony/bedroom/activity/set","vt":"str"},{"t":"regex","v":"^([\\w\\/]+)\\(([\\w\\s]+)\\)(\\/set)$","vt":"str","case":false},{"t":"else"}],"checkall":"false","repair":false,"outputs":5,"x":710,"y":960,"wires":[["664cb048.8e0d9"],["3e162284.55a47e"],["84d1695.bec2098"],["681570b7.b2ad8"],["12541066.18708"]]},{"id":"664cb048.8e0d9","type":"function","z":"46839377.4d6c0c","name":"Denon","func":"msg.payload = msg.payload.toString().toUpperCase();\nreturn msg;","outputs":1,"noerr":0,"x":1090,"y":880,"wires":[["d8212114.3d38e"]]},{"id":"3e162284.55a47e","type":"function","z":"46839377.4d6c0c","name":"Family Room Harmony","func":"switch (msg.payload) {\n case \"on\":\n case \"tv\":\n msg.payload = \"Watch TV\";\n break;\n case \"off\":\n msg.payload = \"PowerOff\";\n break;\n case \"media player\":\n case \"chromecast\":\n msg.payload = \"Watch Shield TV\";\n break;\n case \"game console\":\n msg.payload = \"Play PS4\";\n break;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1040,"y":920,"wires":[["d8212114.3d38e"]]},{"id":"84d1695.bec2098","type":"function","z":"46839377.4d6c0c","name":"Bedroom Harmony","func":"switch (msg.payload) {\n case \"on\":\n case \"tv\":\n msg.payload = \"Watch TV\";\n break;\n case \"off\":\n msg.payload = \"PowerOff\";\n break;\n case \"media player\":\n case \"chromecast\":\n msg.payload = \"Watch Chromecast\";\n break;\n case \"game console\":\n msg.payload = \"Watch Roku\";\n break;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1050,"y":960,"wires":[["d8212114.3d38e"]]},{"id":"681570b7.b2ad8","type":"function","z":"46839377.4d6c0c","name":"Value topic","func":"let regex = /([\\w\\/]+)\\(([\\w\\s]+)\\)(\\/set)/g\nlet topicParts = regex.exec(msg.topic);\n\nmsg.topic = topicParts[1] + topicParts[3];\nmsg.payload = msg.payload === \"on\" ? topicParts[2] : \"PowerOff\";\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":1000,"wires":[["d8212114.3d38e"]]},{"id":"12541066.18708","type":"function","z":"46839377.4d6c0c","name":"Default","func":"return msg;","outputs":1,"noerr":0,"x":1080,"y":1040,"wires":[["d8212114.3d38e"]]}]
{
"zway/diningRoom/diningRoomLight": {
"id": "zway/diningRoom/diningRoomLight",
"type": "action.devices.types.LIGHT",
"traits": [
{
"trait": "action.devices.traits.OnOff",
"commands": {
"action.devices.commands.OnOff": {
"on": "zway/diningRoom/diningRoomLight/set"
}
},
"state": {
"on": "zway/diningRoom/diningRoomLight"
}
},
{
"trait": "action.devices.traits.Brightness",
"commands": {
"action.devices.commands.BrightnessAbsolute": {
"brightness": "zway/diningRoom/diningRoomLight/set"
}
},
"state": {
"brightness": "zway/diningRoom/diningRoomLight"
}
}
],
"name": {
"defaultNames": [],
"name": "dining room light",
"nicknames": []
},
"willReportState": false,
"attributes": null,
"roomHint": "Dining Room",
"deviceInfo": {
"manufacturer": "HomeSeer",
"model": "WSD100+",
"hwVersion": "1.0",
"swVersion": "1.0"
}
},
"harmony/default/activity": {
"id": "harmony/default/activity",
"type": "action.devices.types.SWITCH",
"traits": [
{
"trait": "action.devices.traits.OnOff",
"commands": {
"action.devices.commands.OnOff": {
"on": "harmony/default/activity/set"
}
},
"state": {
"on": "harmony/default/activity"
}
},
{
"trait": "action.devices.traits.Modes",
"commands": {
"action.devices.commands.SetModes": {
"updateModeSettings": {
"input source": "harmony/default/activity/set"
}
}
},
"state": {
"currentModeSettings": {
"input source": "harmony/default/activity"
}
}
}
],
"name": {
"defaultNames": [],
"name": "family room tv",
"nicknames": []
},
"willReportState": false,
"attributes": {
"availableModes": [{
"name": "input source",
"name_values": [{
"name_synonym": ["activity"],
"lang": "en"
}],
"settings": [{
"setting_name": "tv",
"setting_values": [{
"setting_synonym": ["tv", "television"],
"lang": "en"
}]
}, {
"setting_name": "media player",
"setting_values": [{
"setting_synonym": ["shield", "chromecast"],
"lang": "en"
}]
}, {
"setting_name": "game console",
"setting_values": [{
"setting_synonym": ["PS4", "playstation", "playstation 4"],
"lang": "en"
}]
}],
"ordered": false
}]
},
"roomHint": "Family Room",
"deviceInfo": {
"manufacturer": "Logitech",
"model": "Harmony Hub",
"hwVersion": "1.0",
"swVersion": "1.0"
}
},
"neato/botvac/default": {
"id": "neato/botvac/default",
"type": "action.devices.types.VACUUM",
"traits": [
{
"trait": "action.devices.traits.StartStop",
"commands": {
"action.devices.commands.StartStop": {
"start": "neato/botvac/default/clean/set"
}
},
"state": {
"isRunning": "neato/botvac/default/docked",
"isPaused": false
}
}
],
"name": {
"defaultNames": [],
"name": "vacuum",
"nicknames": []
},
"willReportState": false,
"attributes": {
"pausable": false
},
"roomHint": "Family Room",
"deviceInfo": {
"manufacturer": "Neato",
"model": "Botvac D80",
"hwVersion": "1.0",
"swVersion": "1.0"
}
}
}

Google Actions API

An implementation of the Google Actions API for node-red. Implements SYNC, QUERY, EXECUTE, and REQUEST_SYNC.

Heavily based on the sample node.js implementation here.

Usage

  1. Ensure that the OAuth 2 for Google Actions flows have been put in place. This endpoint will verify the bearer token for requests against the token stores in that flow.
  2. Paste into a new flow.
  3. Create a googleDevices.json file that holds Google device representations of devices you want to expose to Google. See attached example. This file will be read and injected into a flow variable at node-red startup according to the file node at the top of this flow.
  4. This provides the skeleton for the implementation. The "switch" node will provide all of the outputs for different EXECUTE commands that Google can send at this time. Wire these outputs to your system accordingly. The implementation use the data read in step #3 to answer each of these endpoint calls accordingly.
  5. The QUERY calls require that your system maintain a running state store of current values for your devices. This implementation assumes that there is a global variable called "deviceStates" that is just a key-value object that maps a state key to its current simple value. How you implement this piece is system specific.

In my case, because MQTT drives everything, the setting id is an MQTT topic, and the value is just the last value published to that topic. Because these settings are retained in my MQTT server, if node-red restarts, the last value is always resent, so this cache is always up to date.

The googlDevices.json file will specify "traits" that a device implements, and it will specify "commands" and "states" that it maintains for each trait. States map directly to the keys in the "deviceStates" variable. Commands, in my case, map to the MQTT topic that the command will trigger a message to. When a QUERY request is received for a device, this flow will look at the config for that device, and for each trait, will populate the result object with the state defined in the config, but replace the value of each property with the value from deviceStates.

For EXEC commands, it will read the config for the device, find the command definition that has been sent, and translate each of the command properties that have been sent to an MQTT message that it will send the new value to, which it then sends out to my MQTT out nodes. If a command parameter that was passed isn't defined in the config, it is ignored.

@mukowman
Copy link

Great write up, struggled with the deviceStates variable to begin with but found how to get it working, only thing I haven't been able to achieve is the states that get pulled when running the QUERY. Always seem to get "TypeError: Cannot read property 'states' of undefined"
Using the below JSON for the deviceStates, any help would be appreciated.

[ { "id": "123", "type": "action.devices.types.LIGHT", "traits": [ "action.devices.traits.OnOff" ], "name": { "defaultNames": [ "device123" ], "name": "123", "nicknames": [ "Christmas Tree" ] }, "willReportState": true, "states": { "on": true }, "roomHint": "living room", "deviceInfo": { "manufacturer": "Foo", "model": "Bar", "hwVersion": "1.0", "swVersion": "1.0" }, "customData": { "fooValue": 12, "barValue": false, "bazValue": "dancing alpaca" } } ]

@i8beef
Copy link
Author

i8beef commented Feb 13, 2018

My old approach to this was a little brittle around that piece, as the device states would store a "states" property on the object, and if you didn't REMOVE that during a QUERY or SYNC request, it would be invalid to Google. My NEW approach (see updated Gist) instead uses a static JSON config file for devices you want to expose. That config uses a more in depth "traits" property that specifies MQTT topics that represent state, command targets, etc.

This lets me have a STATIC config, and then for each endpoint:

  1. SYNC - Just return this object, but convert the "traits" property to a flat list of the "trait" keys in the config object.
  2. QUERY - Use the config to determine the MQTT topic that holds the state for that var. It then reads that state from the global device cache (just a map of MQTT topic to last heard state) to set the values appropriately for the response.
  3. EXEC - Uses the config to determine the right MQTT topics for each requested command per target device, and spits out those messages as an array to be sent through MQTT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment