diff --git a/docs/node/images/sentence_dynamic_example_01.png b/docs/node/images/sentence_dynamic_example_01.png new file mode 100644 index 0000000000..b592f9e705 Binary files /dev/null and b/docs/node/images/sentence_dynamic_example_01.png differ diff --git a/docs/node/images/sentence_dynamic_example_02.png b/docs/node/images/sentence_dynamic_example_02.png new file mode 100644 index 0000000000..32006c26b5 Binary files /dev/null and b/docs/node/images/sentence_dynamic_example_02.png differ diff --git a/docs/node/sentence.md b/docs/node/sentence.md index 0ba5638c9d..74df7cab67 100644 --- a/docs/node/sentence.md +++ b/docs/node/sentence.md @@ -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 -[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. +### 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) + diff --git a/examples/node/sentence/sentence_dynamic_response_01.json b/examples/node/sentence/sentence_dynamic_response_01.json new file mode 100644 index 0000000000..52b114e1fb --- /dev/null +++ b/examples/node/sentence/sentence_dynamic_response_01.json @@ -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"]]}] diff --git a/examples/node/sentence/sentence_dynamic_response_02.json b/examples/node/sentence/sentence_dynamic_response_02.json new file mode 100644 index 0000000000..99c7cdfcde --- /dev/null +++ b/examples/node/sentence/sentence_dynamic_response_02.json @@ -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}] diff --git a/examples/node/sentence/sentence_usage.json b/examples/node/sentence/sentence_usage.json index 2b14a3a770..e154678d15 100644 --- a/examples/node/sentence/sentence_usage.json +++ b/examples/node/sentence/sentence_usage.json @@ -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"]]}] diff --git a/src/common/controllers/helpers.ts b/src/common/controllers/helpers.ts index bbb2379bb1..ca2405d13a 100644 --- a/src/common/controllers/helpers.ts +++ b/src/common/controllers/helpers.ts @@ -64,6 +64,7 @@ export function createExposeAsControllerDependences({ return { ...controllerDeps, + clientEvents, exposeAsConfigNode, status, }; diff --git a/src/common/integration/Integration.ts b/src/common/integration/Integration.ts index 28b60672a1..51d7fc1f76 100644 --- a/src/common/integration/Integration.ts +++ b/src/common/integration/Integration.ts @@ -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', } diff --git a/src/const.ts b/src/const.ts index f4e68fd0a6..879fcffa23 100644 --- a/src/const.ts +++ b/src/const.ts @@ -133,6 +133,7 @@ export enum TypedInputTypes { PreviousValue = 'previousValue', CalendarItem = 'calendarItem', DeviceId = 'deviceId', + Enviroment = 'env', } export enum TimeUnit { diff --git a/src/editor/exposenode.ts b/src/editor/exposenode.ts index 1e9dba274c..72a4a9b46d 100644 --- a/src/editor/exposenode.ts +++ b/src/editor/exposenode.ts @@ -1,6 +1,7 @@ import { EditorRED } from 'node-red'; import { NO_VERSION, NodeType, ValueIntegrationMode } from '../const'; +import { SentenceMode } from '../nodes/sentence/const'; import { HassExposedConfig, HassNodeProperties } from './types'; import * as haUtils from './utils'; @@ -88,6 +89,14 @@ export function init(n: HassNodeProperties) { renderAlert(type); break; case NodeType.Sentence: + { + const mode = $('#node-input-mode').val() as SentenceMode; + $('#exposed-as-row').toggle(mode === SentenceMode.Trigger); + if (!isAddNodeSelected('server')) { + renderAlert(type); + } + } + break; case NodeType.Webhook: if (!isAddNodeSelected('server')) { renderAlert(type); @@ -236,7 +245,7 @@ const NodeMinIntegraionVersion = { [NodeType.Device]: '4.0.2', [NodeType.Number]: '1.3.0', [NodeType.Select]: '1.4.0', - [NodeType.Sentence]: '2.2.0', + [NodeType.Sentence]: '4.1.0', [NodeType.Sensor]: '1.1.0', [NodeType.Switch]: '1.1.0', [NodeType.Tag]: '0.5.0', diff --git a/src/nodes/sentence/SentenceInputController.ts b/src/nodes/sentence/SentenceInputController.ts new file mode 100644 index 0000000000..7d7eea5c42 --- /dev/null +++ b/src/nodes/sentence/SentenceInputController.ts @@ -0,0 +1,60 @@ +import InputOutputController, { + InputProperties, +} from '../../common/controllers/InputOutputController'; +import InputError from '../../common/errors/InputError'; +import NoConnectionError from '../../common/errors/NoConnectionError'; +import { MessageType } from '../../common/integration/Integration'; +import { DataSource } from '../../common/services/InputService'; +import { SentenceNode, SentenceNodeProperties } from '.'; + +export default class SentenceController extends InputOutputController< + SentenceNode, + SentenceNodeProperties +> { + public async onInput({ + done, + message, + parsedMessage, + send, + }: InputProperties): Promise { + if (!this.integration?.isConnected) { + throw new NoConnectionError(); + } + if (!this.integration?.isIntegrationLoaded) { + throw new InputError( + 'home-assistant.error.integration_not_loaded', + 'home-assistant.status.error', + ); + } + + const response = + parsedMessage.response.source === DataSource.Config + ? await this.typedInputService.getValue( + parsedMessage.response.value, + parsedMessage.responseType.value, + { + message, + }, + ) + : parsedMessage.response.value; + + this.status.setSending(); + + await this.homeAssistant.websocket.send({ + type: MessageType.SentenceResponse, + response, + response_id: parsedMessage.responseId.value, + }); + + await this.setCustomOutputs( + this.node.config.outputProperties, + message, + { + config: this.node.config, + }, + ); + this.status.setSuccess(); + send(message); + done(); + } +} diff --git a/src/nodes/sentence/SentenceIntegration.ts b/src/nodes/sentence/SentenceIntegration.ts index c253129dad..48b725c38b 100644 --- a/src/nodes/sentence/SentenceIntegration.ts +++ b/src/nodes/sentence/SentenceIntegration.ts @@ -3,25 +3,34 @@ import BidirectionalIntegration, { } from '../../common/integration/BidrectionalIntegration'; import { MessageType } from '../../common/integration/Integration'; import { SentenceNode } from '.'; +import { SentenceResponseType } from './const'; export interface SentenceDiscoveryPayload extends DiscoveryMessage { type: MessageType.SentenceTrigger; sentences: string[]; response?: string; + response_type?: SentenceResponseType; + response_timeout?: number; } export default class SentenceIntegration extends BidirectionalIntegration { protected getDiscoveryPayload(): SentenceDiscoveryPayload { const response = - this.node.config.response === '' + this.node.config.triggerResponse === '' ? undefined - : this.node.config.response; + : this.node.config.triggerResponse; + + // set the timeout to 1000 milliseconds if not set and convert to seconds + const timeout = (this.node.config.responseTimeout ?? 1000) / 1000; + return { type: MessageType.SentenceTrigger, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion server_id: this.node.config.server!, sentences: this.node.config.sentences, response, + response_type: this.node.config.triggerResponseType, + response_timeout: timeout, }; } } diff --git a/src/nodes/sentence/SentenceController.ts b/src/nodes/sentence/SentenceOutputController.ts similarity index 88% rename from src/nodes/sentence/SentenceController.ts rename to src/nodes/sentence/SentenceOutputController.ts index e9a67752cd..b9abd5659b 100644 --- a/src/nodes/sentence/SentenceController.ts +++ b/src/nodes/sentence/SentenceOutputController.ts @@ -8,10 +8,11 @@ interface SentenceResponse { sentence: string; result: Record | null; deviceId: string | null; + responseId: number; } const ExposeAsController = ExposeAsMixin(OutputController); -export default class SentenseController extends ExposeAsController { +export default class SentenceController extends ExposeAsController { public async onReceivedMessage(data: SentenceResponse) { if (!this.isEnabled) return; @@ -26,6 +27,7 @@ export default class SentenseController extends ExposeAsController { [TypedInputTypes.DeviceId]: data.deviceId, }, ); + message._sentence_response_id = data.responseId; this.status.setSuccess('home-assistant.status.triggered'); this.node.send(message); } diff --git a/src/nodes/sentence/const.ts b/src/nodes/sentence/const.ts new file mode 100644 index 0000000000..40f9cb99f1 --- /dev/null +++ b/src/nodes/sentence/const.ts @@ -0,0 +1,9 @@ +export enum SentenceMode { + Trigger = 'trigger', + Response = 'response', +} + +export enum SentenceResponseType { + Dynamic = 'dynamic', + Fixed = 'fixed', +} diff --git a/src/nodes/sentence/editor.html b/src/nodes/sentence/editor.html index 65eb657a81..c269ae03db 100644 --- a/src/nodes/sentence/editor.html +++ b/src/nodes/sentence/editor.html @@ -1,4 +1,5 @@ +
@@ -11,22 +12,71 @@
+ + +
+ +
    -
    +
    + + +
    + +
    - + + +
    + +
    + + +
    + +
    + +
      -
      +