Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import assert from 'assert';
import * as ota from './lib/ota';
import allDefinitions from './devices';
import { Definition, Fingerprint, Zh, OnEventData, OnEventType, Configure, Expose, Tz, OtaUpdateAvailableResult } from './lib/types';
import {generateDefinition} from './lib/generateDefinition';

export {
Definition as Definition,
Expand Down Expand Up @@ -88,7 +89,7 @@ function validateDefinition(definition: Definition) {
assert.ok(Array.isArray(definition.exposes) || typeof definition.exposes === 'function', 'Exposes incorrect');
}

export function addDefinition(definition: Definition) {
function processExtensions(definition: Definition): Definition {
if ('extend' in definition) {
if (Array.isArray(definition.extend)) {
// Modern extend, merges properties, e.g. when both extend and definition has toZigbee, toZigbee will be combined
Expand Down Expand Up @@ -172,6 +173,12 @@ export function addDefinition(definition: Definition) {
}
}

return definition
}

function prepareDefinition(definition: Definition): Definition {
definition = processExtensions(definition);

definition.toZigbee.push(
toZigbee.scene_store, toZigbee.scene_recall, toZigbee.scene_add, toZigbee.scene_remove, toZigbee.scene_remove_all,
toZigbee.scene_rename, toZigbee.read, toZigbee.write,
Expand All @@ -182,7 +189,6 @@ export function addDefinition(definition: Definition) {
}

validateDefinition(definition);
definitions.splice(0, 0, definition);

if (!definition.options) definition.options = [];
const optionKeys = definition.options.map((o) => o.name);
Expand All @@ -198,6 +204,14 @@ export function addDefinition(definition: Definition) {
}
}

return definition
}

export function addDefinition(definition: Definition) {
definition = prepareDefinition(definition)

definitions.splice(0, 0, definition);

if ('fingerprint' in definition) {
for (const fingerprint of definition.fingerprint) {
addToLookup(fingerprint.modelID, definition);
Expand All @@ -215,8 +229,8 @@ for (const definition of allDefinitions) {
addDefinition(definition);
}

export function findByDevice(device: Zh.Device) {
let definition = findDefinition(device);
export function findByDevice(device: Zh.Device, generateForUnknown: boolean = false) {
let definition = findDefinition(device, generateForUnknown);
if (definition && definition.whiteLabel) {
const match = definition.whiteLabel.find((w) => 'fingerprint' in w && w.fingerprint.find((f) => isFingerprintMatch(f, device)));
if (match) {
Expand All @@ -231,14 +245,24 @@ export function findByDevice(device: Zh.Device) {
return definition;
}

export function findDefinition(device: Zh.Device): Definition {
export function findDefinition(device: Zh.Device, generateForUnknown: boolean = false): Definition {
if (!device) {
return null;
}

const candidates = getFromLookup(device.modelID);
if (!candidates) {
return null;
if (!generateForUnknown || device.type === 'Coordinator') {
return null;
}

const definition = generateDefinition(device);
if (!definition) {
return null
}
// Do not add this definition to cache,
// as device configuration might change.
return prepareDefinition(definition);
} else if (candidates.length === 1 && candidates[0].hasOwnProperty('zigbeeModel')) {
return candidates[0];
} else {
Expand Down
64 changes: 64 additions & 0 deletions src/lib/generateDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Cluster} from 'zigbee-herdsman/dist/zcl/tstype';
import {Definition, ModernExtend, Zh} from './types';
import * as e from './modernExtend';
import {Endpoint} from 'zigbee-herdsman/dist/controller/model';

export function generateDefinition(device: Zh.Device): Definition {
const deviceExtenders: ModernExtend[] = [];

device.endpoints.forEach((endpoint) => {
const addExtenders = (cluster: Cluster, knownExtenders: extendersObject) => {
const clusterName = cluster.name || cluster.ID.toString();
if (!knownExtenders.hasOwnProperty(clusterName)) {
return;
}

const extenderProviders = knownExtenders[clusterName];
const extenders = extenderProviders.map((extender: extenderProvider): ModernExtend => {
if (typeof extender !== 'function') {
return extender;
}
return extender(endpoint, cluster);
});

deviceExtenders.push(...(extenders));
};

endpoint.getInputClusters().forEach((cluster) => {
addExtenders(cluster, inputExtenders);
});
endpoint.getOutputClusters().forEach((cluster) => {
addExtenders(cluster, outputExtenders);
});
});

const definition: Partial<Definition> = {
model: device.modelID || '',
vendor: device.manufacturerName || '',
description: 'Generated from device information',
extend: deviceExtenders,
generated: true,
};

return definition as Definition;
}

// This configurator type provides some flexibility in terms of how ModernExtend configuration can be obtained.
// I.e. if cluster has optional attributes - this type can be used
// to define function that will generate more feature-full extension.
type extenderConfigurator = (endpoint: Endpoint, cluster: Cluster) => ModernExtend
// extenderProvider defines a type that will produce a `ModernExtend`
// either directly, or by calling a function.
type extenderProvider = ModernExtend | extenderConfigurator
type extendersObject = {[name: string]: extenderProvider[]}

const inputExtenders: extendersObject = {
'msTemperatureMeasurement': [e.temperature()],
'msPressureMeasurement': [e.pressure()],
'msRelativeHumidity': [e.humidity()],
'genOnOff': [e.onOff({powerOnBehavior: false})],
};

const outputExtenders: extendersObject = {
'genIdentify': [e.identify()],
};
20 changes: 20 additions & 0 deletions src/lib/modernExtend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export function setupConfigureForReporting(
return configure;
}

export function identify(): ModernExtend {
return {
toZigbee: [tz.identify],
isModernExtend: true,
};
}

export interface OnOffArgs {
powerOnBehavior?: boolean, ota?: DefinitionOta, skipDuplicateTransaction?: boolean, endpoints?: {[s: string]: number},
configureReporting?: boolean,
Expand Down Expand Up @@ -528,3 +535,16 @@ export function humidity(args?: Partial<NumericArgs>) {
});
}

export function pressure(args?: Partial<NumericArgs>): ModernExtend {
return numeric({
name: 'pressure',
cluster: 'msPressureMeasurement',
attribute: 'measuredValue',
reporting: {min: '10_SECONDS', max: '1_HOUR', change: 100},
description: 'The measured atmospheric pressure',
unit: 'hPa',
scale: 100,
readOnly: true,
...args,
});
}
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export type Definition = {
meta?: DefinitionMeta,
onEvent?: OnEvent,
ota?: DefinitionOta,
generated?: boolean,
} & ({ zigbeeModel: string[] } | { fingerprint: Fingerprint[] })
& ({ extend: Extend | ModernExtend[], fromZigbee?: Fz.Converter[], toZigbee?: Tz.Converter[],
exposes?: (Expose[] | ((device: Zh.Device | undefined, options: KeyValue | undefined) => Expose[])) } |
Expand Down
106 changes: 106 additions & 0 deletions test/generateDefinition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Definition } from '../src/lib/types';
import fz from '../src/converters/fromZigbee'

import { repInterval } from '../src/lib/constants';
import {assertDefintion, assertDefinitionArgs, mockDevice, reportingItem} from './modernExtend.test';
import { findByDevice} from '../src';
import Device from 'zigbee-herdsman/dist/controller/model/device';

const assertGeneratedDefinition = async (args: assertDefinitionArgs) => {
const getDefinition = (device: Device): Definition => {
return findByDevice(device, true);
}

const definition = getDefinition(args.device)

expect(definition.model).toEqual(args.device.modelID)

return await assertDefintion({findByDeviceFn: getDefinition, ...args})
}

describe('GenerateDefinition', () => {
test('empty', async () => {
await assertGeneratedDefinition({
device: mockDevice({modelID: 'empty', endpoints: [{inputClusters: [], outputClusters:[]}]}),
meta: undefined,
fromZigbee: [],
toZigbee: [],
exposes: ['linkquality'],
bind: [],
read: [],
configureReporting: [],
});
});

test('input(msTemperatureMeasurement),output(genIdentify)', async () => {
await assertGeneratedDefinition({
device: mockDevice({modelID: 'temp', endpoints: [{inputClusters: ['msTemperatureMeasurement'], outputClusters:['genIdentify']}]}),
meta: undefined,
fromZigbee: [fz.temperature],
toZigbee: ['identify'],
exposes: ['linkquality', 'temperature'],
bind: {1: ['msTemperatureMeasurement']},
read: {1: [['msTemperatureMeasurement', ['measuredValue']]]},
configureReporting: {
1: [
['msTemperatureMeasurement', [reportingItem('measuredValue', 0, repInterval.MAX, 1)]],
],
},
});
});

test('input(msPressureMeasurement)', async () => {
await assertGeneratedDefinition({
device: mockDevice({modelID: 'pressure', endpoints: [{inputClusters: ['msPressureMeasurement'], outputClusters:[]}]}),
meta: undefined,
fromZigbee: [fz.pressure],
toZigbee: [],
exposes: ['linkquality', 'pressure'],
bind: {1: ['msPressureMeasurement']},
read: {1: [['msPressureMeasurement', ['measuredValue']]]},
configureReporting: {
1: [
['msPressureMeasurement', [reportingItem('measuredValue', 0, repInterval.MAX, 1)]],
],
},
});
});

test('input(msRelativeHumidity)', async () => {
await assertGeneratedDefinition({
device: mockDevice({modelID: 'humidity', endpoints: [{inputClusters: ['msRelativeHumidity'], outputClusters:[]}]}),
meta: undefined,
fromZigbee: [fz.humidity],
toZigbee: [],
exposes: ['humidity', 'linkquality'],
bind: {1: ['msRelativeHumidity']},
read: {1: [['msRelativeHumidity', ['measuredValue']]]},
configureReporting: {
1: [
['msRelativeHumidity', [reportingItem('measuredValue', 0, repInterval.MAX, 1)]],
],
},
});
});

test('input(msTemperatureMeasurement, genOnOff)', async () => {
await assertGeneratedDefinition({
device: mockDevice({modelID: 'combo', endpoints: [{inputClusters: ['msTemperatureMeasurement', 'genOnOff'], outputClusters:[]}]}),
meta: undefined,
fromZigbee: [fz.temperature, fz.on_off],
toZigbee: ['state', 'on_time', 'off_wait_time'],
exposes: ['linkquality', 'switch(state)', 'temperature'],
bind: {1: ['msTemperatureMeasurement', 'genOnOff']},
read: {1: [
['msTemperatureMeasurement', ['measuredValue']],
['genOnOff', ['onOff']],
]},
configureReporting: {
1: [
['msTemperatureMeasurement', [reportingItem('measuredValue', 0, repInterval.MAX, 1)]],
['genOnOff', [reportingItem('onOff', 0, repInterval.MAX, 1)]],
],
},
});
});
});
31 changes: 31 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ describe('index.js', () => {
expect(definition).toBeNull();
});

it('Find by device should generate for unknown', () => {
const endpoints = [
{
ID: 1, profileID: undefined, deviceID: undefined,
getInputClusters() {
return [];
},
getOutputClusters() {
return [{name: 'genIdentify'}]
},
},
];
const device = {
type: 'EndDevice',
manufacturerID: undefined,
modelID: 'test_generate',
endpoints,
getEndpoint: (ID) => endpoints.find((e) => e.ID === ID),
};

const definition = index.findByDevice(device, true);
expect(definition.model).toBe('test_generate');
expect(definition.vendor).toBe('');
expect(definition.description).toBe('Generated from device information');
expect(definition.extend).toBeUndefined();
expect(definition.fromZigbee).toHaveLength(0);
expect(definition.toZigbee).toHaveLength(11);
expect(definition.exposes).toHaveLength(1);
expect(definition.options).toHaveLength(0);
});

it('Find by device when device has modelID should match', () => {
const endpoints = [
{ID: 1, profileID: undefined, deviceID: undefined, inputClusters: [], outputClusters: []},
Expand Down
Loading