zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
985 lines • 238 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 (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