node-red-contrib-home-assistant-websocket
Version:
Node-RED integration with Home Assistant through websocket and REST API
249 lines (248 loc) • 13.1 kB
JavaScript
"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;