node-red-contrib-home-assistant-websocket
Version:
Node-RED integration with Home Assistant through websocket and REST API
209 lines (208 loc) • 7.54 kB
JavaScript
"use strict";
const Joi = require('joi');
const { merge } = require('lodash');
const EventsNode = require('./EventsNode');
const { INTEGRATION_UNLOADED, INTEGRATION_NOT_LOADED } = require('../const');
const { STATUS_SHAPE_DOT, STATUS_SHAPE_RING } = require('../helpers/status');
const DEFAULT_NODE_OPTIONS = {
debug: false,
config: {
haConfig: {},
exposeToHomeAssistant: (nodeDef) => nodeDef.exposeToHomeAssistant === undefined
? false
: nodeDef.exposeToHomeAssistant,
},
};
class EventsHaNode extends EventsNode {
constructor({ node, config, RED, status, nodeOptions = {} }) {
nodeOptions = merge({}, DEFAULT_NODE_OPTIONS, nodeOptions);
super({ node, config, RED, status, nodeOptions });
// Check if there's a server selected
if (this.nodeConfig.server) {
// Determine if node needs to be removed from Home Assistant because it's no longer exposed
this.removeFromHA = !!(this.nodeConfig.exposeToHomeAssistant === false &&
this.server.exposedNodes[this.id] === true);
// Save expose state so we can check if it needs to removed when it's not exposed anymore
this.server.exposedNodes[this.node.id] =
this.nodeConfig.exposeToHomeAssistant;
}
this.init();
}
get lastPayload() {
return this.state.getLastPayload();
}
set lastPayload(payload) {
this.state.setLastPayload(payload);
}
async init() {
if (this.isIntegrationLoaded) {
this.registerEntity();
this.removeFromHomeAssistant();
}
}
async onClose(removed) {
super.onClose(removed);
if (removed) {
if (this.isIntegrationLoaded &&
this.nodeConfig.exposeToHomeAssistant) {
this.removeFromHomeAssistant(true);
}
}
this.removeSubscription();
}
onHaEventsOpen() {
this.subscription = null;
}
onHaIntegration(type) {
super.onHaIntegration(type);
switch (type) {
case INTEGRATION_UNLOADED:
case INTEGRATION_NOT_LOADED:
this.removeSubscription();
if (this.node.type !== 'trigger-state') {
this.isEnabled = true;
}
break;
}
}
getDiscoveryPayload(config) {
return {
type: 'nodered/discovery',
server_id: this.nodeConfig.server.id,
node_id: this.node.id,
component: 'switch',
state: this.isEnabled,
config,
};
}
async registerEntity(status = true) {
if (super.registerEntity() === false) {
return;
}
const haConfig = {};
// Handle both event node and sensor node switch HA config
const config = this.nodeConfig.haConfig || this.nodeConfig.config;
config
.filter((c) => c.value.length)
.forEach((e) => (haConfig[e.property] = e.value));
try {
const payload = this.getDiscoveryPayload(haConfig);
this.node.debug(`Registering with Home Assistant`);
this.subscription = await this.homeAssistant.subscribeMessage(this.onHaEventMessage.bind(this), payload, { resubscribe: false });
}
catch (e) {
this.status.setFailed(this.RED._('config-server.status.error'));
this.node.error(e.message);
return;
}
if (status) {
this.status.setSuccess(this.RED._('config-server.status.registered'));
}
this.registered = true;
}
onHaEventMessage(evt) {
if (evt.type === undefined) {
// Need to set type prior to 0.20.0
evt.type = 'state_changed';
}
if (evt.type) {
switch (evt.type) {
case 'state_changed':
this.isEnabled = evt.state;
this.updateHomeAssistant();
break;
case 'automation_triggered':
this.handleTriggerMessage(evt.data);
break;
}
}
}
async handleTriggerMessage(data = {}) {
if (this.isEnabled === false) {
return;
}
const schema = Joi.object({
entity_id: Joi.string().allow(null),
skip_condition: Joi.boolean().default(false),
output_path: Joi.boolean().default(true),
});
let validatedData, entity, entityId;
try {
validatedData = await schema.validateAsync(data);
entityId = validatedData.entity_id || this.getNodeEntityId();
if (!entityId) {
throw new Error('Entity filter type is not set to exact and no entity_id found in trigger data.');
}
entity = this.homeAssistant.getStates(entityId);
if (!entity) {
throw new Error(`entity_id provided by trigger event not found in cache: ${entityId}`);
}
}
catch (e) {
this.status.setFailed('Error');
this.node.error(`Trigger Error: ${e.message}`, {});
return;
}
const eventMessage = {
event_type: 'triggered',
entity_id: entity.entity_id,
event: {
entity_id: entity.entity_id,
old_state: entity,
new_state: entity,
},
};
if (!validatedData.skip_condition) {
this.triggerNode(eventMessage);
return;
}
const conditionalValue = validatedData.output_path;
const msg = {
topic: entityId,
payload: eventMessage.event.new_state.state,
data: eventMessage.event,
};
this.status.set({
shape: conditionalValue ? STATUS_SHAPE_DOT : STATUS_SHAPE_RING,
text: this.status.appendDateString(eventMessage.event.new_state.state),
});
this.send(conditionalValue ? [msg, null] : [null, msg]);
}
getNodeEntityId() { }
triggerNode() { }
updateHomeAssistant() {
if (!this.isIntegrationLoaded)
return;
const message = {
type: 'nodered/entity',
server_id: this.nodeConfig.server.id,
node_id: this.node.id,
state: this.isEnabled,
};
this.homeAssistant.send(message);
}
// Remove from Home Assistant when `Expose to Home Assistant` is unchecked
removeFromHomeAssistant(nodeRemoved = false) {
if (!this.homeAssistant.isIntegrationLoaded ||
(!this.removeFromHA && !nodeRemoved) ||
(this.nodeConfig.entityType &&
this.nodeConfig.entityType !== 'switch')) {
return;
}
const payload = { ...this.getDiscoveryPayload(), remove: true };
this.homeAssistant.send(payload);
this.removeFromHA = false;
this.removeSubscription();
// Enabled node when removing it from Home Assistant as there is no
// way to do so once it's removed except for the trigger-state node
this.isEnabled = true;
}
async removeSubscription() {
if (this.subscription) {
this.node.debug('Unregistering from HA');
await this.subscription().catch(() => { });
}
this.subscription = null;
}
}
module.exports = EventsHaNode;