zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
502 lines • 22.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.develcoModernExtend = void 0;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const exposes_1 = require("./exposes");
const modernExtend_1 = require("./modernExtend");
const utils_1 = require("./utils");
const manufacturerOptions = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO };
exports.develcoModernExtend = {
addCustomClusterManuSpecificDevelcoGenBasic: () => (0, modernExtend_1.deviceAddCustomCluster)("genBasic", {
name: "genBasic",
ID: zigbee_herdsman_1.Zcl.Clusters.genBasic.ID,
attributes: {
develcoPrimarySwVersion: {
name: "develcoPrimarySwVersion",
ID: 0x8000,
type: zigbee_herdsman_1.Zcl.DataType.OCTET_STR,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
},
develcoPrimaryHwVersion: {
name: "develcoPrimaryHwVersion",
ID: 0x8020,
type: zigbee_herdsman_1.Zcl.DataType.OCTET_STR,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
},
develcoLedControl: {
name: "develcoLedControl",
ID: 0x8100,
type: zigbee_herdsman_1.Zcl.DataType.BITMAP8,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
},
develcoTxPower: {
name: "develcoTxPower",
ID: 0x8101,
type: zigbee_herdsman_1.Zcl.DataType.ENUM8,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xff,
},
},
commands: {},
commandsResponse: {},
}),
addCustomClusterManuSpecificDevelcoIasZone: () => (0, modernExtend_1.deviceAddCustomCluster)("ssIasZone", {
name: "ssIasZone",
ID: zigbee_herdsman_1.Zcl.Clusters.ssIasZone.ID,
attributes: {
develcoZoneStatusInterval: {
name: "develcoZoneStatusInterval",
ID: 0x8000,
type: zigbee_herdsman_1.Zcl.DataType.UINT16,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xffff,
},
develcoAlarmOffDelay: {
name: "develcoAlarmOffDelay",
ID: 0x8001,
type: zigbee_herdsman_1.Zcl.DataType.UINT16,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xffff,
},
},
commands: {},
commandsResponse: {},
}),
addCustomClusterManuSpecificDevelcoAirQuality: () => (0, modernExtend_1.deviceAddCustomCluster)("manuSpecificDevelcoAirQuality", {
name: "manuSpecificDevelcoAirQuality",
ID: 0xfc03,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
attributes: {
measuredValue: { name: "measuredValue", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff },
minMeasuredValue: { name: "minMeasuredValue", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff },
maxMeasuredValue: { name: "maxMeasuredValue", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff },
resolution: { name: "resolution", ID: 0x0003, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff },
},
commands: {},
commandsResponse: {},
}),
addCustomDevelcoSeMeteringCluster: () => (0, modernExtend_1.deviceAddCustomCluster)("seMetering", {
name: "seMetering",
ID: zigbee_herdsman_1.Zcl.Clusters.seMetering.ID,
attributes: {
develcoPulseConfiguration: {
name: "develcoPulseConfiguration",
ID: 0x0300,
type: zigbee_herdsman_1.Zcl.DataType.UINT16,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xffff,
},
develcoCurrentSummation: {
name: "develcoCurrentSummation",
ID: 0x0301,
type: zigbee_herdsman_1.Zcl.DataType.UINT48,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xffffffffffff,
},
develcoInterfaceMode: {
name: "develcoInterfaceMode",
ID: 0x0302,
type: zigbee_herdsman_1.Zcl.DataType.ENUM16,
manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DEVELCO,
write: true,
max: 0xffff,
},
},
commands: {},
commandsResponse: {},
}),
readGenBasicPrimaryVersions: () => {
/*
* Develco (and there B2C brand Frient) do not use swBuildId
* The versions are stored in develcoPrimarySwVersion and develcoPrimaryHwVersion, we read them during configure.
*/
const configure = [
async (device, coordinatorEndpoint, definition) => {
for (const ep of device.endpoints) {
if (ep.supportsInputCluster("genBasic")) {
try {
const data = await ep.read("genBasic", ["develcoPrimarySwVersion", "develcoPrimaryHwVersion"], manufacturerOptions);
if (data.develcoPrimarySwVersion !== undefined) {
device.softwareBuildID = data.develcoPrimarySwVersion.join(".");
}
if (data.develcoPrimaryHwVersion !== undefined) {
device.hardwareVersion = Number.parseInt(data.develcoPrimaryHwVersion.join(""), 10);
}
device.save();
}
catch {
/* catch timeouts of sleeping devices */
}
break;
}
}
},
];
return { configure, isModernExtend: true };
},
voc: (args) => (0, modernExtend_1.numeric)({
name: "voc",
cluster: "manuSpecificDevelcoAirQuality",
attribute: "measuredValue",
reporting: { min: "1_MINUTE", max: "1_HOUR", change: 10 },
description: "Measured VOC value",
// from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
// "The mean molar mass of this mixture is 110 g/mol and hence,
// 1 ppb TVOC corresponds to 4.5 μg/m3."
scale: (value, type) => {
if (type === "from") {
return value * 4.5;
}
return value;
},
unit: "µg/m³",
access: "STATE_GET",
...args,
}),
airQuality: () => {
// NOTE: do not setup reporting, this is handled by the voc() modernExtend
const clusterName = "manuSpecificDevelcoAirQuality";
const attributeName = "measuredValue";
const propertyName = "air_quality";
const access = exposes_1.access.STATE;
const expose = exposes_1.presets
.enum("air_quality", access, ["excellent", "good", "moderate", "poor", "unhealthy", "out_of_range", "unknown"])
.withDescription("Measured air quality");
const fromZigbee = [
{
cluster: clusterName,
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[attributeName] !== undefined) {
const vocPpb = Number.parseFloat(msg.data[attributeName]);
// from aqszb-110-technical-manual-air-quality-sensor-04-08-20.pdf page 6, section 2.2 voc
// this contains a ppb to level mapping table.
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let airQuality;
if (vocPpb <= 65) {
airQuality = "excellent";
}
else if (vocPpb <= 220) {
airQuality = "good";
}
else if (vocPpb <= 660) {
airQuality = "moderate";
}
else if (vocPpb <= 2200) {
airQuality = "poor";
}
else if (vocPpb <= 5500) {
airQuality = "unhealthy";
}
else if (vocPpb > 5500) {
airQuality = "out_of_range";
}
else {
airQuality = "unknown";
}
return { [propertyName]: airQuality };
}
},
},
];
return { exposes: [expose], fromZigbee, isModernExtend: true };
},
batteryLowAA: () => {
/*
* Per the technical documentation for AQSZB-110:
* To detect low battery the system can monitor the "BatteryVoltage" by setting up a reporting interval of every 12 hour.
* When a voltage of 2.5V is measured the battery should be replaced.
* Low batt LED indication–RED LED will blink twice every 60 second.
*
* Similar notes found in other 2x AA powered Develco devices like HMSZB-110 and MOSZB-140
*/
const clusterName = "genPowerCfg";
const attributeName = "batteryVoltage";
const propertyName = "battery_low";
const expose = exposes_1.presets.battery_low();
const fromZigbee = [
{
cluster: clusterName,
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[attributeName] !== undefined && msg.data[attributeName] < 255) {
const voltage = msg.data[attributeName];
return { [propertyName]: voltage <= 25 };
}
},
},
];
return { exposes: [expose], fromZigbee, isModernExtend: true };
},
temperature: (args) => (0, modernExtend_1.temperature)({
...args,
}),
deviceTemperature: (args) => (0, modernExtend_1.deviceTemperature)({
reporting: { min: "5_MINUTES", max: "1_HOUR", change: 2 }, // Device temperature reports with 2 degree change
...args,
}),
currentSummation: (args) => (0, modernExtend_1.numeric)({
name: "current_summation",
cluster: "seMetering",
attribute: "develcoCurrentSummation",
description: "Current summation value sent to the display. e.g. 570 = 0,570 kWh",
access: "SET",
valueMin: 0,
valueMax: 268435455,
...args,
}),
pulseConfiguration: (args) => (0, modernExtend_1.numeric)({
name: "pulse_configuration",
cluster: "seMetering",
attribute: "develcoPulseConfiguration",
description: "Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535",
access: "ALL",
valueMin: 0,
valueMax: 65535,
...args,
}),
ledControl: () => {
const expose = exposes_1.presets
.composite("led_control", "led_control", exposes_1.access.ALL)
.withFeature(exposes_1.presets
.binary("indicate_faults", exposes_1.access.ALL, true, false)
.withDescription("Enable/disable LED indication for faults (e.g., lost connection to gateway)"))
.withFeature(exposes_1.presets.binary("indicate_mains_power", exposes_1.access.ALL, true, false).withDescription("Enable/disable green LED indication for mains power status"));
const fromZigbee = [
{
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (_model, msg, _publish, _options, _meta) => {
if (Object.hasOwn(msg.data, "develcoLedControl")) {
const ledControl = msg.data.develcoLedControl;
return {
led_control: {
indicate_faults: (ledControl & 1) > 0,
indicate_mains_power: (ledControl & 2) > 0,
},
};
}
},
},
];
const toZigbee = [
{
key: ["led_control"],
convertSet: async (entity, _key, value, meta) => {
// biome-ignore lint/style/useNamingConvention: zigbee2mqtt uses snake_case for exposed attributes
const currentState = meta.state.led_control || {
indicate_faults: false,
indicate_mains_power: false,
};
// biome-ignore lint/style/useNamingConvention: zigbee2mqtt uses snake_case for exposed attributes
const newState = { ...currentState, ...value };
let bitmap = 0;
if (newState.indicate_faults)
bitmap |= 1;
if (newState.indicate_mains_power)
bitmap |= 2;
await entity.write("genBasic", { develcoLedControl: bitmap }, manufacturerOptions);
return { state: { led_control: newState } };
},
convertGet: async (entity, _key, _meta) => {
await entity.read("genBasic", ["develcoLedControl"], manufacturerOptions);
},
},
];
const configure = [
async (device, _coordinatorEndpoint, _definition) => {
for (const ep of device.endpoints) {
if (ep.supportsInputCluster("genBasic")) {
try {
await ep.read("genBasic", ["develcoLedControl"], manufacturerOptions);
}
catch {
/* catch timeouts of sleeping devices */
}
break;
}
}
},
];
return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true };
},
txPower: () => {
const expose = exposes_1.presets
.enum("tx_power", exposes_1.access.ALL, ["CE", "FCC"])
.withDescription("TX power mode for regulatory compliance (CE or FCC). Requires device rejoin to apply.");
const fromZigbee = [
{
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (_model, msg, _publish, _options, _meta) => {
if (Object.hasOwn(msg.data, "develcoTxPower")) {
return { tx_power: msg.data.develcoTxPower === 0 ? "CE" : "FCC" };
}
},
},
];
const toZigbee = [
{
key: ["tx_power"],
convertSet: async (entity, _key, value, _meta) => {
const numericValue = value === "CE" ? 0 : 1;
await entity.write("genBasic", { develcoTxPower: numericValue }, manufacturerOptions);
return { state: { tx_power: value } };
},
convertGet: async (entity, _key, _meta) => {
await entity.read("genBasic", ["develcoTxPower"], manufacturerOptions);
},
},
];
const configure = [
async (device, _coordinatorEndpoint, _definition) => {
for (const ep of device.endpoints) {
if (ep.supportsInputCluster("genBasic")) {
try {
await ep.read("genBasic", ["develcoTxPower"], manufacturerOptions);
}
catch {
/* catch timeouts of sleeping devices */
}
break;
}
}
},
];
return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true };
},
zoneStatusInterval: () => {
const expose = exposes_1.presets
.numeric("zone_status_interval", exposes_1.access.ALL)
.withUnit("s")
.withValueMin(0)
.withValueMax(65535)
.withDescription("Heartbeat interval in seconds. Controls the periodic interval between ZoneStatusChange commands (default 300s)");
const fromZigbee = [
{
cluster: "ssIasZone",
type: ["attributeReport", "readResponse"],
convert: (_model, msg, _publish, _options, _meta) => {
if (Object.hasOwn(msg.data, "develcoZoneStatusInterval")) {
return { zone_status_interval: msg.data.develcoZoneStatusInterval };
}
},
},
];
const toZigbee = [
{
key: ["zone_status_interval"],
convertSet: async (entity, _key, value, _meta) => {
await entity.write("ssIasZone", { develcoZoneStatusInterval: value }, manufacturerOptions);
return { state: { zone_status_interval: value } };
},
convertGet: async (entity, _key, _meta) => {
await entity.read("ssIasZone", ["develcoZoneStatusInterval"], manufacturerOptions);
},
},
];
const configure = [
async (device, _coordinatorEndpoint, _definition) => {
for (const ep of device.endpoints) {
if (ep.supportsInputCluster("ssIasZone")) {
try {
await ep.read("ssIasZone", ["develcoZoneStatusInterval"], manufacturerOptions);
}
catch {
/* catch timeouts of sleeping devices */
}
break;
}
}
},
];
return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true };
},
acConnected: () => {
const expose = exposes_1.presets
.binary("ac_connected", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device is connected to AC mains power")
.withCategory("diagnostic");
const fromZigbee = [
{
cluster: "ssIasZone",
type: ["commandStatusChangeNotification", "attributeReport", "readResponse"],
convert: (_model, msg, _publish, _options, _meta) => {
const zoneStatus = "zonestatus" in msg.data ? msg.data.zonestatus : msg.data.zoneStatus;
if (zoneStatus !== undefined) {
// Bit 7 = 1 means "mains power lost", so ac_connected = false
// Bit 7 = 0 means "mains power ok", so ac_connected = true
return { ac_connected: (zoneStatus & (1 << 7)) === 0 };
}
},
},
];
const configure = [
async (device, _coordinatorEndpoint, _definition) => {
for (const ep of device.endpoints) {
if (ep.supportsInputCluster("ssIasZone")) {
try {
await ep.read("ssIasZone", ["zoneStatus"]);
}
catch {
/* catch timeouts of sleeping devices */
}
break;
}
}
},
];
return { exposes: [expose], fromZigbee, configure, isModernExtend: true };
},
customPulseTrigger: (options) => {
const endpointNames = options?.endpointNames || [];
const durationExpose = exposes_1.presets
.numeric("duration", exposes_1.access.STATE_SET)
.withLabel("Pulse duration")
.withUnit("s")
.withValueMin(0.0)
.withValueMax(3600)
.withDescription("Duration of the pulse.");
const triggerExpose = exposes_1.presets
.enum("trigger", exposes_1.access.SET, ["press"])
.withDescription("Trigger a timed pulse. The length of the pulse is defined by 'Pulse duration'. If the 'Pulse duration' is undefined a default value of 1s will be used.");
const exposes = [...(0, utils_1.exposeEndpoints)(durationExpose, endpointNames), ...(0, utils_1.exposeEndpoints)(triggerExpose, endpointNames)];
const toZigbee = [
{
key: ["trigger", "duration"],
convertSet: async (entity, key, value, meta) => {
if (key === "duration") {
const val = value === null ? 1.0 : Number(value);
return { state: { [key]: val } };
}
if (key === "trigger" && value === "press") {
const epSuffix = meta.endpoint_name ? `_${meta.endpoint_name}` : "";
const stateLookupKey = `duration${epSuffix}`;
const seconds = meta.state[stateLookupKey] !== undefined ? Number(meta.state[stateLookupKey]) : 1.0;
const deciseconds = Math.round(seconds * 10);
await entity.command("genOnOff", "onWithTimedOff", {
ctrlbits: 0,
ontime: deciseconds,
offwaittime: 0,
}, meta.options);
return { state: { [key]: null } };
}
},
},
];
return {
isModernExtend: true,
exposes,
toZigbee,
};
},
};
//# sourceMappingURL=develco.js.map