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
|
|
|
|
2025-08-05 19:55:50 +02: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
|
|
|
|
|
|
|
|
const discoveryPrefix = 'homeassistant';
|
|
|
|
|
const nodeId = 'plejd';
|
|
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
/** @type {import('./types/Mqtt').MQTT_TYPES} */
|
2021-04-21 21:07:17 +02:00
|
|
|
const MQTT_TYPES = {
|
|
|
|
|
LIGHT: 'light',
|
2021-05-03 09:45:57 +02:00
|
|
|
SCENE: 'scene',
|
2021-04-21 21:07:17 +02:00
|
|
|
SWITCH: 'switch',
|
2021-05-03 09:45:57 +02:00
|
|
|
DEVICE_AUTOMATION: 'device_automation',
|
2021-04-21 21:07:17 +02:00
|
|
|
};
|
|
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
/** @type {import('./types/Mqtt').TOPIC_TYPES} */
|
|
|
|
|
const TOPIC_TYPES = {
|
2021-04-26 13:13:10 +02:00
|
|
|
CONFIG: 'config',
|
|
|
|
|
STATE: 'state',
|
|
|
|
|
AVAILABILITY: 'availability',
|
2025-08-05 19:55:50 +02:00
|
|
|
SET: 'set',
|
2021-04-26 13:13:10 +02:00
|
|
|
};
|
|
|
|
|
|
2023-08-16 15:32:53 +02:00
|
|
|
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) =>
|
|
|
|
|
`${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
|
2021-03-31 20:07:46 +02:00
|
|
|
|
|
|
|
|
const getTopicName = (
|
2021-05-03 09:45:57 +02:00
|
|
|
/** @type { string } */ uniqueId,
|
|
|
|
|
/** @type { import('./types/Mqtt').MqttType } */ mqttDeviceType,
|
|
|
|
|
/** @type { import('./types/Mqtt').TopicType } */ topicType,
|
|
|
|
|
) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`;
|
2021-03-31 20:07:46 +02:00
|
|
|
|
2023-08-16 15:32:53 +02:00
|
|
|
const getButtonEventTopic = (/** @type {string} */ deviceId) =>
|
|
|
|
|
`${getTopicName(deviceId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
|
2021-05-20 07:24:22 +02:00
|
|
|
const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trig`;
|
2023-08-16 15:32:53 +02:00
|
|
|
const getSceneEventTopic = (/** @type {string} */ sceneId) =>
|
|
|
|
|
`${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
|
2021-04-26 13:13:10 +02:00
|
|
|
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
|
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
const decodeTopicRegexp =
|
|
|
|
|
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/;
|
2021-02-01 21:19:22 +01:00
|
|
|
|
|
|
|
|
const decodeTopic = (topic) => {
|
|
|
|
|
const matches = decodeTopicRegexp.exec(topic);
|
|
|
|
|
if (!matches) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return matches.groups;
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
const getOutputDeviceDiscoveryPayload = (
|
2021-03-31 20:07:46 +02:00
|
|
|
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
|
|
|
|
|
) => ({
|
2023-08-18 20:07:40 +02:00
|
|
|
name: null,
|
2021-03-31 23:28:25 +02:00
|
|
|
unique_id: device.uniqueId,
|
2021-05-03 09:45:57 +02:00
|
|
|
'~': getBaseTopic(device.uniqueId, device.type),
|
|
|
|
|
state_topic: `~/${TOPIC_TYPES.STATE}`,
|
2025-08-05 19:55:50 +02:00
|
|
|
command_topic: `~/${TOPIC_TYPES.SET}`,
|
2021-05-03 09:45:57 +02:00
|
|
|
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
2019-12-21 15:01:15 +00:00
|
|
|
optimistic: false,
|
2021-03-31 20:07:46 +02:00
|
|
|
qos: 1,
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: false, // State update messages from HA should not be retained
|
2020-01-21 14:24:02 +00:00
|
|
|
device: {
|
2021-05-07 10:59:52 +02:00
|
|
|
identifiers: `${device.uniqueId}`,
|
2020-01-21 14:24:02 +00:00
|
|
|
manufacturer: 'Plejd',
|
|
|
|
|
model: device.typeName,
|
|
|
|
|
name: device.name,
|
2021-05-07 08:52:57 +02:00
|
|
|
...(device.roomName !== undefined ? { suggested_area: device.roomName } : {}),
|
2021-01-22 15:49:02 +01:00
|
|
|
sw_version: device.version,
|
|
|
|
|
},
|
2021-05-03 09:45:57 +02:00
|
|
|
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
|
2025-08-05 19:55:50 +02:00
|
|
|
...(device.type === MQTT_TYPES.LIGHT &&
|
|
|
|
|
device.colorTempSettings &&
|
|
|
|
|
device.colorTempSettings.behavior === 'adjustable'
|
|
|
|
|
? {
|
|
|
|
|
min_mireds: 1000000 / device.colorTempSettings.minTemperatureLimit,
|
|
|
|
|
max_mireds: 1000000 / device.colorTempSettings.maxTemperatureLimit,
|
|
|
|
|
supported_color_modes: ['color_temp'],
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2019-12-10 22:01:12 +01:00
|
|
|
});
|
|
|
|
|
|
2021-04-26 13:13:10 +02:00
|
|
|
const getSceneDiscoveryPayload = (
|
2021-03-31 20:07:46 +02:00
|
|
|
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
|
|
|
|
|
) => ({
|
|
|
|
|
name: sceneDevice.name,
|
2021-04-24 09:22:36 +02:00
|
|
|
unique_id: sceneDevice.uniqueId,
|
2021-05-03 09:45:57 +02:00
|
|
|
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
|
2025-08-05 19:55:50 +02:00
|
|
|
command_topic: `~/${TOPIC_TYPES.SET}`,
|
2021-05-03 09:45:57 +02:00
|
|
|
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
2021-04-24 09:22:36 +02:00
|
|
|
payload_on: 'ON',
|
2021-03-31 20:07:46 +02:00
|
|
|
qos: 1,
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: false, // State update messages from HA should not be retained
|
2020-02-20 13:02:47 +01:00
|
|
|
});
|
|
|
|
|
|
2021-05-01 19:41:29 +02:00
|
|
|
const getInputDeviceTriggerDiscoveryPayload = (
|
|
|
|
|
/** @type {import('./types/DeviceRegistry').InputDevice} */ inputDevice,
|
|
|
|
|
) => ({
|
|
|
|
|
automation_type: 'trigger',
|
|
|
|
|
payload: `${inputDevice.input}`,
|
2021-05-05 19:02:10 +02:00
|
|
|
'~': getBaseTopic(inputDevice.deviceId, MQTT_TYPES.DEVICE_AUTOMATION),
|
2021-05-01 19:41:29 +02:00
|
|
|
qos: 1,
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
|
|
|
|
subtype: `button_${inputDevice.input + 1}`,
|
2021-05-05 19:02:10 +02:00
|
|
|
topic: `~/${TOPIC_TYPES.STATE}`,
|
2021-05-01 19:41:29 +02:00
|
|
|
type: 'button_short_press',
|
|
|
|
|
device: {
|
|
|
|
|
identifiers: `${inputDevice.deviceId}`,
|
|
|
|
|
manufacturer: 'Plejd',
|
|
|
|
|
model: inputDevice.typeName,
|
|
|
|
|
name: inputDevice.name,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-04-26 13:13:10 +02:00
|
|
|
const getSceneDeviceTriggerhDiscoveryPayload = (
|
|
|
|
|
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
|
|
|
|
|
) => ({
|
|
|
|
|
automation_type: 'trigger',
|
2021-05-20 07:24:22 +02:00
|
|
|
'~': getBaseTopic(`${sceneDevice.uniqueId}_trig`, MQTT_TYPES.DEVICE_AUTOMATION),
|
2021-04-26 13:13:10 +02:00
|
|
|
qos: 1,
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
2021-05-03 09:45:57 +02:00
|
|
|
topic: `~/${TOPIC_TYPES.STATE}`,
|
2021-04-26 13:13:10 +02:00
|
|
|
type: 'scene',
|
|
|
|
|
subtype: 'trigger',
|
|
|
|
|
device: {
|
2021-05-20 07:24:22 +02:00
|
|
|
identifiers: `${sceneDevice.uniqueId}_trigger`,
|
2021-04-26 13:13:10 +02:00
|
|
|
manufacturer: 'Plejd',
|
|
|
|
|
model: sceneDevice.typeName,
|
|
|
|
|
name: sceneDevice.name,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-31 20:07:46 +02:00
|
|
|
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
|
2025-08-05 19:55:50 +02:00
|
|
|
const AVAILABLITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
2021-03-31 20:07:46 +02:00
|
|
|
|
2019-12-04 11:17:06 +01:00
|
|
|
class MqttClient extends EventEmitter {
|
2021-03-31 20:07:46 +02:00
|
|
|
/** @type {import('DeviceRegistry')} */
|
2021-02-01 21:19:22 +01:00
|
|
|
deviceRegistry;
|
|
|
|
|
|
2021-02-20 15:33:06 +01:00
|
|
|
static EVENTS = {
|
|
|
|
|
connected: 'connected',
|
|
|
|
|
stateChanged: 'stateChanged',
|
|
|
|
|
};
|
|
|
|
|
|
2021-04-21 21:07:17 +02:00
|
|
|
/**
|
|
|
|
|
* @param {import("DeviceRegistry")} deviceRegistry
|
|
|
|
|
*/
|
2021-02-01 21:19:22 +01:00
|
|
|
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, {
|
2025-08-05 19:55:50 +02:00
|
|
|
clean: true, // We're moving to not saving mqtt messages
|
2021-04-21 21:07:17 +02:00
|
|
|
clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`,
|
2021-02-01 21:19:22 +01:00
|
|
|
password: this.config.mqttPassword,
|
2025-08-05 19:55:50 +02:00
|
|
|
properties: {
|
|
|
|
|
sessionExpiryInterval: 120, // 2 minutes sessions for the QoS, after that old messages are discarded
|
|
|
|
|
},
|
|
|
|
|
protocolVersion: 5,
|
2021-04-21 21:07:17 +02:00
|
|
|
queueQoSZero: true,
|
|
|
|
|
username: this.config.mqttUsername,
|
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
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
this.emit(MqttClient.EVENTS.connected);
|
|
|
|
|
|
|
|
|
|
// Testing to skip listening to HA birth messages all together
|
|
|
|
|
// this.client.subscribe(
|
|
|
|
|
// startTopics,
|
|
|
|
|
// {
|
|
|
|
|
// qos: 1,
|
|
|
|
|
// nl: true, // don't echo back messages sent
|
|
|
|
|
// rap: true, // retain as published - don't force retain = 0
|
|
|
|
|
// rh: 0, // Retain handling 0 presumably ignores retained messages
|
|
|
|
|
// },
|
|
|
|
|
// (err) => {
|
|
|
|
|
// if (err) {
|
|
|
|
|
// logger.error('Unable to subscribe to status topics', err);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// this.emit(MqttClient.EVENTS.connected);
|
|
|
|
|
// },
|
|
|
|
|
// );
|
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-02-22 09:50:06 +01:00
|
|
|
try {
|
2025-08-05 19:55:50 +02:00
|
|
|
logger.verbose(`Received mqtt message on ${topic}`);
|
|
|
|
|
const decodedTopic = decodeTopic(topic);
|
|
|
|
|
if (decodedTopic) {
|
|
|
|
|
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
|
|
|
|
let device;
|
|
|
|
|
|
|
|
|
|
if (decodedTopic.type === MQTT_TYPES.SCENE) {
|
|
|
|
|
logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
|
|
|
|
|
device = this.deviceRegistry.getScene(decodedTopic.id);
|
|
|
|
|
} else {
|
|
|
|
|
logger.verbose(`Getting device ${decodedTopic.id} from registry`);
|
|
|
|
|
device = this.deviceRegistry.getOutputDevice(decodedTopic.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const messageString = message.toString();
|
|
|
|
|
const isJsonMessage = messageString.startsWith('{');
|
|
|
|
|
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
|
2021-02-01 21:19:22 +01:00
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
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(MqttClient.EVENTS.stateChanged, device, command);
|
|
|
|
|
} else {
|
|
|
|
|
logger.warn(
|
|
|
|
|
`Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`,
|
2021-02-01 21:19:22 +01:00
|
|
|
);
|
2025-08-05 19:55:50 +02:00
|
|
|
}
|
|
|
|
|
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`);
|
2021-02-01 21:19:22 +01:00
|
|
|
}
|
2025-08-05 19:55:50 +02:00
|
|
|
} else {
|
|
|
|
|
logger.verbose(
|
|
|
|
|
`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`,
|
|
|
|
|
);
|
2021-01-25 08:06:28 +01:00
|
|
|
}
|
2025-08-05 19:55:50 +02:00
|
|
|
// }
|
2021-02-22 09:50:06 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
logger.error(`Error processing mqtt message on topic ${topic}`, err);
|
2021-01-21 21:31:37 +01:00
|
|
|
}
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reconnect() {
|
|
|
|
|
this.client.reconnect();
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 15:33:06 +01:00
|
|
|
cleanup() {
|
|
|
|
|
this.client.removeAllListeners();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 21:25:34 +01:00
|
|
|
disconnect(callback) {
|
2021-05-03 09:45:57 +02:00
|
|
|
logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...');
|
2021-03-31 20:07:46 +02:00
|
|
|
this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => {
|
2021-05-03 09:45:57 +02:00
|
|
|
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
|
2025-08-05 19:55:50 +02:00
|
|
|
AVAILABLITY.OFFLINE,
|
2021-05-03 09:45:57 +02:00
|
|
|
{
|
|
|
|
|
retain: true,
|
|
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
|
|
|
|
|
allSceneDevices.forEach((sceneDevice) => {
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
2025-08-05 19:55:50 +02:00
|
|
|
AVAILABLITY.OFFLINE,
|
2021-05-03 09:45:57 +02:00
|
|
|
{
|
|
|
|
|
retain: true,
|
|
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
2021-01-29 21:25:34 +01:00
|
|
|
});
|
|
|
|
|
this.client.end(callback);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-01 21:19:22 +01:00
|
|
|
sendDiscoveryToHomeAssistant() {
|
2025-08-05 19:55:50 +02:00
|
|
|
// -------- DISCOVERY FOR OUTPUT DEVICES -------------
|
|
|
|
|
|
2021-03-31 20:07:46 +02:00
|
|
|
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}`);
|
|
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
|
2025-08-05 19:55:50 +02:00
|
|
|
// Publish mqtt CONFIG message which will create the device in Home Assistant
|
2021-05-03 09:45:57 +02:00
|
|
|
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG),
|
|
|
|
|
JSON.stringify(configPayload),
|
|
|
|
|
{
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA and MQTT broker restarts
|
|
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
`Sent discovery message for ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// -------- CLEANUP RETAINED MESSAGES FOR OUTPUT DEVICES -------------
|
|
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
`Forcefully removing any retained SET, STATE, and AVAILABILITY messages for ${outputDevice.name}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Forcefully remove retained (from Home Assistant) SET messages (wanted state from HA)
|
|
|
|
|
this.client.publish(getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.SET), null, {
|
|
|
|
|
retain: true, // Retain true to remove previously retained message
|
|
|
|
|
qos: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Forcefully remove retained (from us, v0.11 and before) AVAILABILITY messages
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABLILITY),
|
|
|
|
|
null,
|
|
|
|
|
{
|
|
|
|
|
retain: true, // Retain true to remove previously retained message
|
|
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.debug(`Removal messages sent for ${outputDevice.name}`);
|
|
|
|
|
|
|
|
|
|
logger.debug(`Setting device as AVAILABILITY = ONLINE: ${outputDevice.name}`);
|
|
|
|
|
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
|
|
|
|
AVAILABLITY.ONLINE,
|
|
|
|
|
{
|
|
|
|
|
retain: false, // Availability messages should NOT be retained
|
2021-04-21 21:07:17 +02:00
|
|
|
qos: 1,
|
2021-05-03 09:45:57 +02:00
|
|
|
},
|
|
|
|
|
);
|
2021-03-31 20:07:46 +02:00
|
|
|
});
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
// -------- DISCOVERY FOR INPUT DEVICES -------------
|
|
|
|
|
|
2021-05-01 19:41:29 +02:00
|
|
|
const allInputDevices = this.deviceRegistry.getAllInputDevices();
|
|
|
|
|
logger.info(`Sending discovery for ${allInputDevices.length} Plejd input devices`);
|
|
|
|
|
allInputDevices.forEach((inputDevice) => {
|
|
|
|
|
logger.debug(`Sending discovery for ${inputDevice.name}`);
|
|
|
|
|
const inputInputPayload = getInputDeviceTriggerDiscoveryPayload(inputDevice);
|
|
|
|
|
logger.info(
|
2021-05-05 19:02:10 +02:00
|
|
|
`Discovered ${inputDevice.typeName} (${inputDevice.type}) named ${inputDevice.name} (${inputDevice.bleInputAddress} : ${inputDevice.uniqueId}).`,
|
2021-05-01 19:41:29 +02:00
|
|
|
);
|
2021-05-05 19:34:34 +02:00
|
|
|
logger.verbose(
|
|
|
|
|
`Publishing ${getTopicName(
|
|
|
|
|
inputDevice.uniqueId,
|
|
|
|
|
MQTT_TYPES.DEVICE_AUTOMATION,
|
|
|
|
|
TOPIC_TYPES.CONFIG,
|
|
|
|
|
)} with payload ${JSON.stringify(inputInputPayload)}`,
|
|
|
|
|
);
|
2021-05-01 19:41:29 +02:00
|
|
|
|
2021-05-05 19:34:34 +02:00
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG),
|
|
|
|
|
JSON.stringify(inputInputPayload),
|
|
|
|
|
{
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
2021-05-05 19:34:34 +02:00
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
2021-05-01 19:41:29 +02:00
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
// -------- DISCOVERY FOR SCENE DEVICES -------------
|
|
|
|
|
|
2021-03-31 20:07:46 +02:00
|
|
|
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}`);
|
2019-12-04 11:17:06 +01:00
|
|
|
|
2021-04-26 13:13:10 +02:00
|
|
|
const sceneConfigPayload = getSceneDiscoveryPayload(sceneDevice);
|
2021-01-22 15:49:02 +01:00
|
|
|
logger.info(
|
2021-03-31 20:07:46 +02:00
|
|
|
`Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`,
|
2021-01-22 15:49:02 +01:00
|
|
|
);
|
2019-12-13 14:13:00 +01:00
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
|
|
|
|
|
JSON.stringify(sceneConfigPayload),
|
|
|
|
|
{
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
2021-05-03 09:45:57 +02:00
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
2021-04-26 13:13:10 +02:00
|
|
|
|
|
|
|
|
const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice);
|
|
|
|
|
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(
|
2021-05-03 09:45:57 +02:00
|
|
|
getTriggerUniqueId(sceneDevice.uniqueId),
|
|
|
|
|
MQTT_TYPES.DEVICE_AUTOMATION,
|
|
|
|
|
TOPIC_TYPES.CONFIG,
|
2021-04-26 13:13:10 +02:00
|
|
|
),
|
|
|
|
|
JSON.stringify(sceneTriggerConfigPayload),
|
|
|
|
|
{
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
2021-04-26 13:13:10 +02:00
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-08-05 19:55:50 +02:00
|
|
|
// setTimeout(() => {
|
|
|
|
|
this.client.publish(
|
|
|
|
|
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
|
|
|
|
AVAILABLITY.ONLINE,
|
|
|
|
|
{
|
|
|
|
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
|
|
|
|
qos: 1,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
// }, 2000);
|
2019-12-04 11:17:06 +01:00
|
|
|
});
|
2025-08-05 19:55:50 +02:00
|
|
|
|
|
|
|
|
// -------- SUBSCRIBE TO INCOMING MESSAGES -------------
|
|
|
|
|
|
|
|
|
|
this.client.subscribe(
|
|
|
|
|
getSubscribePath(),
|
|
|
|
|
{
|
|
|
|
|
qos: 1,
|
|
|
|
|
nl: true, // don't echo back messages sent
|
|
|
|
|
rap: true, // retain as published - don't force retain = 0
|
|
|
|
|
rh: 0, // Retain handling 0 presumably ignores retained messages
|
|
|
|
|
},
|
|
|
|
|
(err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
logger.error('Unable to subscribe to control topics');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2021-03-31 20:07:46 +02:00
|
|
|
/**
|
|
|
|
|
* @param {string} uniqueOutputId
|
|
|
|
|
* @param {{ state: boolean; brightness?: number; }} data
|
|
|
|
|
*/
|
|
|
|
|
updateOutputState(uniqueOutputId, data) {
|
|
|
|
|
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
|
2019-12-04 11:17:06 +01:00
|
|
|
|
|
|
|
|
if (!device) {
|
2021-03-31 20:07:46 +02:00
|
|
|
logger.warn(`Unknown output id ${uniqueOutputId} - 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') {
|
2021-03-31 20:07:46 +02:00
|
|
|
payload = getMqttStateString(data.state);
|
2021-01-22 15:49:02 +01:00
|
|
|
} else {
|
2020-02-29 15:54:08 +00:00
|
|
|
if (device.dimmable) {
|
|
|
|
|
payload = {
|
2021-03-31 20:07:46 +02:00
|
|
|
state: getMqttStateString(data.state),
|
2021-01-22 15:49:02 +01:00
|
|
|
brightness: data.brightness,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
2020-02-29 15:54:08 +00:00
|
|
|
payload = {
|
2021-03-31 20:07:46 +02:00
|
|
|
state: getMqttStateString(data.state),
|
2021-01-22 15:49:02 +01:00
|
|
|
};
|
2020-02-29 15:54:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload = JSON.stringify(payload);
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2021-05-03 09:45:57 +02:00
|
|
|
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
|
|
|
|
this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
|
2025-08-05 19:55:50 +02:00
|
|
|
retain: false,
|
2021-04-21 21:07:17 +02:00
|
|
|
qos: 1,
|
|
|
|
|
});
|
2021-05-03 09:45:57 +02:00
|
|
|
// this.client.publish(
|
|
|
|
|
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
2025-08-05 19:55:50 +02:00
|
|
|
// AVAILABILITY.ONLINE,
|
|
|
|
|
// { retain: false, qos: 1 },
|
2021-05-03 09:45:57 +02:00
|
|
|
// );
|
2019-12-04 11:17:06 +01:00
|
|
|
}
|
2019-12-22 17:48:16 +00:00
|
|
|
|
2021-05-06 07:58:06 +02:00
|
|
|
/**
|
|
|
|
|
* @param {string} deviceId
|
|
|
|
|
* @param {string} deviceInput
|
|
|
|
|
*/
|
|
|
|
|
buttonPressed(deviceId, deviceInput) {
|
|
|
|
|
logger.verbose(`Button ${deviceInput} pressed for deviceId ${deviceId}`);
|
2025-08-05 19:55:50 +02:00
|
|
|
this.client.publish(getButtonEventTopic(deviceId), `${deviceInput}`, {
|
|
|
|
|
retain: false,
|
|
|
|
|
qos: 1,
|
|
|
|
|
});
|
2021-05-01 19:41:29 +02:00
|
|
|
}
|
2021-05-05 19:34:34 +02:00
|
|
|
|
2021-03-31 20:07:46 +02:00
|
|
|
/**
|
|
|
|
|
* @param {string} sceneId
|
|
|
|
|
*/
|
2021-02-01 21:19:22 +01:00
|
|
|
sceneTriggered(sceneId) {
|
|
|
|
|
logger.verbose(`Scene triggered: ${sceneId}`);
|
2025-08-05 19:55:50 +02:00
|
|
|
this.client.publish(getSceneEventTopic(sceneId), '', {
|
|
|
|
|
qos: 1,
|
|
|
|
|
retain: false,
|
|
|
|
|
});
|
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;
|