Skip to content

Commit

Permalink
refactor!: Expose as trigger only passes on a message payload (#1019)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Expose as trigger no longer handles condition validation. It will only pass on the message sent through the service call and which outputs are selected.
  • Loading branch information
zachowj committed Aug 22, 2023
1 parent 57bd871 commit c161216
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 94 deletions.
33 changes: 14 additions & 19 deletions docs/guide/custom_integration/exposed-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
106 changes: 45 additions & 61 deletions src/common/controllers/EposeAsController.ts
Original file line number Diff line number Diff line change
@@ -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<T extends BaseNode>
extends OutputControllerConstructor<T> {
exposeAsConfigNode?: EntityConfigNode;
Expand All @@ -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)
);
}
}
Expand All @@ -49,63 +42,54 @@ export default abstract class ExposeAsController<
return this.exposeAsConfigNode?.state?.isEnabled() ?? true;
}

protected async validateTriggerMessage(
data: TriggerPayload
): Promise<TriggerEventValidationResult> {
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<HassStateChangedEvent> {
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;
}
}
8 changes: 4 additions & 4 deletions src/common/integration/BidirectionalEntityIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
// 0 = all, 1 = first, 2 = second etc.
// comma separated list of numbers is also possible
output_path: string;
message: Record<string, unknown>;
}

interface TriggerEvent {
Expand Down
4 changes: 2 additions & 2 deletions src/nodes/switch/SwitchController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 0 additions & 8 deletions src/nodes/tag/TagController.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -56,11 +55,4 @@ export default class TagController extends ExposeAsController<TagNode> {
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 });
}
}

0 comments on commit c161216

Please sign in to comment.