UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

853 lines (852 loc) • 241 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.ias_occupancy_alarm_1 = exports.ias_sos_alarm_2 = exports.ias_carbon_monoxide_alarm_1_gas_alarm_2 = exports.ias_carbon_monoxide_alarm_1 = exports.ias_contact_alarm_1_report = exports.ias_contact_alarm_1 = exports.ias_smoke_alarm_1 = exports.ias_gas_alarm_2 = exports.ias_gas_alarm_1 = exports.ias_vibration_alarm_1_with_timeout = exports.ias_vibration_alarm_1 = exports.ias_water_leak_alarm_1_report = exports.ias_water_leak_alarm_1 = exports.ias_siren = exports.ias_no_alarm = exports.power_on_behavior = exports.on_off_skip_duplicate_transaction = exports.on_off_force_multiendpoint = exports.on_off = exports.gas_metering = exports.electrical_measurement = exports.metering = exports.meter_identification = exports.color_colortemp = exports.level_config = exports.brightness = exports.occupancy_timeout = exports.occupancy_with_timeout = exports.occupancy = exports.co2 = exports.pressure = exports.soil_moisture = exports.flow = exports.pm25 = exports.humidity = exports.device_temperature = exports.temperature = exports.battery = exports.linkquality_from_basic = exports.lock_user_status_response = exports.lock_pin_code_response = exports.lock_set_pin_code_response = exports.lock = exports.lock_programming_event = exports.lock_operation_event = exports.hvac_user_interface = exports.thermostat_weekly_schedule = exports.thermostat = exports.fan_speed = exports.fan = void 0; exports.ts0201_temperature_humidity_alarm = exports.ias_smoke_alarm_1_develco = exports.elko_thermostat = exports.namron_hvac_user_interface = exports.namron_thermostat = exports.power_source = exports.ias_wd = exports.ias_enroll = exports.checkin_presence = exports.lighting_ballast_configuration = exports.curtain_position_analog_output = exports.cover_state_via_onoff = exports.cover_position_via_brightness = exports.cover_position_tilt = exports.identify = exports.command_off_state = exports.command_on_state = exports.command_emergency = exports.command_move_to_hue = exports.command_move_to_saturation = exports.command_move_hue = exports.command_move_to_color = exports.command_move_to_color_temp = exports.command_color_loop_set = exports.command_step_saturation = exports.command_step_hue = exports.command_move_to_hue_and_saturation = exports.command_enhanced_move_to_hue_and_saturation = exports.command_step_color_temperature = exports.command_move_color_temperature = exports.command_stop = exports.command_step = exports.command_move = exports.command_move_to_level = exports.command_toggle = exports.command_off_with_effect = exports.command_off = exports.command_on = exports.command_cover_close = exports.command_cover_open = exports.command_cover_stop = exports.command_arm = exports.command_panic = exports.command_recall = exports.command_store = exports.ias_occupancy_alarm_1_with_timeout = exports.ias_occupancy_only_alarm_2 = exports.ias_alarm_only_alarm_1 = exports.ias_occupancy_alarm_2 = exports.ias_occupancy_alarm_1_report = void 0; exports.enocean_ptm215z = exports.K4003C_binary_input = exports.diyruz_rspm = exports.diyruz_contact = exports.ewelink_action = exports.restorable_brightness = exports.tint404011_move_to_color_temp = exports.tint_scene = exports.orvibo_raw_2 = exports.orvibo_raw_1 = exports.danfoss_icon_hvac_user_interface = exports.danfoss_icon_regulator = exports.danfoss_icon_battery = exports.danfoss_icon_floor_sensor = exports.danfoss_thermostat_setpoint_scheduled = exports.danfoss_thermostat = exports.meazon_meter = exports.heiman_ir_remote = exports.plaid_battery = exports.keypad20_battery = exports.keypad20states = exports.ptvo_switch_analog_input = exports.ptvo_switch_uart = exports.livolo_switch_state_raw = exports.easycodetouch_action = exports.easycode_action = exports.livolo_pir_state = exports.livolo_illuminance_state = exports.livolo_hygrometer_state = exports.livolo_cover_state = exports.livolo_dimmer_state = exports.livolo_curtain_switch_state = exports.livolo_new_switch_state_4gang = exports.livolo_new_switch_state_2gang = exports.livolo_new_switch_state = exports.livolo_socket_state = exports.livolo_switch_state = exports.tuya_switch_scene = exports.WSZ01_on_off_action = exports.tuya_cover_options = exports.tuya_cover_options_2 = exports.ts0216_siren = exports.terncy_temperature = exports.terncy_contact = exports.ZigUP = exports.DTB190502A1 = exports.terncy_knob = exports.tuya_doorbell_button = exports.wiser_device_info = exports.tuya_led_controller = void 0; exports.CC2530ROUTER_led = exports.diyruz_zintercom_config = exports.diyruz_airsense_config_hum = exports.diyruz_airsense_config_pres = exports.diyruz_airsense_config_temp = exports.diyruz_airsense_config_co2 = exports.diyruz_geiger_config = exports.diyruz_geiger = exports.diyruz_freepad_config = exports.javis_lock_report = exports.heiman_scenes = exports.STS_PRS_251_presence = exports.PGC410EU_presence = exports.ZMCSW032D_cover_position = exports.SAGE206612_state = exports.almond_click = exports.color_stop_raw = exports.adeo_button_65024 = exports.scenes_recall_scene_65024 = exports.heiman_air_quality = exports.heiman_hcho = exports.U02I007C01_water_leak = exports.U02I007C01_contact = exports.keen_home_smart_vent_pressure = exports.qlwz_letv8key_switch = exports.konke_action = exports.ptvo_multistate_action = exports.command_status_change_notification_action = exports.W2_module_carbon_monoxide = exports.legrand_greenpower = exports.legrand_power_alarm = exports.legrand_pilot_wire_mode = exports.legrand_master_switch_center = exports.legrand_scenes = exports.bticino_4027C_binary_input_moving = exports.legrand_binary_input_on_off = exports.legrand_binary_input_moving = exports.hue_smart_button_event = exports.byun_gas_true = exports.byun_gas_false = exports.byun_smoke_true = exports.byun_smoke_false = exports.smartthings_acceleration = exports._3310_humidity = exports.kmpcil_res005_on_off = exports.kmpcil_res005_occupancy = exports.diyruz_freepad_clicks = exports._8840100H_water_leak_alarm = exports.enocean_ptm216z = exports.enocean_ptm215ze = void 0; exports.ignore_multistate_report = exports.ignore_analog_report = exports.ignore_pressure_report = exports.ignore_humidity_report = exports.ignore_temperature_report = exports.ignore_occupancy_report = exports.ignore_illuminance_report = exports.ignore_basic_report = exports.ignore_onoff_report = exports.awox_refresh = exports.awox_refreshColored = exports.awox_colors = exports.SNZB02_humidity = exports.SNZB02_temperature = exports.hw_version = exports.led_on_motion = exports.tuya_multi_action = exports.command_stop_move_raw = exports.sunricher_switch2801K4 = exports.sunricher_switch2801K2 = exports.tuya_operation_mode = exports.sihas_action = exports.sihas_people_cnt = exports.heiman_doorbell_button = exports.rc_110_level_to_scene = exports.wiser_smart_setpoint_command_client = exports.wiser_smart_thermostat_client = exports.schneider_temperature = exports.schneider_ui_action = exports.schneider_pilot_mode = exports.idlock_fw = exports.idlock = exports.ZB003X_occupancy = exports.ZB003X_attr = exports.itcmdr_clicks = exports.ias_keypad = exports.tuya_relay_din_led_indicator = exports.hue_twilight = exports.hue_tap = exports.hue_dimmer_switch = exports.hue_wall_switch = exports.CCTSwitch_D0001_lighting = exports.CCTSwitch_D0001_levelctrl = exports.hue_wall_switch_device_mode = exports.hue_motion_led_indication = exports.hue_motion_sensitivity = exports.DNCKAT_S00X_buttons = exports.KAMI_occupancy = exports.KAMI_contact = exports.CC2530ROUTER_meta = void 0; exports.TS110E_switch_type = exports.TS110E_light_type = exports.TS110E = exports.wiser_smart_thermostat = exports.wiser_lighting_ballast_configuration = exports.schneider_lighting_ballast_configuration = exports.ZM35HQ_attr = exports.terncy_raw = exports.eurotronic_thermostat = exports.viessmann_thermostat = exports.stelpro_thermostat = exports.SP600_power = exports.ias_ace_occupancy_with_timeout = exports.command_on_presence = exports.EKO09738_metering = exports.metering_datek = exports.command_arm_with_transaction = exports.ignore_electrical_measurement = exports.ignore_metering = exports.ignore_tuya_raw = exports.ignore_tuya_set_time = exports.ignore_time_read = exports.ignore_zclversion_read = exports.ignore_haDiagnostic = exports.ignore_genOta = exports.ignore_genLevelCtrl_report = exports.ignore_poll_ctrl = exports.ignore_command_stop = exports.ignore_command_step = exports.ignore_command_off_with_effect = exports.ignore_command_off = exports.ignore_command_on = exports.ignore_genIdentify = exports.ignore_iasace_commandgetpanelstatus = exports.ignore_iaszone_report = exports.ignore_iaszone_statuschange = exports.ignore_iaszone_attreport = exports.ignore_thermostat_report = exports.ignore_closuresWindowCovering_report = exports.ignore_light_color_colortemp_report = exports.ignore_light_brightness_report = exports.ignore_power_report = void 0; const libColor = __importStar(require("../lib/color")); const constants = __importStar(require("../lib/constants")); const exposes = __importStar(require("../lib/exposes")); const logger_1 = require("../lib/logger"); const globalStore = __importStar(require("../lib/store")); const utils_1 = require("../lib/utils"); const utils = __importStar(require("../lib/utils")); const NS = "zhc:fz"; const defaultSimulatedBrightness = 255; const e = exposes.presets; const ea = exposes.access; // #region Generic/recommended converters exports.fan = { cluster: "hvacFanCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.fanMode !== undefined) { const key = (0, utils_1.getKey)(constants.fanMode, msg.data.fanMode); return { fan_mode: key, fan_state: key === "off" ? "OFF" : "ON" }; } }, }; exports.fan_speed = { cluster: "genLevelCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.currentLevel !== undefined) { const property = (0, utils_1.postfixWithEndpointName)("speed", msg, model, meta); return { [property]: msg.data.currentLevel }; } }, }; exports.thermostat = { cluster: "hvacThermostat", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const dontMapPIHeatingDemand = model.meta?.thermostat?.dontMapPIHeatingDemand; if (msg.data.localTemp !== undefined) { 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.localTemperatureCalibration !== undefined) { result[(0, utils_1.postfixWithEndpointName)("local_temperature_calibration", msg, model, meta)] = (0, utils_1.precisionRound)(msg.data.localTemperatureCalibration, 2) / 10; } if (msg.data.outdoorTemp !== undefined) { 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.occupancy !== undefined) { result[(0, utils_1.postfixWithEndpointName)("occupancy", msg, model, meta)] = msg.data.occupancy % 2 > 0; } if (msg.data.occupiedHeatingSetpoint !== undefined) { 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.unoccupiedHeatingSetpoint !== undefined) { result[(0, utils_1.postfixWithEndpointName)("unoccupied_heating_setpoint", msg, model, meta)] = (0, utils_1.precisionRound)(msg.data.unoccupiedHeatingSetpoint, 2) / 100; } if (msg.data.occupiedCoolingSetpoint !== undefined) { result[(0, utils_1.postfixWithEndpointName)("occupied_cooling_setpoint", msg, model, meta)] = (0, utils_1.precisionRound)(msg.data.occupiedCoolingSetpoint, 2) / 100; } if (msg.data.unoccupiedCoolingSetpoint !== undefined) { result[(0, utils_1.postfixWithEndpointName)("unoccupied_cooling_setpoint", msg, model, meta)] = (0, utils_1.precisionRound)(msg.data.unoccupiedCoolingSetpoint, 2) / 100; } if (msg.data.setpointChangeAmount !== undefined) { result[(0, utils_1.postfixWithEndpointName)("setpoint_change_amount", msg, model, meta)] = msg.data.setpointChangeAmount / 100; } if (msg.data.setpointChangeSource !== undefined) { 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.setpointChangeSourceTimeStamp !== undefined) { 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.remoteSensing !== undefined) { 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.ctrlSeqeOfOper !== undefined) { result[(0, utils_1.postfixWithEndpointName)("control_sequence_of_operation", msg, model, meta)] = constants.thermostatControlSequenceOfOperations[msg.data.ctrlSeqeOfOper]; } if (msg.data.programingOperMode !== undefined) { result[(0, utils_1.postfixWithEndpointName)("programming_operation_mode", msg, model, meta)] = constants.thermostatProgrammingOperationModes[msg.data.programingOperMode]; } if (msg.data.systemMode !== undefined) { result[(0, utils_1.postfixWithEndpointName)("system_mode", msg, model, meta)] = constants.thermostatSystemModes[msg.data.systemMode]; } if (msg.data.runningMode !== undefined) { result[(0, utils_1.postfixWithEndpointName)("running_mode", msg, model, meta)] = constants.thermostatRunningMode[msg.data.runningMode]; } if (msg.data.runningState !== undefined) { result[(0, utils_1.postfixWithEndpointName)("running_state", msg, model, meta)] = constants.thermostatRunningStates[msg.data.runningState]; } if (msg.data.pIHeatingDemand !== undefined) { 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.pICoolingDemand !== undefined) { // 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.tempSetpointHold !== undefined) { result[(0, utils_1.postfixWithEndpointName)("temperature_setpoint_hold", msg, model, meta)] = msg.data.tempSetpointHold === 1; } if (msg.data.tempSetpointHoldDuration !== undefined) { result[(0, utils_1.postfixWithEndpointName)("temperature_setpoint_hold_duration", msg, model, meta)] = msg.data.tempSetpointHoldDuration; } if (msg.data.minHeatSetpointLimit !== undefined) { 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.maxHeatSetpointLimit !== undefined) { 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.absMinHeatSetpointLimit !== undefined) { 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.absMaxHeatSetpointLimit !== undefined) { 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.absMinCoolSetpointLimit !== undefined) { 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.absMaxCoolSetpointLimit !== undefined) { 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.minSetpointDeadBand !== undefined) { 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.acLouverPosition !== undefined) { result[(0, utils_1.postfixWithEndpointName)("ac_louver_position", msg, model, meta)] = constants.thermostatAcLouverPositions[msg.data.acLouverPosition]; } return result; }, }; exports.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.heatSetpoint !== undefined) { entry.heating_setpoint = transition.heatSetpoint / 100; } if (transition.coolSetpoint !== undefined) { entry.cooling_setpoint = transition.coolSetpoint / 100; } transitions.push(entry); } return { [(0, utils_1.postfixWithEndpointName)("weekly_schedule", msg, model, meta)]: { days, transitions } }; }, }; exports.hvac_user_interface = { cluster: "hvacUserInterfaceCfg", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.keypadLockout !== undefined) { result.keypad_lockout = constants.keypadLockoutMode[msg.data.keypadLockout] !== undefined ? constants.keypadLockoutMode[msg.data.keypadLockout] : msg.data.keypadLockout; } if (msg.data.tempDisplayMode !== undefined) { result.temperature_display_mode = constants.temperatureDisplayMode[msg.data.tempDisplayMode] !== undefined ? constants.temperatureDisplayMode[msg.data.tempDisplayMode] : msg.data.tempDisplayMode; } return result; }, }; exports.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], }; }, }; exports.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], }; }, }; exports.lock = { cluster: "closuresDoorLock", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.lockState !== undefined) { result[(0, utils_1.postfixWithEndpointName)("state", msg, model, meta)] = msg.data.lockState === 1 ? "LOCK" : "UNLOCK"; const lookup = ["not_fully_locked", "locked", "unlocked"]; result[(0, utils_1.postfixWithEndpointName)("lock_state", msg, model, meta)] = lookup[msg.data.lockState]; } if (msg.data.autoRelockTime !== undefined) { result[(0, utils_1.postfixWithEndpointName)("auto_relock_time", msg, model, meta)] = msg.data.autoRelockTime; } if (msg.data.soundVolume !== undefined) { result[(0, utils_1.postfixWithEndpointName)("sound_volume", msg, model, meta)] = constants.lockSoundVolume[msg.data.soundVolume]; } if (msg.data.doorState !== undefined) { const lookup = { 0: "open", 1: "closed", 2: "error_jammed", 3: "error_forced_open", 4: "error_unspecified", 255: "undefined", }; result[(0, utils_1.postfixWithEndpointName)("door_state", msg, model, meta)] = lookup[msg.data.doorState]; } return result; }, }; exports.lock_set_pin_code_response = { cluster: "closuresDoorLock", type: ["commandSetPinCodeRsp", "commandClearPinCodeRsp"], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.status === 0) { if (msg.type === "commandSetPinCodeRsp") { result.last_successful_pincode_save = Date.now(); } if (msg.type === "commandClearPinCodeRsp") { result.last_successful_pincode_clear = Date.now(); } } if (Object.keys(result).length > 0) { return result; } }, }; exports.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?.expose_pin && data.pincodevalue) { result.users[userId].pin_code = data.pincodevalue; } return result; }, }; exports.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?.expose_pin && data.pincodevalue) { result.users[userId].pin_code = data.pincodevalue; } return result; }, }; exports.linkquality_from_basic = { cluster: "genBasic", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { return { linkquality: msg.linkquality }; }, }; exports.battery = { cluster: "genPowerCfg", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const payload = {}; // If voltageToPercentage is specified, it means we do not trust the percentage // returned by the device and are instead calculating it ourselves. if (model.meta?.battery?.voltageToPercentage == null && msg.data.batteryPercentageRemaining !== undefined && msg.data.batteryPercentageRemaining < 255) { // Some devices do not comply to the ZCL and report a // batteryPercentageRemaining of 100 when the battery is full (should be 200). const dontDividePercentage = 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.batteryVoltage !== undefined && msg.data.batteryVoltage < 255) { // Deprecated: voltage is = mV now but should be V payload.voltage = msg.data.batteryVoltage * 100; if (model.meta?.battery?.voltageToPercentage) { payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, model.meta.battery.voltageToPercentage); } } if (msg.data.batteryAlarmState !== undefined) { const battery1Low = (msg.data.batteryAlarmState & (1 << 0) || msg.data.batteryAlarmState & (1 << 1) || msg.data.batteryAlarmState & (1 << 2) || msg.data.batteryAlarmState & (1 << 3)) > 0; const battery2Low = (msg.data.batteryAlarmState & (1 << 10) || msg.data.batteryAlarmState & (1 << 11) || msg.data.batteryAlarmState & (1 << 12) || msg.data.batteryAlarmState & (1 << 13)) > 0; const battery3Low = (msg.data.batteryAlarmState & (1 << 20) || msg.data.batteryAlarmState & (1 << 21) || msg.data.batteryAlarmState & (1 << 22) || msg.data.batteryAlarmState & (1 << 23)) > 0; payload.battery_low = battery1Low || battery2Low || battery3Low; } return payload; }, }; exports.temperature = { cluster: "msTemperatureMeasurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.measuredValue !== undefined) { const temperature = Number.parseFloat(msg.data.measuredValue) / 100.0; const property = (0, utils_1.postfixWithEndpointName)("temperature", msg, model, meta); return { [property]: temperature }; } }, }; exports.device_temperature = { cluster: "genDeviceTempCfg", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.currentTemperature !== undefined) { const value = Number.parseInt(msg.data.currentTemperature); return { device_temperature: value }; } }, }; exports.humidity = { cluster: "msRelativeHumidity", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const humidity = Number.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 }; } }, }; exports.pm25 = { cluster: "pm25Measurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.measuredValue !== undefined) { return { pm25: msg.data.measuredValue }; } }, }; exports.flow = { cluster: "msFlowMeasurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const flow = Number.parseFloat(msg.data.measuredValue) / 10.0; const property = (0, utils_1.postfixWithEndpointName)("flow", msg, model, meta); if (msg.data.measuredValue !== undefined) { return { [property]: flow }; } }, }; exports.soil_moisture = { cluster: "msSoilMoisture", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const soilMoisture = Number.parseFloat(msg.data.measuredValue) / 100.0; return { soil_moisture: soilMoisture }; }, }; exports.pressure = { cluster: "msPressureMeasurement", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { let pressure = 0; if (msg.data.scaledValue !== undefined) { const scale = msg.endpoint.getClusterAttributeValue("msPressureMeasurement", "scale"); pressure = msg.data.scaledValue / 10 ** scale / 100.0; // convert to hPa } else { pressure = Number.parseFloat(msg.data.measuredValue); } return { pressure }; }, }; exports.co2 = { cluster: "msCO2", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { return { co2: Math.floor(msg.data.measuredValue * 1000000) }; }, }; exports.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.occupancy !== undefined) { const payload = { occupancy: msg.data.occupancy % 2 > 0 }; utils.noOccupancySince(msg.endpoint, options, publish, payload.occupancy ? "stop" : "start"); return payload; } }, }; exports.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?.occupancy_timeout != null ? 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; }, }; exports.occupancy_timeout = { cluster: "msOccupancySensing", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.pirOToUDelay !== undefined) { return { occupancy_timeout: msg.data.pirOToUDelay }; } }, }; exports.brightness = { cluster: "genLevelCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.currentLevel !== undefined) { const property = (0, utils_1.postfixWithEndpointName)("brightness", msg, model, meta); return { [property]: msg.data.currentLevel }; } }, }; exports.level_config = { cluster: "genLevelCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const level_config = (0, utils_1.postfixWithEndpointName)("level_config", msg, model, meta); const result = {}; result[level_config] = {}; // onOffTransitionTime - range 0x0000 to 0xffff - optional if (msg.data.onOffTransitionTime !== undefined && 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.onTransitionTime !== undefined && 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.offTransitionTime !== undefined && 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.startUpCurrentLevel !== undefined && 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.onLevel !== undefined && 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.options !== undefined) { result[level_config].execute_if_off = !!(Number(msg.data.options) & 1); } if (Object.keys(result[level_config]).length > 0) { return result; } }, }; exports.color_colortemp = { cluster: "lightingColorCtrl", type: ["attributeReport", "readResponse"], options: [exposes.options.color_sync()], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.colorTemperature !== undefined) { const color_temp = (0, utils_1.postfixWithEndpointName)("color_temp", msg, model, meta); result[color_temp] = msg.data.colorTemperature; } if (msg.data.startUpColorTemperature !== undefined) { const color_temp_startup = (0, utils_1.postfixWithEndpointName)("color_temp_startup", msg, model, meta); result[color_temp_startup] = msg.data.startUpColorTemperature; } if (msg.data.colorMode !== undefined) { const color_mode = (0, utils_1.postfixWithEndpointName)("color_mode", msg, model, meta); result[color_mode] = constants.colorModeLookup[msg.data.colorMode] !== undefined ? constants.colorModeLookup[msg.data.colorMode] : msg.data.colorMode; } if (msg.data.currentX !== undefined || msg.data.currentY !== undefined || msg.data.currentSaturation !== undefined || msg.data.currentHue !== undefined || msg.data.enhancedCurrentHue !== undefined) { const color = (0, utils_1.postfixWithEndpointName)("color", msg, model, meta); result[color] = {}; if (msg.data.currentX !== undefined) { result[color].x = (0, utils_1.mapNumberRange)(msg.data.currentX, 0, 65535, 0, 1, 4); } if (msg.data.currentY !== undefined) { result[color].y = (0, utils_1.mapNumberRange)(msg.data.currentY, 0, 65535, 0, 1, 4); } if (msg.data.currentSaturation !== undefined) { result[color].saturation = (0, utils_1.mapNumberRange)(msg.data.currentSaturation, 0, 254, 0, 100); } if (msg.data.currentHue !== undefined) { result[color].hue = (0, utils_1.mapNumberRange)(msg.data.currentHue, 0, 254, 0, 360, 0); } if (msg.data.enhancedCurrentHue !== undefined) { result[color].hue = (0, utils_1.mapNumberRange)(msg.data.enhancedCurrentHue, 0, 65535, 0, 360, 1); } } if (msg.data.options !== undefined) { /* * 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) */ const color_options = (0, utils_1.postfixWithEndpointName)("color_options", msg, model, meta); result[color_options] = { execute_if_off: (msg.data.options & (1 << 0)) > 0 }; } // Use postfixWithEndpointName with an empty value to get just the postfix that // needs to be added to the result key. const epPostfix = (0, utils_1.postfixWithEndpointName)("", msg, model, meta); // 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, epPostfix)); }, }; exports.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; }, }; exports.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.instantaneousDemand !== undefined) { 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 (msg.data.currentSummDelivered !== undefined) { const value = msg.data.currentSummDelivered; const property = (0, utils_1.postfixWithEndpointName)("energy", msg, model, meta); payload[property] = value * (factor ?? 1); } if (msg.data.currentSummReceived !== undefined) { const value = msg.data.currentSummReceived; const property = (0, utils_1.postfixWithEndpointName)("produced_energy", msg, model, meta); payload[property] = value * (factor ?? 1); } return payload; }, }; exports.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: "neutralCurrent", name: "current_neutral", 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[entry.key] !== undefined) { 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.powerFactor !== undefined) { 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.powerFactorPhB !== undefined) { 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.powerFactorPhC !== undefined) { 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; }, }; exports.gas_metering = { 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.instantaneousDemand !== undefined) { const power = msg.data.instantaneousDemand; const property = utils.postfixWithEndpointName("power", msg, model, meta); payload[property] = utils.precisionRound(power * (factor ?? 1), 2); } if (msg.data.currentSummDelivered !== undefined) { const value = msg.data.currentSummDelivered; const property = utils.postfixWithEndpointName("energy", msg, model, meta); payload[property] = utils.precisionRound(value * (factor ?? 1), 2); } if (msg.data.status !== undefined) { const value = msg.data.status; const property = utils.postfixWithEndpointName("status", msg, model, meta);