mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-10-23 22:32:40 +00:00
feat: Add api for device re-interview (#22788)
* feat: add api for device re-interview - Adds an API that allows for the re-interview of a device. This can be useful a device firmware upgrade adds new device endpoints (as is the case when upgrading an Inovelli VZM31-SN to 2.18). Without the ability to re-interview, one must remove and re-add the device. * rename from reinterview to interview * publish devices after interview. * only allow device ids or names, not endpoints
This commit is contained in:
1 parent
9f5b5d1a76
commit
b6ad6418a1
3 files changed
+119
No files matched your search
@@ -40,6 +40,7 @@ export default class Bridge extends Extension {
|
||||
'device/options': this.deviceOptions,
|
||||
'device/configure_reporting': this.deviceConfigureReporting,
|
||||
'device/remove': this.deviceRemove,
|
||||
'device/interview': this.deviceInterview,
|
||||
'device/generate_external_definition': this.deviceGenerateExternalDefinition,
|
||||
'device/rename': this.deviceRename,
|
||||
'group/add': this.groupAdd,
|
||||
@@ -500,6 +501,25 @@ export default class Bridge extends Extension {
|
||||
}, null);
|
||||
}
|
||||
|
||||
@bind async deviceInterview(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('id')) {
|
||||
throw new Error(`Invalid payload`);
|
||||
}
|
||||
|
||||
const device = this.getEntity('device', message.id) as Device;
|
||||
|
||||
try {
|
||||
await device.zh.interview();
|
||||
} catch (error) {
|
||||
throw new Error(`interview of '${device.name}' (${device.ieeeAddr}) failed: ${error}`, {cause: error});
|
||||
}
|
||||
|
||||
// publish devices so that the front-end has up-to-date info.
|
||||
await this.publishDevices();
|
||||
|
||||
return utils.getResponse(message, {id: message.id}, null);
|
||||
}
|
||||
|
||||
@bind async deviceGenerateExternalDefinition(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('id')) {
|
||||
throw new Error(`Invalid payload`);
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('Bridge', () => {
|
||||
logger.setTransportsEnabled(false);
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.interview.mockClear();
|
||||
device.removeFromDatabase.mockClear();
|
||||
device.removeFromNetwork.mockClear();
|
||||
extension.lastJoinedDeviceIeeeAddr = null;
|
||||
@@ -727,6 +728,103 @@ describe('Bridge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow interviewing a device by friendly name', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
zigbeeHerdsman.devices.bulb.interview.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'}));
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{"id":"bulb"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
|
||||
// The following indicates that devices have published.
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/devices',
|
||||
expect.any(String),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow interviewing a device by ieeeAddr', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
zigbeeHerdsman.devices.bulb.interview.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: "0x000b57fffec6a5b2"}));
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{"id":"0x000b57fffec6a5b2"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
|
||||
// The following indicates that devices have published.
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/devices',
|
||||
expect.any(String),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should throw error on invalid device interview payload', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{},"status":"error","error":"Invalid payload"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should throw error on non-existing device interview', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should throw error on id is device endpoint', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{},"status":"error","error":"Device 'bulb/1' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should throw error on id is a group', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{},"status":"error","error":"Device 'group_1' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should throw error on when interview fails', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
zigbeeHerdsman.devices.bulb.interview.mockClear();
|
||||
zigbeeHerdsman.devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong')))
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/interview',
|
||||
stringify({"data":{},"status":"error","error":"interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should error when generate_external_definition is invalid', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: ZNCZ02LM.ieeeAddr}));
|
||||
|
||||
@@ -127,6 +127,7 @@ class Device {
|
||||
this.softwareBuildID = softwareBuildID;
|
||||
this.interviewCompleted = interviewCompleted;
|
||||
this.modelID = modelID;
|
||||
this.interview = jest.fn();
|
||||
this.interviewing = interviewing;
|
||||
this.meta = {};
|
||||
this.ping = jest.fn();
|
||||
|
||||
Reference in new issue
Block a user