zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
296 lines (294 loc) • 14.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDefinition = void 0;
const utils_1 = require("./utils");
const m = __importStar(require("./modernExtend"));
const zh = __importStar(require("zigbee-herdsman/dist"));
const philips_1 = require("./philips");
const logger_1 = require("./logger");
const NS = 'zhc:gendef';
// Generator allows to define instances of GeneratedExtend that have typed arguments to extender.
class Generator {
extend;
args;
source;
lib;
constructor(args) {
this.extend = args.extend;
this.args = args.args;
this.source = args.source;
this.lib = args.lib;
}
getExtend() {
return this.extend(this.args);
}
getSource() {
let jsonArgs = JSON.stringify(this.args);
if (!this.args || jsonArgs === '{}') {
jsonArgs = '';
}
return this.source + '(' + jsonArgs + ')';
}
}
function generateSource(definition, generatedExtend) {
const imports = {};
const importsDeduplication = new Set();
generatedExtend.forEach((e) => {
const lib = e.lib ?? 'modernExtend';
if (!(lib in imports))
imports[lib] = [];
const importName = e.getSource().split('(')[0];
if (!importsDeduplication.has(importName)) {
importsDeduplication.add(importName);
imports[lib].push(importName);
}
});
const importsStr = Object.entries(imports)
.map((e) => `const {${e[1].join(', ')}} = require('zigbee-herdsman-converters/lib/${e[0]}');`).join('\n');
return `${importsStr}
const definition = {
zigbeeModel: ['${definition.zigbeeModel}'],
model: '${definition.model}',
vendor: '${definition.vendor}',
description: 'Automatically generated definition',
extend: [${generatedExtend.map((e) => e.getSource()).join(', ')}],
meta: ${JSON.stringify(definition.meta || {})},
};
module.exports = definition;`;
}
async function generateDefinition(device) {
// Map cluster to all endpoints that have this cluster.
const mapClusters = (endpoint, clusters, clusterMap) => {
for (const cluster of clusters) {
if (!clusterMap.has(cluster.name)) {
clusterMap.set(cluster.name, []);
}
const endpointsWithCluster = clusterMap.get(cluster.name);
endpointsWithCluster.push(endpoint);
}
};
const knownInputClusters = inputExtenders.map((ext) => ext[0]).flat(1);
const knownOutputClusters = outputExtenders.map((ext) => ext[0]).flat(1);
const inputClusterMap = new Map();
const outputClusterMap = new Map();
for (const endpoint of device.endpoints) {
// Filter clusters to leave only the ones that we can generate extenders for.
const inputClusters = endpoint.getInputClusters().filter((c) => knownInputClusters.find((known) => known === c.name));
const outputClusters = endpoint.getOutputClusters().filter((c) => knownOutputClusters.find((known) => known === c.name));
mapClusters(endpoint, inputClusters, inputClusterMap);
mapClusters(endpoint, outputClusters, outputClusterMap);
}
// Generate extenders
const usedExtenders = [];
const generatedExtend = [];
const addGenerators = async (clusterName, endpoints, extenders) => {
const extender = extenders.find((e) => e[0].includes(clusterName));
if (!extender || usedExtenders.includes(extender)) {
return;
}
usedExtenders.push(extender);
generatedExtend.push(...(await extender[1](device, endpoints)));
};
for (const [cluster, endpoints] of inputClusterMap) {
await addGenerators(cluster, endpoints, inputExtenders);
}
for (const [cluster, endpoints] of outputClusterMap) {
await addGenerators(cluster, endpoints, outputExtenders);
}
const extenders = generatedExtend.map((e) => e.getExtend());
// Generated definition below will provide this.
extenders.forEach((extender) => {
extender.endpoint = undefined;
});
// Currently multiEndpoint is enabled if device has more then 1 endpoint.
// It is possible to better check if device should be considered multiEndpoint
// based, for example, on generator arguments(i.e. presence of "endpointNames"),
// but this will be enough for now.
const endpointsWithoutGreenPower = device.endpoints.filter((e) => e.ID !== 242);
const multiEndpoint = endpointsWithoutGreenPower.length > 1;
if (multiEndpoint) {
const endpoints = {};
for (const endpoint of endpointsWithoutGreenPower) {
endpoints[endpoint.ID.toString()] = endpoint.ID;
}
// Add to beginning for better visibility.
generatedExtend.unshift(new Generator({ extend: m.deviceEndpoints, args: { endpoints }, source: 'deviceEndpoints' }));
extenders.unshift(generatedExtend[0].getExtend());
}
const definition = {
zigbeeModel: [device.modelID],
model: device.modelID ?? '',
vendor: device.manufacturerName ?? '',
description: 'Automatically generated definition',
extend: extenders,
generated: true,
};
if (multiEndpoint) {
definition.meta = { multiEndpoint };
}
const externalDefinitionSource = generateSource(definition, generatedExtend);
return { externalDefinitionSource, definition };
}
exports.generateDefinition = generateDefinition;
function stringifyEps(endpoints) {
return endpoints.map((e) => e.ID.toString());
}
// This function checks if provided array of endpoints contain
// only first device endpoint, which is passed in as `firstEndpoint`.
function onlyFirstDeviceEnpoint(device, endpoints) {
return endpoints.length === 1 && endpoints[0].ID === device.endpoints[0].ID;
}
// maybeEndpoints returns either `toExtend` if only first device endpoint is provided
// as `endpoints`, or `endpointNames` with `toExtend`.
// This allows to drop unnecessary `endpointNames` argument if it is not needed.
function maybeEndpointArgs(device, endpoints, toExtend) {
if (onlyFirstDeviceEnpoint(device, endpoints)) {
return toExtend;
}
return { endpointNames: stringifyEps(endpoints), ...toExtend };
}
// If generator will have endpoint argument - generator implementation
// should not provide it if only the first device endpoint is passed in.
// If multiple endpoints provided(maybe including the first device endpoint) -
// they all should be passed as an argument, where possible, to be explicit.
const inputExtenders = [
[['msTemperatureMeasurement'], async (d, eps) => [
new Generator({ extend: m.temperature, args: maybeEndpointArgs(d, eps), source: 'temperature' }),
]],
[['msPressureMeasurement'], async (d, eps) => [new Generator({ extend: m.pressure, args: maybeEndpointArgs(d, eps), source: 'pressure' })]],
[['msRelativeHumidity'], async (d, eps) => [new Generator({ extend: m.humidity, args: maybeEndpointArgs(d, eps), source: 'humidity' })]],
[['msCO2'], async (d, eps) => [new Generator({ extend: m.co2, args: maybeEndpointArgs(d, eps), source: 'co2' })]],
[['genPowerCfg'], async (d, eps) => [new Generator({ extend: m.battery, source: 'battery' })]],
[['genOnOff', 'lightingColorCtrl'], extenderOnOffLight],
[['seMetering', 'haElectricalMeasurement'], extenderElectricityMeter],
[['closuresDoorLock'], extenderLock],
[['msIlluminanceMeasurement'], async (d, eps) => [
new Generator({ extend: m.illuminance, args: maybeEndpointArgs(d, eps), source: 'illuminance' }),
]],
[['msOccupancySensing'], async (d, eps) => [
new Generator({ extend: m.occupancy, source: 'occupancy' }),
]],
[['ssIasZone'], async (d, eps) => [
new Generator({ extend: m.iasZoneAlarm, args: {
zoneType: 'generic',
zoneAttributes: ['alarm_1', 'alarm_2', 'tamper', 'battery_low'],
}, source: 'iasZoneAlarm' }),
]],
[['ssIasWd'], async (d, eps) => [
new Generator({ extend: m.iasWarning, source: 'iasWarning' }),
]],
[['genDeviceTempCfg'], async (d, eps) => [
new Generator({ extend: m.deviceTemperature, args: maybeEndpointArgs(d, eps), source: 'deviceTemperature' }),
]],
[['pm25Measurement'], async (d, eps) => [new Generator({ extend: m.pm25, args: maybeEndpointArgs(d, eps), source: 'pm25' })]],
[['msFlowMeasurement'], async (d, eps) => [new Generator({ extend: m.flow, args: maybeEndpointArgs(d, eps), source: 'flow' })]],
[['msSoilMoisture'], async (d, eps) => [new Generator({ extend: m.soilMoisture, args: maybeEndpointArgs(d, eps), source: 'soilMoisture' })]],
[['closuresWindowCovering'], async (d, eps) => [
new Generator({ extend: m.windowCovering, args: { controls: ['lift', 'tilt'] }, source: 'windowCovering' }),
]],
[['genIdentify'], async (d, eps) => [new Generator({ extend: m.identify, source: 'identify' })]],
];
const outputExtenders = [
[['genOnOff'], async (d, eps) => [
new Generator({ extend: m.commandsOnOff, args: maybeEndpointArgs(d, eps), source: 'commandsOnOff' }),
]],
[['genLevelCtrl'], async (d, eps) => [
new Generator({ extend: m.commandsLevelCtrl, args: maybeEndpointArgs(d, eps), source: 'commandsLevelCtrl' }),
]],
[['lightingColorCtrl'], async (d, eps) => [
new Generator({ extend: m.commandsColorCtrl, args: maybeEndpointArgs(d, eps), source: 'commandsColorCtrl' }),
]],
[['closuresWindowCovering'], async (d, eps) => [
new Generator({ extend: m.commandsWindowCovering, args: maybeEndpointArgs(d, eps), source: 'commandsWindowCovering' }),
]],
];
async function extenderLock(device, endpoints) {
// TODO: Support multiple endpoints
if (endpoints.length > 1) {
logger_1.logger.warning('extenderLock can accept only one endpoint', NS);
}
const endpoint = endpoints[0];
const pinCodeCount = await (0, utils_1.getClusterAttributeValue)(endpoint, 'closuresDoorLock', 'numOfPinUsersSupported', 50);
return [new Generator({ extend: m.lock, args: { pinCodeCount }, source: `lock` })];
}
async function extenderOnOffLight(device, endpoints) {
const generated = [];
const lightEndpoints = endpoints.filter((e) => e.supportsInputCluster('lightingColorCtrl'));
const onOffEndpoints = endpoints.filter((e) => lightEndpoints.findIndex((ep) => e.ID === ep.ID) === -1);
if (onOffEndpoints.length !== 0) {
let endpointNames = undefined;
if (!onlyFirstDeviceEnpoint(device, endpoints)) {
endpointNames = endpoints.map((e) => e.ID.toString());
}
generated.push(new Generator({ extend: m.onOff, args: { powerOnBehavior: false, endpointNames }, source: 'onOff' }));
}
for (const endpoint of lightEndpoints) {
// In case read fails, support all features with 31
const colorCapabilities = await (0, utils_1.getClusterAttributeValue)(endpoint, 'lightingColorCtrl', 'colorCapabilities', 31);
const supportsHueSaturation = (colorCapabilities & 1 << 0) > 0;
const supportsEnhancedHueSaturation = (colorCapabilities & 1 << 1) > 0;
const supportsColorXY = (colorCapabilities & 1 << 3) > 0;
const supportsColorTemperature = (colorCapabilities & 1 << 4) > 0;
const args = {};
if (supportsColorTemperature) {
const minColorTemp = await (0, utils_1.getClusterAttributeValue)(endpoint, 'lightingColorCtrl', 'colorTempPhysicalMin', 150);
const maxColorTemp = await (0, utils_1.getClusterAttributeValue)(endpoint, 'lightingColorCtrl', 'colorTempPhysicalMax', 500);
args.colorTemp = { range: [minColorTemp, maxColorTemp] };
}
if (supportsColorXY) {
args.color = true;
if (supportsHueSaturation || supportsEnhancedHueSaturation) {
args.color = {};
if (supportsHueSaturation)
args.color.modes = ['xy', 'hs'];
if (supportsEnhancedHueSaturation)
args.color.enhancedHue = true;
}
}
if (endpoint.getDevice().manufacturerID === zh.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V) {
generated.push(new Generator({ extend: philips_1.philipsLight, args, source: `philipsLight`, lib: 'philips' }));
}
else {
generated.push(new Generator({ extend: m.light, args, source: `light` }));
}
}
return generated;
}
async function extenderElectricityMeter(device, endpoints) {
// TODO: Support multiple endpoints
if (endpoints.length > 1) {
logger_1.logger.warning('extenderElectricityMeter can accept only one endpoint', NS);
}
const endpoint = endpoints[0];
const metering = endpoint.supportsInputCluster('seMetering');
const electricalMeasurements = endpoint.supportsInputCluster('haElectricalMeasurement');
const args = {};
if (!metering || !electricalMeasurements) {
args.cluster = metering ? 'metering' : 'electrical';
}
return [new Generator({ extend: m.electricityMeter, args, source: `electricityMeter` })];
}
//# sourceMappingURL=generateDefinition.js.map