Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion server/monitor-types/mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ class MqttMonitorType extends MonitorType {
}
}

/**
* This function checks if the actual MQTT topic matches the subscribed topic.
* To handle MQTT wildcards, it converts the subscribed topic into a regex pattern.
* @param {string} subscribedTopic MQTT subscribed topic
* @returns {RegExp} RegExp if the actual topic matches the subscribed topic
*/
static mqttTopicRegex(subscribedTopic) {
//eslint-disable-next-line
subscribedTopic = subscribedTopic.replace(/([$.|?*{}()\[\]\\])/g, "\\$1"); // Escape special regex chars except + and #
//eslint-disable-next-line
subscribedTopic = subscribedTopic.replace(/(\+)/g, ".+"); // Replace + with regex for one or more characters except slash
subscribedTopic = subscribedTopic.replace(/#/g, ".*"); // Replace # with regex for zero or more levels (including slashes)
return new RegExp(`^${subscribedTopic}$`);
}

/**
* Connect to MQTT Broker, subscribe to topic and receive message as String
* @param {string} hostname Hostname / address of machine to test
Expand Down Expand Up @@ -89,12 +104,15 @@ class MqttMonitorType extends MonitorType {
clientId: "uptime-kuma_" + Math.random().toString(16).substr(2, 8)
});

let regexTopic;

client.on("connect", () => {
log.debug("mqtt", "MQTT connected");

try {
client.subscribe(topic, () => {
log.debug("mqtt", "MQTT subscribed to topic");
regexTopic = MqttMonitorType.mqttTopicRegex(topic);
});
} catch (e) {
client.end();
Expand All @@ -110,7 +128,7 @@ class MqttMonitorType extends MonitorType {
});

client.on("message", (messageTopic, message) => {
if (messageTopic === topic) {
if (regexTopic.test(messageTopic)) {
client.end();
clearTimeout(timeoutID);
resolve(message.toString("utf8"));
Expand Down
54 changes: 54 additions & 0 deletions test/backend-test/test-mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,58 @@ describe("MqttMonitorType", {
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});

test("should match exact topic without wildcards", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor/temperature");
assert.ok(regex.test("sensor/temperature") === true);
assert.ok(regex.test("sensor/humidity") === false);
});

test("should match exact topic without wildcards but with special characters", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor.pomme/temperature");
assert.ok(regex.test("sensor.pomme/temperature") === true);
assert.ok(regex.test("sensor.pomme/humidity") === false);
});

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a testcase specifically for the escaping of #, + and the other regex characters.

test("should check specifically for the escaping of '?' regex char", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor.pomme/tempera?ture");
assert.ok(regex.test("sensor.pomme/tempera?ture") === true);
assert.ok(regex.test("sensor.pomme/humi?dity") === false);
});

test("should check specifically for the escaping of '*' regex char", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor.pomme/tempera*ture");
assert.ok(regex.test("sensor.pomme/tempera*ture") === true);
assert.ok(regex.test("sensor.pomme/humi*dity") === false);
});

test("should match + wildcard for single level", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor/+/temperature");
assert.ok(regex.test("sensor/room1/temperature") === true);
assert.ok(regex.test("sensor/room2/temperature") === true);
assert.ok(regex.test("sensor/room1/humidity") === false);
assert.ok(regex.test("sensor/temperature") === false);
});

test("should match # wildcard for multi-level", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor/#");
assert.ok(regex.test("sensor/room1") === true);
assert.ok(regex.test("sensor/room1/temperature") === true);
assert.ok(regex.test("sensor/") === true);
assert.ok(regex.test("actuator/room1") === false);
});

test("should combine + and # wildcards", () => {
const regex = MqttMonitorType.mqttTopicRegex("sensor/+/status/#");
assert.ok(regex.test("sensor/room1/status/online") === true);
assert.ok(regex.test("sensor/room2/status/offline/extra") === true);
assert.ok(regex.test("sensor/status") === false);
assert.ok(regex.test("sensor/room1") === false);
});

test("should escape special regex characters in topic", () => {
const regex = MqttMonitorType.mqttTopicRegex("some.topic/+/value$");
assert.ok(regex.test("some.topic/abc/value$") === true);
assert.ok(regex.test("some.topic/abc/value") === false);
});
});
Loading