Skip to content

Commit

Permalink
Prefer fingerprint over zigbeeModel identification. Koenkk/zigbee2mqt…
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Jul 12, 2020
1 parent 4810ed5 commit 3f4e2ca
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 40 deletions.
104 changes: 65 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,95 @@ const devices = require('./devices');
const toZigbee = require('./converters/toZigbee');
const fromZigbee = require('./converters/fromZigbee');

const byZigbeeModel = new Map();
const withFingerprint = [];
// key: zigbeeModel, value: array of definitions (most of the times 1)
const lookup = new Map();
const definitions = [];

function arrayEquals(as, bs) {
if (as.length !== bs.length) return false;
for (const a of as) if (!bs.includes(a)) return false;
return true;
}

function addToLookupMaps(device) {
if (device.hasOwnProperty('fingerprint')) {
withFingerprint.push(device);
function addToLookup(zigbeeModel, definition) {
zigbeeModel = zigbeeModel ? zigbeeModel.toLowerCase() : null;
if (!lookup.has(zigbeeModel)) {
lookup.set(zigbeeModel, []);
}

if (device.hasOwnProperty('zigbeeModel')) {
for (const zigbeeModel of device.zigbeeModel) {
byZigbeeModel.set(zigbeeModel.toLowerCase(), device);
}
if (!lookup.get(zigbeeModel).includes(definition)) {
lookup.get(zigbeeModel).push(definition);
}
}
for (const device of devices) {
addToLookupMaps(device);

function getFromLookup(zigbeeModel) {
zigbeeModel = zigbeeModel ? zigbeeModel.toLowerCase() : null;
if (lookup.has(zigbeeModel)) {
return lookup.get(zigbeeModel);
}

zigbeeModel = zigbeeModel ? zigbeeModel.replace(/\0.*$/g, '').trim() : null;
return lookup.get(zigbeeModel);
}

function findByZigbeeModel(model) {
if (!model) {
return null;
function addDefinition(definition) {
definitions.push(definition);

if (definition.hasOwnProperty('fingerprint')) {
for (const fingerprint of definition.fingerprint) {
addToLookup(fingerprint.modelID, definition);
}
}

model = model.toLowerCase();
if (definition.hasOwnProperty('zigbeeModel')) {
for (const zigbeeModel of definition.zigbeeModel) {
addToLookup(zigbeeModel, definition);
}
}
}

let definition = byZigbeeModel.get(model);
for (const definition of devices) {
addDefinition(definition);
}

if (!definition) {
definition = byZigbeeModel.get(model.replace(/\0.*$/g, '').trim());
function findByZigbeeModel(zigbeeModel) {
if (!zigbeeModel) {
return null;
}

return definition;
const candidates = getFromLookup(zigbeeModel);
return candidates ? candidates[0] : null;
}

function findByDevice(device) {
let definition = findByZigbeeModel(device.modelID);

if (!definition) {
// Find by fingerprint
loop:
for (const definitionWithFingerprint of withFingerprint) {
for (const fingerprint of definitionWithFingerprint.fingerprint) {
if (fingerprintMatch(fingerprint, device)) {
definition = definitionWithFingerprint;
break loop;
if (!device) {
return null;
}

const candidates = getFromLookup(device.modelID);
if (candidates.length === 0) {
return null;
} else if (candidates.length === 1) {
return candidates[0];
} else {
// Multiple candidates possible, first try to match based on fingerprint, return the first matching one.
for (const candidate of candidates) {
if (candidate.hasOwnProperty('fingerprint')) {
for (const fingerprint of candidate.fingerprint) {
if (fingerprintMatch(fingerprint, device)) {
return candidate;
}
}
}
}
}

return definition;
// Match based on fingerprint failed, return first matching definition based on zigbeeModel
for (const candidate of candidates) {
if (candidate.hasOwnProperty('zigbeeModel')) {
return candidate;
}
}
}
}

function fingerprintMatch(fingerprint, device) {
Expand Down Expand Up @@ -95,19 +127,13 @@ function fingerprintMatch(fingerprint, device) {
return match;
}


function addDeviceDefinition(device) {
devices.push(device);
addToLookupMaps(device);
}

module.exports = {
devices,
devices: definitions,
findByZigbeeModel,
findByDevice,
toZigbeeConverters: toZigbee,
fromZigbeeConverters: fromZigbee,
addDeviceDefinition,
addDeviceDefinition: addDefinition,
// Can be used to handle events for devices which are not fully paired yet (no modelID).
// Example usecase: https://github.com/Koenkk/zigbee2mqtt/issues/2399#issuecomment-570583325
onEvent: async (type, data, device) => {
Expand Down
33 changes: 32 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,36 @@ describe('index.js', () => {
expect(definition.model).toBe('XBee');
});

it('Find device by fingerprint prefer over zigbeeModel', () => {
const mullerEndpoints = [
{ID: 1, profileID: 49246, deviceID: 544, inputClusters: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], outputClusters: [25]},
{ID: 242, profileID: 41440, deviceID: 102, inputClusters: [33], outputClusters: [33]},
];
const muller = {
type: 'Router',
manufacturerID: 4635,
manufacturerName: 'MLI',
modelID: 'CCT Lighting',
powerSource: 'Mains (single phase)',
endpoints: mullerEndpoints,
getEndpoint: (ID) => mullerEndpoints.find((e) => e.ID === ID),
};

const sunricher = {
// Mock, not the actual fingerprint.
type: 'Router',
manufacturerID: 9999,
manufacturerName: 'SunRicher',
modelID: 'CCT Lighting',
powerSource: 'Mains (single phase)',
endpoints: [],
getEndpoint: (ID) => null,
};

expect(index.findByDevice(sunricher).model).toBe('ZG192910-4');
expect(index.findByDevice(muller).model).toBe('404031');
});

it('Verify devices.js definitions', () => {
function verifyKeys(expected, actual, id) {
expected.forEach((key) => {
Expand Down Expand Up @@ -157,14 +187,15 @@ describe('index.js', () => {
foundModels.push(device.model);
});
});

it('Verify addDeviceDefinition', () => {
const mockZigbeeModel = 'my-mock-device';
const mockDevice = {
zigbeeModel: [mockZigbeeModel],
model: 'mock-model'
};
const undefinedDevice = index.findByZigbeeModel(mockDevice.model);
expect(undefinedDevice).toBeUndefined();
expect(undefinedDevice).toBeNull();
const beforeAdditionDeviceCount = index.devices.length;
index.addDeviceDefinition(mockDevice);
expect(beforeAdditionDeviceCount + 1).toBe(index.devices.length);
Expand Down

0 comments on commit 3f4e2ca

Please sign in to comment.