UNPKG

node-red-contrib-home-assistant-websocket

Version:
237 lines (236 loc) 9.15 kB
"use strict"; const selectn = require('selectn'); const { merge } = require('lodash'); const ComparatorService = require('../common/services/ComparatorService').default; const State = require('../common/State').default; const TransformState = require('../common/TransformState').default; const { createControllerDependencies, } = require('../common/controllers/helpers'); const { debugToClient } = require('../helpers/node'); const { getHomeAssistant } = require('../homeAssistant'); const DEFAULT_NODE_OPTIONS = { config: { debugenabled: {}, name: {}, server: { isNode: true }, version: (nodeDef) => nodeDef.version || 0, }, input: { topic: { messageProp: 'topic' }, payload: { messageProp: 'payload' }, }, }; class BaseNode { constructor({ node, config, RED, status, nodeOptions = {} }) { var _a, _b; this.node = node; this.RED = RED; this.options = merge({}, DEFAULT_NODE_OPTIONS, nodeOptions); this._eventHandlers = _eventHandlers; this._internals = _internals; this.status = status; this.state = new State(this.node); // TODO: move this to initializer and pass in as a parameter this.nodeConfig = Object.entries(this.options.config).reduce((acc, [key, value]) => { if (value.isNode) { acc[key] = this.RED.nodes.getNode(config[key]); } else if (typeof value === 'function') { acc[key] = value.call(this, config); } else { acc[key] = config[key]; } return acc; }, {}); node.on('input', this._eventHandlers.preOnInput.bind(this)); node.on('close', this._eventHandlers.preOnClose.bind(this)); // TODO: move to initializer after controllers are converted to typescript const { jsonataService, nodeRedContextService, typedInputService } = createControllerDependencies(this.node, this.homeAssistant); const transformState = new TransformState((_b = (_a = this.server) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.ha_boolean); this.comparatorService = new ComparatorService({ jsonataService, nodeRedContextService, homeAssistant: this.homeAssistant, transformState, }); this.nodeRedContextService = nodeRedContextService; this.jsonataService = jsonataService; this.typedInputService = typedInputService; this.transformState = transformState; const name = selectn('nodeConfig.name', this); this.node.debug(`instantiated node, name: ${name || 'undefined'}`); } get server() { var _a; return (_a = this === null || this === void 0 ? void 0 : this.nodeConfig) === null || _a === void 0 ? void 0 : _a.server; } get homeAssistant() { return getHomeAssistant(this.server); } get isConnected() { return this.homeAssistant && this.homeAssistant.isConnected; } get isHomeAssistantRunning() { return this.isConnected && this.homeAssistant.isHomeAssistantRunning; } get isIntegrationLoaded() { return this.isConnected && this.homeAssistant.isIntegrationLoaded; } get isEnabled() { return this.state.isEnabled(); } set isEnabled(value) { this.state.setEnabled(value); this.status.setNodeState(value); } // Subclasses should override these as hooks into common events // eslint-disable-next-line @typescript-eslint/no-unused-vars onClose(removed) { } onInput() { } send() { this.node.send(...arguments); } sendSplit(message, data, send) { if (!send) { send = this.send; } delete message._msgid; message.parts = { id: this.RED.util.generateId(), type: 'array', count: data.length, len: 1, }; let pos = 0; for (let i = 0; i < data.length; i++) { message.payload = data.slice(pos, pos + 1)[0]; message.parts.index = i; pos += 1; send(this.RED.util.cloneMessage(message)); } } debugToClient(debugMsg, topic = 'sent data') { debugToClient(this.node, debugMsg, topic); } getCastValue(datatype, value) { return this.transformState.transform(datatype, value); } castState(entity, type) { if (entity) { entity.original_state = entity.state; entity.state = this.getCastValue(type, entity.state); } } // TODO: Remove after controllers are converted to typescript setContextValue(val, location, property, message) { this.nodeRedContextService.set(val, location, property, message); } async getComparatorResult(comparatorType, comparatorValue, actualValue, comparatorValueDatatype, { message, entity, prevEntity }) { return this.comparatorService.getComparatorResult(comparatorType, comparatorValue, actualValue, comparatorValueDatatype, { message, entity, prevEntity }); } async evaluateJSONata(expression, objs = {}) { return this.jsonataService.evaluate(expression, objs); } async getTypedInputValue(value, valueType, props = {}) { return this.typedInputService.getValue(value, valueType, props); } async setCustomOutputs(properties = [], message, extras) { for (const item of properties) { const value = await this.getTypedInputValue(item.value, item.valueType, { message, ...extras, }); try { this.nodeRedContextService.set(value, item.propertyType, item.property, message); } catch (e) { this.node.warn(`Custom Ouput Error (${item.propertyType}:${item.property}): ${e.message}`); } } } } const _internals = { parseInputMessage(inputOptions, msg) { if (!inputOptions) return; const parsedResult = {}; for (const [fieldKey, fieldConfig] of Object.entries(inputOptions)) { // Find messageProp value if it's a string or Array // When it's an array lowest valid index takes precedent const messageProp = Array.isArray(fieldConfig.messageProp) ? fieldConfig.messageProp.reduce((val, cur) => val || selectn(cur, msg), undefined) : selectn(fieldConfig.messageProp, msg); // Try to load from message const result = { key: fieldKey, value: messageProp, source: 'message', validation: null, }; // If message missing value and node has config that can be used instead if (result.value === undefined && fieldConfig.configProp) { result.value = selectn(fieldConfig.configProp, this.nodeConfig); result.source = 'config'; } if (result.value === undefined && fieldConfig.default !== undefined) { result.value = typeof fieldConfig.default === 'function' ? fieldConfig.default.call(this) : fieldConfig.default; result.source = 'default'; } // If value not found in both config and message if (result.value === undefined) { result.source = 'missing'; } // If validation for value is configured run validation, optionally throwing on failed validation if (fieldConfig.validation) { const { error, value } = fieldConfig.validation.schema.validate(result.value, { convert: true, }); if (error && fieldConfig.validation.haltOnFail) throw error; result.validation = { error, value, }; } // Assign result to config key value parsedResult[fieldKey] = result; } return parsedResult; }, }; const _eventHandlers = { preOnInput(message, send, done) { try { const parsedMessage = _internals.parseInputMessage.call(this, this.options.input, message); this.onInput({ parsedMessage, message, send, done, }); } catch (e) { if (e && e.isJoi) { this.status.setFailed('Error'); done(e.message); return; } throw e; } }, async preOnClose(removed, done) { this.node.debug(`closing node. Reason: ${removed ? 'node deleted' : 'node re-deployed'}`); try { await this.onClose(removed); done(); } catch (e) { this.node.error(e.message); } }, }; module.exports = BaseNode;