diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 36f20e3e1..8a2d4b7bd 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -677,6 +677,7 @@ export default class Bridge extends Extension { const friendlyName = entity.name; let block = false; let force = false; + let keepConfig = false; let clearCache = false; let blockForceLog = ""; @@ -684,8 +685,9 @@ export default class Bridge extends Extension { const payload = message as Zigbee2MQTTAPI["bridge/request/device/remove"]; block = !!payload.block; force = !!payload.force; + keepConfig = !!payload.keep_config; clearCache = !!payload.clear_cache; - blockForceLog = ` (block: ${block}, force: ${force}, clear cache: ${clearCache})`; + blockForceLog = ` (block: ${block}, force: ${force}, keep config: ${keepConfig}, clear cache: ${clearCache})`; } else if (entityType === "group" && messageIsObject) { const payload = message as Zigbee2MQTTAPI["bridge/request/group/remove"]; force = !!payload.force; @@ -710,7 +712,9 @@ export default class Bridge extends Extension { this.zigbee.removeDeviceFromLookup(entity.ID); } - settings.removeDevice(entity.ID as string); + if (!keepConfig) { + settings.removeDevice(entity.ID as string); + } } else { if (force) { entity.zh.removeFromDatabase(); @@ -739,7 +743,13 @@ export default class Bridge extends Extension { // Refresh Cluster definition await this.publishDefinitions(); - const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = {id: ID, block, force, clear_cache: clearCache}; + const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = { + id: ID, + block, + force, + keep_config: keepConfig, + clear_cache: clearCache, + }; return utils.getResponse(message, responseData); } diff --git a/lib/types/api.ts b/lib/types/api.ts index dbc9308bd..261e032e7 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -622,6 +622,7 @@ export interface Zigbee2MQTTAPI { id: string; block?: boolean; force?: boolean; + keep_config?: boolean; clear_cache?: boolean; }; @@ -629,6 +630,7 @@ export interface Zigbee2MQTTAPI { id: string; block: boolean; force: boolean; + keep_config: boolean; clear_cache: boolean; }; diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 948acd6f0..4e82df515 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -2988,7 +2988,7 @@ describe("Extension: Bridge", () => { expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/remove", - stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}), + stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: false}, status: "ok"}), {}, ); expect(settings.get().blocklist).toStrictEqual([]); @@ -3009,7 +3009,7 @@ describe("Extension: Bridge", () => { expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/remove", - stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}), + stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: false}, status: "ok"}), {}, ); }); @@ -3027,7 +3027,7 @@ describe("Extension: Bridge", () => { expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/remove", - stringify({data: {id: "bulb", block: false, force: true, clear_cache: false}, status: "ok"}), + stringify({data: {id: "bulb", block: false, force: true, keep_config: false, clear_cache: false}, status: "ok"}), {}, ); }); @@ -3044,12 +3044,30 @@ describe("Extension: Bridge", () => { expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/remove", - stringify({data: {id: "bulb", block: true, force: true, clear_cache: false}, status: "ok"}), + stringify({data: {id: "bulb", block: true, force: true, keep_config: false, clear_cache: false}, status: "ok"}), {}, ); expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]); }); + it("Should allow to keep configuration when removing device", async () => { + const device = devices.bulb; + const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup"); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", keep_config: true})); + await flushPromises(); + expect(device.removeFromDatabase).not.toHaveBeenCalled(); + expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(settings.getDevice("bulb")).toBeDefined(); + expect(removeSpy).not.toHaveBeenCalled(); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/remove", + stringify({data: {id: "bulb", block: false, force: false, keep_config: true, clear_cache: false}, status: "ok"}), + {}, + ); + }); + it("Should allow to clear cache when removing device", async () => { const device = devices.bulb; const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup"); @@ -3063,7 +3081,7 @@ describe("Extension: Bridge", () => { expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object)); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/remove", - stringify({data: {id: "bulb", block: false, force: false, clear_cache: true}, status: "ok"}), + stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: true}, status: "ok"}), {}, ); }); @@ -3135,7 +3153,7 @@ describe("Extension: Bridge", () => { stringify({ data: {}, status: "error", - error: "Failed to remove device 'bulb' (block: false, force: false, clear cache: false) (Error: device timeout)", + error: "Failed to remove device 'bulb' (block: false, force: false, keep config: false, clear cache: false) (Error: device timeout)", }), {}, );