diff --git a/data/configuration.example.yaml b/data/configuration.example.yaml index 7506fd7b83..277938bf9a 100644 --- a/data/configuration.example.yaml +++ b/data/configuration.example.yaml @@ -12,6 +12,8 @@ frontend: # MQTT settings mqtt: + # Enable MQTT connection + enabled: true # MQTT base topic for zigbee2mqtt MQTT messages base_topic: zigbee2mqtt # MQTT server URL diff --git a/lib/controller.ts b/lib/controller.ts index 1bd694c771..09a15fc2a3 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -165,12 +165,21 @@ export class Controller { logger.info(`Currently ${deviceCount} devices are joined.`); // MQTT - try { - await this.mqtt.connect(); - } catch (error) { - logger.error(`MQTT failed to connect, exiting... (${(error as Error).message})`); - await this.zigbee.stop(); - return await this.exit(1); + if (settings.get().mqtt.enabled) { + try { + await this.mqtt.connect(); + } catch (error) { + logger.error(`MQTT failed to connect, exiting... (${(error as Error).message})`); + await this.zigbee.stop(); + return await this.exit(1); + } + } else { + if (!settings.get().frontend.enabled) { + logger.error("MQTT and Frontend are both disabled, process is unable to start, exiting..."); + await this.zigbee.stop(); + return await this.exit(1); + } + logger.info("MQTT is disabled, skipping connection"); } if (abortSignal.aborted) { @@ -360,7 +369,9 @@ export class Controller { // Wrap-up this.state.stop(); - await this.mqtt.disconnect(); + if (settings.get().mqtt.enabled) { + await this.mqtt.disconnect(); + } try { await this.zigbee.stop(); diff --git a/lib/mqtt.ts b/lib/mqtt.ts index 50729a8623..13ebf9308f 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -28,6 +28,9 @@ export default class Mqtt { public retainedMessages: {[s: string]: {topic: string; payload: string; options: MqttPublishOptions}} = {}; get info() { + if (!settings.get().mqtt.enabled) { + return {version: undefined, server: ""}; + } return { version: this.client.options.protocolVersion, server: `${this.client.options.protocol}://${this.client.options.host}:${this.client.options.port}`, @@ -35,6 +38,9 @@ export default class Mqtt { } get stats() { + if (!settings.get().mqtt.enabled) { + return {connected: false, queued: 0}; + } return { connected: this.isConnected(), queued: this.client.queue.length, @@ -145,7 +151,7 @@ export default class Mqtt { // Set timer at interval to check if connected to MQTT server. this.connectionTimer = setInterval(() => { - if (!this.isConnected()) { + if (settings.get().mqtt.enabled && !this.isConnected()) { logger.error("Not connected to MQTT server!"); } }, utils.seconds(10)); @@ -228,7 +234,7 @@ export default class Mqtt { this.eventBus.emitMQTTMessagePublished({topic, payload, options: finalOptions}); if (!this.isConnected()) { - if (!finalOptions.skipLog) { + if (!finalOptions.skipLog && settings.get().mqtt.enabled) { logger.error("Not connected to MQTT server!"); logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`); } diff --git a/lib/types/api.ts b/lib/types/api.ts index eac20e8bd1..e8e32e7754 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -120,6 +120,7 @@ export interface Zigbee2MQTTSettings { passive: {timeout: number}; }; mqtt: { + enabled: boolean; base_topic: string; include_device_information: boolean; force_disable_retain: boolean; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index d265836e29..6a7b3ca790 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -117,6 +117,13 @@ "type": "object", "title": "MQTT", "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enable MQTT connection. When false, the application runs without connecting to any MQTT broker.", + "default": true, + "requiresRestart": true + }, "base_topic": { "type": "string", "title": "Base topic", @@ -130,6 +137,7 @@ "title": "MQTT server", "requiresRestart": true, "description": "MQTT server URL (use mqtts:// for SSL/TLS connection)", + "default": "mqtt://localhost", "examples": ["mqtt://localhost:1883"] }, "keepalive": { @@ -226,7 +234,7 @@ "maximum": 268435456 } }, - "required": ["server"] + "required": [] }, "serial": { "type": "object", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 38dc2861ad..35d436fdd1 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -45,7 +45,9 @@ export const defaults = { base_url: "/", }, mqtt: { + enabled: true, base_topic: "zigbee2mqtt", + server: "mqtt://localhost", include_device_information: false, force_disable_retain: false, // 1MB = roughly 3.5KB per device * 300 devices for `/bridge/devices` @@ -158,6 +160,7 @@ export function writeMinimalDefaults(): void { const minimal = { version: CURRENT_VERSION, mqtt: { + enabled: true, base_topic: defaults.mqtt.base_topic, server: "mqtt://localhost:1883", }, diff --git a/test/controller.test.ts b/test/controller.test.ts index 70fcbae89d..4d66a40643 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -383,6 +383,27 @@ describe("Controller", () => { expect(settings.get().onboarding).toStrictEqual(true); }); + it("Start controller with MQTT disabled should be successful", async () => { + settings.set(["mqtt", "enabled"], false); + settings.set(["frontend", "enabled"], true); + await controller.start(); + await flushPromises(); + await controller.stop(); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(0, false); + }); + + it("Start controller fails due to MQTT and frontend being disabled at the same time", async () => { + settings.set(["mqtt", "enabled"], false); + settings.set(["frontend", "enabled"], false); + await controller.start(); + await flushPromises(); + expect(mockLogger.error).toHaveBeenCalledWith("MQTT and Frontend are both disabled, process is unable to start, exiting..."); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1, false); + }); + it("Start controller and stop with restart", async () => { await controller.start(); await controller.stop(true); diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 5a3a45f556..737c0d4119 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -311,6 +311,7 @@ describe("Extension: Bridge", () => { }, }, mqtt: { + enabled: true, base_topic: "zigbee2mqtt", force_disable_retain: false, include_device_information: false, diff --git a/test/extensions/health.test.ts b/test/extensions/health.test.ts index d27c6dc579..df51f96c04 100644 --- a/test/extensions/health.test.ts +++ b/test/extensions/health.test.ts @@ -312,4 +312,10 @@ describe("Extension: Health", () => { }); expect(calls[0][2]).toStrictEqual({retain: true, qos: 1}); }); + + it("reports mqtt stats as disconnected when MQTT is disabled", () => { + settings.set(["mqtt", "enabled"], false); + + expect(controller.mqtt.stats).toStrictEqual({connected: false, queued: 0}); + }); }); diff --git a/test/onboarding.test.ts b/test/onboarding.test.ts index 513fc4c0e4..206db7a820 100644 --- a/test/onboarding.test.ts +++ b/test/onboarding.test.ts @@ -80,6 +80,7 @@ vi.mock("zigbee2mqtt-windfront", () => ({ const SETTINGS_MINIMAL_DEFAULTS = { version: settings.CURRENT_VERSION, mqtt: { + enabled: settings.defaults.mqtt!.enabled, base_topic: settings.defaults.mqtt!.base_topic, server: "mqtt://localhost:1883", }, @@ -133,6 +134,7 @@ const SAMPLE_SETTINGS_INIT = { const SAMPLE_SETTINGS_SAVE = { version: settings.CURRENT_VERSION, mqtt: { + enabled: true, base_topic: "zigbee2mqtt2", server: "mqtt://192.168.1.200:1883", }, @@ -252,7 +254,11 @@ describe("Onboarding", () => { if (expectWriteMinimal) { const minimal = process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER ? Object.assign({}, SETTINGS_MINIMAL_DEFAULTS, { - mqtt: {server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic}, + mqtt: { + enabled: SETTINGS_MINIMAL_DEFAULTS.mqtt.enabled, + server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, + base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic, + }, }) : SETTINGS_MINIMAL_DEFAULTS; @@ -978,6 +984,7 @@ describe("Onboarding", () => { port: SETTINGS_MINIMAL_DEFAULTS.frontend.port, }, mqtt: { + enabled: SETTINGS_MINIMAL_DEFAULTS.mqtt.enabled, base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic, server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, user: "abcd", @@ -1200,7 +1207,11 @@ describe("Onboarding", () => { await expect(p).resolves.toStrictEqual(true); expect(data.read()).toStrictEqual( Object.assign({}, SAMPLE_SETTINGS_SAVE, { - mqtt: {server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, base_topic: SAMPLE_SETTINGS_SAVE.mqtt.base_topic}, + mqtt: { + enabled: SAMPLE_SETTINGS_SAVE.mqtt.enabled, + server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, + base_topic: SAMPLE_SETTINGS_SAVE.mqtt.base_topic, + }, }), ); }); @@ -1216,7 +1227,11 @@ describe("Onboarding", () => { await expect(p).resolves.toStrictEqual(true); const expected = Object.assign({}, SETTINGS_MINIMAL_DEFAULTS, { - mqtt: {server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic}, + mqtt: { + enabled: SETTINGS_MINIMAL_DEFAULTS.mqtt.enabled, + server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, + base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic, + }, }); // @ts-expect-error mock delete expected.onboarding; diff --git a/test/settings.test.ts b/test/settings.test.ts index 051a441c02..def1ec2064 100644 --- a/test/settings.test.ts +++ b/test/settings.test.ts @@ -72,6 +72,7 @@ describe("Settings", () => { enabled: true, }, mqtt: { + enabled: true, base_topic: "zigbee2mqtt", server: "mqtt://localhost", }, @@ -309,6 +310,7 @@ describe("Settings", () => { write(configurationFile, contentConfiguration); const expected = { + enabled: true, base_topic: "zigbee2mqtt", include_device_information: false, maximum_packet_size: 1048576, @@ -357,6 +359,7 @@ describe("Settings", () => { write(configurationFile, contentConfiguration); const expected = { + enabled: true, base_topic: "zigbee2mqtt", include_device_information: false, maximum_packet_size: 1048576, @@ -678,6 +681,7 @@ describe("Settings", () => { }, }; const expected = { + enabled: true, base_topic: "zigbee2mqtt", include_device_information: false, maximum_packet_size: 1048576, @@ -692,6 +696,13 @@ describe("Settings", () => { expect(settings.get().mqtt).toStrictEqual(expected); }); + it("Should disable MQTT when mqtt.enabled is false", () => { + write(configurationFile, { + mqtt: {enabled: false, server: "mqtt://localhost"}, + }); + expect(settings.get().mqtt.enabled).toStrictEqual(false); + }); + it("Should add groups with specific ID", () => { write(configurationFile, {});