UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

883 lines 269 kB
"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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.definitions = void 0; const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const constants = __importStar(require("../lib/constants")); const ewelink_1 = require("../lib/ewelink"); const exposes = __importStar(require("../lib/exposes")); const logger_1 = require("../lib/logger"); const m = __importStar(require("../lib/modernExtend")); const reporting = __importStar(require("../lib/reporting")); const sonoff_1 = require("../lib/sonoff"); const tuya = __importStar(require("../lib/tuya")); const utils = __importStar(require("../lib/utils")); const { ewelinkAction, ewelinkBattery } = ewelink_1.modernExtend; const NS = "zhc:sonoff"; const manufacturerOptions = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_COOLKIT_TECHNOLOGY_CO_LTD, disableDefaultResponse: false, }; const defaultResponseOptions = { disableDefaultResponse: false }; const e = exposes.presets; const ea = exposes.access; // SWV-ZN/ZF history request cache const swvzfReqCache = {}; // SWV-ZN/ZF 30-day history multi-package merge const swvzfRespCache = {}; // SWV-ZN/ZF multi-package merge cache expiration time const swvzfCacheExpireTime = 5 * 1000; // 5s // Build a fromZigbee converter for attributes reported as big-endian 32-bit integers. const bigEndianNumericFzConvert = (name, attributeKey) => { return (model, msg, publish, options, meta) => { if (!(attributeKey in msg.data)) { return; } const rawValue = msg.data[attributeKey]; utils.assertNumber(rawValue); return { [name]: (((rawValue & 0xff) << 24) | ((rawValue & 0xff00) << 8) | ((rawValue >>> 8) & 0xff00) | ((rawValue >>> 24) & 0xff)) >>> 0, }; }; }; // **************************** SWV-ZN/ZF related ↑ **************************** const fzLocal = { key_action_event: { cluster: "customSonoffSnzb01m", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if ("keyActionEvent" in msg.data) { const event = utils.getFromLookup(msg.data.keyActionEvent, { 1: "single", 2: "double", 3: "long", 4: "triple" }); return { action: `${event}_button_${msg.endpoint.ID}` }; } }, }, router_config: { cluster: "genLevelCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.currentLevel !== undefined) { result.light_indicator_level = msg.data.currentLevel; } }, }, on_off_clear_electricity: { cluster: "genOnOff", type: ["attributeReport", "readResponse"], options: [exposes.options.state_action()], convert: (model, msg, publish, options, meta) => { // Device keeps reporting a acCurrentPowerValue after turning OFF. // Make sure power = 0 when turned OFF // https://github.com/Koenkk/zigbee2mqtt/issues/28470 let result = fz.on_off.convert(model, msg, publish, options, meta); if (msg.data.onOff === 0) { result = { ...result, power: 0, current: 0 }; } return result; }, }, }; const tzLocal = { on_off_fixed_on_time: { ...tz.on_off, convertSet: async (entity, key, value, meta) => { // https://github.com/Koenkk/zigbee2mqtt/issues/27980 const localMeta = meta; if (localMeta.message.on_time != null) { utils.assertNumber(localMeta.message.on_time, "on_time"); localMeta.message = { ...localMeta.message, on_time: localMeta.message.on_time / 10 }; } return await tz.on_off.convertSet(entity, key, value, localMeta); }, }, }; const sonoffExtend = { addCustomClusterEwelink: () => { return m.deviceAddCustomCluster("customClusterEwelink", { name: "customClusterEwelink", ID: 0xfc11, attributes: { networkLed: { name: "networkLed", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true }, backLight: { name: "backLight", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true }, faultCode: { name: "faultCode", ID: 0x0010, type: zigbee_herdsman_1.Zcl.DataType.INT32, write: true, min: -2147483648 }, radioPower: { name: "radioPower", ID: 0x0012, type: zigbee_herdsman_1.Zcl.DataType.INT16, write: true, min: -32768 }, radioPowerWithManuCode: { name: "radioPowerWithManuCode", ID: 0x0012, type: zigbee_herdsman_1.Zcl.DataType.INT16, manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_COOLKIT_TECHNOLOGY_CO_LTD, write: true, min: -32768, }, delayedPowerOnState: { name: "delayedPowerOnState", ID: 0x0014, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true }, delayedPowerOnTime: { name: "delayedPowerOnTime", ID: 0x0015, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff }, externalTriggerMode: { name: "externalTriggerMode", ID: 0x0016, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, detachRelayMode: { name: "detachRelayMode", ID: 0x0017, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true }, deviceWorkMode: { name: "deviceWorkMode", ID: 0x0018, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, detachRelayMode2: { name: "detachRelayMode2", ID: 0x0019, type: zigbee_herdsman_1.Zcl.DataType.BITMAP8, write: true }, motorTravelCalibrationAction: { name: "motorTravelCalibrationAction", ID: 0x5001, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, lackWaterCloseValveTimeout: { name: "lackWaterCloseValveTimeout", ID: 0x5011, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff }, motorTravelCalibrationStatus: { name: "motorTravelCalibrationStatus", ID: 0x5012, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, motorRunStatus: { name: "motorRunStatus", ID: 0x5013, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, acCurrentCurrentValue: { name: "acCurrentCurrentValue", ID: 0x7004, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, acCurrentVoltageValue: { name: "acCurrentVoltageValue", ID: 0x7005, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, acCurrentPowerValue: { name: "acCurrentPowerValue", ID: 0x7006, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, outlet_control_protect: { name: "outlet_control_protect", ID: 0x7007, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, energyToday: { name: "energyToday", ID: 0x7009, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, energyMonth: { name: "energyMonth", ID: 0x700a, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, energyYesterday: { name: "energyYesterday", ID: 0x700b, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, setCalibrationAction: { name: "setCalibrationAction", ID: 0x001d, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true }, calibrationStatus: { name: "calibrationStatus", ID: 0x001e, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, calibrationProgress: { name: "calibrationProgress", ID: 0x0020, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, minBrightnessThreshold: { name: "minBrightnessThreshold", ID: 0x4001, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, dimmingLightRate: { name: "dimmingLightRate", ID: 0x4003, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff }, transitionTime: { name: "transitionTime", ID: 0x001f, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff }, programmableStepperSequence: { name: "programmableStepperSequence", ID: 0x0022, type: zigbee_herdsman_1.Zcl.DataType.ARRAY, write: true }, }, commands: { protocolData: { name: "protocolData", ID: 0x01, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.LIST_UINT8 }] }, }, commandsResponse: {}, }); }, programmableStepperSequence(sequences) { const stepComposite = (n) => { return e .composite(`step_${n}`, `step_${n}`, ea.ALL) .withFeature(e.binary("enable_step", ea.ALL, true, false).withDescription("Enable/disable this step.")) .withFeature(e.binary("relay_outlet_1", ea.ALL, true, false).withDescription("Outlet 1 relay state.")) .withFeature(e.binary("relay_outlet_2", ea.ALL, true, false).withDescription("Outlet 2 relay state.")); }; const exposes = sequences.map((seq) => { return e .composite(`programmable_stepper_seq${seq}`, `programmable_stepper_seq${seq}`, ea.ALL) .withDescription(`Configure programmable stepper sequence ${seq}.`) .withFeature(e.binary("enable_stepper", ea.ALL, true, false).withDescription("Enable/disable the stepper sequence.")) .withFeature(e .numeric("switch_outlet", ea.ALL) .withValueMin(1) .withValueMax(2) .withValueStep(1) .withDescription("The outlet channel of the external trigger switch bound to this sequence.")) .withFeature(e.binary("enable_double_press", ea.ALL, true, false).withDescription("Enable/disable double press to switch steps.")) .withFeature(e .numeric("double_press_interval", ea.ALL) .withValueMin(0) .withValueMax(32767) .withValueStep(1) .withUnit("ms") .withDescription("Set the double press interval for step switching.")) .withFeature(stepComposite(1)) .withFeature(stepComposite(2)) .withFeature(stepComposite(3)) .withFeature(stepComposite(4)); }); const toZigbee = [ { key: [...sequences.map((seq) => `programmable_stepper_seq${seq}`)], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const array = new Uint8Array(11); // ZCL Array array[0] = 0x01; array[1] = 9; array[2] = 1; // Sequence configs const seqStr = key.replace("programmable_stepper_seq", ""); const seqIndex = Number.parseInt(seqStr, 10) - 1; array[3] = (value.enable_stepper ? 0x80 : 0x00) | (seqIndex & 0x7f); array[4] = (value.switch_outlet - 1) & 0xff; array[5] = (value.enable_double_press ? 0x80 : 0x00) | ((value.double_press_interval >> 8) & 0x7f); array[6] = value.double_press_interval & 0xff; // Steps for (let i = 0; i < 4; i++) { const step = value[`step_${i + 1}`] ?? {}; array[7 + i] = (step.enable_step ? 0x80 : 0x00) | (step.relay_outlet_1 ? 0x01 : 0x00) | (step.relay_outlet_2 ? 0x02 : 0x00); } await entity.write("customClusterEwelink", { [0x0022]: { value: { elementType: 0x20, elements: array, }, type: 0x48, }, }, utils.getOptions(meta.mapped, entity)); return { state: { [key]: value, }, }; }, }, ]; const fromZigbee = [ { cluster: "customClusterEwelink", type: ["attributeReport"], convert: (model, msg) => { if (!msg.data?.programmableStepperSequence) { return; } const array = new Uint8Array(msg.data.programmableStepperSequence); if (array[0] !== 0x01) { return; } const seqCount = array[2]; const seqDataOffset = 3; const result = {}; for (let i = 0; i < seqCount; i++) { const offset = seqDataOffset + i * 8; // Steps const steps = {}; for (let j = 0; j < 4; j++) { const currentBuffer = array[offset + 4 + j]; steps[`step_${j + 1}`] = { enable_step: !!(currentBuffer & 0x80), relay_outlet_1: !!(currentBuffer & 0x01), relay_outlet_2: !!(currentBuffer & 0x02), }; } // Sequence configs const seqNum = (array[offset] & 0x7f) + 1; result[`programmable_stepper_seq${seqNum}`] = { enable_stepper: !!(array[offset] & 0x80), switch_outlet: array[offset + 1] + 1, enable_double_press: !!(array[offset + 2] & 0x80), double_press_interval: ((array[offset + 2] & 0x7f) << 8) | array[offset + 3], ...steps, }; } return result; }, }, ]; return { exposes, fromZigbee, toZigbee, isModernExtend: true, }; }, inchingControlSet: (args = {}, maxTime = 3599.5) => { const { endpointNames = undefined } = args; const clusterName = "customClusterEwelink"; const commandName = "protocolData"; const exposes = utils.exposeEndpoints(e .composite("inching_control_set", "inching_control_set", ea.SET) .withDescription("Device Inching function Settings. The device will automatically turn off (turn on) " + "after each turn on (turn off) for a specified period of time.") .withFeature(e.binary("inching_control", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable inching function.")) .withFeature(e .numeric("inching_time", ea.SET) .withDescription("Delay time for executing a inching action.") .withUnit("seconds") .withValueMin(0.5) .withValueMax(maxTime) .withValueStep(0.5)) .withFeature(e.binary("inching_mode", ea.SET, "ON", "OFF").withDescription("Set inching off or inching on mode.").withValueToggle("ON")), endpointNames); const toZigbee = [ { key: ["inching_control_set"], convertSet: async (entity, key, value, meta) => { let inchingControl = "inching_control"; let inchingTime = "inching_time"; let inchingMode = "inching_mode"; if (meta.endpoint_name) { inchingControl = `inching_control_${meta.endpoint_name}`; inchingTime = `inching_time_${meta.endpoint_name}`; inchingMode = `inching_mode_${meta.endpoint_name}`; } const tmpTime = Number(Math.round(Number((value[inchingTime] * 2).toFixed(1))).toFixed(1)); const payloadValue = []; payloadValue[0] = 0x01; // Cmd payloadValue[1] = 0x17; // SubCmd payloadValue[2] = 0x07; // Length payloadValue[3] = 0x80; // SeqNum payloadValue[4] = 0x00; // Mode if (value[inchingControl] !== "DISABLE") { payloadValue[4] |= 0x80; } if (value[inchingMode] !== "OFF") { payloadValue[4] |= 0x01; } if (meta.endpoint_name === "l2") { payloadValue[5] = 0x01; // Channel 2 } else { payloadValue[5] = 0x00; // Channel 1 } payloadValue[6] = tmpTime & 0xff; // Timeout payloadValue[7] = (tmpTime >> 8) & 0xff; payloadValue[8] = 0x00; // Reserve payloadValue[9] = 0x00; payloadValue[10] = 0x00; // CheckCode for (let i = 0; i < payloadValue[2] + 3; i++) { payloadValue[10] ^= payloadValue[i]; } await entity.command(clusterName, commandName, { data: payloadValue }, { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_COOLKIT_TECHNOLOGY_CO_LTD }); return { state: { [key]: value } }; }, }, ]; return { exposes: exposes, fromZigbee: [], toZigbee, isModernExtend: true, }; }, weeklySchedule: () => { const scheduleDescription = 'The preset heating schedule to use when the system mode is set to "auto" (indicated with ⏲ on the TRV). ' + "Up to 6 transitions can be defined per day, where a transition is expressed in the format 'HH:mm/temperature', each " + "separated by a space. The first transition for each day must start at 00:00 and the valid temperature range is 4-35°C " + "(in 0.5°C steps). The temperature will be set at the time of the first transition until the time of the next transition, " + "e.g. '04:00/20 10:00/25' will result in the temperature being set to 20°C at 04:00 until 10:00, when it will change to 25°C."; const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; const exposes = days.map((day) => e.text(`weekly_schedule_${day}`, ea.STATE_SET).withCategory("config").withDescription(scheduleDescription)); const fromZigbee = [ { cluster: "hvacThermostat", type: ["commandGetWeeklyScheduleRsp"], convert: (model, msg, publish, options, meta) => { const day = Object.entries(constants.thermostatDayOfWeek).find((d) => msg.data.dayofweek & (1 << +d[0]))[1]; const transitions = msg.data.transitions // TODO: heatSetpoint is optional, that possibly affects the return .map((t) => { const totalMinutes = t.transitionTime; const hours = totalMinutes / 60; const rHours = Math.floor(hours); const minutes = (hours - rHours) * 60; const rMinutes = Math.round(minutes); const strHours = rHours.toString().padStart(2, "0"); const strMinutes = rMinutes.toString().padStart(2, "0"); return `${strHours}:${strMinutes}/${t.heatSetpoint / 100}`; }) .sort() .join(" "); return { [`weekly_schedule_${day}`]: transitions, }; }, }, ]; // Helper function to parse and validate a schedule string const parseScheduleString = (scheduleValue, dayName) => { // Transition format: HH:mm/temperature const transitionRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])\/(\d+(\.5)?)$/; const rawTransitions = scheduleValue.split(" ").sort(); if (rawTransitions.length > 6) { throw new Error(`Invalid schedule for ${dayName}: days must have no more than 6 transitions`); } const transitions = []; for (const transition of rawTransitions) { const matches = transition.match(transitionRegex); if (!matches) { throw new Error(`Invalid schedule for ${dayName}: transitions must be in format HH:mm/temperature (e.g. 12:00/15.5), found: ${transition}`); } const hour = Number.parseInt(matches[1], 10); const mins = Number.parseInt(matches[2], 10); const temp = Number.parseFloat(matches[3]); if (temp < 4 || temp > 35) { throw new Error(`Invalid schedule for ${dayName}: temperature value must be between 4-35 (inclusive), found: ${temp}`); } transitions.push({ transitionTime: hour * 60 + mins, heatSetpoint: Math.round(temp * 100), }); } if (transitions[0].transitionTime !== 0) { throw new Error(`Invalid schedule for ${dayName}: the first transition of each day should start at 00:00`); } return { numoftrans: rawTransitions.length, transitions, }; }; // Helper function to get day bit from day name const getDayBit = (dayName) => { const dayKey = utils.getKey(constants.thermostatDayOfWeek, dayName, null); if (dayKey === null) { throw new Error(`Invalid schedule: invalid day name, found: ${dayName}`); } return Number(dayKey); }; // Helper function to send setWeeklySchedule command const sendScheduleCommand = async (entity, dayofweek, numoftrans, transitions, meta) => { await entity.command("hvacThermostat", "setWeeklySchedule", { dayofweek, numoftrans, mode: 1 << 0, // heat transitions, }, utils.getOptions(meta.mapped, entity)); }; const toZigbee = [ // Single/multi day converter with batching support { key: days.map((day) => `weekly_schedule_${day}`), convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); // Extract all weekly_schedule keys from the message (if message exists) const message = meta.message; const scheduleKeys = message ? Object.keys(message).filter((k) => k.startsWith("weekly_schedule_") && days.includes(k.replace("weekly_schedule_", ""))) : []; // For single-key messages or when message is not available, process normally (original behavior) if (scheduleKeys.length <= 1) { const dayName = key.replace("weekly_schedule_", ""); const dayBit = getDayBit(dayName); const parsed = parseScheduleString(value, dayName); await sendScheduleCommand(entity, 1 << dayBit, parsed.numoftrans, parsed.transitions, meta); return { state: { [key]: value } }; } // Process all schedule keys from the message with batching // Group days by their schedule string to optimize Zigbee commands const scheduleGroups = new Map(); for (const scheduleKey of scheduleKeys) { const dayName = scheduleKey.replace("weekly_schedule_", ""); const schedule = message[scheduleKey]; utils.assertString(schedule, scheduleKey); const existing = scheduleGroups.get(schedule); if (existing) { existing.push(dayName); } else { scheduleGroups.set(schedule, [dayName]); } } const stateUpdates = {}; // Send one command per unique schedule, combining days with identical schedules for (const [schedule, daysWithSchedule] of scheduleGroups) { // Parse and validate the schedule (only need to do once per unique schedule) const parsed = parseScheduleString(schedule, daysWithSchedule.join(", ")); // Build dayofweek bitmask for all days with this schedule let dayofweek = 0; for (const dayName of daysWithSchedule) { dayofweek |= 1 << getDayBit(dayName); stateUpdates[`weekly_schedule_${dayName}`] = schedule; } await sendScheduleCommand(entity, dayofweek, parsed.numoftrans, parsed.transitions, meta); } return { state: stateUpdates }; }, }, ]; return { exposes, fromZigbee, toZigbee, isModernExtend: true, }; }, cyclicTimedIrrigation: () => { const exposes = e .composite("cyclic_timed_irrigation", "cyclic_timed_irrigation", ea.ALL) .withDescription("Smart water valve cycle timing irrigation") .withFeature(e.numeric("current_count", ea.STATE).withDescription("Number of times it has been executed").withUnit("times")) .withFeature(e .numeric("total_number", ea.STATE_SET) .withDescription("Total times of circulating irrigation") .withUnit("times") .withValueMin(0) .withValueMax(100)) .withFeature(e .numeric("irrigation_duration", ea.STATE_SET) .withDescription("Single irrigation duration") .withUnit("seconds") .withValueMin(0) .withValueMax(86400)) .withFeature(e .numeric("irrigation_interval", ea.STATE_SET) .withDescription("Time interval between two adjacent irrigation") .withUnit("seconds") .withValueMin(0) .withValueMax(86400)); const fromZigbee = [ { cluster: "customClusterEwelink", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const attributeKey = 0x5008; // attr if (attributeKey in msg.data) { // logger.debug(` from zigbee 0x5008 cluster ${msg.data[attributeKey]} `, NS); // logger.debug(msg.data[attributeKey]); //logger.debug(`meta.rawData details:`, NS); //logger.debug(` - Hex: ${msg.meta.rawData.toString('hex')}`, NS); const rawData = msg.meta.rawData; /*eg:raw data: 082b0a0850420a0101000000ef00000064*/ /*zcl frame: 082b0a attrid: 0850 data type :42 data payload:0a0101000000ef00000064*/ /*0a:data len 01:currentCount 01:totalNumber 00 00 00 ef:irrigationDurationBuffer 00 00 00 64:irrigationIntervalBuffer*/ const dataStartIndex = 7; /*data payload start index*/ //logger.debug(`rawData====> ${rawData[0+dataStartIndex]} ${rawData[1+dataStartIndex]} ${rawData[2+dataStartIndex]} ${rawData[3+dataStartIndex]} ${rawData[4+dataStartIndex]} ${rawData[5+dataStartIndex]} `, NS); //logger.debug(`rawData====> ${rawData[6+dataStartIndex]} ${rawData[7+dataStartIndex]} ${rawData[8+dataStartIndex]} ${rawData[9+dataStartIndex]} `, NS); const currentCountBuffer = rawData.readUInt8(0 + dataStartIndex); const totalNumberBuffer = rawData.readUInt8(1 + dataStartIndex); const irrigationDurationBuffer = rawData.readUInt32BE(2 + dataStartIndex); const irrigationIntervalBuffer = rawData.readUInt32BE(6 + dataStartIndex); //logger.debug(`currentCountBuffer ${currentCountBuffer}`, NS); //logger.debug(`totalNumberOfTimesBuffer ${totalNumberBuffer}`, NS); //logger.debug(`irrigationDurationBuffer ${irrigationDurationBuffer}`, NS); //logger.debug(`irrigationIntervalBuffer ${irrigationIntervalBuffer}`, NS); return { cyclic_timed_irrigation: { current_count: currentCountBuffer, total_number: totalNumberBuffer, irrigation_duration: irrigationDurationBuffer, irrigation_interval: irrigationIntervalBuffer, }, }; } }, }, ]; const toZigbee = [ { key: ["cyclic_timed_irrigation"], convertSet: async (entity, key, value, meta) => { // logger.debug(`to zigbee cyclic_timed_irrigation ${key}`, NS); // const currentCount:string = 'current_count'; // logger.debug(`to zigbee cyclic_timed_irrigation ${value[currentCount as keyof typeof value]}`, NS); const totalNumber = "total_number"; // logger.debug(`to zigbee cyclic_timed_irrigation ${value[totalNumber as keyof typeof value]}`, NS); const irrigationDuration = "irrigation_duration"; // logger.debug(`to zigbee cyclic_timed_irrigation ${value[irrigationDuration as keyof typeof value]}`, NS); const irrigationInterval = "irrigation_interval"; // logger.debug(`to zigbee cyclic_timed_irrigation ${value[irrigationInterval as keyof typeof value]}`, NS); // const payloadValue = []; const payloadValue = new Uint8Array(11); payloadValue[0] = 0x0a; payloadValue[1] = 0x00; payloadValue[2] = value[totalNumber] & 0xff; payloadValue[3] = (value[irrigationDuration] >> 24) & 0xff; payloadValue[4] = (value[irrigationDuration] >> 16) & 0xff; payloadValue[5] = (value[irrigationDuration] >> 8) & 0xff; payloadValue[6] = value[irrigationDuration] & 0xff; payloadValue[7] = (value[irrigationInterval] >> 24) & 0xff; payloadValue[8] = (value[irrigationInterval] >> 16) & 0xff; payloadValue[9] = (value[irrigationInterval] >> 8) & 0xff; payloadValue[10] = value[irrigationInterval] & 0xff; const payload = { [0x5008]: { value: payloadValue, type: 0x42 } }; await entity.write("customClusterEwelink", payload, defaultResponseOptions); return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { await entity.read("customClusterEwelink", [0x5008], defaultResponseOptions); }, }, ]; return { exposes: [exposes], fromZigbee, toZigbee, isModernExtend: true, }; }, cyclicQuantitativeIrrigation: () => { const exposes = e .composite("cyclic_quantitative_irrigation", "cyclic_quantitative_irrigation", ea.ALL) .withDescription("Smart water valve circulating quantitative irrigation") .withFeature(e.numeric("current_count", ea.STATE).withDescription("Number of times it has been executed").withUnit("times")) .withFeature(e .numeric("total_number", ea.STATE_SET) .withDescription("Total times of circulating irrigation") .withUnit("times") .withValueMin(0) .withValueMax(100)) .withFeature(e .numeric("irrigation_capacity", ea.STATE_SET) .withDescription("Single irrigation capacity") .withUnit("liter") .withValueMin(0) .withValueMax(6500)) .withFeature(e .numeric("irrigation_interval", ea.STATE_SET) .withDescription("Time interval between two adjacent irrigation") .withUnit("seconds") .withValueMin(0) .withValueMax(86400)); const fromZigbee = [ { cluster: "customClusterEwelink", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const attributeKey = 0x5009; // attr if (attributeKey in msg.data) { // logger.debug(` from zigbee 0x5009 cluster ${msg.data[attributeKey]} `, NS); // logger.debug(msg.data[attributeKey]); const rawData = msg.meta.rawData; /*eg:raw data: 082b0a0850420a0101000000ef00000064*/ /*zcl frame: 082b0a attrid: 0850 data type :42 data payload:0a0101000000ef00000064*/ /*0a:data len 01:currentCount 01:totalNumber 00 00 00 ef:irrigationCapacityBuffer 00 00 00 64:irrigationIntervalBuffer*/ const dataStartIndex = 7; /*data payload start index*/ //logger.debug(`rawData====> ${rawData[0+dataStartIndex]} ${rawData[1+dataStartIndex]} ${rawData[2+dataStartIndex]} ${rawData[3+dataStartIndex]} ${rawData[4+dataStartIndex]} ${rawData[5+dataStartIndex]} `, NS); //logger.debug(`rawData====> ${rawData[6+dataStartIndex]} ${rawData[7+dataStartIndex]} ${rawData[8+dataStartIndex]} ${rawData[9+dataStartIndex]} `, NS); const currentCountBuffer = rawData.readUInt8(0 + dataStartIndex); const totalNumberBuffer = rawData.readUInt8(1 + dataStartIndex); const irrigationCapacityBuffer = rawData.readUInt32BE(2 + dataStartIndex); const irrigationIntervalBuffer = rawData.readUInt32BE(6 + dataStartIndex); //logger.debug(`currentCountBuffer ${currentCountBuffer}`, NS); //logger.debug(`totalNumberBuffer ${totalNumberBuffer}`, NS); //logger.debug(`irrigationCapacityBuffer ${irrigationCapacityBuffer}`, NS); //logger.debug(`irrigationIntervalBuffer ${irrigationIntervalBuffer}`, NS); return { cyclic_quantitative_irrigation: { current_count: currentCountBuffer, total_number: totalNumberBuffer, irrigation_capacity: irrigationCapacityBuffer, irrigation_interval: irrigationIntervalBuffer, }, }; } }, }, ]; const toZigbee = [ { key: ["cyclic_quantitative_irrigation"], convertSet: async (entity, key, value, meta) => { // logger.debug(`to zigbee cyclic_Quantitative_irrigation ${key}`, NS); // const currentCount:string = 'current_count'; // logger.debug(`to zigbee cyclic_Quantitative_irrigation ${value[currentCount as keyof typeof value]}`, NS); const totalNumber = "total_number"; // logger.debug(`to zigbee cyclic_Quantitative_irrigation ${value[totalNumber as keyof typeof value]}`, NS); const irrigationCapacity = "irrigation_capacity"; // logger.debug(`to zigbee cyclic_Quantitative_irrigation ${value[irrigationCapacity as keyof typeof value]}`, NS); const irrigationInterval = "irrigation_interval"; // logger.debug(`to zigbee cyclic_Quantitative_irrigation ${value[irrigationInterval as keyof typeof value]}`, NS); const payloadValue = new Uint8Array(11); payloadValue[0] = 0x0a; payloadValue[1] = 0x00; payloadValue[2] = value[totalNumber] & 0xff; payloadValue[3] = (value[irrigationCapacity] >> 24) & 0xff; payloadValue[4] = (value[irrigationCapacity] >> 16) & 0xff; payloadValue[5] = (value[irrigationCapacity] >> 8) & 0xff; payloadValue[6] = value[irrigationCapacity] & 0xff; payloadValue[7] = (value[irrigationInterval] >> 24) & 0xff; payloadValue[8] = (value[irrigationInterval] >> 16) & 0xff; payloadValue[9] = (value[irrigationInterval] >> 8) & 0xff; payloadValue[10] = value[irrigationInterval] & 0xff; const payload = { [0x5009]: { value: payloadValue, type: 0x42 } }; await entity.write("customClusterEwelink", payload, defaultResponseOptions); return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { await entity.read("customClusterEwelink", [0x5009], defaultResponseOptions); }, }, ]; return { exposes: [exposes], fromZigbee, toZigbee, isModernExtend: true, }; }, externalSwitchTriggerMode: (args = {}) => { const clusterName = "customClusterEwelink"; const attributeName = "externalTriggerMode"; const { endpointNames = undefined } = args; const description = "External trigger mode, which can be one of edge, pulse, " + "following(off), following(on). The appropriate triggering mode can be selected according to the type of " + "external switch to achieve a better use experience."; const exposes = utils.exposeEndpoints(e .enum("external_trigger_mode", ea.ALL, ["edge", "pulse", "following(off)", "following(on)"]) .withDescription(description) .withCategory("config"), endpointNames); const fromZigbee = [ { cluster: clusterName, type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const lookup = { edge: 0, pulse: 1, "following(off)": 2, "following(on)": 130 }; // logger.debug(`from zigbee msg.data['externalTriggerMode'] ${msg.data['externalTriggerMode']}`, NS); if (msg.data.externalTriggerMode !== undefined) { let switchType = "edge"; for (const name in lookup) { if (lookup[name] === msg.data.externalTriggerMode) { switchType = name; break; } } // logger.debug(`form zigbee switchType ${switchType}`, NS); return { external_trigger_mode: switchType }; } }, }, ]; const toZigbee = [ { key: ["external_trigger_mode"], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); value = value.toLowerCase(); const lookup = { edge: 0, pulse: 1, "following(off)": 2, "following(on)": 130 }; const tmpValue = utils.getFromLookup(value, lookup); await entity.write(clusterName, { [attributeName]: tmpValue }, defaultResponseOptions); return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { await entity.read(clusterName, [attributeName], defaultResponseOptions); }, }, ]; return { exposes: exposes, fromZigbee, toZigbee, isModernExtend: true, }; }, detachRelayModeControl: (relayCount) => { const clusterName = "customClusterEwelink"; const attributeName = "detachRelayMode2"; const exposes = e.composite("detach_relay_mode", "detach_relay_mode", ea.ALL); if (1 === relayCount) { exposes .withDescription("Relay separation mode. Can be used when the load is a smart device (such as smart light), " + "when we control the wall switch, do not want to turn off the power of the smart light, but through " + "a scene command to control the smart light on or off, then we can enable the relay separation mode.") .withFeature(e.binary("detach_relay_outlet1", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")); } else if (2 === relayCount) { exposes .withDescription("Relay separation mode. Can be used when the load is a smart device (such as smart light), " + "when we control the wall switch, do not want to turn off the power of the smart light, but through " + "a scene command to control the smart light on or off, then we can enable the relay separation mode.") .withFeature(e.binary("detach_relay_outlet1", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")) .withFeature(e.binary("detach_relay_outlet2", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")); } else if (3 === relayCount) { exposes .withDescription("Relay separation mode. Can be used when the load is a smart device (such as smart light), " + "when we control the wall switch, do not want to turn off the power of the smart light, but through " + "a scene command to control the smart light on or off, then we can enable the relay separation mode.") .withFeature(e.binary("detach_relay_outlet1", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")) .withFeature(e.binary("detach_relay_outlet2", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")) .withFeature(e.binary("detach_relay_outlet3", ea.SET, "ENABLE", "DISABLE").withDescription("Enable/disable detach relay.")); } const fromZigbee = [ { cluster: clusterName, type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.detachRelayMode2 !== undefined) { const detachMode = msg.data.detachRelayMode2; logger_1.logger.debug(`form zigbee detachRelayMode2 ${detachMode}`, NS); const datachRelayStatus = { detach_relay_outlet1: "DISABLE", detach_relay_outlet2: "DISABLE", detach_relay_outlet3: "DISABLE", }; if ((detachMode & 0x01) !== 0) { datachRelayStatus.detach_relay_outlet1 = "ENABLE"; } if ((detachMode & 0x02) !== 0) { datachRelayStatus.detach_relay_outlet2 = "ENABLE"; } if ((detachMode & 0x04) !== 0) { datachRelayStatus.detach_relay_outlet3 = "ENABLE"; } return { detach_relay_mode: datachRelayStatus }; } }, }, ]; const toZigbee = [ { key: ["detach_relay_mode"], convertSet: async (entity, key, value, meta) => { // logger.debug(`from zigbee 'key' ${key}`, NS); const detachRelay1 = "detach_relay_outlet1"; // logger.debug(`from zigbee detachRelay1: ${value[detachRelay1 as keyof typeof value]}`, NS); const detachRelay2 = "detach_relay_outlet2"; // logger.debug(`from zigbee detachRelay2: ${value[detachRelay2 as keyof typeof value]}`, NS); const detachRelay3 = "detach_relay_outlet3"; // logger.debug(`from zigbee detachRelay3: ${value[detachRelay3 as keyof typeof value]}`, NS); let detachRelayMask = 0; if (value[detachRelay1] === "ENABLE") { detachRelayMask |= 0x01; } else { detachRelayMask &= ~0x01; } if (value[detachRelay2] === "ENABLE") { detachRelayMask |= 0x02; } else { detachRelayMask &= ~0x02; } if (value[detachRelay3] === "ENABLE") { detachRelayMask |= 0x04; } else { detachRelayMask &= ~0x04; } // logger.info(`from zigbee detachRelayMask: ${detachRelayMask}`, NS); await entity.write(clusterName, { [attributeName]: detachRelayMask }, defaultResponseOptions); return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { await entity.read(clusterName, [attributeName], defaultResponseOptions); }, }, ]; return { exposes: [exposes], fromZigbee, toZigbee, isModernExtend: true, }; }, buildOverloadProtectionPayload: (value, powerMaxLimit, currentMaxLimit) => { let maxCurrent = 1000 * Number(value.max_current ?? 0); const minCurrent = 1000 * Number(value.min_current ?? 0); const maxVoltage = 1000 * Number(value.max_voltage ?? 0); const minVoltage = 1000 * Number(value.min_voltage ?? 0); let maxPower = 1000 * Number(value.max_power ?? 0); const minPower = 1000 * Number(value.min_power ?? 0); const enableMinCurrent = value.enable_min_current; const enableMaxVoltage = value.enable_max_voltage; const enableMinVoltage = value.enable_min_voltage; const enableMinPower = value.enable_min_power; const payloadValue = new Uint8Array(30); let index = 0; payloadValue[index++] = 0; payloadValue[index++] = 0x04; payloadValue[index++] = 27; payloadValue[index++] = 1; payl