UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,442 lines • 373 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ictcg1 = exports.tuyaGetDataValue = exports.getMetaValue = exports.convertTimeTo2ByteHexArray = exports.convertDecimalValueTo2ByteHexArray = exports.isCoverInverted = exports.logUnexpectedDataValue = exports.convertRawToTimer = exports.convertWeekdaysTo1ByteHexArray = exports.logDataPoint = exports.convertRawToCycleTimer = exports.sendDataPointStringBuffer = exports.sendDataPointBitmap = exports.sendDataPointRaw = exports.sendDataPointEnum = exports.sendDataPointBool = exports.sendDataPointValue = exports.sendDataPoint = exports.convertStringToHexArray = exports.sendDataPoints = exports.convertDecimalValueTo4ByteHexArray = exports.getCoverStateEnums = exports.getDataPointNames = exports.logUnexpectedDataType = exports.logUnexpectedDataPoint = exports.getTypeName = exports.getDataValue = exports.moesSwitch = exports.dpValueFromStringBuffer = exports.dpValueFromBitmap = exports.dpValueFromRaw = exports.dpValueFromIntValue = exports.dpValueFromBool = exports.dataPoints = exports.dpValueFromEnum = exports.firstDpValue = exports.ZMLookups = exports.msLookups = exports.giexWaterValve = exports.thermostatPresets = exports.thermostatSystemModes4 = exports.thermostatSystemModes3 = exports.thermostatSystemModes2 = exports.tuyaHPSCheckingResult = exports.thermostatSystemModes = exports.thermostatControlSequenceOfOperations = exports.toZigbee = exports.tz = exports.fromZigbee = exports.fz = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const fs_1 = __importDefault(require("fs")); const globalStore = __importStar(require("./store")); const utils = __importStar(require("./utils")); const fromZigbee_1 = __importDefault(require("../converters/fromZigbee")); const fromZigbeeStore = {}; const exposes = __importStar(require("./exposes")); const constants = __importStar(require("./constants")); const light = __importStar(require("./light")); const logger_1 = require("./logger"); // get object property name (key) by it's value const getKey = (object, value) => { for (const key in object) { if (object[key] == value) return key; } }; const dataTypes = { raw: 0, // [ bytes ] bool: 1, // [0/1] value: 2, // [ 4 byte value ] string: 3, // [ N byte string ] enum: 4, // [ 0-255 ] bitmap: 5, // [ 1,2,4 bytes ] as bits }; const convertMultiByteNumberPayloadToSingleDecimalNumber = (chunks) => { // Destructuring "chunks" is needed because it's a Buffer // and we need a simple array. let value = 0; for (let i = 0; i < chunks.length; i++) { value = value << 8; value += chunks[i]; } return value; }; function getDataValue(dpValue) { switch (dpValue.datatype) { case dataTypes.raw: return dpValue.data; case dataTypes.bool: return dpValue.data[0] === 1; case dataTypes.value: return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data); case dataTypes.string: // eslint-disable-next-line let dataString = ''; // Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091 for (let i = 0; i < dpValue.data.length; ++i) { dataString += String.fromCharCode(dpValue.data[i]); } return dataString; case dataTypes.enum: return dpValue.data[0]; case dataTypes.bitmap: return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data); } } exports.getDataValue = getDataValue; function getTypeName(dpValue) { const entry = Object.entries(dataTypes).find(([typeName, typeId]) => typeId === dpValue.datatype); return (entry ? entry[0] : 'unknown'); } exports.getTypeName = getTypeName; function logUnexpectedDataPoint(where, msg, dpValue, meta) { logger_1.logger.debug(`Received unexpected Tuya DataPoint #${dpValue.dp} from ${meta.device.ieeeAddr} with raw data '${JSON.stringify(dpValue)}': \ type='${msg.type}', datatype='${getTypeName(dpValue)}', value='${getDataValue(dpValue)}', known DP# usage: \ ${JSON.stringify(getDataPointNames(dpValue))}`, `zhc:${where}`); } exports.logUnexpectedDataPoint = logUnexpectedDataPoint; function logUnexpectedDataType(where, msg, dpValue, meta, expectedDataType) { logger_1.logger.debug(`Received Tuya DataPoint #${dpValue.dp} with unexpected datatype from ${meta.device.ieeeAddr} with raw data \ '${JSON.stringify(dpValue)}': type='${msg.type}', datatype='${getTypeName(dpValue)}' (instead of '${expectedDataType}'), \ value='${getDataValue(dpValue)}', known DP# usage: ${JSON.stringify(getDataPointNames(dpValue))}`, `zhc:${where}`); } exports.logUnexpectedDataType = logUnexpectedDataType; function getDataPointNames(dpValue) { const entries = Object.entries(dataPoints).filter(([dpName, dpId]) => dpId === dpValue.dp); return entries.map(([dpName, dpId]) => dpName); } exports.getDataPointNames = getDataPointNames; const coverStateOverride = { // Contains all covers which differentiate from the default enum states // Use manufacturerName to identify device! // https://github.com/Koenkk/zigbee2mqtt/issues/5596#issuecomment-759408189 '_TZE200_rddyvrci': { close: 1, open: 2, stop: 0 }, '_TZE200_wmcdj3aq': { close: 0, open: 2, stop: 1 }, '_TZE200_cowvfni3': { close: 0, open: 2, stop: 1 }, '_TYST11_cowvfni3': { close: 0, open: 2, stop: 1 }, }; // Gets an array containing which enums have to be used in order for the correct close/open/stop commands to be sent function getCoverStateEnums(manufacturerName) { if (manufacturerName in coverStateOverride) { return coverStateOverride[manufacturerName]; } else { return { close: 2, open: 0, stop: 1 }; // defaults } } exports.getCoverStateEnums = getCoverStateEnums; function convertDecimalValueTo4ByteHexArray(value) { const hexValue = Number(value).toString(16).padStart(8, '0'); const chunk1 = hexValue.substring(0, 2); const chunk2 = hexValue.substring(2, 4); const chunk3 = hexValue.substring(4, 6); const chunk4 = hexValue.substring(6); return [chunk1, chunk2, chunk3, chunk4].map((hexVal) => parseInt(hexVal, 16)); } exports.convertDecimalValueTo4ByteHexArray = convertDecimalValueTo4ByteHexArray; let gSec = undefined; async function sendDataPoints(entity, dpValues, cmd = 'dataRequest', seq = undefined) { if (seq === undefined) { if (gSec === undefined) { gSec = 0; } else { gSec++; gSec %= 0xFFFF; } seq = gSec; } await entity.command('manuSpecificTuya', cmd || 'dataRequest', { seq, dpValues, }, { disableDefaultResponse: true }); return seq; } exports.sendDataPoints = sendDataPoints; function convertStringToHexArray(value) { const asciiKeys = []; for (let i = 0; i < value.length; i++) { asciiKeys.push(value[i].charCodeAt(0)); } return asciiKeys; } exports.convertStringToHexArray = convertStringToHexArray; function dpValueFromIntValue(dp, value) { return { dp, datatype: dataTypes.value, data: convertDecimalValueTo4ByteHexArray(value) }; } exports.dpValueFromIntValue = dpValueFromIntValue; function dpValueFromBool(dp, value) { return { dp, datatype: dataTypes.bool, data: [value ? 1 : 0] }; } exports.dpValueFromBool = dpValueFromBool; function dpValueFromEnum(dp, value) { return { dp, datatype: dataTypes.enum, data: [value] }; } exports.dpValueFromEnum = dpValueFromEnum; function dpValueFromStringBuffer(dp, stringBuffer) { return { dp, datatype: dataTypes.string, data: stringBuffer }; } exports.dpValueFromStringBuffer = dpValueFromStringBuffer; function dpValueFromRaw(dp, rawBuffer) { return { dp, datatype: dataTypes.raw, data: rawBuffer }; } exports.dpValueFromRaw = dpValueFromRaw; function dpValueFromBitmap(dp, bitmapBuffer) { return { dp, datatype: dataTypes.bitmap, data: bitmapBuffer }; } exports.dpValueFromBitmap = dpValueFromBitmap; // Return `seq` - transaction ID for handling concrete response async function sendDataPoint(entity, dpValue, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValue], cmd, seq); } exports.sendDataPoint = sendDataPoint; async function sendDataPointValue(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromIntValue(dp, value)], cmd, seq); } exports.sendDataPointValue = sendDataPointValue; async function sendDataPointBool(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromBool(dp, value)], cmd, seq); } exports.sendDataPointBool = sendDataPointBool; async function sendDataPointEnum(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromEnum(dp, value)], cmd, seq); } exports.sendDataPointEnum = sendDataPointEnum; async function sendDataPointRaw(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromRaw(dp, value)], cmd, seq); } exports.sendDataPointRaw = sendDataPointRaw; async function sendDataPointBitmap(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromBitmap(dp, value)], cmd, seq); } exports.sendDataPointBitmap = sendDataPointBitmap; async function sendDataPointStringBuffer(entity, dp, value, cmd, seq = undefined) { return await sendDataPoints(entity, [dpValueFromStringBuffer(dp, value)], cmd, seq); } exports.sendDataPointStringBuffer = sendDataPointStringBuffer; function convertRawToCycleTimer(value) { let timernr = 0; let starttime = '00:00'; let endtime = '00:00'; let irrigationDuration = 0; let pauseDuration = 0; let weekdays = 'once'; let timeractive = 0; if (value.length > 11) { timernr = value[1]; timeractive = value[2]; if (value[3] > 0) { weekdays = (value[3] & 0x01 ? 'Su' : '') + (value[3] & 0x02 ? 'Mo' : '') + (value[3] & 0x04 ? 'Tu' : '') + (value[3] & 0x08 ? 'We' : '') + (value[3] & 0x10 ? 'Th' : '') + (value[3] & 0x20 ? 'Fr' : '') + (value[3] & 0x40 ? 'Sa' : ''); } else { weekdays = 'once'; } let minsincemidnight = value[4] * 256 + value[5]; // @ts-ignore starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0'); minsincemidnight = value[6] * 256 + value[7]; // @ts-ignore endtime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0'); irrigationDuration = value[8] * 256 + value[9]; pauseDuration = value[10] * 256 + value[11]; } return { timernr: timernr, starttime: starttime, endtime: endtime, irrigationDuration: irrigationDuration, pauseDuration: pauseDuration, weekdays: weekdays, active: timeractive, }; } exports.convertRawToCycleTimer = convertRawToCycleTimer; function logDataPoint(where, msg, dpValue, meta) { logger_1.logger.info(`Received Tuya DataPoint #${dpValue.dp} from ${meta.device.ieeeAddr} with raw data '${JSON.stringify(dpValue)}': \ type='${msg.type}', datatype='${getTypeName(dpValue)}', value='${getDataValue(dpValue)}', known DP# usage: \ ${JSON.stringify(getDataPointNames(dpValue))}`, `zhc:${where}`); } exports.logDataPoint = logDataPoint; const thermostatSystemModes2 = { 0: 'auto', 1: 'cool', 2: 'heat', 3: 'dry', 4: 'fan', }; exports.thermostatSystemModes2 = thermostatSystemModes2; const thermostatSystemModes3 = { 0: 'auto', 1: 'heat', 2: 'off', }; exports.thermostatSystemModes3 = thermostatSystemModes3; const thermostatSystemModes4 = { 0: 'off', 1: 'auto', 2: 'heat', }; exports.thermostatSystemModes4 = thermostatSystemModes4; const thermostatWeekFormat = { 0: '5+2', 1: '6+1', 2: '7', }; const thermostatForceMode = { 0: 'normal', 1: 'open', 2: 'close', }; const thermostatPresets = { 0: 'away', 1: 'schedule', 2: 'manual', 3: 'comfort', 4: 'eco', 5: 'boost', 6: 'complex', }; exports.thermostatPresets = thermostatPresets; const thermostatScheduleMode = { 1: 'single', // One schedule for all days 2: 'weekday/weekend', // Weekdays(2-5) and Holidays(6-1) 3: 'weekday/sat/sun', // Weekdays(2-6), Saturday(7), Sunday(1) 4: '7day', // 7 day schedule }; const silvercrestModes = { white: 0, color: 1, effect: 2, }; const silvercrestEffects = { steady: '00', snow: '01', rainbow: '02', snake: '03', twinkle: '04', firework: '05', horizontal_flag: '06', waves: '07', updown: '08', vintage: '09', fading: '0a', collide: '0b', strobe: '0c', sparkles: '0d', carnaval: '0e', glow: '0f', }; const fanModes = { 0: 'low', 1: 'medium', 2: 'high', 3: 'auto', }; // Motion sensor lookups const msLookups = { OSensitivity: { 0: 'sensitive', 1: 'normal', 2: 'cautious', }, VSensitivity: { 0: 'speed_priority', 1: 'normal_priority', 2: 'accuracy_priority', }, Mode: { 0: 'general_model', 1: 'temporaty_stay', 2: 'basic_detection', 3: 'sensor_test', }, }; exports.msLookups = msLookups; const tvThermostatMode = { 0: 'off', 1: 'heat', 2: 'auto', }; const tvThermostatPreset = { 0: 'auto', 1: 'manual', 2: 'holiday', 3: 'holiday', }; // Zemismart ZM_AM02 Roller Shade Converter const ZMLookups = { AM02Mode: { 0: 'morning', 1: 'night', }, AM02Control: { 0: 'open', 1: 'stop', 2: 'close', 3: 'continue', }, AM02Direction: { 0: 'forward', 1: 'back', }, AM02WorkState: { 0: 'opening', 1: 'closing', }, AM02Border: { 0: 'up', 1: 'down', 2: 'down_delete', }, AM02Situation: { 0: 'fully_open', 1: 'fully_close', }, AM02MotorWorkingMode: { 0: 'continuous', 1: 'intermittently', }, }; exports.ZMLookups = ZMLookups; const moesSwitch = { powerOnBehavior: { 0: 'off', 1: 'on', 2: 'previous', }, indicateLight: { 0: 'off', 1: 'switch', 2: 'position', 3: 'freeze', }, }; exports.moesSwitch = moesSwitch; const tuyaHPSCheckingResult = { 0: 'checking', 1: 'check_success', 2: 'check_failure', 3: 'others', 4: 'comm_fault', 5: 'radar_fault', }; exports.tuyaHPSCheckingResult = tuyaHPSCheckingResult; function convertWeekdaysTo1ByteHexArray(weekdays) { let nr = 0; if (weekdays == 'once') { return nr; } if (weekdays.includes('Mo')) { nr |= 0x40; } if (weekdays.includes('Tu')) { nr |= 0x20; } if (weekdays.includes('We')) { nr |= 0x10; } if (weekdays.includes('Th')) { nr |= 0x08; } if (weekdays.includes('Fr')) { nr |= 0x04; } if (weekdays.includes('Sa')) { nr |= 0x02; } if (weekdays.includes('Su')) { nr |= 0x01; } return [nr]; } exports.convertWeekdaysTo1ByteHexArray = convertWeekdaysTo1ByteHexArray; function convertRawToTimer(value) { let timernr = 0; let starttime = '00:00'; let duration = 0; let weekdays = 'once'; let timeractive = ''; if (value.length > 12) { timernr = value[1]; const minsincemidnight = value[2] * 256 + value[3]; // @ts-ignore starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0'); duration = value[4] * 256 + value[5]; if (value[6] > 0) { weekdays = (value[6] & 0x01 ? 'Su' : '') + (value[6] & 0x02 ? 'Mo' : '') + (value[6] & 0x04 ? 'Tu' : '') + (value[6] & 0x08 ? 'We' : '') + (value[6] & 0x10 ? 'Th' : '') + (value[6] & 0x20 ? 'Fr' : '') + (value[6] & 0x40 ? 'Sa' : ''); } else { weekdays = 'once'; } timeractive = value[8]; } return { timernr: timernr, time: starttime, duration: duration, weekdays: weekdays, active: timeractive }; } exports.convertRawToTimer = convertRawToTimer; function logUnexpectedDataValue(where, msg, dpValue, meta, valueKind, expectedMinValue = null, expectedMaxValue = null) { if (expectedMinValue === null) { if (expectedMaxValue === null) { logger_1.logger.debug(`Received Tuya DataPoint #${dpValue.dp} with invalid value ${getDataValue(dpValue)} for ${valueKind} \ from ${meta.device.ieeeAddr}`, `zhc:${where}`); } else { logger_1.logger.debug(`Received Tuya DataPoint #${dpValue.dp} with invalid value ${getDataValue(dpValue)} for ${valueKind} \ from ${meta.device.ieeeAddr} which is higher than the expected maximum of ${expectedMaxValue}`, `zhc:${where}`); } } else { if (expectedMaxValue === null) { logger_1.logger.debug(`Received Tuya DataPoint #${dpValue.dp} with invalid value ${getDataValue(dpValue)} for ${valueKind} \ from ${meta.device.ieeeAddr} which is lower than the expected minimum of ${expectedMinValue}`, `zhc:${where}`); } else { logger_1.logger.debug(`Received Tuya DataPoint #${dpValue.dp} with invalid value ${getDataValue(dpValue)} for ${valueKind} \ from ${meta.device.ieeeAddr} which is outside the expected range from ${expectedMinValue} to ${expectedMaxValue}`, `zhc:${where}`); } } } exports.logUnexpectedDataValue = logUnexpectedDataValue; // Contains all covers which need their position inverted by default // Default is 100 = open, 0 = closed; Devices listed here will use 0 = open, 100 = closed instead // Use manufacturerName to identify device! // Don't invert _TZE200_cowvfni3: https://github.com/Koenkk/zigbee2mqtt/issues/6043 const coverPositionInvert = ['_TZE200_wmcdj3aq', '_TZE200_nogaemzt', '_TZE200_xuzcvlku', '_TZE200_xaabybja', '_TZE200_rmymn92d', '_TZE200_gubdgai2', '_TZE200_r0jdjrvi']; // Gets a boolean indicating whether the cover by this manufacturerName needs reversed positions function isCoverInverted(manufacturerName) { // Return true if cover is listed in coverPositionInvert // Return false by default, not inverted return coverPositionInvert.includes(manufacturerName); } exports.isCoverInverted = isCoverInverted; function convertDecimalValueTo2ByteHexArray(value) { const hexValue = Number(value).toString(16).padStart(4, '0'); const chunk1 = hexValue.substr(0, 2); const chunk2 = hexValue.substr(2); return [chunk1, chunk2].map((hexVal) => parseInt(hexVal, 16)); } exports.convertDecimalValueTo2ByteHexArray = convertDecimalValueTo2ByteHexArray; function convertTimeTo2ByteHexArray(time) { const timeArray = time.split(':'); if (timeArray.length != 2) { throw new Error('Time format incorrect'); } const timeHour = parseInt(timeArray[0]); const timeMinute = parseInt(timeArray[1]); if (timeHour > 23 || timeMinute > 59) { throw new Error('Time incorrect'); } return convertDecimalValueTo2ByteHexArray(timeHour * 60 + timeMinute); } exports.convertTimeTo2ByteHexArray = convertTimeTo2ByteHexArray; const dataPoints = { wateringTimer: { valve_state_auto_shutdown: 2, water_flow: 3, shutdown_timer: 11, remaining_watering_time: 101, valve_state: 102, last_watering_duration: 107, battery: 110, }, // Common data points // Below data points are usually shared between devices state: 1, heatingSetpoint: 2, coverPosition: 2, dimmerLevel: 3, dimmerMinLevel: 3, localTemp: 3, coverArrived: 3, occupancy: 3, mode: 4, fanMode: 5, dimmerMaxLevel: 5, motorDirection: 5, config: 5, childLock: 7, coverChange: 7, runningState: 14, valveDetection: 20, battery: 21, tempCalibration: 44, // Data points above 100 are usually custom function data points waterLeak: 101, minTemp: 102, maxTemp: 103, windowDetection: 104, boostTime: 105, coverSpeed: 105, forceMode: 106, comfortTemp: 107, ecoTemp: 108, valvePos: 109, batteryLow: 110, weekFormat: 111, scheduleWorkday: 112, scheduleHoliday: 113, awayTemp: 114, windowOpen: 115, autoLock: 116, awayDays: 117, // Manufacturer specific // Earda eardaDimmerLevel: 2, // Siterwell Thermostat siterwellWindowDetection: 18, // Moes Thermostat moesHold: 2, moesScheduleEnable: 3, moesHeatingSetpoint: 16, moesMaxTempLimit: 18, moesMaxTemp: 19, moesDeadZoneTemp: 20, moesLocalTemp: 24, moesMinTempLimit: 26, moesTempCalibration: 27, moesValve: 36, moesChildLock: 40, moesSensor: 43, moesSchedule: 101, etopErrorStatus: 13, // MoesS Thermostat moesSsystemMode: 1, moesSheatingSetpoint: 2, moesSlocalTemp: 3, moesSboostHeating: 4, moesSboostHeatingCountdown: 5, moesSreset: 7, moesSwindowDetectionFunktion_A2: 8, moesSwindowDetection: 9, moesSchildLock: 13, moesSbattery: 14, moesSschedule: 101, moesSvalvePosition: 104, moesSboostHeatingCountdownTimeSet: 103, moesScompensationTempSet: 105, moesSecoMode: 106, moesSecoModeTempSet: 107, moesSmaxTempSet: 108, moesSminTempSet: 109, moesCoverCalibration: 3, moesCoverBacklight: 7, moesCoverMotorReversal: 8, // Neo T&H neoOccupancy: 101, neoPowerType: 101, neoMelody: 102, neoDuration: 103, neoTamper: 103, neoAlarm: 104, neoTemp: 105, neoTempScale: 106, neoHumidity: 106, neoMinTemp: 107, neoMaxTemp: 108, neoMinHumidity: 109, neoMaxHumidity: 110, neoUnknown2: 112, neoTempAlarm: 113, neoTempHumidityAlarm: 113, neoHumidityAlarm: 114, neoUnknown3: 115, neoVolume: 116, // Neo AlarmOnly neoAOBattPerc: 15, neoAOMelody: 21, neoAODuration: 7, neoAOAlarm: 13, neoAOVolume: 5, // Saswell TRV saswellHeating: 3, saswellWindowDetection: 8, saswellFrostDetection: 10, saswellTempCalibration: 27, saswellChildLock: 40, saswellState: 101, saswellLocalTemp: 102, saswellHeatingSetpoint: 103, saswellValvePos: 104, saswellBatteryLow: 105, saswellAwayMode: 106, saswellScheduleMode: 107, saswellScheduleEnable: 108, saswellScheduleSet: 109, saswellSetpointHistoryDay: 110, saswellTimeSync: 111, saswellSetpointHistoryWeek: 112, saswellSetpointHistoryMonth: 113, saswellSetpointHistoryYear: 114, saswellLocalHistoryDay: 115, saswellLocalHistoryWeek: 116, saswellLocalHistoryMonth: 117, saswellLocalHistoryYear: 118, saswellMotorHistoryDay: 119, saswellMotorHistoryWeek: 120, saswellMotorHistoryMonth: 121, saswellMotorHistoryYear: 122, saswellScheduleSunday: 123, saswellScheduleMonday: 124, saswellScheduleTuesday: 125, saswellScheduleWednesday: 126, saswellScheduleThursday: 127, saswellScheduleFriday: 128, saswellScheduleSaturday: 129, saswellAntiScaling: 130, // HY thermostat hyHeating: 102, hyExternalTemp: 103, hyAwayDays: 104, hyAwayTemp: 105, hyMaxTempProtection: 106, hyMinTempProtection: 107, hyTempCalibration: 109, hyHysteresis: 110, hyProtectionHysteresis: 111, hyProtectionMaxTemp: 112, hyProtectionMinTemp: 113, hyMaxTemp: 114, hyMinTemp: 115, hySensor: 116, hyPowerOnBehavior: 117, hyWeekFormat: 118, hyWorkdaySchedule1: 119, hyWorkdaySchedule2: 120, hyHolidaySchedule1: 121, hyHolidaySchedule2: 122, hyState: 125, hyHeatingSetpoint: 126, hyLocalTemp: 127, hyMode: 128, hyChildLock: 129, hyAlarm: 130, // Silvercrest silvercrestChangeMode: 2, silvercrestSetBrightness: 3, silvercrestSetColorTemp: 4, silvercrestSetColor: 5, silvercrestSetEffect: 6, // Fantem fantemPowerSupplyMode: 101, fantemReportingTime: 102, fantemExtSwitchType: 103, fantemTempCalibration: 104, fantemHumidityCalibration: 105, fantemLoadDetectionMode: 105, fantemLuxCalibration: 106, fantemExtSwitchStatus: 106, fantemTemp: 107, fantemHumidity: 108, fantemMotionEnable: 109, fantemControlMode: 109, fantemBattery: 110, fantemLedEnable: 111, fantemReportingEnable: 112, fantemLoadType: 112, fantemLoadDimmable: 113, // Woox wooxSwitch: 102, wooxBattery: 14, wooxSmokeTest: 8, // FrankEver frankEverTimer: 9, frankEverTreshold: 101, // Dinrail power meter switch dinrailPowerMeterTotalEnergy: 17, dinrailPowerMeterCurrent: 18, dinrailPowerMeterPower: 19, dinrailPowerMeterVoltage: 20, dinrailPowerMeterTotalEnergy2: 101, dinrailPowerMeterPower2: 103, // tuya smart air box tuyaSabCO2: 2, tuyaSabTemp: 18, tuyaSabHumidity: 19, tuyaSabVOC: 21, tuyaSabFormaldehyd: 22, // tuya Smart Air House Keeper, Multifunctionale air quality detector. // CO2, Temp, Humidity, VOC and Formaldehyd same as Smart Air Box tuyaSahkMP25: 2, tuyaSahkCO2: 22, tuyaSahkFormaldehyd: 20, // Tuya CO (carbon monoxide) smart air box tuyaSabCOalarm: 1, tuyaSabCO: 2, // Moes MS-105 Dimmer moes105DimmerState1: 1, moes105DimmerLevel1: 2, moes105DimmerState2: 7, moes105DimmerLevel2: 8, // TuYa Radar Sensor trsPresenceState: 1, trsSensitivity: 2, trsMotionState: 102, trsIlluminanceLux: 103, trsDetectionData: 104, trsScene: 112, trsMotionDirection: 114, trsMotionSpeed: 115, // TuYa Radar Sensor with fall function trsfPresenceState: 1, trsfSensitivity: 2, trsfMotionState: 102, trsfIlluminanceLux: 103, trsfTumbleSwitch: 105, trsfTumbleAlarmTime: 106, trsfScene: 112, trsfMotionDirection: 114, trsfMotionSpeed: 115, trsfFallDownStatus: 116, trsfStaticDwellAlarm: 117, trsfFallSensitivity: 118, // Human Presence Sensor AIR msVSensitivity: 101, msOSensitivity: 102, msVacancyDelay: 103, msMode: 104, msVacantConfirmTime: 105, msReferenceLuminance: 106, msLightOnLuminancePrefer: 107, msLightOffLuminancePrefer: 108, msLuminanceLevel: 109, msLedStatus: 110, // TV01 Moes Thermostat tvMode: 2, tvWindowDetection: 8, tvFrostDetection: 10, tvHeatingSetpoint: 16, tvLocalTemp: 24, tvTempCalibration: 27, tvWorkingDay: 31, tvHolidayTemp: 32, tvBattery: 35, tvChildLock: 40, tvErrorStatus: 45, tvHolidayMode: 46, tvBoostTime: 101, tvOpenWindowTemp: 102, tvComfortTemp: 104, tvEcoTemp: 105, tvWeekSchedule: 106, tvHeatingStop: 107, tvMondaySchedule: 108, tvWednesdaySchedule: 109, tvFridaySchedule: 110, tvSundaySchedule: 111, tvTuesdaySchedule: 112, tvThursdaySchedule: 113, tvSaturdaySchedule: 114, tvBoostMode: 115, // HOCH / WDYK DIN Rail hochCountdownTimer: 9, hochFaultCode: 26, hochRelayStatus: 27, hochChildLock: 29, hochVoltage: 101, hochCurrent: 102, hochActivePower: 103, hochLeakageCurrent: 104, hochTemperature: 105, hochRemainingEnergy: 106, hochRechargeEnergy: 107, hochCostParameters: 108, hochLeakageParameters: 109, hochVoltageThreshold: 110, hochCurrentThreshold: 111, hochTemperatureThreshold: 112, hochTotalActivePower: 113, hochEquipmentNumberType: 114, hochClearEnergy: 115, hochLocking: 116, hochTotalReverseActivePower: 117, hochHistoricalVoltage: 118, hochHistoricalCurrent: 119, // NOUS SMart LCD Temperature and Humidity Sensor E6 nousTemperature: 1, nousHumidity: 2, nousBattery: 4, nousTempUnitConvert: 9, nousMaxTemp: 10, nousMinTemp: 11, nousMaxHumi: 12, nousMinHumi: 13, nousTempAlarm: 14, nousHumiAlarm: 15, nousHumiSensitivity: 20, nousTempSensitivity: 19, nousTempReportInterval: 17, nousHumiReportInterval: 18, // TUYA Temperature and Humidity Sensor tthTemperature: 1, tthHumidity: 2, tthBatteryLevel: 3, tthBattery: 4, // TUYA / HUMIDITY/ILLUMINANCE/TEMPERATURE SENSOR thitBatteryPercentage: 3, thitIlluminanceLux: 7, tIlluminanceLux: 2, thitHumidity: 9, thitTemperature: 8, // TUYA SMART VIBRATION SENSOR tuyaVibration: 10, // TUYA WLS-100z Water Leak Sensor wlsWaterLeak: 1, wlsBatteryPercentage: 4, // Evanell evanellMode: 2, evanellHeatingSetpoint: 4, evanellLocalTemp: 5, evanellBattery: 6, evanellChildLock: 8, // ZMAM02 Zemismart RF Courtain Converter AM02Control: 1, AM02PercentControl: 2, AM02PercentState: 3, AM02Mode: 4, AM02Direction: 5, AM02WorkState: 7, AM02CountdownLeft: 9, AM02TimeTotal: 10, AM02SituationSet: 11, AM02Fault: 12, AM02Border: 16, AM02MotorWorkingMode: 20, AM02AddRemoter: 101, // Matsee Tuya Garage Door Opener garageDoorTrigger: 1, garageDoorContact: 3, garageDoorStatus: 12, // Moes switch with optional neutral moesSwitchPowerOnBehavior: 14, moesSwitchIndicateLight: 15, // X5H thermostat x5hState: 1, x5hMode: 2, x5hWorkingStatus: 3, x5hSound: 7, x5hFrostProtection: 10, x5hSetTemp: 16, x5hSetTempCeiling: 19, x5hCurrentTemp: 24, x5hTempCorrection: 27, x5hWeeklyProcedure: 30, x5hWorkingDaySetting: 31, x5hFactoryReset: 39, x5hChildLock: 40, x5hSensorSelection: 43, x5hFaultAlarm: 45, x5hTempDiff: 101, x5hProtectionTempLimit: 102, x5hOutputReverse: 103, x5hBackplaneBrightness: 104, // Connected thermostat connecteState: 1, connecteMode: 2, connecteHeatingSetpoint: 16, connecteLocalTemp: 24, connecteTempCalibration: 28, connecteChildLock: 30, connecteTempFloor: 101, connecteSensorType: 102, connecteHysteresis: 103, connecteRunningState: 104, connecteTempProgram: 105, connecteOpenWindow: 106, connecteMaxProtectTemp: 107, // TuYa Smart Human Presence Sensor tshpsPresenceState: 1, tshpscSensitivity: 2, tshpsMinimumRange: 3, tshpsMaximumRange: 4, tshpsTargetDistance: 9, tshpsDetectionDelay: 101, tshpsFadingTime: 102, tshpsIlluminanceLux: 104, tshpsCLI: 103, // not recognize tshpsSelfTest: 6, // not recognize // TuYa Luminance Motion sensor lmsState: 1, lmsBattery: 4, lmsSensitivity: 9, lmsKeepTime: 10, lmsIlluminance: 12, // Alecto SMART-SMOKE10 alectoSmokeState: 1, alectoSmokeValue: 2, alectoSelfChecking: 8, alectoCheckingResult: 9, alectoSmokeTest: 11, alectoLifecycle: 12, alectoBatteryState: 14, alectoBatteryPercentage: 15, alectoSilence: 16, // BAC-002-ALZB - Moes like thermostat with Fan control bacFanMode: 28, // Human Presence Sensor Zigbee Radiowave Tuya HPSZInductionState: 1, HPSZPresenceTime: 101, HPSZLeavingTime: 102, HPSZLEDState: 103, giexWaterValve: { battery: 108, currentTemperature: 106, cycleIrrigationInterval: 105, cycleIrrigationNumTimes: 103, irrigationEndTime: 102, irrigationStartTime: 101, irrigationTarget: 104, lastIrrigationDuration: 114, mode: 1, state: 2, waterConsumed: 111, }, zsHeatingSetpoint: 16, zsChildLock: 40, zsTempCalibration: 104, zsLocalTemp: 24, zsBatteryVoltage: 35, zsComfortTemp: 101, zsEcoTemp: 102, zsHeatingSetpointAuto: 105, zsOpenwindowTemp: 116, zsOpenwindowTime: 117, zsErrorStatus: 45, zsMode: 2, zsAwaySetting: 103, zsBinaryOne: 106, zsBinaryTwo: 107, zsScheduleMonday: 109, zsScheduleTuesday: 110, zsScheduleWednesday: 111, zsScheduleThursday: 112, zsScheduleFriday: 113, zsScheduleSaturday: 114, zsScheduleSunday: 115, }; exports.dataPoints = dataPoints; function firstDpValue(msg, meta, converterName) { const dpValues = msg.data.dpValues; for (let index = 1; index < dpValues.length; index++) { logger_1.logger.debug(`Additional DP #${dpValues[index].dp} with data ${JSON.stringify(dpValues[index])} will be ignored! \ Use a for loop in the fromZigbee converter (see \ https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)`, `zhc:${converterName}`); } return dpValues[0]; } exports.firstDpValue = firstDpValue; const numberWithinRange = (number, min, max) => { if (number > max) { return max; } else if (number < min) { return min; } else { return number; } }; const holdUpdateBrightness324131092621 = (deviceID) => { if (fromZigbeeStore[deviceID] && fromZigbeeStore[deviceID].brightnessSince && fromZigbeeStore[deviceID].brightnessDirection) { const duration = Date.now() - fromZigbeeStore[deviceID].brightnessSince; const delta = (duration / 10) * (fromZigbeeStore[deviceID].brightnessDirection === 'up' ? 1 : -1); const newValue = fromZigbeeStore[deviceID].brightnessValue + delta; fromZigbeeStore[deviceID].brightnessValue = numberWithinRange(newValue, 1, 255); } }; function getMetaValue(entity, definition, key, groupStrategy = 'first') { if (entity.constructor.name === 'Group' && entity.members.length > 0) { const values = []; for (const memberMeta of definition) { if (memberMeta.meta && memberMeta.meta.hasOwnProperty(key)) { if (groupStrategy === 'first') { return memberMeta.meta[key]; } values.push(memberMeta.meta[key]); } else { values.push(undefined); } } if (groupStrategy === 'allEqual' && (new Set(values)).size === 1) { return values[0]; } } else if (definition && definition.meta && definition.meta.hasOwnProperty(key)) { return definition.meta[key]; } return undefined; } exports.getMetaValue = getMetaValue; const tuyaGetDataValue = (dataType, data) => { switch (dataType) { case dataTypes.raw: return data; case dataTypes.bool: return data[0] === 1; case dataTypes.value: return convertMultiByteNumberPayloadToSingleDecimalNumber(data); case dataTypes.string: // eslint-disable-next-line let dataString = ''; // Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091 for (let i = 0; i < data.length; ++i) { dataString += String.fromCharCode(data[i]); } return dataString; case dataTypes.enum: return data[0]; case dataTypes.bitmap: return convertMultiByteNumberPayloadToSingleDecimalNumber(data); } }; exports.tuyaGetDataValue = tuyaGetDataValue; const toPercentage = (value, min, max) => { if (value > max) { value = max; } else if (value < min) { value = min; } const normalised = (value - min) / (max - min); return Math.round(normalised * 100); }; const postfixWithEndpointName = (name, msg, definition) => { if (definition.meta && definition.meta.multiEndpoint) { const endpointName = definition.hasOwnProperty('endpoint') ? getKey(definition.endpoint(msg.device), msg.endpoint.ID) : msg.endpoint.ID; return `${name}_${endpointName}`; } else { return name; } }; const transactionStore = {}; const hasAlreadyProcessedMessage = (msg, model, transaction = null, key = null) => { if (model.meta && model.meta.publishDuplicateTransaction) return false; const current = transaction !== null ? transaction : msg.meta.zclTransactionSequenceNumber; key = key || msg.device.ieeeAddr; if (transactionStore[key] === current) return true; transactionStore[key] = current; return false; }; const addActionGroup = (payload, msg, definition) => { const disableActionGroup = definition.meta && definition.meta.disableActionGroup; if (!disableActionGroup && msg.groupID) { payload.action_group = msg.groupID; } }; const ratelimitedDimmer = (model, msg, publish, options, meta) => { const deviceID = msg.device.ieeeAddr; const payload = {}; let duration = 0; if (!fromZigbeeStore[deviceID]) { fromZigbeeStore[deviceID] = { lastmsg: false }; } const s = fromZigbeeStore[deviceID]; if (s.lastmsg) { duration = Date.now() - s.lastmsg; } else { s.lastmsg = Date.now(); } if (duration > 500) { s.lastmsg = Date.now(); payload.action = 'brightness'; payload.brightness = msg.data.level; publish(payload); } }; const ictcg1 = (model, msg, publish, options, action) => { const deviceID = msg.device.ieeeAddr; const payload = {}; if (!fromZigbeeStore[deviceID]) { fromZigbeeStore[deviceID] = { since: false, direction: false, value: 255, publish: publish }; } const s = fromZigbeeStore[deviceID]; // if rate == 70 so we rotate slowly const rate = (msg.data.rate == 70) ? 0.3 : 1; if (action === 'move') { s.since = Date.now(); const direction = msg.data.movemode === 1 ? 'left' : 'right'; s.direction = direction; payload.action = `rotate_${direction}`; } else if (action === 'level') { s.value = msg.data.level; const direction = s.value === 0 ? 'left' : 'right'; payload.action = `rotate_${direction}_quick`; payload.brightness = s.value; } else if (action === 'stop') { if (s.direction) { const duration = Date.now() - s.since; const delta = Math.round(rate * (duration / 10) * (s.direction === 'left' ? -1 : 1)); const newValue = s.value + delta; if (newValue >= 0 && newValue <= 255) { s.value = newValue; } } payload.action = 'rotate_stop'; payload.brightness = s.value; s.direction = false; } if (s.timerId) { clearInterval(s.timerId); s.timerId = false; } if (action === 'move') { s.timerId = setInterval(() => { const duration = Date.now() - s.since; const delta = Math.round(rate * (duration / 10) * (s.direction === 'left' ? -1 : 1)); const newValue = s.value + delta; if (newValue >= 0 && newValue <= 255) { s.value = newValue; } payload.brightness = s.value; s.since = Date.now(); s.publish(payload); }, 200); } return payload.brightness; }; exports.ictcg1 = ictcg1; const SAFETY_MIN_SECS = 10; const CAPACITY = 'capacity'; const DURATION = 'duration'; const OFF = 'OFF'; const ON = 'ON'; const toLocalTime = (time, timezone) => { if (time === '--:--:--') { return time; } const local = new Date(`2000-01-01T${time}.000${timezone}`); // Using 1970 instead produces edge cases return local.toTimeString().split(' ').shift(); }; const giexFzModelConverters = { QT06_1: { // _TZE200_sh1btabb timezone is GMT+8 time: (value) => toLocalTime(value, '+08:00'), }, }; const giexTzModelConverters = { QT06_2: { // _TZE200_a7sghmms irrigation time should not be less than 10 secs as per GiEX advice irrigationTarget: (value, mode) => value > 0 && value < SAFETY_MIN_SECS && mode === DURATION ? SAFETY_MIN_SECS : value, }, }; const giexWaterValve = { battery: 'battery', currentTemperature: 'current_temperature', cycleIrrigationInterval: 'cycle_irrigation_interval', cycleIrrigationNumTimes: 'cycle_irrigation_num_times', irrigationEndTime: 'irrigation_end_time', irrigationStartTime: 'irrigation_start_time', irrigationTarget: 'irrigation_target', lastIrrigationDuration: 'last_irrigation_duration', mode: 'mode', state: 'state', waterConsumed: 'water_consumed', }; exports.giexWaterValve = giexWaterValve; const fromZigbee1 = { TS0222: { cluster: 'manuSpecificTuya', type: ['commandDataResponse', 'commandDataReport'], convert: (model, msg, publish, options, meta) => { const result = {}; for (const dpValue of msg.data.dpValues) { const dp = dpValue.dp; const value = getDataValue(dpValue); switch (dp) { case 2: result.illuminance = value; result.illuminance_lux = value; break; case 4: result.battery = value; break; default: logger_1.logger.debug(`Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`, 'zhc:legacy:fz:ts0222'); } } return result; }, }, watering_timer: { cluster: 'manuSpecificTuya', type: ['commandDataReport'], convert: (model, msg, publish, options, meta) => { const result = {}; for (const dpValue of msg.data.dpValues) { const dp = dpValue.dp; // First we get the data point ID const value = getDataValue(dpValue); // This function will take care of converting the data to proper JS type switch (dp) { case dataPoints.wateringTimer.water_flow: { result.water_flow = value; break; } case dataPoints.wateringTimer.remaining_watering_time: { result.remaining_watering_time = value; break; } case dataPoints.wateringTimer.last_watering_duration: { result.last_watering_duration = value; break; } case dataPoints.wateringTimer.valve_state: { result.valve_state = value; break; } case dataPoints.wateringTimer.shutdown_timer: { result.shutdown_timer = value; break; } case dataPoints.wateringTimer.valve_state_auto_shutdown: { result.valve_state_auto_shutdown = value; result.valve_state = value; break; } case dataPoints.wateringTimer.battery: { result.battery = value; break; } default: { logger_1.logger.debug(`>>> UNKNOWN DP #${dp} with data "${JSON.stringify(dpValue)}"`, 'zhc:legacy:fz:watering_timer'); } } } return result; }, }, ZM35HQ_battery: { cluster: 'manuSpecificTuya', type: ['commandDataReport'], convert: (model, msg, publish, options, meta) => { const dpValue = firstDpValue(msg, meta, 'ZM35HQ'); const dp = dpValue.dp; const value = getDataValue(dpValue); if (dp === 4) return { battery: value }; else { logger_1.logger.debug(`Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`, 'zhc:legacy:fz:zm35hq'); } }, }, ZMRM02: { cluster: 'manuSpecificTuya', type: ['commandGetData', 'commandSetDataResponse', 'commandDataResponse'], convert: (model, msg, publish, options, meta) => { const dpValue = firstDpValue(msg, meta, 'ZMRM02'); if (dpValue.dp === 10) { return { battery: getDataValue(dpValue) }; } else { const button = dpValue.dp; const actionValue = getDataValue(dpValue); const lookup = { 0: 'single', 1: 'double', 2: 'hold' }; const action = lookup[actionValue]; return { action: `button_${button}_${action}` }; } }, }, SA12IZL: { cluster: 'manuSpecificTuya', type: ['commandDataResponse', 'commandDataReport'], convert: (model, msg, publish, options, meta) => { const result = {}; for (const dpValue of msg.data.dpValues) { const dp = dpValue.dp; const value = getDataValue(dpValue); switch (dp) { case dataPoints.state: result.smoke = value === 0; break; case 15: result.battery = value; break; case 16: result.silence_siren = value; break; case 20: { const alarm = { 0: true, 1: false }; result.alarm = alarm[value]; break; } default: logger_1.logger.debug(`Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`, 'zhc:legacy:fz:sa12izl'); } } return result; }, }, R7049_status: { cluster: 'manuSpecificTuya', type: ['commandDataResponse', 'commandDataReport'], convert: (model, msg, publish, options, meta) => { const result = {}; for (const dpValue of msg.data.dpValues) { const dp = dpValue.dp; // First we get the data point ID const value = getDataValue(dpValue); // This function will take care of converting the data to proper JS type switch (dp) { case 1: result.smoke = Boolean(!value); break; case 8: result.test_alarm = value; break; case 9: { const testAlarmResult = { 0: 'checking', 1: 'check_success', 2: 'check_failure', 3: 'others' }; result.test_alarm_result = testAlarmResult[value]; break; } case 11: result.fault_alarm = Boolean(value); break; case 14: { const batteryLevels = { 0: 'low', 1: 'middle', 2: 'high' }; result.battery_level = batteryLevels[value]; result.battery_low = value === 0; break; } case 16: result.silence_siren = value; break; case 20: { const alarm = { 0: true, 1: false }; result.alarm = alarm[value]; break; } default: logger_1.logger.debug(`Unrecognized DP #${dp} with data ${JSON.stringify(dpValue)}`, 'zhc:legacy:fz:r7049_status'); } } return result; }, }, woox_R7060: { cluster: 'manuSpecificTuya', type: ['commandActiveStatusReport'], convert: (model, msg, publish, options, meta) => { const dpValue = firstDpValue(msg, meta, 'woox_R7060'); const dp = dpValue.dp; const value = getDataValue(dpValue); switch (dp) { case dataPoints.wooxSwitch: return { state: value === 2 ? 'OFF' : 'ON' }; case 101: return { batt