zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
952 lines • 215 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 zigbee_herdsman_1 = require("zigbee-herdsman");
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 light = __importStar(require("../lib/light"));
const legacy = __importStar(require("../lib/legacy"));
const exposes = __importStar(require("../lib/exposes"));
const logger_1 = require("../lib/logger");
const NS = 'zhc:tz';
const manufacturerOptions = {
sunricher: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD },
lumi: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, disableDefaultResponse: true },
eurotronic: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.NXP_SEMICONDUCTORS },
danfoss: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DANFOSS_A_S },
hue: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V },
ikea: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.IKEA_OF_SWEDEN },
sinope: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SINOPE_TECHNOLOGIES },
tint: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC },
legrand: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP, disableDefaultResponse: true },
viessmann: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.VIESSMANN_ELEKTRONIK_GMBH },
nodon: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.NODON },
};
const converters1 = {
on_off: {
key: ['state', 'on_time', 'off_wait_time'],
convertSet: async (entity, key, value, meta) => {
const state = utils.isString(meta.message.state) ? meta.message.state.toLowerCase() : null;
utils.validateValue(state, ['toggle', 'off', 'on']);
if (state === 'on' && (meta.message.hasOwnProperty('on_time') || meta.message.hasOwnProperty('off_wait_time'))) {
const onTime = meta.message.hasOwnProperty('on_time') ? meta.message.on_time : 0;
const offWaitTime = meta.message.hasOwnProperty('off_wait_time') ? meta.message.off_wait_time : 0;
if (typeof onTime !== 'number') {
throw Error('The on_time value must be a number!');
}
if (typeof offWaitTime !== 'number') {
throw Error('The off_wait_time value must be a number!');
}
const payload = { ctrlbits: 0, ontime: Math.round(onTime * 10), offwaittime: Math.round(offWaitTime * 10) };
await entity.command('genOnOff', 'onWithTimedOff', payload, utils.getOptions(meta.mapped, entity));
}
else {
await entity.command('genOnOff', state, {}, utils.getOptions(meta.mapped, entity));
if (state === 'toggle') {
const currentState = meta.state[`state${meta.endpoint_name ? `_${meta.endpoint_name}` : ''}`];
return currentState ? { state: { state: currentState === 'OFF' ? 'ON' : 'OFF' } } : {};
}
else {
return { state: { state: state.toUpperCase() } };
}
}
},
convertGet: async (entity, key, meta) => {
await entity.read('genOnOff', ['onOff']);
},
},
light_color: {
key: ['color'],
options: [exposes.options.color_sync(), exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
let command;
const newColor = libColor.Color.fromConverterArg(value);
const newState = {};
const zclData = { transtime: utils.getTransition(entity, key, meta).time };
const supportsHueAndSaturation = utils.getMetaValue(entity, meta.mapped, 'supportsHueAndSaturation', 'allEqual', true);
const supportsEnhancedHue = utils.getMetaValue(entity, meta.mapped, 'supportsEnhancedHue', 'allEqual', true);
if (newColor.isHSV() && !supportsHueAndSaturation) {
// The color we got is HSV but the bulb does not support Hue/Saturation mode
throw new Error('This light does not support Hue/Saturation, please use X/Y instead.');
}
else if (newColor.isRGB() || newColor.isXY()) {
// Convert RGB to XY color mode because Zigbee doesn't support RGB (only x/y and hue/saturation)
const xy = newColor.isRGB() ? newColor.rgb.gammaCorrected().toXY().rounded(4) : newColor.xy;
// Some bulbs e.g. RB 185 C don't turn to red (they don't respond at all) when x: 0.701 and y: 0.299
// is send. These values are e.g. send by Home Assistant when clicking red in the color wheel.
// If we slightly modify these values the bulb will respond.
// https://github.com/home-assistant/home-assistant/issues/31094
if (utils.getMetaValue(entity, meta.mapped, 'applyRedFix', 'allEqual', false) && xy.x == 0.701 && xy.y === 0.299) {
xy.x = 0.7006;
xy.y = 0.2993;
}
newState.color_mode = constants.colorModeLookup[1];
newState.color = xy.toObject();
zclData.colorx = utils.mapNumberRange(xy.x, 0, 1, 0, 65535);
zclData.colory = utils.mapNumberRange(xy.y, 0, 1, 0, 65535);
command = 'moveToColor';
}
else if (newColor.isHSV()) {
const hsv = newColor.hsv;
const hsvCorrected = hsv.colorCorrected(meta);
newState.color_mode = constants.colorModeLookup[0];
newState.color = hsv.toObject(false);
if (hsv.hue !== null) {
if (supportsEnhancedHue) {
zclData.enhancehue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535);
}
else {
zclData.hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254);
}
// @ts-expect-error
zclData.direction = value.direction || 0;
}
if (hsv.saturation != null) {
zclData.saturation = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254);
}
if (hsv.value !== null) {
// fallthrough to genLevelCtrl
// @ts-expect-error
value.brightness = utils.mapNumberRange(hsvCorrected.value, 0, 100, 0, 254);
}
if (hsv.hue !== null && hsv.saturation !== null) {
if (supportsEnhancedHue) {
command = 'enhancedMoveToHueAndSaturation';
}
else {
command = 'moveToHueAndSaturation';
}
}
else if (hsv.hue !== null) {
if (supportsEnhancedHue) {
command = 'enhancedMoveToHue';
}
else {
command = 'moveToHue';
}
}
else if (hsv.saturation !== null) {
command = 'moveToSaturation';
}
}
if (value.hasOwnProperty('brightness')) {
await entity.command('genLevelCtrl', 'moveToLevelWithOnOff',
// @ts-expect-error
{ level: Number(value.brightness), transtime: utils.getTransition(entity, key, meta).time }, utils.getOptions(meta.mapped, entity));
}
await entity.command('lightingColorCtrl', command, zclData, utils.getOptions(meta.mapped, entity));
return { state: libColor.syncColorState(newState, meta.state, entity, meta.options),
readAfterWriteTime: zclData.transtime * 100 };
},
convertGet: async (entity, key, meta) => {
await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta));
},
},
light_colortemp: {
key: ['color_temp', 'color_temp_percent'],
options: [exposes.options.color_sync(), exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
const [colorTempMin, colorTempMax] = light.findColorTempRange(entity);
const preset = { 'warmest': colorTempMax, 'warm': 454, 'neutral': 370, 'cool': 250, 'coolest': colorTempMin };
if (key === 'color_temp_percent') {
utils.assertNumber(value);
value = utils.mapNumberRange(value, 0, 100, ((colorTempMin != null) ? colorTempMin : 154), ((colorTempMax != null) ? colorTempMax : 500)).toString();
}
if (utils.isString(value) && value in preset) {
value = utils.getFromLookup(value, preset);
}
value = Number(value);
// ensure value within range
utils.assertNumber(value);
value = light.clampColorTemp(value, colorTempMin, colorTempMax);
const payload = { colortemp: value, transtime: utils.getTransition(entity, key, meta).time };
await entity.command('lightingColorCtrl', 'moveToColorTemp', payload, utils.getOptions(meta.mapped, entity));
return {
state: libColor.syncColorState({ 'color_mode': constants.colorModeLookup[2], 'color_temp': value }, meta.state, entity, meta.options), readAfterWriteTime: payload.transtime * 100,
};
},
convertGet: async (entity, key, meta) => {
await entity.read('lightingColorCtrl', ['colorMode', 'colorTemperature']);
},
},
};
const converters2 = {
// #region Generic converters
read: {
key: ['read'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const result = await entity.read(value.cluster, value.attributes, (value.hasOwnProperty('options') ? value.options : {}));
logger_1.logger.info(`Read result of '${value.cluster}': ${JSON.stringify(result)}`, NS);
if (value.hasOwnProperty('state_property')) {
return { state: { [value.state_property]: result } };
}
},
},
write: {
key: ['write'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const options = utils.getOptions(meta.mapped, entity);
if (value.hasOwnProperty('options')) {
Object.assign(options, value.options);
}
await entity.write(value.cluster, value.payload, options);
logger_1.logger.info(`Wrote '${JSON.stringify(value.payload)}' to '${value.cluster}'`, NS);
},
},
command: {
key: ['command'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const options = utils.getOptions(meta.mapped, entity);
await entity.command(value.cluster, value.command, (value.hasOwnProperty('payload') ? value.payload : {}), options);
logger_1.logger.info(`Invoked '${value.cluster}.${value.command}' with payload '${JSON.stringify(value.payload)}'`, NS);
},
},
factory_reset: {
key: ['reset'],
convertSet: async (entity, key, value, meta) => {
await entity.command('genBasic', 'resetFactDefault', {}, utils.getOptions(meta.mapped, entity));
},
},
identify: {
key: ['identify'],
options: [exposes.options.identify_timeout()],
convertSet: async (entity, key, value, meta) => {
// External value takes priority over options for compatibility
const identifyTimeout = value ?? meta.options.identify_timeout ?? 3;
await entity.command('genIdentify', 'identify', { identifytime: identifyTimeout }, utils.getOptions(meta.mapped, entity));
},
},
zcl_command: {
key: ['zclcommand'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const payload = (value.hasOwnProperty('payload') ? value.payload : {});
utils.assertEndpoint(entity);
await entity.zclCommand(value.cluster, value.command, payload, (value.hasOwnProperty('options') ? value.options : {}));
logger_1.logger.info(`Invoked ZCL command ${value.cluster}.${value.command} with payload '${JSON.stringify(payload)}'`, NS);
},
},
arm_mode: {
key: ['arm_mode'],
convertSet: async (entity, key, value, meta) => {
utils.assertEndpoint(entity);
utils.assertObject(value, key);
if (Array.isArray(meta.mapped))
throw new Error(`Not supported for groups`);
const isNotification = value.hasOwnProperty('transaction');
const modeSrc = isNotification ? constants.armNotification : constants.armMode;
const mode = utils.getKey(modeSrc, value.mode, undefined, Number);
if (mode === undefined) {
throw new Error(`Unsupported mode: '${value.mode}', should be one of: ${Object.values(modeSrc)}`);
}
if (isNotification) {
await entity.commandResponse('ssIasAce', 'armRsp', { armnotification: mode }, {}, value.transaction);
// Do not update PanelStatus after confirming transaction.
// Instead the server should send an arm_mode command with the necessary state.
// e.g. exit_delay as a result of arm_all_zones
return;
}
let panelStatus = mode;
if (meta.mapped.model === '3400-D') {
panelStatus = mode !== 0 && mode !== 4 ? 0x80 : 0x00;
}
globalStore.putValue(entity, 'panelStatus', panelStatus);
const payload = { panelstatus: panelStatus, secondsremain: 0, audiblenotif: 0, alarmstatus: 0 };
await entity.commandResponse('ssIasAce', 'panelStatusChanged', payload);
},
},
battery_percentage_remaining: {
key: ['battery'],
convertGet: async (entity, key, meta) => {
await entity.read('genPowerCfg', ['batteryPercentageRemaining']);
},
},
battery_voltage: {
key: ['battery', 'voltage'],
convertGet: async (entity, key, meta) => {
await entity.read('genPowerCfg', ['batteryVoltage']);
},
},
power_on_behavior: {
key: ['power_on_behavior'],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value, key);
value = value.toLowerCase();
const lookup = { 'off': 0, 'on': 1, 'toggle': 2, 'previous': 255 };
await entity.write('genOnOff', { startUpOnOff: utils.getFromLookup(value, lookup) }, utils.getOptions(meta.mapped, entity));
return { state: { power_on_behavior: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('genOnOff', ['startUpOnOff']);
},
},
light_color_mode: {
key: ['color_mode'],
convertGet: async (entity, key, meta) => {
await entity.read('lightingColorCtrl', ['colorMode']);
},
},
light_color_options: {
key: ['color_options'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const options = (value.hasOwnProperty('execute_if_off') && value.execute_if_off) ? 1 : 0;
await entity.write('lightingColorCtrl', { options }, utils.getOptions(meta.mapped, entity));
return { state: { 'color_options': value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('lightingColorCtrl', ['options']);
},
},
lock: {
key: ['state'],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value, key);
await entity.command('closuresDoorLock', `${value.toLowerCase()}Door`, { 'pincodevalue': '' }, utils.getOptions(meta.mapped, entity));
return { readAfterWriteTime: 200 };
},
convertGet: async (entity, key, meta) => {
await entity.read('closuresDoorLock', ['lockState']);
},
},
lock_auto_relock_time: {
key: ['auto_relock_time'],
convertSet: async (entity, key, value, meta) => {
await entity.write('closuresDoorLock', { autoRelockTime: value }, utils.getOptions(meta.mapped, entity));
return { state: { auto_relock_time: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('closuresDoorLock', ['autoRelockTime']);
},
},
lock_sound_volume: {
key: ['sound_volume'],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value, key);
utils.validateValue(value, constants.lockSoundVolume);
await entity.write('closuresDoorLock', { soundVolume: constants.lockSoundVolume.indexOf(value) }, utils.getOptions(meta.mapped, entity));
return { state: { sound_volume: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('closuresDoorLock', ['soundVolume']);
},
},
pincode_lock: {
key: ['pin_code'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const user = value.user;
const userType = value.user_type || 'unrestricted';
const userEnabled = value.hasOwnProperty('user_enabled') ? value.user_enabled : true;
const pinCode = value.pin_code;
if (isNaN(user))
throw new Error('user must be numbers');
const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount');
if (!utils.isInRange(0, pinCodeCount - 1, user))
throw new Error('user must be in range for device');
if (pinCode == null) {
await entity.command('closuresDoorLock', 'clearPinCode', { 'userid': user }, utils.getOptions(meta.mapped, entity));
}
else {
if (isNaN(pinCode))
throw new Error('pinCode must be a number');
const typeLookup = { 'unrestricted': 0, 'year_day_schedule': 1, 'week_day_schedule': 2, 'master': 3, 'non_access': 4 };
const payload = {
'userid': user,
'userstatus': userEnabled ? 1 : 3,
'usertype': utils.getFromLookup(userType, typeLookup),
'pincodevalue': pinCode.toString(),
};
await entity.command('closuresDoorLock', 'setPinCode', payload, utils.getOptions(meta.mapped, entity));
}
},
convertGet: async (entity, key, meta) => {
// @ts-expect-error
const user = meta && meta.message && meta.message.pin_code ? meta.message.pin_code.user : undefined;
if (user === undefined) {
const max = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount');
// Get all
const options = utils.getOptions(meta.mapped, entity);
for (let i = 0; i < max; i++) {
await entity.command('closuresDoorLock', 'getPinCode', { userid: i }, options);
}
}
else {
if (isNaN(user)) {
throw new Error('user must be numbers');
}
const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount');
if (!utils.isInRange(0, pinCodeCount - 1, user)) {
throw new Error('userId must be in range for device');
}
await entity.command('closuresDoorLock', 'getPinCode', { userid: user }, utils.getOptions(meta.mapped, entity));
}
},
},
lock_userstatus: {
key: ['user_status'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const user = value.user;
if (isNaN(user)) {
throw new Error('user must be numbers');
}
const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount');
if (!utils.isInRange(0, pinCodeCount - 1, user)) {
throw new Error('user must be in range for device');
}
const status = utils.getKey(constants.lockUserStatus, value.status, undefined, Number);
if (status === undefined) {
throw new Error(`Unsupported status: '${value.status}', should be one of: ${Object.values(constants.lockUserStatus)}`);
}
await entity.command('closuresDoorLock', 'setUserStatus', {
'userid': user,
'userstatus': status,
}, utils.getOptions(meta.mapped, entity));
},
convertGet: async (entity, key, meta) => {
// @ts-expect-error
const user = meta && meta.message && meta.message.user_status ? meta.message.user_status.user : undefined;
const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount');
if (user === undefined) {
const max = pinCodeCount;
// Get all
const options = utils.getOptions(meta.mapped, entity);
for (let i = 0; i < max; i++) {
await entity.command('closuresDoorLock', 'getUserStatus', { userid: i }, options);
}
}
else {
if (isNaN(user)) {
throw new Error('user must be numbers');
}
if (!utils.isInRange(0, pinCodeCount - 1, user)) {
throw new Error('userId must be in range for device');
}
await entity.command('closuresDoorLock', 'getUserStatus', { userid: user }, utils.getOptions(meta.mapped, entity));
}
},
},
cover_via_brightness: {
key: ['position', 'state'],
options: [exposes.options.invert_cover()],
convertSet: async (entity, key, value, meta) => {
if (typeof value !== 'number') {
utils.assertString(value, key);
value = value.toLowerCase();
if (value === 'stop') {
await entity.command('genLevelCtrl', 'stop', {}, utils.getOptions(meta.mapped, entity));
return;
}
const lookup = { 'open': 100, 'close': 0 };
value = utils.getFromLookup(value, lookup);
}
const invert = utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
!meta.options.invert_cover : meta.options.invert_cover;
utils.assertNumber(value);
const position = invert ? 100 - value : value;
await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', { level: utils.mapNumberRange(Number(position), 0, 100, 0, 255).toString(), transtime: 0 }, utils.getOptions(meta.mapped, entity));
return { state: { position: value }, readAfterWriteTime: 0 };
},
convertGet: async (entity, key, meta) => {
await entity.read('genLevelCtrl', ['currentLevel']);
},
},
warning: {
key: ['warning'],
convertSet: async (entity, key, value, meta) => {
const mode = { 'stop': 0, 'burglar': 1, 'fire': 2, 'emergency': 3, 'police_panic': 4, 'fire_panic': 5, 'emergency_panic': 6 };
const level = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 };
const strobeLevel = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 };
const values = {
// @ts-expect-error
mode: value.mode || 'emergency',
// @ts-expect-error
level: value.level || 'medium',
// @ts-expect-error
strobe: value.hasOwnProperty('strobe') ? value.strobe : true,
// @ts-expect-error
duration: value.hasOwnProperty('duration') ? value.duration : 10,
// @ts-expect-error
strobeDutyCycle: value.hasOwnProperty('strobe_duty_cycle') ? value.strobe_duty_cycle * 10 : 0,
// @ts-expect-error
strobeLevel: value.hasOwnProperty('strobe_level') ? utils.getFromLookup(value.strobe_level, strobeLevel) : 1,
};
let info;
// https://github.com/Koenkk/zigbee2mqtt/issues/8310 some devices require the info to be reversed.
if (Array.isArray(meta.mapped))
throw new Error(`Not supported for groups`);
if (['SIRZB-110', 'SRAC-23B-ZBSR', 'AV2010/29A', 'AV2010/24A'].includes(meta.mapped.model)) {
info = (utils.getFromLookup(values.mode, mode)) + ((values.strobe ? 1 : 0) << 4) + (utils.getFromLookup(values.level, level) << 6);
}
else {
info = (utils.getFromLookup(values.mode, mode) << 4) + ((values.strobe ? 1 : 0) << 2) + (utils.getFromLookup(values.level, level));
}
await entity.command('ssIasWd', 'startWarning', { startwarninginfo: info, warningduration: values.duration,
strobedutycycle: values.strobeDutyCycle, strobelevel: values.strobeLevel }, utils.getOptions(meta.mapped, entity));
},
},
ias_max_duration: {
key: ['max_duration'],
convertSet: async (entity, key, value, meta) => {
await entity.write('ssIasWd', { 'maxDuration': value });
return { state: { max_duration: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('ssIasWd', ['maxDuration']);
},
},
warning_simple: {
key: ['alarm'],
convertSet: async (entity, key, value, meta) => {
const alarmState = (value === 'alarm' || value === 'OFF' ? 0 : 1);
let info;
// For Develco SMSZB-120 and HESZB-120, introduced change in fw 4.0.5, tested backward with 4.0.4
if (Array.isArray(meta.mapped))
throw new Error(`Not supported for groups`);
if (['SMSZB-120', 'HESZB-120'].includes(meta.mapped.model)) {
info = ((alarmState) << 7) + ((alarmState) << 6);
}
else {
info = (3 << 6) + ((alarmState) << 2);
}
await entity.command('ssIasWd', 'startWarning', { startwarninginfo: info, warningduration: 300, strobedutycycle: 0, strobelevel: 0 }, utils.getOptions(meta.mapped, entity));
},
},
squawk: {
key: ['squawk'],
convertSet: async (entity, key, value, meta) => {
utils.assertObject(value, key);
const state = { 'system_is_armed': 0, 'system_is_disarmed': 1 };
const level = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 };
const values = {
state: value.state,
level: value.level || 'very_high',
strobe: value.hasOwnProperty('strobe') ? value.strobe : false,
};
const info = (utils.getFromLookup(values.state, state)) + ((values.strobe ? 1 : 0) << 4) +
(utils.getFromLookup(values.level, level) << 6);
await entity.command('ssIasWd', 'squawk', { squawkinfo: info }, utils.getOptions(meta.mapped, entity));
},
},
cover_state: {
key: ['state'],
convertSet: async (entity, key, value, meta) => {
const lookup = { 'open': 'upOpen', 'close': 'downClose', 'stop': 'stop', 'on': 'upOpen', 'off': 'downClose' };
utils.assertString(value, key);
value = value.toLowerCase();
await entity.command('closuresWindowCovering', utils.getFromLookup(value, lookup), {}, utils.getOptions(meta.mapped, entity));
},
},
cover_position_tilt: {
key: ['position', 'tilt'],
options: [exposes.options.invert_cover()],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value, key);
const isPosition = (key === 'position');
const invert = !(utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
!meta.options.invert_cover : meta.options.invert_cover);
const position = invert ? 100 - value : value;
// Zigbee officially expects 'open' to be 0 and 'closed' to be 100 whereas
// HomeAssistant etc. work the other way round.
// For zigbee-herdsman-converters: open = 100, close = 0
await entity.command('closuresWindowCovering', isPosition ? 'goToLiftPercentage' : 'goToTiltPercentage', isPosition ? { percentageliftvalue: position } : { percentagetiltvalue: position }, utils.getOptions(meta.mapped, entity));
return { state: { [isPosition ? 'position' : 'tilt']: value } };
},
convertGet: async (entity, key, meta) => {
const isPosition = (key === 'position');
await entity.read('closuresWindowCovering', [isPosition ? 'currentPositionLiftPercentage' : 'currentPositionTiltPercentage']);
},
},
occupancy_timeout: {
// Sets delay after motion detector changes from occupied to unoccupied
key: ['occupancy_timeout'],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value);
value *= 1;
await entity.write('msOccupancySensing', { pirOToUDelay: value }, utils.getOptions(meta.mapped, entity));
return { state: { occupancy_timeout: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('msOccupancySensing', ['pirOToUDelay']);
},
},
level_config: {
key: ['level_config'],
convertSet: async (entity, key, value, meta) => {
const state = {};
// parse payload to grab the keys
if (typeof value === 'string') {
try {
value = JSON.parse(value);
}
catch (e) {
throw new Error('Payload is not valid JSON');
}
}
utils.assertObject(value, key);
// onOffTransitionTime - range 0x0000 to 0xffff - optional
if (value.hasOwnProperty('on_off_transition_time')) {
let onOffTransitionTimeValue = Number(value.on_off_transition_time);
if (onOffTransitionTimeValue > 65535)
onOffTransitionTimeValue = 65535;
if (onOffTransitionTimeValue < 0)
onOffTransitionTimeValue = 0;
await entity.write('genLevelCtrl', { onOffTransitionTime: onOffTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
Object.assign(state, { on_off_transition_time: onOffTransitionTimeValue });
}
// onTransitionTime - range 0x0000 to 0xffff - optional
// 0xffff = use onOffTransitionTime
if (value.hasOwnProperty('on_transition_time')) {
let onTransitionTimeValue = value.on_transition_time;
if (typeof onTransitionTimeValue === 'string' && onTransitionTimeValue.toLowerCase() == 'disabled') {
onTransitionTimeValue = 65535;
}
else {
onTransitionTimeValue = Number(onTransitionTimeValue);
}
if (onTransitionTimeValue > 65535)
onTransitionTimeValue = 65534;
if (onTransitionTimeValue < 0)
onTransitionTimeValue = 0;
await entity.write('genLevelCtrl', { onTransitionTime: onTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
// reverse translate number -> preset
if (onTransitionTimeValue == 65535) {
onTransitionTimeValue = 'disabled';
}
Object.assign(state, { on_transition_time: onTransitionTimeValue });
}
// offTransitionTime - range 0x0000 to 0xffff - optional
// 0xffff = use onOffTransitionTime
if (value.hasOwnProperty('off_transition_time')) {
let offTransitionTimeValue = value.off_transition_time;
if (typeof offTransitionTimeValue === 'string' && offTransitionTimeValue.toLowerCase() == 'disabled') {
offTransitionTimeValue = 65535;
}
else {
offTransitionTimeValue = Number(offTransitionTimeValue);
}
if (offTransitionTimeValue > 65535)
offTransitionTimeValue = 65534;
if (offTransitionTimeValue < 0)
offTransitionTimeValue = 0;
await entity.write('genLevelCtrl', { offTransitionTime: offTransitionTimeValue }, utils.getOptions(meta.mapped, entity));
// reverse translate number -> preset
if (offTransitionTimeValue == 65535) {
offTransitionTimeValue = 'disabled';
}
Object.assign(state, { off_transition_time: offTransitionTimeValue });
}
// startUpCurrentLevel - range 0x00 to 0xff - optional
// 0x00 = return to minimum supported level
// 0xff = return to previous previous
if (value.hasOwnProperty('current_level_startup')) {
let startUpCurrentLevelValue = value.current_level_startup;
if (typeof startUpCurrentLevelValue === 'string' && startUpCurrentLevelValue.toLowerCase() == 'previous') {
startUpCurrentLevelValue = 255;
}
else if (typeof startUpCurrentLevelValue === 'string' && startUpCurrentLevelValue.toLowerCase() == 'minimum') {
startUpCurrentLevelValue = 0;
}
else {
startUpCurrentLevelValue = Number(startUpCurrentLevelValue);
}
if (startUpCurrentLevelValue > 255)
startUpCurrentLevelValue = 254;
if (startUpCurrentLevelValue < 0)
startUpCurrentLevelValue = 1;
await entity.write('genLevelCtrl', { startUpCurrentLevel: startUpCurrentLevelValue }, utils.getOptions(meta.mapped, entity));
// reverse translate number -> preset
if (startUpCurrentLevelValue == 255) {
startUpCurrentLevelValue = 'previous';
}
if (startUpCurrentLevelValue == 0) {
startUpCurrentLevelValue = 'minimum';
}
Object.assign(state, { current_level_startup: startUpCurrentLevelValue });
}
// onLevel - range 0x00 to 0xff - optional
// Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous".
if (value.hasOwnProperty('on_level')) {
let onLevel = value.on_level;
if (typeof onLevel === 'string' && onLevel.toLowerCase() == 'previous') {
onLevel = 255;
}
else {
onLevel = Number(onLevel);
}
if (onLevel > 255)
onLevel = 254;
if (onLevel < 1)
onLevel = 1;
await entity.write('genLevelCtrl', { onLevel }, utils.getOptions(meta.mapped, entity));
Object.assign(state, { on_level: onLevel == 255 ? 'previous' : onLevel });
}
// 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 (value.hasOwnProperty('execute_if_off')) {
const executeIfOffValue = !!value.execute_if_off;
await entity.write('genLevelCtrl', { options: executeIfOffValue ? 1 : 0 }, utils.getOptions(meta.mapped, entity));
Object.assign(state, { execute_if_off: executeIfOffValue });
}
if (Object.keys(state).length > 0) {
return { state: { level_config: state } };
}
},
convertGet: async (entity, key, meta) => {
for (const attribute of [
'onOffTransitionTime', 'onTransitionTime', 'offTransitionTime', 'startUpCurrentLevel',
'onLevel', 'options',
]) {
try {
await entity.read('genLevelCtrl', [attribute]);
}
catch (ex) {
// continue regardless of error, all these are optional in ZCL
}
}
},
},
ballast_config: {
key: ['ballast_config',
'ballast_minimum_level',
'ballast_maximum_level',
'ballast_power_on_level'],
// zcl attribute names are camel case, but we want to use snake case in the outside communication
convertSet: async (entity, key, value, meta) => {
if (key === 'ballast_config') {
value = utils.toCamelCase(value);
for (const [attrName, attrValue] of Object.entries(value)) {
const attributes = { [attrName]: attrValue };
await entity.write('lightingBallastCfg', attributes);
}
}
if (key === 'ballast_minimum_level') {
await entity.write('lightingBallastCfg', { 'minLevel': value });
}
if (key === 'ballast_maximum_level') {
await entity.write('lightingBallastCfg', { 'maxLevel': value });
}
if (key === 'ballast_power_on_level') {
await entity.write('lightingBallastCfg', { 'powerOnLevel': value });
}
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
let result = {};
for (const attrName of [
'ballast_status',
'min_level',
'max_level',
'power_on_level',
'power_on_fade_time',
'intrinsic_ballast_factor',
'ballast_factor_adjustment',
'lamp_quantity',
'lamp_type',
'lamp_manufacturer',
'lamp_rated_hours',
'lamp_burn_hours',
'lamp_alarm_mode',
'lamp_burn_hours_trip_point',
]) {
try {
// @ts-expect-error
result = { ...result, ...(await entity.read('lightingBallastCfg', [utils.toCamelCase(attrName)])) };
}
catch (ex) {
// continue regardless of error
}
}
if (key === 'ballast_config') {
logger_1.logger.debug(`ballast_config attribute results received: ${JSON.stringify(utils.toSnakeCase(result))}`, NS);
}
},
},
light_brightness_step: {
key: ['brightness_step', 'brightness_step_onoff'],
options: [exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
const onOff = key.endsWith('_onoff');
const command = onOff ? 'stepWithOnOff' : 'step';
value = Number(value);
utils.assertNumber(value, key);
const mode = value > 0 ? 0 : 1;
const transition = utils.getTransition(entity, key, meta).time;
const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition };
await entity.command('genLevelCtrl', command, payload, utils.getOptions(meta.mapped, entity));
if (meta.state.hasOwnProperty('brightness')) {
utils.assertNumber(meta.state.brightness);
let brightness = onOff || meta.state.state === 'ON' ? meta.state.brightness + value : meta.state.brightness;
if (value === 0) {
const entityToRead = utils.getEntityOrFirstGroupMember(entity);
if (entityToRead) {
brightness = (await entityToRead.read('genLevelCtrl', ['currentLevel'])).currentLevel;
}
}
brightness = Math.min(254, brightness);
brightness = Math.max(onOff || meta.state.state === 'OFF' ? 0 : 1, brightness);
if (utils.getMetaValue(entity, meta.mapped, 'turnsOffAtBrightness1', 'allEqual', false)) {
if (onOff && value < 0 && brightness === 1) {
brightness = 0;
}
else if (onOff && value > 0 && meta.state.brightness === 0) {
brightness++;
}
}
return { state: { brightness, state: brightness === 0 ? 'OFF' : 'ON' } };
}
},
},
light_brightness_move: {
key: ['brightness_move', 'brightness_move_onoff'],
convertSet: async (entity, key, value, meta) => {
if (value === 'stop' || value === 0) {
await entity.command('genLevelCtrl', 'stop', {}, utils.getOptions(meta.mapped, entity));
// As we cannot determine the new brightness state, we read it from the device
await utils.sleep(500);
const target = utils.getEntityOrFirstGroupMember(entity);
const onOff = (await target.read('genOnOff', ['onOff'])).onOff;
const brightness = (await target.read('genLevelCtrl', ['currentLevel'])).currentLevel;
return { state: { brightness, state: onOff === 1 ? 'ON' : 'OFF' } };
}
else {
value = Number(value);
utils.assertNumber(value, key);
const payload = { movemode: value > 0 ? 0 : 1, rate: Math.abs(value) };
const command = key.endsWith('onoff') ? 'moveWithOnOff' : 'move';
await entity.command('genLevelCtrl', command, payload, utils.getOptions(meta.mapped, entity));
}
},
},
light_colortemp_step: {
key: ['color_temp_step'],
options: [exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
value = Number(value);
utils.assertNumber(value, key);
const mode = value > 0 ? 1 : 3;
const transition = utils.getTransition(entity, key, meta).time;
const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition, minimum: 0, maximum: 600 };
await entity.command('lightingColorCtrl', 'stepColorTemp', payload, utils.getOptions(meta.mapped, entity));
// We cannot determine the color temperature from the current state so we read it, because
// - We don't know the max/min values
// - Color mode could have been switched (x/y or hue/saturation)
const entityToRead = utils.getEntityOrFirstGroupMember(entity);
if (entityToRead) {
await utils.sleep(100 + (transition * 100));
await entityToRead.read('lightingColorCtrl', ['colorTemperature']);
}
},
},
light_colortemp_move: {
key: ['colortemp_move', 'color_temp_move'],
convertSet: async (entity, key, value, meta) => {
if (key === 'color_temp_move' && (value === 'stop' || utils.isNumber(value))) {
value = value === 'stop' ? value : Number(value);
const payload = { minimum: 0, maximum: 600 };
if (value === 'stop' || value === 0) {
payload.rate = 1;
payload.movemode = 0;
}
else {
utils.assertNumber(value, key);
payload.rate = Math.abs(value);
payload.movemode = value > 0 ? 1 : 3;
}
await entity.command('lightingColorCtrl', 'moveColorTemp', payload, utils.getOptions(meta.mapped, entity));
// We cannot determine the color temperaturefrom the current state so we read it, because
// - Color mode could have been switched (x/y or colortemp)
if (value === 'stop' || value === 0) {
const entityToRead = utils.getEntityOrFirstGroupMember(entity);
if (entityToRead) {
await utils.sleep(100);
await entityToRead.read('lightingColorCtrl', ['colorTemperature', 'colorMode']);
}
}
}
else {
// Deprecated
const payload = { minimum: 153, maximum: 370, rate: 55 };
const stop = (val) => ['stop', 'release', '0'].some((el) => val.includes(el));
const up = (val) => ['1', 'up'].some((el) => val.includes(el));
const arr = [value.toString()];
const moverate = meta.message.hasOwnProperty('rate') ? Number(meta.message.rate) : 55;
payload.rate = moverate;
if (arr.filter(stop).length) {
payload.movemode = 0;
}
else {
payload.movemode = arr.filter(up).length ? 1 : 3;
}
await entity.command('lightingColorCtrl', 'moveColorTemp', payload, utils.getOptions(meta.mapped, entity));
}
},
},
light_color_and_colortemp_via_color: {
key: ['color', 'color_temp', 'color_temp_percent'],
options: [exposes.options.color_sync(), exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
if (key == 'color') {
return await converters1.light_color.convertSet(entity, key, value, meta);
}
else if (key == 'color_temp' || key == 'color_temp_percent') {
utils.assertNumber(value);
const xy = libColor.ColorXY.fromMireds(value);
const payload = {
transtime: utils.getTransition(entity, key, meta).time,
colorx: utils.mapNumberRange(xy.x, 0, 1, 0, 65535),
colory: utils.mapNumberRange(xy.y, 0, 1, 0, 65535),
};
await entity.command('lightingColorCtrl', 'moveToColor', payload, utils.getOptions(meta.mapped, entity));
return {
state: libColor.syncColorState({ 'color_mode': constants.colorModeLookup[2], 'color_temp': value }, meta.state, entity, meta.options), readAfterWriteTime: payload.transtime * 100,
};
}
},
convertGet: async (entity, key, meta) => {
await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta));
},
},
light_hue_saturation_step: {
key: ['hue_step', 'saturation_step'],
options: [exposes.options.transition()],
convertSet: async (entity, key, value, meta) => {
value = Number(value);
utils.assertNumber(value, key);
const command = key === 'hue_step' ? 'stepHue' : 'stepSaturation';
const attribute = key === 'hue_step' ? 'currentHue' : 'currentSaturation';
const mode = value > 0 ? 1 : 3;
const transition = utils.getTransition(entity, key, meta).time;
const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition };
await entity.command('lightingColorCtrl', command, payload, utils.getOptions(meta.mapp