#3281 Add tests for new bridge API

This commit is contained in:
Koen Kanters 2020-05-24 18:16:39 +02:00
parent 1638b81723
commit 409fb2407a
6 changed files with 283 additions and 75 deletions

View File

@ -19,7 +19,7 @@ const ExtensionHomeAssistant = require('./extension/homeassistant');
const ExtensionConfigure = require('./extension/configure');
const ExtensionDeviceGroupMembership = require('./extension/legacy/deviceGroupMembership');
const ExtensionBridgeLegacy = require('./extension/legacy/bridgeLegacy');
// const ExtensionBridge = require('./extension/bridge');
const ExtensionBridge = require('./extension/bridge');
const ExtensionGroups = require('./extension/groups');
const ExtensionAvailability = require('./extension/availability');
const ExtensionBind = require('./extension/bind');
@ -49,10 +49,12 @@ class Controller {
new ExtensionBind(...args),
new ExtensionOnEvent(...args),
new ExtensionOTAUpdate(...args),
// new ExtensionBridge(...args),
];
/* istanbul ignore else */
if (settings.get().experimental.new_api) {
this.extensions.push(new ExtensionBridge(...args));
}
if (settings.get().advanced.legacy_api) {
this.extensions.push(new ExtensionBridgeLegacy(...args));
}

View File

