Skip to content

Commit

Permalink
fix(event-state): Handle errors through for initial connection
Browse files Browse the repository at this point in the history
Fixes: #1102

fix(poll-state): Handle errors through for initial connection

fix(trigger-state): Handle errors through for initial connection
  • Loading branch information
zachowj committed Oct 9, 2023
1 parent 470b6ac commit e460713
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 63 deletions.
28 changes: 18 additions & 10 deletions src/common/errors/inputErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Joi from 'joi';
import { isError as isJoiError } from 'joi';

import { RED } from '../../globals';
import { NodeDone } from '../../types/nodes';
Expand All @@ -12,24 +12,32 @@ interface Dependencies {
status?: Status;
}

export function inputErrorHandler(e: unknown, deps?: Dependencies) {
export function getErrorData(e: unknown) {
let statusMessage = RED._('home-assistant.status.error');
if (e instanceof Joi.ValidationError) {
if (isJoiError(e)) {
statusMessage = RED._('home-assistant.status.validation_error');
deps?.done?.(e);
} else if (isHomeAssistantApiError(e)) {
deps?.done?.(new HomeAssistantError(e));
e = new HomeAssistantError(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));
e = new Error(e);
} else {
deps?.done?.(new Error(`Unrecognized error: ${JSON.stringify(e)}`));
e = new Error(
RED._('home-assistant.error.unrecognized_error', {
error: JSON.stringify(e),
})
);
}

return { error: e as Error, statusMessage };
}

export function inputErrorHandler(e: unknown, deps?: Dependencies) {
const { error, statusMessage } = getErrorData(e);

deps?.status?.setFailed(statusMessage);
deps?.done?.(error);
}

export function setTimeoutWithErrorHandling(
Expand Down
1 change: 1 addition & 0 deletions src/common/errors/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"no_connection": "No connection to Home Assistant",
"pattern_not_matched": "Pattern not matched",
"unknown_error": "Unknown error",
"unrecognized_error": "Unrecognized error: __error__",
"user_not_admin": "User required to have admin privileges in Home Assistant"
}
}
Expand Down
27 changes: 2 additions & 25 deletions src/common/events/Events.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import EventEmitter from 'events';
import Joi from 'joi';
import { Node } from 'node-red';

import { RED } from '../../globals';
import { NodeDone } from '../../types/nodes';
import BaseError from '../errors/BaseError';
import HomeAssistantError, {
isHomeAssistantApiError,
} from '../errors/HomeAssistantError';
import JSONataError from '../errors/JSONataError';
import { getErrorData } from '../errors/inputErrorHandler';
import Status from '../status/Status';

type EventHandler = (...args: any[]) => void | Promise<void>;
Expand Down Expand Up @@ -41,24 +35,7 @@ export default class Events {
// eslint-disable-next-line n/no-callback-literal
await callback(...args);
} catch (e) {
let statusMessage = RED._('home-assistant.status.error');
let error = e;
if (e instanceof Joi.ValidationError) {
error = new JSONataError(e);
statusMessage = RED._(
'home-assistant.status.validation_error'
);
} else if (isHomeAssistantApiError(e)) {
error = new HomeAssistantError(e);
} else if (e instanceof BaseError) {
statusMessage = e.statusMessage;
} else if (typeof e === 'string') {
error = new Error(e);
} else {
error = new Error(
`Unrecognized error ${JSON.stringify(e)}`
);
}
const { error, statusMessage } = getErrorData(e);
this.node.error(error);
this.#status?.setFailed(statusMessage);
}
Expand Down
17 changes: 13 additions & 4 deletions src/common/services/JSONataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function isJSONataError(error: unknown): error is JSONataError {

return (
'code' in error &&
'value' in error &&
'message' in error &&
'stack' in error &&
'message' in error
'token' in error
);
}

Expand All @@ -48,7 +48,15 @@ export default class JSONataService {
}

async evaluate(expression: string, objs: Record<string, any> = {}) {
const expr = RED.util.prepareJSONataExpression(expression, this.#node);
let expr: Expression;
try {
expr = RED.util.prepareJSONataExpression(expression, this.#node);
} catch (err) {
if (isJSONataError(err)) {
throw new JSONataError(err);
}
throw err;
}
const { entity, message, prevEntity } = objs;

expr.assign('entity', () => entity);
Expand All @@ -74,7 +82,8 @@ export default class JSONataService {
expr.assign('sampleSize', sampleSize);

try {
return evaluateJSONataExpression(expr, message);
// await here to catch JSONataError
return await evaluateJSONataExpression(expr, message);
} catch (err) {
if (isJSONataError(err)) {
throw new JSONataError(err);
Expand Down
2 changes: 1 addition & 1 deletion src/nodes/events-state/EventsStateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default class EventsStateController extends ExposeAsController {
if (isNaN(timer) || timer < 0) {
throw new ConfigError([
'server-state-changed.error.invalid_for',
{ for: timer },
{ for: timer, type: this.node.config.forType },
]);
}

Expand Down
22 changes: 19 additions & 3 deletions src/nodes/events-state/events.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { getErrorData } from '../../common/errors/inputErrorHandler';
import ClientEvents from '../../common/events/ClientEvents';
import Status from '../../common/status/Status';
import HomeAssistant from '../../homeAssistant/HomeAssistant';
import { createStateChangeEvents } from '../trigger-state/helpers';
import { EventsStateNode } from '.';
import EventsStateController from './EventsStateController';

export function startListeners(
clientEvents: ClientEvents,
controller: EventsStateController,
homeAssistant: HomeAssistant,
node: EventsStateNode,
clientEvents: ClientEvents
status: Status
) {
let eventTopic = `ha_events:state_changed`;

Expand All @@ -22,13 +26,25 @@ export function startListeners(
);

if (node.config.outputInitially) {
const generateStateChanges = async () => {
const events = createStateChangeEvents(homeAssistant);
for (const event of events) {
await controller
.onHaEventsStateChanged(event, true)
.catch((e) => {
const { error, statusMessage } = getErrorData(e);
status.setError(statusMessage);
node.error(error);
});
}
};
// Here for when the node is deploy without the server config being deployed
if (homeAssistant.isHomeAssistantRunning) {
controller.onDeploy();
generateStateChanges();
} else {
clientEvents.addListener(
'ha_client:initial_connection_ready',
controller.onStatesLoaded.bind(controller)
generateStateChanges
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/nodes/events-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ export default function eventsStateNode(
});
controller.setExposeAsConfigNode(exposeAsConfigNode);

startListeners(controller, homeAssistant, this, clientEvents);
startListeners(clientEvents, controller, homeAssistant, this, status);
}
2 changes: 1 addition & 1 deletion src/nodes/events-state/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"server-state-changed": {
"error": {
"entity_id_required": "Entity ID is required",
"invalid_for": "`Invalid value for 'for': __for__`"
"invalid_for": "Invalid config value for 'for': \"__for__\" (type: __type__)"
},
"label": {
"entity": "Entity",
Expand Down
11 changes: 6 additions & 5 deletions src/nodes/poll-state/PollStateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import OutputController, {
OutputControllerConstructor,
} from '../../common/controllers/OutputController';
import ConfigError from '../../common/errors/ConfigError';
import { getErrorData } from '../../common/errors/inputErrorHandler';
import ComparatorService from '../../common/services/ComparatorService';
import TransformState, { TransformType } from '../../common/TransformState';
import { TypedInputTypes } from '../../const';
Expand Down Expand Up @@ -130,11 +131,11 @@ export default class PollStateController extends ExposeAsController {
clearInterval(this.#timer);
this.#updateinterval = interval;
this.#timer = setInterval(async () => {
try {
await this.onTimer();
} catch (e) {
this.node.error(e);
}
await this.onTimer().catch((e) => {
const { error, statusMessage } = getErrorData(e);
this.status.setError(statusMessage);
this.node.error(error);
});
}, this.#updateinterval);
}
}
Expand Down
22 changes: 18 additions & 4 deletions src/nodes/poll-state/events.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { getErrorData } from '../../common/errors/inputErrorHandler';
import ClientEvents from '../../common/events/ClientEvents';
import Status from '../../common/status/Status';
import { HA_CLIENT_READY, HA_EVENTS, TypedInputTypes } from '../../const';
import { getEntitiesFromJsonata } from '../../helpers/utils';
import { HaEvent } from '../../homeAssistant';
import HomeAssistant from '../../homeAssistant/HomeAssistant';
import { PollStateNodeProperties } from '.';
import { PollStateNode, PollStateNodeProperties } from '.';
import PollStateController from './PollStateController';

export function startListeners({
export async function startListeners({
config,
clientEvents,
controller,
homeAssistant,
node,
status,
}: {
config: PollStateNodeProperties;
clientEvents: ClientEvents;
controller: PollStateController;
homeAssistant: HomeAssistant;
node: PollStateNode;
status: Status;
}) {
if (config.outputOnChanged) {
clientEvents.addListener(
Expand All @@ -26,7 +32,11 @@ export function startListeners({

if (config.outputInitially) {
if (homeAssistant.isHomeAssistantRunning) {
controller.onTimer();
await controller.onTimer().catch((e) => {
const { error, statusMessage } = getErrorData(e);
status.setError(statusMessage);
node.error(error);
});
} else {
clientEvents.addListener(
'ha_client:initial_connection_ready',
Expand All @@ -36,7 +46,11 @@ export function startListeners({
}

if (homeAssistant.isHomeAssistantRunning) {
controller.onIntervalUpdate();
await controller.onIntervalUpdate().catch((e) => {
const { error, statusMessage } = getErrorData(e);
status.setError(statusMessage);
node.error(error);
});
} else {
clientEvents.addListener(
HA_CLIENT_READY,
Expand Down
2 changes: 2 additions & 0 deletions src/nodes/poll-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,7 @@ export default function pollStateNode(
config: this.config,
controller,
homeAssistant,
node: this,
status,
});
}
20 changes: 11 additions & 9 deletions src/nodes/trigger-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Joi from 'joi';

import { createControllerDependencies } from '../../common/controllers/helpers';
import ConfigError from '../../common/errors/ConfigError';
import { getErrorData } from '../../common/errors/inputErrorHandler';
import ClientEvents from '../../common/events/ClientEvents';
import Events from '../../common/events/Events';
import ComparatorService from '../../common/services/ComparatorService';
Expand Down Expand Up @@ -157,22 +158,23 @@ export default function triggerState(
);

if (controller.isEnabled && this.config.outputInitially) {
const emitEvents = () => {
const generateStateChanges = async () => {
const events = createStateChangeEvents(homeAssistant);
events.forEach((event) => {
clientEvents.emit(
`ha_events:state_changed:${event.entity_id}`,
event
);
});
for (const event of events) {
await controller.onEntityStateChanged(event).catch((e) => {
const { error, statusMessage } = getErrorData(e);
status.setError(statusMessage);
this.error(error);
});
}
};
// Here for when the node is deploy without the server config being deployed
if (homeAssistant.isHomeAssistantRunning) {
emitEvents();
generateStateChanges();
} else {
clientEvents.addListener(
'ha_client:initial_connection_ready',
emitEvents
generateStateChanges
);
}
}
Expand Down

0 comments on commit e460713

Please sign in to comment.