From 7b3c07dcfe804f1d04196f60ed48445f240d50ef Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sat, 13 Jun 2020 19:35:09 +0200 Subject: [PATCH] Implement renaming functionallity. https://github.com/Koenkk/zigbee2mqtt/issues/3281 --- lib/extension/bridge.js | 46 +++++++++++++++++++++++++ test/bridge.test.js | 76 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/lib/extension/bridge.js b/lib/extension/bridge.js index 2f5dc5789..8662901f1 100644 --- a/lib/extension/bridge.js +++ b/lib/extension/bridge.js @@ -10,11 +10,14 @@ class Bridge extends Extension { constructor(zigbee, mqtt, state, publishEntityState, eventBus) { super(zigbee, mqtt, state, publishEntityState, eventBus); + this.lastJoinedDeviceIeeeAddr = null; this.requestLookup = { 'permitjoin': this.permitJoin.bind(this), 'device/remove': this.deviceRemove.bind(this), 'group/remove': this.groupRemove.bind(this), + 'device/rename': this.deviceRename.bind(this), + 'group/rename': this.groupRename.bind(this), }; } @@ -44,6 +47,10 @@ class Bridge extends Extension { } async onZigbeeEvent(type, data, resolvedEntity) { + if (type === 'deviceJoined' && resolvedEntity) { + this.lastJoinedDeviceIeeeAddr = resolvedEntity.device.ieeeAddr; + } + if (['deviceJoined', 'deviceLeave', 'deviceInterview'].includes(type)) { let payload; const ieeeAddress = data.device ? data.device.ieeeAddr : data.ieeeAddr; @@ -81,6 +88,14 @@ class Bridge extends Extension { return this.removeEntity('group', message); } + async deviceRename(message) { + return this.renameEntity('device', message); + } + + async groupRename(message) { + return this.renameEntity('group', message); + } + async permitJoin(message) { const value = typeof message === 'object' ? message.value : message; await this.zigbee.permitJoin(value); @@ -92,6 +107,37 @@ class Bridge extends Extension { * Utils */ + renameEntity(entityType, message) { + const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true; + if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) || !message.hasOwnProperty('to')) { + throw new Error(`Invalid payload`); + } + + if (deviceAndHasLast && !this.lastJoinedDeviceIeeeAddr) { + throw new Error('No device has joined since start') + } + + const from = deviceAndHasLast ? this.lastJoinedDeviceIeeeAddr : message.from; + const to = message.to; + const entity = this.zigbee.resolveEntity(from); + if (!entity || entity.type !== entityType) { + throw new Error(`${utils.capitalize(entityType)} '${from}' does not exist`); + } + + + settings.changeFriendlyName(from, to); + + if (entity.type === 'device') { + this.publishDevices(); + this.eventBus.emit(`deviceRenamed`, {device: entity.device}); + } else { + this.publishGroups(); + this.eventBus.emit(`groupRenamed`, {group: entity.group}); + } + + return utils.getResponse(message, {from: entity.settings.friendlyName, to}, null); + } + async removeEntity(entityType, message) { const ID = typeof message === 'object' ? message.ID : message.trim(); const entity = this.zigbee.resolveEntity(ID); diff --git a/test/bridge.test.js b/test/bridge.test.js index 65a23a699..74f61ef21 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -315,4 +315,80 @@ describe('Bridge', () => { {retain: false, qos: 0}, expect.any(Function) ); }); + + it('Should allow rename device', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', JSON.stringify({from: 'bulb', to: 'bulb_new_name'})); + await flushPromises(); + expect(settings.getDevice('bulb')).toBeNull(); + expect(settings.getDevice('bulb_new_name')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendlyName": "bulb_new_name", "friendly_name": "bulb_new_name", "retain": true}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/rename', + JSON.stringify({"data":{"from":"bulb","to":"bulb_new_name"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should allow rename group', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', JSON.stringify({from: 'group_1', to: 'group_new_name'})); + await flushPromises(); + expect(settings.getGroup('group_1')).toBeNull(); + expect(settings.getGroup('group_new_name')).toStrictEqual({"ID": 1, "devices": [], "friendlyName": "group_new_name", "friendly_name": "group_new_name", "optimistic": true, "retain": false}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/rename', + JSON.stringify({"data":{"from":"group_1","to":"group_new_name"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on invalid device rename payload', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', JSON.stringify({from_bla: 'bulb', to: 'bulb_new_name'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/rename', + JSON.stringify({"data":{},"status":"error","error":"Invalid payload"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on non-existing device rename', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', JSON.stringify({from: 'bulb_not_existing', to: 'bulb_new_name'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/rename', + JSON.stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing' does not exist"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should allow to rename last joined device', async () => { + MQTT.publish.mockClear(); + await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', JSON.stringify({last: true, to: 'bulb_new_name'})); + await flushPromises(); + expect(settings.getDevice('bulb')).toBeNull(); + expect(settings.getDevice('bulb_new_name')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendlyName": "bulb_new_name", "friendly_name": "bulb_new_name", "retain": true}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/rename', + JSON.stringify({"data":{"from":"bulb","to":"bulb_new_name"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error when renaming last joined device but none has joined', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', JSON.stringify({last: true, to: 'bulb_new_name'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/rename', + JSON.stringify({"data":{},"status":"error","error":"No device has joined since start"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); });