diff --git a/locales/en-US/wait-until.json b/locales/en-US/wait-until.json index 646b7d06cb..60b54af641 100644 --- a/locales/en-US/wait-until.json +++ b/locales/en-US/wait-until.json @@ -6,6 +6,11 @@ "entity_location": "Entity location", "timeout": "Timeout", "wait_until": "Wait until" + }, + "status": { + "timed_out": "timed out", + "true": "true", + "waiting": "waiting" } } } diff --git a/src/common/controllers/InputOutputController.ts b/src/common/controllers/InputOutputController.ts index b314ffcb58..f85d98dda5 100644 --- a/src/common/controllers/InputOutputController.ts +++ b/src/common/controllers/InputOutputController.ts @@ -1,7 +1,6 @@ import Joi from 'joi'; import { NodeMessageInFlow } from 'node-red'; -import { RED } from '../../globals'; import { BaseNode, NodeDone, @@ -9,7 +8,7 @@ import { NodeProperties, NodeSend, } from '../../types/nodes'; -import BaseError from '../errors/BaseError'; +import { inputErrorHandler } from '../errors/inputErrorHandler'; import { NodeEvent } from '../events/Events'; import Integration from '../integration/Integration'; import InputService, { ParsedMessage } from '../services/InputService'; @@ -76,7 +75,7 @@ export default abstract class InputOutputController< return; } } catch (error) { - this.#inputErrorHandler(error, done); + inputErrorHandler(error, { done, status: this.status }); } } } @@ -94,7 +93,7 @@ export default abstract class InputOutputController< done, }); } catch (e) { - this.#inputErrorHandler(e, done); + inputErrorHandler(e, { done, status: this.status }); } } @@ -105,24 +104,6 @@ export default abstract class InputOutputController< send, }: InputProperties): Promise; - #inputErrorHandler(e: unknown, done?: NodeDone) { - let statusMessage = RED._('home-assistant.status.error'); - if (e instanceof Joi.ValidationError) { - statusMessage = RED._('home-assistant.status.validation_error'); - done?.(e); - } else if (e instanceof BaseError) { - statusMessage = e.statusMessage; - done?.(e); - } else if (e instanceof Error) { - done?.(e); - } else if (typeof e === 'string') { - done?.(new Error(e)); - } else { - done?.(new Error(`Unrecognised error: ${e}`)); - } - this.status.setFailed(statusMessage); - } - protected addOptionalInput( key: string, schema: Joi.ObjectSchema, diff --git a/src/common/errors/inputErrorHandler.ts b/src/common/errors/inputErrorHandler.ts new file mode 100644 index 0000000000..a603fc5aa4 --- /dev/null +++ b/src/common/errors/inputErrorHandler.ts @@ -0,0 +1,44 @@ +import Joi from 'joi'; + +import { RED } from '../../globals'; +import { NodeDone } from '../../types/nodes'; +import Status from '../status/Status'; +import BaseError from './BaseError'; +interface Dependencies { + done?: NodeDone; + status?: Status; +} + +export function inputErrorHandler(e: unknown, deps?: Dependencies) { + let statusMessage = RED._('home-assistant.status.error'); + if (e instanceof Joi.ValidationError) { + statusMessage = RED._('home-assistant.status.validation_error'); + deps?.done?.(e); + } else if (e instanceof BaseError) { + statusMessage = e.statusMessage; + deps?.done?.(e); + } else if (e instanceof Error) { + deps?.done?.(e); + } else if (typeof e === 'string') { + deps?.done?.(new Error(e)); + } else { + deps?.done?.(new Error(`Unrecognised error: ${e}`)); + } + deps?.status?.setFailed(statusMessage); +} + +export function setTimeoutWithErrorHandling( + callback: (...args: any[]) => void, + timeout: number, + deps?: Dependencies +): NodeJS.Timeout { + const timeoutId = setTimeout(() => { + try { + callback(); + } catch (e) { + inputErrorHandler(e, deps); + } + }, timeout); + + return timeoutId; +} diff --git a/src/nodes/wait-until/WaitUntilController.ts b/src/nodes/wait-until/WaitUntilController.ts index 1cd71e545f..a802988c82 100644 --- a/src/nodes/wait-until/WaitUntilController.ts +++ b/src/nodes/wait-until/WaitUntilController.ts @@ -8,9 +8,12 @@ import InputOutputController, { InputProperties, } from '../../common/controllers/InputOutputController'; import InputError from '../../common/errors/InputError'; +import { setTimeoutWithErrorHandling } from '../../common/errors/inputErrorHandler'; import ClientEvents from '../../common/events/ClientEvents'; import ComparatorService from '../../common/services/ComparatorService'; +import { DataSource } from '../../common/services/InputService'; import JSONataService from '../../common/services/JSONataService'; +import { EntityFilterType, TypedInputTypes } from '../../const'; import { renderTemplate } from '../../helpers/mustache'; import { getTimeInMilliseconds, @@ -118,7 +121,7 @@ export default class WaitUntil extends InputOutputController< const { send, done } = this.#savedConfig; clearTimeout(this.#timeoutId); this.#active = false; - this.status.setSuccess('true'); + this.status.setSuccess('ha-wait-until.status.true'); event.new_state.timeSinceChangedMs = Date.now() - new Date(event.new_state.last_changed).getTime(); @@ -136,7 +139,12 @@ export default class WaitUntil extends InputOutputController< done(); } - async onInput({ message, parsedMessage, send, done }: InputProperties) { + protected async onInput({ + message, + parsedMessage, + send, + done, + }: InputProperties) { clearTimeout(this.#timeoutId); const config: SavedConfig = { @@ -155,8 +163,8 @@ export default class WaitUntil extends InputOutputController< // Render mustache templates in the entity id field if ( - parsedMessage.entityId.source === 'config' && - config.entityIdFilterType === 'exact' + parsedMessage.entityId.source === DataSource.Config && + config.entityIdFilterType === EntityFilterType.Exact ) { config.entityId = renderTemplate( parsedMessage.entityId.value, @@ -170,8 +178,8 @@ export default class WaitUntil extends InputOutputController< // it to timeout let timeout = Number(config.timeout); if ( - parsedMessage.timeout.source === 'config' && - this.node.config.timeoutType === 'jsonata' + parsedMessage.timeout.source === DataSource.Config && + this.node.config.timeoutType === TypedInputTypes.JSONata ) { timeout = this.#jsonataService.evaluate( parsedMessage.timeout.value, @@ -192,7 +200,7 @@ export default class WaitUntil extends InputOutputController< this.#clientEvents.removeListeners(); const eventTopic = `ha_events:state_changed${ - config.entityIdFilterType === 'exact' + config.entityIdFilterType === EntityFilterType.Exact ? `:${config.entityId.trim()}` : '' }`; @@ -203,37 +211,41 @@ export default class WaitUntil extends InputOutputController< this.#savedMessage = message; this.#active = true; - let statusText = 'waiting'; + let statusText = 'ha-wait-until.status.waiting'; if (timeout > 0) { statusText = getWaitStatusText(timeout, config.timeoutUnits); timeout = getTimeInMilliseconds(timeout, config.timeoutUnits); - this.#timeoutId = setTimeout(() => { - const state = Object.assign( - {}, - this.#homeAssistant.websocket.getStates( - config.entityId - ) as HassEntity - ); - - state.timeSinceChangedMs = - Date.now() - new Date(state.last_changed).getTime(); - - this.setCustomOutputs( - this.node.config.outputProperties, - message, - { - entity: state, - config: this.node.config, - } - ); - - this.#active = false; - this.status.setFailed('timed out'); - send([null, message]); - done(); - }, timeout); + this.#timeoutId = setTimeoutWithErrorHandling( + () => { + const state = Object.assign( + {}, + this.#homeAssistant.websocket.getStates( + config.entityId + ) as HassEntity + ); + + state.timeSinceChangedMs = + Date.now() - new Date(state.last_changed).getTime(); + + this.setCustomOutputs( + this.node.config.outputProperties, + message, + { + entity: state, + config: this.node.config, + } + ); + + this.#active = false; + this.status.setFailed('ha-wait-until.status.timted_out'); + send([null, message]); + done(); + }, + timeout, + { done, status: this.status } + ); } this.status.setText(statusText); this.#savedConfig = config; @@ -241,7 +253,7 @@ export default class WaitUntil extends InputOutputController< // Only check current state when filter type is exact if ( config.checkCurrentState === true && - config.entityIdFilterType === 'exact' + config.entityIdFilterType === EntityFilterType.Exact ) { const currentState = this.#homeAssistant.websocket.getStates( config.entityId @@ -256,6 +268,12 @@ export default class WaitUntil extends InputOutputController< } } + protected onClose(removed: boolean, done?: NodeDone): void { + this.#clientEvents.removeListeners(); + clearTimeout(this.#timeoutId); + done?.(); + } + #onResetInput(): boolean { clearTimeout(this.#timeoutId); this.#active = false;