hassio-plejd/plejd/MqttClient.js
Victor Hagelbäck ef7a5086a1 Handle when scene and device have the same Id
- Catch emitted errors in Mqtt
2021-02-10 10:10:28 +01:00

244 lines
7.4 KiB
JavaScript

const EventEmitter = require('events');
const mqtt = require('mqtt');
const Configuration = require('./Configuration');
const Logger = require('./Logger');
const startTopics = ['hass/status', 'homeassistant/status'];
const logger = Logger.getLogger('plejd-mqtt');
// #region discovery
const discoveryPrefix = 'homeassistant';
const nodeId = 'plejd';
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
const getConfigPath = (plug) => `${getPath(plug)}/config`;
const getStateTopic = (plug) => `${getPath(plug)}/state`;
const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`;
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
const getSceneEventTopic = () => 'plejd/event/scene';
const decodeTopicRegexp = new RegExp(
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
);
const decodeTopic = (topic) => {
const matches = decodeTopicRegexp.exec(topic);
if (!matches) {
return null;
}
return matches.groups;
};
const getDiscoveryPayload = (device) => ({
schema: 'json',
name: device.name,
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
state_topic: getStateTopic(device),
command_topic: getCommandTopic(device),
availability_topic: getAvailabilityTopic(device),
optimistic: false,
brightness: `${device.dimmable}`,
device: {
identifiers: `${device.serialNumber}_${device.id}`,
manufacturer: 'Plejd',
model: device.typeName,
name: device.name,
sw_version: device.version,
},
});
const getSwitchPayload = (device) => ({
name: device.name,
state_topic: getStateTopic(device),
command_topic: getCommandTopic(device),
optimistic: false,
device: {
identifiers: `${device.serialNumber}_${device.id}`,
manufacturer: 'Plejd',
model: device.typeName,
name: device.name,
sw_version: device.version,
},
});
// #endregion
class MqttClient extends EventEmitter {
deviceRegistry;
constructor(deviceRegistry) {
super();
this.config = Configuration.getOptions();
this.deviceRegistry = deviceRegistry;
}
init() {
logger.info('Initializing MQTT connection for Plejd addon');
this.client = mqtt.connect(this.config.mqttBroker, {
username: this.config.mqttUsername,
password: this.config.mqttPassword,
});
this.client.on('error', (err) => {
logger.warn('Error emitted from mqtt client', err);
});
this.client.on('connect', () => {
logger.info('Connected to MQTT.');
this.client.subscribe(startTopics, (err) => {
if (err) {
logger.error('Unable to subscribe to status topics');
}
this.emit('connected');
});
this.client.subscribe(getSubscribePath(), (err) => {
if (err) {
logger.error('Unable to subscribe to control topics');
}
});
});
this.client.on('close', () => {
logger.verbose('Warning: mqtt channel closed event, reconnecting...');
this.reconnect();
});
this.client.on('message', (topic, message) => {
if (startTopics.includes(topic)) {
logger.info('Home Assistant has started. lets do discovery.');
this.emit('connected');
} else {
const decodedTopic = decodeTopic(topic);
if (decodedTopic) {
let device = this.deviceRegistry.getDevice(decodedTopic.id);
const messageString = message.toString();
const isJsonMessage = messageString.startsWith('{');
const command = isJsonMessage
? JSON.parse(messageString)
: messageString;
if (!isJsonMessage && messageString === 'ON' && this.deviceRegistry.getScene(decodedTopic.id)) {
// Guess that id that got state command without dim value belongs to Scene, not Device
// This guess could very well be wrong depending on the installation...
logger.warn(`Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. OFF commands still sent to device.`);
device = this.deviceRegistry.getScene(decodedTopic.id);
}
const deviceName = device ? device.name : '';
switch (decodedTopic.command) {
case 'set':
logger.verbose(
`Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${messageString}`,
);
if (device) {
this.emit('stateChanged', device, command);
} else {
logger.warn(
`Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`,
);
}
break;
case 'state':
case 'config':
case 'availability':
logger.verbose(
`Sent mqtt ${decodedTopic.command} command for ${
decodedTopic.type
}, ${deviceName} (${decodedTopic.id}). ${
decodedTopic.command === 'availability' ? messageString : ''
}`,
);
break;
default:
logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`);
}
} else {
logger.verbose(`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`);
}
}
});
}
reconnect() {
this.client.reconnect();
}
disconnect(callback) {
this.deviceRegistry.allDevices.forEach((device) => {
this.client.publish(getAvailabilityTopic(device), 'offline');
});
this.client.end(callback);
}
sendDiscoveryToHomeAssistant() {
logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`);
this.deviceRegistry.allDevices.forEach((device) => {
logger.debug(`Sending discovery for ${device.name}`);
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
logger.info(
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
);
this.client.publish(getConfigPath(device), JSON.stringify(payload));
setTimeout(() => {
this.client.publish(getAvailabilityTopic(device), 'online');
}, 2000);
});
}
updateState(deviceId, data) {
const device = this.deviceRegistry.getDevice(deviceId);
if (!device) {
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
return;
}
logger.verbose(
`Updating state for ${device.name}: ${data.state}${
data.brightness ? `, dim: ${data.brightness}` : ''
}`,
);
let payload = null;
if (device.type === 'switch') {
payload = data.state === 1 ? 'ON' : 'OFF';
} else {
if (device.dimmable) {
payload = {
state: data.state === 1 ? 'ON' : 'OFF',
brightness: data.brightness,
};
} else {
payload = {
state: data.state === 1 ? 'ON' : 'OFF',
};
}
payload = JSON.stringify(payload);
}
this.client.publish(getStateTopic(device), payload);
this.client.publish(getAvailabilityTopic(device), 'online');
}
sceneTriggered(sceneId) {
logger.verbose(`Scene triggered: ${sceneId}`);
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }));
}
}
module.exports = MqttClient;