diff --git a/docs/guide/custom_integration/exposed-nodes.md b/docs/guide/custom_integration/exposed-nodes.md index dc0f86d6cf3..44c4a20bfa1 100644 --- a/docs/guide/custom_integration/exposed-nodes.md +++ b/docs/guide/custom_integration/exposed-nodes.md @@ -8,34 +8,29 @@ state](../../node/trigger-state.md), and [poll state](../../node/poll-state.md) will have the option to be exposed to Home Assistant, and when enabled, it will show up in Home Assistant as a switch. Turning on and off these switches will disable/enable the nodes in Node-RED. This should help people who find -themselves having to make input_booleans in HA to enable/disable flows. +themselves having to make input_booleans in HA to enable/disable flows. This is a much cleaner way to do it. ## Trigger an exposed event node from a service call `nodered.trigger` -Event nodes that are triggered by a service call will have their status color -blue when `skip_condition` is `true` and when `false` it will stay green with -the text _(triggered)_ appended after the state in the status text. +Exposed nodes can be triggered from a service call. The service call is +`nodered.trigger` and it takes the following data properties: -Data properties of the service call: +### entity_id -**entity_id** +- Required -The only data property of the service call that is required is an `entity_id` of -the switch that is associated with a node in NR. +The entity_id of the exposed node to trigger. This is the entity_id of the node in Home Assistant. For example, if the entity_id of the node in Home Assistant is `switch.my_node`, then the entity_id to use in the service call is `switch.my_node`. -**trigger_entity_id** +### output_path -Will be the entity that triggers the node. It is optional and only required if -the node entity filter is not set to `exact`. +- Optional +- Defaults to 0 +- Can be a comma separated list of output paths -**skip_condition** +The output path of the node to send the message through. When this value is set to 0, the message will be sent through all output paths. If this value is set to 1, the message will be sent through the first output path. When this value is set to 2, the message will be sent through the second output path, and so on. -It can be used when you don't want the conditionals of the node to be check and -just have it pass the entity through. Defaults to `false` +### message -For the trigger: state node custom output will not be evaluated. +- Required -**output_path** - -When `skip_condition` is `true` this allows you to choose which output to send -the message through. Defaults to `true` the top output +The message the triggered node will output. This can be any valid JSON object. For example, if the message is `{ "payload": "hello world" }`, then the message will be sent to the node as `msg.payload` with the value of `hello world`. diff --git a/src/common/controllers/EposeAsController.ts b/src/common/controllers/EposeAsController.ts index 26d2692eb8b..7864694cea0 100644 --- a/src/common/controllers/EposeAsController.ts +++ b/src/common/controllers/EposeAsController.ts @@ -1,20 +1,13 @@ -import Joi from 'joi'; - import { HaEvent } from '../../homeAssistant'; import HomeAssistant from '../../homeAssistant/HomeAssistant'; import { EntityConfigNode } from '../../nodes/entity-config'; -import { HassEntity, HassStateChangedEvent } from '../../types/home-assistant'; -import { BaseNode } from '../../types/nodes'; +import { BaseNode, NodeMessage } from '../../types/nodes'; import Events from '../events/Events'; import { TriggerPayload } from '../integration/BidirectionalEntityIntegration'; import OutputController, { OutputControllerConstructor, } from './OutputController'; -interface TriggerEventValidationResult extends TriggerPayload { - entity: HassEntity; -} - export interface ExposeAsControllerConstructor extends OutputControllerConstructor { exposeAsConfigNode?: EntityConfigNode; @@ -34,13 +27,13 @@ export default abstract class ExposeAsController< this.homeAssistant = props.homeAssistant; if (props.exposeAsConfigNode) { - const exposeAsConfigEvents = new Events({ + this.exposeAsConfigEvents = new Events({ node: this.node, emitter: props.exposeAsConfigNode, }); - exposeAsConfigEvents.addListener( + this.exposeAsConfigEvents.addListener( HaEvent.AutomationTriggered, - this.onTriggered.bind(this) + this.#onTriggered.bind(this) ); } } @@ -49,63 +42,54 @@ export default abstract class ExposeAsController< return this.exposeAsConfigNode?.state?.isEnabled() ?? true; } - protected async validateTriggerMessage( - data: TriggerPayload - ): Promise { - const schema = Joi.object({ - entity_id: Joi.string().allow(null), - skip_condition: Joi.boolean().default(false), - output_path: Joi.boolean().default(true), - }); - - const validatedData = await schema.validateAsync(data); - - const entityId = validatedData.entity_id ?? this.getNodeEntityId(); - - if (!entityId) { - throw new Error( - 'Entity filter type is not set to exact and no entity_id found in trigger data.' - ); - } - - const entity = this.homeAssistant.websocket.getStates(entityId); - - if (!entity) { - throw new Error( - `entity_id provided by trigger event not found in cache: ${entityId}` - ); + // Find the number of outputs by looking at the number of wires + get #numberOfOutputs(): number { + if ('wires' in this.node && Array.isArray(this.node.wires)) { + return this.node.wires.length; } - return { - ...validatedData, - payload: data.payload, - entity, - }; + return 0; } - protected getEventPayload( - entity: HassEntity - ): Partial { - const payload = { - event_type: 'triggered', - entity_id: entity.entity_id, - event: { - entity_id: entity.entity_id, - old_state: entity, - new_state: entity, - }, - }; - - return payload; - } + #onTriggered(data: TriggerPayload) { + if (!this.isEnabled) return; + + const outputCount = this.#numberOfOutputs; + + // If there are no outputs, there is nothing to do + if (outputCount === 0) return; + + // Remove any paths that are greater than the number of outputs + const paths = data.output_path + .split(',') + .map((path) => Number(path)) + .filter((path) => path <= outputCount); + + // If there are no paths, there is nothing to do + if (paths.length === 0) return; + + let payload: NodeMessage | (NodeMessage | null)[]; + + // If there is only one path and it is 0 or 1, return the payload as is + if (paths.length === 1 && (paths.includes(0) || paths.includes(1))) { + payload = data.message; + } else if (paths.includes(0)) { + // create an array the size of the number of outputs and fill it with the payload + payload = new Array(outputCount).fill([data.message]); + } else { + // create an array and fill it with the message only if index exists in paths + payload = new Array(outputCount) + .fill(0) + .map((_, index) => + paths.includes(index + 1) ? data.message : null + ); + } - protected getNodeEntityId(): string | undefined { - return undefined; + this.status.setSuccess('home-assistant.status.triggered'); + this.node.send(payload); } - protected abstract onTriggered(data: TriggerPayload): void; - - public getexposeAsConfigEvents(): Events | undefined { + public getExposeAsConfigEvents(): Events | undefined { return this.exposeAsConfigEvents; } } diff --git a/src/common/integration/BidirectionalEntityIntegration.ts b/src/common/integration/BidirectionalEntityIntegration.ts index d0753f81c4f..ddf4b327d58 100644 --- a/src/common/integration/BidirectionalEntityIntegration.ts +++ b/src/common/integration/BidirectionalEntityIntegration.ts @@ -8,10 +8,10 @@ import UnidirectionalEntityIntegration, { } from './UnidirectionalEntityIntegration'; export interface TriggerPayload { - entity_id?: string; - skip_condition: boolean; - output_path: boolean; - payload?: boolean | string | number | Record; + // 0 = all, 1 = first, 2 = second etc. + // comma separated list of numbers is also possible + output_path: string; + message: Record; } interface TriggerEvent { diff --git a/src/nodes/switch/SwitchController.ts b/src/nodes/switch/SwitchController.ts index f5c44f8d5fe..7cabc2aa4d4 100644 --- a/src/nodes/switch/SwitchController.ts +++ b/src/nodes/switch/SwitchController.ts @@ -95,8 +95,8 @@ export default class SwitchController extends InputOutputController< const message: NodeMessage = { topic: 'triggered', }; - if (data.payload !== undefined) { - message.payload = data.payload; + if (data.message !== undefined) { + message.payload = data.message; } if (this.#isSwitchEntityEnabled) { diff --git a/src/nodes/tag/TagController.ts b/src/nodes/tag/TagController.ts index 13372052bf2..38efa4d810a 100644 --- a/src/nodes/tag/TagController.ts +++ b/src/nodes/tag/TagController.ts @@ -1,7 +1,6 @@ import cloneDeep from 'lodash.clonedeep'; import ExposeAsController from '../../common/controllers/EposeAsController'; -import { TriggerPayload } from '../../common/integration/BidirectionalEntityIntegration'; import { TAGS_ALL } from '../../const'; import { HassEvent } from '../../types/home-assistant'; import { NodeMessage } from '../../types/nodes'; @@ -56,11 +55,4 @@ export default class TagController extends ExposeAsController { this.status.setSuccess(`${tagName || tagId} scanned`); this.node.send(msg); } - - public onTriggered(data: TriggerPayload): void { - if (!this.isEnabled) return; - - this.status.setSuccess('home-assistant.status.triggered'); - this.node.send({ payload: data.payload }); - } }