iobroker.shelly
Version:
Integrate your Shelly devices into ioBroker via MQTT or CoAP
1,042 lines (907 loc) • 41.3 kB
JavaScript
'use strict';
const BaseClient = require('./base').BaseClient;
const BaseServer = require('./base').BaseServer;
const datapoints = require('../datapoints');
const mqtt = require('mqtt-connection');
const net = require('net');
const adapterVersion = require('../../package.json').version;
const INIT_SRC = 'iobroker-init';
/**
* checks if function is an asynchron function
*
* @param funct - function
*/
function isAsync(funct) {
if (funct && funct.constructor) {
return funct.constructor.name == 'AsyncFunction';
}
return undefined;
}
class MQTTClient extends BaseClient {
constructor(adapter, objectHelper, eventEmitter, stream) {
super('mqtt', adapter, objectHelper, eventEmitter);
this.stream = stream;
this.mqttprefix;
this.messageCache = {};
this.messageId = 1;
this.will = undefined;
this.initializing = false; // use to delay publish packets after new connection
this.publishQueue = []; // queue for publish packets during initialization
this.processingQueue = false; // flag to indicate queue processing is in progress
this.destroying = false; // flag to prevent new packets from being queued during destruction
this.client;
this.start();
}
start() {
this.client = mqtt(this.stream);
this.listener();
}
/**
* to get sure, that an instance will be start more than one, we check for running instances
* if an instance run with same name (shellyswitch-12345), we destroy the old instance
*
* @param self - my instance
*/
static _registerRun(self) {
if (self) {
this.clientlist = this.clientlist || {};
const name = self.getId();
if (name && this.clientlist[name]) {
this.clientlist[name].destroy();
}
this.clientlist[name] = self;
}
}
/**
* @inheritdoc
*/
getSerialId() {
if (!this.serialId) {
let id = this.getId();
if (id) {
id = id.replace(/(.+?)\/(.+?)\/(.*)/, '$2');
this.serialId = id.replace(/(.+)-(.+)/, '$2');
}
}
return this.serialId;
}
/**
* @inheritdoc
*/
getDeviceClass() {
if (!this.deviceClass) {
let id = this.getId();
if (id) {
id = id.replace(/(.+?)\/(.+?)\/(.*)/, '$2');
this.deviceClass = id.replace(/(.+)-(.+)/, '$1');
}
}
return this.deviceClass;
}
/**
* @inheritdoc
*/
getDeviceType() {
if (!this.deviceType) {
const deviceClass = this.getDeviceClass();
this.deviceType = datapoints.getDeviceTypeByClass(deviceClass);
}
return this.deviceType;
}
/**
* @inheritdoc
*/
getDeviceId() {
if (!this.deviceId) {
const deviceType = this.getDeviceType();
const serialId = this.getSerialId();
if (deviceType && serialId) {
this.deviceId = `${deviceType}#${serialId}#1`;
}
}
return this.deviceId;
}
getMqttPrefix() {
return this.mqttprefix;
}
replacePrefixIn(topic) {
return new String(topic).replace(new RegExp('<mqttprefix>', 'g'), this.getMqttPrefix());
}
/**
* Process queued publish packets sequentially
*/
async processPublishQueue() {
// Atomic check-and-set to prevent race conditions
// Both operations must be synchronous (no await between them)
if (this.processingQueue || this.publishQueue.length === 0) {
return;
}
this.processingQueue = true;
this.adapter.log.silly(
`[MQTT] Publish: ${this.getLogInfo()} - start processing ${this.publishQueue.length} queued packets`,
);
try {
while (this.publishQueue.length > 0) {
const packet = this.publishQueue.shift();
try {
await this.processPublishPacket(packet);
} catch (err) {
this.adapter.log.error(
`[MQTT] Publish: ${this.getLogInfo()} - error processing packet: ${err && err.message ? err.message : err}`,
);
}
}
} finally {
this.processingQueue = false;
}
this.adapter.log.silly(`[MQTT] Publish: ${this.getLogInfo()} - processing queued packets done`);
}
/**
* Process a single publish packet
*
* @param packet - MQTT publish packet
*/
async processPublishPacket(packet) {
if (this.adapter.isUnloaded) {
return;
}
this.adapter.log.silly(`[MQTT] Publish: ${this.getLogInfo()} - ${JSON.stringify(packet)}`);
if (packet.payload) {
this.adapter.log.debug(
`[MQTT] Publish: ${this.getLogInfo()} - topic: ${packet.topic}, qos: ${packet.qos}, payload: ${packet.payload.toString()}`,
);
// Generation 1
if (this.getDeviceGen() === 1) {
if (packet.topic === 'shellies/announce') {
try {
const payloadObj = JSON.parse(packet.payload);
// Update IP address
const ip = payloadObj?.ip;
if (ip && ip !== this.getIP()) {
await this.setIP(ip, 'Gen 1 shellies/announce');
await this.adapter.deviceStatusUpdate(this.getDeviceId(), true); // Device online
}
// Device Mode information (new)
const newDeviceMode = payloadObj?.mode;
if (newDeviceMode) {
await this.setDeviceMode(newDeviceMode);
}
} catch {
// we do not change anything
}
}
}
// Generation 2+
if (this.getDeviceGen() >= 2) {
if (packet.topic === 'iobroker/rpc') {
try {
const payloadObj = JSON.parse(packet.payload.toString());
// Error handling for Gen 2+ devices
if (payloadObj?.error) {
this.adapter.log.error(
`[MQTT] Received error message for ${this.getLogInfo()} - from "${payloadObj.src}": ${JSON.stringify(payloadObj.error)}`,
);
}
} catch (err) {
this.adapter.log.debug(
`[MQTT] Error parsing command response: ${this.getLogInfo()} - topic: ${packet.topic}, error: ${err}`,
);
}
}
if (packet.topic === `${INIT_SRC}/rpc`) {
try {
const payloadObj = JSON.parse(packet.payload.toString());
// Update IP address
const ip = payloadObj?.result?.eth?.ip || payloadObj?.result?.wifi?.sta_ip;
if (ip && ip !== this.getIP()) {
await this.setIP(ip, `Gen 2+ ${INIT_SRC}/rpc`);
await this.adapter.deviceStatusUpdate(this.getDeviceId(), true); // Device online
}
// Device Mode information (new)
if (payloadObj?.result?.profile) {
const newDeviceMode = payloadObj?.result?.profile;
if (newDeviceMode) {
await this.setDeviceMode(newDeviceMode);
}
}
} catch (err) {
this.adapter.log.debug(
`[MQTT] Error parsing init command response: ${this.getLogInfo()} - topic: ${packet.topic}, error: ${err}`,
);
}
}
// Log debug messages for Gen 2+ devices (if enabled in instance configuration)
if (packet.topic === `${this.getMqttPrefix()}/debug/log`) {
if (this.adapter.config.logDebugMessages) {
this.adapter.log.info(
`[Shelly Debug Message] ${this.getLogInfo()}: ${packet.payload.toString().trim()}`,
);
}
}
}
}
await this.createIoBrokerState(packet.topic, packet.payload);
}
async destroy() {
// Set destroying flag to prevent new packets from being queued
this.destroying = true;
// Wait for initialization to complete
if (this.initializing) {
this.adapter.log.debug(`[MQTT] Destroying ${this.getLogInfo()}, waiting for initialization to complete`);
let cnt = 5 * 60; // wait up to 60 seconds
while (this.initializing && cnt-- > 0) {
await this.adapter.delay(200);
}
}
// Wait for queue processing to complete
if (this.processingQueue) {
this.adapter.log.debug(`[MQTT] Destroying ${this.getLogInfo()}, waiting for queue processing to complete`);
let cnt = 5 * 60; // wait up to 60 seconds
while (this.processingQueue && cnt-- > 0) {
await this.adapter.delay(200);
}
}
// Process any remaining queued packets
if (this.publishQueue.length > 0) {
this.adapter.log.debug(
`[MQTT] Destroying ${this.getLogInfo()}, processing ${this.publishQueue.length} remaining queued packets`,
);
await this.processPublishQueue();
}
super.destroy();
this.adapter.log.debug(`[MQTT] Destroying`);
for (const messageId in this.messageCache) {
if (this.messageCache[messageId].resendid) {
clearTimeout(this.messageCache[messageId].resendid);
}
}
this.messageCache = {};
this.messageId = 1;
this.mqttprefix = undefined;
this.will = undefined;
this.publishQueue = [];
this.processingQueue = false;
if (this.client) {
this.client.removeAllListeners();
this.client.destroy();
}
}
/**
* @inheritdoc
*/
publishStateValue(topic, value) {
if (topic.includes('<mqttprefix>') && !this.getMqttPrefix()) {
this.adapter.log.warn(
`[MQTT] Unable to publish message to ${this.getLogInfo()} - mqtt prefix was not set but is required for this message: ${topic} = ${value}`,
);
return;
}
topic = this.replacePrefixIn(topic);
const qos = parseInt(this.adapter.config.qos) ?? 0;
this.messageId &= 0xffffffff;
this.messageId++;
try {
this.adapter.log.debug(
`[MQTT] Send state to ${this.getLogInfo()} with QoS ${qos}: ${topic} = ${value} (${this.messageId})`,
);
this.client.publish({ topic: topic, payload: value, qos: qos, messageId: this.messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Unable to publish message to ${this.getLogInfo()} - received error "${err}": ${topic} = ${value}`,
);
}
if (qos > 0) {
this.deleteResendState2ClientFromTopic(topic);
this.resendState2Client('publish', this.messageId, {
topic,
payload: value,
qos,
dup: true,
messageId: this.messageId,
});
}
}
resendState2Client(cmd, messageId, message) {
const retaintime = 5 * 1000;
if (
!this.messageCache[messageId] ||
this.messageCache[messageId].cmd !== cmd ||
this.messageCache[messageId].message !== message
) {
this.messageCache[messageId] = {
ts: Date.now(),
cmd,
count: 0,
message,
};
}
if (this.messageCache[messageId] && this.messageCache[messageId].count < 10) {
clearTimeout(this.messageCache[messageId].resendid);
this.messageCache[messageId].resendid = setTimeout(() => {
if (this.messageCache[messageId]) {
const ts = Date.now();
this.messageCache[messageId].count++;
this.messageCache[messageId].ts = ts;
switch (this.messageCache[messageId].cmd) {
case 'publish':
try {
this.client.publish(this.messageCache[messageId].message);
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (publish): ${this.getLogInfo()} - error: ${err}`,
);
}
break;
case 'pubrel':
try {
this.client.pubrel({ messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubrel): ${this.getLogInfo()} - error: ${err}`,
);
}
break;
case 'pubrec':
try {
this.client.pubrec({ messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubrec): ${this.getLogInfo()} - error: ${err}`,
);
}
break;
case 'pubcomp':
try {
this.client.pubcomp({ messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubcomp): ${this.getLogInfo()} - error: ${err}`,
);
}
break;
default:
break;
}
this.resendState2Client(cmd, messageId, message);
}
}, retaintime);
}
}
deleteResendState2Client(messageId) {
if (this.messageCache[messageId]) {
clearTimeout(this.messageCache[messageId].resendid);
delete this.messageCache[messageId];
}
}
deleteResendState2ClientFromTopic(topic) {
for (const messageId in this.messageCache) {
if (
this.messageCache[messageId].message &&
this.messageCache[messageId].cmd === 'publish' &&
this.messageCache[messageId].message.topic === topic
) {
clearTimeout(this.messageCache[messageId].resendid);
delete this.messageCache[messageId];
}
}
}
getCachedMessage(messageId) {
return this.messageCache[messageId];
}
/**
* State changes from device will be saved in the ioBroker states
*
* @param topic
* @param payload - object can be ervery type of value
*/
async createIoBrokerState(topic, payload) {
this.adapter.log.silly(
`[MQTT] Message for ${this.getLogInfo()}: ${topic} / ${JSON.stringify(payload)} (${payload.toString()})`,
);
const dps = this.getAllTypePublishStates();
for (const i in dps) {
const dp = dps[i];
const deviceId = this.getDeviceId();
const fullStateId = `${deviceId}.${dp.state}`;
let value = payload.toString();
this.adapter.log.silly(
`[MQTT] Message with value for ${this.getLogInfo()}: ${topic} -> state: ${fullStateId}, value: ${value}`,
);
try {
if (this.replacePrefixIn(dp.mqtt.mqtt_publish) === topic) {
if (dp.mqtt?.mqtt_publish_funct) {
value = isAsync(dp.mqtt.mqtt_publish_funct)
? await dp.mqtt.mqtt_publish_funct(value, this)
: dp.mqtt.mqtt_publish_funct(value, this);
}
if (dp.common.type === 'boolean' && value === 'false') {
value = false;
}
if (dp.common.type === 'boolean' && value === 'true') {
value = true;
}
if (dp.common.type === 'number' && value !== undefined && value !== null) {
value = Number(value);
}
if (Object.prototype.hasOwnProperty.call(this.device, dp.state)) {
if (
value !== undefined &&
(!Object.prototype.hasOwnProperty.call(this.stateValueCache, fullStateId) ||
this.stateValueCache[fullStateId] !== value ||
this.adapter.config.updateUnchangedObjects)
) {
this.adapter.log.debug(
`[MQTT] State change ${this.getLogInfo()}: ${topic} -> state: ${fullStateId}, value: ${JSON.stringify(value)}`,
);
this.stateValueCache[fullStateId] = value;
this.objectHelper.setOrUpdateObject(
fullStateId,
{
type: 'state',
common: dp.common,
},
['name'],
value,
);
}
}
}
} catch (err) {
this.adapter.log.error(
`[MQTT] Error ${err} in function dp.mqtt.mqtt_publish_funct of state ${fullStateId} for ${this.getLogInfo()}`,
);
}
}
this.objectHelper.processObjectQueue(() => {});
}
async setMqttPrefixHttp() {
if (this.mqttprefix) {
return this.mqttprefix;
}
this.adapter.log.debug(
`[MQTT] Started mqttprefix fallback via HTTP for client ${this.getId()} (Gen ${this.getDeviceGen()})`,
);
if (this.getDeviceGen() == 1) {
try {
const body = await this.requestAsync('/settings');
if (body) {
const settings = JSON.parse(body);
this.mqttprefix = settings.mqtt.id;
this.adapter.log.debug(
`[MQTT] Requested mqttprefix for client ${this.getId()} (Gen 1): ${this.mqttprefix}`,
);
return this.mqttprefix;
}
} catch (err) {
if (err && err.response && err.response.status == 401) {
this.adapter.log.error(
`[MQTT] Wrong http username or http password! Please enter the user credential from restricted login for client ${this.getId()}`,
);
} else {
this.adapter.log.error(
`[MQTT] Error in function setMqttPrefixHttp (Gen 1) for client ${this.getId()}: ${err}`,
);
}
}
} else if (this.getDeviceGen() >= 2) {
try {
const body = await this.requestAsync('/rpc/Mqtt.GetConfig');
if (body) {
const settings = JSON.parse(body);
this.mqttprefix = settings.topic_prefix;
this.adapter.log.debug(
`[MQTT] Requested mqttprefix for client ${this.getId()} (Gen 2+): ${this.mqttprefix}`,
);
return this.mqttprefix;
}
} catch (err) {
this.adapter.log.error(
`[MQTT] Error in function setMqttPrefixHttp (Gen 2+) for client ${this.getId()}: ${err}`,
);
}
}
return undefined;
}
setMqttPrefixByWill(topic) {
// Gen1: "shellies/huhu-shellybutton1-A4CF12F454A3/online"
// Gen2: "shellyplus1pm-44179394d4d4/online"
if (this.mqttprefix) {
return this.mqttprefix;
}
if (topic) {
const arr = topic.split('/');
if (this.getDeviceGen() === 1) {
if (arr[0] === 'shellies') {
this.mqttprefix = arr[1];
this.adapter.log.debug(`[MQTT] setMqttPrefixByWill (Gen 1): ${this.mqttprefix}`);
return this.mqttprefix;
}
} else if (this.getDeviceGen() >= 2) {
this.mqttprefix = arr.slice(0, -1).join('/');
this.adapter.log.debug(`[MQTT] setMqttPrefixByWill (Gen 2+): ${this.mqttprefix}`);
return this.mqttprefix;
}
}
return undefined;
}
listener() {
// client connected
this.client.on('connect', async packet => {
this.id = packet.clientId;
this.adapter.log.debug(`[MQTT] Client connected: ${JSON.stringify(packet)}`);
if (this.deviceExists()) {
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" processing existing device`);
if (
packet.username === this.adapter.config.mqttusername &&
packet.password?.toString() === this.adapter.config.mqttpassword
) {
// check for existing instances
MQTTClient._registerRun(this);
const ip = this.stream?.remoteAddress ?? null;
this.adapter.log.info(`[MQTT] Device with client id "${packet.clientId}" connected from ${ip}!`);
this.initializing = true;
// Device Mode information (init)
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" init device mode`);
await this.initDeviceModeFromState();
// accept connection
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" accepting connection`);
try {
this.client.connack({ returnCode: 0 });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (connack): "${packet.clientId}" - error: ${err}`,
);
return;
}
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" deleting old states`);
await this.deleteOldStates();
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" create objects`);
await this.createObjects();
// Save last will
// Gen 1: {"retain": false, "qos": 0, "topic" :"shellies/shellyswitch25-C45BBE798F0F/online", "payload": {"type":"Buffer","data": [102,97,108,115,101]}}
// Gen 2+: {"retain": false, "qos": 0, "topic": "shellypro2pm-30c6f7850a64/online", "payload": {"type":"Buffer","data":[102,97,108,115,101]}}
if (packet.will) {
this.will = packet.will;
this.adapter.log.debug(
`[MQTT] Last will for client id "${packet.clientId}" saved: ${JSON.stringify(this.will)}`,
);
}
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" setting ip`);
if (ip && !String(ip).endsWith('.1')) {
// TODO: remove workaround to skip in Docker env
await this.setIP(ip, 'MQTT connect');
} else {
await this.initIPFromState();
}
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" updating device status`);
await this.adapter.deviceStatusUpdate(this.getDeviceId(), true); // Device online
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" setting http states`);
await this.httpIoBrokerState();
this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" setting mqtt prefix`);
if (this.will?.topic) {
this.setMqttPrefixByWill(this.will.topic);
} else {
await this.setMqttPrefixHttp();
}
if (!this.getMqttPrefix()) {
this.adapter.log.error(
`[MQTT] Unable to get mqttprefix of client with id "${packet.clientId}"`,
);
}
//this.adapter.log.silly(`[MQTT] Client id "${packet.clientId}" accepting connection`);
try {
//this.client.connack({ returnCode: 0 });
// Device Mode information (request)
if (this.getDeviceGen() >= 2) {
this.adapter.log.silly(
`[MQTT] Client id "${packet.clientId}" retrieving device mode information (Gen 2+)`,
);
try {
// Ask for more information (like IP address)
this.publishStateValue(
'<mqttprefix>/rpc',
JSON.stringify({
id: this.getNextMsgId(),
src: INIT_SRC,
method: 'Shelly.GetDeviceInfo',
params: { ident: true },
}),
);
this.publishStateValue(
'<mqttprefix>/rpc',
JSON.stringify({
id: this.getNextMsgId(),
src: INIT_SRC,
method: 'Shelly.GetStatus',
}),
);
this.publishStateValue('<mqttprefix>/command', 'status_update');
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (publish): "${packet.clientId}" - error: ${err}`,
);
}
}
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (connack): "${packet.clientId}" - error: ${err}`,
);
}
this.adapter.log.info(`[MQTT] Device with client id "${packet.clientId}" initialized.`);
this.initializing = false;
// Process any queued publish packets
if (this.publishQueue.length > 0) {
this.adapter.log.debug(
`[MQTT] Processing ${this.publishQueue.length} queued packets for ${this.getLogInfo()}`,
);
try {
await this.processPublishQueue();
} catch (err) {
this.adapter.log.error(
`[MQTT] Error processing publish queue for ${this.getLogInfo()}: ${err && err.message ? err.message : err}`,
);
}
}
} else {
if (packet.username === undefined) {
this.adapter.log.error(
`[MQTT] Wrong MQTT authentication of client "${packet.clientId}": No username transmitted by client`,
);
} else if (packet.username !== this.adapter.config.mqttusername) {
this.adapter.log.error(
`[MQTT] Wrong MQTT authentication of client "${packet.clientId}": Incorrect username (expected "${this.adapter.config.mqttusername}")`,
);
}
if (packet.password === undefined) {
this.adapter.log.error(
`[MQTT] Wrong MQTT authentication of client "${packet.clientId}": No password transmitted by client`,
);
} else if (packet.password?.toString() !== this.adapter.config.mqttpassword) {
this.adapter.log.error(
`[MQTT] Wrong MQTT authentication of client "${packet.clientId}": Incorrect password`,
);
}
try {
this.client.connack({ returnCode: 4 });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (connack): "${packet.clientId}" - error: ${err}`,
);
}
}
} else {
this.adapter.log.error(
`[MQTT] (Shelly?) device unknown, configuration for client with id "${packet.clientId}" ${this.getLogInfo()} does not exist! Maybe this device is not supported in this adapter version (${adapterVersion}).`,
);
this.adapter.log.error(
`[MQTT] DO NOT CHANGE THE CLIENT-ID OF YOUR SHELLY DEVICES (see adapter documentation for details)`,
);
try {
this.client.connack({ returnCode: 4 });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (connack): "${packet.clientId}" - error: ${err}`,
);
}
}
});
this.client.on('close', status => {
this.initializing = false;
this.adapter.log.info(`[MQTT] Client Close: ${this.getLogInfo()} (${status})`);
this.destroy();
});
this.client.on('error', error => {
this.adapter.log.info(`[MQTT] Client Error: ${this.getLogInfo()} (${error})`);
// this.destroy();
});
this.client.on('disconnect', () => {
this.initializing = false;
this.adapter.log.info(`[MQTT] Client Disconnect: ${this.getLogInfo()}`);
this.destroy();
});
this.client.on('timeout', () => {
this.adapter.log.info(`[MQTT] Client Timeout: ${this.getLogInfo()}`);
// this.destroy();
});
this.client.on('publish', async packet => {
if (this.adapter.isUnloaded) {
return;
}
// Send QoS acknowledgment immediately to avoid client timeouts
switch (packet.qos) {
case 1:
this.adapter.log.silly(`[MQTT] puback: ${this.getLogInfo()}`);
try {
this.client.puback({ messageId: packet.messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (puback): ${this.getLogInfo()} - qos: ${packet.qos}, error: ${err}`,
);
}
break;
case 2:
this.adapter.log.silly(`[MQTT] pubrec: ${this.getLogInfo()} - messageId: ${packet.messageId}`);
try {
this.client.pubrec({ messageId: packet.messageId });
this.resendState2Client('pubrec', packet.messageId);
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubrec): ${this.getLogInfo()} - qos: ${packet.qos}, error: ${err}`,
);
}
break;
default:
break;
}
// Don't queue packets if we're being destroyed
if (this.destroying) {
this.adapter.log.debug(
`[MQTT] Publish: ${this.getLogInfo()} - destruction in progress, ignoring packet`,
);
return;
}
// Always queue packets to ensure they are processed in order
this.publishQueue.push(packet);
// Start processing queue if not already processing
if (!this.processingQueue && !this.initializing) {
await this.processPublishQueue();
}
});
// this.client pinged
this.client.on('pingreq', () => {
try {
this.client.pingresp();
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pingresp): ${this.getLogInfo()} - error: ${err}`,
);
}
});
// response for QoS2
this.client.on('pubrec', packet => {
const qosmsg = this.getCachedMessage(packet.messageId);
if (qosmsg && qosmsg.cmd === 'publish') {
try {
this.client.pubrel({ messageId: packet.messageId });
this.resendState2Client('pubrel', packet.messageId);
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubrel): ${this.getLogInfo()} - error: ${err}`,
);
}
} else {
this.adapter.log.info(
`[MQTT] Client ${this.getLogInfo()} received pubrec for unknown messageId: ${packet.messageId}`,
);
}
});
// response for QoS2
this.client.on('pubcomp', packet => {
const qosmsg = this.getCachedMessage(packet.messageId);
if (qosmsg && qosmsg.cmd === 'pubrec') {
this.deleteResendState2Client(packet.messageId);
} else {
this.adapter.log.info(
`[MQTT] Client ${this.getLogInfo()} received pubcomp for unknown messageId: ${packet.messageId}`,
);
}
});
// response for QoS2
this.client.on('pubrel', packet => {
const qosmsg = this.getCachedMessage(packet.messageId);
if (qosmsg && qosmsg.cmd === 'pubrec') {
try {
this.deleteResendState2Client(packet.messageId);
this.client.pubcomp({ messageId: packet.messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (pubcomp): ${this.getLogInfo()} - error: ${err}`,
);
}
} else {
this.adapter.log.info(
`[MQTT] Client ${this.getLogInfo()} received pubrel for unknown messageId: ${packet.messageId}`,
);
}
});
// response for QoS1
this.client.on('puback', packet => {
// remove this message from queue
const qosmsg = this.getCachedMessage(packet.messageId);
if (qosmsg && qosmsg.cmd === 'publish') {
this.deleteResendState2Client(packet.messageId);
} else {
this.adapter.log.info(
`[MQTT] Client ${this.getLogInfo()} received puback for unknown messageId: ${packet.messageId}`,
);
}
});
this.client.on('unsubscribe', packet => {
this.adapter.log.debug(`[MQTT] Unsubscribe ${this.getLogInfo()}: ${JSON.stringify(packet)}`);
try {
this.client.unsuback({ messageId: packet.messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (unsuback): ${this.getLogInfo()} - error: ${err}`,
);
}
});
// this.client subscribed
this.client.on('subscribe', packet => {
// send a suback with messageId and granted QoS level
this.adapter.log.debug(`[MQTT] Subscribe ${this.getLogInfo()}: ${JSON.stringify(packet)}`);
const granted = [];
for (const i in packet.subscriptions) {
granted.push(packet.subscriptions[i].qos);
}
if (packet.topic) {
this.adapter.log.debug(`[MQTT] Subscribe topic ${this.getLogInfo()}: ${packet.topic}`);
}
try {
this.client.suback({ granted: granted, messageId: packet.messageId });
} catch (err) {
this.adapter.log.debug(
`[MQTT] Client communication error (suback): ${this.getLogInfo()} - error: ${err}`,
);
}
});
}
}
class MQTTServer extends BaseServer {
constructor(adapter, objectHelper, eventEmitter) {
//if (!(this instanceof MQTTServer)) return new MQTTServer(adapter, objectHelper, eventEmitter);
super(adapter, objectHelper, eventEmitter);
this.server = new net.Server();
this.clients = {};
}
listen() {
this.server.on('connection', stream => {
this.adapter.log.debug(`[MQTT Server] New connection from ${stream.remoteAddress}`);
const client = new MQTTClient(this.adapter, this.objectHelper, this.eventEmitter, stream);
this.clients[stream.remoteAddress] = client;
stream.on('timeout', () => {
this.adapter.log.debug(`[MQTT Server] Timeout for ${stream.remoteAddress} (${client.getLogInfo()})`);
client.destroy();
stream.destroy();
});
stream.on('unload', () => {
this.adapter.log.debug(`[MQTT Server] Unload for ${stream.remoteAddress} (${client.getLogInfo()})`);
client.destroy();
stream.destroy();
});
stream.on('error', () => {
this.adapter.log.debug(`[MQTT Server] Error for ${stream.remoteAddress} (${client.getLogInfo()})`);
if (client) {
client.destroy();
stream.destroy();
}
});
stream.on('close', () => {
this.adapter.log.debug(`[MQTT Server] Close for ${stream.remoteAddress} (${client.getLogInfo()})`);
delete this.clients[stream.remoteAddress];
});
stream.on('end', () => {
this.adapter.log.debug(`[MQTT Server] End for ${stream.remoteAddress} (${client.getLogInfo()})`);
stream.end();
});
});
this.server.on('close', () => this.adapter.log.debug(`[MQTT Server] Closing listener`));
this.server.on('error', error => this.adapter.log.debug(`[MQTT Server] Error in listener: ${error}`));
this.server.listen(this.adapter.config.port, this.adapter.config.bind, () =>
this.adapter.log.debug(
`[MQTT Server] Started listener on ${this.adapter.config.bind}:${this.adapter.config.port}`,
),
);
}
destroy() {
super.destroy();
this.adapter.log.debug(`[MQTT Server] Destroying`);
for (const i in this.clients) {
this.clients[i].destroy();
}
this.server.close();
}
}
module.exports = {
MQTTServer,
};