UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

985 lines • 238 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; }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../lib/utils"); const globalStore = __importStar(require("../lib/store")); const constants = __importStar(require("../lib/constants")); const libColor = __importStar(require("../lib/color")); const utils = __importStar(require("../lib/utils")); const exposes = __importStar(require("../lib/exposes")); const logger_1 = require("../lib/logger"); const NS = 'zhc:fz'; const defaultSimulatedBrightness = 255; const e = exposes.presets; const ea = exposes.access; const converters1 = { // #region Generic/recommended converters fan: { cluster: 'hvacFanCtrl', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('fanMode')) { const key = (0, utils_1.getKey)(constants.fanMode, msg.data.fanMode); return { fan_mode: key, fan_state: key === 'off' ? 'OFF' : 'ON' }; } }, }, thermostat: { cluster: 'hvacThermostat', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; const dontMapPIHeatingDemand = model.meta && model.meta.thermostat && model.meta.thermostat.dontMapPIHeatingDemand; if (msg.data.hasOwnProperty('localTemp')) { const value = (0, utils_1.precisionRound)(msg.data['localTemp'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('local_temperature', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('localTemperatureCalibration')) { result[(0, utils_1.postfixWithEndpointName)('local_temperature_calibration', msg, model, meta)] = (0, utils_1.precisionRound)(msg.data['localTemperatureCalibration'], 2) / 10; } if (msg.data.hasOwnProperty('outdoorTemp')) { const value = (0, utils_1.precisionRound)(msg.data['outdoorTemp'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('outdoor_temperature', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('occupancy')) { result[(0, utils_1.postfixWithEndpointName)('occupancy', msg, model, meta)] = (msg.data.occupancy % 2) > 0; } if (msg.data.hasOwnProperty('occupiedHeatingSetpoint')) { const value = (0, utils_1.precisionRound)(msg.data['occupiedHeatingSetpoint'], 2) / 100; // Stelpro will return -325.65 when set to off, value is not realistic anyway if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('occupied_heating_setpoint', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('unoccupiedHeatingSetpoint')) { result[(0, utils_1.postfixWithEndpointName)('unoccupied_heating_setpoint', msg, model, meta)] = (0, utils_1.precisionRound)(msg.data['unoccupiedHeatingSetpoint'], 2) / 100; } if (msg.data.hasOwnProperty('occupiedCoolingSetpoint')) { result[(0, utils_1.postfixWithEndpointName)('occupied_cooling_setpoint', msg, model, meta)] = (0, utils_1.precisionRound)(msg.data['occupiedCoolingSetpoint'], 2) / 100; } if (msg.data.hasOwnProperty('unoccupiedCoolingSetpoint')) { result[(0, utils_1.postfixWithEndpointName)('unoccupied_cooling_setpoint', msg, model, meta)] = (0, utils_1.precisionRound)(msg.data['unoccupiedCoolingSetpoint'], 2) / 100; } if (msg.data.hasOwnProperty('setpointChangeAmount')) { result[(0, utils_1.postfixWithEndpointName)('setpoint_change_amount', msg, model, meta)] = msg.data['setpointChangeAmount'] / 100; } if (msg.data.hasOwnProperty('setpointChangeSource')) { const lookup = { 0: 'manual', 1: 'schedule', 2: 'externally' }; result[(0, utils_1.postfixWithEndpointName)('setpoint_change_source', msg, model, meta)] = lookup[msg.data['setpointChangeSource']]; } if (msg.data.hasOwnProperty('setpointChangeSourceTimeStamp')) { const date = new Date(2000, 0, 1); date.setSeconds(msg.data['setpointChangeSourceTimeStamp']); const value = (0, utils_1.toLocalISOString)(date); result[(0, utils_1.postfixWithEndpointName)('setpoint_change_source_timestamp', msg, model, meta)] = value; } if (msg.data.hasOwnProperty('remoteSensing')) { const value = msg.data['remoteSensing']; result[(0, utils_1.postfixWithEndpointName)('remote_sensing', msg, model, meta)] = { local_temperature: ((value & 1) > 0) ? 'remotely' : 'internally', outdoor_temperature: ((value & 1 << 1) > 0) ? 'remotely' : 'internally', occupancy: ((value & 1 << 2) > 0) ? 'remotely' : 'internally', }; } if (msg.data.hasOwnProperty('ctrlSeqeOfOper')) { result[(0, utils_1.postfixWithEndpointName)('control_sequence_of_operation', msg, model, meta)] = constants.thermostatControlSequenceOfOperations[msg.data['ctrlSeqeOfOper']]; } if (msg.data.hasOwnProperty('programingOperMode')) { result[(0, utils_1.postfixWithEndpointName)('programming_operation_mode', msg, model, meta)] = constants.thermostatProgrammingOperationModes[msg.data['programingOperMode']]; } if (msg.data.hasOwnProperty('systemMode')) { result[(0, utils_1.postfixWithEndpointName)('system_mode', msg, model, meta)] = constants.thermostatSystemModes[msg.data['systemMode']]; } if (msg.data.hasOwnProperty('runningMode')) { result[(0, utils_1.postfixWithEndpointName)('running_mode', msg, model, meta)] = constants.thermostatRunningMode[msg.data['runningMode']]; } if (msg.data.hasOwnProperty('runningState')) { result[(0, utils_1.postfixWithEndpointName)('running_state', msg, model, meta)] = constants.thermostatRunningStates[msg.data['runningState']]; } if (msg.data.hasOwnProperty('pIHeatingDemand')) { result[(0, utils_1.postfixWithEndpointName)('pi_heating_demand', msg, model, meta)] = (0, utils_1.mapNumberRange)(msg.data['pIHeatingDemand'], 0, (dontMapPIHeatingDemand ? 100 : 255), 0, 100); } if (msg.data.hasOwnProperty('pICoolingDemand')) { // we assume the behavior is consistent for pIHeatingDemand + pICoolingDemand for the same vendor result[(0, utils_1.postfixWithEndpointName)('pi_cooling_demand', msg, model, meta)] = (0, utils_1.mapNumberRange)(msg.data['pICoolingDemand'], 0, (dontMapPIHeatingDemand ? 100 : 255), 0, 100); } if (msg.data.hasOwnProperty('tempSetpointHold')) { result[(0, utils_1.postfixWithEndpointName)('temperature_setpoint_hold', msg, model, meta)] = msg.data['tempSetpointHold'] == 1; } if (msg.data.hasOwnProperty('tempSetpointHoldDuration')) { result[(0, utils_1.postfixWithEndpointName)('temperature_setpoint_hold_duration', msg, model, meta)] = msg.data['tempSetpointHoldDuration']; } if (msg.data.hasOwnProperty('minHeatSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['minHeatSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('min_heat_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('maxHeatSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['maxHeatSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('max_heat_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('absMinHeatSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['absMinHeatSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('abs_min_heat_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('absMaxHeatSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['absMaxHeatSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('abs_max_heat_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('absMinCoolSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['absMinCoolSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('abs_min_cool_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('absMaxCoolSetpointLimit')) { const value = (0, utils_1.precisionRound)(msg.data['absMaxCoolSetpointLimit'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('abs_max_cool_setpoint_limit', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('minSetpointDeadBand')) { const value = (0, utils_1.precisionRound)(msg.data['minSetpointDeadBand'], 2) / 100; if (value >= -273.15) { result[(0, utils_1.postfixWithEndpointName)('min_setpoint_dead_band', msg, model, meta)] = value; } } if (msg.data.hasOwnProperty('acLouverPosition')) { result[(0, utils_1.postfixWithEndpointName)('ac_louver_position', msg, model, meta)] = constants.thermostatAcLouverPositions[msg.data['acLouverPosition']]; } return result; }, }, thermostat_weekly_schedule: { cluster: 'hvacThermostat', type: ['commandGetWeeklyScheduleRsp'], convert: (model, msg, publish, options, meta) => { const days = []; for (let i = 0; i < 8; i++) { if ((msg.data['dayofweek'] & 1 << i) > 0) { days.push(constants.thermostatDayOfWeek[i]); } } const transitions = []; for (const transition of msg.data.transitions) { const entry = { time: transition.transitionTime }; if (transition.hasOwnProperty('heatSetpoint')) { entry['heating_setpoint'] = transition['heatSetpoint'] / 100; } if (transition.hasOwnProperty('coolSetpoint')) { entry['cooling_setpoint'] = transition['coolSetpoint'] / 100; } transitions.push(entry); } return { [(0, utils_1.postfixWithEndpointName)('weekly_schedule', msg, model, meta)]: { days, transitions } }; }, }, hvac_user_interface: { cluster: 'hvacUserInterfaceCfg', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('keypadLockout')) { result.keypad_lockout = constants.keypadLockoutMode.hasOwnProperty(msg.data['keypadLockout']) ? constants.keypadLockoutMode[msg.data['keypadLockout']] : msg.data['keypadLockout']; } if (msg.data.hasOwnProperty('tempDisplayMode')) { result.temperature_display_mode = constants.temperatureDisplayMode.hasOwnProperty(msg.data['tempDisplayMode']) ? constants.temperatureDisplayMode[msg.data['tempDisplayMode']] : msg.data['tempDisplayMode']; } return result; }, }, lock_operation_event: { cluster: 'closuresDoorLock', type: 'commandOperationEventNotification', convert: (model, msg, publish, options, meta) => { const lookup = { 0: 'unknown', 1: 'lock', 2: 'unlock', 3: 'lock_failure_invalid_pin_or_id', 4: 'lock_failure_invalid_schedule', 5: 'unlock_failure_invalid_pin_or_id', 6: 'unlock_failure_invalid_schedule', 7: 'one_touch_lock', 8: 'key_lock', 9: 'key_unlock', 10: 'auto_lock', 11: 'schedule_lock', 12: 'schedule_unlock', 13: 'manual_lock', 14: 'manual_unlock', 15: 'non_access_user_operational_event', }; return { action: lookup[msg.data['opereventcode']], action_user: msg.data['userid'], action_source: msg.data['opereventsrc'], action_source_name: constants.lockSourceName[msg.data['opereventsrc']], }; }, }, lock_programming_event: { cluster: 'closuresDoorLock', type: 'commandProgrammingEventNotification', convert: (model, msg, publish, options, meta) => { const lookup = { 0: 'unknown', 1: 'master_code_changed', 2: 'pin_code_added', 3: 'pin_code_deleted', 4: 'pin_code_changed', 5: 'rfid_code_added', 6: 'rfid_code_deleted', }; return { action: lookup[msg.data['programeventcode']], action_user: msg.data['userid'], action_source: msg.data['programeventsrc'], action_source_name: constants.lockSourceName[msg.data['programeventsrc']], }; }, }, lock: { cluster: 'closuresDoorLock', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('lockState')) { result.state = msg.data.lockState == 1 ? 'LOCK' : 'UNLOCK'; const lookup = ['not_fully_locked', 'locked', 'unlocked']; result.lock_state = lookup[msg.data['lockState']]; } if (msg.data.hasOwnProperty('autoRelockTime')) { result.auto_relock_time = msg.data.autoRelockTime; } if (msg.data.hasOwnProperty('soundVolume')) { result.sound_volume = constants.lockSoundVolume[msg.data.soundVolume]; } if (msg.data.hasOwnProperty('doorState')) { const lookup = { 0: 'open', 1: 'closed', 2: 'error_jammed', 3: 'error_forced_open', 4: 'error_unspecified', 0xff: 'undefined' }; result.door_state = lookup[msg.data['doorState']]; } return result; }, }, lock_pin_code_response: { cluster: 'closuresDoorLock', type: ['commandGetPinCodeRsp'], options: [exposes.options.expose_pin()], convert: (model, msg, publish, options, meta) => { const { data } = msg; let status = constants.lockUserStatus[data.userstatus]; if (status === undefined) { status = `not_supported_${data.userstatus}`; } const userId = data.userid.toString(); const result = { users: {} }; result.users[userId] = { status: status }; if (options && options.expose_pin && data.pincodevalue) { result.users[userId].pin_code = data.pincodevalue; } return result; }, }, lock_user_status_response: { cluster: 'closuresDoorLock', type: ['commandGetUserStatusRsp'], options: [exposes.options.expose_pin()], convert: (model, msg, publish, options, meta) => { const { data } = msg; let status = constants.lockUserStatus[data.userstatus]; if (status === undefined) { status = `not_supported_${data.userstatus}`; } const userId = data.userid.toString(); const result = { users: {} }; result.users[userId] = { status: status }; if (options && options.expose_pin && data.pincodevalue) { result.users[userId].pin_code = data.pincodevalue; } return result; }, }, linkquality_from_basic: { cluster: 'genBasic', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { return { linkquality: msg.linkquality }; }, }, battery: { cluster: 'genPowerCfg', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const payload = {}; if (msg.data.hasOwnProperty('batteryPercentageRemaining') && (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). const dontDividePercentage = model.meta && model.meta.battery && model.meta.battery.dontDividePercentage; let percentage = msg.data['batteryPercentageRemaining']; percentage = dontDividePercentage ? percentage : percentage / 2; payload.battery = (0, utils_1.precisionRound)(percentage, 2); } if (msg.data.hasOwnProperty('batteryVoltage') && (msg.data['batteryVoltage'] < 255)) { // Deprecated: voltage is = mV now but should be V payload.voltage = msg.data['batteryVoltage'] * 100; if (model.meta && model.meta.battery && model.meta.battery.voltageToPercentage) { payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, model.meta.battery.voltageToPercentage); } } if (msg.data.hasOwnProperty('batteryAlarmState')) { 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; payload.battery_low = battery1Low || battery2Low || battery3Low; } return payload; }, }, temperature: { cluster: 'msTemperatureMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('measuredValue')) { const temperature = parseFloat(msg.data['measuredValue']) / 100.0; const property = (0, utils_1.postfixWithEndpointName)('temperature', msg, model, meta); return { [property]: temperature }; } }, }, device_temperature: { cluster: 'genDeviceTempCfg', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('currentTemperature')) { const value = parseInt(msg.data['currentTemperature']); return { device_temperature: value }; } }, }, humidity: { cluster: 'msRelativeHumidity', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const humidity = parseFloat(msg.data['measuredValue']) / 100.0; const property = (0, utils_1.postfixWithEndpointName)('humidity', msg, model, meta); // https://github.com/Koenkk/zigbee2mqtt/issues/798 // Sometimes the sensor publishes non-realistic vales, it should only publish message // in the 0 - 100 range, don't produce messages beyond these values. if (humidity >= 0 && humidity <= 100) { return { [property]: humidity }; } }, }, pm25: { cluster: 'pm25Measurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('measuredValue')) { return { pm25: msg.data['measuredValue'] }; } }, }, flow: { cluster: 'msFlowMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const flow = parseFloat(msg.data['measuredValue']) / 10.0; const property = (0, utils_1.postfixWithEndpointName)('flow', msg, model, meta); if (msg.data.hasOwnProperty('measuredValue')) { return { [property]: flow }; } }, }, soil_moisture: { cluster: 'msSoilMoisture', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const soilMoisture = parseFloat(msg.data['measuredValue']) / 100.0; return { soil_moisture: soilMoisture }; }, }, illuminance: { cluster: 'msIlluminanceMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { // DEPRECATED: only return lux here (change illuminance_lux -> illuminance) const illuminance = msg.data['measuredValue']; const illuminanceLux = illuminance === 0 ? 0 : Math.pow(10, (illuminance - 1) / 10000); return { illuminance: illuminance, illuminance_lux: illuminanceLux }; }, }, pressure: { cluster: 'msPressureMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { let pressure = 0; if (msg.data.hasOwnProperty('scaledValue')) { const scale = msg.endpoint.getClusterAttributeValue('msPressureMeasurement', 'scale'); pressure = msg.data['scaledValue'] / Math.pow(10, scale) / 100.0; // convert to hPa } else { pressure = parseFloat(msg.data['measuredValue']); } return { pressure }; }, }, co2: { cluster: 'msCO2', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { return { co2: Math.floor(msg.data.measuredValue * 1000000) }; }, }, occupancy: { // This is for occupancy sensor that send motion start AND stop messages cluster: 'msOccupancySensing', type: ['attributeReport', 'readResponse'], options: [exposes.options.no_occupancy_since_false()], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('occupancy')) { const payload = { occupancy: (msg.data.occupancy % 2) > 0 }; utils.noOccupancySince(msg.endpoint, options, publish, payload.occupancy ? 'stop' : 'start'); return payload; } }, }, occupancy_with_timeout: { // This is for occupancy sensor that only send a message when motion detected, // but do not send a motion stop. // Therefore we need to publish the no_motion detected by ourselves. cluster: 'msOccupancySensing', type: ['attributeReport', 'readResponse'], options: [exposes.options.occupancy_timeout(), exposes.options.no_occupancy_since_true()], convert: (model, msg, publish, options, meta) => { if (msg.data.occupancy !== 1) { // In case of 0 no occupancy is reported. // https://github.com/Koenkk/zigbee2mqtt/issues/467 return; } // The occupancy sensor only sends a message when motion detected. // Therefore we need to publish the no_motion detected by ourselves. const timeout = options && options.hasOwnProperty('occupancy_timeout') ? Number(options.occupancy_timeout) : 90; // Stop existing timers because motion is detected and set a new one. clearTimeout(globalStore.getValue(msg.endpoint, 'occupancy_timer', null)); if (timeout !== 0) { const timer = setTimeout(() => { publish({ occupancy: false }); }, timeout * 1000); globalStore.putValue(msg.endpoint, 'occupancy_timer', timer); } const payload = { occupancy: true }; utils.noOccupancySince(msg.endpoint, options, publish, 'start'); return payload; }, }, occupancy_timeout: { cluster: 'msOccupancySensing', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('pirOToUDelay')) { return { occupancy_timeout: msg.data.pirOToUDelay }; } }, }, brightness: { cluster: 'genLevelCtrl', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('currentLevel')) { const property = (0, utils_1.postfixWithEndpointName)('brightness', msg, model, meta); return { [property]: msg.data['currentLevel'] }; } }, }, level_config: { cluster: 'genLevelCtrl', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = { 'level_config': {} }; // onOffTransitionTime - range 0x0000 to 0xffff - optional if (msg.data.hasOwnProperty('onOffTransitionTime') && (msg.data['onOffTransitionTime'] !== undefined)) { result.level_config.on_off_transition_time = Number(msg.data['onOffTransitionTime']); } // onTransitionTime - range 0x0000 to 0xffff - optional // 0xffff = use onOffTransitionTime if (msg.data.hasOwnProperty('onTransitionTime') && (msg.data['onTransitionTime'] !== undefined)) { result.level_config.on_transition_time = Number(msg.data['onTransitionTime']); if (result.level_config.on_transition_time == 65535) { result.level_config.on_transition_time = 'disabled'; } } // offTransitionTime - range 0x0000 to 0xffff - optional // 0xffff = use onOffTransitionTime if (msg.data.hasOwnProperty('offTransitionTime') && (msg.data['offTransitionTime'] !== undefined)) { result.level_config.off_transition_time = Number(msg.data['offTransitionTime']); if (result.level_config.off_transition_time == 65535) { result.level_config.off_transition_time = 'disabled'; } } // startUpCurrentLevel - range 0x00 to 0xff - optional // 0x00 = return to minimum supported level // 0xff - return to previous previous if (msg.data.hasOwnProperty('startUpCurrentLevel') && (msg.data['startUpCurrentLevel'] !== undefined)) { result.level_config.current_level_startup = Number(msg.data['startUpCurrentLevel']); if (result.level_config.current_level_startup == 255) { result.level_config.current_level_startup = 'previous'; } if (result.level_config.current_level_startup == 0) { result.level_config.current_level_startup = 'minimum'; } } // onLevel - range 0x00 to 0xff - optional // Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous". if (msg.data.hasOwnProperty('onLevel') && (msg.data['onLevel'] !== undefined)) { result.level_config.on_level = Number(msg.data['onLevel']); if (result.level_config.on_level === 255) { result.level_config.on_level = 'previous'; } } // options - 8-bit map // bit 0: ExecuteIfOff - when 0, Move commands are ignored if the device is off; // when 1, CurrentLevel can be changed while the device is off. // bit 1: CoupleColorTempToLevel - when 1, changes to level also change color temperature. // (What this means is not defined, but it's most likely to be "dim to warm".) if (msg.data.hasOwnProperty('options') && msg.data['options'] !== undefined) { result.level_config.execute_if_off = !!(Number(msg.data['options']) & 1); } if (Object.keys(result.level_config).length > 0) { return result; } }, }, color_colortemp: { cluster: 'lightingColorCtrl', type: ['attributeReport', 'readResponse'], options: [exposes.options.color_sync()], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('colorTemperature')) { result.color_temp = msg.data['colorTemperature']; } if (msg.data.hasOwnProperty('startUpColorTemperature')) { result.color_temp_startup = msg.data['startUpColorTemperature']; } if (msg.data.hasOwnProperty('colorMode')) { result.color_mode = constants.colorModeLookup.hasOwnProperty(msg.data['colorMode']) ? constants.colorModeLookup[msg.data['colorMode']] : msg.data['colorMode']; } if (msg.data.hasOwnProperty('currentX') || msg.data.hasOwnProperty('currentY') || msg.data.hasOwnProperty('currentSaturation') || msg.data.hasOwnProperty('currentHue') || msg.data.hasOwnProperty('enhancedCurrentHue')) { result.color = {}; if (msg.data.hasOwnProperty('currentX')) { result.color.x = (0, utils_1.mapNumberRange)(msg.data['currentX'], 0, 65535, 0, 1, 4); } if (msg.data.hasOwnProperty('currentY')) { result.color.y = (0, utils_1.mapNumberRange)(msg.data['currentY'], 0, 65535, 0, 1, 4); } if (msg.data.hasOwnProperty('currentSaturation')) { result.color.saturation = (0, utils_1.mapNumberRange)(msg.data['currentSaturation'], 0, 254, 0, 100); } if (msg.data.hasOwnProperty('currentHue')) { result.color.hue = (0, utils_1.mapNumberRange)(msg.data['currentHue'], 0, 254, 0, 360, 0); } if (msg.data.hasOwnProperty('enhancedCurrentHue')) { result.color.hue = (0, utils_1.mapNumberRange)(msg.data['enhancedCurrentHue'], 0, 65535, 0, 360, 1); } } if (msg.data.hasOwnProperty('options')) { /* * Bit | Value & Summary * -------------------------- * 0 | 0: Do not execute command if the On/Off cluster, OnOff attribute is 0x00 (FALSE) * | 1: Execute command if the On/Off cluster, OnOff attribute is 0x00 (FALSE) */ result.color_options = { execute_if_off: ((msg.data.options & 1 << 0) > 0) }; } // handle color property sync // NOTE: this should the last thing we do, as we need to have processed all attributes, // we use assign here so we do not lose other attributes. return Object.assign(result, libColor.syncColorState(result, meta.state, msg.endpoint, options)); }, }, meter_identification: { cluster: 'haMeterIdentification', type: ['readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; const elements = [ /* 0x000A*/ 'softwareRevision', /* 0x000D*/ 'availablePower', /* 0x000E*/ 'powerThreshold', ]; for (const at of elements) { const atSnake = at.split(/(?=[A-Z])/).join('_').toLowerCase(); if (msg.data[at]) { result[atSnake] = msg.data[at]; } } return result; }, }, metering: { /** * When using this converter also add the following to the configure method of the device: * await readMeteringPowerConverterAttributes(endpoint); */ cluster: 'seMetering', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (utils.hasAlreadyProcessedMessage(msg, model)) return; const payload = {}; const multiplier = msg.endpoint.getClusterAttributeValue('seMetering', 'multiplier'); const divisor = msg.endpoint.getClusterAttributeValue('seMetering', 'divisor'); const factor = multiplier && divisor ? multiplier / divisor : null; if (msg.data.hasOwnProperty('instantaneousDemand')) { let power = msg.data['instantaneousDemand']; if (factor != null) { power = (power * factor) * 1000; // kWh to Watt } const property = (0, utils_1.postfixWithEndpointName)('power', msg, model, meta); payload[property] = power; } if (factor != null && (msg.data.hasOwnProperty('currentSummDelivered') || msg.data.hasOwnProperty('currentSummReceived'))) { if (msg.data.hasOwnProperty('currentSummDelivered')) { const data = msg.data['currentSummDelivered']; const value = (parseInt(data[0]) << 32) + parseInt(data[1]); const property = (0, utils_1.postfixWithEndpointName)('energy', msg, model, meta); payload[property] = value * factor; } if (msg.data.hasOwnProperty('currentSummReceived')) { const data = msg.data['currentSummReceived']; const value = (parseInt(data[0]) << 32) + parseInt(data[1]); const property = (0, utils_1.postfixWithEndpointName)('produced_energy', msg, model, meta); payload[property] = value * factor; } } return payload; }, }, electrical_measurement: { /** * When using this converter also add the following to the configure method of the device: * await readEletricalMeasurementConverterAttributes(endpoint); */ cluster: 'haElectricalMeasurement', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (utils.hasAlreadyProcessedMessage(msg, model)) return; const getFactor = (key) => { const multiplier = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', `${key}Multiplier`); const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', `${key}Divisor`); const factor = multiplier && divisor ? multiplier / divisor : 1; return factor; }; const lookup = [ { key: 'activePower', name: 'power', factor: 'acPower' }, { key: 'activePowerPhB', name: 'power_phase_b', factor: 'acPower' }, { key: 'activePowerPhC', name: 'power_phase_c', factor: 'acPower' }, { key: 'apparentPower', name: 'power_apparent', factor: 'acPower' }, { key: 'apparentPowerPhB', name: 'power_apparent_phase_b', factor: 'acPower' }, { key: 'apparentPowerPhC', name: 'power_apparent_phase_c', factor: 'acPower' }, { key: 'reactivePower', name: 'power_reactive', factor: 'acPower' }, { key: 'reactivePowerPhB', name: 'power_reactive_phase_b', factor: 'acPower' }, { key: 'reactivePowerPhC', name: 'power_reactive_phase_c', factor: 'acPower' }, { key: 'rmsCurrent', name: 'current', factor: 'acCurrent' }, { key: 'rmsCurrentPhB', name: 'current_phase_b', factor: 'acCurrent' }, { key: 'rmsCurrentPhC', name: 'current_phase_c', factor: 'acCurrent' }, { key: 'rmsVoltage', name: 'voltage', factor: 'acVoltage' }, { key: 'rmsVoltagePhB', name: 'voltage_phase_b', factor: 'acVoltage' }, { key: 'rmsVoltagePhC', name: 'voltage_phase_c', factor: 'acVoltage' }, { key: 'acFrequency', name: 'ac_frequency', factor: 'acFrequency' }, { key: 'dcPower', name: 'power', factor: 'dcPower' }, { key: 'dcCurrent', name: 'current', factor: 'dcCurrent' }, { key: 'dcVoltage', name: 'voltage', factor: 'dcVoltage' }, ]; const payload = {}; for (const entry of lookup) { if (msg.data.hasOwnProperty(entry.key)) { const factor = getFactor(entry.factor); const property = (0, utils_1.postfixWithEndpointName)(entry.name, msg, model, meta); const value = msg.data[entry.key] * factor; payload[property] = value; } } if (msg.data.hasOwnProperty('powerFactor')) { const property = (0, utils_1.postfixWithEndpointName)('power_factor', msg, model, meta); payload[property] = (0, utils_1.precisionRound)(msg.data['powerFactor'] / 100, 2); } if (msg.data.hasOwnProperty('powerFactorPhB')) { const property = (0, utils_1.postfixWithEndpointName)('power_factor_phase_b', msg, model, meta); payload[property] = (0, utils_1.precisionRound)(msg.data['powerFactorPhB'] / 100, 2); } if (msg.data.hasOwnProperty('powerFactorPhC')) { const property = (0, utils_1.postfixWithEndpointName)('power_factor_phase_c', msg, model, meta); payload[property] = (0, utils_1.precisionRound)(msg.data['powerFactorPhC'] / 100, 2); } return payload; }, }, on_off: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], options: [exposes.options.state_action()], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('onOff')) { const payload = {}; const property = (0, utils_1.postfixWithEndpointName)('state', msg, model, meta); const state = msg.data['onOff'] === 1 ? 'ON' : 'OFF'; payload[property] = state; if (options && options.state_action) { payload['action'] = (0, utils_1.postfixWithEndpointName)(state.toLowerCase(), msg, model, meta); } return payload; } }, }, on_off_force_multiendpoint: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], options: [exposes.options.state_action()], convert: (model, msg, publish, options, meta) => { // This converted is need instead of `fz.on_off` when no meta: {multiEndpoint: true} can be defined for this device // but it is needed for the `state`. E.g. when a switch has 3 channels (state_l1, state_l2, state_l3) but // has combined power measurements (power, energy)) if (msg.data.hasOwnProperty('onOff')) { const payload = {}; const endpointName = model.hasOwnProperty('endpoint') ? utils.getKey(model.endpoint(meta.device), msg.endpoint.ID) : msg.endpoint.ID; const state = msg.data['onOff'] === 1 ? 'ON' : 'OFF'; payload[`state_${endpointName}`] = state; if (options && options.state_action) { payload['action'] = `${state.toLowerCase()}_${endpointName}`; } return payload; } }, }, on_off_skip_duplicate_transaction: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], options: [exposes.options.state_action()], convert: (model, msg, publish, options, meta) => { // Device sends multiple messages with the same transactionSequenceNumber, // prevent that multiple messages get send. // https://github.com/Koenkk/zigbee2mqtt/issues/3687 if (msg.data.hasOwnProperty('onOff') && !(0, utils_1.hasAlreadyProcessedMessage)(msg, model)) { const payload = {}; const property = (0, utils_1.postfixWithEndpointName)('state', msg, model, meta); const state = msg.data['onOff'] === 1 ? 'ON' : 'OFF'; payload[property] = state; if (options && options.state_action) { payload['action'] = (0, utils_1.postfixWithEndpointName)(state.toLowerCase(), msg, model, meta); } return payload; } }, }, power_on_behavior: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const lookup = { 0: 'off', 1: 'on', 2: 'toggle', 255: 'previous' }; if (msg.data.hasOwnProperty('startUpOnOff')) { const property = (0, utils_1.postfixWithEndpointName)('power_on_behavior', msg, model, meta); return { [property]: lookup[msg.data['startUpOnOff']] }; } }, }, ias_no_alarm: { cluster: 'ssIasZone', type: ['attributeReport', 'commandStatusChangeNotification'], convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zoneStatus; return { tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_siren: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; return { alarm: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, supervision_reports: (zoneStatus & 1 << 4) > 0, restore_reports: (zoneStatus & 1 << 5) > 0, ac_status: (zoneStatus & 1 << 7) > 0, test: (zoneStatus & 1 << 8) > 0, }; }, }, ias_water_leak_alarm_1: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; return { water_leak: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_water_leak_alarm_1_report: { cluster: 'ssIasZone', type: 'attributeReport', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zoneStatus; return { water_leak: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_vibration_alarm_1: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; return { vibration: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_vibration_alarm_1_with_timeout: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', options: [exposes.options.vibration_timeout()], convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; const timeout = options && options.hasOwnProperty('vibration_timeout') ? Number(options.vibration_timeout) : 90; // Stop existing timers because vibration is detected and set a new one. globalStore.getValue(msg.endpoint, 'timers', []).forEach((t) => clearTimeout(t)); globalStore.putValue(msg.endpoint, 'timers', []); if (timeout !== 0) { const timer = setTimeout(() => { publish({ vibration: false }); }, timeout * 1000); globalStore.getValue(msg.endpoint, 'timers').push(timer); } return { vibration: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_gas_alarm_1: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; return { gas: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_gas_alarm_2: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.zonestatus; return { gas: (zoneStatus & 1 << 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, }; }, }, ias_smoke_alarm_1: { cluster: 'ssIasZone', type: ['commandStatusChangeNotification', 'attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.type === 'commandStatusChangeNotification' ? msg.data.zonestatus : msg.data.zoneStatus; return { smoke: (zoneStatus & 1) > 0, tamper: (zoneStatus & 1 << 2) > 0, battery_low: (zoneStatus & 1 << 3) > 0, supervision_reports: (zoneStatus & 1 << 4) > 0, restore_reports: (zoneStatus & 1 << 5) > 0, trouble: (zoneStatus & 1 << 6) > 0, ac_status: (zoneStatus & 1 << 7) > 0, test: (zoneStatus & 1 << 8) > 0, battery_defect: (zoneStatus & 1 << 9) > 0, }; }, }, ias_contact_alarm_1: { cluster: 'ssIasZone', type: 'commandStatusChangeNotification', convert: (model, msg, publish, options, meta) => { const zoneStatus = msg.data.z