zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,102 lines • 110 kB
JavaScript
"use strict";
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GPDF_COMMANDS = exports.TIME_LOOKUP = void 0;
exports.setupAttributes = setupAttributes;
exports.setupConfigureForReporting = setupConfigureForReporting;
exports.setupConfigureForBinding = setupConfigureForBinding;
exports.setupConfigureForReading = setupConfigureForReading;
exports.determineEndpoint = determineEndpoint;
exports.forceDeviceType = forceDeviceType;
exports.forcePowerSource = forcePowerSource;
exports.linkQuality = linkQuality;
exports.battery = battery;
exports.deviceTemperature = deviceTemperature;
exports.identify = identify;
exports.onOff = onOff;
exports.commandsOnOff = commandsOnOff;
exports.customTimeResponse = customTimeResponse;
exports.illuminance = illuminance;
exports.temperature = temperature;
exports.pressure = pressure;
exports.flow = flow;
exports.humidity = humidity;
exports.soilMoisture = soilMoisture;
exports.occupancy = occupancy;
exports.co2 = co2;
exports.pm25 = pm25;
exports.light = light;
exports.commandsLevelCtrl = commandsLevelCtrl;
exports.commandsColorCtrl = commandsColorCtrl;
exports.lightingBallast = lightingBallast;
exports.lock = lock;
exports.windowCovering = windowCovering;
exports.commandsWindowCovering = commandsWindowCovering;
exports.iasZoneAlarm = iasZoneAlarm;
exports.iasWarning = iasWarning;
exports.electricityMeter = electricityMeter;
exports.gasMeter = gasMeter;
exports.genericGreenPower = genericGreenPower;
exports.commandsScenes = commandsScenes;
exports.enumLookup = enumLookup;
exports.numeric = numeric;
exports.binary = binary;
exports.text = text;
exports.actionEnumLookup = actionEnumLookup;
exports.quirkAddEndpointCluster = quirkAddEndpointCluster;
exports.quirkCheckinInterval = quirkCheckinInterval;
exports.reconfigureReportingsOnDeviceAnnounce = reconfigureReportingsOnDeviceAnnounce;
exports.skipDefaultResponse = skipDefaultResponse;
exports.deviceEndpoints = deviceEndpoints;
exports.deviceAddCustomCluster = deviceAddCustomCluster;
exports.ignoreClusterReport = ignoreClusterReport;
exports.bindCluster = bindCluster;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const node_assert_1 = __importDefault(require("node:assert"));
const fz = __importStar(require("../converters/fromZigbee"));
const tz = __importStar(require("../converters/toZigbee"));
const logger_1 = require("../lib/logger");
const globalStore = __importStar(require("../lib/store"));
const exposes_1 = require("./exposes");
const light_1 = require("./light");
const utils_1 = require("./utils");
function getEndpointsWithCluster(device, cluster, type) {
if (!device.endpoints) {
throw new Error(`${device.ieeeAddr} ${device.endpoints}`);
}
const endpoints = type === "input"
? device.endpoints.filter((ep) => ep.getInputClusters().find((c) => ((0, utils_1.isNumber)(cluster) ? c.ID === cluster : c.name === cluster)))
: device.endpoints.filter((ep) => ep.getOutputClusters().find((c) => ((0, utils_1.isNumber)(cluster) ? c.ID === cluster : c.name === cluster)));
if (endpoints.length === 0) {
throw new Error(`Device ${device.ieeeAddr} has no ${type} cluster ${cluster}`);
}
return endpoints;
}
const IAS_EXPOSE_LOOKUP = {
occupancy: exposes_1.presets.binary("occupancy", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected occupancy"),
contact: exposes_1.presets.binary("contact", exposes_1.access.STATE, false, true).withDescription("Indicates whether the device is opened or closed"),
smoke: exposes_1.presets.binary("smoke", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected smoke"),
water_leak: exposes_1.presets.binary("water_leak", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected a water leak"),
carbon_monoxide: exposes_1.presets.binary("carbon_monoxide", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected carbon monoxide"),
sos: exposes_1.presets.binary("sos", exposes_1.access.STATE, true, false).withLabel("SOS").withDescription("Indicates whether the SOS alarm is triggered"),
vibration: exposes_1.presets.binary("vibration", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected vibration"),
alarm: exposes_1.presets.binary("alarm", exposes_1.access.STATE, true, false).withDescription("Indicates whether the alarm is triggered"),
gas: exposes_1.presets.binary("gas", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected gas"),
alarm_1: exposes_1.presets.binary("alarm_1", exposes_1.access.STATE, true, false).withDescription("Indicates whether IAS Zone alarm 1 is active"),
alarm_2: exposes_1.presets.binary("alarm_2", exposes_1.access.STATE, true, false).withDescription("Indicates whether IAS Zone alarm 2 is active"),
tamper: exposes_1.presets.binary("tamper", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device is tampered").withCategory("diagnostic"),
rain: exposes_1.presets.binary("rain", exposes_1.access.STATE, true, false).withDescription("Indicates whether the device detected rainfall"),
battery_low: exposes_1.presets
.binary("battery_low", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the battery of the device is almost empty")
.withCategory("diagnostic"),
supervision_reports: exposes_1.presets
.binary("supervision_reports", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device issues reports on zone operational status")
.withCategory("diagnostic"),
restore_reports: exposes_1.presets
.binary("restore_reports", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device issues reports on alarm no longer being present")
.withCategory("diagnostic"),
ac_status: exposes_1.presets
.binary("ac_status", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device mains voltage supply is at fault")
.withCategory("diagnostic"),
test: exposes_1.presets
.binary("test", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device is currently performing a test")
.withCategory("diagnostic"),
trouble: exposes_1.presets
.binary("trouble", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device is currently having trouble")
.withCategory("diagnostic"),
battery_defect: exposes_1.presets
.binary("battery_defect", exposes_1.access.STATE, true, false)
.withDescription("Indicates whether the device battery is defective")
.withCategory("diagnostic"),
};
exports.TIME_LOOKUP = {
MAX: 65000,
"4_HOURS": 14400,
"1_HOUR": 3600,
"30_MINUTES": 1800,
"5_MINUTES": 300,
"2_MINUTES": 120,
"1_MINUTE": 60,
"10_SECONDS": 10,
"5_SECONDS": 5,
"1_SECOND": 1,
MIN: 0,
};
function convertReportingConfigTime(time) {
if ((0, utils_1.isString)(time)) {
if (!(time in exports.TIME_LOOKUP))
throw new Error(`Reporting time '${time}' is unknown`);
return exports.TIME_LOOKUP[time];
}
return time;
}
async function setupAttributes(entity, coordinatorEndpoint, cluster, config, configureReporting = true, read = true) {
const endpoints = (0, utils_1.isEndpoint)(entity) ? [entity] : getEndpointsWithCluster(entity, cluster, "input");
const ieeeAddr = (0, utils_1.isEndpoint)(entity) ? entity.deviceIeeeAddress : entity.ieeeAddr;
for (const endpoint of endpoints) {
logger_1.logger.debug(`Configure reporting: ${configureReporting}, read: ${read} for ${ieeeAddr}/${endpoint.ID} ${cluster} ${JSON.stringify(config)}`, "zhc:setupattribute");
// Split into chunks of 4 to prevent to message becoming too big.
const chunks = (0, utils_1.splitArrayIntoChunks)(config, 4);
if (configureReporting) {
await endpoint.bind(cluster, coordinatorEndpoint);
for (const chunk of chunks) {
await endpoint.configureReporting(cluster, chunk.map((a) => ({
minimumReportInterval: convertReportingConfigTime(a.min),
maximumReportInterval: convertReportingConfigTime(a.max),
reportableChange: a.change,
attribute: a.attribute,
})));
}
}
if (read) {
for (const chunk of chunks) {
try {
// Don't fail configuration if reading this attribute fails
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/7074
await endpoint.read(cluster, chunk.map((a) => ((0, utils_1.isString)(a) ? a : (0, utils_1.isObject)(a.attribute) ? a.attribute.ID : a.attribute)));
}
catch (e) {
logger_1.logger.debug(`Reading attribute failed: ${e}`, "zhc:setupattribute");
}
}
}
}
}
function setupConfigureForReporting(cluster, attribute, args) {
const { config = false, access = undefined, endpointNames = undefined, singleEndpoint = false } = args;
const configureReporting = !!config;
const read = !!(access & exposes_1.access.GET);
if (configureReporting || read) {
const configure = async (device, coordinatorEndpoint, definition) => {
const reportConfig = config ? { ...config, attribute: attribute } : { attribute, min: -1, max: -1, change: -1 };
let endpoints;
if (endpointNames) {
(0, node_assert_1.default)(!singleEndpoint, "`endpointNames` cannot be used together with `singleEndpoint`");
const definitionEndpoints = definition.endpoint(device);
const endpointIds = endpointNames.map((e) => definitionEndpoints[e]);
endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID));
}
else {
endpoints = getEndpointsWithCluster(device, cluster, "input");
if (singleEndpoint) {
endpoints = [endpoints[0]];
}
}
for (const endpoint of endpoints) {
await setupAttributes(endpoint, coordinatorEndpoint, cluster, [reportConfig], configureReporting, read);
}
};
return configure;
}
return undefined;
}
function setupConfigureForBinding(cluster, clusterType, endpointNames) {
const configure = async (device, coordinatorEndpoint, definition) => {
if (endpointNames) {
const definitionEndpoints = definition.endpoint(device);
const endpointIds = endpointNames.map((e) => definitionEndpoints[e]);
const endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID));
for (const endpoint of endpoints) {
await endpoint.bind(cluster, coordinatorEndpoint);
}
}
else {
const endpoints = getEndpointsWithCluster(device, cluster, clusterType);
for (const endpoint of endpoints) {
await endpoint.bind(cluster, coordinatorEndpoint);
}
}
};
return configure;
}
function setupConfigureForReading(cluster, attributes, endpointNames) {
const configure = async (device, coordinatorEndpoint, definition) => {
if (endpointNames) {
const definitionEndpoints = definition.endpoint(device);
const endpointIds = endpointNames.map((e) => definitionEndpoints[e]);
const endpoints = device.endpoints.filter((e) => endpointIds.includes(e.ID));
for (const endpoint of endpoints) {
await endpoint.read(cluster, attributes);
}
}
else {
const endpoints = getEndpointsWithCluster(device, cluster, "input");
for (const endpoint of endpoints) {
await endpoint.read(cluster, attributes);
}
}
};
return configure;
}
function determineEndpoint(entity, meta, cluster) {
const { device, endpoint_name } = meta;
if (endpoint_name !== undefined) {
// In case an explicit endpoint is given, always send it to that endpoint
return entity;
}
// In case no endpoint is given, match the first endpoint which support the cluster.
return device.endpoints.find((e) => e.supportsInputCluster(cluster)) ?? device.endpoints[0];
}
// #region General
function forceDeviceType(args) {
const configure = [
(device, coordinatorEndpoint, definition) => {
device.type = args.type;
device.save();
},
];
return { configure, isModernExtend: true };
}
function forcePowerSource(args) {
const configure = [
(device, coordinatorEndpoint, definition) => {
device.powerSource = args.powerSource;
device.save();
},
];
return { configure, isModernExtend: true };
}
function linkQuality(args = {}) {
const { reporting = false, attribute = "zclVersion", reportingConfig = { min: "1_HOUR", max: "4_HOURS", change: 0 } } = args;
// Exposes is empty because the application (e.g. Z2M) adds a linkquality sensor
// for every device already.
const exposes = [];
const fromZigbee = [
{
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
return { linkquality: msg.linkquality };
},
},
];
const result = { exposes, fromZigbee, isModernExtend: true };
if (reporting) {
result.configure = [setupConfigureForReporting("genBasic", attribute, { config: reportingConfig, access: exposes_1.access.GET })];
}
return result;
}
function battery(args = {}) {
const { percentage = true, voltage = false, lowStatus = false, percentageReporting = true, voltageReporting = false, dontDividePercentage = false, percentageReportingConfig = { min: "1_HOUR", max: "MAX", change: 10 }, voltageReportingConfig = { min: "1_HOUR", max: "MAX", change: 10 }, lowStatusReportingConfig = undefined, voltageToPercentage = undefined, } = args;
const exposes = [];
if (percentage) {
exposes.push(exposes_1.presets
.numeric("battery", exposes_1.access.STATE_GET)
.withUnit("%")
.withDescription("Remaining battery in %")
.withValueMin(0)
.withValueMax(100)
.withCategory("diagnostic"));
}
if (voltage) {
exposes.push(exposes_1.presets.numeric("voltage", exposes_1.access.STATE_GET).withUnit("mV").withDescription("Reported battery voltage in millivolts").withCategory("diagnostic"));
}
if (lowStatus) {
exposes.push(exposes_1.presets.binary("battery_low", exposes_1.access.STATE, true, false).withDescription("Empty battery indicator").withCategory("diagnostic"));
}
const fromZigbee = [
{
cluster: "genPowerCfg",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};
if (msg.data.batteryPercentageRemaining !== undefined && msg.data.batteryPercentageRemaining < 255) {
// Some devices do not comply to the ZCL and report a
// batteryPercentageRemaining of 100 when the battery is full (should be 200).
let percentage = msg.data.batteryPercentageRemaining;
percentage = dontDividePercentage ? percentage : percentage / 2;
if (percentage)
payload.battery = (0, utils_1.precisionRound)(percentage, 2);
}
if (msg.data.batteryVoltage !== undefined && msg.data.batteryVoltage < 255) {
// Deprecated: voltage is = mV now but should be V
if (voltage)
payload.voltage = msg.data.batteryVoltage * 100;
if (voltageToPercentage) {
payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, voltageToPercentage);
}
}
if (msg.data.batteryAlarmState !== undefined) {
const battery1Low = (msg.data.batteryAlarmState & (1 << 0) ||
msg.data.batteryAlarmState & (1 << 1) ||
msg.data.batteryAlarmState & (1 << 2) ||
msg.data.batteryAlarmState & (1 << 3)) > 0;
const battery2Low = (msg.data.batteryAlarmState & (1 << 10) ||
msg.data.batteryAlarmState & (1 << 11) ||
msg.data.batteryAlarmState & (1 << 12) ||
msg.data.batteryAlarmState & (1 << 13)) > 0;
const battery3Low = (msg.data.batteryAlarmState & (1 << 20) ||
msg.data.batteryAlarmState & (1 << 21) ||
msg.data.batteryAlarmState & (1 << 22) ||
msg.data.batteryAlarmState & (1 << 23)) > 0;
if (lowStatus)
payload.battery_low = battery1Low || battery2Low || battery3Low;
}
return payload;
},
},
];
const toZigbee = [
{
key: ["battery", "voltage"],
convertGet: async (entity, key, meta) => {
// Don't fail GET request if reading fails
// Split reading is needed for more clear debug logs
const ep = determineEndpoint(entity, meta, "genPowerCfg");
try {
await ep.read("genPowerCfg", ["batteryPercentageRemaining"]);
}
catch (e) {
logger_1.logger.debug(`Reading batteryPercentageRemaining failed: ${e}, device probably doesn't support it`, "zhc:setupattribute");
}
try {
await ep.read("genPowerCfg", ["batteryVoltage"]);
}
catch (e) {
logger_1.logger.debug(`Reading batteryVoltage failed: ${e}, device probably doesn't support it`, "zhc:setupattribute");
}
},
},
];
const result = { exposes, fromZigbee, toZigbee, configure: [], isModernExtend: true };
if (percentageReporting || voltageReporting) {
if (percentageReporting) {
result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryPercentageRemaining", {
config: percentageReportingConfig,
access: exposes_1.access.STATE_GET,
singleEndpoint: true,
}));
}
if (voltageReporting) {
result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryVoltage", {
config: voltageReportingConfig,
access: exposes_1.access.STATE_GET,
singleEndpoint: true,
}));
}
result.configure.push((0, utils_1.configureSetPowerSourceWhenUnknown)("Battery"));
}
if (voltageToPercentage || dontDividePercentage) {
const meta = { battery: {} };
if (voltageToPercentage)
meta.battery.voltageToPercentage = voltageToPercentage;
if (dontDividePercentage)
meta.battery.dontDividePercentage = dontDividePercentage;
result.meta = meta;
}
if (lowStatusReportingConfig) {
result.configure.push(setupConfigureForReporting("genPowerCfg", "batteryAlarmState", {
config: lowStatusReportingConfig,
access: exposes_1.access.STATE_GET,
singleEndpoint: true,
}));
}
return result;
}
function deviceTemperature(args = {}) {
return numeric({
name: "device_temperature",
cluster: "genDeviceTempCfg",
attribute: "currentTemperature",
reporting: { min: "5_MINUTES", max: "1_HOUR", change: 1 },
description: "Temperature of the device",
unit: "°C",
access: "STATE_GET",
entityCategory: "diagnostic",
...args,
});
}
function identify(args = { isSleepy: false }) {
const { isSleepy } = args;
const normal = exposes_1.presets.enum("identify", exposes_1.access.SET, ["identify"]).withDescription("Initiate device identification").withCategory("config");
const sleepy = exposes_1.presets
.enum("identify", exposes_1.access.SET, ["identify"])
.withDescription("Initiate device identification. This device is asleep by default." +
"You may need to wake it up first before sending the identify command.")
.withCategory("config");
const exposes = isSleepy ? [sleepy] : [normal];
const identifyTimeout = exposes_1.presets
.numeric("identify_timeout", exposes_1.access.SET)
.withDescription("Sets the duration of the identification procedure in seconds (i.e., how long the device would flash)." +
"The value ranges from 1 to 30 seconds (default: 3).")
.withValueMin(1)
.withValueMax(30);
const toZigbee = [
{
key: ["identify"],
options: [identifyTimeout],
convertSet: async (entity, key, value, meta) => {
const identifyTimeout = meta.options.identify_timeout ?? 3;
await entity.command("genIdentify", "identify", { identifytime: identifyTimeout }, (0, utils_1.getOptions)(meta.mapped, entity));
},
},
];
return { exposes, toZigbee, isModernExtend: true };
}
function onOff(args = {}) {
const { powerOnBehavior = true, skipDuplicateTransaction = false, configureReporting = true, endpointNames = undefined, description = undefined, ota = false, } = args;
const exposes = description ? (0, utils_1.exposeEndpoints)(exposes_1.presets.switch(description), endpointNames) : (0, utils_1.exposeEndpoints)(exposes_1.presets.switch(), endpointNames);
const fromZigbee = [skipDuplicateTransaction ? fz.on_off_skip_duplicate_transaction : fz.on_off];
const toZigbee = [endpointNames ? { ...tz.on_off, endpoints: endpointNames } : tz.on_off];
if (powerOnBehavior) {
exposes.push(...(0, utils_1.exposeEndpoints)(exposes_1.presets.power_on_behavior(["off", "on", "toggle", "previous"]), endpointNames));
fromZigbee.push(fz.power_on_behavior);
toZigbee.push(tz.power_on_behavior);
}
const result = { exposes, fromZigbee, toZigbee, isModernExtend: true };
if (ota)
result.ota = ota;
if (configureReporting) {
result.configure = [
async (device, coordinatorEndpoint) => {
await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "onOff", min: "MIN", max: "MAX", change: 1 }]);
if (powerOnBehavior) {
try {
// Don't fail configure if reading this attribute fails, some devices don't support it.
await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "startUpOnOff", min: "MIN", max: "MAX", change: 1 }], false);
}
catch (e) {
if (e.message.includes("UNSUPPORTED_ATTRIBUTE")) {
logger_1.logger.debug("Reading startUpOnOff failed, this features is unsupported", "zhc:onoff");
}
else {
throw e;
}
}
}
},
(0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)"),
];
}
return result;
}
function commandsOnOff(args = {}) {
const { commands = ["on", "off", "toggle"], bind = true, endpointNames = undefined } = args;
let actions = commands;
if (endpointNames) {
actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`));
}
const exposes = [exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)")];
const actionPayloadLookup = {
commandOn: "on",
commandOff: "off",
commandOffWithEffect: "off",
commandToggle: "toggle",
};
const fromZigbee = [
{
cluster: "genOnOff",
type: ["commandOn", "commandOff", "commandOffWithEffect", "commandToggle"],
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
const payload = { action: (0, utils_1.postfixWithEndpointName)(actionPayloadLookup[msg.type], msg, model, meta) };
(0, utils_1.addActionGroup)(payload, msg, model);
return payload;
},
},
];
const result = { exposes, fromZigbee, isModernExtend: true };
if (bind)
result.configure = [setupConfigureForBinding("genOnOff", "output", endpointNames)];
return result;
}
function customTimeResponse(start) {
// The Zigbee Cluster Library specification states that the genTime.time response should be the
// number of seconds since 1st Jan 2000 00:00:00 UTC. This extend modifies that:
// 1970_UTC: number of seconds since the Unix Epoch (1st Jan 1970 00:00:00 UTC)
// 2000_LOCAL: seconds since 1 January in the local time zone.
// Disable the responses of zigbee-herdsman and respond here instead.
const onEvent = [
async (type, data, device, options, state) => {
if (!device.customReadResponse) {
device.customReadResponse = (frame, endpoint) => {
if (frame.isCluster("genTime")) {
const payload = {};
if (start === "1970_UTC") {
const time = Math.round(new Date().getTime() / 1000);
payload.time = time;
payload.localTime = time - new Date().getTimezoneOffset() * 60;
}
else if (start === "2000_LOCAL") {
const oneJanuary2000 = new Date("January 01, 2000 00:00:00 UTC+00:00").getTime();
const secondsUTC = Math.round((new Date().getTime() - oneJanuary2000) / 1000);
payload.time = secondsUTC - new Date().getTimezoneOffset() * 60;
}
endpoint.readResponse("genTime", frame.header.transactionSequenceNumber, payload).catch((e) => {
logger_1.logger.warning(`Custom time response failed for '${device.ieeeAddr}': ${e}`, "zhc:customtimeresponse");
});
return true;
}
return false;
};
}
},
];
return { onEvent, isModernExtend: true };
}
// #endregion
// #region Measurement and Sensing
function illuminance(args = {}) {
const luxScale = (value, type) => {
let result = value;
if (type === "from") {
result = 10 ** ((result - 1) / 10000);
}
return result;
};
const result = numeric({
name: "illuminance",
cluster: "msIlluminanceMeasurement",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 }, // 5 lux
description: "Measured illuminance",
unit: "lx",
scale: luxScale,
access: "STATE_GET",
...args,
});
const fzIlluminanceRaw = {
cluster: "msIlluminanceMeasurement",
type: ["attributeReport", "readResponse"],
options: [exposes_1.options.illuminance_raw()],
convert: (model, msg, publish, options, meta) => {
if (options.illuminance_raw) {
return { illuminance_raw: msg.data.measuredValue };
}
},
};
result.fromZigbee.push(fzIlluminanceRaw);
const exposeIlluminanceRaw = (device, options) => {
return options?.illuminance_raw ? [exposes_1.presets.illuminance_raw()] : [];
};
result.exposes.push(exposeIlluminanceRaw);
return result;
}
function temperature(args = {}) {
return numeric({
name: "temperature",
cluster: "msTemperatureMeasurement",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 },
description: "Measured temperature value",
unit: "°C",
scale: 100,
access: "STATE_GET",
...args,
});
}
function pressure(args = {}) {
return numeric({
name: "pressure",
cluster: "msPressureMeasurement",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 50 }, // 5 kPa
description: "The measured atmospheric pressure",
unit: "kPa",
scale: 10,
access: "STATE_GET",
...args,
});
}
function flow(args = {}) {
return numeric({
name: "flow",
cluster: "msFlowMeasurement",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 10 },
description: "Measured water flow",
unit: "m³/h",
scale: 10,
access: "STATE_GET",
...args,
});
}
function humidity(args = {}) {
return numeric({
name: "humidity",
cluster: "msRelativeHumidity",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 },
description: "Measured relative humidity",
unit: "%",
scale: 100,
access: "STATE_GET",
...args,
});
}
function soilMoisture(args = {}) {
return numeric({
name: "soil_moisture",
cluster: "msSoilMoisture",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 100 },
description: "Measured soil moisture value",
unit: "%",
scale: 100,
access: "STATE_GET",
...args,
});
}
function occupancy(args = {}) {
const { reporting = true, reportingConfig = { min: "MIN", max: "1_HOUR", change: 0 }, pirConfig = undefined, ultrasonicConfig = undefined, contactConfig = undefined, endpointNames = undefined, } = args;
const templateExposes = [exposes_1.presets.occupancy().withAccess(exposes_1.access.STATE_GET)];
const exposes = endpointNames
? templateExposes.flatMap((exp) => endpointNames.map((ep) => exp.withEndpoint(ep)))
: templateExposes;
const fromZigbee = [
{
cluster: "msOccupancySensing",
type: ["attributeReport", "readResponse"],
options: [exposes_1.options.no_occupancy_since_false()],
convert: (model, msg, publish, options, meta) => {
if ("occupancy" in msg.data && (!endpointNames || endpointNames.includes((0, utils_1.getEndpointName)(msg, model, meta).toString()))) {
const propertyName = (0, utils_1.postfixWithEndpointName)("occupancy", msg, model, meta);
const payload = { [propertyName]: (msg.data.occupancy & 1) > 0 };
(0, utils_1.noOccupancySince)(msg.endpoint, options, publish, payload[propertyName] ? "stop" : "start");
return payload;
}
},
},
];
const toZigbee = [
{
key: ["occupancy"],
convertGet: async (entity, key, meta) => {
await determineEndpoint(entity, meta, "msOccupancySensing").read("msOccupancySensing", ["occupancy"]);
},
},
];
const settingsExtends = [];
const settingsTemplate = {
cluster: "msOccupancySensing",
description: "",
endpointNames: endpointNames,
access: "ALL",
entityCategory: "config",
};
const attributesForReading = [];
if (pirConfig) {
if (pirConfig.includes("otu_delay")) {
settingsExtends.push(numeric({
name: "occupancy_timeout",
attribute: "pirOToUDelay",
valueMin: 0,
valueMax: 65534,
unit: "s",
...settingsTemplate,
description: "Time in seconds before occupancy is cleared after the last detected movement.",
}));
attributesForReading.push("pirOToUDelay");
}
if (pirConfig.includes("uto_delay")) {
settingsExtends.push(numeric({
name: "pir_uto_delay",
attribute: "pirUToODelay",
valueMin: 0,
valueMax: 65534,
...settingsTemplate,
}));
attributesForReading.push("pirUToODelay");
}
if (pirConfig.includes("uto_threshold")) {
settingsExtends.push(numeric({
name: "pir_uto_threshold",
attribute: "pirUToOThreshold",
valueMin: 1,
valueMax: 254,
...settingsTemplate,
}));
attributesForReading.push("pirUToOThreshold");
}
}
if (ultrasonicConfig) {
if (pirConfig.includes("otu_delay")) {
settingsExtends.push(numeric({
name: "ultrasonic_otu_delay",
attribute: "ultrasonicOToUDelay",
valueMin: 0,
valueMax: 65534,
...settingsTemplate,
}));
attributesForReading.push("ultrasonicOToUDelay");
}
if (pirConfig.includes("uto_delay")) {
settingsExtends.push(numeric({
name: "ultrasonic_uto_delay",
attribute: "ultrasonicUToODelay",
valueMin: 0,
valueMax: 65534,
...settingsTemplate,
}));
attributesForReading.push("ultrasonicUToODelay");
}
if (pirConfig.includes("uto_threshold")) {
settingsExtends.push(numeric({
name: "ultrasonic_uto_threshold",
attribute: "ultrasonicUToOThreshold",
valueMin: 1,
valueMax: 254,
...settingsTemplate,
}));
attributesForReading.push("ultrasonicUToOThreshold");
}
}
if (contactConfig) {
if (pirConfig.includes("otu_delay")) {
settingsExtends.push(numeric({
name: "contact_otu_delay",
attribute: "contactOToUDelay",
valueMin: 0,
valueMax: 65534,
...settingsTemplate,
}));
attributesForReading.push("contactOToUDelay");
}
if (pirConfig.includes("uto_delay")) {
settingsExtends.push(numeric({
name: "contact_uto_delay",
attribute: "contactUToODelay",
valueMin: 0,
valueMax: 65534,
...settingsTemplate,
}));
attributesForReading.push("contactUToODelay");
}
if (pirConfig.includes("uto_threshold")) {
settingsExtends.push(numeric({
name: "contact_uto_threshold",
attribute: "contactUToOThreshold",
valueMin: 1,
valueMax: 254,
...settingsTemplate,
}));
attributesForReading.push("contactUToOThreshold");
}
}
settingsExtends.map((extend) => exposes.push(...extend.exposes));
settingsExtends.map((extend) => fromZigbee.push(...extend.fromZigbee));
settingsExtends.map((extend) => toZigbee.push(...extend.toZigbee));
const configure = [];
if (attributesForReading.length > 0)
configure.push(setupConfigureForReading("msOccupancySensing", attributesForReading, endpointNames));
if (reporting) {
configure.push(setupConfigureForReporting("msOccupancySensing", "occupancy", {
config: reportingConfig,
access: exposes_1.access.STATE_GET,
endpointNames: endpointNames,
}));
}
return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
}
function co2(args = {}) {
return numeric({
name: "co2",
cluster: "msCO2",
label: "CO2",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 0.00005 }, // 50 ppm change
description: "Measured value",
unit: "ppm",
scale: 0.000001,
access: "STATE_GET",
...args,
});
}
function pm25(args = {}) {
return numeric({
name: "pm25",
cluster: "pm25Measurement",
attribute: "measuredValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
description: "Measured PM2.5 (particulate matter) concentration",
unit: "µg/m³",
access: "STATE_GET",
...args,
});
}
function light(args = {}) {
const { effect = true, powerOnBehavior = true, configureReporting = false, ota = false, color = undefined, levelConfig = undefined, turnsOffAtBrightness1 = false, endpointNames = undefined, levelReportingConfig = undefined, } = args;
let { colorTemp = undefined } = args;
if (colorTemp) {
colorTemp = { startup: true, ...colorTemp };
}
const argsColor = color
? {
modes: ["xy"],
applyRedFix: false,
enhancedHue: true,
...((0, utils_1.isObject)(color) ? color : {}),
}
: false;
const lightExpose = (0, utils_1.exposeEndpoints)(exposes_1.presets.light().withBrightness(), endpointNames);
const fromZigbee = [fz.on_off, fz.brightness, fz.ignore_basic_report, fz.level_config];
const toZigbee = [
endpointNames ? { ...tz.light_onoff_brightness, endpoints: endpointNames } : tz.light_onoff_brightness,
tz.ignore_transition,
tz.level_config,
tz.ignore_rate,
tz.light_brightness_move,
tz.light_brightness_step,
];
const meta = {};
if (colorTemp || argsColor) {
fromZigbee.push(fz.color_colortemp);
if (colorTemp && argsColor)
toZigbee.push(tz.light_color_colortemp);
else if (colorTemp)
toZigbee.push(tz.light_colortemp);
else if (argsColor)
toZigbee.push(tz.light_color);
toZigbee.push(tz.light_color_mode, tz.light_color_options);
}
if (colorTemp) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
lightExpose.forEach((e) => e.withColorTemp(colorTemp.range));
toZigbee.push(tz.light_colortemp_move, tz.light_colortemp_step);
if (colorTemp.startup) {
toZigbee.push(tz.light_colortemp_startup);
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
lightExpose.forEach((e) => e.withColorTempStartup(colorTemp.range));
}
}
if (argsColor) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
lightExpose.forEach((e) => e.withColor(argsColor.modes));
toZigbee.push(tz.light_hue_saturation_move, tz.light_hue_saturation_step);
if (argsColor.modes.includes("hs")) {
meta.supportsHueAndSaturation = true;
}
if (argsColor.applyRedFix) {
meta.applyRedFix = true;
}
if (!argsColor.enhancedHue) {
meta.supportsEnhancedHue = false;
}
}
if (levelConfig) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
lightExpose.forEach((e) => (levelConfig.features ? e.withLevelConfig(levelConfig.features) : e.withLevelConfig()));
toZigbee.push(tz.level_config);
}
const exposes = lightExpose;
if (effect) {
const effects = exposes_1.presets.effect();
if (color) {
effects.values.push("colorloop", "stop_colorloop");
}
exposes.push(...(0, utils_1.exposeEndpoints)(effects, endpointNames));
toZigbee.push(tz.effect);
}
if (powerOnBehavior) {
exposes.push(...(0, utils_1.exposeEndpoints)(exposes_1.presets.power_on_behavior(["off", "on", "toggle", "previous"]), endpointNames));
fromZigbee.push(fz.power_on_behavior);
toZigbee.push(tz.power_on_behavior);
}
if (turnsOffAtBrightness1) {
meta.turnsOffAtBrightness1 = turnsOffAtBrightness1;
}
const configure = [
async (device, coordinatorEndpoint, definition) => {
await (0, light_1.configure)(device, coordinatorEndpoint, true);
if (configureReporting) {
await setupAttributes(device, coordinatorEndpoint, "genOnOff", [{ attribute: "onOff", min: "MIN", max: "MAX", change: 1 }]);
await setupAttributes(device, coordinatorEndpoint, "genLevelCtrl", [
{ attribute: "currentLevel", min: "5_SECONDS", max: "MAX", change: 1, ...(levelReportingConfig || {}) },
]);
if (colorTemp) {
await setupAttributes(device, coordinatorEndpoint, "lightingColorCtrl", [
{ attribute: "colorTemperature", min: "10_SECONDS", max: "MAX", change: 1 },
]);
}
if (argsColor) {
const attributes = [];
if (argsColor.modes.includes("xy")) {
attributes.push({ attribute: "currentX", min: "10_SECONDS", max: "MAX", change: 1 }, { attribute: "currentY", min: "10_SECONDS", max: "MAX", change: 1 });
}
if (argsColor.modes.includes("hs")) {
attributes.push({ attribute: argsColor.enhancedHue ? "enhancedCurrentHue" : "currentHue", min: "10_SECONDS", max: "MAX", change: 1 }, { attribute: "currentSaturation", min: "10_SECONDS", max: "MAX", change: 1 });
}
await setupAttributes(device, coordinatorEndpoint, "lightingColorCtrl", attributes);
}
}
},
(0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)"),
];
const result = { exposes, fromZigbee, toZigbee, configure, meta, isModernExtend: true };
if (ota)
result.ota = ota;
return result;
}
function commandsLevelCtrl(args = {}) {
const { commands = [
"brightness_move_to_level",
"brightness_move_up",
"brightness_move_down",
"brightness_step_up",
"brightness_step_down",
"brightness_stop",
], bind = true, endpointNames = undefined, } = args;
let actions = commands;
if (endpointNames) {
actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`));
}
const exposes = [
exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)").withCategory("diagnostic"),
];
const fromZigbee = [fz.command_move_to_level, fz.command_move, fz.command_step, fz.command_stop];
const result = { exposes, fromZigbee, isModernExtend: true };
if (bind)
result.configure = [setupConfigureForBinding("genLevelCtrl", "output", endpointNames)];
return result;
}
function commandsColorCtrl(args = {}) {
const { commands = [
"color_temperature_move_stop",
"color_temperature_move_up",
"color_temperature_move_down",
"color_temperature_step_up",
"color_temperature_step_down",
"enhanced_move_to_hue_and_saturation",
"move_to_hue_and_saturation",
"color_hue_step_up",
"color_hue_step_down",
"color_saturation_step_up",
"color_saturation_step_down",
"color_loop_set",
"color_temperature_move",
"color_move",
"hue_move",
"hue_stop",
"move_to_saturation",
"move_to_hue",
], bind = true, endpointNames = undefined, } = args;
let actions = commands;
if (endpointNames) {
actions = commands.flatMap((c) => endpointNames.map((e) => `${c}_${e}`));
}
const exposes = [
exposes_1.presets.enum("action", exposes_1.access.STATE, actions).withDescription("Triggered action (e.g. a button click)").withCategory("diagnostic"),
];
const fromZigbee = [
fz.command_move_color_temperature,
fz.command_step_color_temperature,
fz.command_enhanced_move_to_hue_and_saturation,
fz.command_move_to_hue_and_saturation,
fz.command_step_hue,
fz.command_step_saturation,
fz.command_color_loop_set,
fz.command_move_to_color_temp,
fz.command_move_to_color,
fz.command_move_hue,
fz.command_move_to_saturation,
fz.command_move_to_hue,
];
const result = { exposes, fromZigbee, isModernExtend: true };
if (bind)
result.configure = [setupConfigureForBinding("lightingColorCtrl", "output", endpointNames)];
return result;
}
function lightingBallast() {
const result = {
fromZigbee: [fz.lighting_ballast_configuration],
toZigbee: [tz.ballast_config],
exposes: [
new exposes_1.Numeric("ballast_minimum_level", exposes_1.access.ALL)
.withValueMin(1)
.withValueMax(254)
.withDescription("Specifies the minimum light output of the ballast"),
new exposes_1.Numeric("ballast_maximum_level", exposes_1.access.ALL)
.withValueMin(1)
.withValueMax(254)
.withDescription("Specifies the maximum light output of the ballast"),
],
configure: [setupConfigureForReading("lightingBallastCfg", ["minLevel", "maxLevel"])],
isModernExtend: true,
};
return result;
}
function lock(args) {
const { endpointNames = undefined, pinCodeCount } = args;
const fromZigbee = [fz.lock, fz.lock_operation_event, fz.lock_programming_event, fz.lock_pin_code_response, fz.lock_user_status_response];
const toZigbee = [{ ...tz.lock, endpoints: endpointNames }, tz.pincode_lock, tz.lock_userstatus, tz.lock_auto_relock_time, tz.lock_sound_volume];
const exposes = [
exposes_1.presets.lock(),
exposes_1.presets.pincode(),
exposes_1.presets.lock_action(),
exposes_1.presets.lock_action_source_name(),
exposes_1.presets.lock_action_user(),
exposes_1.presets.auto_relock_time().withValueMin(0).withValueMax(3600),
exposes_1.presets.sound_volume(),
];
const configure = [
setupConfigureForReporting("closuresDoorLock", "lockState", { config: { min: "MIN", max: "1_HOUR", change: 0 }, access: exposes_1.access.STATE_GET }),
];
const meta = { pinCodeCount: pinCodeCount };
const result = { fromZigbee, toZigbee, exposes, configure, meta, isModernExtend: true };
if (endpointNames) {
result.exposes = (0, utils_1.flatten)(exposes.map((expose) => endpointNames.map((endpoint) => expose.clone().withEndpoint(endpoint))));
}
return result;
}
function windowCovering(args) {
const { stateSource = "lift", configureReporting = true, controls, coverInverted = false, coverMode, endpointNames = undefined } = args;
let coverExpose = exposes_1.presets.cover();
if (controls.includes("lift"))
coverExpose = coverExpose.withPosition();
if (controls.includes("tilt"))
coverExpose = coverExpose.withTilt();
const exposes = [coverExpose];
const fromZigbee = [fz.cover_position_tilt];
const toZigbee = [{ ...tz.cover_state, endpoints: endpointNames }, tz.cover_position_tilt];
const result = { exposes, fromZigbee, toZigbee, isModernExtend: true };
if (configureReporting) {
const configure = [];
if (controls.includes("lift")) {
configure.push(setupConfigureForReporting("closuresWindowCovering", "currentPositionLiftPercentage", {
config: { min: "1_SECOND", max: "MAX", change: 1 },
access: exposes_1.access.STATE_GET,
}));
}
if (controls.includes("til