Skip to content

Commit

Permalink
feat(trigger-state): Update listener to allow listening to multiple t…
Browse files Browse the repository at this point in the history
…ypes at once

This commit updates the error message in the `trigger-state` module to provide a more descriptive explanation when an entity is required but not provided. The code changes in `trigger-state/locale.json` modify the `entity_id_required` error message to `entity_required`. This improvement enhances the clarity and usability of the `trigger-state` node.

#fixes: 1103
#fixes: 1402
  • Loading branch information
zachowj committed Aug 5, 2024
1 parent 392e8ad commit a14a7f3
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 86 deletions.
19 changes: 8 additions & 11 deletions src/nodes/events-state/EventsStateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,15 @@ export default class EventsStateController extends ExposeAsController {
const oldState = evt.event?.old_state?.state;
const newState = evt.event?.new_state?.state;

let found = false;
Object.entries(this.node.config.entities).some(([type, ids]) => {
ids?.some((id) => {
if (shouldIncludeEvent(evt.entity_id, id, type)) {
found = true;
}
return found;
});
return found;
});
const valid = Object.entries(this.node.config.entities).some(
([type, ids]) => {
return ids?.some((id) =>
shouldIncludeEvent(evt.entity_id, id, type),
);
},
);

if (!found) {
if (!valid) {
return false;
}

Expand Down
23 changes: 15 additions & 8 deletions src/nodes/trigger-state/TriggerStateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,21 @@ export default class TriggerStateController extends ExposeAsController {

const eventMessage = cloneDeep(evt);

if (
!eventMessage.event.new_state ||
!shouldIncludeEvent(
eventMessage.entity_id,
this.node.config.entityId,
this.node.config.entityIdType,
)
) {
if (!eventMessage.event.new_state) {
return;
}

// Check if the entity_id is in the list of entities to watch
const valid = Object.entries(this.node.config.entities).some(
([type, ids]) => {
return ids?.some((id) =>
shouldIncludeEvent(eventMessage.entity_id, id, type),
);
},
);

// If the entity_id is not in the list of entities to watch, return
if (!valid) {
return;
}

Expand Down
24 changes: 1 addition & 23 deletions src/nodes/trigger-state/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,8 @@
<input type="text" id="node-input-server" />
</div>

<!-- Entity ID Filter and Filter Type -->
<div class="form-row">
<label
for="node-input-entityId"
data-i18n="trigger-state.label.entity"
></label>
<select type="text" id="node-input-entityIdType">
<option
value="exact"
data-i18n="trigger-state.label.entity_filter_type_option.exact"
></option>
<option
value="list"
data-i18n="trigger-state.label.entity_filter_type_option.list"
></option>
<option
value="regex"
data-i18n="trigger-state.label.entity_filter_type_option.regex"
></option>
<option
value="substring"
data-i18n="trigger-state.label.entity_filter_type_option.substring"
></option>
</select>
<div id="entity-list"></div>
</div>

<div class="form-row">
Expand Down
90 changes: 66 additions & 24 deletions src/nodes/trigger-state/editor.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { EditorNodeDef, EditorRED } from 'node-red';

import { IdSelectorType } from '../../common/const';
import { TransformType } from '../../common/TransformState';
import {
ComparatorType,
EntityType,
NodeType,
TypedInputTypes,
} from '../../const';
import EntitySelector from '../../editor/components/EntitySelector';
import IdSelector, {
getSelectedIds,
} from '../../editor/components/idSelector/IdSelector';
import * as exposeNode from '../../editor/exposenode';
import ha, { NodeCategory, NodeColor } from '../../editor/ha';
import * as haServer from '../../editor/haserver';
import { i18n } from '../../editor/i18n';
import { HassNodeProperties } from '../../editor/types';
import { saveEntityType } from '../entity-config/editor/helpers';
import { EntitySelector } from '../events-state';
import {
ComparatorPropertyType,
Constraint,
Expand All @@ -26,15 +30,16 @@ import {
declare const RED: EditorRED;

interface TriggerStateEditorNodeProperties extends HassNodeProperties {
entityId: string | string[];
entityIdType: string;
entities: EntitySelector;
constraints: Constraint[];
customOutputs: CustomOutput[];
outputInitially: boolean;
stateType: TransformType;
enableInput: boolean;

// deprecated but still needed for migration
entityId: undefined;
entityIdType: undefined;
exposeToHomeAssistant: undefined;
haConfig: undefined;
entityid: undefined;
Expand Down Expand Up @@ -72,7 +77,16 @@ const TriggerStateEditor: EditorNodeDef<TriggerStateEditorNodeProperties> = {
icon: 'font-awesome/fa-map-signs',
paletteLabel: 'trigger: state',
label: function () {
return this.name || `trigger-state: ${this.entityId}`;
let label: string[] = [];
if (this.entities) {
Object.entries(this.entities).forEach(([, ids]) => {
if (Array.isArray(ids) && ids?.length) {
label = [...label, ...ids];
}
});
}

return this.name || `trigger-state: ${label}`;
},
labelStyle: ha.labelStyle,
defaults: {
Expand All @@ -88,8 +102,29 @@ const TriggerStateEditor: EditorNodeDef<TriggerStateEditorNodeProperties> = {
filter: (config) => config.entityType === EntityType.Switch,
required: false,
},
entityId: { value: '', required: true },
entityIdType: { value: 'exact' },
entities: {
value: {
[IdSelectorType.Entity]: [''],
[IdSelectorType.Substring]: [],
[IdSelectorType.Regex]: [],
},
// TODO: After v1.0 uncomment validation because now it will throw errors for nodes that have a version prior to 6
// validate: (value) => {
// if (!value) {
// return false;
// }

// Object.entries(value).forEach(([_, ids]) => {
// if (Array.isArray(ids)) {
// if (ids.some((id) => id.length)) {
// return true;
// }
// }
// });

// return false;
// },
},
debugEnabled: { value: false },
constraints: {
value: [
Expand All @@ -110,6 +145,8 @@ const TriggerStateEditor: EditorNodeDef<TriggerStateEditorNodeProperties> = {
enableInput: { value: false },

// deprecated but still needed for migration
entityId: { value: undefined },
entityIdType: { value: undefined },
exposeToHomeAssistant: { value: undefined },
haConfig: { value: undefined },
entityid: { value: undefined },
Expand All @@ -124,16 +161,25 @@ const TriggerStateEditor: EditorNodeDef<TriggerStateEditorNodeProperties> = {
const $constraintList = $('#constraint-list');
const $outputList = $('#output-list');

haServer.init(this, '#node-input-server', (serverId) => {
entitySelector.serverChanged(serverId);
haServer.init(this, '#node-input-server', () => {
idSelector.refreshOptions();
});
saveEntityType(EntityType.Switch, 'exposeAsEntityConfig');
const entitySelector = new EntitySelector({
filterTypeSelector: '#node-input-entityIdType',
entityId: this.entityId,
serverId: haServer.getSelectedServerId(),

const idSelector = new IdSelector({
element: '#entity-list',
types: [
IdSelectorType.Entity,
IdSelectorType.Substring,
IdSelectorType.Regex,
],
headerText: i18n('server-state-changed.label.entities'),
});
Object.entries(this.entities).forEach(([type, ids]) => {
ids?.forEach((id) => {
idSelector.addId(type as IdSelectorType, id);
});
});
$('#dialog-form').data('entitySelector', entitySelector);

let availableEntities: string[] = [];
let availableProperties: string[] = [];
Expand Down Expand Up @@ -566,17 +612,13 @@ const TriggerStateEditor: EditorNodeDef<TriggerStateEditorNodeProperties> = {

this.constraints = constraints;
this.customOutputs = outputs;
const entitySelector = $('#dialog-form').data(
'entitySelector',
) as EntitySelector;
this.entityId = entitySelector.entityId;
entitySelector.destroy();
},
oneditcancel: function () {
const entitySelector = $('#dialog-form').data(
'entitySelector',
) as EntitySelector;
entitySelector.destroy();

const entities = getSelectedIds('#entity-list');
this.entities = {
[IdSelectorType.Entity]: entities[IdSelectorType.Entity],
[IdSelectorType.Substring]: entities[IdSelectorType.Substring],
[IdSelectorType.Regex]: entities[IdSelectorType.Regex],
};
},
};

Expand Down
50 changes: 32 additions & 18 deletions src/nodes/trigger-state/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Joi from 'joi';

import { IdSelectorType } from '../../common/const';
import { createControllerDependencies } from '../../common/controllers/helpers';
import ConfigError from '../../common/errors/ConfigError';
import { getErrorData } from '../../common/errors/inputErrorHandler';
Expand All @@ -9,21 +10,21 @@ import ComparatorService from '../../common/services/ComparatorService';
import InputService from '../../common/services/InputService';
import State from '../../common/State';
import EventsStatus from '../../common/status/EventStatus';
import { StatusColor, StatusShape } from '../../common/status/Status';
import TransformState, { TransformType } from '../../common/TransformState';
import { EntityFilterType } from '../../const';
import { RED } from '../../globals';
import { migrate } from '../../helpers/migrate';
import { getExposeAsConfigNode, getServerConfigNode } from '../../helpers/node';
import { getHomeAssistant } from '../../homeAssistant';
import { BaseNode, BaseNodeProperties } from '../../types/nodes';
import { EntitySelector } from '../events-state';
import { Constraint, CustomOutput, DISABLE, ENABLE } from './const';
import { createStateChangeEvents } from './helpers';
import TriggerStateController from './TriggerStateController';
import TriggerStateStatus from './TriggerStateStatus';

export interface TriggerStateProperties extends BaseNodeProperties {
entityId: string | string[];
entityIdType: string;
entities: EntitySelector;
debugEnabled: boolean;
constraints: Constraint[];
customOutputs: CustomOutput[];
Expand Down Expand Up @@ -61,8 +62,18 @@ export default function triggerState(

this.config = migrate(config);

if (!this.config.entityId) {
throw new ConfigError('trigger-state.error.enttity_id_required');
if (
!this.config?.entities[IdSelectorType.Entity]?.length &&
!this.config?.entities[IdSelectorType.Substring]?.length &&
!this.config?.entities[IdSelectorType.Regex]?.length
) {
const error = new ConfigError('trigger-state.error.entity_required');
this.status({
fill: StatusColor.Red,
shape: StatusShape.Ring,
text: error.statusMessage,
});
throw error;
}

const serverConfigNode = getServerConfigNode(this.config.server);
Expand Down Expand Up @@ -142,21 +153,24 @@ export default function triggerState(
);
}

let eventTopic = 'ha_events:state_changed';

// If the entity id type is exact, then we need to listen to a specific entity
if (this.config.entityIdType === EntityFilterType.Exact) {
const id = Array.isArray(this.config.entityId)
? this.config.entityId[0]
: this.config.entityId;
eventTopic = `${eventTopic}:${id.trim()}`;
if (
config.entities[IdSelectorType.Substring].length === 0 &&
config.entities[IdSelectorType.Regex].length === 0
) {
for (const entity of config.entities[IdSelectorType.Entity]) {
const eventTopic = `ha_events:state_changed:${entity}`;
clientEvents.addListener(
eventTopic,
controller.onEntityStateChanged.bind(controller),
);
}
} else {
clientEvents.addListener(
'ha_events:state_changed',
controller.onEntityStateChanged.bind(controller),
);
}

clientEvents.addListener(
eventTopic,
controller.onEntityStateChanged.bind(controller),
);

if (controller.isEnabled && this.config.outputInitially) {
const generateStateChanges = async () => {
const events = createStateChangeEvents(homeAssistant);
Expand Down
2 changes: 1 addition & 1 deletion src/nodes/trigger-state/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"error": {
"custom_output_message_needs_to_be_object": "Custom output message needs to be an key/value object",
"entity_id_not_found": "Entity \"__entity_id__\" not found",
"entity_id_required": "Entity ID is required"
"entity_required": "An entity is required"
},
"label": {
"comparator_option": {
Expand Down
Loading

0 comments on commit a14a7f3

Please sign in to comment.