2019-12-04 11:17:06 +01:00
|
|
|
const EventEmitter = require('events');
|
|
|
|
|
const mqtt = require('mqtt');
|
2021-02-01 21:19:22 +01:00
|
|
|
|
|
|
|
|
const Configuration = require('./Configuration');
|
2021-01-21 21:31:37 +01:00
|
|
|
const Logger = require('./Logger');
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-01-28 14:24:04 +01:00
|
|
|
const startTopics = ['hass/status', 'homeassistant/status'];
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-01-22 15:49:02 +01:00
|
|
|
const logger = Logger.getLogger('plejd-mqtt');
|
2019-12-04 11:17:06 +01:00
|
|
|
|
|
|
|
|
// #region discovery
|
|
|
|
|
|
|
|
|
|
const discoveryPrefix = 'homeassistant';
|
|
|
|
|
const nodeId = 'plejd';
|
|
|
|
|
|
|
|
|
|
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
2021-01-22 15:49:02 +01:00
|
|
|
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
|
|
|
|
|
const getConfigPath = (plug) => `${getPath(plug)}/config`;
|
|
|
|
|
const getStateTopic = (plug) => `${getPath(plug)}/state`;
|
2021-01-30 10:00:09 +01:00
|
|
|
const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`;
|
2021-01-22 15:49:02 +01:00
|
|
|
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
|
|
|
|
|
const getSceneEventTopic = () => 'plejd/event/scene';
|
|
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-22 15:49:02 +01:00
|
|
|
const getDiscoveryPayload = (device) => ({
|
2019-12-21 15:01:15 +00:00
|
|
|
schema: 'json',
|
2019-12-10 22:01:12 +01:00
|
|
|
name: device.name,
|
2020-01-27 20:43:52 +00:00
|
|
|
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
2019-12-10 22:01:12 +01:00
|
|
|
state_topic: getStateTopic(device),
|
|
|
|
|
command_topic: getCommandTopic(device),
|
2021-01-29 21:25:34 +01:00
|
|
|
availability_topic: getAvailabilityTopic(device),
|
2019-12-21 15:01:15 +00:00
|
|
|
optimistic: false,
|
2020-01-21 14:24:02 +00:00
|
|
|
brightness: `${device.dimmable}`,
|
|
|
|
|
device: {
|
2021-01-22 15:49:02 +01:00
|
|
|
identifiers: `${device.serialNumber}_${device.id}`,
|
2020-01-21 14:24:02 +00:00
|
|
|
manufacturer: 'Plejd',
|
|
|
|
|
model: device.typeName,
|
|
|
|
|
name: device.name,
|
2021-01-22 15:49:02 +01:00
|
|
|
sw_version: device.version,
|
|
|
|
|
},
|
2019-12-10 22:01:12 +01:00
|
|
|
});
|
|
|
|
|
|
2021-01-22 15:49:02 +01:00
|
|
|
const getSwitchPayload = (device) => ({
|
2020-02-20 13:02:47 +01:00
|
|
|
name: device.name,
|
|
|
|
|
state_topic: getStateTopic(device),
|
|
|
|
|
command_topic: getCommandTopic(device),
|
|
|
|
|
optimistic: false,
|
|
|
|
|
device: {
|
2021-01-22 15:49:02 +01:00
|
|
|
identifiers: `${device.serialNumber}_${device.id}`,
|
2020-02-20 13:02:47 +01:00
|
|
|
manufacturer: 'Plejd',
|
|
|
|
|
model: device.typeName,
|
|
|
|
|
name: device.name,
|
2021-01-22 15:49:02 +01:00
|
|
|
sw_version: device.version,
|
|
|
|
|
},
|
2020-02-20 13:02:47 +01:00
|
|
|
});
|
|
|
|
|
|
2019-12-04 11:17:06 +01:00
|
|
|
// #endregion
|
|
|
|
|
|
|
|
|
|
class MqttClient extends EventEmitter {
|
2021-02-01 21:19:22 +01:00
|
|
|
deviceRegistry;
|
|
|
|
|
|
|
|
|
|
constructor(deviceRegistry) {
|
2019-12-04 11:17:06 +01:00
|
|
|
super();
|
|
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
this.config = Configuration.getOptions();
|
|
|
|
|
this.deviceRegistry = deviceRegistry;
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
2021-01-22 15:49:02 +01:00
|
|
|
logger.info('Initializing MQTT connection for Plejd addon');
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
this.client = mqtt.connect(this.config.mqttBroker, {
|
|
|
|
|
username: this.config.mqttUsername,
|
|
|
|
|
password: this.config.mqttPassword,
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
|
2021-02-10 10:10:28 +01:00
|
|
|
this.client.on('error', (err) => {
|
|
|
|
|
logger.warn('Error emitted from mqtt client', err);
|
|
|
|
|
});
|
|
|
|
|
|
2019-12-04 11:17:06 +01:00
|
|
|
this.client.on('connect', () => {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.info('Connected to MQTT.');
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-01-28 14:24:04 +01:00
|
|
|
this.client.subscribe(startTopics, (err) => {
|
2019-12-04 11:17:06 +01:00
|
|
|
if (err) {
|
2021-01-30 10:00:09 +01:00
|
|
|
logger.error('Unable to subscribe to status topics');
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
this.emit('connected');
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.client.subscribe(getSubscribePath(), (err) => {
|
|
|
|
|
if (err) {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.error('Unable to subscribe to control topics');
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.client.on('close', () => {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.verbose('Warning: mqtt channel closed event, reconnecting...');
|
2021-02-01 21:19:22 +01:00
|
|
|
this.reconnect();
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.client.on('message', (topic, message) => {
|
2021-01-29 20:41:55 +01:00
|
|
|
if (startTopics.includes(topic)) {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.info('Home Assistant has started. lets do discovery.');
|
2021-02-01 21:19:22 +01:00
|
|
|
this.emit('connected');
|
|
|
|
|
} else {
|
|
|
|
|
const decodedTopic = decodeTopic(topic);
|
|
|
|
|
if (decodedTopic) {
|
2021-02-10 10:10:28 +01:00
|
|
|
let device = this.deviceRegistry.getDevice(decodedTopic.id);
|
|
|
|
|
|
|
|
|
|
const messageString = message.toString();
|
|
|
|
|
const isJsonMessage = messageString.startsWith('{');
|
2021-02-11 22:45:13 +01:00
|
|
|
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
|
2021-02-10 10:10:28 +01:00
|
|
|
|
2021-02-11 22:45:13 +01:00
|
|
|
if (
|
|
|
|
|
!isJsonMessage
|
|
|
|
|
&& messageString === 'ON'
|
|
|
|
|
&& this.deviceRegistry.getScene(decodedTopic.id)
|
|
|
|
|
) {
|
2021-02-10 10:10:28 +01:00
|
|
|
// 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...
|
2021-02-11 22:45:13 +01:00
|
|
|
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.',
|
|
|
|
|
);
|
2021-02-10 10:10:28 +01:00
|
|
|
device = this.deviceRegistry.getScene(decodedTopic.id);
|
|
|
|
|
}
|
2021-02-01 21:19:22 +01:00
|
|
|
const deviceName = device ? device.name : '';
|
|
|
|
|
|
|
|
|
|
switch (decodedTopic.command) {
|
|
|
|
|
case 'set':
|
|
|
|
|
logger.verbose(
|
2021-02-10 10:10:28 +01:00
|
|
|
`Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${messageString}`,
|
2021-02-01 21:19:22 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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(
|
2021-02-01 21:36:40 +01:00
|
|
|
`Sent mqtt ${decodedTopic.command} command for ${
|
|
|
|
|
decodedTopic.type
|
|
|
|
|
}, ${deviceName} (${decodedTopic.id}). ${
|
2021-02-10 10:10:28 +01:00
|
|
|
decodedTopic.command === 'availability' ? messageString : ''
|
2021-02-01 21:36:40 +01:00
|
|
|
}`,
|
2021-02-01 21:19:22 +01:00
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`);
|
|
|
|
|
}
|
2021-01-25 08:06:28 +01:00
|
|
|
} else {
|
2021-02-11 22:45:13 +01:00
|
|
|
logger.verbose(
|
|
|
|
|
`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`,
|
|
|
|
|
);
|
2021-01-25 08:06:28 +01:00
|
|
|
}
|
2021-01-21 21:31:37 +01:00
|
|
|
}
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reconnect() {
|
|
|
|
|
this.client.reconnect();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 21:25:34 +01:00
|
|
|
disconnect(callback) {
|
2021-02-01 21:19:22 +01:00
|
|
|
this.deviceRegistry.allDevices.forEach((device) => {
|
2021-01-30 10:00:09 +01:00
|
|
|
this.client.publish(getAvailabilityTopic(device), 'offline');
|
2021-01-29 21:25:34 +01:00
|
|
|
});
|
|
|
|
|
this.client.end(callback);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
sendDiscoveryToHomeAssistant() {
|
|
|
|
|
logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`);
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
this.deviceRegistry.allDevices.forEach((device) => {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.debug(`Sending discovery for ${device.name}`);
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-02-01 21:36:40 +01:00
|
|
|
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
2021-01-22 15:49:02 +01:00
|
|
|
logger.info(
|
|
|
|
|
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
|
|
|
|
|
);
|
2019-12-13 14:13:00 +01:00
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
this.client.publish(getConfigPath(device), JSON.stringify(payload));
|
2021-01-29 21:25:34 +01:00
|
|
|
setTimeout(() => {
|
2021-02-01 21:19:22 +01:00
|
|
|
this.client.publish(getAvailabilityTopic(device), 'online');
|
2021-01-29 21:25:34 +01:00
|
|
|
}, 2000);
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-21 15:01:15 +00:00
|
|
|
updateState(deviceId, data) {
|
2021-02-01 21:19:22 +01:00
|
|
|
const device = this.deviceRegistry.getDevice(deviceId);
|
2019-12-04 11:17:06 +01:00
|
|
|
|
|
|
|
|
if (!device) {
|
2021-01-21 21:31:37 +01:00
|
|
|
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
|
2019-12-04 11:17:06 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-25 08:06:28 +01:00
|
|
|
logger.verbose(
|
|
|
|
|
`Updating state for ${device.name}: ${data.state}${
|
|
|
|
|
data.brightness ? `, dim: ${data.brightness}` : ''
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2019-12-21 15:01:15 +00:00
|
|
|
let payload = null;
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2020-02-29 15:54:08 +00:00
|
|
|
if (device.type === 'switch') {
|
|
|
|
|
payload = data.state === 1 ? 'ON' : 'OFF';
|
2021-01-22 15:49:02 +01:00
|
|
|
} else {
|
2020-02-29 15:54:08 +00:00
|
|
|
if (device.dimmable) {
|
|
|
|
|
payload = {
|
|
|
|
|
state: data.state === 1 ? 'ON' : 'OFF',
|
2021-01-22 15:49:02 +01:00
|
|
|
brightness: data.brightness,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
2020-02-29 15:54:08 +00:00
|
|
|
payload = {
|
2021-01-22 15:49:02 +01:00
|
|
|
state: data.state === 1 ? 'ON' : 'OFF',
|
|
|
|
|
};
|
2020-02-29 15:54:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload = JSON.stringify(payload);
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-22 15:49:02 +01:00
|
|
|
this.client.publish(getStateTopic(device), payload);
|
2021-01-30 10:00:09 +01:00
|
|
|
this.client.publish(getAvailabilityTopic(device), 'online');
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
2019-12-22 17:48:16 +00:00
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
sceneTriggered(sceneId) {
|
|
|
|
|
logger.verbose(`Scene triggered: ${sceneId}`);
|
|
|
|
|
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }));
|
2019-12-22 17:48:16 +00:00
|
|
|
}
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-22 15:49:02 +01:00
|
|
|
module.exports = MqttClient;
|