Skip to content

Commit

Permalink
add async execution for switch/outlet
Browse files Browse the repository at this point in the history
  • Loading branch information
andrei-tatar committed Aug 17, 2021
1 parent 57140fa commit 5eb1453
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 102 deletions.
29 changes: 29 additions & 0 deletions doc/nodes/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Asynchronous command exection

**This feature is experimental and only enabled right now for switch/outlet nodes. Be aware that it might have breaking changes in the upcoming releases**

This feature allows the replacement of the regular NORA command handlers with user defined command handlers. Right now a command is handled in the following way:

1. Google sends a command to NORA
2. NORA handles the command by updating the state
3. NORA sends the reply back to Google
4. NORA also sends the updated state to Node-RED

Async command execution allows to replace point 2 with a Node-RED flow.

**Important Notes**:

- the timeout period from when a command handler begins until a response is awaited is 1500 msec. If more time passes, NORA responds to google that the device is not available (offline)
- when sending a message to the async `response` node, make sure to preserve the `msg._asyncCommandId` property. It's used to correlate the async response with the request

## Example flows:

### Handling the On/Off command on a switch to update the state:
```
[{"id":"6ed0ba1c.8e17f4","type":"noraf-async","z":"68061750.490f48","name":"response","x":620,"y":260,"wires":[]},{"id":"d126cedc.feabf","type":"function","z":"68061750.490f48","name":"","func":"//we only replace the payload, to keep the msg id intact\n\nmsg.payload = {\n state: {\n on: msg.payload.on,\n },\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":260,"wires":[["6ed0ba1c.8e17f4"]]},{"id":"b127c595.a98df8","type":"noraf-switch","z":"68061750.490f48","devicename":"Thingie","roomhint":"","name":"","passthru":false,"errorifstateunchaged":false,"nora":"e76a4bf6.431dd8","topic":"","onvalue":"true","onvalueType":"bool","offvalue":"false","offvalueType":"bool","twofactor":"off","twofactorpin":"","filter":false,"asyncCmd":true,"outputs":2,"x":300,"y":240,"wires":[["637078a8.004118"],["d126cedc.feabf"]]},{"id":"637078a8.004118","type":"debug","z":"68061750.490f48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":490,"y":220,"wires":[]},{"id":"e76a4bf6.431dd8","type":"noraf-config","name":"Nora Testing","group":"test","twofactor":"off","twofactorpin":"","localexecution":true,"structure":""}]
```

### Returning an error code:
```
[{"id":"52bf7fe0.e5491","type":"inject","z":"68061750.490f48","name":"on","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":150,"y":220,"wires":[["b127c595.a98df8"]]},{"id":"6ed0ba1c.8e17f4","type":"noraf-async","z":"68061750.490f48","name":"response","x":620,"y":260,"wires":[]},{"id":"d126cedc.feabf","type":"function","z":"68061750.490f48","name":"","func":"//we only replace the payload, to keep the msg id intact\n\nif (msg.payload.on) {\n msg.payload = {\n errorCode: 'alreadyOn',\n };\n} else {\n msg.payload = {\n state: {\n on: false,\n },\n };\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":260,"wires":[["6ed0ba1c.8e17f4"]]},{"id":"b127c595.a98df8","type":"noraf-switch","z":"68061750.490f48","devicename":"Thingie","roomhint":"","name":"","passthru":false,"errorifstateunchaged":false,"nora":"e76a4bf6.431dd8","topic":"","onvalue":"true","onvalueType":"bool","offvalue":"false","offvalueType":"bool","twofactor":"off","twofactorpin":"","filter":false,"asyncCmd":true,"outputs":2,"x":300,"y":240,"wires":[["637078a8.004118"],["d126cedc.feabf"]]},{"id":"637078a8.004118","type":"debug","z":"68061750.490f48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":490,"y":220,"wires":[]},{"id":"a6c6d10c.9740c","type":"inject","z":"68061750.490f48","name":"off","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":150,"y":260,"wires":[["b127c595.a98df8"]]},{"id":"e76a4bf6.431dd8","type":"noraf-config","name":"Nora Testing","group":"test","twofactor":"off","twofactorpin":"","localexecution":true,"structure":""}]
```
2 changes: 1 addition & 1 deletion doc/nodes/common.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
- `Acknowledge` - Google Assistant will ask for confirmation when a command is invoked, preventing accidental command execution
- `Pin` - Google Assistant will ask for a pin code when a command is invoked, preventing unauthorized access
- `Topic` Attaches the string value as a `topic` property to any outgoing message from the node (also used to optionally filter the input messages)

- `Async command execution` if checked it enables [asynchronous command execution](./async.md) for this node
131 changes: 51 additions & 80 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@andrei-tatar/nora-firebase-common": "^1.3.4",
"cbor": "^7.0.6",
"cbor": "^8.0.0",
"firebase": "^8.8.0",
"node-fetch": "^2.6.1",
"rxjs": "^7.3.0"
Expand Down
6 changes: 5 additions & 1 deletion src/nodes/nora-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ module.exports = function (RED: any) {
handleNodeInput({
node: this,
nodeConfig: config,
handler: msg => AsyncCommandsRegistry.handle(msg._asyncCommandId, msg.payload),
handler: msg => AsyncCommandsRegistry.handle({
id: msg._asyncCommandId,
response: msg.payload,
warn: w => this.warn(w),
}),
});
});
};
Expand Down
20 changes: 19 additions & 1 deletion src/nodes/nora-outlet.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,16 @@
filter: {
value: false,
},
asyncCmd: {
value: false,
},
outputs: {
value: 1,
},
},
inputs: 1,
outputs: 1,
outputLabels: ["state", "async command"],
paletteLabel: 'outlet',
label: function () {
return this.name || this.devicename || 'outlet';
Expand All @@ -83,6 +90,13 @@
$('#node-twofactor-pin').hide();
}
});
$('#node-input-asyncCmd').change(function () {
$('.hide-when-async')[$(this).is(':checked') ? 'hide' : 'show']();
});
},
oneditsave: function () {
var node = this;
node.outputs = $('#node-input-asyncCmd').is(':checked') ? 2 : 1;
},
});
</script>
Expand All @@ -105,6 +119,10 @@
<input type="checkbox" id="node-input-filter" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmd"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" id="node-input-asyncCmd" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row hide-when-async">
<label style="width:auto" for="node-input-errorifstateunchaged"><i class="fa fa-exclamation-triangle"></i> If state doesn't change via voice, warn user: </label>
<input type="checkbox" id="node-input-errorifstateunchaged" style="display:inline-block; width:auto; vertical-align:top;">
</div>
Expand Down Expand Up @@ -145,7 +163,7 @@
</script>

<script type="text/x-red" data-help-name="noraf-outlet">
<p>
<p>
<a href="https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/outlet/README.md">https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/outlet/README.md</a>
</p>
</script>
3 changes: 2 additions & 1 deletion src/nodes/nora-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ module.exports = function (RED: any) {
},
noraSpecific: {
returnOnOffErrorCodeIfStateAlreadySet: !!config.errorifstateunchaged,
}
asyncCommandExecution: !!config.asyncCmd,
},
},
updateStatus: ({ state, update }) => {
update(`(${state.on ? 'on' : 'off'})`);
Expand Down
Loading

0 comments on commit 5eb1453

Please sign in to comment.