Skip to content

Commit

Permalink
feat(sentence): Add support for custom responses in the sentence node
Browse files Browse the repository at this point in the history
  • Loading branch information
zachowj committed Sep 11, 2024
1 parent 2904468 commit bdfcd95
Show file tree
Hide file tree
Showing 20 changed files with 469 additions and 50 deletions.
Binary file added docs/node/images/sentence_dynamic_example_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/node/images/sentence_dynamic_example_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 91 additions & 13 deletions docs/node/sentence.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,125 @@
# Sentence

The Sentence node triggers when the Home Assistant [Assist](https://www.home-assistant.io/voice_control/) feature matches a sentence from a voice assistant using the default [conversation agent](https://www.home-assistant.io/integrations/conversation/). This node is used for voice control integrations, allowing specific voice commands to trigger automations within Node-RED.
The **Sentence** node is triggered when the Home Assistant [Assist](https://www.home-assistant.io/voice_control/) feature matches a sentence from a voice assistant using the default [conversation agent](https://www.home-assistant.io/integrations/conversation/). This node enables voice control integrations, allowing specific voice commands to trigger automations within Node-RED.

Sentences are allowed to use some basic template syntax. Check Home Assistant [documentation](https://www.home-assistant.io/docs/automation/trigger/#sentence-trigger) for more information.
Sentences support basic template syntax. Refer to the Home Assistant [documentation](https://www.home-assistant.io/docs/automation/trigger/#sentence-trigger) for more details.

::: warning
_Needs [Custom Integration](https://github.com/zachowj/hass-node-red) installed
in Home Assistant for this node to function_
This node requires the [Custom Integration](https://github.com/zachowj/hass-node-red) to be installed in Home Assistant.
:::

## Configuration
## Configuration Options

### Mode

- **Type**: `string`
- **Options**:
- `trigger`
- `response`

Defines the node’s function.
If set to **trigger**, the node activates when a sentence is matched.
If set to **response**, the node sends a dynamic response back to Home Assistant.

### Sentences

- Type: `string`
- **Type**: `string`

A list of sentences to match. Supports basic template syntax. For more details, see the Home Assistant [documentation](https://www.home-assistant.io/docs/automation/trigger/#sentence-trigger).

### Response Type

- **Type**: `string`
- **Options**:
- `dynamic`
- `fixed`

Specifies the type of response sent to Home Assistant:

- **Fixed**: The same response is sent for all matched sentences.
- **Dynamic**: The response is determined by the node set to response mode.

### Response

- **Type**: `string`

The message sent to Home Assistant when a sentence is matched. This option is used only if the **Response Type** is set to `fixed`.

### Fallback Response

- **Type**: `string`

The message sent to Home Assistant when a timeout occurs. This is used when the **Response Type** is set to `dynamic`.

### Response Timeout

- **Type**: `number`

Specifies the time in milliseconds to wait for a response before sending the **Fallback Response**. This option is used only when the **Response Type** is set to `dynamic`.

A list of sentences to match. Sentences are allowed to use some basic template syntax. Check Home Assistant [documentation](https://www.home-assistant.io/docs/automation/trigger/#sentence-trigger) for more information.
### Expose As

### Expose as
- **Type**: `string`

- Type: `string`
Select an entity to create a switch in Home Assistant. Turning the switch on or off will enable or disable this node in Node-RED.

When an entity is selected a switch entity will be created in Home Assistant. Turning on and off this switch will disable/enable the nodes in Node-RED.
## Inputs

All properties must be provided in `msg.payload`.

Example input:

```json
{
"response": "The light is now on"
}
```

### Response

- **Type**: `string`

The response sent to Home Assistant when a sentence is matched. This is used only when the **Response Type** is set to `dynamic`.

## Outputs

Value types:

- `trigger id`: sentence that triggered the flow
- `config`: config properties of the node
- `trigger id`: The sentence that triggered the flow.
- `config`: The node's configuration properties.

## Examples

<InfoPanelOnly>

[link](https://zachowj.github.io/node-red-contrib-home-assistant-websocket/node/sentence.html#examples)
For detailed usage examples, see the [Examples](https://zachowj.github.io/node-red-contrib-home-assistant-websocket/node/sentence.html#examples) section.

</InfoPanelOnly>

<DocsOnly>

### Basic Usage

![screenshot](./images/sentence_01.png)

@[code](@examples/node/sentence/sentence_usage.json)

### Dynamic Response Examples

#### Example 1: Count Lights On and Off

This example shows how to use the Sentence node to provide a dynamic response based on the number of lights that are on or off in the house. It also turns on any lights that were off.

![screenshot](./images/sentence_dynamic_example_01.png)

@[code](@examples/node/sentence/sentence_dynamic_response_01.json)

#### Example 2: Check Garage Sensor

This example checks the status of a binary sensor to determine whether the car is in the garage. If the car is present, a response is sent confirming its location. If the car is not present, the response states that the car is not in the garage.

![screenshot](./images/sentence_dynamic_example_02.png)

@[code](@examples/node/sentence/sentence_dynamic_response_02.json)

</DocsOnly>
1 change: 1 addition & 0 deletions examples/node/sentence/sentence_dynamic_response_01.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id":"5b44e487c5795df9","type":"ha-get-entities","z":"01b6599a0739a237","name":"lights off count","server":"","version":1,"rules":[{"condition":"state_object","property":"entity_id","logic":"starts_with","value":"light.","valueType":"str"},{"condition":"state_object","property":"state","logic":"is","value":"off","valueType":"str"}],"outputType":"array","outputEmptyResults":true,"outputLocationType":"msg","outputLocation":"lightsOff","outputResultsCount":1,"x":312,"y":800,"wires":[["99c90d5fd17832c9","7c588d1510201532"]]},{"id":"99c90d5fd17832c9","type":"ha-get-entities","z":"01b6599a0739a237","name":"lights on count","server":"","version":1,"rules":[{"condition":"state_object","property":"entity_id","logic":"starts_with","value":"light.","valueType":"str"},{"condition":"state_object","property":"state","logic":"is","value":"on","valueType":"str"}],"outputType":"count","outputEmptyResults":false,"outputLocationType":"msg","outputLocation":"lightsOnCount","outputResultsCount":1,"x":504,"y":800,"wires":[["f6e701c97c731979"]]},{"id":"09cb88520209e712","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":132,"y":752,"wires":[["0b2ef68df13c5b94"]]},{"id":"0b2ef68df13c5b94","type":"api-call-service","z":"01b6599a0739a237","name":"","server":"","version":7,"debugenabled":false,"action":"conversation.process","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"{\"text\": \"let there be light\"}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"results"}],"queue":"none","blockInputOverrides":true,"domain":"conversation","service":"process","x":332,"y":752,"wires":[["edfa883350eca925"]]},{"id":"edfa883350eca925","type":"debug","z":"01b6599a0739a237","name":"output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.response.speech.plain.speech","targetType":"msg","statusVal":"","statusType":"auto","x":686,"y":752,"wires":[]},{"id":"b389cf7e343fc938","type":"api-call-service","z":"01b6599a0739a237","name":"","server":"","version":7,"debugenabled":false,"action":"light.turn_on","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"light","service":"turn_on","x":890,"y":848,"wires":[[]]},{"id":"28191039a0c43c00","type":"change","z":"01b6599a0739a237","name":"build entity list","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"target\": { \"entity_id\": lightsOff.entity_id}}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":848,"wires":[["b389cf7e343fc938"]]},{"id":"794a0664ef0e9ab0","type":"ha-sentence","z":"01b6599a0739a237","name":"let there be light","server":"5d205f70b1e41e9f","version":2,"inputs":0,"outputs":1,"exposeAsEntityConfig":"","mode":"trigger","sentences":["let there be light"],"response":"Something took to long","responseType":"jsonata","triggerResponseType":"dynamic","responseTimeout":1000,"outputProperties":[{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"payload","propertyType":"msg","value":"","valueType":"triggerId"}],"x":128,"y":800,"wires":[["5b44e487c5795df9"]]},{"id":"f6e701c97c731979","type":"ha-sentence","z":"01b6599a0739a237","name":"","server":"5d205f70b1e41e9f","version":2,"inputs":1,"outputs":1,"exposeAsEntityConfig":"","mode":"response","sentences":[],"response":"lightsOnCount & \" lights were already on turning on \" & $count(lightsOffCount) & \" lights that were off\"\t","responseType":"jsonata","triggerResponseType":"fixed","responseTimeout":1000,"outputProperties":[],"x":692,"y":800,"wires":[[]]},{"id":"7c588d1510201532","type":"switch","z":"01b6599a0739a237","name":"any lights off?","property":"lightsOff.length","propertyType":"msg","rules":[{"t":"gt","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":506,"y":848,"wires":[["28191039a0c43c00"]]}]
1 change: 1 addition & 0 deletions examples/node/sentence/sentence_dynamic_response_02.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id":"d140cd86c26cbbe5","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":204,"y":960,"wires":[["c18458aa0fda5201"]]},{"id":"c18458aa0fda5201","type":"api-call-service","z":"01b6599a0739a237","name":"is the car in the garage","server":"","version":7,"debugenabled":false,"action":"conversation.process","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"{\"text\": \"is the car in the garage\"}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"results"}],"queue":"none","blockInputOverrides":true,"domain":"conversation","service":"process","x":462,"y":960,"wires":[["080f0fda9ce2ebc9"]]},{"id":"080f0fda9ce2ebc9","type":"debug","z":"01b6599a0739a237","name":"output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.response.speech.plain.speech","targetType":"msg","statusVal":"","statusType":"auto","x":674,"y":960,"wires":[]},{"id":"aac0b36dca0c47b4","type":"ha-sentence","z":"01b6599a0739a237","name":"is the car in the garage","server":"5d205f70b1e41e9f","version":2,"inputs":0,"outputs":1,"exposeAsEntityConfig":"","mode":"trigger","sentences":["is the car in the garage"],"response":"Something took to long","responseType":"jsonata","triggerResponseType":"dynamic","responseTimeout":1000,"outputProperties":[{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"payload","propertyType":"msg","value":"","valueType":"triggerId"}],"x":220,"y":1008,"wires":[["6a2a0e27afac616e"]]},{"id":"2dd86795094ba553","type":"ha-sentence","z":"01b6599a0739a237","name":"","server":"5d205f70b1e41e9f","version":2,"inputs":1,"outputs":1,"exposeAsEntityConfig":"","mode":"response","sentences":[],"response":"payload","responseType":"msg","triggerResponseType":"fixed","responseTimeout":1000,"outputProperties":[],"x":940,"y":1008,"wires":[[]]},{"id":"6a2a0e27afac616e","type":"api-current-state","z":"01b6599a0739a237","name":"","server":"","version":3,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","entity_id":"binary_sensor.car_in_garage","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":522,"y":1008,"wires":[["fa724a7fe743faaa"],["30171db069e062f0"]]},{"id":"fa724a7fe743faaa","type":"change","z":"01b6599a0739a237","name":"yes","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"response\": \"Yes the car is in the garage\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":1008,"wires":[["2dd86795094ba553"]]},{"id":"30171db069e062f0","type":"change","z":"01b6599a0739a237","name":"no","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"response\": \"No the car is not in the garage\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":772,"y":1056,"wires":[["2dd86795094ba553"]]},{"id":"b4d9680db1056a02","type":"inject","z":"01b6599a0739a237","name":"set car in garage","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":224,"y":1056,"wires":[["0c940aece619684d"]]},{"id":"0c940aece619684d","type":"ha-binary-sensor","z":"01b6599a0739a237","name":"","entityConfig":"c9ce588e6f25ce5d","version":0,"state":"payload","stateType":"msg","attributes":[],"inputOverride":"allow","outputProperties":[],"x":496,"y":1056,"wires":[[]]},{"id":"763df26767e56a92","type":"inject","z":"01b6599a0739a237","name":"remove car from garage","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":254,"y":1104,"wires":[["0c940aece619684d"]]},{"id":"c9ce588e6f25ce5d","type":"ha-entity-config","server":"5d205f70b1e41e9f","deviceConfig":"","name":"car in garage","version":"6","entityType":"binary_sensor","haConfig":[{"property":"name","value":"car in garage"},{"property":"icon","value":""},{"property":"entity_picture","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":""}],"resend":true,"debugEnabled":false}]
2 changes: 1 addition & 1 deletion examples/node/sentence/sentence_usage.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"id":"2f10cb78ab2f486f","type":"debug","z":"39a6dba4b503e011","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":382,"y":384,"wires":[]},{"id":"5c563c29d19f4aab","type":"ha-sentence","z":"39a6dba4b503e011","name":"","server":"bf5874816710d0c7","version":0,"outputs":1,"sentences":["[it's ]party time","happy (new year|birthday)","hello world"],"outputProperties":[{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"payload","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"sentences","propertyType":"msg","value":"sentences","valueType":"config"}],"x":176,"y":384,"wires":[["2f10cb78ab2f486f"]]},{"id":"4868b36e0e57db4f","type":"api-call-service","z":"39a6dba4b503e011","name":"process sentence","server":"","version":5,"debugenabled":false,"domain":"conversation","service":"process","areaId":[],"deviceId":[],"entityId":[],"data":" {\"text\": payload}\t","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":422,"y":336,"wires":[[]]},{"id":"0bd711e6ab321cf9","type":"inject","z":"39a6dba4b503e011","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"happy new year","payloadType":"str","x":200,"y":336,"wires":[["4868b36e0e57db4f"]]},{"id":"7b19f1bcd79bf7ea","type":"inject","z":"39a6dba4b503e011","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"happy birthday","payloadType":"str","x":204,"y":288,"wires":[["4868b36e0e57db4f"]]},{"id":"fdec3314ea56130d","type":"inject","z":"39a6dba4b503e011","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"party time","payloadType":"str","x":184,"y":240,"wires":[["4868b36e0e57db4f"]]},{"id":"92aa9c7e046ebba3","type":"inject","z":"39a6dba4b503e011","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"it's party time","payloadType":"str","x":198,"y":192,"wires":[["4868b36e0e57db4f"]]},{"id":"4c1ecc1396d9eddf","type":"inject","z":"39a6dba4b503e011","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"hello world","payloadType":"str","x":188,"y":144,"wires":[["4868b36e0e57db4f"]]}]
[{"id":"2f10cb78ab2f486f","type":"debug","z":"01b6599a0739a237","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":418,"y":1136,"wires":[]},{"id":"5c563c29d19f4aab","type":"ha-sentence","z":"01b6599a0739a237","name":"","server":"5d205f70b1e41e9f","version":2,"inputs":0,"outputs":1,"exposeAsEntityConfig":"","mode":"trigger","sentences":["[it's ]party time","happy (new year|birthday)","hello world"],"response":"","responseType":"fixed","responseTimeout":1000,"outputProperties":[{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"payload","propertyType":"msg","value":"","valueType":"triggerId"},{"property":"sentences","propertyType":"msg","value":"sentences","valueType":"config"}],"x":212,"y":1136,"wires":[["2f10cb78ab2f486f"]]},{"id":"4868b36e0e57db4f","type":"api-call-service","z":"01b6599a0739a237","name":"process sentence","server":"","version":7,"debugenabled":false,"action":"conversation.process","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":" {\"text\": payload}\t","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"conversation","service":"process","x":458,"y":1088,"wires":[[]]},{"id":"0bd711e6ab321cf9","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"happy new year","payloadType":"str","x":236,"y":1088,"wires":[["4868b36e0e57db4f"]]},{"id":"7b19f1bcd79bf7ea","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"happy birthday","payloadType":"str","x":240,"y":1040,"wires":[["4868b36e0e57db4f"]]},{"id":"fdec3314ea56130d","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"party time","payloadType":"str","x":220,"y":992,"wires":[["4868b36e0e57db4f"]]},{"id":"92aa9c7e046ebba3","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"it's party time","payloadType":"str","x":234,"y":944,"wires":[["4868b36e0e57db4f"]]},{"id":"4c1ecc1396d9eddf","type":"inject","z":"01b6599a0739a237","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"hello world","payloadType":"str","x":224,"y":896,"wires":[["4868b36e0e57db4f"]]}]
1 change: 1 addition & 0 deletions src/common/controllers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function createExposeAsControllerDependences({

return {
...controllerDeps,
clientEvents,
exposeAsConfigNode,
status,
};
Expand Down
1 change: 1 addition & 0 deletions src/common/integration/Integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum MessageType {
Entity = 'nodered/entity',
RemoveDevice = 'nodered/device/remove',
SentenceTrigger = 'nodered/sentence',
SentenceResponse = 'nodered/sentence_response',
UpdateConfig = 'nodered/entity/update_config',
Webhook = 'nodered/webhook',
}
Expand Down
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export enum TypedInputTypes {
PreviousValue = 'previousValue',
CalendarItem = 'calendarItem',
DeviceId = 'deviceId',
Enviroment = 'env',
}

export enum TimeUnit {
Expand Down
Loading

0 comments on commit bdfcd95

Please sign in to comment.