Refactor mqtt messaging to use unique id:s and trigger scenes rather than switches

This commit is contained in:
Victor Hagelbäck 2021-03-31 20:07:46 +02:00
parent 9a76a3ba50
commit 7de1238c12

View file

@ -13,12 +13,22 @@ const logger = Logger.getLogger('plejd-mqtt');
const discoveryPrefix = 'homeassistant'; const discoveryPrefix = 'homeassistant';
const nodeId = 'plejd'; const nodeId = 'plejd';
const getMqttUniqueId = (/** @type {string} */ uniqueId) => `${nodeId}.${uniqueId}`;
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`; const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${plug.type}/${nodeId}/${getMqttUniqueId(plug.uniqueId)}`;
const getConfigPath = (plug) => `${getPath(plug)}/config`;
const getStateTopic = (plug) => `${getPath(plug)}/state`; const getTopicName = (
const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`; /** @type {{ uniqueId: string; type: string; }} */ plug,
const getCommandTopic = (plug) => `${getPath(plug)}/set`; /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType,
) => `${getBaseTopic(plug)}/${topicType}`;
const TOPICS = {
CONFIG: 'config',
STATE: 'state',
AVAILABILITY: 'availability',
COMMAND: 'set',
};
const getSceneEventTopic = () => 'plejd/event/scene'; const getSceneEventTopic = () => 'plejd/event/scene';
const decodeTopicRegexp = new RegExp( const decodeTopicRegexp = new RegExp(
@ -33,17 +43,22 @@ const decodeTopic = (topic) => {
return matches.groups; return matches.groups;
}; };
const getDiscoveryPayload = (device) => ({ const getLightDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
) => ({
schema: 'json', schema: 'json',
name: device.name, name: device.name,
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, unique_id: getMqttUniqueId(device.uniqueId),
state_topic: getStateTopic(device), '~': getBaseTopic(device),
command_topic: getCommandTopic(device), state_topic: `~/${TOPICS.STATE}`,
availability_topic: getAvailabilityTopic(device), command_topic: `~/${TOPICS.COMMAND}`,
availability_topic: `~/${TOPICS.AVAILABILITY}`,
optimistic: false, optimistic: false,
brightness: `${device.dimmable}`, qos: 1,
retain: true,
brightness: device.dimmable,
device: { device: {
identifiers: `${device.serialNumber}_${device.id}`, identifiers: `${device.deviceId}`,
manufacturer: 'Plejd', manufacturer: 'Plejd',
model: device.typeName, model: device.typeName,
name: device.name, name: device.name,
@ -51,23 +66,32 @@ const getDiscoveryPayload = (device) => ({
}, },
}); });
const getSwitchPayload = (device) => ({ const getScenehDiscoveryPayload = (
name: device.name, /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
state_topic: getStateTopic(device), ) => ({
command_topic: getCommandTopic(device), name: sceneDevice.name,
'~': getBaseTopic(sceneDevice),
state_topic: `~/${TOPICS.STATE}`,
command_topic: `~/${TOPICS.COMMAND}`,
optimistic: false, optimistic: false,
qos: 1,
retain: true,
device: { device: {
identifiers: `${device.serialNumber}_${device.id}`, identifiers: `${sceneDevice.uniqueId}`,
manufacturer: 'Plejd', manufacturer: 'Plejd',
model: device.typeName, model: sceneDevice.typeName,
name: device.name, name: sceneDevice.name,
sw_version: device.version, sw_version: sceneDevice.version,
}, },
}); });
// #endregion // #endregion
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
class MqttClient extends EventEmitter { class MqttClient extends EventEmitter {
/** @type {import('DeviceRegistry')} */
deviceRegistry; deviceRegistry;
static EVENTS = { static EVENTS = {
@ -125,7 +149,7 @@ class MqttClient extends EventEmitter {
} else { } else {
const decodedTopic = decodeTopic(topic); const decodedTopic = decodeTopic(topic);
if (decodedTopic) { if (decodedTopic) {
let device = this.deviceRegistry.getDevice(decodedTopic.id); let device = this.deviceRegistry.getOutputDevice(decodedTopic.id);
const messageString = message.toString(); const messageString = message.toString();
const isJsonMessage = messageString.startsWith('{'); const isJsonMessage = messageString.startsWith('{');
@ -144,6 +168,7 @@ class MqttClient extends EventEmitter {
); );
device = this.deviceRegistry.getScene(decodedTopic.id); device = this.deviceRegistry.getScene(decodedTopic.id);
} }
const deviceName = device ? device.name : ''; const deviceName = device ? device.name : '';
switch (decodedTopic.command) { switch (decodedTopic.command) {
@ -195,35 +220,55 @@ class MqttClient extends EventEmitter {
} }
disconnect(callback) { disconnect(callback) {
this.deviceRegistry.allDevices.forEach((device) => { this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => {
this.client.publish(getAvailabilityTopic(device), 'offline'); this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE);
}); });
this.client.end(callback); this.client.end(callback);
} }
sendDiscoveryToHomeAssistant() { sendDiscoveryToHomeAssistant() {
logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`); const allOutputDevices = this.deviceRegistry.getAllOutputDevices();
logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`);
allOutputDevices.forEach((outputDevice) => {
logger.debug(`Sending discovery for ${outputDevice.name}`);
this.deviceRegistry.allDevices.forEach((device) => { const configPayload = getLightDiscoveryPayload(outputDevice);
logger.debug(`Sending discovery for ${device.name}`);
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
logger.info( logger.info(
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`, `Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
); );
this.client.publish(getConfigPath(device), JSON.stringify(payload)); this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload));
setTimeout(() => { setTimeout(() => {
this.client.publish(getAvailabilityTopic(device), 'online'); this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE);
}, 2000);
});
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`);
allSceneDevices.forEach((sceneDevice) => {
logger.debug(`Sending discovery for ${sceneDevice.name}`);
const configPayload = getScenehDiscoveryPayload(sceneDevice);
logger.info(
`Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`,
);
this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(configPayload));
setTimeout(() => {
this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE);
}, 2000); }, 2000);
}); });
} }
updateState(deviceId, data) { /**
const device = this.deviceRegistry.getDevice(deviceId); * @param {string} uniqueOutputId
* @param {{ state: boolean; brightness?: number; }} data
*/
updateOutputState(uniqueOutputId, data) {
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
if (!device) { if (!device) {
logger.warn(`Unknown device id ${deviceId} - not handled by us.`); logger.warn(`Unknown output id ${uniqueOutputId} - not handled by us.`);
return; return;
} }
@ -235,26 +280,29 @@ class MqttClient extends EventEmitter {
let payload = null; let payload = null;
if (device.type === 'switch') { if (device.type === 'switch') {
payload = data.state === 1 ? 'ON' : 'OFF'; payload = getMqttStateString(data.state);
} else { } else {
if (device.dimmable) { if (device.dimmable) {
payload = { payload = {
state: data.state === 1 ? 'ON' : 'OFF', state: getMqttStateString(data.state),
brightness: data.brightness, brightness: data.brightness,
}; };
} else { } else {
payload = { payload = {
state: data.state === 1 ? 'ON' : 'OFF', state: getMqttStateString(data.state),
}; };
} }
payload = JSON.stringify(payload); payload = JSON.stringify(payload);
} }
this.client.publish(getStateTopic(device), payload); this.client.publish(getTopicName(device, 'state'), payload);
this.client.publish(getAvailabilityTopic(device), 'online'); this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE);
} }
/**
* @param {string} sceneId
*/
sceneTriggered(sceneId) { sceneTriggered(sceneId) {
logger.verbose(`Scene triggered: ${sceneId}`); logger.verbose(`Scene triggered: ${sceneId}`);
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId })); this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }));