Refactor mqtt messaging to use unique id:s and trigger scenes rather than switches
This commit is contained in:
parent
9a76a3ba50
commit
7de1238c12
1 changed files with 87 additions and 39 deletions
|
|
@ -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 }));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue