Skip to content

Commit

Permalink
feat(issues): Implement issue detection for Home Assistant nodes
Browse files Browse the repository at this point in the history
- Added functionality to check Home Assistant nodes for issues on deploy and at regular intervals.
- Checks include validation of entity IDs, area IDs, device IDs, floor IDs, and label IDs.
  • Loading branch information
zachowj committed Aug 26, 2024
1 parent c0126dd commit 61eb103
Show file tree
Hide file tree
Showing 44 changed files with 1,723 additions and 146 deletions.
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
{
"files": ["*.json", "*.md", "*.yaml", "*.yml"],
"options": { "tabWidth": 2, "singleQuote": false }
},
{
"files": "*.handlebars",
"options": {
"singleQuote": false
}
}
]
}
22 changes: 12 additions & 10 deletions src/common/State.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { storageService } from '../globals';
import { BaseNode, NodeDone } from '../types/nodes';
import { NodeEvent } from './events/Events';
import Storage, { LastPayloadData } from './Storage';
import { LastPayloadData } from './services/Storage';

export default class State {
#enabled = true;
Expand All @@ -9,15 +10,16 @@ export default class State {

constructor(node: BaseNode) {
this.#node = node;
this.#enabled = Storage.getNodeData(node.id, 'isEnabled') ?? true;
this.#lastPayload = Storage.getNodeData(node.id, 'lastPayload');
this.#enabled =
storageService.getNodeData(node.id, 'isEnabled') ?? true;
this.#lastPayload = storageService.getNodeData(node.id, 'lastPayload');

node.on(NodeEvent.Close, this.#onClose.bind(this));
}

async #onClose(removed: boolean, done: NodeDone) {
if (removed) {
await Storage.removeNodeData(this.#node.id).catch(done);
await storageService.removeNodeData(this.#node.id).catch(done);
}
done();
}
Expand All @@ -28,17 +30,17 @@ export default class State {

async setEnabled(state: boolean) {
this.#enabled = state;
await Storage.saveNodeData(this.#node.id, 'isEnabled', state).catch(
this.#node.error,
);
await storageService
.saveNodeData(this.#node.id, 'isEnabled', state)
.catch(this.#node.error);
this.#emitChange();
}

async setLastPayload(payload: LastPayloadData) {
this.#lastPayload = payload;
await Storage.saveNodeData(this.#node.id, 'lastPayload', payload).catch(
this.#node.error,
);
await storageService
.saveNodeData(this.#node.id, 'lastPayload', payload)
.catch(this.#node.error);
this.#emitChange();
}

Expand Down
65 changes: 65 additions & 0 deletions src/common/services/IssueService/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { BaseNodeProperties } from '../../../types/nodes';
import { IssueType } from '.';
import { getHomeAssistant, isDynamicValue } from './helpers';

export function getInvalidIds(
type: IssueType,
config: BaseNodeProperties,
ids: string | string[] | undefined,
): string[] {
const invalidIds: string[] = [];

if (!ids) {
return invalidIds;
}

if (!Array.isArray(ids)) {
ids = [ids];
}

const ha = getHomeAssistant(config);
if (!ha?.websocket.isStatesLoaded) {
return invalidIds;
}

for (const id of ids) {
if (isDynamicValue(id)) {
continue;
}

switch (type) {
case IssueType.AreaId:
if (!ha.websocket.getArea(id)) {
invalidIds.push(id);
}
break;
case IssueType.DeviceId:
if (!ha.websocket.getDevice(id)) {
invalidIds.push(id);
}
break;
case IssueType.EntityId:
if (!ha.websocket.getEntity(id)) {
invalidIds.push(id);
}
break;
case IssueType.FloorId:
if (!ha.websocket.getFloor(id)) {
invalidIds.push(id);
}
break;
case IssueType.LabelId:
if (!ha.websocket.getLabel(id)) {
invalidIds.push(id);
}
break;
case IssueType.StateId:
if (!ha.websocket.getStates(id)) {
invalidIds.push(id);
}
break;
}
}

return invalidIds;
}
99 changes: 99 additions & 0 deletions src/common/services/IssueService/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { NodeDef } from 'node-red';

import { NodeType } from '../../../const';
import { RED } from '../../../globals';
import { isString } from '../../../helpers/assert';
import { containsMustache, isNodeRedEnvVar } from '../../../helpers/utils';
import { homeAssistantConnections } from '../../../homeAssistant';
import { BaseNodeProperties } from '../../../types/nodes';
import { Issue } from '.';

/**
* Get the Home Assistant instance from the node.
*
* @param node The node to get the Home Assistant instance from
* @returns The Home Assistant instance
*/
export function getHomeAssistant(node: BaseNodeProperties) {
if (node.type === NodeType.Server) {
return homeAssistantConnections.get(node.id);
}
if ('server' in node && isString(node.server)) {
const server = RED.nodes.getNode(node.server);
if (!server) {
return;
}
return homeAssistantConnections.get(server.id);
}
}

/**
* Get the server ID from the node.
*
* @param node The node to get the server ID from
* @returns The server ID
*/
export function getServerId(node: BaseNodeProperties) {
if (node.type === NodeType.Server) {
return node.id;
}

return node.server;
}

/**
* Check if the issue is included in the array of issues.
*
* @param issues The array of issues to check
* @param issue The issue to check for
* @returns {boolean} True if the issue is included in the array of issues
*/
export function includesIssue(issues: Issue[], issue: Issue) {
return issues.some((i) => isSameIssue(i, issue));
}

/**
* Check if the value is a mustache template or a Node-RED environment variable.
*
* @param value
* @returns {boolean}
*/
export function isDynamicValue(value: string): boolean {
return containsMustache(value) || isNodeRedEnvVar(value);
}

/**
* Check if all the registries, states and services data are loaded.
*
* @param node The node to check
* @returns {boolean} True if the Home Assistant data is loaded
*/
export function isHomeAssistantDataLoaded(node: BaseNodeProperties) {
const ha = getHomeAssistant(node);
return ha?.websocket.isAllRegistriesLoaded;
}

/**
* Check if the node is a Home Assistant node.
*
* @param node The node to check
* @returns {boolean} True if the node is a Home Assistant node
*/
export function isHomeAssistantNode(node: NodeDef): node is BaseNodeProperties {
return Object.values(NodeType).includes(node.type as NodeType);
}

/**
* Check if the issues are the same.
*
* @param a The first issue to compare
* @param b The second issue to compare
* @returns True if the issues are the same
*/
function isSameIssue(a: Issue, b: Issue) {
return (
a.type === b.type &&
a.identity === b.identity &&
a.message === b.message
);
}
Loading

0 comments on commit 61eb103

Please sign in to comment.