Skip to content

Commit

Permalink
fix(wait-until): Catch errors thrown during timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
zachowj committed Nov 13, 2022
1 parent cdfca60 commit eab4c19
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 56 deletions.
5 changes: 5 additions & 0 deletions locales/en-US/wait-until.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"entity_location": "Entity location",
"timeout": "Timeout",
"wait_until": "Wait until"
},
"status": {
"timed_out": "timed out",
"true": "true",
"waiting": "waiting"
}
}
}
25 changes: 3 additions & 22 deletions src/common/controllers/InputOutputController.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import Joi from 'joi';
import { NodeMessageInFlow } from 'node-red';

import { RED } from '../../globals';
import {
BaseNode,
NodeDone,
NodeMessage,
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';
Expand Down Expand Up @@ -76,7 +75,7 @@ export default abstract class InputOutputController<
return;
}
} catch (error) {
this.#inputErrorHandler(error, done);
inputErrorHandler(error, { done, status: this.status });
}
}
}
Expand All @@ -94,7 +93,7 @@ export default abstract class InputOutputController<
done,
});
} catch (e) {
this.#inputErrorHandler(e, done);
inputErrorHandler(e, { done, status: this.status });
}
}

Expand All @@ -105,24 +104,6 @@ export default abstract class InputOutputController<
send,
}: InputProperties): Promise<void>;

#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,
Expand Down
44 changes: 44 additions & 0 deletions src/common/errors/inputErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -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;
}
86 changes: 52 additions & 34 deletions src/nodes/wait-until/WaitUntilController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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 = {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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()}`
: ''
}`;
Expand All @@ -203,45 +211,49 @@ 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;

// 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
Expand All @@ -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;
Expand Down

0 comments on commit eab4c19

Please sign in to comment.