UNPKG

node-red-contrib-home-assistant-websocket

Version:
249 lines (248 loc) 13.1 kB
"use strict"; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _ActionController_instances, _ActionController_queue, _ActionController_hasDeprecatedWarned, _ActionController_hasDeprecatedWarnedTargetEntityId, _ActionController_parseJSON, _ActionController_isValidContextData, _ActionController_mergeContextData, _ActionController_getTargetData, _ActionController_processInput; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = require("lodash"); const selectn_1 = __importDefault(require("selectn")); const InputOutputController_1 = __importDefault(require("../../common/controllers/InputOutputController")); const InputError_1 = __importDefault(require("../../common/errors/InputError")); const NoConnectionError_1 = __importDefault(require("../../common/errors/NoConnectionError")); const InputService_1 = require("../../common/services/InputService"); const const_1 = require("../../const"); const globals_1 = require("../../globals"); const mustache_1 = require("../../helpers/mustache"); const utils_1 = require("../../helpers/utils"); const const_2 = require("./const"); class ActionController extends InputOutputController_1.default { constructor() { super(...arguments); _ActionController_instances.add(this); _ActionController_queue.set(this, []); _ActionController_hasDeprecatedWarned.set(this, false); _ActionController_hasDeprecatedWarnedTargetEntityId.set(this, false); } async onInput({ message, parsedMessage, send, done, }) { var _a, _b, _c, _d; if (!this.homeAssistant.websocket.isConnected && this.node.config.queue === const_2.Queue.None) { throw new NoConnectionError_1.default(); } const states = this.homeAssistant.websocket.getStates(); const render = (0, mustache_1.generateRenderTemplate)(message, this.node.context(), states); let action = parsedMessage.action.value; // TODO: Remove in version 1.0 if (parsedMessage.action.source === InputService_1.DataSource.Transformed) { if (!__classPrivateFieldGet(this, _ActionController_hasDeprecatedWarned, "f")) { __classPrivateFieldSet(this, _ActionController_hasDeprecatedWarned, true, "f"); this.node.warn(globals_1.RED._('ha-action.error.domain_service_deprecated')); } } else if (parsedMessage.action.source === InputService_1.DataSource.Config) { action = render(action); } const [domain, service] = action.toLowerCase().split('.'); if (!domain || !service) { throw new InputError_1.default([ 'ha-action.error.invalid_action_format', { action }, ]); } const target = __classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_getTargetData).call(this, parsedMessage.target.value, message); const mergedData = await __classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_mergeContextData).call(this, (0, selectn_1.default)('payload.data', message), message, render); // TODO: Remove in version 1.0 - Check if entity_id should be in the data field not the target field if (((_a = parsedMessage.action) === null || _a === void 0 ? void 0 : _a.value) && typeof target.entity_id === 'string') { const services = this.homeAssistant.websocket.getServices(); if (((_d = (_c = (_b = services[domain]) === null || _b === void 0 ? void 0 : _b[service]) === null || _c === void 0 ? void 0 : _c.fields) === null || _d === void 0 ? void 0 : _d.entity_id) !== undefined && !mergedData.entity_id) { if (!__classPrivateFieldGet(this, _ActionController_hasDeprecatedWarnedTargetEntityId, "f")) { __classPrivateFieldSet(this, _ActionController_hasDeprecatedWarnedTargetEntityId, true, "f"); this.node.warn(globals_1.RED._('ha-action.error.entity_id_target_data')); } mergedData.entity_id = target.entity_id; target.entity_id = undefined; } } const queueItem = { domain, service, data: Object.keys(mergedData).length ? mergedData : undefined, target, message, done, send, }; if (!this.homeAssistant.isConnected) { switch (this.node.config.queue) { case const_2.Queue.First: if (__classPrivateFieldGet(this, _ActionController_queue, "f").length === 0) { __classPrivateFieldSet(this, _ActionController_queue, [queueItem], "f"); } break; case const_2.Queue.All: __classPrivateFieldGet(this, _ActionController_queue, "f").push(queueItem); break; case const_2.Queue.Last: __classPrivateFieldSet(this, _ActionController_queue, [queueItem], "f"); break; } this.node.debug(`Queueing: ${JSON.stringify({ domain, service, target, data: mergedData, })}`); this.status.setText(`${__classPrivateFieldGet(this, _ActionController_queue, "f").length} queued`); return; } await __classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_processInput).call(this, queueItem); } async onClientReady() { while (__classPrivateFieldGet(this, _ActionController_queue, "f").length) { const item = __classPrivateFieldGet(this, _ActionController_queue, "f").pop(); if (item) { await __classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_processInput).call(this, item); } } } } _ActionController_queue = new WeakMap(), _ActionController_hasDeprecatedWarned = new WeakMap(), _ActionController_hasDeprecatedWarnedTargetEntityId = new WeakMap(), _ActionController_instances = new WeakSet(), _ActionController_parseJSON = function _ActionController_parseJSON(data) { try { return JSON.parse(data); } catch (e) { throw new InputError_1.default([ 'ha-action.error.invalid_json', { json: data }, ]); } }, _ActionController_isValidContextData = function _ActionController_isValidContextData(data) { return (typeof data === 'object' && !Array.isArray(data) && data !== undefined && data !== null); }, _ActionController_mergeContextData = /** * Merges the payload, message context, and rendered template data to create a final data object. * The priority order for merging is 'Config, Global Ctx, Flow Ctx, Payload' with the rightmost value winning. * * @param payload - The payload data to merge (default: {}). * @param message - The NodeMessage object containing the message context. * @param render - The function used to render the template. * @returns The merged data object. */ async function _ActionController_mergeContextData(payload = {}, message, render) { let configData = {}; if (this.node.config.data.length) { switch (this.node.config.dataType) { case const_1.TypedInputTypes.JSONata: configData = await this.jsonataService.evaluate(this.node.config.data, { message, }); break; case const_1.TypedInputTypes.JSON: configData = __classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_parseJSON).call(this, render(this.node.config.data, this.node.config.mustacheAltTags)); break; } } // Calculate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning let contextData = {}; if (this.node.config.mergeContext) { const ctx = this.node.context(); const flowVal = ctx.flow.get(this.node.config.mergeContext); const globalVal = ctx.global.get(this.node.config.mergeContext); if (__classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_isValidContextData).call(this, globalVal)) { contextData = globalVal; } if (__classPrivateFieldGet(this, _ActionController_instances, "m", _ActionController_isValidContextData).call(this, flowVal)) { contextData = { ...contextData, ...flowVal }; } } if (this.node.config.blockInputOverrides) { return { ...configData, ...contextData }; } return { ...configData, ...contextData, ...payload }; }, _ActionController_getTargetData = function _ActionController_getTargetData(payload, message) { const render = (0, mustache_1.generateRenderTemplate)(message, this.node.context(), this.homeAssistant.websocket.getStates()); const map = { floorId: 'floor_id', areaId: 'area_id', deviceId: 'device_id', entityId: 'entity_id', labelId: 'label_id', }; const configTarget = {}; for (const key in map) { const prop = map[key]; const target = this.node.config[key]; configTarget[prop] = target ? [...target] : undefined; if (Array.isArray(configTarget[prop])) { // If length is 0 set it to undefined so the target can be overridden from the data field if (configTarget[prop].length === 0) { configTarget[prop] = undefined; } else { // Render env vars or mustache templates configTarget[prop].forEach((target, index) => { configTarget[prop][index] = (0, utils_1.isNodeRedEnvVar)(target) ? globals_1.RED.util.evaluateNodeProperty(target, 'env', this.node, message) : render(target); }); // If prop has a length of 1 convert it to a string if (configTarget[prop].length === 1) { configTarget[prop] = configTarget[prop][0]; } } } else if (configTarget[prop] !== undefined) { configTarget[prop] = render(configTarget[prop]); if (prop === 'entity_id') { // Convert possible comma delimited list to array configTarget.entity_id = configTarget.entity_id.reduce((acc, curr) => acc.concat(curr.indexOf(',') ? curr.split(',').map((e) => e.trim()) : curr), []); } } } const targets = (0, lodash_1.merge)(configTarget, payload); // remove undefined values Object.keys(targets).forEach((key) => targets[key] === undefined && delete targets[key]); return targets; }, _ActionController_processInput = async function _ActionController_processInput(queueItem) { const { domain, service, data, message, target, done, send } = queueItem; this.status.setSending(); const debug = { domain, service, target, data }; this.debugToClient(debug); this.node.debug(`Calling Service: ${JSON.stringify(debug)}`); const response = await this.homeAssistant.websocket.callService(domain, service, data, target); this.status.setSuccess([ 'ha-action.status.action_called', { domain, service }, ]); await this.setCustomOutputs(this.node.config.outputProperties, message, { config: this.node.config, data: { domain, service, data, target, }, results: response === null || response === void 0 ? void 0 : response.response, }); send(message); done(); }; exports.default = ActionController;