@ -1,5 +1,3 @@
/* istanbul ignore file newApi */
const logger = require('../util/logger');
const utils = require('../util/utils');
const Extension = require('./extension');
@ -8,22 +6,25 @@ const settings = require('../util/settings');
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
class BridgeLegacy extends Extension {
class Bridge extends Extension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.requestLookup = {
'permitjoin': this.requestPermitJoin.bind(this),
'device/remove': this.deviceRemove.bind(this),
'device/forceremove': this.deviceForceRemove.bind(this),
'device/ban': this.deviceBan.bind(this),
'group/remove': this.groupRemove.bind(this),
'permitjoin': this.permitJoin.bind(this),
// 'device/remove': this.deviceRemove.bind(this),
// 'device/forceremove': this.deviceForceRemove.bind(this),
// 'device/ban': this.deviceBan.bind(this),
// 'group/remove': this.groupRemove.bind(this),
};
}
async onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/+`);
this.zigbee2mqttVersion = await utils.getZigbee2mqttVersion();
this.coordinatorVersion = await this.zigbee.getCoordinatorVersion();
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/#`);
await this.publishInfo();
await this.publishDevices();
await this.publishGroups();
@ -74,23 +75,23 @@ class BridgeLegacy extends Extension {
* Requests
*/
async deviceRemove(message) {
return this.removeForceRemoveOrBanEntity('remove', 'device', message);
}
// async deviceRemove(message) {
// return this.removeForceRemoveOrBanEntity('remove', 'device', message);
// }
async deviceForceRemove(message) {
return this.removeForceRemoveOrBanEntity('force_remove', 'device', message);
}
// async deviceForceRemove(message) {
// return this.removeForceRemoveOrBanEntity('force_remove', 'device', message);
// }
async deviceBan(message) {
return this.removeForceRemoveOrBanEntity('ban', 'device', message);
}
// async deviceBan(message) {
// return this.removeForceRemoveOrBanEntity('ban', 'device', message);
// }
async groupRemove(message) {
return this.removeForceRemoveOrBanEntity('remove', 'group', message);
}
// async groupRemove(message) {
// return this.removeForceRemoveOrBanEntity('remove', 'group', message);
// }
async requestPermitJoin(message) {
async permitJoin(message) {
const value = typeof message === 'object' ? message.value : message;
await this.zigbee.permitJoin(value);
await this.publishInfo();
@ -101,59 +102,57 @@ class BridgeLegacy extends Extension {
* Utils
*/
async removeForceRemoveOrBanEntity(action, entityType, message) {
const ID = typeof message === 'object' ? message.ID : message.trim();
const entity = this.zigbee.resolveEntity(ID);
if (!entity || entity.type !== entityType) {
throw new Error(`${ID} is not a ${entityType}`);
}
// async removeForceRemoveOrBanEntity(action, entityType, message) {
// const ID = typeof message === 'object' ? message.ID : message.trim();
// const entity = this.zigbee.resolveEntity(ID);
// if (!entity || entity.type !== entityType) {
// throw new Error(`${ID} is not a ${entityType}`);
// }
const lookup = {
ban: ['banned', 'Banning', 'ban'],
force_remove: ['force_removed', 'Force removing', 'force remove'],
remove: ['removed', 'Removing', 'remove'],
};
// const lookup = {
// ban: ['banned', 'Banning', 'ban'],
// force_remove: ['force_removed', 'Force removing', 'force remove'],
// remove: ['removed', 'Removing', 'remove'],
// };
try {
logger.info(`${lookup[action][1]} '${entity.settings.friendlyName}'`);
if (entity.type === 'device') {
if (action === 'ban') {
settings.banDevice(entity.settings.ID);
}
// try {
// logger.info(`${lookup[action][1]} '${entity.settings.friendlyName}'`);
// if (entity.type === 'device') {
// if (action === 'ban') {
// settings.banDevice(entity.settings.ID);
// }
action === 'force_remove' ?
await entity.device.removeFromDatabase() : await entity.device.removeFromNetwork();
} else {
await entity.group.removeFromDatabase();
}
// action === 'force_remove' ?
// await entity.device.removeFromDatabase() : await entity.device.removeFromNetwork();
// } else {
// await entity.group.removeFromDatabase();
// }
// Fire event
if (entity.type === 'device') {
this.eventBus.emit('deviceRemoved', {device: entity.device});
}
// // Fire event
// if (entity.type === 'device') {
// this.eventBus.emit('deviceRemoved', {device: entity.device});
// }
// Remove from configuration.yaml
entity.type === 'device' ?
settings.removeDevice(entity.settings.ID) : settings.removeGroup(entity.settings.ID);
// // Remove from configuration.yaml
// entity.type === 'device' ?
// settings.removeDevice(entity.settings.ID) : settings.removeGroup(entity.settings.ID);
// Remove from state
this.state.remove(entity.settings.ID);
// // Remove from state
// this.state.remove(entity.settings.ID);
logger.info(`Successfully ${lookup[action][0]} ${entity.settings.friendlyName}`);
entity.type === 'device' ? this.publishDevices() : this.publishGroups();
return utils.getResponse(message, {ID}, null);
} catch (error) {
throw new Error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
}
}
// logger.info(`Successfully ${lookup[action][0]} ${entity.settings.friendlyName}`);
// entity.type === 'device' ? this.publishDevices() : this.publishGroups();
// return utils.getResponse(message, {ID}, null);
// } catch (error) {
// throw new Error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
// }
// }
async publishInfo() {
const info = await utils.getZigbee2mqttVersion();
const coordinator = await this.zigbee.getCoordinatorVersion();
const payload = {
version: info.version,
commit: info.commitHash,
coordinator,
version: this.zigbee2mqttVersion.version,
commit: this.zigbee2mqttVersion.commitHash,
coordinator: this.coordinatorVersion,
logLevel: logger.getLevel(),
permitJoin: await this.zigbee.getPermitJoin(),
};
@ -177,7 +176,7 @@ class BridgeLegacy extends Extension {
type: device.type,
networkAddress: device.networkAddress,
supported: !!definition,
friendlyName: resolved.settings.friendlyName,
friendlyName: resolved.name,
definition: definitionPayload,
powerSource: device.powerSource,
softwareBuildID: device.softwareBuildID,
@ -195,7 +194,7 @@ class BridgeLegacy extends Extension {
const resolved = this.zigbee.resolveEntity(group);
return {
ID: group.groupID,
friendlyName: resolved.settings.friendlyName,
friendlyName: resolved.name,
members: group.members.map((m) => {
return {
ieeeAddress: m.deviceIeeeAddress,
@ -209,4 +208,4 @@ class BridgeLegacy extends Extension {
}
}
module.exports = BridgeLegacy;
module.exports = Bridge;

View File

@ -41,6 +41,7 @@ const defaults = {
experimental: {
// json or attribute or attribute_and_json
output: 'json',
new_api: false,
},
advanced: {
legacy_api: true,

View File

@ -124,7 +124,6 @@ function getObjectsProperty(objects, key, defaultValue) {
return defaultValue;
}
/* istanbul ignore next newApi */
function getResponse(request, data, error) {
const response = {data, status: error ? 'error' : 'ok'};
if (error) response.error = error;

View File

@ -149,7 +149,6 @@ class Zigbee extends events.EventEmitter {
* }
*/
resolveEntity(key) {
/* istanbul ignore next newApi */
assert(
typeof key === 'string' || typeof key === 'number' ||
key.constructor.name === 'Device' || key.constructor.name === 'Group',
@ -217,7 +216,7 @@ class Zigbee extends events.EventEmitter {
if (!group) group = this.createGroup(entity.ID);
return {type: 'group', group, settings: entity, name: entity.friendlyName};
}
} /* istanbul ignore else newApi */ else if (key.constructor.name === 'Device') {
} else if (key.constructor.name === 'Device') {
const setting = settings.getEntity(key.ieeeAddr);
return {
type: 'device',
@ -233,7 +232,7 @@ class Zigbee extends events.EventEmitter {
type: 'group',
group: key,
settings: setting,
name: setting.friendlyName,
name: setting ? setting.friendlyName : key.groupID,
};
}
}

208
test/bridge.test.js Normal file
View File

@ -0,0 +1,208 @@
const data = require('./stub/data');
const logger = require('./stub/logger');
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const MQTT = require('./stub/mqtt');
const settings = require('../lib/util/settings');
const Controller = require('../lib/controller');
const flushPromises = () => new Promise(setImmediate);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
const {coordinator, bulb, unsupported} = zigbeeHerdsman.devices;
zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr);
zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr);
zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr);
describe('Bridge', () => {
let controller;
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.set(['advanced', 'legacy_api'], false);
settings.set(['experimental', 'new_api'], true);
data.writeDefaultState();
logger.info.mockClear();
logger.warn.mockClear();
MQTT.publish.mockClear();
controller = new Controller();
await controller.start();
await flushPromises();
});
it('Should publish bridge info on startup', async () => {
const version = await require('../lib/util/utils').getZigbee2mqttVersion();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info',
JSON.stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1,"revision":20190425}},"logLevel":"info","permitJoin":false}),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish devices on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
JSON.stringify([{"ieeeAddress":"0x000b57fffec6a5b2","type":"Router","networkAddress":40369,"supported":true,"friendlyName":"bulb","definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"},"powerSource":"Mains (single phase)","dateCode":null,"interviewing":false,"interviewCompleted":true},{"ieeeAddress":"0x0017880104e45518","type":"EndDevice","networkAddress":6536,"supported":false,"friendlyName":"0x0017880104e45518","definition":null,"powerSource":"Battery","dateCode":null,"interviewing":false,"interviewCompleted":true}]),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish devices on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/groups',
JSON.stringify([{"ID":1,"friendlyName":"group_1","members":[]},{"ID":15071,"friendlyName":"group_tradfri_remote","members":[]},{"ID":99,"friendlyName":99,"members":[]},{"ID":11,"friendlyName":"group_with_tradfri","members":[]},{"ID":2,"friendlyName":"group_2","members":[]}]),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event when device joined', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceJoined","data":{"friendlyName":"bulb","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
it('Should publish event when device interview started', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'started'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"started","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device interview failed', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'failed'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"failed","ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device interview successful', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'successful'});
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.unsupported, status: 'successful'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"bulb","status":"successful","ieeeAddress":"0x000b57fffec6a5b2","supported":true,"definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"}}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceInterview","data":{"friendlyName":"0x0017880104e45518","status":"successful","ieeeAddress":"0x0017880104e45518","supported":false,"definition":null}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device leaves', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
JSON.stringify({"type":"deviceLeave","data":{"ieeeAddress":"0x000b57fffec6a5b2"}}),
{ retain: false, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should allow permit join', async () => {
zigbeeHerdsman.permitJoin.mockClear();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', 'true');
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":true},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
zigbeeHerdsman.permitJoin.mockClear();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false}));
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":false},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Should put transaction in response when request is done with transaction', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false, "transaction": 22}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{"value":false},"status":"ok", "transaction": 22}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Should put erorr in response when request fails', async () => {
zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {throw new Error('Failed to connect to adapter')});
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/permitJoin', JSON.stringify({"value": false}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/permitJoin',
JSON.stringify({"data":{},"status":"error","error": "Failed to connect to adapter"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Coverage satisfaction', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/random', JSON.stringify({"value": false}));
const device = zigbeeHerdsman.devices.bulb;
await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
});
});