zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
853 lines (852 loc) • 241 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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);