'use strict';
const globalStore = require('../lib/store');
const utils = require('../lib/utils');
const herdsman = require('zigbee-herdsman');
const legacy = require('../lib/legacy');
const light = require('../lib/light');
const constants = require('../lib/constants');
const libColor = require('../lib/color');
const exposes = require('../lib/exposes');
const manufacturerOptions = {
    sunricher: { manufacturerCode: herdsman.Zcl.ManufacturerCode.SHENZHEN_SUNRICH },
    xiaomi: { manufacturerCode: herdsman.Zcl.ManufacturerCode.LUMI_UNITED_TECH, disableDefaultResponse: true },
    osram: { manufacturerCode: herdsman.Zcl.ManufacturerCode.OSRAM },
    eurotronic: { manufacturerCode: herdsman.Zcl.ManufacturerCode.JENNIC },
    danfoss: { manufacturerCode: herdsman.Zcl.ManufacturerCode.DANFOSS },
    hue: { manufacturerCode: herdsman.Zcl.ManufacturerCode.PHILIPS },
    ikea: { manufacturerCode: herdsman.Zcl.ManufacturerCode.IKEA_OF_SWEDEN },
    sinope: { manufacturerCode: herdsman.Zcl.ManufacturerCode.SINOPE_TECH },
    tint: { manufacturerCode: herdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT },
    legrand: { manufacturerCode: herdsman.Zcl.ManufacturerCode.VANTAGE, disableDefaultResponse: true },
    viessmann: { manufacturerCode: herdsman.Zcl.ManufacturerCode.VIESSMAN_ELEKTRO },
};
const converters = {
    // #region Generic converters
    read: {
        key: ['read'],
        convertSet: async (entity, key, value, meta) => {
            const result = await entity.read(value.cluster, value.attributes, (value.hasOwnProperty('options') ? value.options : {}));
            meta.logger.info(`Read result of '${value.cluster}': ${JSON.stringify(result)}`);
            if (value.hasOwnProperty('state_property')) {
                return { state: { [value.state_property]: result } };
            }
        },
    },
    write: {
        key: ['write'],
        convertSet: async (entity, key, value, meta) => {
            const options = utils.getOptions(meta.mapped, entity);
            if (value.hasOwnProperty('options')) {
                Object.assign(options, value.options);
            }
            await entity.write(value.cluster, value.payload, options);
            meta.logger.info(`Wrote '${JSON.stringify(value.payload)}' to '${value.cluster}'`);
        },
    },
    command: {
        key: ['command'],
        convertSet: async (entity, key, value, meta) => {
            const options = utils.getOptions(meta.mapped, entity);
            await entity.command(value.cluster, value.command, (value.hasOwnProperty('payload') ? value.payload : {}), options);
            meta.logger.info(`Invoked '${value.cluster}.${value.command}' with payload '${JSON.stringify(value.payload)}'`);
        },
    },
    factory_reset: {
        key: ['reset'],
        convertSet: async (entity, key, value, meta) => {
            await entity.command('genBasic', 'resetFactDefault', {}, utils.getOptions(meta.mapped, entity));
        },
    },
    identify: {
        key: ['identify'],
        convertSet: async (entity, key, value, meta) => {
            await entity.command('genIdentify', 'identify', { identifytime: value }, utils.getOptions(meta.mapped, entity));
        },
    },
    arm_mode: {
        key: ['arm_mode'],
        convertSet: async (entity, key, value, meta) => {
            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) => {
            value = value.toLowerCase();
            const lookup = { 'off': 0, 'on': 1, 'toggle': 2, 'previous': 255 };
            utils.validateValue(value, Object.keys(lookup));
            await entity.write('genOnOff', { startUpOnOff: lookup[value] }, 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) => {
            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) => {
            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.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) => {
            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');
            if (!utils.isInRange(0, meta.mapped.meta.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 };
                utils.validateValue(userType, Object.keys(typeLookup));
                const payload = {
                    'userid': user,
                    'userstatus': userEnabled ? 1 : 3,
                    'usertype': typeLookup[userType],
                    'pincodevalue': pinCode.toString(),
                };
                await entity.command('closuresDoorLock', 'setPinCode', payload, utils.getOptions(meta.mapped, entity));
            }
        },
        convertGet: async (entity, key, meta) => {
            const user = meta && meta.message && meta.message.pin_code ? meta.message.pin_code.user : undefined;
            if (user === undefined) {
                const max = meta.mapped.meta.pinCodeCount;
                // Get all
                const options = utils.getOptions(meta);
                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');
                }
                if (!utils.isInRange(0, meta.mapped.meta.pinCodeCount - 1, user)) {
                    throw new Error('userId must be in range for device');
                }
                await entity.command('closuresDoorLock', 'getPinCode', { userid: user }, utils.getOptions(meta));
            }
        },
    },
    lock_userstatus: {
        key: ['user_status'],
        convertSet: async (entity, key, value, meta) => {
            const user = value.user;
            if (isNaN(user)) {
                throw new Error('user must be numbers');
            }
            if (!utils.isInRange(0, meta.mapped.meta.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) => {
            const user = meta && meta.message && meta.message.user_status ? meta.message.user_status.user : undefined;
            if (user === undefined) {
                const max = meta.mapped.meta.pinCodeCount;
                // Get all
                const options = utils.getOptions(meta);
                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, meta.mapped.meta.pinCodeCount - 1, user)) {
                    throw new Error('userId must be in range for device');
                }
                await entity.command('closuresDoorLock', 'getUserStatus', { userid: user }, utils.getOptions(meta));
            }
        },
    },
    on_off: {
        key: ['state', 'on_time', 'off_wait_time'],
        convertSet: async (entity, key, value, meta) => {
            const state = meta.message.hasOwnProperty('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']);
        },
    },
    cover_via_brightness: {
        key: ['position', 'state'],
        options: [exposes.options.invert_cover()],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value !== 'number') {
                value = value.toLowerCase();
                if (value === 'stop') {
                    await entity.command('genLevelCtrl', 'stop', {}, utils.getOptions(meta.mapped, entity));
                    return;
                }
                const lookup = { 'open': 100, 'close': 0 };
                utils.validateValue(value, Object.keys(lookup));
                value = lookup[value];
            }
            const invert = utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
                !meta.options.invert_cover : meta.options.invert_cover;
            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 = {
                mode: value.mode || 'emergency',
                level: value.level || 'medium',
                strobe: value.hasOwnProperty('strobe') ? value.strobe : true,
                duration: value.hasOwnProperty('duration') ? value.duration : 10,
                strobeDutyCycle: value.hasOwnProperty('strobe_duty_cycle') ? value.strobe_duty_cycle * 10 : 0,
                strobeLevel: value.hasOwnProperty('strobe_level') ? strobeLevel[value.strobe_level] : 1,
            };
            let info;
            // https://github.com/Koenkk/zigbee2mqtt/issues/8310 some devices require the info to be reversed.
            if (['SIRZB-110', 'SRAC-23B-ZBSR', 'AV2010/29A', 'AV2010/24A'].includes(meta.mapped.model)) {
                info = (mode[values.mode]) + ((values.strobe ? 1 : 0) << 4) + (level[values.level] << 6);
            }
            else {
                info = (mode[values.mode] << 4) + ((values.strobe ? 1 : 0) << 2) + (level[values.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, introduced change in fw 4.0.5, tested backward with 4.0.4
            if (['SMSZB-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) => {
            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 = (state[values.state]) + ((values.strobe ? 1 : 0) << 4) + (level[values.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' };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await entity.command('closuresWindowCovering', lookup[value], {}, utils.getOptions(meta.mapped, entity));
        },
    },
    cover_position_tilt: {
        key: ['position', 'tilt'],
        options: [exposes.options.invert_cover()],
        convertSet: async (entity, key, value, meta) => {
            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) => {
            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');
                }
            }
            // 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 = {};
                    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 });
            }
            converters.ballast_config.convertGet(entity, key, meta);
        },
        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 {
                    result = { ...result, ...(await entity.read('lightingBallastCfg', [utils.toCamelCase(attrName)])) };
                }
                catch (ex) {
                    // continue regardless of error
                }
            }
            if (key === 'ballast_config') {
                meta.logger.warn(`ballast_config attribute results received: ${JSON.stringify(utils.toSnakeCase(result))}`);
            }
        },
    },
    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);
            if (isNaN(value)) {
                throw new Error(`${key} value of message: '${JSON.stringify(meta.message)}' invalid`);
            }
            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')) {
                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);
                if (isNaN(value)) {
                    throw new Error(`${key} value of message: '${JSON.stringify(meta.message)}' invalid`);
                }
                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);
            if (isNaN(value)) {
                throw new Error(`${key} value of message: '${JSON.stringify(meta.message)}' invalid`);
            }
            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 valus
            // - Color mode could have been swithed (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' || !isNaN(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 {
                    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 swithed (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') ? parseInt(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') {
                const result = await converters.light_color.convertSet(entity, key, value, meta);
                return result;
            }
            else if (key == 'color_temp' || key == 'color_temp_percent') {
                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, meta.logger), 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);
            if (isNaN(value)) {
                throw new Error(`${key} value of message: '${JSON.stringify(meta.message)}' invalid`);
            }
            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.mapped, entity));
            // We cannot determine the hue/saturation from the current state so we read it, because
            // - Color mode could have been swithed (x/y or colortemp)
            const entityToRead = utils.getEntityOrFirstGroupMember(entity);
            if (entityToRead) {
                await utils.sleep(100 + (transition * 100));
                await entityToRead.read('lightingColorCtrl', [attribute, 'colorMode']);
            }
        },
    },
    light_hue_saturation_move: {
        key: ['hue_move', 'saturation_move'],
        convertSet: async (entity, key, value, meta) => {
            value = value === 'stop' ? value : Number(value);
            if (isNaN(value) && value !== 'stop') {
                throw new Error(`${key} value of message: '${JSON.stringify(meta.message)}' invalid`);
            }
            const command = key === 'hue_move' ? 'moveHue' : 'moveSaturation';
            const attribute = key === 'hue_move' ? 'currentHue' : 'currentSaturation';
            const payload = {};
            if (value === 'stop' || value === 0) {
                payload.rate = 1;
                payload.movemode = 0;
            }
            else {
                payload.rate = Math.abs(value);
                payload.movemode = value > 0 ? 1 : 3;
            }
            await entity.command('lightingColorCtrl', command, payload, utils.getOptions(meta.mapped, entity));
            // We cannot determine the hue/saturation from the current state so we read it, because
            // - Color mode could have been swithed (x/y or colortemp)
            if (value === 'stop' || value === 0) {
                const entityToRead = utils.getEntityOrFirstGroupMember(entity);
                if (entityToRead) {
                    await utils.sleep(100);
                    await entityToRead.read('lightingColorCtrl', [attribute, 'colorMode']);
                }
            }
        },
    },
    light_onoff_brightness: {
        key: ['state', 'brightness', 'brightness_percent', 'on_time'],
        options: [exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            const { message } = meta;
            const transition = utils.getTransition(entity, 'brightness', meta);
            const turnsOffAtBrightness1 = utils.getMetaValue(entity, meta.mapped, 'turnsOffAtBrightness1', 'allEqual', false);
            let state = message.hasOwnProperty('state') ? (message.state === null ? null : message.state.toLowerCase()) : undefined;
            let brightness = undefined;
            if (message.hasOwnProperty('brightness')) {
                brightness = Number(message.brightness);
            }
            else if (message.hasOwnProperty('brightness_percent')) {
                brightness = utils.mapNumberRange(Number(message.brightness_percent), 0, 100, 0, 255);
            }
            if (brightness === 255) {
                // Allow 255 for backwards compatibility.
                brightness = 254;
            }
            if (brightness !== undefined && (isNaN(brightness) || brightness < 0 || brightness > 254)) {
                throw new Error(`Brightness value of message: '${JSON.stringify(message)}' invalid, must be a number >= 0 and =< 254`);
            }
            if (state !== undefined && state !== null && ['on', 'off', 'toggle'].includes(state) === false) {
                throw new Error(`State value of message: '${JSON.stringify(message)}' invalid, must be 'ON', 'OFF' or 'TOGGLE'`);
            }
            if ((state === undefined || state === null) && brightness === undefined) {
                throw new Error(`At least one of "brightness" or "state" must have a value: '${JSON.stringify(message)}'`);
            }
            // Infer state from desired brightness if unset. Ideally we'd want to keep it as it is, but this code has always
            // used 'MoveToLevelWithOnOff' so that'd break backwards compatibility. To keep the state, the user
            // has to explicitly set it to null.
            if (state === undefined) {
                // Also write to `meta.message.state` in case we delegate to the `on_off` converter.
                state = meta.message.state = brightness === 0 ? 'off' : 'on';
            }
            let publishBrightness = brightness !== undefined;
            const targetState = state === 'toggle' ? (meta.state.state === 'ON' ? 'off' : 'on') : state;
            if (targetState === 'off') {
                // Simulate 'Off' with transition via 'MoveToLevelWithOnOff', otherwise just use 'Off'.
                // TODO: if this is a group where some members don't support Level Control, turning them off
                //  with transition may have no effect. (Some devices, such as Envilar ZG302-BOX-RELAY, handle
                //  'MoveToLevelWithOnOff' despite not supporting the cluster; others, like the LEDVANCE SMART+
                //  plug, do not.)
                brightness = transition.specified || brightness === 0 ? 0 : undefined;
                if (meta.state.hasOwnProperty('brightness') && meta.state.state === 'ON') {
                    // The light's current level gets clobbered in two cases:
                    //   1. when 'Off' has a transition, in which case it is really 'MoveToLevelWithOnOff'
                    //      https://github.com/Koenkk/zigbee-herdsman-converters/issues/1073
                    //   2. when 'OnLevel' is set: "If OnLevel is not defined, set the CurrentLevel to the stored level."
                    //      https://github.com/Koenkk/zigbee2mqtt/issues/2850#issuecomment-580365633
                    // We need to remember current brightness in case the next 'On' does not provide it. `meta` is not reliable
                    // here, as it will get clobbered too if reporting is configured.
                    globalStore.putValue(entity, 'brightness', meta.state.brightness);
                    globalStore.putValue(entity, 'turnedOffWithTransition', brightness !== undefined);
                }
            }
            else if (targetState === 'on' && brightness === undefined) {
                // Simulate 'On' with transition via 'MoveToLevelWithOnOff', or restore the level from before
                // it was clobbered by a previous transition to off; otherwise just use 'On'.
                // TODO: same problem as above.
                // TODO: if transition is not specified, should use device default (OnTransitionTime), not 0.
                if (transition.specified || globalStore.getValue(entity, 'turnedOffWithTransition') === true) {
                    const levelConfig = utils.getObjectProperty(meta.state, 'level_config', {});
                    let onLevel = utils.getObjectProperty(levelConfig, 'on_level', 0);
                    if (onLevel === 0 && entity.meta.onLevelSupported !== false) {
                        try {
                            const attributeRead = await entity.read('genLevelCtrl', ['onLevel']);
                            if (attributeRead !== undefined) {
                                onLevel = attributeRead['onLevel'];
                            }
                        }
                        catch (e) {
                            // OnLevel not supported
                        }
                    }
                    if (onLevel === 0) {
                        onLevel = 'previous';
                        entity.meta.onLevelSupported = false;
                        entity.save();
                    }
                    if (onLevel === 255 || onLevel === 'previous') {
                        const current = utils.getObjectProperty(meta.state, 'brightness', 254);
                        brightness = globalStore.getValue(entity, 'brightness', current);
                    }
                    else {
                        brightness = onLevel;
                    }
                    // Published state might have gotten clobbered by reporting.
                    publishBrightness = true;
                }
            }
            if (brightness === undefined) {
                const result = await converters.on_off.convertSet(entity, 'state', state, meta);
                result.readAfterWriteTime = 0;
                if (result.state && result.state.state === 'ON' && meta.state.brightness === 0) {
                    result.state.brightness = 1;
                }
                return result;
            }
            if (brightness === 0 && (targetState === 'on' || state === null)) {
                brightness = 1;
            }
            if (brightness === 1 && turnsOffAtBrightness1) {
                brightness = 2;
            }
            if (targetState !== 'off') {
                globalStore.putValue(entity, 'brightness', brightness);
                globalStore.clearValue(entity, 'turnedOffWithTransition');
            }
            await entity.command('genLevelCtrl', state === null ? 'moveToLevel' : 'moveToLevelWithOnOff', { level: Number(brightness), transtime: transition.time }, utils.getOptions(meta.mapped, entity));
            const result = { state: {}, readAfterWriteTime: transition.time * 100 };
            if (publishBrightness) {
                result.state.brightness = Number(brightness);
            }
            if (state !== null) {
                result.state.state = brightness === 0 ? 'OFF' : 'ON';
            }
            return result;
        },
        convertGet: async (entity, key, meta) => {
            if (key === 'brightness') {
                await entity.read('genLevelCtrl', ['currentLevel']);
            }
            else if (key === 'state') {
                await converters.on_off.convertGet(entity, key, 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, meta.logger);
            const preset = { 'warmest': colorTempMax, 'warm': 454, 'neutral': 370, 'cool': 250, 'coolest': colorTempMin };
            if (key === 'color_temp_percent') {
                value = utils.mapNumberRange(value, 0, 100, ((colorTempMin != null) ? colorTempMin : 154), ((colorTempMax != null) ? colorTempMax : 500)).toString();
            }
            if (typeof value === 'string' && isNaN(value)) {
                if (value.toLowerCase() in preset) {
                    value = preset[value.toLowerCase()];
                }
                else {
                    throw new Error(`Unknown preset '${value}'`);
                }
            }
            value = Number(value);
            // ensure value within range
            value = light.clampColorTemp(value, colorTempMin, colorTempMax, meta.logger);
            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, meta.logger), readAfterWriteTime: payload.transtime * 100,
            };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingColorCtrl', ['colorMode', 'colorTemperature']);
        },
    },
    light_colortemp_startup: {
        key: ['color_temp_startup'],
        convertSet: async (entity, key, value, meta) => {
            const [colorTempMin, colorTempMax] = light.findColorTempRange(entity, meta.logger);
            const preset = { 'warmest': colorTempMax, 'warm': 454, 'neutral': 370, 'cool': 250, 'coolest': colorTempMin, 'previous': 65535 };
            if (typeof value === 'string' && isNaN(value)) {
                if (value.toLowerCase() in preset) {
                    value = preset[value.toLowerCase()];
                }
                else {
                    throw new Error(`Unknown preset '${value}'`);
                }
            }
            value = Number(value);
            // ensure value within range
            // we do allow one exception for 0xffff, which is to restore the previous value
            if (value != 65535) {
                value = light.clampColorTemp(value, colorTempMin, colorTempMax, meta.logger);
            }
            await entity.write('lightingColorCtrl', { startUpColorTemperature: value }, utils.getOptions(meta.mapped, entity));
            return { state: { color_temp_startup: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingColorCtrl', ['startUpColorTemperature']);
        },
    },
    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 slighlty 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);
                    }
                    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
                    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', { 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, meta.logger),
                readAfterWriteTime: zclData.transtime * 100 };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta));
        },
    },
    light_color_colortemp: {
        /**
         * This converter is a combination of light_color and light_colortemp and
         * can be used instead of the two individual converters. When used to set,
         * it actually calls out to light_color or light_colortemp to get the
         * return value. When used to get, it gets both color and colorTemp in
         * one call.
         * The reason for the existence of this somewhat peculiar converter is
         * that some lights don't report their state when changed. To fix this,
         * we query the state after we set it. We want to query color and colorTemp
         * both when setting either, because both change when setting one. This
         * converter is used to do just that.
         */
        key: ['color', 'color_temp', 'color_temp_percent'],
        options: [exposes.options.color_sync(), exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (key == 'color') {
                const result = await converters.light_color.convertSet(entity, key, value, meta);
                return result;
            }
            else if (key == 'color_temp' || key == 'color_temp_percent') {
                const result = await converters.light_colortemp.convertSet(entity, key, value, meta);
                return result;
            }
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta, ['colorTemperature']));
        },
    },
    effect: {
        key: ['effect', 'alert', 'flash'],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'effect') {
                const lookup = { blink: 0, breathe: 1, okay: 2, channel_change: 11, finish_effect: 254, stop_effect: 255 };
                value = value.toLowerCase();
                utils.validateValue(value, Object.keys(lookup));
                const payload = { effectid: lookup[value], effectvariant: 0 };
                await entity.command('genIdentify', 'triggerEffect', payload, utils.getOptions(meta.mapped, entity));
            }
            else if (key === 'alert' || key === 'flash') { // Deprecated
                let effectid = 0;
                const lookup = { 'select': 0x00, 'lselect': 0x01, 'none': 0xFF };
                if (key === 'flash') {
                    if (value === 2) {
                        value = 'select';
                    }
                    else if (value === 10) {
                        value = 'lselect';
                    }
                }
                effectid = lookup[value];
                const payload = { effectid, effectvariant: 0 };
                await entity.command('genIdentify', 'triggerEffect', payload, utils.getOptions(meta.mapped, entity));
            }
        },
    },
    thermostat_remote_sensing: {
        key: ['remote_sensing'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { remoteSensing: value });
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['remoteSensing']);
        },
    },
    thermostat_weekly_schedule: {
        key: ['weekly_schedule'],
        convertSet: async (entity, key, value, meta) => {
            /*
             * We want to support a simple human creatable format to send a schedule:
                 {"weekly_schedule": {
                   "dayofweek": ["monday", "tuesday"],
                   "transitions": [
                     {"heatSetpoint": 16, "transitionTime": "0:00"},
                     {"heatSetpoint": 20, "transitionTime": "18:00"},
                     {"heatSetpoint": 16, "transitionTime": "19:30"}
                   ]}}

             * However exposes is not flexible enough to describe something like this. There is a
             *  much more verbose format we also support so that exposes work.
                 {"weekly_schedule": {
                   "dayofweek": [
                     {"day": "monday"},
                     {"day": "tuesday"}
                   ],
                   "transitions": [
                     {"heatSetpoint": 16, "transitionTime": {"hour": 0,  "minute": 0}},
                     {"heatSetpoint": 20, "transitionTime": {"hour": 18, "minute": 0}},
                     {"heatSetpoint": 16, "transitionTime": {"hour": 19, "minute": 30}}
                   ]}}
             */
            const payload = {
                dayofweek: value.dayofweek,
                transitions: value.transitions,
            };
            if (Array.isArray(payload.transitions)) {
                // calculate numoftrans
                if (typeof value.numoftrans !== 'undefined') {
                    meta.logger.warn(`weekly_schedule: ignoring provided numoftrans value (${JSON.stringify(value.numoftrans)}), ` +
                        'this is now calculated automatically');
                }
                payload.numoftrans = payload.transitions.length;
                // mode is calculated below
                if (typeof value.mode !== 'undefined') {
                    meta.logger.warn(`weekly_schedule: ignoring provided mode value (${JSON.stringify(value.mode)}), ` +
                        'this is now calculated automatically');
                }
                payload.mode = [];
                // transform transition payload values if needed
                for (const elem of payload.transitions) {
                    // update payload.mode if needed
                    if (elem.hasOwnProperty('heatSetpoint') && !payload.mode.includes('heat')) {
                        payload.mode.push('heat');
                    }
                    if (elem.hasOwnProperty('coolSetpoint') && !payload.mode.includes('cool')) {
                        payload.mode.push('cool');
                    }
                    // transform setpoint values if numeric
                    if (typeof elem['heatSetpoint'] === 'number') {
                        elem['heatSetpoint'] = Math.round(elem['heatSetpoint'] * 100);
                    }
                    if (typeof elem['coolSetpoint'] === 'number') {
                        elem['coolSetpoint'] = Math.round(elem['coolSetpoint'] * 100);
                    }
                    // accept 24h time notation (e.g. 19:30)
                    if (typeof elem['transitionTime'] === 'string') {
                        const time = elem['transitionTime'].split(':');
                        if ((time.length != 2) || isNaN(time[0]) || isNaN(time[1])) {
                            meta.logger.warn(`weekly_schedule: expected 24h time notation (e.g. 19:30) but got '${elem['transitionTime']}'!`);
                        }
                        else {
                            elem['transitionTime'] = ((parseInt(time[0]) * 60) + parseInt(time[1]));
                        }
                    }
                    else if (typeof elem['transitionTime'] === 'object') {
                        if (!elem['transitionTime'].hasOwnProperty('hour') || !elem['transitionTime'].hasOwnProperty('minute')) {
                            throw new Error('weekly_schedule: expected 24h time object (e.g. {"hour": 19, "minute": 30}), ' +
                                `but got '${JSON.stringify(elem['transitionTime'])}'!`);
                        }
                        else if (isNaN(elem['transitionTime']['hour'])) {
                            throw new Error('weekly_schedule: expected time.hour to be a number, ' +
                                `but got '${elem['transitionTime']['hour']}'!`);
                        }
                        else if (isNaN(elem['transitionTime']['minute'])) {
                            throw new Error('weekly_schedule: expected time.minute to be a number, ' +
                                `but got '${elem['transitionTime']['minute']}'!`);
                        }
                        else {
                            elem['transitionTime'] = ((parseInt(elem['transitionTime']['hour']) * 60) + parseInt(elem['transitionTime']['minute']));
                        }
                    }
                }
            }
            else {
                meta.logger.error('weekly_schedule: transitions is not an array!');
                return;
            }
            // map array of desired modes to bitmask
            let mode = 0;
            for (let m of payload.mode) {
                // lookup mode bit
                m = utils.getKey(constants.thermostatScheduleMode, m.toLowerCase(), m, Number);
                mode |= (1 << m);
            }
            payload.mode = mode;
            // map array of days to desired dayofweek bitmask
            if (typeof payload.dayofweek === 'string')
                payload.dayofweek = [payload.dayofweek];
            if (Array.isArray(payload.dayofweek)) {
                let dayofweek = 0;
                for (let d of payload.dayofweek) {
                    if (typeof d === 'object') {
                        if (!d.hasOwnProperty('day')) {
                            throw new Error('weekly_schedule: expected dayofweek to be string or {"day": "str"}, ' +
                                `but got '${JSON.strinify(d)}'!`);
                        }
                        d = d.day;
                    }
                    // lookup dayofweek bit
                    d = utils.getKey(constants.thermostatDayOfWeek, d.toLowerCase(), d, Number);
                    dayofweek |= (1 << d);
                }
                payload.dayofweek = dayofweek;
            }
            await entity.command('hvacThermostat', 'setWeeklySchedule', payload, utils.getOptions(meta.mapped, entity));
        },
        convertGet: async (entity, key, meta) => {
            const payload = {
                daystoreturn: 0xff,
                modetoreturn: 3, // heat + cool
            };
            await entity.command('hvacThermostat', 'getWeeklySchedule', payload, utils.getOptions(meta.mapped, entity));
        },
    },
    thermostat_system_mode: {
        key: ['system_mode'],
        convertSet: async (entity, key, value, meta) => {
            let systemMode = utils.getKey(constants.thermostatSystemModes, value, undefined, Number);
            if (systemMode === undefined) {
                systemMode = utils.getKey(legacy.thermostatSystemModes, value, value, Number);
            }
            await entity.write('hvacThermostat', { systemMode });
            return { readAfterWriteTime: 250, state: { system_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['systemMode']);
        },
    },
    thermostat_control_sequence_of_operation: {
        key: ['control_sequence_of_operation'],
        convertSet: async (entity, key, value, meta) => {
            let val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, undefined, Number);
            if (val === undefined) {
                val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, value, Number);
            }
            const attributes = { ctrlSeqeOfOper: val };
            await entity.write('hvacThermostat', attributes);
            // NOTE: update the cluster attribute we store as this is used by
            //       SMaBiT AV2010/32's dynamic expose function.
            entity.saveClusterAttributeKeyValue('hvacThermostat', attributes);
            return { readAfterWriteTime: 250, state: { control_sequence_of_operation: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['ctrlSeqeOfOper']);
        },
    },
    thermostat_programming_operation_mode: {
        key: ['programming_operation_mode'],
        convertSet: async (entity, key, value, meta) => {
            const val = utils.getKey(constants.thermostatProgrammingOperationModes, value, undefined, Number);
            if (val === undefined) {
                throw new Error('Programming operation mode invalid, must be one of: ' +
                    Object.values(constants.thermostatProgrammingOperationModes).join(', '));
            }
            await entity.write('hvacThermostat', { programingOperMode: val });
            return { state: { programming_operation_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['programingOperMode']);
        },
    },
    thermostat_temperature_display_mode: {
        key: ['temperature_display_mode'],
        convertSet: async (entity, key, value, meta) => {
            const tempDisplayMode = utils.getKey(constants.temperatureDisplayMode, value, value, Number);
            await entity.write('hvacUserInterfaceCfg', { tempDisplayMode });
            return { readAfterWriteTime: 250, state: { temperature_display_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacUserInterfaceCfg', ['tempDisplayMode']);
        },
    },
    thermostat_keypad_lockout: {
        key: ['keypad_lockout'],
        convertSet: async (entity, key, value, meta) => {
            const keypadLockout = utils.getKey(constants.keypadLockoutMode, value, value, Number);
            await entity.write('hvacUserInterfaceCfg', { keypadLockout });
            return { readAfterWriteTime: 250, state: { keypad_lockout: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacUserInterfaceCfg', ['keypadLockout']);
        },
    },
    thermostat_temperature_setpoint_hold: {
        key: ['temperature_setpoint_hold'],
        convertSet: async (entity, key, value, meta) => {
            const tempSetpointHold = value;
            await entity.write('hvacThermostat', { tempSetpointHold });
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['tempSetpointHold']);
        },
    },
    thermostat_temperature_setpoint_hold_duration: {
        key: ['temperature_setpoint_hold_duration'],
        convertSet: async (entity, key, value, meta) => {
            const tempSetpointHoldDuration = value;
            await entity.write('hvacThermostat', { tempSetpointHoldDuration });
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['tempSetpointHoldDuration']);
        },
    },
    fan_mode: {
        key: ['fan_mode', 'fan_state'],
        convertSet: async (entity, key, value, meta) => {
            if (key == 'fan_state' && value.toLowerCase() == 'on') {
                value = utils.getMetaValue(entity, meta.mapped, 'fanStateOn', 'allEqual', 'on');
            }
            const fanMode = constants.fanMode[value.toLowerCase()];
            await entity.write('hvacFanCtrl', { fanMode });
            return { state: { fan_mode: value.toLowerCase(), fan_state: value.toLowerCase() === 'off' ? 'OFF' : 'ON' } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacFanCtrl', ['fanMode']);
        },
    },
    thermostat_local_temperature: {
        key: ['local_temperature'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['localTemp']);
        },
    },
    thermostat_outdoor_temperature: {
        key: ['outdoor_temperature'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['outdoorTemp']);
        },
    },
    thermostat_local_temperature_calibration: {
        key: ['local_temperature_calibration'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { localTemperatureCalibration: Math.round(value * 10) });
            return { state: { local_temperature_calibration: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['localTemperatureCalibration']);
        },
    },
    thermostat_occupancy: {
        key: ['occupancy'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['occupancy']);
        },
    },
    thermostat_clear_weekly_schedule: {
        key: ['clear_weekly_schedule'],
        convertSet: async (entity, key, value, meta) => {
            await entity.command('hvacThermostat', 'clearWeeklySchedule', {}, utils.getOptions(meta.mapped, entity));
        },
    },
    thermostat_pi_heating_demand: {
        key: ['pi_heating_demand'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['pIHeatingDemand']);
        },
    },
    thermostat_running_state: {
        key: ['running_state'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['runningState']);
        },
    },
    thermostat_occupied_heating_setpoint: {
        key: ['occupied_heating_setpoint'],
        options: [exposes.options.thermostat_unit()],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const occupiedHeatingSetpoint = result;
            await entity.write('hvacThermostat', { occupiedHeatingSetpoint });
            return { state: { occupied_heating_setpoint: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['occupiedHeatingSetpoint']);
        },
    },
    thermostat_unoccupied_heating_setpoint: {
        key: ['unoccupied_heating_setpoint'],
        options: [exposes.options.thermostat_unit()],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const unoccupiedHeatingSetpoint = result;
            await entity.write('hvacThermostat', { unoccupiedHeatingSetpoint });
            return { state: { unoccupied_heating_setpoint: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['unoccupiedHeatingSetpoint']);
        },
    },
    thermostat_occupied_cooling_setpoint: {
        key: ['occupied_cooling_setpoint'],
        options: [exposes.options.thermostat_unit()],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const occupiedCoolingSetpoint = result;
            await entity.write('hvacThermostat', { occupiedCoolingSetpoint });
            return { state: { occupied_cooling_setpoint: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['occupiedCoolingSetpoint']);
        },
    },
    thermostat_unoccupied_cooling_setpoint: {
        key: ['unoccupied_cooling_setpoint'],
        options: [exposes.options.thermostat_unit()],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const unoccupiedCoolingSetpoint = result;
            await entity.write('hvacThermostat', { unoccupiedCoolingSetpoint });
            return { state: { unoccupied_cooling_setpoint: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['unoccupiedCoolingSetpoint']);
        },
    },
    thermostat_setpoint_raise_lower: {
        key: ['setpoint_raise_lower'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { mode: value.mode, amount: Math.round(value.amount) * 100 };
            await entity.command('hvacThermostat', 'setpointRaiseLower', payload, utils.getOptions(meta.mapped, entity));
        },
    },
    thermostat_relay_status_log: {
        key: ['relay_status_log'],
        convertGet: async (entity, key, meta) => {
            await entity.command('hvacThermostat', 'getRelayStatusLog', {}, utils.getOptions(meta.mapped, entity));
        },
    },
    thermostat_running_mode: {
        key: ['running_mode'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['runningMode']);
        },
    },
    thermostat_min_heat_setpoint_limit: {
        key: ['min_heat_setpoint_limit'],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const minHeatSetpointLimit = result;
            await entity.write('hvacThermostat', { minHeatSetpointLimit });
            return { state: { min_heat_setpoint_limit: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['minHeatSetpointLimit']);
        },
    },
    thermostat_max_heat_setpoint_limit: {
        key: ['max_heat_setpoint_limit'],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const maxHeatSetpointLimit = result;
            await entity.write('hvacThermostat', { maxHeatSetpointLimit });
            return { state: { max_heat_setpoint_limit: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['maxHeatSetpointLimit']);
        },
    },
    thermostat_min_cool_setpoint_limit: {
        key: ['min_cool_setpoint_limit'],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const minCoolSetpointLimit = result;
            await entity.write('hvacThermostat', { minCoolSetpointLimit });
            return { state: { min_cool_setpoint_limit: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['minCoolSetpointLimit']);
        },
    },
    thermostat_max_cool_setpoint_limit: {
        key: ['max_cool_setpoint_limit'],
        convertSet: async (entity, key, value, meta) => {
            let result;
            if (meta.options.thermostat_unit === 'fahrenheit') {
                result = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
            }
            else {
                result = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            }
            const maxCoolSetpointLimit = result;
            await entity.write('hvacThermostat', { maxCoolSetpointLimit });
            return { state: { max_cool_setpoint_limit: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['maxCoolSetpointLimit']);
        },
    },
    thermostat_ac_louver_position: {
        key: ['ac_louver_position'],
        convertSet: async (entity, key, value, meta) => {
            let acLouverPosition = utils.getKey(constants.thermostatAcLouverPositions, value, undefined, Number);
            if (acLouverPosition === undefined) {
                acLouverPosition = utils.getKey(constants.thermostatAcLouverPositions, value, value, Number);
            }
            await entity.write('hvacThermostat', { acLouverPosition });
            return { state: { ac_louver_position: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['acLouverPosition']);
        },
    },
    electrical_measurement_power: {
        key: ['power'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['activePower']);
        },
    },
    metering_power: {
        key: ['power'],
        convertGet: async (entity, key, meta) => {
            await utils.enforceEndpoint(entity, key, meta).read('seMetering', ['instantaneousDemand']);
        },
    },
    currentsummdelivered: {
        key: ['energy'],
        convertGet: async (entity, key, meta) => {
            await utils.enforceEndpoint(entity, key, meta).read('seMetering', ['currentSummDelivered']);
        },
    },
    frequency: {
        key: ['ac_frequency'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['acFrequency']);
        },
    },
    electrical_measurement_power_reactive: {
        key: ['power_reactive'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['reactivePower']);
        },
    },
    powerfactor: {
        key: ['power_factor'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['powerFactor']);
        },
    },
    acvoltage: {
        key: ['voltage'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['rmsVoltage']);
        },
    },
    accurrent: {
        key: ['current'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', ['rmsCurrent']);
        },
    },
    temperature: {
        key: ['temperature'],
        convertGet: async (entity, key, meta) => {
            await entity.read('msTemperatureMeasurement', ['measuredValue']);
        },
    },
    illuminance: {
        key: ['illuminance', 'illuminance_lux'],
        convertGet: async (entity, key, meta) => {
            await entity.read('msIlluminanceMeasurement', ['measuredValue']);
        },
    },
    // #endregion
    // #region Non-generic converters
    elko_load: {
        key: ['load'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoLoad': value });
            return { state: { load: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoLoad']);
        },
    },
    elko_display_text: {
        key: ['display_text'],
        convertSet: async (entity, key, value, meta) => {
            if (value.length <= 14) {
                await entity.write('hvacThermostat', { 'elkoDisplayText': value });
                return { state: { display_text: value } };
            }
            else {
                throw new Error('Length of text is greater than 14');
            }
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoDisplayText']);
        },
    },
    elko_power_status: {
        key: ['system_mode'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoPowerStatus': value === 'heat' });
            return { state: { system_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoPowerStatus']);
        },
    },
    elko_external_temp: {
        key: ['floor_temp'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoExternalTemp']);
        },
    },
    elko_mean_power: {
        key: ['mean_power'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoMeanPower']);
        },
    },
    elko_child_lock: {
        key: ['child_lock'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoChildLock': value === 'lock' });
            return { state: { child_lock: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoChildLock']);
        },
    },
    elko_frost_guard: {
        key: ['frost_guard'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoFrostGuard': value === 'on' });
            return { state: { frost_guard: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoFrostGuard']);
        },
    },
    elko_night_switching: {
        key: ['night_switching'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoNightSwitching': value === 'on' });
            return { state: { night_switching: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoNightSwitching']);
        },
    },
    elko_relay_state: {
        key: ['running_state'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoRelayState']);
        },
    },
    elko_sensor_mode: {
        key: ['sensor'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoSensor': { 'air': '0', 'floor': '1', 'supervisor_floor': '3' }[value] });
            return { state: { sensor: value } };
        },
    },
    elko_regulator_time: {
        key: ['regulator_time'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoRegulatorTime': value });
            return { state: { sensor: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoRegulatorTime']);
        },
    },
    elko_regulator_mode: {
        key: ['regulator_mode'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoRegulatorMode': value === 'regulator' });
            return { state: { regulator_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoRegulatorMode']);
        },
    },
    elko_local_temperature_calibration: {
        key: ['local_temperature_calibration'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'elkoCalibration': Math.round(value * 10) });
            return { state: { local_temperature_calibration: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoCalibration']);
        },
    },
    elko_max_floor_temp: {
        key: ['max_floor_temp'],
        convertSet: async (entity, key, value, meta) => {
            if (value.length <= 14) {
                await entity.write('hvacThermostat', { 'elkoMaxFloorTemp': value });
                return { state: { max_floor_temp: value } };
            }
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['elkoMaxFloorTemp']);
        },
    },
    livolo_socket_switch_on_off: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value !== 'string') {
                return;
            }
            const state = value.toLowerCase();
            let oldstate = 1;
            if (state === 'on') {
                oldstate = 108;
            }
            let channel = 1.0;
            const postfix = meta.endpoint_name || 'left';
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
            const payloadOn = { 0x0001: { value: Buffer.from([1, 0, 0, 0, 0, 0, 0, 0]), type: 1 } };
            const payloadOff = { 0x0001: { value: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), type: 1 } };
            const payloadOnRight = { 0x0001: { value: Buffer.from([2, 0, 0, 0, 0, 0, 0, 0]), type: 2 } };
            const payloadOffRight = { 0x0001: { value: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), type: 2 } };
            const payloadOnBottomLeft = { 0x0001: { value: Buffer.from([4, 0, 0, 0, 0, 0, 0, 0]), type: 4 } };
            const payloadOffBottomLeft = { 0x0001: { value: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), type: 4 } };
            const payloadOnBottomRight = { 0x0001: { value: Buffer.from([8, 0, 0, 0, 0, 0, 0, 0]), type: 136 } };
            const payloadOffBottomRight = { 0x0001: { value: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), type: 136 } };
            if (postfix === 'left') {
                await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', { level: oldstate, transtime: channel });
                await entity.write('genPowerCfg', (state === 'on') ? payloadOn : payloadOff, {
                    manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                    reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9,
                });
                return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
            }
            else if (postfix === 'right') {
                channel = 2.0;
                await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', { level: oldstate, transtime: channel });
                await entity.write('genPowerCfg', (state === 'on') ? payloadOnRight : payloadOffRight, {
                    manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                    reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9,
                });
                return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
            }
            else if (postfix === 'bottom_right') {
                await entity.write('genPowerCfg', (state === 'on') ? payloadOnBottomRight : payloadOffBottomRight, {
                    manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                    reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9,
                });
                return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
            }
            else if (postfix === 'bottom_left') {
                await entity.write('genPowerCfg', (state === 'on') ? payloadOnBottomLeft : payloadOffBottomLeft, {
                    manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                    reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9,
                });
                return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
            }
            return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
        },
        convertGet: async (entity, key, meta) => {
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
        },
    },
    livolo_switch_on_off: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value !== 'string') {
                return;
            }
            const postfix = meta.endpoint_name || 'left';
            let state = value.toLowerCase();
            let channel = 1;
            if (state === 'on') {
                state = 108;
            }
            else if (state === 'off') {
                state = 1;
            }
            else {
                return;
            }
            if (postfix === 'left') {
                channel = 1.0;
            }
            else if (postfix === 'right') {
                channel = 2.0;
            }
            else {
                return;
            }
            await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', { level: state, transtime: channel });
            return { state: { state: value.toUpperCase() }, readAfterWriteTime: 250 };
        },
        convertGet: async (entity, key, meta) => {
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
        },
    },
    livolo_dimmer_level: {
        key: ['brightness', 'brightness_percent', 'level'],
        convertSet: async (entity, key, value, meta) => {
            // upscale to 100
            value = Number(value);
            let newValue;
            if (key === 'level') {
                if (value >= 0 && value <= 1000) {
                    newValue = utils.mapNumberRange(value, 0, 1000, 0, 100);
                }
                else {
                    throw new Error('Dimmer level is out of range 0..1000');
                }
            }
            else if (key === 'brightness_percent') {
                if (value >= 0 && value <= 100) {
                    newValue = Math.round(value);
                }
                else {
                    throw new Error('Dimmer brightness_percent is out of range 0..100');
                }
            }
            else {
                if (value >= 0 && value <= 255) {
                    newValue = utils.mapNumberRange(value, 0, 255, 0, 100);
                }
                else {
                    throw new Error('Dimmer brightness is out of range 0..255');
                }
            }
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
            const payload = { 0x0301: { value: Buffer.from([newValue, 0, 0, 0, 0, 0, 0, 0]), type: 1 } };
            await entity.write('genPowerCfg', payload, {
                manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9, writeUndiv: true,
            });
            return {
                state: { brightness_percent: newValue, brightness: utils.mapNumberRange(newValue, 0, 100, 0, 255), level: (newValue * 10) },
                readAfterWriteTime: 250,
            };
        },
        convertGet: async (entity, key, meta) => {
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
        },
    },
    livolo_cover_state: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            let payload;
            const options = {
                frameType: 0, manufacturerCode: 0x1ad2, disableDefaultResponse: true,
                disableResponse: true, reservedBits: 3, direction: 1, writeUndiv: true,
                transactionSequenceNumber: 0xe9,
            };
            switch (value) {
                case 'OPEN':
                    payload =
                        { attrId: 0x0000, selector: null, elementData: [0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] };
                    break;
                case 'CLOSE':
                    payload =
                        { attrId: 0x0000, selector: null, elementData: [0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] };
                    break;
                case 'STOP':
                    payload =
                        { attrId: 0x0000, selector: null, elementData: [0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] };
                    break;
                default:
                    throw new Error(`Value '${value}' is not a valid cover position (must be one of 'OPEN' or 'CLOSE')`);
            }
            await entity.writeStructured('genPowerCfg', [payload], options);
            return {
                state: {
                    moving: true,
                },
                readAfterWriteTime: 250,
            };
        },
    },
    livolo_cover_position: {
        key: ['position'],
        convertSet: async (entity, key, value, meta) => {
            const position = 100 - value;
            await entity.command('genOnOff', 'toggle', {}, { transactionSequenceNumber: 0 });
            const payload = { 0x0401: { value: [position, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], type: 1 } };
            await entity.write('genPowerCfg', payload, {
                manufacturerCode: 0x1ad2, disableDefaultResponse: true, disableResponse: true,
                reservedBits: 3, direction: 1, transactionSequenceNumber: 0xe9, writeUndiv: true,
            });
            return {
                state: {
                    position: value,
                    moving: true,
                },
                readAfterWriteTime: 250,
            };
        },
    },
    livolo_cover_options: {
        key: ['options'],
        convertSet: async (entity, key, value, meta) => {
            const options = {
                frameType: 0, manufacturerCode: 0x1ad2, disableDefaultResponse: true,
                disableResponse: true, reservedBits: 3, direction: 1, writeUndiv: true,
                transactionSequenceNumber: 0xe9,
            };
            if (value.hasOwnProperty('motor_direction')) {
                let direction;
                switch (value.motor_direction) {
                    case 'FORWARD':
                        direction = 0x00;
                        break;
                    case 'REVERSE':
                        direction = 0x80;
                        break;
                    default:
                        throw new Error(`livolo_cover_options: ${value.motor_direction} is not a valid motor direction \
                     (must be one of 'FORWARD' or 'REVERSE')`);
                }
                const payload = { 0x1301: { value: [direction, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] } };
                await entity.write('genPowerCfg', payload, options);
            }
            if (value.hasOwnProperty('motor_speed')) {
                if (value.motor_speed < 20 || value.motor_speed > 40) {
                    throw new Error('livolo_cover_options: Motor speed is out of range (20-40)');
                }
                const payload = { 0x1201: { value: [value.motor_speed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] } };
                await entity.write('genPowerCfg', payload, options);
            }
        },
    },
    gledopto_light_onoff_brightness: {
        key: ['state', 'brightness', 'brightness_percent'],
        options: [exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (meta.message && meta.message.hasOwnProperty('transition')) {
                meta.message.transition = meta.message.transition * 3.3;
            }
            if (meta.mapped.model === 'GL-S-007ZS' || meta.mapped.model === 'GL-C-009') {
                // https://github.com/Koenkk/zigbee2mqtt/issues/2757
                // Device doesn't support ON with moveToLevelWithOnOff command
                if (meta.message.hasOwnProperty('state') && meta.message.state.toLowerCase() === 'on') {
                    await converters.on_off.convertSet(entity, key, 'ON', meta);
                    await utils.sleep(1000);
                }
            }
            return await converters.light_onoff_brightness.convertSet(entity, key, value, meta);
        },
        convertGet: async (entity, key, meta) => {
            return await converters.light_onoff_brightness.convertGet(entity, key, meta);
        },
    },
    gledopto_light_colortemp: {
        key: ['color_temp', 'color_temp_percent'],
        options: [exposes.options.color_sync(), exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (meta.message && meta.message.hasOwnProperty('transition')) {
                meta.message.transition = meta.message.transition * 3.3;
            }
            // Gledopto devices turn ON when they are OFF and color is set.
            // https://github.com/Koenkk/zigbee2mqtt/issues/3509
            const state = { state: 'ON' };
            const result = await converters.light_colortemp.convertSet(entity, key, value, meta);
            result.state = { ...result.state, ...state };
            return result;
        },
        convertGet: async (entity, key, meta) => {
            return await converters.light_colortemp.convertGet(entity, key, meta);
        },
    },
    gledopto_light_color: {
        key: ['color'],
        options: [exposes.options.color_sync(), exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (meta.message && meta.message.hasOwnProperty('transition')) {
                meta.message.transition = meta.message.transition * 3.3;
            }
            if (key === 'color' && !meta.message.transition) {
                // Always provide a transition when setting color, otherwise CCT to RGB
                // doesn't work properly (CCT leds stay on).
                meta.message.transition = 0.4;
            }
            // Gledopto devices turn ON when they are OFF and color is set.
            // https://github.com/Koenkk/zigbee2mqtt/issues/3509
            const state = { state: 'ON' };
            const result = await converters.light_color.convertSet(entity, key, value, meta);
            result.state = { ...result.state, ...state };
            return result;
        },
        convertGet: async (entity, key, meta) => {
            return await converters.light_color.convertGet(entity, key, meta);
        },
    },
    gledopto_light_color_colortemp: {
        key: ['color', 'color_temp', 'color_temp_percent'],
        options: [exposes.options.color_sync(), exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (key == 'color') {
                const result = await converters.gledopto_light_color.convertSet(entity, key, value, meta);
                if (result.state && result.state.color.hasOwnProperty('x') && result.state.color.hasOwnProperty('y')) {
                    result.state.color_temp = Math.round(libColor.ColorXY.fromObject(result.state.color).toMireds());
                }
                return result;
            }
            else if (key == 'color_temp' || key == 'color_temp_percent') {
                const result = await converters.gledopto_light_colortemp.convertSet(entity, key, value, meta);
                result.state.color = libColor.ColorXY.fromMireds(result.state.color_temp).rounded(4).toObject();
                return result;
            }
        },
        convertGet: async (entity, key, meta) => {
            return await converters.light_color_colortemp.convertGet(entity, key, meta);
        },
    },
    aqara_motion_sensitivity: {
        key: ['motion_sensitivity'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'low': 1, 'medium': 2, 'high': 3 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await entity.write('aqaraOpple', { 0x010c: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { motion_sensitivity: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x010c], manufacturerOptions.xiaomi);
        },
    },
    RTCZCGQ11LM_presence: {
        key: ['presence'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0142], manufacturerOptions.xiaomi);
        },
    },
    RTCZCGQ11LM_monitoring_mode: {
        key: ['monitoring_mode'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            const lookup = { 'undirected': 0, 'left_right': 1 };
            await entity.write('aqaraOpple', { 0x0144: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { monitoring_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0144], manufacturerOptions.xiaomi);
        },
    },
    RTCZCGQ11LM_approach_distance: {
        key: ['approach_distance'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            const lookup = { 'far': 0, 'medium': 1, 'near': 2 };
            await entity.write('aqaraOpple', { 0x0146: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { approach_distance: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0146], manufacturerOptions.xiaomi);
        },
    },
    RTCZCGQ11LM_reset_nopresence_status: {
        key: ['reset_nopresence_status'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('aqaraOpple', { 0x0157: { value: 1, type: 0x20 } }, manufacturerOptions.xiaomi);
        },
    },
    ZigUP_lock: {
        key: ['led'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'off': 'lockDoor', 'on': 'unlockDoor', 'toggle': 'toggleDoor' };
            await entity.command('closuresDoorLock', lookup[value], { 'pincodevalue': '' });
        },
    },
    LS21001_alert_behaviour: {
        key: ['alert_behaviour'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'siren_led': 3, 'siren': 2, 'led': 1, 'nothing': 0 };
            await entity.write('genBasic', { 0x400a: { value: lookup[value], type: 32 } }, { manufacturerCode: 0x1168, disableDefaultResponse: true, sendWhen: 'active' });
            return { state: { alert_behaviour: value } };
        },
    },
    xiaomi_switch_type: {
        key: ['switch_type'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'toggle': 1, 'momentary': 2 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            await entity.write('aqaraOpple', { 0x000A: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { switch_type: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x000A], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_switch_power_outage_memory: {
        key: ['power_outage_memory'],
        convertSet: async (entity, key, value, meta) => {
            if (['SP-EUC01', 'ZNCZ04LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM',
                'WS-EUK01', 'WS-EUK02', 'WS-EUK03', 'WS-EUK04', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM', 'QBKG28LM', 'QBKG29LM',
                'QBKG31LM', 'QBKG34LM', 'QBKG38LM', 'QBKG39LM', 'QBKG40LM', 'QBKG41LM', 'ZNDDMK11LM', 'ZNLDP13LM', 'ZNQBKG31LM',
                'WS-USC02', 'WS-USC03', 'WS-USC04', 'ZNQBKG24LM', 'ZNQBKG25LM',
            ].includes(meta.mapped.model)) {
                await entity.write('aqaraOpple', { 0x0201: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.xiaomi);
            }
            else if (['ZNCZ02LM', 'QBCZ11LM', 'LLKZMK11LM'].includes(meta.mapped.model)) {
                const payload = value ?
                    [[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x07, 0x01, 0x10, 0x01], [0xaa, 0x80, 0x03, 0xd3, 0x07, 0x08, 0x01]] :
                    [[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x09, 0x01, 0x10, 0x00], [0xaa, 0x80, 0x03, 0xd3, 0x07, 0x0a, 0x01]];
                await entity.write('genBasic', { 0xFFF0: { value: payload[0], type: 0x41 } }, manufacturerOptions.xiaomi);
                await entity.write('genBasic', { 0xFFF0: { value: payload[1], type: 0x41 } }, manufacturerOptions.xiaomi);
            }
            else if (['ZNCZ11LM', 'ZNCZ12LM'].includes(meta.mapped.model)) {
                const payload = value ?
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x01, 0x10, 0x01] :
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x01, 0x10, 0x00];
                await entity.write('genBasic', { 0xFFF0: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
            }
            else {
                throw new Error('Not supported');
            }
            return { state: { power_outage_memory: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (['SP-EUC01', 'ZNCZ04LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM',
                'WS-EUK01', 'WS-EUK02', 'WS-EUK03', 'WS-EUK04', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM', 'QBKG28LM', 'QBKG29LM',
                'QBKG31LM', 'QBKG34LM', 'QBKG38LM', 'QBKG39LM', 'QBKG40LM', 'QBKG41LM', 'ZNDDMK11LM', 'ZNLDP13LM', 'ZNQBKG31LM',
                'WS-USC02', 'WS-USC03', 'WS-USC04', 'ZNQBKG24LM', 'ZNQBKG25LM',
            ].includes(meta.mapped.model)) {
                await entity.read('aqaraOpple', [0x0201]);
            }
            else if (['ZNCZ02LM', 'QBCZ11LM', 'ZNCZ11LM', 'ZNCZ12LM'].includes(meta.mapped.model)) {
                await entity.read('aqaraOpple', [0xFFF0]);
            }
            else {
                throw new Error('Not supported');
            }
        },
    },
    xiaomi_light_power_outage_memory: {
        key: ['power_outage_memory'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('genBasic', { 0xFF19: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.xiaomi);
            return { state: { power_outage_memory: value } };
        },
    },
    xiaomi_power: {
        key: ['power'],
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.endpoints.find((e) => e.supportsInputCluster('genAnalogInput'));
            await endpoint.read('genAnalogInput', ['presentValue']);
        },
    },
    xiaomi_auto_off: {
        key: ['auto_off'],
        convertSet: async (entity, key, value, meta) => {
            if (['ZNCZ04LM', 'ZNCZ12LM', 'SP-EUC01'].includes(meta.mapped.model)) {
                await entity.write('aqaraOpple', { 0x0202: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.xiaomi);
            }
            else if (['ZNCZ11LM'].includes(meta.mapped.model)) {
                const payload = value ?
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x02, 0x10, 0x01] :
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x02, 0x10, 0x00];
                await entity.write('genBasic', { 0xFFF0: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
            }
            else {
                throw new Error('Not supported');
            }
            return { state: { auto_off: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (['ZNCZ04LM', 'ZNCZ12LM', 'SP-EUC01'].includes(meta.mapped.model)) {
                await entity.read('aqaraOpple', [0x0202], manufacturerOptions.xiaomi);
            }
            else {
                throw new Error('Not supported');
            }
        },
    },
    GZCGQ11LM_detection_period: {
        key: ['detection_period'],
        convertSet: async (entity, key, value, meta) => {
            value *= 1;
            await entity.write('aqaraOpple', { 0x0000: { value: [value], type: 0x21 } }, manufacturerOptions.xiaomi);
            return { state: { detection_period: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0000], manufacturerOptions.xiaomi);
        },
    },
    aqara_detection_interval: {
        key: ['detection_interval'],
        convertSet: async (entity, key, value, meta) => {
            value *= 1;
            await entity.write('aqaraOpple', { 0x0102: { value: [value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { detection_interval: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0102], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_overload_protection: {
        key: ['overload_protection'],
        convertSet: async (entity, key, value, meta) => {
            value *= 1;
            await entity.write('aqaraOpple', { 0x020b: { value: [value], type: 0x39 } }, manufacturerOptions.xiaomi);
            return { state: { overload_protection: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x020b], manufacturerOptions.xiaomi);
        },
    },
    aqara_switch_mode_switch: {
        key: ['mode_switch'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'anti_flicker_mode': 4, 'quick_mode': 1 };
            await entity.write('aqaraOpple', { 0x0004: { value: lookup[value], type: 0x21 } }, manufacturerOptions.xiaomi);
            return { state: { mode_switch: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0004], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_button_switch_mode: {
        key: ['button_switch_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'relay': 0, 'relay_and_usb': 1 };
            await entity.write('aqaraOpple', { 0x0226: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { button_switch_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0226], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_socket_button_lock: {
        key: ['button_lock'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'ON': 0, 'OFF': 1 };
            await entity.write('aqaraOpple', { 0x0200: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { button_lock: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0200], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_led_disabled_night: {
        key: ['led_disabled_night'],
        convertSet: async (entity, key, value, meta) => {
            if (['ZNCZ04LM', 'ZNCZ12LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM',
                'QBKG28LM', 'QBKG29LM', 'QBKG31LM', 'QBKG34LM', 'DLKZMK11LM', 'SSM-U01', 'WS-EUK01', 'WS-EUK02',
                'WS-EUK03', 'WS-EUK04', 'SP-EUC01', 'ZNQBKG24LM', 'ZNQBKG25LM'].includes(meta.mapped.model)) {
                await entity.write('aqaraOpple', { 0x0203: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.xiaomi);
            }
            else if (['ZNCZ11LM'].includes(meta.mapped.model)) {
                const payload = value ?
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x03, 0x10, 0x00] :
                    [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x03, 0x10, 0x01];
                await entity.write('genBasic', { 0xFFF0: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
            }
            else {
                throw new Error('Not supported');
            }
            return { state: { led_disabled_night: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (['ZNCZ04LM', 'ZNCZ12LM', 'ZNCZ15LM', 'QBCZ15LM', 'QBCZ14LM', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM',
                'QBKG28LM', 'QBKG29LM', 'QBKG31LM', 'QBKG34LM', 'DLKZMK11LM', 'SSM-U01', 'WS-EUK01', 'WS-EUK02',
                'WS-EUK03', 'WS-EUK04', 'SP-EUC01', 'ZNQBKG24LM', 'ZNQBKG25LM'].includes(meta.mapped.model)) {
                await entity.read('aqaraOpple', [0x0203], manufacturerOptions.xiaomi);
            }
            else {
                throw new Error('Not supported');
            }
        },
    },
    xiaomi_flip_indicator_light: {
        key: ['flip_indicator_light'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'OFF': 0, 'ON': 1 };
            await entity.write('aqaraOpple', { 0x00F0: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { flip_indicator_light: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x00F0], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_dimmer_mode: {
        key: ['dimmer_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'rgbw': 3, 'dual_ct': 1 };
            value = value.toLowerCase();
            if (['rgbw'].includes(value)) {
                await entity.write('aqaraOpple', { 0x0509: { value: lookup[value], type: 0x23 } }, manufacturerOptions.xiaomi);
                await entity.write('aqaraOpple', { 0x050F: { value: 1, type: 0x23 } }, manufacturerOptions.xiaomi);
            }
            else {
                await entity.write('aqaraOpple', { 0x0509: { value: lookup[value], type: 0x23 } }, manufacturerOptions.xiaomi);
                // Turn on dimming channel 1 and channel 2
                await entity.write('aqaraOpple', { 0x050F: { value: 3, type: 0x23 } }, manufacturerOptions.xiaomi);
            }
            return { state: { dimmer_mode: value } };
        },
        convertGet: async (entity, key, value, meta) => {
            await entity.read('aqaraOpple', [0x0509], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_switch_operation_mode_basic: {
        key: ['operation_mode'],
        convertSet: async (entity, key, value, meta) => {
            let targetValue = value.hasOwnProperty('state') ? value.state : value;
            // 1/2 gang switches using genBasic on endpoint 1.
            let attrId;
            let attrValue;
            if (meta.mapped.meta && meta.mapped.meta.multiEndpoint) {
                attrId = { left: 0xFF22, right: 0xFF23 }[meta.endpoint_name];
                // Allow usage of control_relay for 2 gang switches by mapping it to the default side.
                if (targetValue === 'control_relay') {
                    targetValue = `control_${meta.endpoint_name}_relay`;
                }
                attrValue = { control_left_relay: 0x12, control_right_relay: 0x22, decoupled: 0xFE }[targetValue];
                if (attrId == null) {
                    throw new Error(`Unsupported endpoint ${meta.endpoint_name} for changing operation_mode.`);
                }
            }
            else {
                attrId = 0xFF22;
                attrValue = { control_relay: 0x12, decoupled: 0xFE }[targetValue];
            }
            if (attrValue == null) {
                throw new Error('Invalid operation_mode value');
            }
            const endpoint = entity.getDevice().getEndpoint(1);
            const payload = {};
            payload[attrId] = { value: attrValue, type: 0x20 };
            await endpoint.write('genBasic', payload, manufacturerOptions.xiaomi);
            return { state: { operation_mode: targetValue } };
        },
        convertGet: async (entity, key, meta) => {
            let attrId;
            if (meta.mapped.meta && meta.mapped.meta.multiEndpoint) {
                attrId = { left: 0xFF22, right: 0xFF23 }[meta.endpoint_name];
                if (attrId == null) {
                    throw new Error(`Unsupported endpoint ${meta.endpoint_name} for getting operation_mode.`);
                }
            }
            else {
                attrId = 0xFF22;
            }
            await entity.read('genBasic', [attrId], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_switch_operation_mode_opple: {
        key: ['operation_mode'],
        convertSet: async (entity, key, value, meta) => {
            // Support existing syntax of a nested object just for the state field. Though it's quite silly IMO.
            const targetValue = value.hasOwnProperty('state') ? value.state : value;
            // Switches using aqaraOpple 0x0200 on the same endpoints as the onOff clusters.
            const lookupState = { control_relay: 0x01, decoupled: 0x00 };
            await entity.write('aqaraOpple', { 0x0200: { value: lookupState[targetValue], type: 0x20 } }, manufacturerOptions.xiaomi);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0200], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_switch_do_not_disturb: {
        key: ['do_not_disturb'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('aqaraOpple', { 0x0203: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.xiaomi);
            return { state: { do_not_disturb: value } };
        },
    },
    STS_PRS_251_beep: {
        key: ['beep'],
        convertSet: async (entity, key, value, meta) => {
            await entity.command('genIdentify', 'identify', { identifytime: value }, utils.getOptions(meta.mapped, entity));
        },
    },
    xiaomi_curtain_options: {
        key: ['options'],
        convertSet: async (entity, key, value, meta) => {
            const opts = {
                reverse_direction: false,
                hand_open: true,
                reset_limits: false,
                ...value,
            };
            // Legacy names
            if (value.hasOwnProperty('auto_close'))
                opts.hand_open = value.auto_close;
            if (value.hasOwnProperty('reset_move'))
                opts.reset_limits = value.reset_move;
            if (meta.mapped.model === 'ZNCLDJ12LM' || meta.mapped.model === 'ZNCLDJ14LM') {
                await entity.write('genBasic', { 0xff28: { value: opts.reverse_direction, type: 0x10 } }, manufacturerOptions.xiaomi);
                await entity.write('genBasic', { 0xff29: { value: !opts.hand_open, type: 0x10 } }, manufacturerOptions.xiaomi);
                if (opts.reset_limits) {
                    await entity.write('genBasic', { 0xff27: { value: 0x00, type: 0x10 } }, manufacturerOptions.xiaomi);
                }
            }
            else if (meta.mapped.model === 'ZNCLDJ11LM') {
                const payload = [
                    0x07, 0x00, opts.reset_limits ? 0x01 : 0x02, 0x00, opts.reverse_direction ? 0x01 : 0x00, 0x04,
                    !opts.hand_open ? 0x01 : 0x00, 0x12,
                ];
                await entity.write('genBasic', { 0x0401: { value: payload, type: 0x42 } }, manufacturerOptions.xiaomi);
                // hand_open requires a separate request with slightly different payload
                payload[2] = 0x08;
                await entity.write('genBasic', { 0x0401: { value: payload, type: 0x42 } }, manufacturerOptions.xiaomi);
            }
            else {
                throw new Error(`xiaomi_curtain_options set called for not supported model: ${meta.mapped.model}`);
            }
            // Reset limits is an action, not a state.
            delete opts.reset_limits;
            return { state: { options: opts } };
        },
        convertGet: async (entity, key, meta) => {
            if (meta.mapped.model === 'ZNCLDJ11LM') {
                await entity.read('genBasic', [0x0401], manufacturerOptions.xiaomi);
            }
            else {
                throw new Error(`xiaomi_curtain_options get called for not supported model: ${meta.mapped.model}`);
            }
        },
    },
    xiaomi_curtain_position_state: {
        key: ['state', 'position'],
        options: [exposes.options.invert_cover()],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'state' && typeof value === 'string' && value.toLowerCase() === 'stop') {
                if (meta.mapped.model == 'ZNJLBL01LM') {
                    const payload = { 'presentValue': 2 };
                    await entity.write('genMultistateOutput', payload);
                }
                else {
                    await entity.command('closuresWindowCovering', 'stop', {}, utils.getOptions(meta.mapped, entity));
                }
                if (!['ZNCLDJ11LM', 'ZNJLBL01LM', 'ZNCLBL01LM'].includes(meta.mapped.model)) {
                    // The code below is originally added for ZNCLDJ11LM (Koenkk/zigbee2mqtt#4585).
                    // However, in Koenkk/zigbee-herdsman-converters#4039 it was replaced by reading
                    // directly from currentPositionLiftPercentage, so that device is excluded.
                    // For ZNJLBL01LM, in Koenkk/zigbee-herdsman-converters#4163 the position is read
                    // through onEvent each time the motor stops, so it becomes redundant, and the
                    // device is excluded.
                    // The code is left here to avoid breaking compatibility, ideally all devices using
                    // this converter should be tested so the code can be adjusted/deleted.
                    // Xiaomi curtain does not send position update on stop, request this.
                    await entity.read('genAnalogOutput', [0x0055]);
                }
                return { state: { state: 'STOP' } };
            }
            else {
                const lookup = { 'open': 100, 'close': 0, 'on': 100, 'off': 0 };
                value = typeof value === 'string' ? value.toLowerCase() : value;
                value = lookup.hasOwnProperty(value) ? lookup[value] : value;
                value = meta.options.invert_cover ? 100 - value : value;
                if (['ZNCLBL01LM'].includes(meta.mapped.model)) {
                    await entity.command('closuresWindowCovering', 'goToLiftPercentage', { percentageliftvalue: value }, utils.getOptions(meta.mapped, entity));
                }
                else {
                    const payload = { 0x0055: { value, type: 0x39 } };
                    await entity.write('genAnalogOutput', payload);
                }
            }
        },
        convertGet: async (entity, key, meta) => {
            if (['ZNCLBL01LM'].includes(meta.mapped.model)) {
                await entity.read('closuresWindowCovering', ['currentPositionLiftPercentage']);
            }
            else {
                await entity.read('genAnalogOutput', [0x0055]);
            }
        },
    },
    xiaomi_curtain_battery_voltage: {
        key: ['voltage'],
        convertGet: async (entity, key, meta) => {
            switch (meta.mapped.model) {
                case 'ZNCLBL01LM':
                    await entity.read('aqaraOpple', [0x040B], manufacturerOptions.xiaomi);
                    break;
                default:
                    throw new Error(`xiaomi_curtain_battery_voltage - unsupported model: ${meta.mapped.model}`);
            }
        },
    },
    xiaomi_curtain_acn002_charging_status: {
        key: ['charging_status'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0409], manufacturerOptions.xiaomi);
        },
    },
    xiaomi_curtain_acn002_battery: {
        key: ['battery'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x040a], manufacturerOptions.xiaomi);
        },
    },
    ledvance_commands: {
        /* deprectated osram_*/
        key: ['set_transition', 'remember_state', 'osram_set_transition', 'osram_remember_state'],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'osram_set_transition' || key === 'set_transition') {
                if (value) {
                    const transition = (value > 1) ? (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 10 : 1;
                    const payload = { 0x0012: { value: transition, type: 0x21 }, 0x0013: { value: transition, type: 0x21 } };
                    await entity.write('genLevelCtrl', payload);
                }
            }
            else if (key == 'osram_remember_state' || key == 'remember_state') {
                if (value === true) {
                    await entity.command('manuSpecificOsram', 'saveStartupParams', {}, manufacturerOptions.osram);
                }
                else if (value === false) {
                    await entity.command('manuSpecificOsram', 'resetStartupParams', {}, manufacturerOptions.osram);
                }
            }
        },
    },
    SPZ01_power_outage_memory: {
        key: ['power_outage_memory'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('genOnOff', { 0x2000: { value: value ? 0x01 : 0x00, type: 0x20 } });
            return { state: { power_outage_memory: value } };
        },
    },
    tuya_relay_din_led_indicator: {
        key: ['indicator_mode'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            const lookup = { 'off': 0x00, 'on_off': 0x01, 'off_on': 0x02 };
            utils.validateValue(value, Object.keys(lookup));
            const payload = lookup[value];
            await entity.write('genOnOff', { 0x8001: { value: payload, type: 0x30 } });
            return { state: { indicator_mode: value } };
        },
    },
    kmpcil_res005_on_off: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            const options = { disableDefaultResponse: true };
            value = value.toLowerCase();
            utils.validateValue(value, ['toggle', 'off', 'on']);
            if (value === 'toggle') {
                if (!meta.state.hasOwnProperty('state')) {
                    throw new Error('Cannot toggle, state not known yet');
                }
                else {
                    const payload = { 0x0055: { value: (meta.state.state === 'OFF') ? 0x01 : 0x00, type: 0x10 } };
                    await entity.write('genBinaryOutput', payload, options);
                    return { state: { state: meta.state.state === 'OFF' ? 'ON' : 'OFF' } };
                }
            }
            else {
                const payload = { 0x0055: { value: (value.toUpperCase() === 'OFF') ? 0x00 : 0x01, type: 0x10 } };
                await entity.write('genBinaryOutput', payload, options);
                return { state: { state: value.toUpperCase() } };
            }
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genBinaryOutput', ['presentValue']);
        },
    },
    light_onoff_restorable_brightness: {
        /**
         * Some devices reset brightness to 100% when turned on, even if previous brightness was different
         * This uses the stored state of the device to restore to the previous brightness level when turning on
         */
        key: ['state', 'brightness', 'brightness_percent'],
        options: [exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            const deviceState = meta.state || {};
            const message = meta.message;
            const state = message.hasOwnProperty('state') ? message.state.toLowerCase() : null;
            const hasBrightness = message.hasOwnProperty('brightness') || message.hasOwnProperty('brightness_percent');
            // Add brightness if command is 'on' and we can restore previous value
            if (state === 'on' && !hasBrightness && deviceState.brightness > 0) {
                message.brightness = deviceState.brightness;
            }
            return await converters.light_onoff_brightness.convertSet(entity, key, value, meta);
        },
        convertGet: async (entity, key, meta) => {
            return await converters.light_onoff_brightness.convertGet(entity, key, meta);
        },
    },
    JTQJBF01LMBW_JTYJGD01LMBW_sensitivity: {
        key: ['sensitivity'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            const lookup = { 'low': 0x04010000, 'medium': 0x04020000, 'high': 0x04030000 };
            utils.validateValue(value, Object.keys(lookup));
            // Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
            const options = { ...manufacturerOptions.xiaomi, timeout: 35000 };
            await entity.write('ssIasZone', { 0xFFF1: { value: lookup[value], type: 0x23 } }, options);
            return { state: { sensitivity: value } };
        },
    },
    JTQJBF01LMBW_JTYJGD01LMBW_selfest: {
        key: ['selftest'],
        convertSet: async (entity, key, value, meta) => {
            // Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
            const options = { ...manufacturerOptions.xiaomi, timeout: 35000 };
            await entity.write('ssIasZone', { 0xFFF1: { value: 0x03010000, type: 0x23 } }, options);
        },
    },
    aqara_alarm: {
        key: ['gas', 'smoke'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x013a], manufacturerOptions.xiaomi);
        },
    },
    aqara_density: {
        key: ['gas_density', 'smoke_density', 'smoke_density_dbm'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x013b], manufacturerOptions.xiaomi);
        },
    },
    JTBZ01AQA_gas_sensitivity: {
        key: ['gas_sensitivity'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toUpperCase();
            const lookup = { '15%LEL': 1, '10%LEL': 2 };
            await entity.write('aqaraOpple', { 0x010c: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { gas_sensitivity: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x010c], manufacturerOptions.xiaomi);
        },
    },
    aqara_selftest: {
        key: ['selftest'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('aqaraOpple', { 0x0127: { value: true, type: 0x10 } }, manufacturerOptions.xiaomi);
        },
    },
    aqara_buzzer: {
        key: ['buzzer'],
        convertSet: async (entity, key, value, meta) => {
            const attribute = ['JY-GZ-01AQ'].includes(meta.mapped.model) ? 0x013e : 0x013f;
            value = (value.toLowerCase() === 'alarm') ? 15361 : 15360;
            await entity.write('aqaraOpple', { [`${attribute}`]: { value: [`${value}`], type: 0x23 } }, manufacturerOptions.xiaomi);
            value = (value === 15361) ? 0 : 1;
            await entity.write('aqaraOpple', { 0x0126: { value: [`${value}`], type: 0x20 } }, manufacturerOptions.xiaomi);
        },
    },
    aqara_buzzer_manual: {
        key: ['buzzer_manual_alarm', 'buzzer_manual_mute'],
        convertGet: async (entity, key, meta) => {
            if (key === 'buzzer_manual_mute') {
                await entity.read('aqaraOpple', [0x0126], manufacturerOptions.xiaomi);
            }
            else if (key === 'buzzer_manual_alarm') {
                await entity.read('aqaraOpple', [0x013d], manufacturerOptions.xiaomi);
            }
        },
    },
    JYGZ01AQ_heartbeat_indicator: {
        key: ['heartbeat_indicator'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { true: 1, false: 0 };
            await entity.write('aqaraOpple', { 0x013c: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { heartbeat_indicator: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x013c], manufacturerOptions.xiaomi);
        },
    },
    aqara_linkage_alarm: {
        key: ['linkage_alarm'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { true: 1, false: 0 };
            await entity.write('aqaraOpple', { 0x014b: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { linkage_alarm: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x014b], manufacturerOptions.xiaomi);
        },
    },
    JTBZ01AQA_state: {
        key: ['state'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0139], manufacturerOptions.xiaomi);
        },
    },
    aqara_power_outage_count: {
        key: ['power_outage_count'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0002], manufacturerOptions.xiaomi);
        },
    },
    RTCGQ14LM_trigger_indicator: {
        key: ['trigger_indicator'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { true: 1, false: 0 };
            await entity.write('aqaraOpple', { 0x0152: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { trigger_indicator: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0152], manufacturerOptions.xiaomi);
        },
    },
    LLKZMK11LM_interlock: {
        key: ['interlock'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('genBinaryOutput', { 0xff06: { value: value ? 0x01 : 0x00, type: 0x10 } }, manufacturerOptions.xiaomi);
            return { state: { interlock: value } };
        },
    },
    DJT11LM_vibration_sensitivity: {
        key: ['sensitivity'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            const lookup = { 'low': 0x15, 'medium': 0x0B, 'high': 0x01 };
            utils.validateValue(value, Object.keys(lookup));
            const options = { ...manufacturerOptions.xiaomi, timeout: 35000 };
            await entity.write('genBasic', { 0xFF0D: { value: lookup[value], type: 0x20 } }, options);
            return { state: { sensitivity: value } };
        },
    },
    hue_wall_switch_device_mode: {
        key: ['device_mode'],
        convertSet: async (entity, key, value, meta) => {
            const values = ['single_rocker', 'single_push_button', 'dual_rocker', 'dual_push_button'];
            utils.validateValue(value, values);
            await entity.write('genBasic', { 0x0034: { value: values.indexOf(value), type: 48 } }, manufacturerOptions.hue);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genBasic', [0x0034], manufacturerOptions.hue);
        },
    },
    danfoss_thermostat_occupied_heating_setpoint: {
        key: ['occupied_heating_setpoint'],
        convertSet: async (entity, key, value, meta) => {
            const payload = {
                // 1: "User Interaction" Changes occupied heating setpoint and triggers an aggressive reaction
                //   of the actuator as soon as control SW runs, to replicate the behavior of turning the dial on the eTRV.
                setpointType: 1,
                setpoint: (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100,
            };
            await entity.command('hvacThermostat', 'danfossSetpointCommand', payload, manufacturerOptions.danfoss);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['occupiedHeatingSetpoint']);
        },
    },
    danfoss_thermostat_occupied_heating_setpoint_scheduled: {
        key: ['occupied_heating_setpoint_scheduled'],
        convertSet: async (entity, key, value, meta) => {
            const payload = {
                // 0: "Schedule Change" Just changes occupied heating setpoint. No special behavior,
                //   the PID control setpoint will be update with the new setpoint.
                setpointType: 0,
                setpoint: (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100,
            };
            await entity.command('hvacThermostat', 'danfossSetpointCommand', payload, manufacturerOptions.danfoss);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['occupiedHeatingSetpoint']);
        },
    },
    danfoss_mounted_mode_active: {
        key: ['mounted_mode_active'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossMountedModeActive'], manufacturerOptions.danfoss);
        },
    },
    danfoss_mounted_mode_control: {
        key: ['mounted_mode_control'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossMountedModeControl': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'mounted_mode_control': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossMountedModeControl'], manufacturerOptions.danfoss);
        },
    },
    danfoss_thermostat_vertical_orientation: {
        key: ['thermostat_vertical_orientation'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossThermostatOrientation': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'thermostat_vertical_orientation': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossThermostatOrientation'], manufacturerOptions.danfoss);
        },
    },
    danfoss_external_measured_room_sensor: {
        key: ['external_measured_room_sensor'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossExternalMeasuredRoomSensor': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'external_measured_room_sensor': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossExternalMeasuredRoomSensor'], manufacturerOptions.danfoss);
        },
    },
    danfoss_radiator_covered: {
        key: ['radiator_covered'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossRadiatorCovered': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'radiator_covered': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossRadiatorCovered'], manufacturerOptions.danfoss);
        },
    },
    danfoss_viewing_direction: {
        key: ['viewing_direction'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacUserInterfaceCfg', { 'danfossViewingDirection': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'viewing_direction': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacUserInterfaceCfg', ['danfossViewingDirection'], manufacturerOptions.danfoss);
        },
    },
    danfoss_algorithm_scale_factor: {
        key: ['algorithm_scale_factor'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossAlgorithmScaleFactor': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'algorithm_scale_factor': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossAlgorithmScaleFactor'], manufacturerOptions.danfoss);
        },
    },
    danfoss_heat_available: {
        key: ['heat_available'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossHeatAvailable': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'heat_available': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossHeatAvailable'], manufacturerOptions.danfoss);
        },
    },
    danfoss_heat_required: {
        key: ['heat_required'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossHeatRequired'], manufacturerOptions.danfoss);
        },
    },
    danfoss_day_of_week: {
        key: ['day_of_week'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 'danfossDayOfWeek': utils.getKey(constants.thermostatDayOfWeek, value, undefined, Number) };
            await entity.write('hvacThermostat', payload, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'day_of_week': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossDayOfWeek'], manufacturerOptions.danfoss);
        },
    },
    danfoss_trigger_time: {
        key: ['trigger_time'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossTriggerTime': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'trigger_time': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossTriggerTime'], manufacturerOptions.danfoss);
        },
    },
    danfoss_window_open_feature: {
        key: ['window_open_feature'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossWindowOpenFeatureEnable': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'window_open_feature': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossWindowOpenFeatureEnable'], manufacturerOptions.danfoss);
        },
    },
    danfoss_window_open_internal: {
        key: ['window_open_internal'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossWindowOpenInternal'], manufacturerOptions.danfoss);
        },
    },
    danfoss_window_open_external: {
        key: ['window_open_external'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossWindowOpenExternal': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'window_open_external': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossWindowOpenExternal'], manufacturerOptions.danfoss);
        },
    },
    danfoss_load_balancing_enable: {
        key: ['load_balancing_enable'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossLoadBalancingEnable': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'load_balancing_enable': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossLoadBalancingEnable'], manufacturerOptions.danfoss);
        },
    },
    danfoss_load_room_mean: {
        key: ['load_room_mean'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossLoadRoomMean': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'load_room_mean': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossLoadRoomMean'], manufacturerOptions.danfoss);
        },
    },
    danfoss_load_estimate: {
        key: ['load_estimate'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossLoadEstimate'], manufacturerOptions.danfoss);
        },
    },
    danfoss_preheat_status: {
        key: ['preheat_status'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossPreheatStatus'], manufacturerOptions.danfoss);
        },
    },
    danfoss_adaptation_status: {
        key: ['adaptation_run_status'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossAdaptionRunStatus'], manufacturerOptions.danfoss);
        },
    },
    danfoss_adaptation_settings: {
        key: ['adaptation_run_settings'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('hvacThermostat', { 'danfossAdaptionRunSettings': value }, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 200, state: { 'adaptation_run_settings': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossAdaptionRunSettings'], manufacturerOptions.danfoss);
        },
    },
    danfoss_adaptation_control: {
        key: ['adaptation_run_control'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 'danfossAdaptionRunControl': utils.getKey(constants.danfossAdaptionRunControl, value, value, Number) };
            await entity.write('hvacThermostat', payload, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 250, state: { 'adaptation_run_control': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossAdaptionRunControl'], manufacturerOptions.danfoss);
        },
    },
    danfoss_regulation_setpoint_offset: {
        key: ['regulation_setpoint_offset'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 'danfossRegulationSetpointOffset': value };
            await entity.write('hvacThermostat', payload, manufacturerOptions.danfoss);
            return { readAfterWriteTime: 250, state: { 'regulation_setpoint_offset': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossRegulationSetpointOffset'], manufacturerOptions.danfoss);
        },
    },
    danfoss_output_status: {
        key: ['output_status'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossOutputStatus'], manufacturerOptions.danfoss);
        },
    },
    danfoss_room_status_code: {
        key: ['room_status_code'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['danfossRoomStatusCode'], manufacturerOptions.danfoss);
        },
    },
    danfoss_system_status_code: {
        key: ['system_status_code'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haDiagnostic', ['danfossSystemStatusCode'], manufacturerOptions.danfoss);
        },
    },
    danfoss_system_status_water: {
        key: ['system_status_water'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haDiagnostic', ['danfossSystemStatusWater'], manufacturerOptions.danfoss);
        },
    },
    danfoss_multimaster_role: {
        key: ['multimaster_role'],
        convertGet: async (entity, key, meta) => {
            await entity.read('haDiagnostic', ['danfossMultimasterRole'], manufacturerOptions.danfoss);
        },
    },
    ZMCSW032D_cover_position: {
        key: ['position', 'tilt'],
        convertSet: async (entity, key, value, meta) => {
            if (meta.options.hasOwnProperty('time_close') && meta.options.hasOwnProperty('time_open')) {
                const sleepSeconds = async (s) => {
                    return new Promise((resolve) => setTimeout(resolve, s * 1000));
                };
                const oldPosition = meta.state.position;
                if (value == 100) {
                    await entity.command('closuresWindowCovering', 'upOpen', {}, utils.getOptions(meta.mapped, entity));
                }
                else if (value == 0) {
                    await entity.command('closuresWindowCovering', 'downClose', {}, utils.getOptions(meta.mapped, entity));
                }
                else {
                    if (oldPosition > value) {
                        const delta = oldPosition - value;
                        const mutiplicateur = meta.options.time_open / 100;
                        const timeBeforeStop = delta * mutiplicateur;
                        await entity.command('closuresWindowCovering', 'downClose', {}, utils.getOptions(meta.mapped, entity));
                        await sleepSeconds(timeBeforeStop);
                        await entity.command('closuresWindowCovering', 'stop', {}, utils.getOptions(meta.mapped, entity));
                    }
                    else if (oldPosition < value) {
                        const delta = value - oldPosition;
                        const mutiplicateur = meta.options.time_close / 100;
                        const timeBeforeStop = delta * mutiplicateur;
                        await entity.command('closuresWindowCovering', 'upOpen', {}, utils.getOptions(meta.mapped, entity));
                        await sleepSeconds(timeBeforeStop);
                        await entity.command('closuresWindowCovering', 'stop', {}, utils.getOptions(meta.mapped, entity));
                    }
                }
                return { state: { position: value } };
            }
        },
        convertGet: async (entity, key, meta) => {
            const isPosition = (key === 'position');
            await entity.read('closuresWindowCovering', [isPosition ? 'currentPositionLiftPercentage' : 'currentPositionTiltPercentage']);
        },
    },
    namron_thermostat: {
        key: [
            'lcd_brightness', 'button_vibration_level', 'floor_sensor_type', 'sensor', 'powerup_status', 'floor_sensor_calibration',
            'dry_time', 'mode_after_dry', 'temperature_display', 'window_open_check', 'hysterersis', 'display_auto_off_enabled',
            'alarm_airtemp_overvalue', 'away_mode',
        ],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'lcd_brightness') {
                const lookup = { 'low': 0, 'mid': 1, 'high': 2 };
                const payload = { 0x1000: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'button_vibration_level') {
                const lookup = { 'off': 0, 'low': 1, 'high': 2 };
                const payload = { 0x1001: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'floor_sensor_type') {
                const lookup = { '10k': 1, '15k': 2, '50k': 3, '100k': 4, '12k': 5 };
                const payload = { 0x1002: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'sensor') {
                const lookup = { 'air': 0, 'floor': 1, 'both': 2 };
                const payload = { 0x1003: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'powerup_status') {
                const lookup = { 'default': 0, 'last_status': 1 };
                const payload = { 0x1004: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'floor_sensor_calibration') {
                const payload = { 0x1005: { value: Math.round(value * 10), type: 0x28 } }; // INT8S
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'dry_time') {
                const payload = { 0x1006: { value: value, type: 0x20 } }; // INT8U
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'mode_after_dry') {
                const lookup = { 'off': 0, 'manual': 1, 'auto': 2, 'away': 3 };
                const payload = { 0x1007: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'temperature_display') {
                const lookup = { 'room': 0, 'floor': 1 };
                const payload = { 0x1008: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'window_open_check') {
                const payload = { 0x1009: { value: value * 2, type: 0x20 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'hysterersis') {
                const payload = { 0x100A: { value: value * 10, type: 0x20 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'display_auto_off_enabled') {
                const lookup = { 'disabled': 0, 'enabled': 1 };
                const payload = { 0x100B: { value: lookup[value], type: herdsman.Zcl.DataType.enum8 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'alarm_airtemp_overvalue') {
                const payload = { 0x2001: { value: value, type: 0x20 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
            else if (key === 'away_mode') {
                const payload = { 0x2002: { value: Number(value === 'ON'), type: 0x30 } };
                await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
            }
        },
        convertGet: async (entity, key, meta) => {
            switch (key) {
                case 'lcd_brightness':
                    await entity.read('hvacThermostat', [0x1000], manufacturerOptions.sunricher);
                    break;
                case 'button_vibration_level':
                    await entity.read('hvacThermostat', [0x1001], manufacturerOptions.sunricher);
                    break;
                case 'floor_sensor_type':
                    await entity.read('hvacThermostat', [0x1002], manufacturerOptions.sunricher);
                    break;
                case 'sensor':
                    await entity.read('hvacThermostat', [0x1003], manufacturerOptions.sunricher);
                    break;
                case 'powerup_status':
                    await entity.read('hvacThermostat', [0x1004], manufacturerOptions.sunricher);
                    break;
                case 'floor_sensor_calibration':
                    await entity.read('hvacThermostat', [0x1005], manufacturerOptions.sunricher);
                    break;
                case 'dry_time':
                    await entity.read('hvacThermostat', [0x1006], manufacturerOptions.sunricher);
                    break;
                case 'mode_after_dry':
                    await entity.read('hvacThermostat', [0x1007], manufacturerOptions.sunricher);
                    break;
                case 'temperature_display':
                    await entity.read('hvacThermostat', [0x1008], manufacturerOptions.sunricher);
                    break;
                case 'window_open_check':
                    await entity.read('hvacThermostat', [0x1009], manufacturerOptions.sunricher);
                    break;
                case 'hysterersis':
                    await entity.read('hvacThermostat', [0x100A], manufacturerOptions.sunricher);
                    break;
                case 'display_auto_off_enabled':
                    await entity.read('hvacThermostat', [0x100B], manufacturerOptions.sunricher);
                    break;
                case 'alarm_airtemp_overvalue':
                    await entity.read('hvacThermostat', [0x2001], manufacturerOptions.sunricher);
                    break;
                case 'away_mode':
                    await entity.read('hvacThermostat', [0x2002], manufacturerOptions.sunricher);
                    break;
                default: // Unknown key
                    throw new Error(`Unhandled key toZigbee.namron_thermostat.convertGet ${key}`);
            }
        },
    },
    namron_thermostat_child_lock: {
        key: ['child_lock'],
        convertSet: async (entity, key, value, meta) => {
            const keypadLockout = Number(value === 'LOCK');
            await entity.write('hvacUserInterfaceCfg', { keypadLockout });
            return { readAfterWriteTime: 250, state: { child_lock: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacUserInterfaceCfg', ['keypadLockout']);
        },
    },
    easycode_auto_relock: {
        key: ['auto_relock'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('closuresDoorLock', { autoRelockTime: value ? 1 : 0 }, utils.getOptions(meta.mapped, entity));
            return { state: { auto_relock: value } };
        },
    },
    tuya_led_control: {
        key: ['brightness', 'color', 'color_temp'],
        options: [exposes.options.color_sync()],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'brightness' && meta.state.color_mode == constants.colorModeLookup[2] &&
                !meta.message.hasOwnProperty('color') && !meta.message.hasOwnProperty('color_temp')) {
                const zclData = { level: Number(value), transtime: 0 };
                await entity.command('genLevelCtrl', 'moveToLevel', zclData, utils.getOptions(meta.mapped, entity));
                globalStore.putValue(entity, 'brightness', zclData.level);
                return { state: { brightness: zclData.level } };
            }
            if (key === 'brightness' && meta.message.hasOwnProperty('color_temp')) {
                const zclData = { colortemp: utils.mapNumberRange(meta.message.color_temp, 500, 154, 0, 254), transtime: 0 };
                const zclDataBrightness = { level: Number(value), transtime: 0 };
                await entity.command('lightingColorCtrl', 'tuyaRgbMode', { enable: 0 }, {}, { disableDefaultResponse: true });
                await entity.command('lightingColorCtrl', 'moveToColorTemp', zclData, utils.getOptions(meta.mapped, entity));
                await entity.command('genLevelCtrl', 'moveToLevel', zclDataBrightness, utils.getOptions(meta.mapped, entity));
                globalStore.putValue(entity, 'brightness', zclDataBrightness.level);
                const newState = {
                    brightness: zclDataBrightness.level,
                    color_mode: constants.colorModeLookup[2],
                    color_temp: meta.message.color_temp,
                };
                return { state: libColor.syncColorState(newState, meta.state, entity, meta.options, meta.logger),
                    readAfterWriteTime: zclData.transtime * 100 };
            }
            if (key === 'color_temp') {
                const zclData = { colortemp: utils.mapNumberRange(value, 500, 154, 0, 254), transtime: 0 };
                const zclDataBrightness = { level: globalStore.getValue(entity, 'brightness') || 100, transtime: 0 };
                await entity.command('lightingColorCtrl', 'tuyaRgbMode', { enable: 0 }, {}, { disableDefaultResponse: true });
                await entity.command('lightingColorCtrl', 'moveToColorTemp', zclData, utils.getOptions(meta.mapped, entity));
                await entity.command('genLevelCtrl', 'moveToLevel', zclDataBrightness, utils.getOptions(meta.mapped, entity));
                const newState = {
                    brightness: zclDataBrightness.level,
                    color_mode: constants.colorModeLookup[2],
                    color_temp: value,
                };
                return { state: libColor.syncColorState(newState, meta.state, entity, meta.options, meta.logger),
                    readAfterWriteTime: zclData.transtime * 100 };
            }
            const zclData = {
                brightness: globalStore.getValue(entity, 'brightness') || 100,
                hue: utils.mapNumberRange(meta.state.color.h, 0, 360, 0, 254) || 100,
                saturation: utils.mapNumberRange(meta.state.color.s, 0, 100, 0, 254) || 100,
                transtime: 0,
            };
            if (value.h) {
                zclData.hue = utils.mapNumberRange(value.h, 0, 360, 0, 254);
            }
            if (value.hue) {
                zclData.hue = utils.mapNumberRange(value.hue, 0, 360, 0, 254);
            }
            if (value.s) {
                zclData.saturation = utils.mapNumberRange(value.s, 0, 100, 0, 254);
            }
            if (value.saturation) {
                zclData.saturation = utils.mapNumberRange(value.saturation, 0, 100, 0, 254);
            }
            if (value.b) {
                zclData.brightness = Number(value.b);
            }
            if (value.brightness) {
                zclData.brightness = Number(value.brightness);
            }
            if (typeof value === 'number') {
                zclData.brightness = value;
            }
            if (meta.message.hasOwnProperty('color')) {
                if (meta.message.color.h) {
                    zclData.hue = utils.mapNumberRange(meta.message.color.h, 0, 360, 0, 254);
                }
                if (meta.message.color.s) {
                    zclData.saturation = utils.mapNumberRange(meta.message.color.s, 0, 100, 0, 254);
                }
                if (meta.message.color.b) {
                    zclData.brightness = Number(meta.message.color.b);
                }
                if (meta.message.color.brightness) {
                    zclData.brightness = Number(meta.message.color.brightness);
                }
            }
            await entity.command('lightingColorCtrl', 'tuyaRgbMode', { enable: 1 }, {}, { disableDefaultResponse: true });
            await entity.command('lightingColorCtrl', 'tuyaMoveToHueAndSaturationBrightness', zclData, utils.getOptions(meta.mapped, entity));
            globalStore.putValue(entity, 'brightness', zclData.brightness);
            const newState = {
                brightness: zclData.brightness,
                color: {
                    h: utils.mapNumberRange(zclData.hue, 0, 254, 0, 360),
                    hue: utils.mapNumberRange(zclData.hue, 0, 254, 0, 360),
                    s: utils.mapNumberRange(zclData.saturation, 0, 254, 0, 100),
                    saturation: utils.mapNumberRange(zclData.saturation, 0, 254, 0, 100),
                },
                color_mode: constants.colorModeLookup[0],
            };
            return { state: libColor.syncColorState(newState, meta.state, entity, meta.options, meta.logger),
                readAfterWriteTime: zclData.transtime * 100 };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingColorCtrl', [
                'currentHue', 'currentSaturation', 'tuyaBrightness', 'tuyaRgbMode', 'colorTemperature',
            ]);
        },
    },
    tuya_led_controller: {
        key: ['state', 'color'],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'state') {
                if (value.toLowerCase() === 'off') {
                    await entity.command('genOnOff', 'offWithEffect', { effectid: 0x01, effectvariant: 0x01 }, utils.getOptions(meta.mapped, entity));
                }
                else {
                    const payload = { level: 255, transtime: 0 };
                    await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', payload, utils.getOptions(meta.mapped, entity));
                }
                return { state: { state: value.toUpperCase() } };
            }
            else if (key === 'color') {
                const hue = {};
                const saturation = {};
                hue.hue = utils.mapNumberRange(value.h, 0, 360, 0, 254);
                saturation.saturation = utils.mapNumberRange(value.s, 0, 100, 0, 254);
                hue.transtime = saturation.transtime = 0;
                hue.direction = 0;
                await entity.command('lightingColorCtrl', 'moveToHue', hue, {}, utils.getOptions(meta.mapped, entity));
                await entity.command('lightingColorCtrl', 'moveToSaturation', saturation, {}, utils.getOptions(meta.mapped, entity));
            }
        },
        convertGet: async (entity, key, meta) => {
            if (key === 'state') {
                await entity.read('genOnOff', ['onOff']);
            }
            else if (key === 'color') {
                await entity.read('lightingColorCtrl', ['currentHue', 'currentSaturation']);
            }
        },
    },
    RM01_light_onoff_brightness: {
        key: ['state', 'brightness', 'brightness_percent'],
        options: [exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (utils.hasEndpoints(meta.device, [0x12])) {
                const endpoint = meta.device.getEndpoint(0x12);
                return await converters.light_onoff_brightness.convertSet(endpoint, key, value, meta);
            }
            else {
                throw new Error('OnOff and LevelControl not supported on this RM01 device.');
            }
        },
        convertGet: async (entity, key, meta) => {
            if (utils.hasEndpoints(meta.device, [0x12])) {
                const endpoint = meta.device.getEndpoint(0x12);
                return await converters.light_onoff_brightness.convertGet(endpoint, key, meta);
            }
            else {
                throw new Error('OnOff and LevelControl not supported on this RM01 device.');
            }
        },
    },
    RM01_light_brightness_step: {
        options: [exposes.options.transition()],
        key: ['brightness_step', 'brightness_step_onoff'],
        convertSet: async (entity, key, value, meta) => {
            if (utils.hasEndpoints(meta.device, [0x12])) {
                const endpoint = meta.device.getEndpoint(0x12);
                return await converters.light_brightness_step.convertSet(endpoint, key, value, meta);
            }
            else {
                throw new Error('LevelControl not supported on this RM01 device.');
            }
        },
    },
    RM01_light_brightness_move: {
        key: ['brightness_move', 'brightness_move_onoff'],
        convertSet: async (entity, key, value, meta) => {
            if (utils.hasEndpoints(meta.device, [0x12])) {
                const endpoint = meta.device.getEndpoint(0x12);
                return await converters.light_brightness_move.convertSet(endpoint, key, value, meta);
            }
            else {
                throw new Error('LevelControl not supported on this RM01 device.');
            }
        },
    },
    aqara_opple_operation_mode: {
        key: ['operation_mode'],
        convertSet: async (entity, key, value, meta) => {
            // modes:
            // 0 - 'command' mode. keys send commands. useful for binding
            // 1 - 'event' mode. keys send events. useful for handling
            const lookup = { command: 0, event: 1 };
            const endpoint = meta.device.getEndpoint(1);
            await endpoint.write('aqaraOpple', { 'mode': lookup[value.toLowerCase()] }, { manufacturerCode: 0x115f });
            return { state: { operation_mode: value.toLowerCase() } };
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.getEndpoint(1);
            await endpoint.read('aqaraOpple', ['mode'], { manufacturerCode: 0x115f });
        },
    },
    EMIZB_132_mode: {
        key: ['interface_mode'],
        convertSet: async (entity, key, value, meta) => {
            const endpoint = meta.device.getEndpoint(2);
            const lookup = {
                'norwegian_han': { value: 0x0200, acVoltageDivisor: 10, acCurrentDivisor: 10 },
                'norwegian_han_extra_load': { value: 0x0201, acVoltageDivisor: 10, acCurrentDivisor: 10 },
                'aidon_meter': { value: 0x0202, acVoltageDivisor: 10, acCurrentDivisor: 10 },
                'kaifa_and_kamstrup': { value: 0x0203, acVoltageDivisor: 10, acCurrentDivisor: 1000 },
            };
            if (!lookup[value]) {
                throw new Error(`Interface mode '${value}' is not valid, chose: ${Object.keys(lookup)}`);
            }
            await endpoint.write('seMetering', { 0x0302: { value: lookup[value].value, type: 49 } }, { manufacturerCode: 0x1015 });
            // As the device reports the incorrect divisor, we need to set it here
            // https://github.com/Koenkk/zigbee-herdsman-converters/issues/974#issuecomment-604347303
            // Values for norwegian_han and aidon_meter have not been been checked
            endpoint.saveClusterAttributeKeyValue('haElectricalMeasurement', {
                acVoltageMultiplier: 1,
                acVoltageDivisor: lookup[value].acVoltageDivisor,
                acCurrentMultiplier: 1,
                acCurrentDivisor: lookup[value].acCurrentDivisor,
            });
            return { state: { interface_mode: value } };
        },
    },
    eurotronic_thermostat_system_mode: {
        key: ['system_mode'],
        convertSet: async (entity, key, value, meta) => {
            const systemMode = utils.getKey(legacy.thermostatSystemModes, value, value, Number);
            const hostFlags = {};
            switch (systemMode) {
                case 0: // off (window_open for eurotronic)
                    hostFlags['boost'] = false;
                    hostFlags['window_open'] = true;
                    break;
                case 4: // heat (boost for eurotronic)
                    hostFlags['boost'] = true;
                    hostFlags['window_open'] = false;
                    break;
                default:
                    hostFlags['boost'] = false;
                    hostFlags['window_open'] = false;
                    break;
            }
            await converters.eurotronic_host_flags.convertSet(entity, 'eurotronic_host_flags', hostFlags, meta);
        },
        convertGet: async (entity, key, meta) => {
            await converters.eurotronic_host_flags.convertGet(entity, 'eurotronic_host_flags', meta);
        },
    },
    eurotronic_host_flags: {
        key: ['eurotronic_host_flags', 'eurotronic_system_mode'],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value === 'object') {
                // read current eurotronic_host_flags (we will update some of them)
                await entity.read('hvacThermostat', [0x4008], manufacturerOptions.eurotronic);
                const currentHostFlags = meta.state.eurotronic_host_flags ? meta.state.eurotronic_host_flags : {};
                // get full hostFlag object
                const hostFlags = { ...currentHostFlags, ...value };
                // calculate bit value
                let bitValue = 1; // bit 0 always 1
                if (hostFlags.mirror_display) {
                    bitValue |= 1 << 1;
                }
                if (hostFlags.boost) {
                    bitValue |= 1 << 2;
                }
                if (value.hasOwnProperty('window_open') && value.window_open != currentHostFlags.window_open) {
                    if (hostFlags.window_open) {
                        bitValue |= 1 << 5;
                    }
                    else {
                        bitValue |= 1 << 4;
                    }
                }
                if (hostFlags.child_protection) {
                    bitValue |= 1 << 7;
                }
                meta.logger.debug(`eurotronic: host_flags object converted to ${bitValue}`);
                value = bitValue;
            }
            const payload = { 0x4008: { value, type: 0x22 } };
            await entity.write('hvacThermostat', payload, manufacturerOptions.eurotronic);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0x4008], manufacturerOptions.eurotronic);
        },
    },
    eurotronic_error_status: {
        key: ['eurotronic_error_status'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0x4002], manufacturerOptions.eurotronic);
        },
    },
    eurotronic_current_heating_setpoint: {
        key: ['current_heating_setpoint'],
        convertSet: async (entity, key, value, meta) => {
            const val = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            const payload = { 0x4003: { value: val, type: 0x29 } };
            await entity.write('hvacThermostat', payload, manufacturerOptions.eurotronic);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0x4003], manufacturerOptions.eurotronic);
        },
    },
    eurotronic_valve_position: {
        key: ['eurotronic_valve_position', 'valve_position'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 0x4001: { value, type: 0x20 } };
            await entity.write('hvacThermostat', payload, manufacturerOptions.eurotronic);
            return { state: { [key]: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0x4001], manufacturerOptions.eurotronic);
        },
    },
    eurotronic_trv_mode: {
        key: ['eurotronic_trv_mode', 'trv_mode'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 0x4000: { value, type: 0x30 } };
            await entity.write('hvacThermostat', payload, manufacturerOptions.eurotronic);
            return { state: { [key]: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0x4000], manufacturerOptions.eurotronic);
        },
    },
    stelpro_thermostat_outdoor_temperature: {
        key: ['thermostat_outdoor_temperature'],
        convertSet: async (entity, key, value, meta) => {
            if (value > -100 && value < 100) {
                await entity.write('hvacThermostat', { StelproOutdoorTemp: value * 100 });
            }
        },
    },
    DTB190502A1_LED: {
        key: ['LED'],
        convertSet: async (entity, key, value, meta) => {
            if (value === 'default') {
                value = 1;
            }
            const lookup = {
                'OFF': '0',
                'ON': '1',
            };
            value = lookup[value];
            // Check for valid data
            if (((value >= 0) && value < 2) == false)
                value = 0;
            const payload = {
                0x4010: {
                    value,
                    type: 0x21,
                },
            };
            await entity.write('genBasic', payload);
        },
    },
    ptvo_switch_trigger: {
        key: ['trigger', 'interval'],
        convertSet: async (entity, key, value, meta) => {
            value = parseInt(value);
            if (!value) {
                return;
            }
            if (key === 'trigger') {
                await entity.command('genOnOff', 'onWithTimedOff', { ctrlbits: 0, ontime: Math.round(value / 100), offwaittime: 0 });
            }
            else if (key === 'interval') {
                await entity.configureReporting('genOnOff', [{
                        attribute: 'onOff',
                        minimumReportInterval: value,
                        maximumReportInterval: value,
                    }]);
            }
        },
    },
    ptvo_switch_uart: {
        key: ['action'],
        convertSet: async (entity, key, value, meta) => {
            if (!value) {
                return;
            }
            const payload = { 14: { value, type: 0x42 } };
            for (const endpoint of meta.device.endpoints) {
                const cluster = 'genMultistateValue';
                if (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster)) {
                    await endpoint.write(cluster, payload);
                    return;
                }
            }
            await entity.write('genMultistateValue', payload);
        },
    },
    ptvo_switch_analog_input: {
        key: ['l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7', 'l8', 'l9', 'l10', 'l11', 'l12', 'l13', 'l14', 'l15', 'l16'],
        convertGet: async (entity, key, meta) => {
            const epId = parseInt(key.substr(1, 2));
            if (utils.hasEndpoints(meta.device, [epId])) {
                const endpoint = meta.device.getEndpoint(epId);
                await endpoint.read('genAnalogInput', ['presentValue', 'description']);
            }
        },
        convertSet: async (entity, key, value, meta) => {
            const epId = parseInt(key.substr(1, 2));
            if (utils.hasEndpoints(meta.device, [epId])) {
                const endpoint = meta.device.getEndpoint(epId);
                let cluster = 'genLevelCtrl';
                if (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster)) {
                    const value2 = parseInt(value);
                    if (isNaN(value2)) {
                        return;
                    }
                    const payload = { 'currentLevel': value2 };
                    await endpoint.write(cluster, payload);
                    return;
                }
                cluster = 'genAnalogInput';
                if (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster)) {
                    const value2 = parseFloat(value);
                    if (isNaN(value2)) {
                        return;
                    }
                    const payload = { 'presentValue': value2 };
                    await endpoint.write(cluster, payload);
                    return;
                }
            }
            return;
        },
    },
    ptvo_switch_light_brightness: {
        key: ['brightness', 'brightness_percent', 'transition'],
        options: [exposes.options.transition()],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'transition') {
                return;
            }
            const cluster = 'genLevelCtrl';
            if (entity.supportsInputCluster(cluster) || entity.supportsOutputCluster(cluster)) {
                const message = meta.message;
                let brightness = undefined;
                if (message.hasOwnProperty('brightness')) {
                    brightness = Number(message.brightness);
                }
                else if (message.hasOwnProperty('brightness_percent'))
                    brightness = Math.round(Number(message.brightness_percent) * 2.55);
                if ((brightness !== undefined) && (brightness === 0)) {
                    message.state = 'off';
                    message.brightness = 1;
                }
                return await converters.light_onoff_brightness.convertSet(entity, key, value, meta);
            }
            else {
                throw new Error('LevelControl not supported on this endpoint.');
            }
        },
        convertGet: async (entity, key, meta) => {
            const cluster = 'genLevelCtrl';
            if (entity.supportsInputCluster(cluster) || entity.supportsOutputCluster(cluster)) {
                return await converters.light_onoff_brightness.convertGet(entity, key, meta);
            }
            else {
                throw new Error('LevelControl not supported on this endpoint.');
            }
        },
    },
    tint_scene: {
        key: ['tint_scene'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('genBasic', { 0x4005: { value, type: 0x20 } }, manufacturerOptions.tint);
        },
    },
    bticino_4027C_cover_state: {
        key: ['state'],
        options: [exposes.options.invert_cover()],
        convertSet: async (entity, key, value, meta) => {
            const invert = !(utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
                !meta.options.invert_cover : meta.options.invert_cover);
            const lookup = invert ?
                { 'open': 'upOpen', 'close': 'downClose', 'stop': 'stop', 'on': 'upOpen', 'off': 'downClose' } :
                { 'open': 'downClose', 'close': 'upOpen', 'stop': 'stop', 'on': 'downClose', 'off': 'upOpen' };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            let position = 50;
            if (value.localeCompare('open') == 0) {
                position = 100;
            }
            else if (value.localeCompare('close') == 0) {
                position = 0;
            }
            await entity.command('closuresWindowCovering', lookup[value], {}, utils.getOptions(meta.mapped, entity));
            return { state: { position }, readAfterWriteTime: 0 };
        },
    },
    bticino_4027C_cover_position: {
        key: ['position'],
        options: [exposes.options.invert_cover(), exposes.options.no_position_support()],
        convertSet: async (entity, key, value, meta) => {
            const invert = !(utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
                !meta.options.invert_cover : meta.options.invert_cover);
            let newPosition = value;
            if (meta.options.no_position_support) {
                newPosition = value >= 50 ? 100 : 0;
            }
            const position = newPosition;
            if (invert) {
                newPosition = 100 - newPosition;
            }
            await entity.command('closuresWindowCovering', 'goToLiftPercentage', { percentageliftvalue: newPosition }, utils.getOptions(meta.mapped, entity));
            return { state: { ['position']: position }, readAfterWriteTime: 0 };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresWindowCovering', ['currentPositionLiftPercentage']);
        },
    },
    legrand_identify: {
        key: ['identify'],
        convertSet: async (entity, key, value, meta) => {
            if (!value.timeout) {
                const effects = {
                    'blink3': 0x00,
                    'fixed': 0x01,
                    'blinkgreen': 0x02,
                    'blinkblue': 0x03,
                };
                // only works for blink3 & fixed
                const colors = {
                    'default': 0x00,
                    'red': 0x01,
                    'green': 0x02,
                    'blue': 0x03,
                    'lightblue': 0x04,
                    'yellow': 0x05,
                    'pink': 0x06,
                    'white': 0x07,
                };
                const selectedEffect = effects[value.effect] | effects['blink3'];
                const selectedColor = colors[value.color] | colors['default'];
                const payload = { effectid: selectedEffect, effectvariant: selectedColor };
                await entity.command('genIdentify', 'triggerEffect', payload, {});
            }
            else {
                await entity.command('genIdentify', 'identify', { identifytime: 10 }, {});
            }
        },
    },
    legrand_settingEnableLedInDark: {
        // connected power outlet is on attribute 2 and not 1
        key: ['led_in_dark'],
        convertSet: async (entity, key, value, meta) => {
            // enable or disable the LED (blue) when permitJoin=false (LED off)
            const enableLedIfOn = value === 'ON' || (value === 'OFF' ? false : !!value);
            const payload = { 1: { value: enableLedIfOn, type: 16 } };
            await entity.write('manuSpecificLegrandDevices', payload, manufacturerOptions.legrand);
            return { state: { 'led_in_dark': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('manuSpecificLegrandDevices', [0x0001], manufacturerOptions.legrand);
        },
    },
    legrand_settingEnableLedIfOn: {
        key: ['led_if_on'],
        convertSet: async (entity, key, value, meta) => {
            // enable the LED when the light object is "doing something"
            // on the light switch, the LED is on when the light is on,
            // on the shutter switch, the LED is on when te shutter is moving
            const enableLedIfOn = value === 'ON' || (value === 'OFF' ? false : !!value);
            const payload = { 2: { value: enableLedIfOn, type: 16 } };
            await entity.write('manuSpecificLegrandDevices', payload, manufacturerOptions.legrand);
            return { state: { 'led_if_on': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('manuSpecificLegrandDevices', [0x0002], manufacturerOptions.legrand);
        },
    },
    legrand_deviceMode: {
        key: ['device_mode'],
        convertSet: async (entity, key, value, meta) => {
            // enable the dimmer, requires a recent firmware on the device
            const lookup = {
                // dimmer
                'dimmer_on': 0x0101,
                'dimmer_off': 0x0100,
                // contactor
                'switch': 0x0003,
                'auto': 0x0004,
                // pilot wire
                'pilot_on': 0x0002,
                'pilot_off': 0x0001,
            };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            const payload = { 0: { value: lookup[value], type: 9 } };
            await entity.write('manuSpecificLegrandDevices', payload, manufacturerOptions.legrand);
            return { state: { 'device_mode': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('manuSpecificLegrandDevices', [0x0000, 0x0001, 0x0002], manufacturerOptions.legrand);
        },
    },
    legrand_cableOutletMode: {
        key: ['cable_outlet_mode'],
        convertSet: async (entity, key, value, meta) => {
            const mode = {
                'comfort': 0x00,
                'comfort-1': 0x01,
                'comfort-2': 0x02,
                'eco': 0x03,
                'frost_protection': 0x04,
                'off': 0x05,
            };
            const payload = { data: Buffer.from([mode[value]]) };
            await entity.command('manuSpecificLegrandDevices2', 'command0', payload);
            return { state: { 'cable_outlet_mode': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('manuSpecificLegrandDevices2', [0x0000], manufacturerOptions.legrand);
        },
    },
    legrand_powerAlarm: {
        key: ['power_alarm'],
        convertSet: async (entity, key, value, meta) => {
            const enableAlarm = (value === 'DISABLE' || value === false ? false : true);
            const payloadBolean = { 0xf001: { value: enableAlarm ? 0x01 : 0x00, type: 0x10 } };
            const payloadValue = { 0xf002: { value: value, type: 0x29 } };
            await entity.write('haElectricalMeasurement', payloadValue);
            await entity.write('haElectricalMeasurement', payloadBolean);
            // To have consistent information in the system.
            await entity.read('haElectricalMeasurement', [0xf000, 0xf001, 0xf002]);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('haElectricalMeasurement', [0xf000, 0xf001, 0xf002]);
        },
    },
    diyruz_freepad_on_off_config: {
        key: ['switch_type', 'switch_actions'],
        convertGet: async (entity, key, meta) => {
            await entity.read('genOnOffSwitchCfg', ['switchType', 'switchActions']);
        },
        convertSet: async (entity, key, value, meta) => {
            const switchTypesLookup = {
                toggle: 0x00,
                momentary: 0x01,
                multifunction: 0x02,
            };
            const switchActionsLookup = {
                on: 0x00,
                off: 0x01,
                toggle: 0x02,
            };
            const intVal = parseInt(value, 10);
            const switchType = switchTypesLookup.hasOwnProperty(value) ? switchTypesLookup[value] : intVal;
            const switchActions = switchActionsLookup.hasOwnProperty(value) ? switchActionsLookup[value] : intVal;
            const payloads = {
                switch_type: { switchType },
                switch_actions: { switchActions },
            };
            await entity.write('genOnOffSwitchCfg', payloads[key]);
            return { state: { [`${key}`]: value } };
        },
    },
    TYZB01_on_off: {
        key: ['state', 'time_in_seconds'],
        convertSet: async (entity, key, value, meta) => {
            const result = await converters.on_off.convertSet(entity, key, value, meta);
            const lowerCaseValue = value.toLowerCase();
            if (!['on', 'off'].includes(lowerCaseValue)) {
                return result;
            }
            const messageKeys = Object.keys(meta.message);
            const timeInSecondsValue = function () {
                if (messageKeys.includes('state')) {
                    return meta.message.time_in_seconds;
                }
                if (meta.endpoint_name) {
                    return meta.message[`time_in_seconds_${meta.endpoint_name}`];
                }
                return null;
            }();
            if (!timeInSecondsValue) {
                return result;
            }
            const timeInSeconds = Number(timeInSecondsValue);
            if (!Number.isInteger(timeInSeconds) || timeInSeconds < 0 || timeInSeconds > 0xfffe) {
                throw Error('The time_in_seconds value must be convertible to an integer in the ' +
                    'range: <0x0000, 0xFFFE>');
            }
            const on = lowerCaseValue === 'on';
            await entity.command('genOnOff', 'onWithTimedOff', {
                ctrlbits: 0,
                ontime: (on ? 0 : timeInSeconds.valueOf()),
                offwaittime: (on ? timeInSeconds.valueOf() : 0),
            }, utils.getOptions(meta.mapped, entity));
            return result;
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genOnOff', ['onOff']);
        },
    },
    diyruz_geiger_config: {
        key: ['sensitivity', 'led_feedback', 'buzzer_feedback', 'sensors_count', 'sensors_type', 'alert_threshold'],
        convertSet: async (entity, key, rawValue, meta) => {
            const lookup = {
                'OFF': 0x00,
                'ON': 0x01,
            };
            const sensorsTypeLookup = {
                'СБМ-20/СТС-5/BOI-33': '0',
                'СБМ-19/СТС-6': '1',
                'Others': '2',
            };
            let value = lookup.hasOwnProperty(rawValue) ? lookup[rawValue] : parseInt(rawValue, 10);
            if (key == 'sensors_type') {
                value = sensorsTypeLookup.hasOwnProperty(rawValue) ? sensorsTypeLookup[rawValue] : parseInt(rawValue, 10);
            }
            const payloads = {
                sensitivity: { 0xF000: { value, type: 0x21 } },
                led_feedback: { 0xF001: { value, type: 0x10 } },
                buzzer_feedback: { 0xF002: { value, type: 0x10 } },
                sensors_count: { 0xF003: { value, type: 0x20 } },
                sensors_type: { 0xF004: { value, type: 0x30 } },
                alert_threshold: { 0xF005: { value, type: 0x23 } },
            };
            await entity.write('msIlluminanceLevelSensing', payloads[key]);
            return {
                state: { [key]: rawValue },
            };
        },
        convertGet: async (entity, key, meta) => {
            const payloads = {
                sensitivity: ['msIlluminanceLevelSensing', 0xF000],
                led_feedback: ['msIlluminanceLevelSensing', 0xF001],
                buzzer_feedback: ['msIlluminanceLevelSensing', 0xF002],
                sensors_count: ['msIlluminanceLevelSensing', 0xF003],
                sensors_type: ['msIlluminanceLevelSensing', 0xF004],
                alert_threshold: ['msIlluminanceLevelSensing', 0xF005],
            };
            await entity.read(payloads[key][0], [payloads[key][1]]);
        },
    },
    diyruz_airsense_config: {
        key: ['led_feedback', 'enable_abc', 'threshold1', 'threshold2', 'temperature_offset', 'pressure_offset', 'humidity_offset'],
        convertSet: async (entity, key, rawValue, meta) => {
            const lookup = { 'OFF': 0x00, 'ON': 0x01 };
            const value = lookup.hasOwnProperty(rawValue) ? lookup[rawValue] : parseInt(rawValue, 10);
            const payloads = {
                led_feedback: ['msCO2', { 0x0203: { value, type: 0x10 } }],
                enable_abc: ['msCO2', { 0x0202: { value, type: 0x10 } }],
                threshold1: ['msCO2', { 0x0204: { value, type: 0x21 } }],
                threshold2: ['msCO2', { 0x0205: { value, type: 0x21 } }],
                temperature_offset: ['msTemperatureMeasurement', { 0x0210: { value, type: 0x29 } }],
                pressure_offset: ['msPressureMeasurement', { 0x0210: { value, type: 0x2b } }],
                humidity_offset: ['msRelativeHumidity', { 0x0210: { value, type: 0x29 } }],
            };
            await entity.write(payloads[key][0], payloads[key][1]);
            return {
                state: { [key]: rawValue },
            };
        },
        convertGet: async (entity, key, meta) => {
            const payloads = {
                led_feedback: ['msCO2', 0x0203],
                enable_abc: ['msCO2', 0x0202],
                threshold1: ['msCO2', 0x0204],
                threshold2: ['msCO2', 0x0205],
                temperature_offset: ['msTemperatureMeasurement', 0x0210],
                pressure_offset: ['msPressureMeasurement', 0x0210],
                humidity_offset: ['msRelativeHumidity', 0x0210],
            };
            await entity.read(payloads[key][0], [payloads[key][1]]);
        },
    },
    diyruz_zintercom_config: {
        key: ['mode', 'sound', 'time_ring', 'time_talk', 'time_open', 'time_bell', 'time_report'],
        convertSet: async (entity, key, rawValue, meta) => {
            const lookup = { 'OFF': 0x00, 'ON': 0x01 };
            const modeOpenLookup = { 'never': '0', 'once': '1', 'always': '2', 'drop': '3' };
            let value = lookup.hasOwnProperty(rawValue) ? lookup[rawValue] : parseInt(rawValue, 10);
            if (key == 'mode') {
                value = modeOpenLookup.hasOwnProperty(rawValue) ? modeOpenLookup[rawValue] : parseInt(rawValue, 10);
            }
            const payloads = {
                mode: { 0x0051: { value, type: 0x30 } },
                sound: { 0x0052: { value, type: 0x10 } },
                time_ring: { 0x0053: { value, type: 0x20 } },
                time_talk: { 0x0054: { value, type: 0x20 } },
                time_open: { 0x0055: { value, type: 0x20 } },
                time_bell: { 0x0057: { value, type: 0x20 } },
                time_report: { 0x0056: { value, type: 0x20 } },
            };
            await entity.write('closuresDoorLock', payloads[key]);
            return {
                state: { [key]: rawValue },
            };
        },
        convertGet: async (entity, key, meta) => {
            const payloads = {
                mode: ['closuresDoorLock', 0x0051],
                sound: ['closuresDoorLock', 0x0052],
                time_ring: ['closuresDoorLock', 0x0053],
                time_talk: ['closuresDoorLock', 0x0054],
                time_open: ['closuresDoorLock', 0x0055],
                time_bell: ['closuresDoorLock', 0x0057],
                time_report: ['closuresDoorLock', 0x0056],
            };
            await entity.read(payloads[key][0], [payloads[key][1]]);
        },
    },
    power_source: {
        key: ['power_source', 'charging'],
        convertGet: async (entity, key, meta) => {
            await entity.read('genBasic', ['powerSource']);
        },
    },
    ts0201_temperature_humidity_alarm: {
        key: ['alarm_humidity_max', 'alarm_humidity_min', 'alarm_temperature_max', 'alarm_temperature_min'],
        convertSet: async (entity, key, value, meta) => {
            switch (key) {
                case 'alarm_temperature_max':
                case 'alarm_temperature_min':
                case 'alarm_humidity_max':
                case 'alarm_humidity_min': {
                    // await entity.write('manuSpecificTuya_2', {[key]: value});
                    // instead write as custom attribute to override incorrect herdsman dataType from uint16 to int16
                    // https://github.com/Koenkk/zigbee-herdsman/blob/v0.13.191/src/zcl/definition/cluster.ts#L4235
                    const keyToAttributeLookup = { 'alarm_temperature_max': 0xD00A, 'alarm_temperature_min': 0xD00B,
                        'alarm_humidity_max': 0xD00D, 'alarm_humidity_min': 0xD00E };
                    const payload = { [keyToAttributeLookup[key]]: { value: value, type: 0x29 } };
                    await entity.write('manuSpecificTuya_2', payload);
                    break;
                }
                default: // Unknown key
                    meta.logger.warn(`Unhandled key ${key}`);
            }
        },
    },
    heiman_ir_remote: {
        key: ['send_key', 'create', 'learn', 'delete', 'get_list'],
        convertSet: async (entity, key, value, meta) => {
            const options = {
                // Don't send a manufacturerCode (otherwise set in herdsman):
                // https://github.com/Koenkk/zigbee-herdsman-converters/pull/2827
                manufacturerCode: null,
                ...utils.getOptions(meta.mapped, entity),
            };
            switch (key) {
                case 'send_key':
                    await entity.command('heimanSpecificInfraRedRemote', 'sendKey', { id: value['id'], keyCode: value['key_code'] }, options);
                    break;
                case 'create':
                    await entity.command('heimanSpecificInfraRedRemote', 'createId', { modelType: value['model_type'] }, options);
                    break;
                case 'learn':
                    await entity.command('heimanSpecificInfraRedRemote', 'studyKey', { id: value['id'], keyCode: value['key_code'] }, options);
                    break;
                case 'delete':
                    await entity.command('heimanSpecificInfraRedRemote', 'deleteKey', { id: value['id'], keyCode: value['key_code'] }, options);
                    break;
                case 'get_list':
                    await entity.command('heimanSpecificInfraRedRemote', 'getIdAndKeyCodeList', {}, options);
                    break;
                default: // Unknown key
                    throw new Error(`Unhandled key ${key}`);
            }
        },
    },
    scene_store: {
        key: ['scene_store'],
        convertSet: async (entity, key, value, meta) => {
            const isGroup = entity.constructor.name === 'Group';
            const groupid = isGroup ? entity.groupID : value.hasOwnProperty('group_id') ? value.group_id : 0;
            let sceneid = value;
            let scenename = null;
            if (typeof value === 'object') {
                sceneid = value.ID;
                scenename = value.name;
            }
            const response = await entity.command('genScenes', 'store', { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
            if (isGroup) {
                if (meta.membersState) {
                    for (const member of entity.members) {
                        utils.saveSceneState(member, sceneid, groupid, meta.membersState[member.getDevice().ieeeAddr], scenename);
                    }
                }
            }
            else if (response.status === 0) {
                utils.saveSceneState(entity, sceneid, groupid, meta.state, scenename);
            }
            else {
                throw new Error(`Scene add not succesfull ('${herdsman.Zcl.Status[response.status]}')`);
            }
            meta.logger.info('Successfully stored scene');
            return { state: {} };
        },
    },
    scene_recall: {
        key: ['scene_recall'],
        convertSet: async (entity, key, value, meta) => {
            const groupid = entity.constructor.name === 'Group' ? entity.groupID : 0;
            const sceneid = value;
            await entity.command('genScenes', 'recall', { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
            const addColorMode = (newState) => {
                if (newState.hasOwnProperty('color_temp')) {
                    newState.color_mode = constants.colorModeLookup[2];
                }
                else if (newState.hasOwnProperty('color')) {
                    if (newState.color.hasOwnProperty('x')) {
                        newState.color_mode = constants.colorModeLookup[1];
                    }
                    else {
                        newState.color_mode = constants.colorModeLookup[0];
                    }
                }
                return newState;
            };
            const isGroup = entity.constructor.name === 'Group';
            if (isGroup) {
                const membersState = {};
                for (const member of entity.members) {
                    let recalledState = utils.getSceneState(member, sceneid, groupid);
                    if (recalledState) {
                        // add color_mode if saved state does not contain it
                        if (!recalledState.hasOwnProperty('color_mode')) {
                            recalledState = addColorMode(recalledState);
                        }
                        Object.assign(recalledState, libColor.syncColorState(recalledState, meta.state, entity, meta.options, meta.logger));
                        membersState[member.getDevice().ieeeAddr] = recalledState;
                    }
                    else {
                        meta.logger.warn(`Unknown scene was recalled for ${member.getDevice().ieeeAddr}, can't restore state.`);
                        membersState[member.getDevice().ieeeAddr] = {};
                    }
                }
                meta.logger.info('Successfully recalled group scene');
                return { membersState };
            }
            else {
                let recalledState = utils.getSceneState(entity, sceneid, groupid);
                if (recalledState) {
                    // add color_mode if saved state does not contain it
                    if (!recalledState.hasOwnProperty('color_mode')) {
                        recalledState = addColorMode(recalledState);
                    }
                    Object.assign(recalledState, libColor.syncColorState(recalledState, meta.state, entity, meta.options, meta.logger));
                    meta.logger.info('Successfully recalled scene');
                    return { state: recalledState };
                }
                else {
                    meta.logger.warn(`Unknown scene was recalled for ${entity.deviceIeeeAddress}, can't restore state.`);
                    return { state: {} };
                }
            }
        },
    },
    scene_add: {
        key: ['scene_add'],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value !== 'object') {
                throw new Error('Payload should be object.');
            }
            if (!value.hasOwnProperty('ID')) {
                throw new Error('Payload missing ID.');
            }
            if (value.hasOwnProperty('color_temp') && value.hasOwnProperty('color')) {
                throw new Error(`Don't specify both 'color_temp' and 'color'`);
            }
            const isGroup = entity.constructor.name === 'Group';
            const groupid = isGroup ? entity.groupID : value.hasOwnProperty('group_id') ? value.group_id : 0;
            const sceneid = value.ID;
            const scenename = value.name;
            const transtime = value.hasOwnProperty('transition') ? value.transition : 0;
            const state = {};
            const extensionfieldsets = [];
            for (let [attribute, val] of Object.entries(value)) {
                if (attribute === 'state') {
                    extensionfieldsets.push({ 'clstId': 6, 'len': 1, 'extField': [val.toLowerCase() === 'on' ? 1 : 0] });
                    state['state'] = val.toUpperCase();
                }
                else if (attribute === 'brightness') {
                    extensionfieldsets.push({ 'clstId': 8, 'len': 1, 'extField': [val] });
                    state['brightness'] = val;
                }
                else if (attribute === 'position') {
                    const invert = utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ?
                        !meta.options.invert_cover : meta.options.invert_cover;
                    extensionfieldsets.push({ 'clstId': 258, 'len': 1, 'extField': [invert ? 100 - val : val] });
                    state['position'] = val;
                }
                else if (attribute === 'color_temp') {
                    /*
                     * ZCL version 7 added support for ColorTemperatureMireds
                     *
                     * Currently no devices seem to support this, so always fallback to XY conversion. In the future if a device
                     * supports this, or other features get added this the following commit contains an implementation:
                     * https://github.com/Koenkk/zigbee-herdsman-converters/pull/1837/commits/c22175b946b83230ce4e711c2a3796cf2029e78f
                     *
                     * Conversion to XY is allowed according to the ZCL:
                     * `Since there is a direct relation between ColorTemperatureMireds and XY,
                     *  color temperature, if supported, is stored as XY in the scenes table.`
                     *
                     * See https://github.com/Koenkk/zigbee2mqtt/issues/4926#issuecomment-735947705
                     */
                    const [colorTempMin, colorTempMax] = light.findColorTempRange(entity, meta.logger);
                    val = light.clampColorTemp(val, colorTempMin, colorTempMax, meta.logger);
                    const xy = libColor.ColorXY.fromMireds(val);
                    const xScaled = utils.mapNumberRange(xy.x, 0, 1, 0, 65535);
                    const yScaled = utils.mapNumberRange(xy.y, 0, 1, 0, 65535);
                    extensionfieldsets.push({ 'clstId': 768, 'len': 4, 'extField': [xScaled, yScaled] });
                    state['color_mode'] = constants.colorModeLookup[2];
                    state['color_temp'] = val;
                }
                else if (attribute === 'color') {
                    try {
                        val = JSON.parse(val);
                    }
                    catch (e) {
                        e;
                    }
                    const newColor = libColor.Color.fromConverterArg(val);
                    if (newColor.isXY()) {
                        const xScaled = utils.mapNumberRange(newColor.xy.x, 0, 1, 0, 65535);
                        const yScaled = utils.mapNumberRange(newColor.xy.y, 0, 1, 0, 65535);
                        extensionfieldsets.push({
                            'clstId': 768,
                            'len': 4,
                            'extField': [xScaled, yScaled],
                        });
                        state['color_mode'] = constants.colorModeLookup[1];
                        state['color'] = newColor.xy.toObject();
                    }
                    else if (newColor.isHSV()) {
                        const hsvCorrected = newColor.hsv.colorCorrected(meta);
                        if (utils.getMetaValue(entity, meta.mapped, 'supportsEnhancedHue', 'allEqual', true)) {
                            const hScaled = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535);
                            const sScaled = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254);
                            extensionfieldsets.push({
                                'clstId': 768,
                                'len': 13,
                                'extField': [0, 0, hScaled, sScaled, 0, 0, 0, 0],
                            });
                        }
                        else {
                            // The extensionFieldSet is always EnhancedCurrentHue according to ZCL
                            // When the bulb or all bulbs in a group do not support enhanchedHue,
                            const colorXY = hsvCorrected.toXY();
                            const xScaled = utils.mapNumberRange(colorXY.x, 0, 1, 0, 65535);
                            const yScaled = utils.mapNumberRange(colorXY.y, 0, 1, 0, 65535);
                            extensionfieldsets.push({
                                'clstId': 768,
                                'len': 4,
                                'extField': [xScaled, yScaled],
                            });
                        }
                        state['color_mode'] = constants.colorModeLookup[0];
                        state['color'] = newColor.hsv.toObject(false, false);
                    }
                }
            }
            /*
             * Remove scene first
             *
             * Multiple add scene calls will result in the current and previous
             * payloads to be merged. Resulting in unexpected behavior when
             * trying to replace a scene.
             *
             * We accept a SUCESS or NOT_FOUND as a result of the remove call.
             */
            const removeresp = await entity.command('genScenes', 'remove', { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
            if (isGroup || (removeresp.status === 0 || removeresp.status == 133 || removeresp.status == 139)) {
                const response = await entity.command('genScenes', 'add', { groupid, sceneid, scenename: '', transtime, extensionfieldsets }, utils.getOptions(meta.mapped, entity));
                if (isGroup) {
                    if (meta.membersState) {
                        for (const member of entity.members) {
                            utils.saveSceneState(member, sceneid, groupid, state, scenename);
                        }
                    }
                }
                else if (response.status === 0) {
                    utils.saveSceneState(entity, sceneid, groupid, state, scenename);
                }
                else {
                    throw new Error(`Scene add not succesfull ('${herdsman.Zcl.Status[response.status]}')`);
                }
            }
            else {
                throw new Error(`Scene add unable to remove existing scene ('${herdsman.Zcl.Status[removeresp.status]}')`);
            }
            meta.logger.info('Successfully added scene');
            return { state: {} };
        },
    },
    scene_remove: {
        key: ['scene_remove'],
        convertSet: async (entity, key, value, meta) => {
            const groupid = entity.constructor.name === 'Group' ? entity.groupID : 0;
            const sceneid = value;
            const response = await entity.command('genScenes', 'remove', { groupid, sceneid }, utils.getOptions(meta.mapped, entity));
            const isGroup = entity.constructor.name === 'Group';
            if (isGroup) {
                if (meta.membersState) {
                    for (const member of entity.members) {
                        utils.deleteSceneState(member, sceneid, groupid);
                    }
                }
            }
            else if (response.status === 0) {
                utils.deleteSceneState(entity, sceneid, groupid);
            }
            else {
                throw new Error(`Scene remove not succesfull ('${herdsman.Zcl.Status[response.status]}')`);
            }
            meta.logger.info('Successfully removed scene');
        },
    },
    scene_remove_all: {
        key: ['scene_remove_all'],
        convertSet: async (entity, key, value, meta) => {
            const groupid = entity.constructor.name === 'Group' ? entity.groupID : 0;
            const response = await entity.command('genScenes', 'removeAll', { groupid }, utils.getOptions(meta.mapped, entity));
            const isGroup = entity.constructor.name === 'Group';
            if (isGroup) {
                if (meta.membersState) {
                    for (const member of entity.members) {
                        utils.deleteSceneState(member);
                    }
                }
            }
            else if (response.status === 0) {
                utils.deleteSceneState(entity);
            }
            else {
                throw new Error(`Scene remove all not succesfull ('${herdsman.Zcl.Status[response.status]}')`);
            }
            meta.logger.info('Successfully removed all scenes');
        },
    },
    TS0003_curtain_switch: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'close': 1, 'stop': 2, 'open': 1 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            const endpointID = lookup[value];
            const endpoint = entity.getDevice().getEndpoint(endpointID);
            await endpoint.command('genOnOff', 'on', {}, utils.getOptions(meta.mapped, entity));
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genOnOff', ['onOff']);
        },
    },
    ts0216_duration: {
        key: ['duration'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('ssIasWd', { 'maxDuration': value });
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('ssIasWd', ['maxDuration']);
        },
    },
    ts0216_volume: {
        key: ['volume'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('ssIasWd', { 0x0002: { value: utils.mapNumberRange(value, 0, 100, 100, 10), type: 0x20 } });
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('ssIasWd', [0x0002]);
        },
    },
    ts0216_alarm: {
        key: ['alarm'],
        convertSet: async (entity, key, value, meta) => {
            const info = (value) ? (2 << 4) + (1 << 2) + 0 : 0;
            await entity.command('ssIasWd', 'startWarning', { startwarninginfo: info, warningduration: 0, strobedutycycle: 0, strobelevel: 3 }, utils.getOptions(meta.mapped, entity));
        },
    },
    tuya_cover_calibration: {
        key: ['calibration'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'ON': 0, 'OFF': 1 };
            value = value.toUpperCase();
            utils.validateValue(value, Object.keys(lookup));
            const calibration = lookup[value];
            await entity.write('closuresWindowCovering', { tuyaCalibration: calibration });
            return { state: { calibration: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresWindowCovering', ['tuyaCalibration']);
        },
    },
    tuya_cover_reversal: {
        key: ['motor_reversal'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'ON': 1, 'OFF': 0 };
            value = value.toUpperCase();
            utils.validateValue(value, Object.keys(lookup));
            const reversal = lookup[value];
            await entity.write('closuresWindowCovering', { tuyaMotorReversal: reversal });
            return { state: { motor_reversal: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresWindowCovering', ['tuyaMotorReversal']);
        },
    },
    moes_cover_calibration: {
        key: ['calibration_time'],
        convertSet: async (entity, key, value, meta) => {
            const calibration = value * 10;
            await entity.write('closuresWindowCovering', { moesCalibrationTime: calibration });
            return { state: { calibration_time: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresWindowCovering', ['moesCalibrationTime']);
        },
    },
    ZM35HQ_attr: {
        key: [
            'sensitivity', 'keep_time',
        ],
        convertSet: async (entity, key, value, meta) => {
            switch (key) {
                case 'sensitivity':
                    await entity.write('ssIasZone', { currentZoneSensitivityLevel: { 'low': 0, 'medium': 1, 'high': 2 }[value] }, { sendWhen: 'active' });
                    break;
                case 'keep_time':
                    await entity.write('ssIasZone', { 61441: { value: { 30: 0, 60: 1, 120: 2 }[value], type: 0x20 } }, { sendWhen: 'active' });
                    break;
                default: // Unknown key
                    throw new Error(`Unhandled key ${key}`);
            }
        },
        convertGet: async (entity, key, meta) => {
            // Apparently, reading values may interfere with a commandStatusChangeNotification for changed occupancy.
            // Therefore, read "zoneStatus" as well.
            await entity.read('ssIasZone', ['currentZoneSensitivityLevel', 61441, 'zoneStatus'], { sendWhen: 'active' });
        },
    },
    TS0210_sensitivity: {
        key: ['sensitivity'],
        convertSet: async (entity, key, value, meta) => {
            value = utils.toNumber(value, 'sensitivity');
            await entity.write('ssIasZone', { currentZoneSensitivityLevel: value });
            return { state: { sensitivity: value } };
        },
    },
    viessmann_window_open: {
        key: ['window_open'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['viessmannWindowOpenInternal'], manufacturerOptions.viessmann);
        },
    },
    viessmann_window_open_force: {
        key: ['window_open_force'],
        convertSet: async (entity, key, value, meta) => {
            if (typeof value === 'boolean') {
                await entity.write('hvacThermostat', { 'viessmannWindowOpenForce': value }, manufacturerOptions.viessmann);
                return { readAfterWriteTime: 200, state: { 'window_open_force': value } };
            }
            else {
                meta.logger.error('window_open_force must be a boolean!');
            }
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['viessmannWindowOpenForce'], manufacturerOptions.viessmann);
        },
    },
    viessmann_assembly_mode: {
        key: ['assembly_mode'],
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', ['viessmannAssemblyMode'], manufacturerOptions.viessmann);
        },
    },
    dawondns_only_off: {
        key: ['state'],
        convertSet: async (entity, key, value, meta) => {
            value = value.toLowerCase();
            utils.validateValue(value, ['off']);
            await entity.command('genOnOff', value, {}, utils.getOptions(meta.mapped, entity));
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('genOnOff', ['onOff']);
        },
    },
    idlock_master_pin_mode: {
        key: ['master_pin_mode'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('closuresDoorLock', { 0x4000: { value: value === true ? 1 : 0, type: 0x10 } }, { manufacturerCode: 4919 });
            return { state: { master_pin_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresDoorLock', [0x4000], { manufacturerCode: 4919 });
        },
    },
    idlock_rfid_enable: {
        key: ['rfid_enable'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('closuresDoorLock', { 0x4001: { value: value === true ? 1 : 0, type: 0x10 } }, { manufacturerCode: 4919 });
            return { state: { rfid_enable: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresDoorLock', [0x4001], { manufacturerCode: 4919 });
        },
    },
    idlock_service_mode: {
        key: ['service_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'deactivated': 0, 'random_pin_1x_use': 5, 'random_pin_24_hours': 6 };
            await entity.write('closuresDoorLock', { 0x4003: { value: lookup[value], type: 0x20 } }, { manufacturerCode: 4919 });
            return { state: { service_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresDoorLock', [0x4003], { manufacturerCode: 4919 });
        },
    },
    idlock_lock_mode: {
        key: ['lock_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'auto_off_away_off': 0, 'auto_on_away_off': 1, 'auto_off_away_on': 2, 'auto_on_away_on': 3 };
            await entity.write('closuresDoorLock', { 0x4004: { value: lookup[value], type: 0x20 } }, { manufacturerCode: 4919 });
            return { state: { lock_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresDoorLock', [0x4004], { manufacturerCode: 4919 });
        },
    },
    idlock_relock_enabled: {
        key: ['relock_enabled'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('closuresDoorLock', { 0x4005: { value: value === true ? 1 : 0, type: 0x10 } }, { manufacturerCode: 4919 });
            return { state: { relock_enabled: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('closuresDoorLock', [0x4005], { manufacturerCode: 4919 });
        },
    },
    schneider_pilot_mode: {
        key: ['schneider_pilot_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'contactor': 1, 'pilot': 3 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            const mode = lookup[value];
            await entity.write('schneiderSpecificPilotMode', { 'pilotMode': mode }, { manufacturerCode: 0x105e });
            return { state: { schneider_pilot_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('schneiderSpecificPilotMode', ['pilotMode'], { manufacturerCode: 0x105e });
        },
    },
    schneider_dimmer_mode: {
        key: ['dimmer_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'RC': 1, 'RL': 2 };
            utils.validateValue(value, Object.keys(lookup));
            const mode = lookup[value];
            await entity.write('lightingBallastCfg', { 0xe000: { value: mode, type: 0x30 } }, { manufacturerCode: 0x105e });
            return { state: { dimmer_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingBallastCfg', [0xe000], { manufacturerCode: 0x105e });
        },
    },
    wiser_dimmer_mode: {
        key: ['dimmer_mode'],
        convertSet: async (entity, key, value, meta) => {
            const controlMode = utils.getKey(constants.wiserDimmerControlMode, value, value, Number);
            await entity.write('lightingBallastCfg', { 'wiserControlMode': controlMode }, { manufacturerCode: herdsman.Zcl.ManufacturerCode.SCHNEIDER });
            return { state: { dimmer_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('lightingBallastCfg', ['wiserControlMode'], { manufacturerCode: herdsman.Zcl.ManufacturerCode.SCHNEIDER });
        },
    },
    schneider_temperature_measured_value: {
        key: ['temperature_measured_value'],
        convertSet: async (entity, key, value, meta) => {
            await entity.report('msTemperatureMeasurement', { 'measuredValue': Math.round(value * 100) });
        },
    },
    schneider_thermostat_system_mode: {
        key: ['system_mode'],
        convertSet: async (entity, key, value, meta) => {
            const systemMode = utils.getKey(constants.thermostatSystemModes, value, undefined, Number);
            entity.saveClusterAttributeKeyValue('hvacThermostat', { systemMode: systemMode });
            return { state: { system_mode: value } };
        },
    },
    schneider_thermostat_occupied_heating_setpoint: {
        key: ['occupied_heating_setpoint'],
        convertSet: async (entity, key, value, meta) => {
            const occupiedHeatingSetpoint = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            entity.saveClusterAttributeKeyValue('hvacThermostat', { occupiedHeatingSetpoint: occupiedHeatingSetpoint });
            return { state: { occupied_heating_setpoint: value } };
        },
    },
    schneider_thermostat_control_sequence_of_operation: {
        key: ['control_sequence_of_operation'],
        convertSet: async (entity, key, value, meta) => {
            const val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, value, Number);
            entity.saveClusterAttributeKeyValue('hvacThermostat', { ctrlSeqeOfOper: val });
            return { state: { control_sequence_of_operation: value } };
        },
    },
    schneider_thermostat_pi_heating_demand: {
        key: ['pi_heating_demand'],
        convertSet: async (entity, key, value, meta) => {
            entity.saveClusterAttributeKeyValue('hvacThermostat', { pIHeatingDemand: value });
            return { state: { pi_heating_demand: value } };
        },
    },
    schneider_thermostat_keypad_lockout: {
        key: ['keypad_lockout'],
        convertSet: async (entity, key, value, meta) => {
            const keypadLockout = utils.getKey(constants.keypadLockoutMode, value, value, Number);
            entity.write('hvacUserInterfaceCfg', { keypadLockout }, { sendWhen: 'active' });
            entity.saveClusterAttributeKeyValue('hvacUserInterfaceCfg', { keypadLockout });
            return { state: { keypad_lockout: value } };
        },
    },
    ZNCJMB14LM: {
        key: ['theme',
            'standby_enabled',
            'beep_volume',
            'lcd_brightness',
            'language',
            'screen_saver_style',
            'standby_time',
            'font_size',
            'lcd_auto_brightness_enabled',
            'homepage',
            'screen_saver_enabled',
            'standby_lcd_brightness',
            'available_switches',
            'switch_1_text_icon',
            'switch_2_text_icon',
            'switch_3_text_icon',
        ],
        convertSet: async (entity, key, value, meta) => {
            if (key === 'theme') {
                const lookup = { 'classic': 0, 'concise': 1 };
                await entity.write('aqaraOpple', { 0x0215: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { theme: value } };
            }
            else if (key === 'standby_enabled') {
                await entity.write('aqaraOpple', { 0x0213: { value: value, type: 0x10 } }, manufacturerOptions.xiaomi);
                return { state: { standby_enabled: value } };
            }
            else if (key === 'beep_volume') {
                const lookup = { 'mute': 0, 'low': 1, 'medium': 2, 'high': 3 };
                await entity.write('aqaraOpple', { 0x0212: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { beep_volume: value } };
            }
            else if (key === 'lcd_brightness') {
                await entity.write('aqaraOpple', { 0x0211: { value: value, type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { lcd_brightness: value } };
            }
            else if (key === 'language') {
                const lookup = { 'chinese': 0, 'english': 1 };
                await entity.write('aqaraOpple', { 0x0210: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { language: value } };
            }
            else if (key === 'screen_saver_style') {
                const lookup = { 'classic': 1, 'analog clock': 2 };
                await entity.write('aqaraOpple', { 0x0214: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { screen_saver_style: value } };
            }
            else if (key === 'standby_time') {
                await entity.write('aqaraOpple', { 0x0216: { value: value, type: 0x23 } }, manufacturerOptions.xiaomi);
                return { state: { standby_time: value } };
            }
            else if (key === 'font_size') {
                const lookup = { 'small': 3, 'medium': 4, 'large': 5 };
                await entity.write('aqaraOpple', { 0x0217: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { font_size: value } };
            }
            else if (key === 'lcd_auto_brightness_enabled') {
                await entity.write('aqaraOpple', { 0x0218: { value: value, type: 0x10 } }, manufacturerOptions.xiaomi);
                return { state: { lcd_auto_brightness_enabled: value } };
            }
            else if (key === 'homepage') {
                const lookup = { 'scene': 0, 'feel': 1, 'thermostat': 2, 'switch': 3 };
                await entity.write('aqaraOpple', { 0x0219: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { homepage: value } };
            }
            else if (key === 'screen_saver_enabled') {
                await entity.write('aqaraOpple', { 0x0221: { value: value, type: 0x10 } }, manufacturerOptions.xiaomi);
                return { state: { screen_saver_enabled: value } };
            }
            else if (key === 'standby_lcd_brightness') {
                await entity.write('aqaraOpple', { 0x0222: { value: value, type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { standby_lcd_brightness: value } };
            }
            else if (key === 'available_switches') {
                const lookup = { 'none': 0, '1': 1, '2': 2, '1 and 2': 3, '3': 4, '1 and 3': 5, '2 and 3': 6, 'all': 7 };
                await entity.write('aqaraOpple', { 0x022b: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
                return { state: { available_switches: value } };
            }
            else if (key === 'switch_1_text_icon') {
                const lookup = { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, '11': 11 };
                const payload = [];
                const statearr = {};
                if (value.hasOwnProperty('switch_1_icon')) {
                    payload.push(lookup[value.switch_1_icon]);
                    statearr.switch_1_icon = value.switch_1_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_1_icon = '1';
                }
                if (value.hasOwnProperty('switch_1_text')) {
                    payload.push(...value.switch_1_text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_1_text = value.switch_1_text;
                }
                else {
                    payload.push(...''.text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_1_text = '';
                }
                await entity.write('aqaraOpple', { 0x0223: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
                return { state: statearr };
            }
            else if (key === 'switch_2_text_icon') {
                const lookup = { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, '11': 11 };
                const payload = [];
                const statearr = {};
                if (value.hasOwnProperty('switch_2_icon')) {
                    payload.push(lookup[value.switch_2_icon]);
                    statearr.switch_2_icon = value.switch_2_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_2_icon = '1';
                }
                if (value.hasOwnProperty('switch_2_text')) {
                    payload.push(...value.switch_2_text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_2_text = value.switch_2_text;
                }
                else {
                    payload.push(...''.text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_2_text = '';
                }
                await entity.write('aqaraOpple', { 0x0224: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
                return { state: statearr };
            }
            else if (key === 'switch_3_text_icon') {
                const lookup = { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, '11': 11 };
                const payload = [];
                const statearr = {};
                if (value.hasOwnProperty('switch_3_icon')) {
                    payload.push(lookup[value.switch_3_icon]);
                    statearr.switch_3_icon = value.switch_3_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_3_icon = '1';
                }
                if (value.hasOwnProperty('switch_3_text')) {
                    payload.push(...value.switch_3_text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_3_text = value.switch_3_text;
                }
                else {
                    payload.push(...''.text.split('').map((c) => c.charCodeAt(0)));
                    statearr.switch_3_text = '';
                }
                await entity.write('aqaraOpple', { 0x0225: { value: payload, type: 0x41 } }, manufacturerOptions.xiaomi);
                return { state: statearr };
            }
            else {
                throw new Error(`Not supported: '${key}'`);
            }
        },
    },
    ZNCLBL01LM_hooks_lock: {
        key: ['hooks_lock'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'UNLOCK': 0, 'LOCK': 1 };
            await entity.write('aqaraOpple', { 0x0427: { value: lookup[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { [key]: value } };
        },
    },
    ZNCLBL01LM_hooks_state: {
        key: ['hooks_state'],
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0428], manufacturerOptions.xiaomi);
        },
    },
    ZNCLBL01LM_hand_open: {
        key: ['hand_open'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('aqaraOpple', { 0x0401: { value: !value, type: 0x10 } }, manufacturerOptions.xiaomi);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x0401], manufacturerOptions.xiaomi);
        },
    },
    ZNCLBL01LM_limits_calibration: {
        key: ['limits_calibration'],
        convertSet: async (entity, key, value, meta) => {
            switch (value) {
                case 'start':
                    await entity.write('aqaraOpple', { 0x0407: { value: 0x01, type: 0x20 } }, manufacturerOptions.xiaomi);
                    break;
                case 'end':
                    await entity.write('aqaraOpple', { 0x0407: { value: 0x02, type: 0x20 } }, manufacturerOptions.xiaomi);
                    break;
                case 'reset':
                    await entity.write('aqaraOpple', { 0x0407: { value: 0x00, type: 0x20 } }, manufacturerOptions.xiaomi);
                    // also? await entity.write('aqaraOpple', {0x0402: {value: 0x00, type: 0x10}}, manufacturerOptions.xiaomi);
                    break;
            }
        },
    },
    wiser_fip_setting: {
        key: ['fip_setting'],
        convertSet: async (entity, key, value, meta) => {
            const zoneLookup = { 'manual': 1, 'schedule': 2, 'energy_saver': 3, 'holiday': 6 };
            const zonemodeNum = zoneLookup[meta.state.zone_mode];
            const fipLookup = { 'comfort': 0, 'comfort_-1': 1, 'comfort_-2': 2, 'energy_saving': 3,
                'frost_protection': 4, 'off': 5 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(fipLookup));
            const fipmodeNum = fipLookup[value];
            const payload = {
                zonemode: zonemodeNum,
                fipmode: fipmodeNum,
                reserved: 0xff,
            };
            await entity.command('hvacThermostat', 'wiserSmartSetFipMode', payload, { srcEndpoint: 11, disableDefaultResponse: true });
            return { state: { 'fip_setting': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0xe020]);
        },
    },
    wiser_hact_config: {
        key: ['hact_config'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'unconfigured': 0x00, 'setpoint_switch': 0x80, 'setpoint_fip': 0x82, 'fip_fip': 0x83 };
            value = value.toLowerCase();
            utils.validateValue(value, Object.keys(lookup));
            const mode = lookup[value];
            await entity.write('hvacThermostat', { 0xe011: { value: mode, type: 0x18 } });
            return { state: { 'hact_config': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0xe011]);
        },
    },
    wiser_zone_mode: {
        key: ['zone_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { 'manual': 1, 'schedule': 2, 'energy_saver': 3, 'holiday': 6 };
            const zonemodeNum = lookup[value];
            await entity.write('hvacThermostat', { 0xe010: { value: zonemodeNum, type: 0x30 } });
            return { state: { 'zone_mode': value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('hvacThermostat', [0xe010]);
        },
    },
    wiser_vact_calibrate_valve: {
        key: ['calibrate_valve'],
        convertSet: async (entity, key, value, meta) => {
            await entity.command('hvacThermostat', 'wiserSmartCalibrateValve', {}, { srcEndpoint: 11, disableDefaultResponse: true, sendWhen: 'active' });
            return { state: { 'calibrate_valve': value } };
        },
    },
    wiser_sed_zone_mode: {
        key: ['zone_mode'],
        convertSet: async (entity, key, value, meta) => {
            return { state: { 'zone_mode': value } };
        },
    },
    wiser_sed_occupied_heating_setpoint: {
        key: ['occupied_heating_setpoint'],
        convertSet: async (entity, key, value, meta) => {
            const occupiedHeatingSetpoint = (Math.round((value * 2).toFixed(1)) / 2).toFixed(1) * 100;
            entity.saveClusterAttributeKeyValue('hvacThermostat', { occupiedHeatingSetpoint });
            return { state: { 'occupied_heating_setpoint': value } };
        },
    },
    wiser_sed_thermostat_local_temperature_calibration: {
        key: ['local_temperature_calibration'],
        convertSet: async (entity, key, value, meta) => {
            entity.write('hvacThermostat', { localTemperatureCalibration: Math.round(value * 10) }, { srcEndpoint: 11, disableDefaultResponse: true, sendWhen: 'active' });
            return { state: { local_temperature_calibration: value } };
        },
    },
    wiser_sed_thermostat_keypad_lockout: {
        key: ['keypad_lockout'],
        convertSet: async (entity, key, value, meta) => {
            const keypadLockout = utils.getKey(constants.keypadLockoutMode, value, value, Number);
            await entity.write('hvacUserInterfaceCfg', { keypadLockout }, { srcEndpoint: 11, disableDefaultResponse: true, sendWhen: 'active' });
            return { state: { keypad_lockout: value } };
        },
    },
    sihas_set_people: {
        key: ['people'],
        convertSet: async (entity, key, value, meta) => {
            const payload = { 'presentValue': value };
            const endpoint = meta.device.endpoints.find((e) => e.supportsInputCluster('genAnalogInput'));
            await endpoint.write('genAnalogInput', payload);
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.endpoints.find((e) => e.supportsInputCluster('genAnalogInput'));
            await endpoint.read('genAnalogInput', ['presentValue']);
        },
    },
    tuya_operation_mode: {
        key: ['operation_mode'],
        convertSet: async (entity, key, value, meta) => {
            // modes:
            // 0 - 'command' mode. keys send commands. useful for group control
            // 1 - 'event' mode. keys send events. useful for handling
            const lookup = { command: 0, event: 1 };
            const endpoint = meta.device.getEndpoint(1);
            await endpoint.write('genOnOff', { 'tuyaOperationMode': lookup[value.toLowerCase()] });
            return { state: { operation_mode: value.toLowerCase() } };
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.getEndpoint(1);
            await endpoint.read('genOnOff', ['tuyaOperationMode']);
        },
    },
    xiaomi_switch_click_mode: {
        key: ['click_mode'],
        convertSet: async (entity, key, value, meta) => {
            const lookupState = { 'fast': 0x1, 'multi': 0x02 };
            await entity.write('aqaraOpple', { 0x0125: { value: lookupState[value], type: 0x20 } }, manufacturerOptions.xiaomi);
            return { state: { click_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('aqaraOpple', [0x125], manufacturerOptions.xiaomi);
        },
    },
    led_on_motion: {
        key: ['led_on_motion'],
        convertSet: async (entity, key, value, meta) => {
            await entity.write('ssIasZone', { 0x4000: { value: value === true ? 1 : 0, type: 0x10 } }, { manufacturerCode: 4919 });
            return { state: { led_on_motion: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read('ssIasZone', [0x4000], { manufacturerCode: 4919 });
        },
    },
    // #endregion
    // #region Ignore converters
    ignore_transition: {
        key: ['transition'],
        attr: [],
        convertSet: async (entity, key, value, meta) => {
        },
    },
    ignore_rate: {
        key: ['rate'],
        attr: [],
        convertSet: async (entity, key, value, meta) => {
        },
    },
    // #endregion
    // Not a converter, can be used by tests to clear the store.
    __clearStore__: () => {
        globalStore.clear();
    },
};
module.exports = converters;
//# sourceMappingURL=toZigbee.js.map