node-red-contrib-home-assistant-websocket
Version:
Node-RED integration with Home Assistant through websocket and REST API
198 lines (197 loc) • 6.83 kB
JavaScript
"use strict";
const slugify = require('slugify');
const EntityNode = require('../EntityNode');
const nodeOptions = {
config: {
state: {},
stateType: {},
attributes: (nodeConfig) => nodeConfig.attributes || [],
resend: {},
outputLocation: {},
outputLocationType: {},
inputOverride: {},
},
input: {
state: {
messageProp: 'payload.state',
configProp: 'state',
default: 'payload',
},
stateType: {
messageProp: 'payload.stateType',
configProp: 'stateType',
default: 'msg',
},
attributes: {
messageProp: 'payload.attributes',
configProp: 'attributes',
default: [],
},
},
};
class Sensor extends EntityNode {
constructor({ node, config, RED, status }) {
super({ node, config, RED, status, nodeOptions });
}
async registerEntity() {
if (!this.isIntegrationLoaded) {
this.error(this.integrationErrorMessage);
this.status.setFailed('Error');
return;
}
if (this.registered) {
return;
}
const config = {};
this.nodeConfig.config
.filter((c) => c.value.length)
.forEach((e) => (config[e.property] = e.value));
const payload = {
type: 'nodered/discovery',
server_id: this.nodeConfig.server.id,
node_id: this.node.id,
component: this.nodeConfig.entityType,
config,
};
// Add state and attributes to payload if resend enabled
if (this.nodeConfig.resend && this.lastPayload) {
payload.state = this.lastPayload.state;
payload.attributes = this.lastPayload.attributes;
}
this.debugToClient(payload);
this.node.debug(`Registering ${this.nodeConfig.entityType} with HA`);
try {
await this.homeAssistant.send(payload);
this.status.setSuccess('Registered');
this.registered = true;
}
catch (err) {
this.status.setFailed('Error registering');
this.node.error(`Error registering entity. ${err.message ? ` Error Message: ${err.message}` : ''}`);
}
}
onClose(removed) {
super.onClose(removed);
if (this.registered && this.isIntegrationLoaded && removed) {
const payload = {
type: 'nodered/discovery',
server_id: this.nodeConfig.server.id,
node_id: this.node.id,
component: this.nodeConfig.entityType,
remove: true,
};
this.node.debug(`Unregistering ${this.nodeConfig.entityType} from HA`);
this.homeAssistant.send(payload);
}
}
onInput({ parsedMessage, message, send, done }) {
if (!this.isConnected) {
this.status.setFailed('No Connection');
done('Sensor update attempted without connection to server.');
return;
}
if (!this.isIntegrationLoaded) {
this.status.setFailed('Error');
done(this.integrationErrorMessage);
return;
}
let state = parsedMessage.state.value;
let stateType = parsedMessage.stateType.value;
if (this.nodeConfig.inputOverride === 'block') {
state = this.nodeConfig.state;
stateType = this.nodeConfig.stateType;
}
else if (parsedMessage.state.source === 'message' &&
stateType !== 'message') {
// Set default for state from input to string
stateType = 'str';
}
try {
state = this.getTypedInputValue(state, stateType, { message });
}
catch (e) {
this.status.setFailed('Error');
done(`State: ${e.message}`);
return;
}
if (state === undefined) {
this.status.setFailed('Error');
done('State must be defined.');
return;
}
const attributes = this.getAttributes(parsedMessage);
const attr = {};
try {
attributes.forEach((x) => {
// Change string to lower-case and remove unwanted characters
const property = slugify(x.property, {
replacement: '_',
remove: /[^A-Za-z0-9-_~ ]/,
lower: true,
});
attr[property] = this.getTypedInputValue(x.value, x.valueType, {
message,
});
});
}
catch (e) {
this.status.setFailed('Error');
done(`Attribute: ${e.message}`);
return;
}
const payload = {
type: 'nodered/entity',
server_id: this.nodeConfig.server.id,
node_id: this.node.id,
state,
attributes: attr,
};
if (this.nodeConfig.resend) {
this.lastPayload = {
state,
attributes: attr,
};
this.state.setLastPayload(this.lastPayload);
}
this.debugToClient(payload);
this.homeAssistant
.send(payload)
.then(() => {
this.status.setSuccess(state);
if (this.nodeConfig.outputLocationType !== 'none') {
this.setContextValue(payload, this.nodeConfig.outputLocationType || 'msg', this.nodeConfig.outputLocation || 'payload', message);
}
send(message);
done();
})
.catch((err) => {
this.status.setFailed('API Error');
done(`Entity API error. ${err.message ? ` Error Message: ${err.message}` : ''}`);
});
}
getAttributes(parsedMessage) {
let attributes = [];
if (parsedMessage.attributes.source !== 'message' ||
this.nodeConfig.inputOverride === 'block') {
attributes = this.nodeConfig.attributes;
}
else {
if (this.nodeConfig.inputOverride === 'merge') {
const keys = Object.keys(parsedMessage.attributes.value).map((e) => e.toLowerCase());
this.nodeConfig.attributes.forEach((ele) => {
if (!keys.includes(ele.property.toLowerCase())) {
attributes.push(ele);
}
});
}
for (const [prop, val] of Object.entries(parsedMessage.attributes.value)) {
attributes.push({
property: prop,
value: val,
});
}
}
return attributes;
}
}
module.exports = Sensor;