homebridge-plugin-utils
Version:
Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.
157 lines • 6.58 kB
JavaScript
import mqtt from "mqtt";
import util from "node:util";
const MQTT_DEFAULT_RECONNECT_INTERVAL = 60;
export class MqttClient {
brokerUrl;
isConnected;
reconnectInterval;
log;
mqtt;
subscriptions;
topicPrefix;
constructor(brokerUrl, topicPrefix, log, reconnectInterval = MQTT_DEFAULT_RECONNECT_INTERVAL) {
this.brokerUrl = brokerUrl;
this.isConnected = false;
this.log = log;
this.mqtt = null;
this.reconnectInterval = reconnectInterval;
this.subscriptions = {};
this.topicPrefix = topicPrefix;
this.configure();
}
// Connect to the MQTT broker.
configure() {
// Try to connect to the MQTT broker and make sure we catch any URL errors.
try {
this.mqtt = mqtt.connect(this.brokerUrl, { reconnectPeriod: this.reconnectInterval * 1000, rejectUnauthorized: false });
}
catch (error) {
if (error instanceof Error) {
switch (error.message) {
case "Missing protocol":
this.log.error("MQTT Broker: Invalid URL provided: %s.", this.brokerUrl);
break;
default:
this.log.error("MQTT Broker: Error: %s.", error.message);
break;
}
}
}
// We've been unable to even attempt to connect. It's likely we have a configuration issue - we're done here.
if (!this.mqtt) {
return;
}
// Notify the user when we connect to the broker.
this.mqtt.on("connect", () => {
this.isConnected = true;
// Inform users, while redacting authentication credentials.
this.log.info("MQTT Broker: Connected to %s (topic: %s).", this.brokerUrl.replace(/^(.*:\/\/.*:)(.*)(@.*)$/, "$1REDACTED$3"), this.topicPrefix);
});
// Notify the user when we've disconnected.
this.mqtt.on("close", () => {
// We only inform users if we're already connected. Otherwise, we're likely in an error state and that's logged elsewhere.
if (!this.isConnected) {
return;
}
this.isConnected = false;
// Inform users.
this.log.info("MQTT Broker: Connection closed.");
});
// Process inbound messages and pass it to the right message handler.
this.mqtt.on("message", (topic, message) => {
this.subscriptions[topic]?.(message);
});
// Notify the user when there's a connectivity error.
this.mqtt.on("error", (error) => {
const logError = (message) => this.log.error("MQTT Broker: %s. Will retry again in %s minute%s.", message, this.reconnectInterval / 60, this.reconnectInterval / 60 > 1 ? "s" : "");
switch (error.code) {
case "ECONNREFUSED":
logError("Connection refused");
break;
case "ECONNRESET":
logError("Connection reset");
break;
case "ENOTFOUND":
this.mqtt?.end(true);
this.log.error("MQTT Broker: Hostname or IP address not found.");
break;
default:
logError(util.inspect(error, { sorted: true }));
break;
}
});
}
// Publish an MQTT event to a broker.
publish(id, topic, message) {
const expandedTopic = this.expandTopic(id, topic);
// No valid topic returned, we're done.
if (!expandedTopic) {
return;
}
this.log.debug("MQTT publish: %s Message: %s.", expandedTopic, message);
// By default, we publish as: pluginTopicPrefix/id/topic
this.mqtt?.publish(expandedTopic, message);
}
// Subscribe to an MQTT topic.
subscribe(id, topic, callback) {
const expandedTopic = this.expandTopic(id, topic);
// No valid topic returned, we're done.
if (!expandedTopic) {
return;
}
this.log.debug("MQTT subscribe: %s.", expandedTopic);
// Add to our callback list.
this.subscriptions[expandedTopic] = callback;
// Tell MQTT we're subscribing to this event.
// By default, we subscribe as: pluginTopicPrefix/id/topic
this.mqtt?.subscribe(expandedTopic);
}
// Subscribe to a specific MQTT topic and publish a value on a get request.
subscribeGet(id, topic, type, getValue, log = this.log) {
// Return the current status of a given sensor.
this.subscribe(id, topic + "/get", (message) => {
const value = message.toString().toLowerCase();
// When we get the right message, we return the system information JSON.
if (value !== "true") {
return;
}
this.publish(id, topic, getValue());
log.info("MQTT: %s status published.", type);
});
}
// Subscribe to a specific MQTT topic and set a value on a set request.
subscribeSet(id, topic, type, setValue, log = this.log) {
// Return the current status of a given sensor.
this.subscribe(id, topic + "/set", (message) => {
const value = message.toString().toLowerCase();
const logResult = () => log.info("MQTT: set message received for %s: %s.", type, value);
// Set our value and inform the user.
const result = setValue(value, message.toString());
// For callbacks that are promises, we wait until they complete before logging the result.
if (result && typeof result.then === "function") {
result.then(logResult).catch(error => log.error("MQTT: error seting message received for %s: %s. %s", type, value, error));
return;
}
// Log the outcome.
logResult();
});
}
// Unsubscribe to an MQTT topic.
unsubscribe(id, topic) {
const expandedTopic = this.expandTopic(id, topic);
// No valid topic returned, we're done.
if (!expandedTopic) {
return;
}
delete this.subscriptions[expandedTopic];
}
// Expand a topic to a unique, fully formed one.
expandTopic(id, topic) {
// No id, we're done.
if (!id) {
return null;
}
return this.topicPrefix + "/" + id + "/" + topic;
}
}
//# sourceMappingURL=mqttclient.js.map