UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

5,283 lines • 251 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.toZigbee = exports.fromZigbee = exports.modernExtend = exports.lumiModernExtend = exports.manufacturerCode = exports.trv = exports.presence = exports.numericAttributes2Payload = exports.buffer2DataObject = void 0;
const node_buffer_1 = require("node:buffer");
const fz = __importStar(require("../converters/fromZigbee"));
const exposes = __importStar(require("./exposes"));
const logger_1 = require("./logger");
const modernExtend = __importStar(require("./modernExtend"));
const globalStore = __importStar(require("./store"));
const utils_1 = require("./utils");
const NS = "zhc:lumi";
const e = exposes.presets;
const ea = exposes.access;
const buffer2DataObject = (model, buffer) => {
    const dataObject = {};
    if (buffer !== null && node_buffer_1.Buffer.isBuffer(buffer)) {
        // Lumi struct parsing
        for (let i = 0; i < buffer.length - 1; i++) {
            const index = buffer[i];
            let value = null;
            switch (buffer[i + 1]) {
                case 16:
                case 32:
                    // 0x10 ZclBoolean
                    // 0x20 Zcl8BitUint
                    value = buffer.readUInt8(i + 2);
                    i += 2;
                    break;
                case 33:
                    // 0x21 Zcl16BitUint
                    value = buffer.readUInt16LE(i + 2);
                    i += 3;
                    break;
                case 34:
                    // 0x22 Zcl24BitUint
                    value = buffer.readUIntLE(i + 2, 3);
                    i += 4;
                    break;
                case 35:
                    // 0x23 Zcl32BitUint
                    value = buffer.readUInt32LE(i + 2);
                    i += 5;
                    break;
                case 36:
                    // 0x24 Zcl40BitUint
                    value = buffer.readUIntLE(i + 2, 5);
                    i += 6;
                    break;
                case 37:
                    // 0x25 Zcl48BitUint
                    value = buffer.readUIntLE(i + 2, 6);
                    i += 7;
                    break;
                case 38:
                    // 0x26 Zcl56BitUint
                    value = buffer.readUIntLE(i + 2, 7);
                    i += 8;
                    break;
                case 39:
                    // 0x27 Zcl64BitUint
                    value = buffer.readBigUInt64BE(i + 2);
                    i += 9;
                    break;
                case 40:
                    // 0x28 Zcl8BitInt
                    value = buffer.readInt8(i + 2);
                    i += 2;
                    break;
                case 41:
                    // 0x29 Zcl16BitInt
                    value = buffer.readInt16LE(i + 2);
                    i += 3;
                    break;
                case 42:
                    // 0x2A Zcl24BitInt
                    value = buffer.readIntLE(i + 2, 3);
                    i += 4;
                    break;
                case 43:
                    // 0x2B Zcl32BitInt
                    value = buffer.readInt32LE(i + 2);
                    i += 5;
                    break;
                case 44:
                    // 0x2C Zcl40BitInt
                    value = buffer.readIntLE(i + 2, 5);
                    i += 6;
                    break;
                case 45:
                    // 0x2D Zcl48BitInt
                    value = buffer.readIntLE(i + 2, 6);
                    i += 7;
                    break;
                case 46:
                    // 0x2E Zcl56BitInt
                    value = buffer.readIntLE(i + 2, 7);
                    i += 8;
                    break;
                case 47:
                    // 0x2F Zcl64BitInt
                    value = buffer.readBigInt64BE(i + 2);
                    i += 9;
                    break;
                case 57:
                    // 0x39 ZclSingleFloat
                    value = buffer.readFloatLE(i + 2);
                    i += 5;
                    break;
                case 58:
                    // 0x3a ZclDoubleFloat
                    value = buffer.readDoubleLE(i + 2);
                    i += 5;
                    break;
                case 66:
                    // 0x42 unknown, length taken from what seems correct in the logs, maybe is wrong
                    logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}, moving length 1`, NS);
                    i += 2;
                    break;
                case 95:
                    // 0x5f unknown, length taken from what seems correct in the logs, maybe is wrong
                    logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}, moving length 4`, NS);
                    i += 5;
                    break;
                default:
                    logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}`, NS);
            }
            if (value != null) {
                dataObject[index] = value;
            }
        }
    }
    logger_1.logger.debug(`${model.model}: Processed buffer into data \
            ${JSON.stringify(dataObject, (key, value) => (typeof value === "bigint" ? value.toString() : value))}`, NS);
    return dataObject;
};
exports.buffer2DataObject = buffer2DataObject;
const numericAttributes2Payload = async (msg, meta, model, options, dataObject) => {
    let payload = {};
    for (const [key, value] of Object.entries(dataObject)) {
        switch (key) {
            case "0":
                payload.detection_period = value;
                break;
            case "1":
                payload.voltage = value;
                if (model.meta?.battery?.voltageToPercentage) {
                    (0, utils_1.assertNumber)(value);
                    payload.battery = (0, utils_1.batteryVoltageToPercentage)(value, model.meta.battery.voltageToPercentage);
                }
                break;
            case "2":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.power_outage_count = value - 1;
                }
                break;
            case "3":
                if (["WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM", "MCCGQ14LM", "GZCGQ01LM", "JY-GZ-01AQ", "CTP-R01"].includes(model.model)) {
                    // The temperature value is constant 25 °C and does not change, so we ignore it
                    // https://github.com/Koenkk/zigbee2mqtt/issues/11126
                    // https://github.com/Koenkk/zigbee-herdsman-converters/pull/3585
                    // https://github.com/Koenkk/zigbee2mqtt/issues/13253
                }
                else {
                    (0, utils_1.assertNumber)(value);
                    payload.device_temperature = value; // 0x03
                }
                break;
            case "4":
                if ([
                    "WS-USC01",
                    "WS-USC02",
                    "WS-EUK01",
                    "WS-EUK02",
                    "QBKG27LM",
                    "QBKG28LM",
                    "QBKG29LM",
                    "QBKG25LM",
                    "QBKG38LM",
                    "QBKG39LM",
                    "ZNQBKG42LM",
                    "ZNQBKG43LM",
                    "ZNQBKG44LM",
                    "ZNQBKG45LM",
                ].includes(model.model)) {
                    payload.mode_switch = (0, utils_1.getFromLookup)(value, { 4: "anti_flicker_mode", 1: "quick_mode" });
                }
                break;
            case "5":
                (0, utils_1.assertNumber)(value);
                payload.power_outage_count = value - 1;
                break;
            case "6":
                if (["MCCGQ11LM", "SJCGQ11LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    let count = value;
                    // Sometimes, especially when the device is connected through another lumi router, the sensor
                    // send random values after 16 bit (>65536), so we truncate and read this as 16BitUInt.
                    count = Number.parseInt(count.toString(16).slice(-4), 16);
                    payload.trigger_count = count - 1;
                }
                break;
            case "8":
                if (["ZNLDP13LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                break;
            case "9":
                if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                break;
            case "10":
                // Value 29146 is received for SSM-U02 sometimes here:
                // https://github.com/Koenkk/zigbee2mqtt/issues/17961#issuecomment-1616170548
                if (["SSM-U01", "DLKZMK11LM", "SSM-U02", "DLKZMK12LM"].includes(model.model) && (value === 1 || value === 2)) {
                    payload.switch_type = (0, utils_1.getFromLookup)(value, { 1: "toggle", 2: "momentary" });
                }
                break;
            case "11":
                if (["RTCGQ11LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.illuminance = value;
                }
                break;
            case "12":
                if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                break;
            case "13":
                if (["ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                else if (["ZNCLBL01LM"].includes(model.model)) {
                    // Overwrite version advertised by `genBasic` and `genOta` with correct version:
                    // https://github.com/Koenkk/zigbee2mqtt/issues/15745
                    (0, utils_1.assertNumber)(value);
                    meta.device.meta.lumiFileVersion = value;
                    meta.device.softwareBuildID = exports.trv.decodeFirmwareVersionString(value);
                    meta.device.save();
                }
                break;
            case "17":
                if (["ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                break;
            case "100":
                if ([
                    "QBKG18LM",
                    "QBKG20LM",
                    "QBKG31LM",
                    "QBKG39LM",
                    "QBKG41LM",
                    "QBCZ15LM",
                    "LLKZMK11LM",
                    "QBKG12LM",
                    "QBKG03LM",
                    "QBKG25LM",
                ].includes(model.model)) {
                    // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
                    let mapping;
                    switch (model.model) {
                        case "QBCZ15LM":
                            mapping = "relay";
                            break;
                        case "LLKZMK11LM":
                            mapping = "l1";
                            break;
                        default:
                            mapping = "left";
                    }
                    payload[`state_${mapping}`] = value === 1 ? "ON" : "OFF";
                }
                else if (["WXKG14LM", "WXKG16LM", "WXKG17LM"].includes(model.model)) {
                    payload.click_mode = (0, utils_1.getFromLookup)(value, { 1: "fast", 2: "multi" });
                }
                else if (["WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM", "ZNMS12LM", "ZNCLBL01LM", "RTCGQ12LM", "RTCGQ13LM", "RTCGQ14LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                    // https://github.com/Koenkk/zigbee2mqtt/issues/11126
                    // https://github.com/Koenkk/zigbee2mqtt/issues/12279
                }
                else if (["RTCGQ15LM"].includes(model.model)) {
                    payload.occupancy = value;
                }
                else if (["WSDCGQ01LM", "WSDCGQ11LM", "WSDCGQ12LM", "VOCKQJK11LM"].includes(model.model)) {
                    // https://github.com/Koenkk/zigbee2mqtt/issues/798
                    // Sometimes the sensor publishes non-realistic vales, filter these
                    // @ts-expect-error ignore
                    const temperature = Number.parseFloat(value) / 100.0;
                    if (temperature > -65 && temperature < 65) {
                        payload.temperature = temperature;
                    }
                }
                else if (["RTCGQ11LM"].includes(model.model)) {
                    // It contains the occupancy, but in z2m we use a custom timer to do it, so we ignore it
                    // payload.occupancy = value === 1;
                }
                else if (["MCCGQ11LM", "MCCGQ14LM"].includes(model.model)) {
                    payload.contact = value === 0;
                }
                else if (["SJCGQ11LM"].includes(model.model)) {
                    // Ignore the message. It seems not reliable. See discussion here https://github.com/Koenkk/zigbee2mqtt/issues/12018
                    // payload.water_leak = value === 1;
                }
                else if (["SJCGQ13LM"].includes(model.model)) {
                    payload.water_leak = value === 1;
                }
                else if (["JTYJ-GD-01LM/BW"].includes(model.model)) {
                    payload.smoke_density = value;
                }
                else if (["GZCGQ01LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.illuminance = value;
                }
                else {
                    payload.state = value === 1 ? "ON" : "OFF";
                }
                break;
            case "101":
                if ([
                    "QBKG18LM",
                    "QBKG20LM",
                    "QBKG31LM",
                    "QBKG39LM",
                    "QBKG41LM",
                    "QBCZ15LM",
                    "QBKG25LM",
                    "QBKG33LM",
                    "QBKG34LM",
                    "LLKZMK11LM",
                    "QBKG12LM",
                    "QBKG03LM",
                ].includes(model.model)) {
                    // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
                    let mapping;
                    switch (model.model) {
                        case "QBCZ15LM":
                            mapping = "usb";
                            break;
                        case "QBKG25LM":
                        case "QBKG33LM":
                        case "QBKG34LM":
                            mapping = "center";
                            break;
                        case "LLKZMK11LM":
                            mapping = "l2";
                            break;
                        default:
                            mapping = "right";
                    }
                    payload[`state_${mapping}`] = value === 1 ? "ON" : "OFF";
                }
                else if (["RTCGQ12LM", "RTCGQ14LM", "RTCGQ15LM"].includes(model.model)) {
                    // Sometimes RTCGQ14LM reports high illuminance values in the dark
                    // https://github.com/Koenkk/zigbee2mqtt/issues/12596
                    (0, utils_1.assertNumber)(value);
                    const illuminance = value > 65000 ? 0 : value;
                    payload.illuminance = illuminance;
                }
                else if (["WSDCGQ01LM", "WSDCGQ11LM", "WSDCGQ12LM", "VOCKQJK11LM"].includes(model.model)) {
                    // https://github.com/Koenkk/zigbee2mqtt/issues/798
                    // Sometimes the sensor publishes non-realistic vales, filter these
                    // @ts-expect-error ignore
                    const humidity = Number.parseFloat(value) / 100.0;
                    if (humidity >= 0 && humidity <= 100) {
                        payload.humidity = humidity;
                    }
                }
                else if (["ZNJLBL01LM", "ZNCLDJ12LM"].includes(model.model)) {
                    payload.battery = value;
                }
                else if (["ZNCLBL01LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    const battery = value / 2;
                    payload.battery = (0, utils_1.precisionRound)(battery, 2);
                }
                else if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.presence = (0, utils_1.getFromLookup)(value, { 0: false, 1: true, 255: null });
                }
                else if (["ZNXDD01LM"].includes(model.model)) {
                    payload.brightness = value;
                }
                break;
            case "102":
                if (["QBKG25LM", "QBKG33LM", "QBKG34LM"].includes(model.model)) {
                    payload.state_right = value === 1 ? "ON" : "OFF";
                }
                else if (["WSDCGQ01LM", "WSDCGQ11LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.pressure = value / 100.0;
                }
                else if (["WSDCGQ12LM"].includes(model.model)) {
                    // This pressure value is ignored because it is less accurate than reported in the 'scaledValue' attribute
                    // of the 'msPressureMeasurement' cluster
                }
                else if (["RTCZCGQ11LM"].includes(model.model)) {
                    if (meta.device.applicationVersion < 50) {
                        payload.presence_event = (0, utils_1.getFromLookup)(value, {
                            0: "enter",
                            1: "leave",
                            2: "left_enter",
                            3: "right_leave",
                            4: "right_enter",
                            5: "left_leave",
                            6: "approach",
                            7: "away",
                            255: null,
                        });
                    }
                    else {
                        payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
                    }
                }
                else if (["ZNXDD01LM"].includes(model.model)) {
                    payload.color_temp = value;
                }
                break;
            case "103":
                if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.monitoring_mode = (0, utils_1.getFromLookup)(value, { 0: "undirected", 1: "left_right" });
                }
                else if (["ZNXDD01LM"].includes(model.model)) {
                    // const color_temp_min = (value & 0xffff); // 2700
                    // const color_temp_max = (value >> 16) & 0xffff; // 6500
                }
                break;
            case "105":
                if (["RTCGQ13LM"].includes(model.model)) {
                    payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
                }
                else if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.approach_distance = (0, utils_1.getFromLookup)(value, { 0: "far", 1: "medium", 2: "near" });
                }
                else if (["RTCGQ14LM"].includes(model.model)) {
                    payload.detection_interval = value;
                }
                break;
            case "106":
                if (["RTCGQ14LM"].includes(model.model)) {
                    payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
                }
                break;
            case "107":
                if (["RTCGQ14LM"].includes(model.model)) {
                    payload.trigger_indicator = value === 1;
                }
                else if (["ZNCLBL01LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    const position = options.invert_cover ? 100 - value : value;
                    payload.position = position;
                    payload.state = options.invert_cover ? (position > 0 ? "CLOSE" : "OPEN") : position > 0 ? "OPEN" : "CLOSE";
                }
                break;
            case "149":
                (0, utils_1.assertNumber)(value);
                payload.energy = value; // 0x95
                if (["LLKZMK12LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(payload.energy);
                    payload.energy = payload.energy / 1000;
                }
                // Consumption is deprecated
                payload.consumption = payload.energy;
                break;
            case "150":
                if (["KD-R01D"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.voltage = value * 0.01;
                }
                else if (!["JTYJ-GD-01LM/BW"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.voltage = value * 0.1; // 0x96
                }
                break;
            case "151":
                if (["LLKZMK11LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.current = value;
                }
                else {
                    (0, utils_1.assertNumber)(value);
                    payload.current = value * 0.001;
                }
                break;
            case "152":
                if (["DJT11LM"].includes(model.model)) {
                    // We don't know what implies for this device, it contains values like 30, 50,... that don't seem to change
                }
                else {
                    (0, utils_1.assertNumber)(value);
                    payload.power = value; // 0x98
                }
                break;
            case "154":
                if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                break;
            case "159":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "15%LEL", 2: "10%LEL" });
                }
                else if (["MCCGQ13LM"].includes(model.model)) {
                    payload.detection_distance = (0, utils_1.getFromLookup)(value, { 1: "10mm", 2: "20mm", 3: "30mm" });
                }
                break;
            case "160":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas = value === 1;
                }
                else if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.smoke = value === 1;
                }
                break;
            case "161":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas_density = value;
                }
                else if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.smoke_density = value;
                    payload.smoke_density_dbm = (0, utils_1.getFromLookup)(value, {
                        0: 0,
                        1: 0.085,
                        2: 0.088,
                        3: 0.093,
                        4: 0.095,
                        5: 0.1,
                        6: 0.105,
                        7: 0.11,
                        8: 0.115,
                        9: 0.12,
                        10: 0.125,
                    });
                }
                break;
            case "162":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.test = value === 1;
                }
                break;
            case "163":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.buzzer_manual_mute = value === 1;
                }
                break;
            case "164":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.state = (0, utils_1.getFromLookup)(value, { 0: "work", 1: "preparation" });
                }
                else if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.heartbeat_indicator = value === 1;
                }
                break;
            case "165":
                if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.linkage_alarm = value === 1;
                }
                break;
            case "166":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.linkage_alarm = value === 1;
                }
                break;
            case "238":
                if (["ZNXDD01LM"].includes(model.model)) {
                    // We don't know what the value means for these devices.
                }
                else if (["ZNCLBL01LM"].includes(model.model)) {
                    // Overwrite version advertised by `genBasic` and `genOta` with correct version:
                    // https://github.com/Koenkk/zigbee2mqtt/issues/15745
                    (0, utils_1.assertNumber)(value);
                    meta.device.meta.lumiFileVersion = value;
                    meta.device.softwareBuildID = exports.trv.decodeFirmwareVersionString(value);
                    meta.device.save();
                }
                break;
            case "240":
                payload.flip_indicator_light = value === 1 ? "ON" : "OFF";
                break;
            case "247":
                {
                    const dataObject247 = (0, exports.buffer2DataObject)(model, value);
                    if (["CTP-R01"].includes(model.model)) {
                        // execute pending soft switch of operation_mode, if exists
                        const opModeSwitchTask = globalStore.getValue(meta.device, "opModeSwitchTask");
                        if (opModeSwitchTask) {
                            const { callback, newMode } = opModeSwitchTask;
                            try {
                                await callback();
                                payload.operation_mode = newMode;
                                globalStore.putValue(meta.device, "opModeSwitchTask", null);
                            }
                            catch {
                                // do nothing when callback fails
                            }
                        }
                        else {
                            payload.operation_mode = (0, utils_1.getFromLookup)(dataObject247[155], { 0: "action_mode", 1: "scene_mode" });
                        }
                    }
                    const payload247 = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, dataObject247);
                    payload = { ...payload, ...payload247 };
                }
                break;
            case "258":
                payload.detection_interval = value;
                break;
            case "268":
                if (["RTCGQ13LM", "RTCGQ14LM", "RTCZCGQ11LM"].includes(model.model)) {
                    payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
                }
                else if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "15%LEL", 2: "10%LEL" });
                }
                break;
            case "293":
                payload.click_mode = (0, utils_1.getFromLookup)(value, { 1: "fast", 2: "multi" });
                break;
            case "294":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.buzzer_manual_mute = value === 1;
                }
                break;
            case "295":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.test = value === 1;
                }
                break;
            case "313":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.state = (0, utils_1.getFromLookup)(value, { 0: "work", 1: "preparation" });
                }
                break;
            case "314":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas = value === 1;
                }
                else if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.smoke = value === 1;
                }
                break;
            case "315":
                if (["JT-BZ-01AQ/A"].includes(model.model)) {
                    payload.gas_density = value;
                }
                else if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.smoke_density = value;
                    payload.smoke_density_dbm = (0, utils_1.getFromLookup)(value, {
                        0: 0,
                        1: 0.085,
                        2: 0.088,
                        3: 0.093,
                        4: 0.095,
                        5: 0.1,
                        6: 0.105,
                        7: 0.11,
                        8: 0.115,
                        9: 0.12,
                        10: 0.125,
                    });
                }
                break;
            case "316":
                if (["JY-GZ-01AQ"].includes(model.model)) {
                    payload.heartbeat_indicator = value === 1;
                }
                break;
            case "317":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.buzzer_manual_alarm = value === 1;
                }
                break;
            case "320":
                if (["MCCGQ13LM"].includes(model.model)) {
                    payload.tamper = (0, utils_1.getFromLookup)(value, { 0: false, 1: true });
                }
                break;
            case "322":
                if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.presence = (0, utils_1.getFromLookup)(value, { 0: false, 1: true, 255: null });
                }
                break;
            case "323":
                if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.presence_event = (0, utils_1.getFromLookup)(value, {
                        0: "enter",
                        1: "leave",
                        2: "left_enter",
                        3: "right_leave",
                        4: "right_enter",
                        5: "left_leave",
                        6: "approach",
                        7: "away",
                    });
                }
                break;
            case "324":
                if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.monitoring_mode = (0, utils_1.getFromLookup)(value, { 0: "undirected", 1: "left_right" });
                }
                break;
            case "326":
                if (["RTCZCGQ11LM"].includes(model.model)) {
                    payload.approach_distance = (0, utils_1.getFromLookup)(value, { 0: "far", 1: "medium", 2: "near" });
                }
                break;
            case "328":
                if (["CTP-R01"].includes(model.model)) {
                    // detected hard switch of operation_mode (attribute 0x148[328])
                    payload.operation_mode = (0, utils_1.getFromLookup)(msg.data[328], { 0: "action_mode", 1: "scene_mode" });
                }
                break;
            case "329":
                if (["CTP-R01"].includes(model.model)) {
                    // side_up attribute report (attribute 0x149[329])
                    payload.action = "side_up";
                    payload.side = msg.data[329] + 1;
                }
                break;
            case "331":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.linkage_alarm = value === 1;
                }
                break;
            case "332":
                if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
                    payload.linkage_alarm_state = value === 1;
                }
                break;
            case "338":
                if (["RTCGQ14LM"].includes(model.model)) {
                    payload.trigger_indicator = value === 1;
                }
                break;
            case "512":
                if (["ZNCZ15LM", "QBCZ14LM", "QBCZ15LM", "SP-EUC01"].includes(model.model)) {
                    payload.button_lock = value === 1 ? "OFF" : "ON";
                }
                else {
                    const mode = (0, utils_1.getFromLookup)(value, { 1: "control_relay", 0: "decoupled" });
                    payload[(0, utils_1.postfixWithEndpointName)("operation_mode", msg, model, meta)] = mode;
                }
                break;
            case "513":
                payload.power_outage_memory = value === 1;
                break;
            case "514":
                payload.auto_off = value === 1;
                break;
            case "515":
                payload.led_disabled_night = value === 1;
                break;
            case "519":
                payload.consumer_connected = value === 1;
                break;
            case "523":
                (0, utils_1.assertNumber)(value);
                payload.overload_protection = (0, utils_1.precisionRound)(value, 2);
                break;
            case "550":
                payload.button_switch_mode = value === 1 ? "relay_and_usb" : "relay";
                break;
            case "645":
                // aqara z1 lock relay
                payload.lock_relay = value === 1;
                break;
            case "1025":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    payload.hand_open = !value;
                }
                else {
                    // next values update only when curtain finished initial setup and knows current position
                    // @ts-expect-error ignore
                    payload.options = { ...payload.options, reverse_direction: value[2] === "\u0001", hand_open: value[5] === "\u0000" };
                }
                break;
            case "1028":
                payload = {
                    ...payload,
                    motor_state: (0, utils_1.getFromLookup)(value, options.invert_cover ? { 0: "stopped", 1: "closing", 2: "opening" } : { 0: "stopped", 1: "opening", 2: "closing" }),
                    running: !!value,
                };
                break;
            case "1032":
                if (["ZNJLBL01LM"].includes(model.model)) {
                    payload.motor_speed = (0, utils_1.getFromLookup)(value, { 0: "low", 1: "medium", 2: "high" });
                }
                break;
            case "1033":
                if (["ZNJLBL01LM"].includes(model.model)) {
                    payload.charging_status = value === 1;
                }
                break;
            case "1034":
                if (["ZNJLBL01LM"].includes(model.model)) {
                    payload.battery = value;
                }
                break;
            case "1035":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    payload.voltage = value;
                }
                break;
            case "1055":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.target_position = options.invert_cover ? 100 - value : value;
                }
                break;
            case "1056":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    // This is the "target_state" attribute, which takes the following values: 0: 'OPEN', 1: 'CLOSE', 2: 'STOP'.
                    // It is not used because the values 0 and 1 are not always reported.
                    // https://github.com/Koenkk/zigbee-herdsman-converters/pull/4307
                }
                break;
            case "1057":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    payload.motor_state = (0, utils_1.getFromLookup)(value, options.invert_cover ? { 0: "opening", 1: "closing", 2: "stopped" } : { 0: "closing", 1: "opening", 2: "stopped" });
                    (0, utils_1.assertNumber)(value);
                    payload.running = value < 2;
                }
                break;
            case "1061":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    payload.action = (0, utils_1.getFromLookup)(value, options.invert_cover ? { 1: "manual_close", 2: "manual_open" } : { 1: "manual_open", 2: "manual_close" });
                }
                break;
            case "1063":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    (0, utils_1.getFromLookup)(value, { 0: "UNLOCK", 1: "LOCK" });
                }
                break;
            case "1064":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    payload.hooks_state = (0, utils_1.getFromLookup)(value, { 0: "unlocked", 1: "locked", 2: "locking", 3: "unlocking" });
                    payload.hooks_lock = (0, utils_1.getFromLookup)(value, { 0: "UNLOCK", 1: "LOCK", 2: "UNLOCK", 3: "LOCK" });
                }
                break;
            case "1065":
                if (["ZNCLBL01LM"].includes(model.model)) {
                    (0, utils_1.assertNumber)(value);
                    payload.illuminance = value * 50;
                }
                break;
            case "1289":
                payload.dimmer_mode = (0, utils_1.getFromLookup)(value, { 3: "rgbw", 1: "dual_ct" });
                break;
            case "1299":
                if (["ZNXDD01LM"].includes(model.model)) {
                    // maximum color temp (6500)
                }
                break;
            case "1300":
                if (["ZNXDD01LM"].includes(model.model)) {
                    // minimum color temp (2700)
                }
                break;
            case "65281":
                {
                    // @ts-expect-error ignore
                    const payload65281 = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, value);
                    payload = { ...payload, ...payload65281 };
                }
                break;
            case "65282":
                // This is a a complete structure with attributes, like element 0 for state, element 1 for voltage...
                // At this moment we only extract what we are sure, for example, position 0 seems to be always 1 for a
                // occupancy sensor, so we ignore it at this moment
                // @ts-expect-error ignore
                payload.voltage = value[1].elmVal;
                if (model.meta?.battery?.voltageToPercentage) {
                    (0, utils_1.assertNumber)(payload.voltage);
                    payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, model.meta.battery.voltageToPercentage);
                }
                // @ts-expect-error ignore
                payload.power_outage_count = value[4].elmVal - 1;
                break;
            case "mode":
                (0, utils_1.assertNumber)(value);
                payload.operation_mode = ["command", "event"][value];
                break;
            case "modelId":
                // We ignore it, but we add it here to not shown an unknown key in the log
                break;
            case "illuminance":
                // It contains the illuminance and occupancy, but in z2m we use a custom timer to do it, so we ignore it
                break;
            case "displayUnit":
                // Use lumiDisplayUnit modernExtend, but we add it here to not shown an unknown key in the log
                break;
            case "airQuality":
                // Use lumiAirQuality modernExtend, but we add it here to not shown an unknown key in the log
                break;
            default:
                logger_1.logger.debug(`${model.model}: unknown key ${key} with value ${value}`, NS);
        }
    }
    logger_1.logger.debug(`${model.model}: Processed data into payload ${JSON.stringify(payload)}`, NS);
    return payload;
};
exports.numericAttributes2Payload = numericAttributes2Payload;
const numericAttributes2Lookup = async (model, dataObject) => {
    let result = {};
    for (const [key, value] of Object.entries(dataObject)) {
        switch (key) {
            case "247":
                {
                    const dataObject247 = (0, exports.buffer2DataObject)(model, value);
                    const result247 = await numericAttributes2Lookup(model, dataObject247);
                    result = { ...result, ...result247 };
                }
                break;
            case "65281":
                {
                    const result65281 = await numericAttributes2Lookup(model, value);
                    result = { ...result, ...result65281 };
                }
                break;
            default:
                result[key] = value;
        }
    }
    return result;
};
const lumiPresenceConstants = {
    region_event_key: 0x0151,
    region_event_types: {
        Enter: 1,
        Leave: 2,
        Occupied: 4,
        Unoccupied: 8,
    },
    region_config_write_attribute: 0x0150,
    region_config_write_attribute_type: 0x41,
    region_config_cmds: {
        /**
         * Creates new region (or force replaces existing one)
         * with new zones definition.
         */
        create: 1,
        /**
         * Modifies existing region.
         * Note: unused, as it seems to break existing regions
         * (region stops reporting new detection events).
         * Use "create" instead, as it replaces existing region with new one.
         */
        modify: 2,
        /**
         * Deletes existing region.
         */
        delete: 3,
    },
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_regionId_min: 1,
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_regionId_max: 10,
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_zoneY_min: 1,
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_zoneY_max: 7,
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_zoneX_min: 1,
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    region_config_zoneX_max: 4,
    region_config_cmd_suffix_upsert: 0xff,
    region_config_cmd_suffix_delete: 0x00,
};
const lumiPresenceMappers = {
    lumi_presence: {
        region_event_type_names: {
            [lumiPresenceConstants.region_event_types.Enter]: "enter",
            [lumiPresenceConstants.region_event_types.Leave]: "leave",
            [lumiPresenceConstants.region_event_types.Occupied]: "occupied",
            [lumiPresenceConstants.region_event_types.Unoccupied]: "unoccupied",
        },
    },
};
exports.presence = {
    constants: lumiPresenceConstants,
    mappers: lumiPresenceMappers,
    encodeXCellsDefinition: (xCells) => {
        if (!xCells?.length) {
            return 0;
        }
        return [...xCells.values()].reduce((accumulator, marker) => accumulator + exports.presence.encodeXCellIdx(marker), 0);
    },
    encodeXCellIdx: (cellXIdx) => {
        return 2 ** (cellXIdx - 1);
    },
    parseAqaraFp1RegionDeleteInput: (input) => {
        if (!input || typeof input !== "object") {
            return exports.presence.failure({ reason: "NOT_OBJECT" });
        }
        if (!("region_id" in input) || !exports.presence.isAqaraFp1RegionId(input.region_id)) {
            return exports.presence.failure({ reason: "INVALID_REGION_ID" });
        }
        return {
            isSuccess: true,
            payload: {
                command: {
                    region_id: input.region_id,
                },
            },
        };
    },
    parseAqaraFp1RegionUpsertInput: (input) => {
        if (!input || typeof input !== "object") {
            return exports.presence.failure({ reason: "NOT_OBJECT" });
        }
        if (!("region_id" in input) || !exports.presence.isAqaraFp1RegionId(input.region_id)) {
            return exports.presence.failure({ reason: "INVALID_REGION_ID" });
        }
        if (!("zones" in input) || !Array.isArray(input.zones) || !input.zones.length) {
            return exports.presence.failure({ reason: "ZONES_LIST_EMPTY" });
        }
        if (!input.zones.every(exports.presence.isAqaraFp1RegionZoneDefinition)) {
            return exports.presence.failure({ reason: "INVALID_ZONES" });
        }
        return {
            isSuccess: true,
            payload: {
                command: {
                    region_id: input.region_id,
                    zones: input.zones,
                },
            },
        };
    },
    // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
    isAqaraFp1RegionId: (value) => {
        return (typeof value === "number" &&
            value >= exports.presence.constants.region_config_regionId_min &&
            value <= exports.presence.constants.region_config_regionId_max);
    },
    // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
    isAqaraFp1RegionZoneDefinition: (value) => {
        return (value &&
            typeof value === "object" &&
            "x" in value &&
            "y" in value &&
            typeof value.x === "number" &&
            typeof value.y === "number" &&
            value.x >= exports.presence.constants.region_config_zoneX_min &&
            value.x <= exports.presence.constants.region_config_zoneX_max &&
            value.y >= exports.presence.constants.region_config_zoneY_min &&
            value.y <= exports.presence.constants.region_config_zoneY_max);
    },
    failure: (error) => {
        return {
            isSuccess: false,
            error,
        };
    },
};
function readTemperature(buffer, offset) {
    return buffer.readUint16BE(offset) / 100;
}
function writeTemperature(buffer, offset, temperature) {
    buffer.writeUInt16BE(temperature * 100, offset);
}
const dayNames = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
function readDaySelection(buffer, offset) {
    const selectedDays = [];
    dayNames.forEach((day, index) => {
        if ((buffer[offset] >> (index + 1)) % 2 !== 0) {
            selectedDays.push(day);
        }
    });
    return selectedDays;
}
function validateDaySelection(selectedDays) {
    // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
    selectedDays
        .filter((selectedDay) => !dayNames.includes(selectedDay))
        .forEach((invalidValue) => {
        throw new Error(`The value "${invalidValue}" is not a valid day (available values: ${dayNames.join(", ")})`);
    });
}
function writeDaySelection(buffer, offset, selectedDays) {
    validateDaySelection(selectedDays);
    const bitMap = dayNames.reduce((repeat, dayName, index) => {
        const isDaySelected = selectedDays.includes(dayName);
        // @ts-expect-error ignore
        return repeat | (isDaySelected << (index + 1));
    }, 0);
    buffer.writeUInt8(bitMap, offset);
}
const timeNextDayFlag = 1 << 15;
function readTime(buffer, offset) {
    const minutesWithDayFlag = buffer.readUint16BE(offset);
    return minutesWithDayFlag & ~timeNextDayFlag;
}
function validateTime(time) {
    const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
    if (!isPositiveInteger(time)) {
        throw new Error("Time must be a positive integer number");
    }
    if (time >= 24 * 60) {
        throw new Error("Time must be between 00:00 and 23:59");
    }
}
function writeTime(buffer, offset, time, isNextDay) {
    validateTime(time);
    let minutesWithDayFlag = time;
    if (isNextDay) {
        minutesWithDayFlag = minutesWithDayFlag | timeNextDayFlag;
    }
    buffer.writeUInt16BE(minutesWithDayFlag, offset);
}
/**
 * Formats a number of minutes into a user-readable 24-hour time notation in the form hh:mm.
 */
function formatTime(timeMinutes) {
    const hours = Math.floor(timeMinutes / 60);
    const minutes = timeMinutes % 60;
    return `${hours}:${String(minutes).padStart(2, "0")}`;
}
/**
 * Parses a 24-hour time notation string in the form hh:mm into a number of minutes.
 */
function parseTime(timeString) {
    const parts = timeString.split(":");
    if (parts.length !== 2) {
        throw new Error(`Cannot parse time string ${timeString}`);
    }
    const hours = Number.parseInt(parts[0]);
    const minutes = Number.parseInt(parts[1]);
    return hours * 60 + minutes;
}
const stringifiedScheduleFragmentSeparator = "|";
const stringifiedScheduleValueSeparator = ",";
exports.trv = {
    decodeFirmwareVersionString(value) {
        // Add prefix to follow Aqara's versioning schema: https://www.aqara.com/en/version/radiator-thermostat-e1
        const firmwareVersionPrefix = "0.0.0_";
        // Reinterpret from LE integer to byte sequence(e.g., `[25,8,0,0]` corresponds to 0.0.0_0825)
        const buffer = node_buffer_1.Buffer.alloc(4);
        buffer.writeUInt32LE(value);
        const firmwareVersionNumber = (0, utils_1.toNumber)(buffer.reverse().subarray(1).join(""), "firmwareVersionNumber").toString().padStart(4, "0");
        return firmwareVersionPrefix + firmwareVersionNumber;
    },
    decodePreset(value) {
        // Setup mode is the initial device state after powering it ("F11" on display) and not a real preset that can be deliberately
        // set by users, therefore it is exposed as a separate flag.
        return {
            setup: value === 3,
            preset: { 2: "away", 1: "auto", 0: "manual" }[value],
        };
    },
    decodeHeartbeat(meta, model, messageBuffer) {
        const data = (0, exports.buffer2DataObject)(model, messageBuffer);
        const payload = {};
        // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
        Object.entries(data).forEach(([key, value]) => {
            switch (Number.parseInt(key)) {
                case 3:
                    payload.device_temperature = value;
                    break;
                case 5:
                    (0, utils_1.assertNumber)(value);
                    payload.power_outage_count = value - 1;
                    break;
                case 10:
                    // unidentified number, e.g. 32274, 3847
                    break;
                case 13:
                    (0, utils_1.assertNumber)(value);
                    payload.firmware_version = exports.trv.decodeFirmwareVersionString(value);
                    break;
                case 17:
                    // unidentified flag/enum, e.g. 1
                    break;
                case 101:
                    (0, utils_1.assertNumber)(value);
                    Object.assign(payload, exports.trv.decodePreset(value));
                    break;
                case 102:
                    (0, utils_1.assertNumber)(value);
                    payload.local_temperature = value / 100;
                    break;
                case 103:
                    // This takes the following values:
                    //  - `occupied_heating_setpoint` if `system_mode` is `heat` and `preset` is `manual`
                    //  - `away_preset_temperature` if `system_mode` is `heat` and `preset` is `away`
                    //  - `5` if `system_mode` is `off`
                    // It thus behaves similar to `occupied_heating_setpoint` except in `off` mode. Due to this difference,
                    // this value is written to another property to avoid an inconsistency of the `occupied_heating_setpoint`.
                    // TODO How to handle this value? Find better name?
                    (0, utils_1.assertNumber)(value);
                    payload.internal_heating_setpoint = value / 100;
                    break;
                case 104:
                    payload.valve_alarm = value === 1;
                    break;
                case 105:
                    payload.battery = value;
                    break;
                case 106:
                    // unidentified flag/enum, e.g. 0
                    break;
            }
        });
        return payload;
    },
    /**
     * Decode a Zigbee schedule configuration message into a schedule configuration object.
     */
    decodeSchedule(buffer) {
        return {
            days: readDaySelection(buffer, 1),
            events: [
                { time: readTime(buffer, 2), temperature: readTemperature(buffer, 6) },
                { time: readTime(buffer, 8), temperature: readTemperature(buffer, 12) },
                { time: readTime(buffer, 14), temperature: readTemperature(buffer, 18) },
                { time: readTime(buffer, 20), temperature: readTemperature(buffer, 24) },
            ],
        };
    },
    validateSchedule(schedule) {
        const eventCount = 4;
        validateDaySelection(schedule.days);
        if (schedule.events.length !== eventCount) {
            throw new Error(`The schedule object must contain an array of ${eventCount} time/temperature events`);
        }
        // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
        schedule.events.forEach((event) => {
            validateTime(event.time);
            if (event.temperature < 5 || event.temperature > 30) {
                throw new Error("The temperature must be between 5 and 30 °C");
            }
        });
        // Calculate time durations between events
        const durations = schedule.events
            .map((entry, index, entries) => {
            if (index === 0) {
                return 0;
            }
            const time = entry.time;
            const fullDay = 24 * 60;
            const previousTime = entries[index - 1].time;
            const isNextDay = time < previousTime;
            if (isNextDay) {
                return fullDay - previousTime + time;
            }
            return time - previousTime;
        })
            // Remove first entry which is not a duration
            .slice(1);
        const minDuration = 60;
        const hasInvalidDurations = durations.some((duration) => duration < minDuration);
        if (hasInvalidDurations) {
            throw new Error("The individual times must be at least 1 hour apart");
        }
        const maxTotalDuration = 24 * 60;
        const totalDuration = durations.reduce((total, duration) => total + duration, 0);
        if (totalDuration > maxTotalDuration) {
            // this implicitly also makes sure that there is at most one "next day" switch
            throw new Error("The start and end times must be at most 24 hours apart");
        }
    },
    /**
     * Encodes a schedule object into Zigbee message format.
     */
    encodeSchedule(schedule) {
        const buffer = node_buffer_1.Buffer.alloc(26);
        buffer.writeUInt8(0x04);
        writeDaySelection(buffer, 1, schedule.days);
        schedule.events.forEach((event, index, events) => {
            const offset = 2 + index * 6;
            const isNextDay = index > 0 && event.time < events[index - 1].time;
            writeTime(buffer, offset, event.time, isNextDay);
            writeTemperature(buffer, offset + 4, event.temperature);
        });
        return buffer;
    },
    stringifySchedule(schedule) {
        const stringifiedScheduleFragments = [schedule.days.join(stringifiedScheduleValueSeparator)];
        for (const event of schedule.events) {
            const formattedTemperature = Number.isInteger(event.temperature)
                ? event.temperature.toFixed(1) // add ".0" for usability to signal that floats can be used
                : String(event.temperature);
            const entryFragments = [formatTime(event.time), formattedTemperature];
            stringifiedScheduleFragments.push(entryFragments.join(stringifiedScheduleValueSeparator));
        }
        return stringifiedScheduleFragments.join(stringifiedScheduleFragmentSeparator);
    },
    // Parses a schedule configuration string into a configuration object.
    parseSchedule(stringifiedSchedule) {
        const schedule = { days: [], events: [] };
        if (!stringifiedSchedule) {
            return schedule;
        }
        const stringifiedScheduleFragments = stringifiedSchedule.split(stringifiedScheduleFragmentSeparator);
        stringifiedScheduleFragments.forEach((fragment, index) => {
            if (index === 0) {
                // @ts-expect-error ignore
                schedule.days.push(...fragment.split(stringifiedScheduleValueSeparator));
            }
            else {
                const entryFragments = fragment.split(stringifiedScheduleValueSeparator);
                const entry = { time: parseTime(entryFragments[0]), temperature: Number.parseFloat(entryFragments[1]) };
                schedule.events.push(entry);
            }
        });
        return schedule;
    },
};
exports.manufacturerCode = 0x115f; // TODO: from Zcl
const manufacturerOptions = {
    lumi: { manufacturerCode: exports.manufacturerCode, disableDefaultResponse: true },
};
exports.lumiModernExtend = {
    lumiLight: (args) => {
        // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
        args = { powerOutageCount: true, deviceTemperature: true, ...args };
        const colorTemp = args.colorTemp ? { startup: false, range: args.colorTempRange ?? [153, 370] } : undefined;
        const result = modernExtend.light({ effect: false, powerOnBehavior: false, ...args, colorTemp });
        result.fromZigbee.push(exports.fromZigbee.lumi_bulb_interval, fz.ignore_occupancy_report, fz.ignore_humidity_report, fz.ignore_pressure_report, fz.ignore_temperature_report, exports.fromZigbee.lumi_specific);
        if (args.powerOutageCount)
            result.exposes.push(e.power_outage_count());
        if (args.deviceTemperature)
            result.exposes.push(e.device_temperature());
        if (args.powerOutageMemory === "switch") {
            result.toZigbee.push(exports.toZigbee.lumi_switch_power_outage_memory);
            result.exposes.push(e.power_outage_memory());
        }
        else if (args.powerOutageMemory === "light") {
            result.toZigbee.push(exports.toZigbee.lumi_light_power_outage_memory);
            result.exposes.push(e.power_outage_memory().withAccess(ea.STATE_SET));
        }
        else if (args.powerOutageMemory === "enum") {
            const extend = exports.lumiModernExtend.lumiPowerOnBehavior({ lookup: { on: 0, previous: 1, off: 2 } });
            result.toZigbee.push(...extend.toZigbee);
            result.exposes.push(...extend.exposes);
        }
        return result;
    },
    lumiOnOff: (args) => {
        // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
        args = { operationMode: false, lockRelay: false, ...args };
        const result = modernExtend.onOff({ powerOnBehavior: false, ...args });
        result.fromZigbee.push(exports.fromZigbee.lumi_specific);
        result.exposes.push(e.device_temperature(), e.power_outage_count());
        if (args.powerOutageMemory === "binary") {
            const extend = exports.lumiModernExtend.lumiPowerOutageMemory();
            result.toZigbee.push(...extend.toZigbee);
            result.exposes.push(...extend.exposes);
        }
        else if (args.powerOutageMemory === "enum") {
            const extend = exports.lumiModernExtend.lumiPowerOnBehavior();
            result.toZigbee.push(...extend.toZigbee);
            result.exposes.push(...extend.exposes);
        }
        if (args.operationMode === true) {
            const extend = exports.lumiModernExtend.lumiOperationMode({ description: "Decoupled mode for a button" });
            if (args.endpointNames) {
                // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
                args.endpointNames.forEach((ep) => {
                    const epExtend = exports.lumiModernExtend.lumiOperationMode({
                        description: `Decoupled mode for ${ep.toString()} button`,
                        endpointName: ep,
                    });
                    result.toZigbee.push(...epExtend.toZigbee);
                    result.exposes.push(...epExtend.exposes);
                });
            }
            else {
                result.toZigbee.push(...extend.toZigbee);
                result.exposes.push(...extend.exposes);
            }
        }
        if (args.lockRelay) {
            const extend = exports.lumiModernExtend.lumiLockRelay();
            if (args.endpointNames) {
                // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
                args.endpointNames.forEach((ep) => {
                    const epExtend = exports.lumiModernExtend.lumiLockRelay({
                        description: `Locks ${ep.toString()} relay and prevents it from operating`,
                        endpointName: ep,
                    });
                    result.toZigbee.push(...epExtend.toZigbee);
                    result.exposes.push(...epExtend.exposes);
                });
            }
            else {
                result.toZigbee.push(...extend.toZigbee);
                result.exposes.push(...extend.exposes);
            }
        }
        return result;
    },
    lumiSwitchType: (args) => modernExtend.enumLookup({
        name: "switch_type",
        lookup: { toggle: 1, momentary: 2, none: 3 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x000a, type: 0x20 },
        description: "External switch type",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiMotorSpeed: (args) => modernExtend.enumLookup({
        name: "motor_speed",
        lookup: { low: 0, medium: 1, high: 2 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0408, type: 0x20 },
        description: "Controls the motor speed",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiCurtainSpeed: (args) => modernExtend.numeric({
        name: "curtain_speed",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x043b, type: 0x20 },
        description: "Speed of curtain movement",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "ALL",
        unit: "%",
        valueMin: 1,
        valueMax: 100,
        entityCategory: "config",
        ...args,
    }),
    lumiCurtainManualOpenClose: (args) => modernExtend.binary({
        name: "manual_open_close",
        valueOn: ["ON", 1],
        valueOff: ["OFF", 0],
        cluster: "manuSpecificLumi",
        attribute: "curtainHandOpen",
        description: "Gently pull to open/close the curtain automatically",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "ALL",
        entityCategory: "config",
        ...args,
    }),
    lumiCurtainAdaptivePullingSpeed: (args) => modernExtend.binary({
        name: "adaptive_pulling_speed",
        valueOn: ["ON", 1],
        valueOff: ["OFF", 0],
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0442, type: 0x20 },
        description: "The faster/slower the curtain is pulled manually, the faster/slower the curtain will move",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "ALL",
        entityCategory: "config",
        ...args,
    }),
    lumiCurtainManualStop: (args) => modernExtend.binary({
        name: "manual_stop",
        valueOn: ["ON", 1],
        valueOff: ["OFF", 0],
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x043a, type: 0x10 },
        description: "Manually pulling the curtain during operation stops the motor",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "ALL",
        entityCategory: "config",
        ...args,
    }),
    lumiCurtainReverse: (args) => modernExtend.binary({
        name: "reverse_direction",
        valueOn: [true, 1],
        valueOff: [false, 0],
        cluster: "closuresWindowCovering",
        attribute: "windowCoveringMode",
        description: "Whether the curtain direction is inverted",
        access: "ALL",
        entityCategory: "config",
        ...args,
    }),
    lumiCurtainStatus: (args) => modernExtend.enumLookup({
        name: "status",
        lookup: { closing: 0, opening: 1, stopped: 2, blocked: 3 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0421, type: 0x20 },
        description: "Current status of the curtain (Opening, Closing, Stopped, Blocked)",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainLastManualOperation: (args) => modernExtend.enumLookup({
        name: "last_manual_operation",
        lookup: { open: 1, close: 2, stop: 3 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0425, type: 0x20 },
        description: "Last triggered manual operation",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainPosition: (args) => modernExtend.numeric({
        name: "curtain_position",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x041f, type: 0x20 },
        description: "Current position of the curtain",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        unit: "%",
        valueMin: 1,
        valueMax: 100,
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainTraverseTime: (args) => modernExtend.numeric({
        name: "traverse_time",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0403, type: 0x20 },
        description: "Time in seconds to get from one end to another",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        unit: "sec",
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainCalibrationStatus: (args) => modernExtend.enumLookup({
        name: "calibration_status",
        lookup: { not_calibrated: 0, half_calibrated: 1, fully_calibrated: 2 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0426, type: 0x20 },
        description: "Calibration status of the curtain (Not calibrated, Half calibrated, Fully calibrated)",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainCalibrated: (args) => modernExtend.binary({
        name: "calibrated",
        valueOn: [true, 1],
        valueOff: [false, 0],
        cluster: "manuSpecificLumi",
        attribute: "curtainCalibrated",
        description: "Indicates if this device is calibrated",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "STATE",
        entityCategory: "diagnostic",
        ...args,
    }),
    lumiCurtainIdentifyBeep: (args) => modernExtend.enumLookup({
        name: "identify_beep",
        lookup: { short: 0, "1_sec": 1, "2_sec": 2 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0404, type: 0x20 },
        description: "Device will beep for chosen time duration",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "ALL",
        entityCategory: "config",
        ...args,
    }),
    lumiPowerOnBehavior: (args) => modernExtend.enumLookup({
        name: "power_on_behavior",
        lookup: { on: 0, previous: 1, off: 2, inverted: 3 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0517, type: 0x20 },
        description: "Controls the behavior when the device is powered on after power loss",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiPowerOutageMemory: (args) => modernExtend.binary({
        name: "power_outage_memory",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0201, type: 0x10 },
        valueOn: [true, 1],
        valueOff: [false, 0],
        description: "Controls the behavior when the device is powered on after power loss",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiOperationMode: (args) => modernExtend.enumLookup({
        name: "operation_mode",
        lookup: { decoupled: 0, control_relay: 1 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0200, type: 0x20 },
        description: "Decoupled mode for relay",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiAction: (args) => modernExtend.actionEnumLookup({
        actionLookup: { single: 1 },
        cluster: "genMultistateInput",
        attribute: "presentValue",
        ...args,
    }),
    lumiVoc: (args) => modernExtend.numeric({
        name: "voc",
        cluster: "genAnalogInput",
        attribute: "presentValue",
        reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 },
        description: "Measured VOC value",
        unit: "ppb",
        access: "STATE_GET",
        ...args,
    }),
    lumiAirQuality: (args) => modernExtend.enumLookup({
        name: "air_quality",
        lookup: { excellent: 1, good: 2, moderate: 3, poor: 4, unhealthy: 5, unknown: 0 },
        cluster: "manuSpecificLumi",
        attribute: "airQuality",
        zigbeeCommandOptions: { disableDefaultResponse: true },
        description: "Measured air quality",
        access: "STATE_GET",
        ...args,
    }),
    lumiDisplayUnit: (args) => modernExtend.enumLookup({
        name: "display_unit",
        lookup: {
            mgm3_celsius: 0x00, // mg/m³, °C (default)
            ppb_celsius: 0x01, // ppb, °C
            mgm3_fahrenheit: 0x10, // mg/m³, °F
            ppb_fahrenheit: 0x11, // ppb, °F
        },
        cluster: "manuSpecificLumi",
        attribute: "displayUnit",
        zigbeeCommandOptions: { disableDefaultResponse: true },
        description: "Units to show on the display",
        entityCategory: "config",
        ...args,
    }),
    lumiOutageCountRestoreBindReporting: () => {
        const fromZigbee = [
            {
                cluster: "manuSpecificLumi",
                type: ["attributeReport", "readResponse"],
                convert: async (model, msg, publish, options, meta) => {
                    // At least the Aqara TVOC sensor does not send a deviceAnnounce after coming back online.
                    // The reconfigureReportingsOnDeviceAnnounce modernExtend is not usable because of this,
                    //  there is however an outage counter published in the 'special' buffer data reported
                    //  under the manuSpecificLumi cluster as attribute 247, we simple decode and grab value with ID 5.
                    // Normal attribute publishing and decoding will be left to the classic fromZigbee or modernExtends.
                    if (msg.data["247"] !== undefined) {
                        const dataDecoded = (0, exports.buffer2DataObject)(model, msg.data["247"]);
                        if (dataDecoded["5"] !== undefined) {
                            (0, utils_1.assertNumber)(dataDecoded["5"]);
                            const currentOutageCount = dataDecoded["5"] - 1;
                            const previousOutageCount = meta.device?.meta?.outageCount ? meta.device.meta.outageCount : 0;
                            if (currentOutageCount > previousOutageCount) {
                                logger_1.logger.debug(`Restoring binding and reporting, ${msg.device.ieeeAddr} came back after losing power.`, NS);
                                // update outageCount in database
                                meta.device.meta.outageCount = currentOutageCount;
                                meta.device.save();
                                // restore binding
                                for (const endpoint of meta.device.endpoints) {
                                    // restore bindings
                                    for (const b of endpoint.binds) {
                                        try {
                                            await endpoint.bind(b.cluster.name, b.target);
                                        }
                                        catch (error) {
                                            logger_1.logger.debug(`Failed to re-bind ${b.cluster.name} from ${b.target} for ${msg.device.ieeeAddr} (${error})`, NS);
                                        }
                                    }
                                    // restore reporting
                                    for (const c of endpoint.configuredReportings) {
                                        try {
                                            await endpoint.configureReporting(c.cluster.name, [
                                                {
                                                    attribute: c.attribute.name,
                                                    minimumReportInterval: c.minimumReportInterval,
                                                    maximumReportInterval: c.maximumReportInterval,
                                                    reportableChange: c.reportableChange,
                                                },
                                            ]);
                                        }
                                        catch {
                                            logger_1.logger.debug(`Failed to re-setup reporting of ${c.cluster.name}/${c.attribute.name} for ${msg.device.ieeeAddr}.`, NS);
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
            },
        ];
        return { fromZigbee, isModernExtend: true };
    },
    lumiZigbeeOTA: () => {
        // Many Lumi devices miss OTA on endpoint 1 even while supporting it.
        // https://github.com/Koenkk/zigbee2mqtt/issues/10660
        const result = modernExtend.quirkAddEndpointCluster({
            endpointID: 1,
            outputClusters: ["genOta"],
        });
        result.ota = true;
        return result;
    },
    lumiPower: (args) => modernExtend.numeric({
        name: "power",
        cluster: "genAnalogInput",
        attribute: "presentValue",
        reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 },
        description: "Instantaneous measured power",
        unit: "W",
        access: "STATE",
        entityCategory: "diagnostic",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiElectricityMeter: () => {
        const exposes = [e.energy(), e.voltage(), e.current()];
        const fromZigbee = [
            {
                cluster: "manuSpecificLumi",
                type: ["attributeReport", "readResponse"],
                convert: async (model, msg, publish, options, meta) => {
                    return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    lumiOverloadProtection: (args) => modernExtend.numeric({
        name: "overload_protection",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x020b, type: 0x39 },
        description: "Maximum allowed load, turns off if exceeded",
        valueMin: 100,
        valueMax: 3840,
        unit: "W",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiLedIndicator: (args) => modernExtend.binary({
        name: "led_indicator",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0203, type: 0x10 },
        valueOn: ["ON", 1],
        valueOff: ["OFF", 0],
        description: "LED indicator",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiLedDisabledNight: (args) => modernExtend.binary({
        name: "led_disabled_night",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0203, type: 0x10 },
        valueOn: [true, 1],
        valueOff: [false, 0],
        description: "Enables/disables LED indicator at night",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        reporting: false,
        ...args,
    }),
    lumiButtonLock: (args) => modernExtend.binary({
        name: "button_lock",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0200, type: 0x20 },
        valueOn: ["ON", 0],
        valueOff: ["OFF", 1],
        description: "Disables the physical switch button",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiFlipIndicatorLight: (args) => modernExtend.binary({
        name: "flip_indicator_light",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x00f0, type: 0x20 },
        valueOn: ["ON", 1],
        valueOff: ["OFF", 0],
        description: "After turn on, the indicator light turns on while switch is off, and vice versa",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        reporting: false,
        ...args,
    }),
    lumiPreventReset: () => {
        const onEvent = [
            async (type, data, device) => {
                if (
                // options.allow_reset ||
                type !== "message" ||
                    data.type !== "attributeReport" ||
                    data.cluster !== "genBasic" ||
                    !data.data[0xfff0] ||
                    // eg: [0xaa, 0x10, 0x05, 0x41, 0x87, 0x01, 0x01, 0x10, 0x00]
                    !data.data[0xfff0].slice(0, 5).equals(node_buffer_1.Buffer.from([0xaa, 0x10, 0x05, 0x41, 0x87]))) {
                    return;
                }
                const payload = {
                    [0xfff0]: {
                        value: [0xaa, 0x10, 0x05, 0x41, 0x47, 0x01, 0x01, 0x10, 0x01],
                        type: 0x41,
                    },
                };
                await device.getEndpoint(1).write("genBasic", payload, { manufacturerCode: exports.manufacturerCode });
            },
        ];
        return { onEvent, isModernExtend: true };
    },
    lumiClickMode: (args) => modernExtend.enumLookup({
        name: "click_mode",
        lookup: { fast: 1, multi: 2 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0125, type: 0x20 },
        description: "Click mode for wireless button. fast: only supports single click but allows faster response time." +
            "multi: supports multiple types of clicks but is slower, because it awaits multiple clicks.",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiSlider: () => {
        const fromZigbee = [
            {
                cluster: "manuSpecificLumi",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data[652] !== undefined) {
                        const actionLookup = {
                            1: "slider_single",
                            2: "slider_double",
                            3: "slider_hold",
                            4: "slider_up",
                            5: "slider_down",
                        };
                        return {
                            action_slide_time: msg.data[561],
                            action_slide_speed: msg.data[562],
                            action_slide_relative_displacement: msg.data[563],
                            action: actionLookup[msg.data[652]],
                            action_slide_time_delta: msg.data[769],
                        };
                    }
                },
            },
        ];
        const exposes = [
            e.numeric("action_slide_time", ea.STATE).withUnit("ms").withCategory("diagnostic"),
            e.numeric("action_slide_speed", ea.STATE).withUnit("mm/s").withCategory("diagnostic"),
            e.numeric("action_slide_relative_displacement", ea.STATE).withCategory("diagnostic"),
            e
                .numeric("action_slide_time_delta", ea.STATE)
                .withUnit("ms")
                .withCategory("diagnostic"),
            // action is exposed from extraActions inside lumiAction
        ];
        return { fromZigbee, exposes, isModernExtend: true };
    },
    lumiLockRelay: (args) => modernExtend.binary({
        name: "lock_relay",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0285, type: 0x20 },
        valueOn: [true, 1],
        valueOff: [false, 0],
        description: "Locks relay and prevents it from operating",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiSetEventMode: () => {
        // I have no idea, why it is used everywhere, even if not supported
        // modes:
        // 0 - 'command' mode. keys send commands. useful for binding
        // 1 - 'event' mode. keys send events. useful for handling
        const configure = [
            async (device, coordinatorEndpoint, definition) => {
                await device.getEndpoint(1).write("manuSpecificLumi", { mode: 1 }, { manufacturerCode: exports.manufacturerCode, disableResponse: true });
            },
        ];
        return { configure, isModernExtend: true };
    },
    lumiSwitchMode: (args) => modernExtend.enumLookup({
        name: "mode_switch",
        lookup: { quick_mode: 1, anti_flicker_mode: 4 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0004, type: 0x21 },
        description: "Anti flicker mode can be used to solve blinking issues of some lights. Quick mode makes the device respond faster.",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiVibration: () => {
        const exposes = [e.action(["shake", "triple_strike", "movement"])];
        const fromZigbee = [
            {
                cluster: "ssIasZone",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data[45] !== undefined) {
                        const zoneStatus = msg.data[45];
                        const actionLookup = { 1: "shake", 2: "triple_strike" };
                        return { action: actionLookup[zoneStatus] };
                    }
                },
            },
            {
                cluster: "genMultistateInput",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.presentValue !== undefined && msg.data.presentValue === 1) {
                        return { action: "triple_strike" };
                    }
                },
            },
            {
                cluster: "manuSpecificLumi",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data[0x0118] !== undefined && msg.data[0x0118] === 1) {
                        return { action: "movement" };
                    }
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    lumiSensitivityAdjustment: (args) => modernExtend.enumLookup({
        name: "sensitivity_adjustment",
        lookup: { high: 1, medium: 2, low: 3 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x010e, type: 0x20 },
        description: "Sensitivity adjustment for the device",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "SET",
        entityCategory: "config",
        ...args,
    }),
    lumiReportInterval: (args) => modernExtend.enumLookup({
        name: "report_interval",
        lookup: { "1s": 0x01, "5s": 0x02, "10s": 0x03 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0110, type: 0x20 },
        description: "Reporting interval for the device",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        access: "SET",
        entityCategory: "config",
        ...args,
    }),
    lumiMiscellaneous: (args) => {
        // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
        args = { cluster: "manuSpecificLumi", deviceTemperatureAttribute: 3, powerOutageCountAttribute: 5, resetsWhenPairing: false, ...args };
        const exposes = [e.device_temperature(), e.power_outage_count(args.resetsWhenPairing)];
        const fromZigbee = [
            {
                cluster: args.cluster,
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const payload = {};
                    if (msg.data[args.deviceTemperatureAttribute] !== undefined) {
                        const value = msg.data[args.deviceTemperatureAttribute];
                        (0, utils_1.assertNumber)(value);
                        payload.device_temperature = value;
                    }
                    if (msg.data[args.powerOutageCountAttribute] !== undefined) {
                        const value = msg.data[args.powerOutageCountAttribute];
                        (0, utils_1.assertNumber)(value);
                        payload.power_outage_count = value - 1;
                    }
                    return payload;
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    lumiKnobRotation: (args) => {
        const withButtonState = args?.withButtonState || true;
        const exposes = [
            e.action(["start_rotating", "rotation", "stop_rotating"]),
            e.numeric("action_rotation_angle", ea.STATE).withUnit("*").withDescription("Rotation angle").withCategory("diagnostic"),
            e.numeric("action_rotation_angle_speed", ea.STATE).withUnit("*").withDescription("Rotation angle speed").withCategory("diagnostic"),
            e.numeric("action_rotation_percent", ea.STATE).withUnit("%").withDescription("Rotation percent").withCategory("diagnostic"),
            e.numeric("action_rotation_percent_speed", ea.STATE).withUnit("%").withDescription("Rotation percent speed").withCategory("diagnostic"),
            e.numeric("action_rotation_time", ea.STATE).withUnit("ms").withDescription("Rotation time").withCategory("diagnostic"),
        ];
        if (withButtonState) {
            exposes.push(e
                .enum("action_rotation_button_state", ea.STATE, ["released", "pressed"])
                .withDescription("Button state during rotation")
                .withCategory("diagnostic"));
        }
        const fromZigbee = [
            {
                cluster: "manuSpecificLumi",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data[570] !== undefined) {
                        const act = { 1: "start_rotating", 2: "rotation", 3: "stop_rotating" };
                        const state = { 0: "released", 128: "pressed" };
                        return {
                            action: act[msg.data[570] & ~128],
                            action_rotation_button_state: state[msg.data[570] & 128],
                            action_rotation_angle: msg.data[558],
                            action_rotation_angle_speed: msg.data[560],
                            action_rotation_percent: msg.data[563],
                            action_rotation_percent_speed: msg.data[562],
                            action_rotation_time: msg.data[561],
                        };
                    }
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    lumiCommandMode: (args) => {
        // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
        args = { setEventMode: true, ...args };
        const exposes = [
            e
                .enum("operation_mode", ea.ALL, ["event", "command"])
                .withDescription("Command mode is useful for binding. Event mode is useful for processing."),
        ];
        const toZigbee = [
            {
                key: ["operation_mode"],
                convertSet: async (entity, key, value, meta) => {
                    (0, utils_1.assertString)(value);
                    // 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("manuSpecificLumi", { mode: (0, utils_1.getFromLookup)(value.toLowerCase(), lookup) }, { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
                    return { state: { operation_mode: value.toLowerCase() } };
                },
                convertGet: async (entity, key, meta) => {
                    const endpoint = meta.device.getEndpoint(1);
                    await endpoint.read("manuSpecificLumi", ["mode"], { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
                },
            },
        ];
        const result = { exposes, toZigbee, isModernExtend: true };
        if (args.setEventMode) {
            result.configure = exports.lumiModernExtend.lumiSetEventMode().configure;
        }
        return result;
    },
    lumiBattery: (args) => {
        // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
        args = {
            cluster: "manuSpecificLumi",
            percentageAtrribute: 1,
            voltageAttribute: 1,
            ...args,
        };
        const exposes = [e.battery(), e.battery_voltage()];
        const fromZigbee = [
            {
                cluster: args.cluster,
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const payload = {};
                    const lookup = numericAttributes2Lookup(model, msg.data);
                    if (lookup[args.percentageAtrribute.toString()]) {
                        const value = lookup[args.percentageAtrribute];
                        (0, utils_1.assertNumber)(value);
                        if (!args.voltageToPercentage)
                            payload.battery = value;
                    }
                    if (lookup[args.voltageAttribute.toString()]) {
                        const value = lookup[args.voltageAttribute];
                        (0, utils_1.assertNumber)(value);
                        payload.voltage = value;
                        if (args.voltageToPercentage)
                            payload.battery = (0, utils_1.batteryVoltageToPercentage)(value, args.voltageToPercentage);
                    }
                    return payload;
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    fp1ePresence: () => {
        const attribute = { ID: 0x0142, type: 0x20 };
        return modernExtend.binary({
            name: "presence",
            valueOn: [true, 1],
            valueOff: [false, 0],
            access: "STATE_GET",
            cluster: "manuSpecificLumi",
            attribute: attribute,
            description: "Indicates whether the device detected presence",
        });
    },
    fp1eMovement: () => modernExtend.enumLookup({
        name: "movement",
        lookup: { unknown_0: 0, unknown_1: 1, no_presence: 2, movement: 3, no_movement: 4 },
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0160, type: 0x20 },
        zigbeeCommandOptions: { disableDefaultResponse: true },
        description: "Is movement detected?",
        access: "STATE_GET",
    }),
    fp1eTargetDistance: () => modernExtend.numeric({
        name: "target_distance",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x015f, type: 0x23 },
        access: "STATE_GET",
        description: "Distance to the detected target",
        scale: 100,
        precision: 2,
        unit: "m",
    }),
    fp1eDetectionRange: () => modernExtend.numeric({
        name: "detection_range",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x015b, type: 0x23 },
        access: "ALL",
        description: "The device will monitor presence within the detection range",
        scale: 100,
        precision: 2,
        unit: "m",
        valueMin: 0,
        valueMax: 6.0,
        valueStep: 0.3,
    }),
    fp1eSpatialLearning: () => {
        return {
            isModernExtend: true,
            exposes: [e.enum("spatial_learning", ea.SET, ["Start Learning"]).withDescription("Initiate AI Spatial Learning.")],
            toZigbee: [
                {
                    key: ["spatial_learning"],
                    convertSet: async (entity, key, value, meta) => {
                        await entity.write("manuSpecificLumi", { 343: { value: 1, type: 0x20 } }, manufacturerOptions.lumi);
                    },
                },
            ],
        };
    },
    fp1eRestartDevice: () => {
        return {
            isModernExtend: true,
            exposes: [e.enum("restart_device", ea.SET, ["Restart Device"]).withDescription("Restarts the device.")],
            toZigbee: [
                {
                    key: ["restart_device"],
                    convertSet: async (entity, key, value, meta) => {
                        await entity.write("manuSpecificLumi", { 232: { value: 0x00, type: 0x10 } }, manufacturerOptions.lumi);
                    },
                },
            ],
        };
    },
    lumiMultiClick: (args) => modernExtend.binary({
        name: "multi_click",
        cluster: "manuSpecificLumi",
        attribute: { ID: 0x0286, type: 0x20 },
        valueOn: [true, 2],
        valueOff: [false, 1],
        description: "Enable multi-click mode for the switch, otherwise single click",
        access: "ALL",
        entityCategory: "config",
        zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
        ...args,
    }),
    lumiPreventLeave: () => {
        const onEvent = [
            async (type, data, device) => {
                if (type === "message" && data.type === "attributeReport" && data.cluster === "manuSpecificLumi" && data.data[0x00fc] === false) {
                    const payload = {
                        [0x00fc]: {
                            value: true,
                            type: 0x10,
                        },
                    };
                    await device.getEndpoint(1).write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
                }
            },
        ];
        return { onEvent, isModernExtend: true };
    },
    lumiExternalSensor: () => {
        return {
            isModernExtend: true,
            exposes: [
                e
                    .enum("sensor", ea.ALL, ["internal", "external"])
                    .withDescription("Select mode to display sensor: internal or both with external")
                    .withCategory("config"),
                e
                    .numeric("external_temperature", ea.STATE_SET)
                    .withUnit("°C")
                    .withValueMin(-100)
                    .withValueMax(100)
                    .withValueStep(0.1)
                    .withDescription("Value for external temperature sensor")
                    .withCategory("config"),
                e
                    .numeric("external_humidity", ea.STATE_SET)
                    .withUnit("%")
                    .withValueMin(0)
                    .withValueMax(100)
                    .withDescription("Value for external humidity sensor")
                    .withCategory("config"),
            ],
            toZigbee: [
                {
                    key: ["sensor", "external_temperature", "external_humidity"],
                    convertSet: async (entity, key, value, meta) => {
                        const lumiHeader = (counter, length, action) => {
                            const header = [0xaa, 0x71, length + 3, 0x44, counter];
                            const integrity = 512 - header.reduce((sum, elem) => sum + elem, 0);
                            return [...header, integrity, action, 0x41, length];
                        };
                        const fictiveSensor = node_buffer_1.Buffer.from("00158d00019d1b98", "hex");
                        switch (key) {
                            case "sensor": {
                                (0, utils_1.assertEndpoint)(entity);
                                const device = node_buffer_1.Buffer.from(entity.deviceIeeeAddress.substring(2), "hex");
                                const timestamp = node_buffer_1.Buffer.alloc(4);
                                timestamp.writeUInt32BE(Date.now() / 1000);
                                if (value === "external") {
                                    const params1 = [
                                        ...timestamp,
                                        0x15,
                                        ...device,
                                        ...fictiveSensor,
                                        0x00,
                                        0x02,
                                        0x00,
                                        0x55,
                                        0x15,
                                        0x0a,
                                        0x01,
                                        0x00,
                                        0x00,
                                        0x01,
                                        0x06,
                                        0xe6,
                                        0xb9,
                                        0xbf,
                                        0xe5,
                                        0xba,
                                        0xa6,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x01,
                                        0x02,
                                        0x08,
                                        0x65,
                                    ];
                                    const params2 = [
                                        ...timestamp,
                                        0x14,
                                        ...device,
                                        ...fictiveSensor,
                                        0x00,
                                        0x01,
                                        0x00,
                                        0x55,
                                        0x15,
                                        0x0a,
                                        0x01,
                                        0x00,
                                        0x00,
                                        0x01,
                                        0x06,
                                        0xe6,
                                        0xb8,
                                        0xa9,
                                        0xe5,
                                        0xba,
                                        0xa6,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x01,
                                        0x02,
                                        0x07,
                                        0x63,
                                    ];
                                    const val1 = [...lumiHeader(0x12, params1.length, 0x02), ...params1];
                                    const val2 = [...lumiHeader(0x13, params2.length, 0x02), ...params2];
                                    await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                    await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                }
                                else if (value === "internal") {
                                    const params1 = [
                                        ...timestamp,
                                        0x15,
                                        ...device,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                    ];
                                    const params2 = [
                                        ...timestamp,
                                        0x14,
                                        ...device,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                        0x00,
                                    ];
                                    const val1 = [...lumiHeader(0x12, params1.length, 0x04), ...params1];
                                    const val2 = [...lumiHeader(0x13, params2.length, 0x04), ...params2];
                                    await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                    await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                }
                                await entity.read("manuSpecificLumi", [0x172], { manufacturerCode: exports.manufacturerCode });
                                break;
                            }
                            case "external_temperature":
                                if (meta.state.sensor === "external") {
                                    const temperatureBuf = node_buffer_1.Buffer.alloc(4);
                                    const number = (0, utils_1.toNumber)(value);
                                    temperatureBuf.writeFloatBE(Math.round(number * 100));
                                    const params = [...fictiveSensor, 0x00, 0x01, 0x00, 0x55, ...temperatureBuf];
                                    const data = [...lumiHeader(0x12, params.length, 0x05), ...params];
                                    await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                    return { state: { external_temperature: value } };
                                }
                                break;
                            case "external_humidity":
                                if (meta.state.sensor === "external") {
                                    const humidityBuf = node_buffer_1.Buffer.alloc(4);
                                    const number = (0, utils_1.toNumber)(value);
                                    humidityBuf.writeFloatBE(Math.round(number * 100));
                                    const params = [...fictiveSensor, 0x00, 0x02, 0x00, 0x55, ...humidityBuf];
                                    const data = [...lumiHeader(0x12, params.length, 0x05), ...params];
                                    await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                                    return { state: { external_humidity: value } };
                                }
                                break;
                            default: // Unknown key
                                logger_1.logger.debug(`Unhandled key ${key}`, "zhc:lumi:externalSensor");
                        }
                    },
                    convertGet: async (entity, key, meta) => {
                        await entity.read("manuSpecificLumi", [0x172], { manufacturerCode: exports.manufacturerCode });
                    },
                },
            ],
            fromZigbee: [
                {
                    cluster: "manuSpecificLumi",
                    type: ["attributeReport", "readResponse"],
                    convert: (model, msg, publish, options, meta) => {
                        const result = {};
                        // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
                        Object.entries(msg.data).forEach(([key, value]) => {
                            switch (Number.parseInt(key)) {
                                case 0x0172:
                                    result.sensor = (0, utils_1.getFromLookup)(value, { 2: "external", 0: "internal", 1: "internal", 3: "external" });
                                    break;
                                case 0xfff2:
                                    logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:externalSensor");
                                    break;
                                default:
                                    logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:externalSensor");
                            }
                        });
                        return result;
                    },
                },
            ],
        };
    },
};
exports.modernExtend = exports.lumiModernExtend;
const feederDaysLookup = {
    127: "everyday",
    31: "workdays",
    96: "weekend",
    1: "mon",
    2: "tue",
    4: "wed",
    8: "thu",
    16: "fri",
    32: "sat",
    64: "sun",
    85: "mon-wed-fri-sun",
    42: "tue-thu-sat",
};
exports.fromZigbee = {
    // lumi generic
    lumi_basic: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: async (model, msg, publish, options, meta) => {
            return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
        },
    },
    lumi_basic_raw: {
        cluster: "genBasic",
        type: ["raw"],
        convert: async (model, msg, publish, options, meta) => {
            let payload = {};
            if (node_buffer_1.Buffer.isBuffer(msg.data)) {
                const dataObject = (0, exports.buffer2DataObject)(model, msg.data);
                payload = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, dataObject);
            }
            return payload;
        },
    },
    lumi_specific: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: async (model, msg, publish, options, meta) => {
            return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
        },
    },
    lumi_co2: {
        cluster: "msCO2",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            return { co2: Math.floor(msg.data.measuredValue) };
        },
    },
    lumi_pm25: {
        cluster: "pm25Measurement",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.measuredValue) {
                return { pm25: msg.data.measuredValue };
            }
        },
    },
    lumi_contact: {
        cluster: "genOnOff",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            return { contact: msg.data.onOff === 0 };
        },
    },
    lumi_power: {
        cluster: "genAnalogInput",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            return { power: msg.data.presentValue };
        },
    },
    lumi_action: {
        cluster: "genOnOff",
        type: ["attributeReport"],
        convert: (model, msg, publish, options, meta) => {
            if (["QBKG04LM", "QBKG11LM", "QBKG21LM", "QBKG03LM", "QBKG12LM", "QBKG22LM"].includes(model.model) && msg.data["61440"]) {
                return;
            }
            if (model.model === "WXKG11LM") {
                // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
                let clicks;
                if (msg.data.onOff) {
                    clicks = 1;
                }
                else if (msg.data["32768"]) {
                    clicks = msg.data["32768"];
                }
                const actionLookup = { 1: "single", 2: "double", 3: "triple", 4: "quadruple" };
                if (actionLookup[clicks]) {
                    return { action: actionLookup[clicks] };
                }
                return;
            }
            if (["QBKG21LM", "QBKG04LM"].includes(model.model) && msg.endpoint.ID !== 4)
                return;
            let mapping = null;
            if (["QBKG03LM", "QBKG12LM", "QBKG22LM"].includes(model.model))
                mapping = { 4: "left", 5: "right", 6: "both" };
            if (["WXKG02LM_rev1", "WXKG02LM_rev2", "WXKG07LM"].includes(model.model))
                mapping = { 1: "left", 2: "right", 3: "both" };
            // Maybe other QKBG also support release/hold?
            const actionLookup = ["QBKG03LM", "QBKG22LM", "QBKG04LM", "QBKG21LM"].includes(model.model)
                ? { 0: "hold", 1: "release", 2: "double" }
                : { 0: "single", 1: "single" };
            const action = actionLookup[msg.data.onOff];
            const button = mapping?.[msg.endpoint.ID] ? `_${mapping[msg.endpoint.ID]}` : "";
            if (action === "release") {
                const anotherAction = globalStore.getValue(msg.endpoint, "hold", false) ? "hold_release" : "single";
                publish({ action: `${anotherAction}${button}` });
            }
            globalStore.putValue(msg.endpoint, "hold", action === "hold");
            return { action: `${action}${button}` };
        },
    },
    lumi_action_multistate: {
        cluster: "genMultistateInput",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            // cubes
            if (model.model === "MFKZQ01LM") {
                /*
                Source: https://github.com/kirovilya/ioBroker.zigbee
                    +---+
                    | 2 |
                +---+---+---+
                | 4 | 0 | 1 |
                +---+---+---+
                    |M5I|
                    +---+
                    | 3 |
                    +---+
                Side 5 is with the MI logo, side 3 contains the battery door.
                presentValue = 0 = shake
                presentValue = 2 = wakeup
                presentValue = 3 = fly/fall
                presentValue = y + x * 8 + 64 = 90º Flip from side x on top to side y on top
                presentValue = x + 128 = 180º flip to side x on top
                presentValue = x + 256 = push/slide cube while side x is on top
                presentValue = x + 512 = double tap while side x is on top
                */
                const value = msg.data.presentValue;
                let result = null;
                if (value === 0)
                    result = { action: "shake" };
                else if (value === 1)
                    result = { action: "throw" };
                else if (value === 2)
                    result = { action: "wakeup" };
                else if (value === 3)
                    result = { action: "fall" };
                else if (value >= 512)
                    result = { action: "tap", side: value - 512 };
                else if (value >= 256)
                    result = { action: "slide", side: value - 256 };
                else if (value >= 128)
                    result = { action: "flip180", side: value - 128 };
                else if (value >= 64) {
                    result = {
                        action: "flip90",
                        action_from_side: Math.floor((value - 64) / 8),
                        action_to_side: value % 8,
                        action_side: value % 8,
                        from_side: Math.floor((value - 64) / 8),
                        to_side: value % 8,
                        side: value % 8,
                    };
                }
                return result ? result : null;
            }
            if (model.model === "CTP-R01") {
                const value = msg.data.presentValue;
                // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
                let payload;
                if (value === 0)
                    payload = { action: "shake" };
                else if (value === 1)
                    payload = { action: "throw" };
                else if (value === 2)
                    payload = { action: "1_min_inactivity" };
                else if (value === 4)
                    payload = { action: "hold" };
                else if (value >= 1024)
                    payload = { action: "flip_to_side", side: value - 1023 };
                else if (value >= 512)
                    payload = { action: "tap", side: value - 511 };
                else if (value >= 256)
                    payload = { action: "slide", side: value - 255 };
                else if (value >= 128) {
                    payload = {
                        action: "flip180",
                        side: value - 127,
                        action_from_side: 7 - value + 127,
                    };
                }
                else if (value >= 64) {
                    payload = {
                        action: "flip90",
                        side: (value % 8) + 1,
                        action_from_side: Math.floor((value - 64) / 8) + 1,
                    };
                }
                else {
                    logger_1.logger.debug(`${model.model}: unknown action with value ${value}`, NS);
                }
                return payload;
            }
            let actionLookup = { 0: "hold", 1: "single", 2: "double", 3: "triple", 255: "release" };
            // mini switches and opple
            if (model.model === "WXKG12LM") {
                actionLookup = { ...actionLookup, 16: "hold", 17: "release", 18: "shake" };
            }
            if (["WXKG13LM", "WXKG04LM", "WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM"].includes(model.model)) {
                actionLookup = { ...actionLookup, 5: "quintuple", 6: "many" };
            }
            // wall switches
            let buttonLookup = null;
            if (["WXKG02LM_rev2", "WXKG07LM", "WXKG15LM", "WXKG17LM", "WXKG22LM"].includes(model.model)) {
                buttonLookup = { 1: "left", 2: "right", 3: "both" };
            }
            if (["QBKG12LM", "QBKG24LM"].includes(model.model))
                buttonLookup = { 5: "left", 6: "right", 7: "both" };
            if (["QBKG39LM", "QBKG41LM", "WS-EUK02", "WS-EUK04", "QBKG18LM", "QBKG20LM", "QBKG28LM", "QBKG31LM", "ZNQBKG25LM"].includes(model.model)) {
                buttonLookup = { 41: "left", 42: "right", 51: "both" };
            }
            if (["QBKG25LM", "QBKG26LM", "QBKG29LM", "QBKG32LM", "QBKG33LM", "QBKG34LM", "ZNQBKG31LM", "ZNQBKG26LM"].includes(model.model)) {
                buttonLookup = {
                    41: "left",
                    42: "center",
                    43: "right",
                    51: "left_center",
                    52: "left_right",
                    53: "center_right",
                    61: "all",
                };
            }
            // Z1 switches, ZNQBKG38LM only 1 button, so not add buttonLookup
            if (["ZNQBKG39LM"].includes(model.model)) {
                buttonLookup = { 1: "top", 2: "bottom" };
            }
            if (["ZNQBKG40LM"].includes(model.model)) {
                buttonLookup = { 1: "top", 2: "center", 3: "bottom" };
            }
            if (["ZNQBKG41LM"].includes(model.model)) {
                buttonLookup = { 1: "top", 2: "center", 3: "bottom", 4: "wireless" };
            }
            if (["WS-USC02", "WS-USC04"].includes(model.model)) {
                buttonLookup = { 41: "top", 42: "bottom", 51: "both" };
            }
            const action = actionLookup[msg.data.presentValue];
            if (["WXKG04LM", "WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM"].includes(model.model)) {
                clearTimeout(globalStore.getValue(msg.endpoint, "timer"));
                // 0 = hold
                const button = msg.endpoint.ID;
                if (msg.data.presentValue === 0) {
                    // Aqara Opple does not generate a release event when pressed for more than 5 seconds
                    // After 5 seconds of not releasing we assume release.
                    const timer = setTimeout(() => publish({ action: `button_${button}_release` }), 5000);
                    globalStore.putValue(msg.endpoint, "timer", timer);
                }
                return { action: `button_${button}_${action}` };
            }
            if (buttonLookup) {
                const button = buttonLookup[msg.endpoint.ID];
                if (button) {
                    return { action: `${action}_${button}` };
                }
            }
            else {
                return { action };
            }
        },
    },
    lumi_action_analog: {
        cluster: "genAnalogInput",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (model.model === "MFKZQ01LM") {
                /*
                Source: https://github.com/kirovilya/ioBroker.zigbee
                presentValue = rotation angle left < 0, right > 0
                */
                const value = msg.data.presentValue;
                const result = {
                    action: value < 0 ? "rotate_left" : "rotate_right",
                    angle: Math.floor(value * 100) / 100,
                    action_angle: Math.floor(value * 100) / 100,
                };
                return result;
            }
            if (model.model === "CTP-R01") {
                const value = msg.data.presentValue;
                return {
                    action: value < 0 ? "rotate_left" : "rotate_right",
                    action_angle: Math.floor(value * 100) / 100,
                };
            }
        },
    },
    lumi_temperature: {
        cluster: "msTemperatureMeasurement",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const temperature = Number.parseFloat(msg.data.measuredValue) / 100.0;
            // https://github.com/Koenkk/zigbee2mqtt/issues/798
            // Sometimes the sensor publishes non-realistic vales.
            if (temperature > -65 && temperature < 65) {
                return { temperature };
            }
        },
    },
    lumi_pressure: {
        cluster: "msPressureMeasurement",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const result = fz.pressure.convert(model, msg, publish, options, meta);
            if (result && result.pressure > 500 && result.pressure < 2000) {
                return result;
            }
        },
    },
    // lumi class specific
    lumi_feeder: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
            Object.entries(msg.data).forEach(([key, value]) => {
                switch (Number.parseInt(key)) {
                    case 0xfff1: {
                        // @ts-expect-error ignore
                        if (value.length < 8) {
                            logger_1.logger.debug(`Cannot handle ${value}, frame too small`, "zhc:lumi:feeder");
                            return;
                        }
                        // @ts-expect-error ignore
                        const attr = value.slice(3, 7);
                        // @ts-expect-error ignore
                        const len = value.slice(7, 8).readUInt8();
                        // @ts-expect-error ignore
                        const val = value.slice(8, 8 + len);
                        switch (attr.readInt32BE()) {
                            case 0x04150055: // feeding
                                result.feed = "";
                                break;
                            case 0x041502bc: {
                                // feeding report
                                const report = val.toString();
                                result.feeding_source = { 0: "schedule", 1: "manual", 2: "remote" }[Number.parseInt(report.slice(0, 2))];
                                result.feeding_size = Number.parseInt(report.slice(3, 4));
                                break;
                            }
                            case 0x0d680055: // portions per day
                                result.portions_per_day = val.readUInt16BE();
                                break;
                            case 0x0d690055: // weight per day
                                result.weight_per_day = val.readUInt32BE();
                                break;
                            case 0x0d0b0055: // error ?
                                result.error = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: true, 0: false });
                                break;
                            case 0x080008c8: {
                                // schedule string
                                const schlist = val.toString().split(",");
                                const schedule = [];
                                // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
                                schlist.forEach((str) => {
                                    // 7f13000100
                                    if (str !== "//") {
                                        const feedtime = node_buffer_1.Buffer.from(str, "hex");
                                        schedule.push({
                                            days: (0, utils_1.getFromLookup)(feedtime[0], feederDaysLookup),
                                            hour: feedtime[1],
                                            minute: feedtime[2],
                                            size: feedtime[3],
                                        });
                                    }
                                });
                                result.schedule = schedule;
                                break;
                            }
                            case 0x04170055: // indicator
                                result.led_indicator = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "ON", 0: "OFF" });
                                break;
                            case 0x04160055: // child lock
                                result.child_lock = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "LOCK", 0: "UNLOCK" });
                                break;
                            case 0x04180055: // mode
                                result.mode = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "schedule", 0: "manual" });
                                break;
                            case 0x0e5c0055: // serving size
                                result.serving_size = val.readUInt8();
                                break;
                            case 0x0e5f0055: // portion weight
                                result.portion_weight = val.readUInt8();
                                break;
                            case 0x080007d1: // ? 64
                            case 0x0d090055: // ? 00
                                logger_1.logger.debug(`Unhandled attribute ${attr} = ${val}`, "zhc:lumi:feeder");
                                break;
                            default:
                                logger_1.logger.debug(`Unknown attribute ${attr} = ${val}`, "zhc:lumi:feeder");
                        }
                        break;
                    }
                    case 0x00ff: // 80:13:58:91:24:33:20:24:58:53:44:07:05:97:75:17
                    case 0x0007: // 00:00:00:00:1d:b5:a6:ed
                    case 0x00f7: // 05:21:14:00:0d:23:21:25:00:00:09:21:00:01
                        logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:feeder");
                        break;
                    default:
                        logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:feeder");
                }
            });
            return result;
        },
    },
    lumi_trv: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
            Object.entries(msg.data).forEach(([key, value]) => {
                switch (Number.parseInt(key)) {
                    case 0x0271:
                        result.system_mode = (0, utils_1.getFromLookup)(value, { 1: "heat", 0: "off" });
                        break;
                    case 0x0272:
                        // @ts-expect-error ignore
                        Object.assign(result, exports.trv.decodePreset(value));
                        break;
                    case 0x0273:
                        result.window_detection = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
                        break;
                    case 0x0274:
                        result.valve_detection = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
                        break;
                    case 0x0277:
                        result.child_lock = (0, utils_1.getFromLookup)(value, { 1: "LOCK", 0: "UNLOCK" });
                        break;
                    case 0x0279:
                        (0, utils_1.assertNumber)(value);
                        result.away_preset_temperature = (value / 100).toFixed(1);
                        break;
                    case 0x027b:
                        result.calibrated = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
                        break;
                    case 0x027e:
                        result.sensor = (0, utils_1.getFromLookup)(value, { 1: "external", 0: "internal", 2: "external" });
                        break;
                    case 0x040a:
                        result.battery = value;
                        break;
                    case 0x027a:
                        result.window_open = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
                        break;
                    case 0x0275:
                        result.valve_alarm = (0, utils_1.getFromLookup)(value, { 1: true, 0: false, 4: true });
                        break;
                    case 247: {
                        // @ts-expect-error ignore
                        const heartbeat = exports.trv.decodeHeartbeat(meta, model, value);
                        logger_1.logger.debug(`${model.model}: Processed heartbeat message into payload ${JSON.stringify(heartbeat)}`, "zhc:lumi:trv");
                        if (heartbeat.firmware_version) {
                            // Overwrite the "placeholder" version `0.0.0_0025` advertised by `genBasic`
                            // with the correct version from the heartbeat.
                            // This is not reflected in the frontend unless the device is reconfigured
                            // or the whole service restarted.
                            // See https://github.com/Koenkk/zigbee-herdsman-converters/pull/5363#discussion_r1081477047
                            // @ts-expect-error ignore
                            meta.device.softwareBuildID = heartbeat.firmware_version;
                            // biome-ignore lint/performance/noDelete: ignored using `--suppress`
                            delete heartbeat.firmware_version;
                        }
                        Object.assign(result, heartbeat);
                        break;
                    }
                    case 0x027d:
                        result.schedule = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
                        break;
                    case 0x0276: {
                        const buffer = value;
                        // Buffer is empty first message after pairing
                        // https://github.com/Koenkk/zigbee-herdsman-converters/issues/7128
                        if (buffer.length) {
                            const schedule = exports.trv.decodeSchedule(buffer);
                            result.schedule_settings = exports.trv.stringifySchedule(schedule);
                        }
                        break;
                    }
                    case 0x00ee: {
                        meta.device.meta.lumiFileVersion = value;
                        meta.device.save();
                        break;
                    }
                    case 0xfff2:
                    case 0x00ff: // 4e:27:49:bb:24:b6:30:dd:74:de:53:76:89:44:c4:81
                    case 0x027c: // 0x00
                    case 0x0280: // 0x00/0x01
                        logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:trv");
                        break;
                    default:
                        logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:trv");
                }
            });
            return result;
        },
    },
    lumi_presence_region_events: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const payload = {};
            // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
            Object.entries(msg.data).forEach(([key, value]) => {
                const eventKey = Number.parseInt(key);
                switch (eventKey) {
                    case exports.presence.constants.region_event_key: {
                        if (!node_buffer_1.Buffer.isBuffer(value) ||
                            !(typeof value[0] === "string" || typeof value[0] === "number") ||
                            !(typeof value[1] === "string" || typeof value[1] === "number")) {
                            logger_1.logger.warning(`Action: Unrecognized payload structure '${JSON.stringify(value)}'`, NS);
                            break;
                        }
                        const [regionIdRaw, eventTypeCodeRaw] = value;
                        // @ts-expect-error ignore
                        const regionId = Number.parseInt(regionIdRaw, 10);
                        // @ts-expect-error ignore
                        const eventTypeCode = Number.parseInt(eventTypeCodeRaw, 10);
                        if (Number.isNaN(regionId)) {
                            logger_1.logger.warning(`Action: Invalid regionId "${regionIdRaw}"`, NS);
                            break;
                        }
                        if (!Object.values(exports.presence.constants.region_event_types).includes(eventTypeCode)) {
                            logger_1.logger.warning(`Action: Unknown region event type "${eventTypeCode}"`, NS);
                            break;
                        }
                        const eventTypeName = exports.presence.mappers.lumi_presence.region_event_type_names[eventTypeCode];
                        logger_1.logger.debug(`Action: Triggered event (region "${regionId}", type "${eventTypeName}")`, NS);
                        payload.action = `region_${regionId}_${eventTypeName}`;
                        break;
                    }
                }
            });
            return payload;
        },
    },
    lumi_lock_report: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data["65328"]) {
                const data = `0x${msg.data["65328"].toString(16)}`;
                const state = Number(data.substring(2, 4));
                const action = Number(data.substring(4, 6));
                const keynum = Number(data.substring(6, 8));
                if (state === 11) {
                    if (action === 1) {
                        // unknown key
                        return { keyerror: true, inserted: "unknown" };
                    }
                    if (action === 3) {
                        // explicitly disabled key (i.e. reported lost)
                        return { keyerror: true, inserted: keynum };
                    }
                    if (action === 7) {
                        // strange object introduced into the cylinder (e.g. a lock pick)
                        return { keyerror: true, inserted: "strange" };
                    }
                }
                if (state === 12) {
                    if (action === 1) {
                        return { inserted: keynum };
                    }
                    if (action === 11) {
                        return { forgotten: keynum };
                    }
                }
            }
        },
    },
    lumi_occupancy_illuminance: {
        // This is for occupancy sensor that only send a message when motion detected,
        // but do not send a motion stop.
        // Therefore we need to publish the no_motion detected by ourselves.
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        options: [exposes.options.occupancy_timeout_2(), exposes.options.no_occupancy_since_true()],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.illuminance !== undefined) {
                // The occupancy sensor only sends a message when motion detected.
                // Therefore we need to publish the no_motion detected by ourselves.
                let timeout = meta && meta.state && meta.state.detection_interval !== undefined
                    ? Number(meta.state.detection_interval)
                    : ["RTCGQ14LM"].includes(model.model)
                        ? 30
                        : 60;
                timeout =
                    options?.occupancy_timeout != null && Number(options.occupancy_timeout) >= timeout
                        ? Number(options.occupancy_timeout)
                        : timeout + 2;
                // Stop existing timers because motion is detected and set a new one.
                clearTimeout(globalStore.getValue(msg.endpoint, "occupancy_timer", null));
                if (timeout !== 0) {
                    const timer = setTimeout(() => {
                        publish({ occupancy: false });
                    }, timeout * 1000);
                    globalStore.putValue(msg.endpoint, "occupancy_timer", timer);
                }
                // Sometimes RTCGQ14LM reports high illuminance values in the dark
                // https://github.com/Koenkk/zigbee2mqtt/issues/12596
                const illuminance = msg.data.illuminance > 130536 ? 0 : msg.data.illuminance - 65536;
                const payload = { occupancy: true, illuminance };
                (0, utils_1.noOccupancySince)(msg.endpoint, options, publish, "start");
                return payload;
            }
        },
    },
    lumi_curtain_position: {
        cluster: "genAnalogOutput",
        type: ["attributeReport", "readResponse"],
        options: [exposes.options.invert_cover()],
        convert: (model, msg, publish, options, meta) => {
            if (model.model === "ZNCLDJ12LM" && msg.type === "attributeReport" && [0, 2].includes(msg.data.presentValue)) {
                // Incorrect reports from the device, ignore (re-read by onEvent of ZNCLDJ12LM)
                // https://github.com/Koenkk/zigbee-herdsman-converters/pull/1427#issuecomment-663862724
                return;
            }
            let position = (0, utils_1.precisionRound)(msg.data.presentValue, 2);
            position = options.invert_cover ? 100 - position : position;
            const closed = options.invert_cover ? position === 100 : position === 0;
            return { position, state: closed ? "CLOSE" : "OPEN" };
        },
    },
    lumi_curtain_position_tilt: {
        cluster: "closuresWindowCovering",
        type: ["attributeReport", "readResponse"],
        options: [exposes.options.invert_cover()],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const invert = model.meta?.coverInverted ? !options.invert_cover : options.invert_cover;
            if (msg.data.currentPositionLiftPercentage !== undefined && msg.data.currentPositionLiftPercentage <= 100) {
                const value = msg.data.currentPositionLiftPercentage;
                const position = invert ? 100 - value : value;
                const state = invert ? (position > 0 ? "CLOSE" : "OPEN") : position > 0 ? "OPEN" : "CLOSE";
                result[(0, utils_1.postfixWithEndpointName)("position", msg, model, meta)] = position;
                result[(0, utils_1.postfixWithEndpointName)("state", msg, model, meta)] = state;
            }
            if (msg.data.currentPositionTiltPercentage !== undefined && msg.data.currentPositionTiltPercentage <= 100) {
                const value = msg.data.currentPositionTiltPercentage;
                result[(0, utils_1.postfixWithEndpointName)("tilt", msg, model, meta)] = invert ? 100 - value : value;
            }
            return result;
        },
    },
    lumi_operation_mode_basic: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const payload = {};
            if (model.meta && !model.meta.multiEndpoint) {
                const mappingMode = { 18: "control_relay", 254: "decoupled" };
                const key = 0xff22;
                if (msg.data[key] !== undefined) {
                    payload.operation_mode = mappingMode[msg.data[key]];
                }
            }
            else {
                const mappingButton = { 65314: "left", 65315: "right" };
                const mappingMode = { 18: "control_left_relay", 34: "control_right_relay", 254: "decoupled" };
                for (const key in mappingButton) {
                    if (msg.data[key] !== undefined) {
                        payload[`operation_mode_${mappingButton[key]}`] = mappingMode[msg.data[key]];
                    }
                }
            }
            return payload;
        },
    },
    lumi_bulb_interval: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data["65281"]) {
                const data = msg.data["65281"];
                return {
                    state: data["100"] === 1 ? "ON" : "OFF",
                    brightness: data["101"],
                    color_temp: data["102"],
                };
            }
        },
    },
    lumi_on_off: {
        cluster: "genOnOff",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            // Lumi wall switches use endpoint 4, 5 or 6 to indicate an action on the button so we have to skip that.
            if (msg.data.onOff !== undefined && ![4, 5, 6].includes(msg.endpoint.ID)) {
                const property = (0, utils_1.postfixWithEndpointName)("state", msg, model, meta);
                return { [property]: msg.data.onOff === 1 ? "ON" : "OFF" };
            }
        },
    },
    lumi_curtain_status: {
        cluster: "genMultistateOutput",
        type: ["attributeReport"],
        convert: (model, msg, publish, options, meta) => {
            let running = false;
            const data = msg.data;
            let lookup = {};
            // For lumi.curtain.hagl04 and lumi.curtain.hagl07
            if (["ZNCLDJ12LM", "ZNCLDJ14LM"].includes(model.model))
                lookup = { 0: "closing", 1: "opening", 2: "stopped" };
            // for lumi.curtain.acn002
            if (["ZNJLBL01LM"].includes(model.model))
                lookup = { 0: "closing", 1: "opening", 2: "stopped", 3: "blocked" };
            if (data && data.presentValue !== undefined) {
                const value = data.presentValue;
                if (value < 2) {
                    running = true;
                }
                return {
                    motor_state: lookup[value],
                    running: running,
                };
            }
        },
    },
    lumi_curtain_options: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.curtainHandOpen !== undefined) {
                return { hand_open: msg.data.curtainHandOpen === 0 };
            }
            if (msg.data.curtainReverse !== undefined) {
                return { reverse_direction: msg.data.curtainReverse === 1 };
            }
            if (msg.data.curtainCalibrated !== undefined) {
                return { limits_calibration: msg.data.curtainCalibrated === 1 ? "calibrated" : "recalibrate" };
            }
        },
    },
    lumi_vibration_analog: {
        cluster: "closuresDoorLock",
        type: ["attributeReport", "readResponse"],
        options: [
            exposes.options.vibration_timeout(),
            exposes.options.calibration("x"),
            exposes.options.calibration("y"),
            exposes.options.calibration("z"),
        ],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data["85"]) {
                const vibrationLookup = { 1: "vibration", 2: "tilt", 3: "drop" };
                result.action = vibrationLookup[msg.data["85"]];
                // Device only sends a message when vibration is detected.
                // Therefore we need to publish a no_vibration message on our own.
                if (result.action === "vibration") {
                    result.vibration = true;
                    const timeout = options?.vibration_timeout != null ? Number(options.vibration_timeout) : 90;
                    // Stop any existing timer cause vibration detected
                    clearTimeout(globalStore.getValue(msg.endpoint, "vibration_timer", null));
                    globalStore.putValue(msg.endpoint, "vibration_timer", null);
                    // Set new timer to publish no_vibration message
                    if (timeout !== 0) {
                        const timer = setTimeout(() => {
                            publish({ vibration: false });
                        }, timeout * 1000);
                        globalStore.putValue(msg.endpoint, "vibration_timer", timer);
                    }
                }
            }
            if (msg.data["1283"]) {
                result.angle = msg.data["1283"];
            }
            if (msg.data["1285"]) {
                // https://github.com/dresden-elektronik/deconz-rest-plugin/issues/748#issuecomment-419669995
                // Only first 2 bytes are relevant.
                const data = msg.data["1285"] >> 8;
                // Swap byte order
                result.strength = ((data & 0xff) << 8) | ((data >> 8) & 0xff);
            }
            if (msg.data["1288"]) {
                const data = msg.data["1288"];
                const buffer = node_buffer_1.Buffer.alloc(6);
                buffer.writeUIntLE(data, 0, 6);
                let x = buffer.readIntLE(0, 2);
                let y = buffer.readIntLE(2, 2);
                let z = buffer.readIntLE(4, 2);
                // simple offset calibration
                x = (0, utils_1.calibrateAndPrecisionRoundOptions)(x, options, "x");
                y = (0, utils_1.calibrateAndPrecisionRoundOptions)(y, options, "y");
                z = (0, utils_1.calibrateAndPrecisionRoundOptions)(z, options, "z");
                // calibrated accelerometer values
                result.x_axis = x;
                result.y_axis = y;
                result.z_axis = z;
                // calculate angle
                result.angle_x = Math.round((Math.atan(x / Math.sqrt(y * y + z * z)) * 180) / Math.PI);
                result.angle_y = Math.round((Math.atan(y / Math.sqrt(x * x + z * z)) * 180) / Math.PI);
                result.angle_z = Math.round((Math.atan(z / Math.sqrt(x * x + y * y)) * 180) / Math.PI);
                // calculate absolute angle
                const R = Math.sqrt(x * x + y * y + z * z);
                result.angle_x_absolute = Math.round((Math.acos(x / R) * 180) / Math.PI);
                result.angle_y_absolute = Math.round((Math.acos(y / R) * 180) / Math.PI);
            }
            return result;
        },
    },
    lumi_illuminance: {
        cluster: "msIlluminanceMeasurement",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            // also trigger movement, because there is no illuminance without movement
            // https://github.com/Koenkk/zigbee-herdsman-converters/issues/1925
            msg.data.occupancy = 1;
            const payload = fz.occupancy_with_timeout.convert(model, msg, publish, options, meta);
            if (payload) {
                const illuminance = msg.data.measuredValue;
                payload.illuminance = illuminance;
            }
            return payload;
        },
    },
    lumi_occupancy: {
        // This is for occupancy sensor that only send a message when motion detected,
        // but do not send a motion stop.
        // Therefore we need to publish the no_motion detected by ourselves.
        cluster: "msOccupancySensing",
        type: ["attributeReport", "readResponse"],
        options: [exposes.options.occupancy_timeout_2(), exposes.options.no_occupancy_since_true()],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.occupancy !== 1) {
                // In case of 0 no occupancy is reported.
                // https://github.com/Koenkk/zigbee2mqtt/issues/467
                return;
            }
            // The occupancy sensor only sends a message when motion detected.
            // Therefore we need to publish the no_motion detected by ourselves.
            let timeout = meta && meta.state && meta.state.detection_interval !== undefined ? Number(meta.state.detection_interval) : 60;
            timeout =
                options?.occupancy_timeout != null && Number(options.occupancy_timeout) >= timeout ? Number(options.occupancy_timeout) : timeout + 2;
            // Stop existing timers because motion is detected and set a new one.
            clearTimeout(globalStore.getValue(msg.endpoint, "occupancy_timer", null));
            if (timeout !== 0) {
                const timer = setTimeout(() => {
                    publish({ occupancy: false });
                }, timeout * 1000);
                globalStore.putValue(msg.endpoint, "occupancy_timer", timer);
            }
            const payload = { occupancy: true };
            (0, utils_1.noOccupancySince)(msg.endpoint, options, publish, "start");
            return payload;
        },
    },
    lumi_smoke: {
        cluster: "ssIasZone",
        type: "commandStatusChangeNotification",
        convert: (model, msg, publish, options, meta) => {
            const result = fz.ias_smoke_alarm_1.convert(model, msg, publish, options, meta);
            const zoneStatus = msg.data.zonestatus;
            if (result)
                result.test = (zoneStatus & (1 << 1)) > 0;
            return result;
        },
    },
    lumi_gas_density: {
        cluster: "genBasic",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const data = msg.data;
            if (data?.["65281"]) {
                const basicAttrs = data["65281"];
                if (basicAttrs["100"] !== undefined) {
                    return { gas_density: basicAttrs["100"] };
                }
            }
        },
    },
    lumi_gas_sensitivity: {
        cluster: "ssIasZone",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const data = msg.data;
            const lookup = { "1": "low", "2": "medium", "3": "high" };
            if (data && data["65520"] !== undefined) {
                const value = data["65520"];
                if (value?.startsWith("0x020")) {
                    return {
                        sensitivity: lookup[value.charAt(5)],
                    };
                }
            }
        },
    },
    lumi_door_lock_low_battery: {
        cluster: "genPowerCfg",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (msg.data.batteryAlarmMask !== undefined) {
                return { battery_low: msg.data.batteryAlarmMask === 1 };
            }
        },
    },
    lumi_door_lock_report: {
        cluster: "closuresDoorLock",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            const lockStatusLookup = {
                1: "finger_not_match",
                2: "password_not_match",
                3: "reverse_lock", // disable open from outside
                4: "reverse_lock_cancel", // enable open from outside
                5: "locked",
                6: "lock_opened",
                7: "finger_add",
                8: "finger_delete",
                9: "password_add",
                10: "password_delete",
                11: "lock_opened_inside", // Open form inside reverse lock enable
                12: "lock_opened_outside", // Open form outside reverse lock disable
                13: "ring_bell",
                14: "change_language_to",
                15: "finger_open",
                16: "password_open",
                17: "door_closed",
            };
            if (model.model === "ZNMS11LM") {
                if (msg.data["65296"]) {
                    // finger/password success
                    const data = msg.data["65296"].toString(16);
                    const command = data.substr(0, 1); // 1 finger open, 2 password open
                    const userId = data.substr(5, 2);
                    const userType = data.substr(1, 1); // 1 admin, 2 user
                    result.data = data;
                    result.action = `${lockStatusLookup[14 + Number.parseInt(command, 16)] + (userType === "1" ? "_admin" : "_user")}_id${Number.parseInt(userId, 16).toString()}`;
                    result.action_user = Number.parseInt(userId, 16);
                }
                else if (msg.data["65297"]) {
                    // finger, password failed or bell
                    const data = msg.data["65297"].toString(16);
                    const times = data.substr(0, 1);
                    const type = data.substr(5, 2); // 00 bell, 02 password, 40 error finger
                    result.data = data;
                    if (type === "40") {
                        result.action_action = lockStatusLookup[1];
                        result.action_repeat = Number.parseInt(times, 16);
                    }
                    else if (type === "02") {
                        result.action = lockStatusLookup[2];
                        result.action_repeat = Number.parseInt(times, 16);
                    }
                    else if (type === "00") {
                        result.action = lockStatusLookup[13];
                    }
                }
                else if (msg.data["65281"]?.["1"]) {
                    // user added/delete
                    const data = msg.data["65281"]["1"].toString(16);
                    const command = data.substr(0, 1); // 1 add, 2 delete
                    const userId = data.substr(5, 2);
                    result.action = lockStatusLookup[6 + Number.parseInt(command, 16)];
                    result.action_user = Number.parseInt(userId, 16);
                }
            }
            if (["ZNMS12LM", "ZNMS13LM"].includes(model.model)) {
                if (msg.data["65526"]) {
                    // lock final status
                    // Convert data back to hex to decode
                    const data = node_buffer_1.Buffer.from(msg.data["65526"], "ascii").toString("hex");
                    const command = data.substr(6, 4);
                    if (command === "0301" || // ZNMS12LM
                        command === "0341" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[4];
                        result.state = "UNLOCK";
                        result.reverse = "UNLOCK";
                    }
                    else if (command === "0311" || // ZNMS12LM
                        command === "0351" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[4];
                        result.state = "LOCK";
                        result.reverse = "UNLOCK";
                    }
                    else if (command === "0205" || // ZNMS12LM
                        command === "0245" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[3];
                        result.state = "UNLOCK";
                        result.reverse = "LOCK";
                    }
                    else if (command === "0215" || // ZNMS12LM
                        command === "0255" || // ZNMS13LM
                        command === "1355" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[3];
                        result.state = "LOCK";
                        result.reverse = "LOCK";
                    }
                    else if (command === "0111" || // ZNMS12LM
                        command === "1351" || // ZNMS13LM locked from inside
                        command === "1451" // ZNMS13LM locked from outside
                    ) {
                        result.action = lockStatusLookup[5];
                        result.state = "LOCK";
                        result.reverse = "UNLOCK";
                    }
                    else if (command === "0b00" || // ZNMS12LM
                        command === "0640" || // ZNMS13LM
                        command === "0600" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[12];
                        result.state = "UNLOCK";
                        result.reverse = "UNLOCK";
                    }
                    else if (command === "0c00" || // ZNMS12LM
                        command === "2300" || // ZNMS13LM
                        command === "0540" || // ZNMS13LM
                        command === "0440" // ZNMS13LM
                    ) {
                        result.action = lockStatusLookup[11];
                        result.state = "UNLOCK";
                        result.reverse = "UNLOCK";
                    }
                    else if (command === "2400" || // ZNMS13LM door closed from insed
                        command === "2401" // ZNMS13LM door closed from outside
                    ) {
                        result.action = lockStatusLookup[17];
                        result.state = "UNLOCK";
                        result.reverse = "UNLOCK";
                    }
                }
                else if (msg.data["65296"]) {
                    // finger/password success
                    const data = node_buffer_1.Buffer.from(msg.data["65296"], "ascii").toString("hex");
                    const command = data.substr(6, 2); // 1 finger open, 2 password open
                    const userId = data.substr(12, 2);
                    const userType = data.substr(8, 1); // 1 admin, 2 user
                    result.action = `${lockStatusLookup[14 + Number.parseInt(command, 16)] + (userType === "1" ? "_admin" : "_user")}_id${Number.parseInt(userId, 16).toString()}`;
                    result.action_user = Number.parseInt(userId, 16);
                }
                else if (msg.data["65297"]) {
                    // finger, password failed or bell
                    const data = node_buffer_1.Buffer.from(msg.data["65297"], "ascii").toString("hex");
                    const times = data.substr(6, 2);
                    const type = data.substr(12, 2); // 00 bell, 02 password, 40 error finger
                    if (type === "40") {
                        result.action = lockStatusLookup[1];
                        result.action_repeat = Number.parseInt(times, 16);
                    }
                    else if (type === "00") {
                        result.action = lockStatusLookup[13];
                        result.action_repeat = null;
                    }
                    else if (type === "02") {
                        result.action = lockStatusLookup[2];
                        result.action_repeat = Number.parseInt(times, 16);
                    }
                }
                else if (msg.data["65281"]) {
                    // password added/delete
                    const data = node_buffer_1.Buffer.from(msg.data["65281"], "ascii").toString("hex");
                    const command = data.substr(18, 2); // 1 add, 2 delete
                    const userId = data.substr(12, 2);
                    result.action = lockStatusLookup[6 + Number.parseInt(command, 16)];
                    result.action_user = Number.parseInt(userId, 16);
                }
                else if (msg.data["65522"]) {
                    // set language
                    const data = node_buffer_1.Buffer.from(msg.data["65522"], "ascii").toString("hex");
                    const langId = data.substr(6, 2); // 1 chinese, 2: english
                    result.action = lockStatusLookup[14] + (langId === "2" ? "_english" : "_chinese");
                }
            }
            return result;
        },
    },
    lumi_action_on: {
        cluster: "genOnOff",
        type: "commandOn",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            return { action: "button_2_single" };
        },
    },
    lumi_action_off: {
        cluster: "genOnOff",
        type: "commandOff",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            return { action: "button_1_single" };
        },
    },
    lumi_action_step: {
        cluster: "genLevelCtrl",
        type: "commandStep",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const button = msg.data.stepmode === 0 ? "4" : "3";
            return { action: `button_${button}_single` };
        },
    },
    lumi_action_stop: {
        cluster: "genLevelCtrl",
        type: "commandStop",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            if (globalStore.hasValue(msg.endpoint, "button")) {
                const value = globalStore.getValue(msg.endpoint, "button");
                const duration = Date.now() - value.start;
                const payload = { action: `button_${value.button}_release`, action_duration: duration };
                return payload;
            }
        },
    },
    lumi_action_move: {
        cluster: "genLevelCtrl",
        type: "commandMove",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const button = msg.data.movemode === 0 ? "4" : "3";
            globalStore.putValue(msg.endpoint, "button", { button, start: Date.now() });
            return { action: `button_${button}_hold` };
        },
    },
    lumi_action_step_color_temp: {
        cluster: "lightingColorCtrl",
        type: "commandStepColorTemp",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
            let action;
            if (model.model === "WXCJKG12LM") {
                // for WXCJKG12LM model it's double click event on buttons 3 and 4
                action = msg.data.stepmode === 1 ? "3_double" : "4_double";
            }
            else {
                // but for WXCJKG13LM model it's single click event on buttons 5 and 6
                action = msg.data.stepmode === 1 ? "5_single" : "6_single";
            }
            return { action: `button_${action}` };
        },
    },
    lumi_action_move_color_temp: {
        cluster: "lightingColorCtrl",
        type: "commandMoveColorTemp",
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const stop = msg.data.movemode === 0;
            let result = null;
            if (stop) {
                const button = globalStore.getValue(msg.endpoint, "button").button;
                const duration = Date.now() - globalStore.getValue(msg.endpoint, "button").start;
                result = { action: `button_${button}_release`, duration, action_duration: duration };
            }
            else {
                const button = msg.data.movemode === 3 ? "6" : "5";
                result = { action: `button_${button}_hold` };
                globalStore.putValue(msg.endpoint, "button", { button, start: Date.now() });
            }
            return result;
        },
    },
    // lumi device specific
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    lumi_action_WXKG01LM: {
        // Unique converter
        cluster: "genOnOff",
        type: ["attributeReport", "readResponse"],
        options: [
            e
                .numeric("hold_timeout", ea.SET)
                .withValueMin(0)
                .withDescription("The WXKG01LM only reports a button press and release." +
                "By default, a hold action is published when there is at least 1000 ms between both events. It could be that due to " +
                "delays in the network the release message is received late. This causes a single click to be identified as a hold " +
                "action. If you are experiencing this you can try experimenting with this option (e.g. set it to 2000) (value is in ms)."),
            e
                .numeric("hold_timeout_expire", ea.SET)
                .withValueMin(0)
                .withDescription("Sometimes it happens that the button does not send a " +
                "release. To avoid problems a release is automatically send after a timeout. The default timeout is 4000 ms, you can " +
                "increase it with this option (value is in ms)."),
        ],
        convert: (model, msg, publish, options, meta) => {
            if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
                return;
            const state = msg.data.onOff;
            // 0 = click down, 1 = click up, else = multiple clicks
            if (state === 0) {
                const timer = setTimeout(() => {
                    publish({ action: "hold" });
                    globalStore.putValue(msg.endpoint, "timer", null);
                    globalStore.putValue(msg.endpoint, "hold", Date.now());
                    const holdTimer = setTimeout(() => {
                        globalStore.putValue(msg.endpoint, "hold", false);
                    }, options.hold_timeout_expire || 4000);
                    globalStore.putValue(msg.endpoint, "hold_timer", holdTimer);
                    // After 4000 milliseconds of not receiving release we assume it will not happen.
                }, options.hold_timeout || 1000); // After 1000 milliseconds of not releasing we assume hold.
                globalStore.putValue(msg.endpoint, "timer", timer);
            }
            else if (state === 1) {
                if (globalStore.getValue(msg.endpoint, "hold")) {
                    const duration = Date.now() - globalStore.getValue(msg.endpoint, "hold");
                    publish({ action: "release", duration: duration });
                    globalStore.putValue(msg.endpoint, "hold", false);
                }
                if (globalStore.getValue(msg.endpoint, "timer")) {
                    clearTimeout(globalStore.getValue(msg.endpoint, "timer"));
                    globalStore.putValue(msg.endpoint, "timer", null);
                    publish({ action: "single" });
                }
            }
            else {
                const clicks = msg.data["32768"];
                const actionLookup = { 1: "single", 2: "double", 3: "triple", 4: "quadruple" };
                const payload = actionLookup[clicks] ? actionLookup[clicks] : "many";
                publish({ action: payload });
            }
        },
    },
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    lumi_smart_panel_ZNCJMB14LM: {
        cluster: "manuSpecificLumi",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if (msg.data[0x0215] !== undefined) {
                const lookup = { 0: "classic", 1: "concise" };
                result.theme = lookup[msg.data[0x0215]];
            }
            if (msg.data[0x0214] !== undefined) {
                const lookup = { 1: "classic", 2: "analog clock" };
                result.screen_saver_style = lookup[msg.data[0x0214]];
            }
            if (msg.data[0x0213] !== undefined) {
                result.standby_enabled = !!(msg.data[0x0213] & 1);
            }
            if (msg.data[0x0212] !== undefined) {
                const lookup = { 0: "mute", 1: "low", 2: "medium", 3: "high" };
                result.beep_volume = lookup[msg.data[0x0212]];
            }
            if (msg.data[0x0211] !== undefined) {
                result.lcd_brightness = msg.data[0x0211];
            }
            if (msg.data[0x022b] !== undefined) {
                const lookup = { 0: "none", 1: "1", 2: "2", 3: "1 and 2", 4: "3", 5: "1 and 3", 6: "2 and 3", 7: "all" };
                result.available_switches = lookup[msg.data[0x022b]];
            }
            if (msg.data[0x217] !== undefined) {
                const lookup = { 3: "small", 4: "medium", 5: "large" };
                result.font_size = lookup[msg.data[0x217]];
            }
            if (msg.data[0x219] !== undefined) {
                const lookup = { 0: "scene", 1: "feel", 2: "thermostat", 3: "switch" };
                result.homepage = lookup[msg.data[0x219]];
            }
            if (msg.data[0x210] !== undefined) {
                const lookup = { 0: "chinese", 1: "english" };
                result.language = lookup[msg.data[0x210]];
            }
            if (msg.data[0x216] !== undefined) {
                result.standby_time = msg.data[0x216];
            }
            if (msg.data[0x218] !== undefined) {
                result.lcd_auto_brightness_enabled = !!(msg.data[0x218] & 1);
            }
            if (msg.data[0x221] !== undefined) {
                result.screen_saver_enabled = !!(msg.data[0x221] & 1);
            }
            if (msg.data[0x222] !== undefined) {
                result.standby_lcd_brightness = msg.data[0x222];
            }
            if (msg.data[0x223] !== undefined) {
                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 textarr = msg.data[0x223].slice(1, msg.data[0x223].length);
                result.switch_1_icon = lookup[msg.data[0x223][0]];
                result.switch_1_text = String.fromCharCode(...textarr);
            }
            if (msg.data[0x224] !== undefined) {
                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 textarr = msg.data[0x224].slice(1, msg.data[0x224].length);
                result.switch_2_icon = lookup[msg.data[0x224][0]];
                result.switch_2_text = String.fromCharCode(...textarr);
            }
            if (msg.data[0x225] !== undefined) {
                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 textarr = msg.data[0x225].slice(1, msg.data[0x225].length);
                result.switch_3_icon = lookup[msg.data[0x225][0]];
                result.switch_3_text = String.fromCharCode(...textarr);
            }
            return result;
        },
    },
};
exports.toZigbee = {
    // lumi generic
    lumi_power: {
        key: ["power"],
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.endpoints.find((e) => e.supportsInputCluster("genAnalogInput")) ?? entity;
            await endpoint.read("genAnalogInput", ["presentValue"]);
        },
    },
    lumi_led_disabled_night: {
        key: ["led_disabled_night"],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if ([
                "ZNCZ04LM",
                "ZNCZ12LM",
                "ZNCZ15LM",
                "QBCZ14LM",
                "QBCZ15LM",
                "QBKG17LM",
                "QBKG18LM",
                "QBKG19LM",
                "QBKG20LM",
                "QBKG25LM",
                "QBKG26LM",
                "QBKG27LM",
                "QBKG28LM",
                "QBKG29LM",
                "QBKG30LM",
                "QBKG31LM",
                "QBKG32LM",
                "QBKG33LM",
                "QBKG34LM",
                "DLKZMK11LM",
                "SSM-U01",
                "WS-EUK01",
                "WS-EUK02",
                "WS-EUK03",
                "WS-EUK04",
                "SP-EUC01",
                "ZNQBKG24LM",
                "ZNQBKG25LM",
                "ZNQBKG26LM",
                "ZNQBKG38LM",
                "ZNQBKG39LM",
                "ZNQBKG40LM",
                "ZNQBKG41LM",
            ].includes(meta.mapped.model)) {
                await entity.write("manuSpecificLumi", { 515: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
            }
            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", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
            }
            else {
                throw new Error("Not supported");
            }
            return { state: { led_disabled_night: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if ([
                "ZNCZ04LM",
                "ZNCZ12LM",
                "ZNCZ15LM",
                "QBCZ15LM",
                "QBCZ14LM",
                "QBKG17LM",
                "QBKG18LM",
                "QBKG19LM",
                "QBKG20LM",
                "QBKG25LM",
                "QBKG26LM",
                "QBKG27LM",
                "QBKG28LM",
                "QBKG29LM",
                "QBKG30LM",
                "QBKG31LM",
                "QBKG32LM",
                "QBKG33LM",
                "QBKG34LM",
                "DLKZMK11LM",
                "SSM-U01",
                "WS-EUK01",
                "WS-EUK02",
                "WS-EUK03",
                "WS-EUK04",
                "SP-EUC01",
                "ZNQBKG24LM",
                "ZNQBKG25LM",
                "ZNQBKG26LM",
                "ZNQBKG38LM",
                "ZNQBKG39LM",
                "ZNQBKG40LM",
                "ZNQBKG41LM",
            ].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0x0203], manufacturerOptions.lumi);
            }
            else {
                throw new Error("Not supported");
            }
        },
    },
    lumi_flip_indicator_light: {
        key: ["flip_indicator_light"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { OFF: 0, ON: 1 };
            await entity.write("manuSpecificLumi", { 240: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { flip_indicator_light: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x00f0], manufacturerOptions.lumi);
        },
    },
    lumi_power_outage_count: {
        key: ["power_outage_count"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0002], manufacturerOptions.lumi);
        },
    },
    // lumi class specific
    lumi_feeder: {
        key: ["feed", "schedule", "led_indicator", "child_lock", "mode", "serving_size", "portion_weight"],
        convertSet: async (entity, key, value, meta) => {
            const sendAttr = async (attrCode, value, length) => {
                // @ts-expect-error ignore
                entity.sendSeq = ((entity.sendSeq || 0) + 1) % 256;
                // @ts-expect-error ignore
                const val = node_buffer_1.Buffer.from([0x00, 0x02, entity.sendSeq, 0, 0, 0, 0, 0]);
                // @ts-expect-error ignore
                entity.sendSeq += 1;
                val.writeInt32BE(attrCode, 3);
                val.writeUInt8(length, 7);
                let v = node_buffer_1.Buffer.alloc(length);
                switch (length) {
                    case 1:
                        v.writeUInt8(value);
                        break;
                    case 2:
                        v.writeUInt16BE(value);
                        break;
                    case 4:
                        v.writeUInt32BE(value);
                        break;
                    default:
                        // @ts-expect-error ignore
                        v = value;
                }
                await entity.write("manuSpecificLumi", { 65521: { value: node_buffer_1.Buffer.concat([val, v]), type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
            };
            switch (key) {
                case "feed":
                    await sendAttr(0x04150055, 1, 1);
                    break;
                case "schedule": {
                    const schedule = [];
                    // @ts-expect-error ignore
                    // biome-ignore lint/complexity/noForEach: ignored using `--suppress`
                    value.forEach((item) => {
                        const schedItem = node_buffer_1.Buffer.from([(0, utils_1.getKey)(feederDaysLookup, item.days, 0x7f), item.hour, item.minute, item.size, 0]);
                        schedule.push(schedItem.toString("hex"));
                    });
                    const val = node_buffer_1.Buffer.concat([node_buffer_1.Buffer.from(schedule.join(",")), node_buffer_1.Buffer.from([0])]);
                    // @ts-expect-error ignore
                    await sendAttr(0x080008c8, val, val.length);
                    break;
                }
                case "led_indicator":
                    await sendAttr(0x04170055, (0, utils_1.getFromLookup)(value, { ON: 1, OFF: 0 }), 1);
                    break;
                case "child_lock":
                    await sendAttr(0x04160055, (0, utils_1.getFromLookup)(value, { UNLOCK: 0, LOCK: 1 }), 1);
                    break;
                case "mode":
                    await sendAttr(0x04180055, (0, utils_1.getFromLookup)(value, { manual: 0, schedule: 1 }), 1);
                    break;
                case "serving_size":
                    // @ts-expect-error ignore
                    await sendAttr(0x0e5c0055, value, 4);
                    break;
                case "portion_weight":
                    // @ts-expect-error ignore
                    await sendAttr(0x0e5f0055, value, 4);
                    break;
                default: // Unknown key
                    logger_1.logger.warning(`Unhandled key ${key}`, "zhc:lumi:feeder");
            }
            return { state: { [key]: value } };
        },
    },
    lumi_detection_distance: {
        key: ["detection_distance"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, "detection_distance");
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            const lookup = { "10mm": 1, "20mm": 2, "30mm": 3 };
            await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
            return { state: { detection_distance: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x010c], { manufacturerCode: exports.manufacturerCode });
        },
    },
    lumi_trv: {
        key: [
            "system_mode",
            "preset",
            "window_detection",
            "valve_detection",
            "child_lock",
            "away_preset_temperature",
            "calibrate",
            "sensor",
            "external_temperature_input",
            "identify",
            "schedule",
            "schedule_settings",
        ],
        convertSet: async (entity, key, value, meta) => {
            const lumiHeader = (counter, params, action) => {
                const header = [0xaa, 0x71, params.length + 3, 0x44, counter];
                const integrity = 512 - header.reduce((sum, elem) => sum + elem, 0);
                return [...header, integrity, action, 0x41, params.length];
            };
            const sensor = node_buffer_1.Buffer.from("00158d00019d1b98", "hex");
            switch (key) {
                case "system_mode":
                    await entity.write("manuSpecificLumi", { 625: { value: (0, utils_1.getFromLookup)(value, { off: 0, heat: 1 }), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "preset":
                    await entity.write("manuSpecificLumi", { 626: { value: (0, utils_1.getFromLookup)(value, { manual: 0, auto: 1, away: 2 }), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "window_detection":
                    await entity.write("manuSpecificLumi", {
                        627: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
                    }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "valve_detection":
                    await entity.write("manuSpecificLumi", {
                        628: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
                    }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "child_lock":
                    await entity.write("manuSpecificLumi", {
                        631: { value: (0, utils_1.getFromLookup)(value, { UNLOCK: 0, LOCK: 1 }), type: 0x20 },
                    }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "away_preset_temperature":
                    await entity.write("manuSpecificLumi", {
                        633: { value: Math.round((0, utils_1.toNumber)(value, "away_preset_temperature") * 100), type: 0x23 },
                    }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "sensor": {
                    (0, utils_1.assertEndpoint)(entity);
                    const device = node_buffer_1.Buffer.from(entity.deviceIeeeAddress.substring(2), "hex");
                    const timestamp = node_buffer_1.Buffer.alloc(4);
                    timestamp.writeUInt32BE(Date.now() / 1000);
                    if (value === "external") {
                        const params1 = [
                            ...timestamp,
                            0x3d,
                            0x04,
                            ...device,
                            ...sensor,
                            0x00,
                            0x01,
                            0x00,
                            0x55,
                            0x13,
                            0x0a,
                            0x02,
                            0x00,
                            0x00,
                            0x64,
                            0x04,
                            0xce,
                            0xc2,
                            0xb6,
                            0xc8,
                            0x00,
                            0x00,
                            0x00,
                            0x00,
                            0x00,
                            0x01,
                            0x3d,
                            0x64,
                            0x65,
                        ];
                        const params2 = [
                            ...timestamp,
                            0x3d,
                            0x05,
                            ...device,
                            ...sensor,
                            0x08,
                            0x00,
                            0x07,
                            0xfd,
                            0x16,
                            0x0a,
                            0x02,
                            0x0a,
                            0xc9,
                            0xe8,
                            0xb1,
                            0xb8,
                            0xd4,
                            0xda,
                            0xcf,
                            0xdf,
                            0xc0,
                            0xeb,
                            0x00,
                            0x00,
                            0x00,
                            0x00,
                            0x00,
                            0x01,
                            0x3d,
                            0x04,
                            0x65,
                        ];
                        const val1 = [...lumiHeader(0x12, params1, 0x02), ...params1];
                        const val2 = [...lumiHeader(0x13, params2, 0x02), ...params2];
                        await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                        await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                    }
                    else if (value === "internal") {
                        const params1 = [...timestamp, 0x3d, 0x05, ...device, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
                        const params2 = [...timestamp, 0x3d, 0x04, ...device, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
                        const val1 = [...lumiHeader(0x12, params1, 0x04), ...params1];
                        const val2 = [...lumiHeader(0x13, params2, 0x04), ...params2];
                        await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                        await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                        await entity.read("hvacThermostat", ["localTemp"]);
                    }
                    break;
                }
                case "external_temperature_input":
                    if (meta.state.sensor === "external") {
                        const temperatureBuf = node_buffer_1.Buffer.alloc(4);
                        const number = (0, utils_1.toNumber)(value);
                        temperatureBuf.writeFloatBE(Math.round(number * 100));
                        const params = [...sensor, 0x00, 0x01, 0x00, 0x55, ...temperatureBuf];
                        const data = [...lumiHeader(0x12, params, 0x05), ...params];
                        await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                    }
                    break;
                case "calibrate":
                    await entity.write("manuSpecificLumi", { 624: { value: 1, type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "identify":
                    await entity.command("genIdentify", "identify", { identifytime: 5 }, {});
                    break;
                case "schedule":
                    await entity.write("manuSpecificLumi", {
                        637: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
                    }, { manufacturerCode: exports.manufacturerCode });
                    break;
                case "schedule_settings": {
                    // @ts-expect-error ignore
                    const schedule = exports.trv.parseSchedule(value);
                    exports.trv.validateSchedule(schedule);
                    const buffer = exports.trv.encodeSchedule(schedule);
                    await entity.write("manuSpecificLumi", { 630: { value: buffer, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
                    break;
                }
                default: // Unknown key
                    logger_1.logger.debug(`Unhandled key ${key}`, "zhc:lumi:trv");
            }
        },
        convertGet: async (entity, key, meta) => {
            const dict = {
                system_mode: 0x0271,
                preset: 0x0272,
                window_detection: 0x0273,
                valve_detection: 0x0274,
                child_lock: 0x0277,
                away_preset_temperature: 0x0279,
                calibrated: 0x027b,
                sensor: 0x027e,
                schedule: 0x027d,
                schedule_settings: 0x0276,
            };
            await entity.read("manuSpecificLumi", [(0, utils_1.getFromLookup)(key, dict)], { manufacturerCode: exports.manufacturerCode });
        },
    },
    lumi_presence_region_upsert: {
        key: ["region_upsert"],
        convertSet: async (entity, key, value, meta) => {
            const commandWrapper = exports.presence.parseAqaraFp1RegionUpsertInput(value);
            if (!commandWrapper.isSuccess) {
                logger_1.logger.warning(
                // @ts-expect-error untyped
                `Encountered an error (${commandWrapper.error.reason}) while parsing configuration commands (input: ${JSON.stringify(value)})`, NS);
                return;
            }
            const command = commandWrapper.payload.command;
            logger_1.logger.debug(`Trying to create region ${command.region_id}`, NS);
            const sortedZonesAccumulator = {};
            const sortedZonesWithSets = command.zones.reduce((accumulator, zone) => {
                if (!accumulator[zone.y]) {
                    accumulator[zone.y] = new Set();
                }
                accumulator[zone.y].add(zone.x);
                return accumulator;
            }, sortedZonesAccumulator);
            const sortedZones = Object.entries(sortedZonesWithSets).reduce((acc, [key, value]) => {
                const numKey = Number.parseInt(key, 10); // Convert string key back to number
                acc[numKey] = Array.from(value);
                return acc;
            }, {});
            const deviceConfig = new Uint8Array(7);
            // Command parameters
            deviceConfig[0] = exports.presence.constants.region_config_cmds.create;
            deviceConfig[1] = command.region_id;
            deviceConfig[6] = exports.presence.constants.region_config_cmd_suffix_upsert;
            // Zones definition
            deviceConfig[2] |= exports.presence.encodeXCellsDefinition(sortedZones["1"]);
            deviceConfig[2] |= exports.presence.encodeXCellsDefinition(sortedZones["2"]) << 4;
            deviceConfig[3] |= exports.presence.encodeXCellsDefinition(sortedZones["3"]);
            deviceConfig[3] |= exports.presence.encodeXCellsDefinition(sortedZones["4"]) << 4;
            deviceConfig[4] |= exports.presence.encodeXCellsDefinition(sortedZones["5"]);
            deviceConfig[4] |= exports.presence.encodeXCellsDefinition(sortedZones["6"]) << 4;
            deviceConfig[5] |= exports.presence.encodeXCellsDefinition(sortedZones["7"]);
            logger_1.logger.info(`Create region ${command.region_id} ${(0, utils_1.printNumbersAsHexSequence)([...deviceConfig], 2)}`, NS);
            const payload = {
                [exports.presence.constants.region_config_write_attribute]: {
                    value: deviceConfig,
                    type: exports.presence.constants.region_config_write_attribute_type,
                },
            };
            await entity.write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
        },
    },
    lumi_presence_region_delete: {
        key: ["region_delete"],
        convertSet: async (entity, key, value, meta) => {
            const commandWrapper = exports.presence.parseAqaraFp1RegionDeleteInput(value);
            if (!commandWrapper.isSuccess) {
                logger_1.logger.warning(
                // @ts-expect-error ignore
                `Encountered an error (${commandWrapper.error.reason}) while parsing configuration commands (input: ${JSON.stringify(value)})`, NS);
                return;
            }
            const command = commandWrapper.payload.command;
            logger_1.logger.debug(`trying to delete region ${command.region_id}`, NS);
            const deviceConfig = new Uint8Array(7);
            // Command parameters
            deviceConfig[0] = exports.presence.constants.region_config_cmds.delete;
            deviceConfig[1] = command.region_id;
            deviceConfig[6] = exports.presence.constants.region_config_cmd_suffix_delete;
            // Zones definition
            deviceConfig[2] = 0;
            deviceConfig[3] = 0;
            deviceConfig[4] = 0;
            deviceConfig[5] = 0;
            logger_1.logger.info(`Delete region ${command.region_id} (${(0, utils_1.printNumbersAsHexSequence)([...deviceConfig], 2)})`, NS);
            const payload = {
                [exports.presence.constants.region_config_write_attribute]: {
                    value: deviceConfig,
                    type: exports.presence.constants.region_config_write_attribute_type,
                },
            };
            await entity.write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
        },
    },
    lumi_cube_operation_mode: {
        key: ["operation_mode"],
        convertSet: (entity, key, value, meta) => {
            const lookup = { action_mode: 0, scene_mode: 1 };
            /**
             * schedule the callback to run when the configuration window comes
             */
            const callback = async () => {
                await entity.write("manuSpecificLumi", { 328: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode, disableDefaultResponse: true });
                logger_1.logger.info("operation_mode switch success!", "zhc:lumi:cube");
            };
            globalStore.putValue(meta.device, "opModeSwitchTask", { callback, newMode: value });
            logger_1.logger.info("Now give your cube a forceful throw motion (Careful not to drop it)!", "zhc:lumi:cube");
        },
    },
    lumi_switch_operation_mode_basic: {
        key: ["operation_mode"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertEndpoint)(entity);
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            let targetValue = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
            // 1/2 gang switches using genBasic on endpoint 1.
            // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
            let attrId;
            let attrValue;
            if (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 = (0, utils_1.getFromLookup)(targetValue, { control_left_relay: 0x12, control_right_relay: 0x22, decoupled: 0xfe });
                if (attrId == null) {
                    throw new Error(`Unsupported endpoint ${meta.endpoint_name} for changing operation_mode.`);
                }
            }
            else {
                attrId = 0xff22;
                attrValue = (0, utils_1.getFromLookup)(targetValue, { control_relay: 0x12, decoupled: 0xfe });
            }
            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.lumi);
            return { state: { operation_mode: targetValue } };
        },
        convertGet: async (entity, key, meta) => {
            // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
            let attrId;
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (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.lumi);
        },
    },
    lumi_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 = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
            // Switches using manuSpecificLumi 0x0200 on the same endpoints as the onOff clusters.
            const lookupState = { control_relay: 0x01, decoupled: 0x00 };
            await entity.write("manuSpecificLumi", { 512: { value: (0, utils_1.getFromLookup)(targetValue, lookupState), type: 0x20 } }, manufacturerOptions.lumi);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0200], manufacturerOptions.lumi);
        },
    },
    lumi_detection_interval: {
        key: ["detection_interval"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertNumber)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value *= 1;
            await entity.write("manuSpecificLumi", { 258: { value: [value], type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { detection_interval: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0102], manufacturerOptions.lumi);
        },
    },
    lumi_overload_protection: {
        key: ["overload_protection"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertNumber)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value *= 1;
            await entity.write("manuSpecificLumi", { 523: { value: [value], type: 0x39 } }, manufacturerOptions.lumi);
            return { state: { overload_protection: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x020b], manufacturerOptions.lumi);
        },
    },
    lumi_switch_mode_switch: {
        key: ["mode_switch"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { anti_flicker_mode: 4, quick_mode: 1 };
            await entity.write("manuSpecificLumi", { 4: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x21 } }, manufacturerOptions.lumi);
            return { state: { mode_switch: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0004], manufacturerOptions.lumi);
        },
    },
    lumi_button_switch_mode: {
        key: ["button_switch_mode"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { relay: 0, relay_and_usb: 1 };
            await entity.write("manuSpecificLumi", { 550: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { button_switch_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0226], manufacturerOptions.lumi);
        },
    },
    lumi_socket_button_lock: {
        key: ["button_lock"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { ON: 0, OFF: 1 };
            await entity.write("manuSpecificLumi", { 512: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { button_lock: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0200], manufacturerOptions.lumi);
        },
    },
    lumi_dimmer_mode: {
        key: ["dimmer_mode"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { rgbw: 3, dual_ct: 1 };
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            // @ts-expect-error ignore
            if (["rgbw"].includes(value)) {
                await entity.write("manuSpecificLumi", { 1289: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, manufacturerOptions.lumi);
                await entity.write("manuSpecificLumi", { 1295: { value: 1, type: 0x23 } }, manufacturerOptions.lumi);
            }
            else {
                await entity.write("manuSpecificLumi", { 1289: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, manufacturerOptions.lumi);
                // Turn on dimming channel 1 and channel 2
                await entity.write("manuSpecificLumi", { 1295: { value: 3, type: 0x23 } }, manufacturerOptions.lumi);
            }
            return { state: { dimmer_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0509], manufacturerOptions.lumi);
        },
    },
    lumi_switch_do_not_disturb: {
        key: ["do_not_disturb"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 515: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
            return { state: { do_not_disturb: value } };
        },
    },
    lumi_switch_type: {
        key: ["switch_type"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { toggle: 1, momentary: 2 };
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            await entity.write("manuSpecificLumi", { 10: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { switch_type: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x000a], manufacturerOptions.lumi);
        },
    },
    lumi_switch_power_outage_memory: {
        key: ["power_outage_memory"],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if ([
                "SP-EUC01",
                "ZNCZ04LM",
                "ZNCZ15LM",
                "QBCZ14LM",
                "QBCZ15LM",
                "SSM-U01",
                "SSM-U02",
                "DLKZMK11LM",
                "DLKZMK12LM",
                "WS-EUK01",
                "WS-EUK02",
                "WS-EUK03",
                "WS-EUK04",
                "QBKG17LM",
                "QBKG18LM",
                "QBKG19LM",
                "QBKG20LM",
                "QBKG25LM",
                "QBKG26LM",
                "QBKG27LM",
                "QBKG28LM",
                "QBKG29LM",
                "QBKG30LM",
                "QBKG31LM",
                "QBKG32LM",
                "QBKG33LM",
                "QBKG34LM",
                "QBKG38LM",
                "QBKG39LM",
                "QBKG40LM",
                "QBKG41LM",
                "ZNDDMK11LM",
                "ZNLDP13LM",
                "ZNQBKG31LM",
                "WS-USC02",
                "WS-USC03",
                "WS-USC04",
                "ZNQBKG24LM",
                "ZNQBKG25LM",
                "ZNQBKG26LM",
                "JWDL001A",
                "SSWQD02LM",
                "SSWQD03LM",
                "XDD11LM",
                "XDD12LM",
                "XDD13LM",
                "ZNLDP12LM",
                "ZNLDP13LM",
                "ZNXDD01LM",
                "WS-USC01",
            ].includes(meta.mapped.model)) {
                await entity.write("manuSpecificLumi", { 513: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
            }
            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", { 65520: { value: payload[0], type: 0x41 } }, manufacturerOptions.lumi);
                await entity.write("genBasic", { 65520: { value: payload[1], type: 0x41 } }, manufacturerOptions.lumi);
            }
            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", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
            }
            else if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
                // Support existing syntax of a nested object just for the state field. Though it's quite silly IMO.
                const targetValue = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
                const lookupState = { on: 0x01, electric_appliances_on: 0x00, electric_appliances_off: 0x02, inverted: 0x03 };
                await entity.write("manuSpecificLumi", { 1303: { value: (0, utils_1.getFromLookup)(targetValue, lookupState), type: 0x20 } }, manufacturerOptions.lumi);
            }
            else {
                throw new Error("Not supported");
            }
            return { state: { power_outage_memory: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if ([
                "SP-EUC01",
                "ZNCZ04LM",
                "ZNCZ15LM",
                "QBCZ14LM",
                "QBCZ15LM",
                "SSM-U01",
                "SSM-U02",
                "DLKZMK11LM",
                "DLKZMK12LM",
                "WS-EUK01",
                "WS-EUK02",
                "WS-EUK03",
                "WS-EUK04",
                "QBKG17LM",
                "QBKG18LM",
                "QBKG19LM",
                "QBKG20LM",
                "QBKG25LM",
                "QBKG26LM",
                "QBKG27LM",
                "QBKG28LM",
                "QBKG29LM",
                "QBKG30LM",
                "QBKG31LM",
                "QBKG32LM",
                "QBKG33LM",
                "QBKG34LM",
                "QBKG38LM",
                "QBKG39LM",
                "QBKG40LM",
                "QBKG41LM",
                "ZNDDMK11LM",
                "ZNLDP13LM",
                "ZNQBKG31LM",
                "WS-USC02",
                "WS-USC03",
                "WS-USC04",
                "ZNQBKG24LM",
                "ZNQBKG25LM",
                "ZNQBKG26LM",
                "JWDL001A",
                "SSWQD02LM",
                "SSWQD03LM",
                "XDD11LM",
                "XDD12LM",
                "XDD13LM",
                "ZNLDP12LM",
                "ZNLDP13LM",
                "ZNXDD01LM",
                "WS-USC01",
            ].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0x0201]);
            }
            else if (["ZNCZ02LM", "QBCZ11LM", "ZNCZ11LM", "ZNCZ12LM"].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0xfff0]);
            }
            else if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0x0517]);
            }
            else {
                throw new Error("Not supported");
            }
        },
    },
    lumi_light_power_outage_memory: {
        key: ["power_outage_memory"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("genBasic", { 65305: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
            return { state: { power_outage_memory: value } };
        },
    },
    lumi_auto_off: {
        key: ["auto_off"],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (["ZNCZ04LM", "ZNCZ12LM", "SP-EUC01"].includes(meta.mapped.model)) {
                await entity.write("manuSpecificLumi", { 514: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
            }
            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", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
            }
            else {
                throw new Error("Not supported");
            }
            return { state: { auto_off: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (["ZNCZ04LM", "ZNCZ12LM", "SP-EUC01"].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0x0202], manufacturerOptions.lumi);
            }
            else {
                throw new Error("Not supported");
            }
        },
    },
    lumi_detection_period: {
        key: ["detection_period"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertNumber)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value *= 1;
            await entity.write("manuSpecificLumi", { 0: { value: [value], type: 0x21 } }, manufacturerOptions.lumi);
            return { state: { detection_period: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0000], manufacturerOptions.lumi);
        },
    },
    lumi_motion_sensitivity: {
        key: ["motion_sensitivity"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { low: 1, medium: 2, high: 3 };
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { motion_sensitivity: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x010c], manufacturerOptions.lumi);
        },
    },
    lumi_presence: {
        key: ["presence"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0142], manufacturerOptions.lumi);
        },
    },
    lumi_monitoring_mode: {
        key: ["monitoring_mode"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            const lookup = { undirected: 0, left_right: 1 };
            await entity.write("manuSpecificLumi", { 324: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { monitoring_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0144], manufacturerOptions.lumi);
        },
    },
    lumi_approach_distance: {
        key: ["approach_distance"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            const lookup = { far: 0, medium: 1, near: 2 };
            await entity.write("manuSpecificLumi", { 326: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { approach_distance: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0146], manufacturerOptions.lumi);
        },
    },
    lumi_reset_nopresence_status: {
        key: ["reset_nopresence_status"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 343: { value: 1, type: 0x20 } }, manufacturerOptions.lumi);
        },
    },
    lumi_switch_click_mode: {
        key: ["click_mode"],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
                await entity.write("manuSpecificLumi", { 646: { value: (0, utils_1.getFromLookup)(value, { fast: 0x1, multi: 0x02 }), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { click_mode: value } };
            }
            await entity.write("manuSpecificLumi", { 293: { value: (0, utils_1.getFromLookup)(value, { fast: 0x1, multi: 0x02 }), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { click_mode: value } };
        },
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
                await entity.read("manuSpecificLumi", [0x0286], manufacturerOptions.lumi);
            }
            else {
                await entity.read("manuSpecificLumi", [0x125], manufacturerOptions.lumi);
            }
        },
    },
    lumi_switch_lock_relay_opple: {
        key: ["lock_relay"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 645: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { lock_relay: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0285], manufacturerOptions.lumi);
        },
    },
    lumi_operation_mode_opple: {
        key: ["operation_mode"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value);
            // 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("manuSpecificLumi", { mode: (0, utils_1.getFromLookup)(value.toLowerCase(), lookup) }, { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
            return { state: { operation_mode: value.toLowerCase() } };
        },
        convertGet: async (entity, key, meta) => {
            const endpoint = meta.device.getEndpoint(1);
            await endpoint.read("manuSpecificLumi", ["mode"], { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
        },
    },
    lumi_vibration_sensitivity: {
        key: ["sensitivity"],
        convertSet: async (entity, key, value, meta) => {
            if ((0, utils_1.isString)(value)) {
                // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
                value = (0, utils_1.getFromLookup)(value, { low: 0x15, medium: 0x0b, high: 0x01 });
            }
            (0, utils_1.assertNumber)(value);
            const options = { ...manufacturerOptions.lumi, timeout: 35000 };
            await entity.write("genBasic", { 65293: { value, type: 0x20 } }, options);
            return { state: { sensitivity: value } };
        },
    },
    lumi_interlock: {
        key: ["interlock"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("genBinaryOutput", { 65286: { value: value ? 0x01 : 0x00, type: 0x10 } }, manufacturerOptions.lumi);
            return { state: { interlock: value } };
        },
    },
    lumi_curtain_options: {
        key: ["options"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertObject)(value);
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            const opts = {
                reverse_direction: false,
                hand_open: true,
                reset_limits: false,
                ...value,
            };
            // Legacy names
            if (value.auto_close != null)
                opts.hand_open = value.auto_close;
            if (value.reset_move != null)
                opts.reset_limits = value.reset_move;
            if (meta.mapped.model === "ZNCLDJ12LM") {
                await entity.write("genBasic", { 65320: { value: opts.reverse_direction, type: 0x10 } }, manufacturerOptions.lumi);
                await entity.write("genBasic", { 65321: { value: !opts.hand_open, type: 0x10 } }, manufacturerOptions.lumi);
                if (opts.reset_limits) {
                    await entity.write("genBasic", { 65319: { value: 0x00, type: 0x10 } }, manufacturerOptions.lumi);
                }
            }
            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", { 1025: { value: payload, type: 0x42 } }, manufacturerOptions.lumi);
                // hand_open requires a separate request with slightly different payload
                payload[2] = 0x08;
                await entity.write("genBasic", { 1025: { value: payload, type: 0x42 } }, manufacturerOptions.lumi);
            }
            else {
                throw new Error(`lumi_curtain_options set called for not supported model: ${meta.mapped.model}`);
            }
            // Reset limits is an action, not a state.
            // biome-ignore lint/performance/noDelete: ignored using `--suppress`
            delete opts.reset_limits;
            return { state: { options: opts } };
        },
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (meta.mapped.model === "ZNCLDJ11LM") {
                await entity.read("genBasic", [0x0401], manufacturerOptions.lumi);
            }
            else {
                throw new Error(`lumi_curtain_options get called for not supported model: ${meta.mapped.model}`);
            }
        },
    },
    lumi_curtain_position_state: {
        key: ["state", "position"],
        options: [exposes.options.invert_cover()],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (key === "state" && typeof value === "string" && value.toLowerCase() === "stop") {
                if (["ZNJLBL01LM", "ZNCLDJ14LM"].includes(meta.mapped.model)) {
                    const payload = { presentValue: 2 };
                    await entity.write("genMultistateOutput", payload);
                }
                else {
                    await entity.command("closuresWindowCovering", "stop", {}, (0, utils_1.getOptions)(meta.mapped, entity));
                }
                if (!["ZNCLDJ11LM", "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.
                    // Lumi curtain does not send position update on stop, request this.
                    await entity.read("genAnalogOutput", [0x0055]);
                }
            }
            else {
                const lookup = { open: 100, close: 0, on: 100, off: 0 };
                // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
                value = typeof value === "string" ? value.toLowerCase() : value;
                if ((0, utils_1.isString)(value)) {
                    // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
                    value = (0, utils_1.getFromLookup)(value, lookup);
                }
                (0, utils_1.assertNumber)(value);
                // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
                value = meta.options.invert_cover ? 100 - value : value;
                if (["ZNCLBL01LM"].includes(meta.mapped.model)) {
                    await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: value }, (0, utils_1.getOptions)(meta.mapped, entity));
                }
                else {
                    const payload = { presentValue: value };
                    await entity.write("genAnalogOutput", payload);
                }
                return { state: { position: value } };
            }
        },
        convertGet: async (entity, key, meta) => {
            if (!Array.isArray(meta.mapped) && "ZNJLBL01LM" === meta.mapped.model) {
                // https://github.com/Koenkk/zigbee2mqtt/issues/23056
                logger_1.logger.debug(`Not reading position of ZNJLBL01LM since device doesn't support it`, NS);
            }
            else if (!Array.isArray(meta.mapped) && ["ZNCLBL01LM"].includes(meta.mapped.model)) {
                await entity.read("closuresWindowCovering", ["currentPositionLiftPercentage"]);
            }
            else {
                await entity.read("genAnalogOutput", [0x0055]);
            }
        },
    },
    lumi_curtain_battery_voltage: {
        key: ["voltage"],
        convertGet: async (entity, key, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            switch (meta.mapped.model) {
                case "ZNCLBL01LM":
                    await entity.read("manuSpecificLumi", [0x040b], manufacturerOptions.lumi);
                    break;
                default:
                    throw new Error(`lumi_curtain_battery_voltage - unsupported model: ${meta.mapped.model}`);
            }
        },
    },
    lumi_curtain_charging_status: {
        key: ["charging_status"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0409], manufacturerOptions.lumi);
        },
    },
    lumi_curtain_battery: {
        key: ["battery"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x040a], manufacturerOptions.lumi);
        },
    },
    lumi_trigger_indicator: {
        key: ["trigger_indicator"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 338: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { trigger_indicator: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0152], manufacturerOptions.lumi);
        },
    },
    lumi_curtain_hooks_lock: {
        key: ["hooks_lock"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { UNLOCK: 0, LOCK: 1 };
            await entity.write("manuSpecificLumi", { 1063: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { [key]: value } };
        },
    },
    lumi_curtain_hooks_state: {
        key: ["hooks_state"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0428], manufacturerOptions.lumi);
        },
    },
    lumi_curtain_hand_open: {
        key: ["hand_open"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { curtainHandOpen: !value }, manufacturerOptions.lumi);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", ["curtainHandOpen"], manufacturerOptions.lumi);
        },
    },
    lumi_curtain_reverse: {
        key: ["reverse_direction"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { curtainReverse: value }, manufacturerOptions.lumi);
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", ["curtainReverse"], manufacturerOptions.lumi);
        },
    },
    lumi_curtain_limits_calibration: {
        key: ["limits_calibration"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value);
            const normalizedValue = value.toLowerCase();
            switch (normalizedValue) {
                case "start":
                    await entity.write("manuSpecificLumi", { 1031: { value: 0x01, type: 0x20 } }, manufacturerOptions.lumi);
                    break;
                case "end":
                    await entity.write("manuSpecificLumi", { 1031: { value: 0x02, type: 0x20 } }, manufacturerOptions.lumi);
                    break;
                case "reset":
                    await entity.write("manuSpecificLumi", { 1031: { value: 0x00, type: 0x20 } }, manufacturerOptions.lumi);
                    // also? await entity.write('manuSpecificLumi', {0x0402: {value: 0x00, type: 0x10}}, manufacturerOptions.lumi);
                    break;
            }
        },
    },
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    lumi_curtain_limits_calibration_ZNCLDJ14LM: {
        key: ["limits_calibration"],
        options: [
            e.enum("limits_calibration", ea.ALL, ["calibrated", "recalibrate", "open", "close"]).withDescription("Recalibrate the position limits"),
        ],
        convertSet: async (entity, key, value, meta) => {
            switch (value) {
                case "recalibrate":
                    await entity.write("manuSpecificLumi", { curtainCalibrated: false }, manufacturerOptions.lumi);
                    break;
                case "open":
                    await entity.write("genMultistateOutput", { presentValue: 1 }, manufacturerOptions.lumi);
                    break;
                case "close":
                    await entity.write("genMultistateOutput", { presentValue: 0 }, manufacturerOptions.lumi);
                    break;
            }
        },
    },
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    lumi_curtain_automatic_calibration_ZNCLDJ01LM: {
        key: ["automatic_calibration"],
        convertSet: async (entity, key, value, meta) => {
            // Check if the curtain is already calibrated
            const checkIfCalibrated = async () => {
                const result = await entity.read("manuSpecificLumi", ["curtainCalibrated"]);
                return result ? result.curtainCalibrated : false;
            };
            if (await checkIfCalibrated()) {
                logger_1.logger.info("End positions already calibrated. Reset the calibration before proceeding.", NS);
                return;
            }
            // Reset Calibration
            await entity.write("manuSpecificLumi", { 1031: { value: 0x00, type: 0x20 } }, manufacturerOptions.lumi);
            logger_1.logger.info("Starting the calibration process...", NS);
            // Wait for 3 seconds
            await (0, utils_1.sleep)(3000);
            // Move the curtain to one direction
            await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: 100 }, (0, utils_1.getOptions)(meta.mapped, entity));
            logger_1.logger.info("Moving curtain and waiting to reach the end position.", NS);
            // Wait until the curtain gets into a moving state, then wait until it gets blocked or stopped
            const waitForStateTransition = async (initialStates, desiredStates) => {
                return await new Promise((resolve) => {
                    const checkState = async () => {
                        const result = await entity.read("manuSpecificLumi", [0x0421]);
                        const state = result ? result[0x0421] : null;
                        if (!initialStates.includes(state)) {
                            const checkDesiredState = async () => {
                                const result = await entity.read("manuSpecificLumi", [0x0421]);
                                const state = result ? result[0x0421] : null;
                                if (desiredStates.includes(state)) {
                                    resolve();
                                }
                                else {
                                    setTimeout(checkDesiredState, 500);
                                }
                            };
                            setTimeout(checkDesiredState, 500);
                        }
                        else {
                            setTimeout(checkState, 500);
                        }
                    };
                    void checkState();
                });
            };
            await waitForStateTransition([2, 3], [2, 3]);
            // Wait for 1 second
            await (0, utils_1.sleep)(1000);
            // Set First Calibration Position
            await entity.write("manuSpecificLumi", { 1031: { value: 0x01, type: 0x20 } }, manufacturerOptions.lumi);
            logger_1.logger.info("End position 1 has been set.", NS);
            // Wait for 3 seconds
            await (0, utils_1.sleep)(3000);
            // Move the curtain in the opposite direction
            await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: 0 }, (0, utils_1.getOptions)(meta.mapped, entity));
            logger_1.logger.info("Moving curtain in the opposite direction and waiting to reach the end position.", NS);
            // Wait until the curtain gets into a moving state, then wait until it gets blocked or stopped
            await waitForStateTransition([2, 3], [2, 3]);
            // Wait for 1 second
            await (0, utils_1.sleep)(1000);
            // Set Second Calibration Position
            await entity.write("manuSpecificLumi", { 1031: { value: 0x02, type: 0x20 } }, manufacturerOptions.lumi);
            logger_1.logger.info("End position 2 has been set.", NS);
            logger_1.logger.info("Calibration process completed.", NS);
        },
    },
    lumi_buzzer: {
        key: ["buzzer"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, key);
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            const attribute = ["JY-GZ-01AQ"].includes(meta.mapped.model) ? 0x013e : 0x013f;
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase() === "alarm" ? 15361 : 15360;
            await entity.write("manuSpecificLumi", { [`${attribute}`]: { value: [`${value}`], type: 0x23 } }, manufacturerOptions.lumi);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value === 15361 ? 0 : 1;
            await entity.write("manuSpecificLumi", { 294: { value: [`${value}`], type: 0x20 } }, manufacturerOptions.lumi);
        },
    },
    lumi_buzzer_manual: {
        key: ["buzzer_manual_alarm", "buzzer_manual_mute"],
        convertGet: async (entity, key, meta) => {
            if (key === "buzzer_manual_mute") {
                await entity.read("manuSpecificLumi", [0x0126], manufacturerOptions.lumi);
            }
            else if (key === "buzzer_manual_alarm") {
                await entity.read("manuSpecificLumi", [0x013d], manufacturerOptions.lumi);
            }
        },
    },
    lumi_heartbeat_indicator: {
        key: ["heartbeat_indicator"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 316: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { heartbeat_indicator: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x013c], manufacturerOptions.lumi);
        },
    },
    lumi_selftest: {
        key: ["selftest"],
        convertSet: async (entity, key, value, meta) => {
            if (Array.isArray(meta.mapped))
                throw new Error("Not supported for groups");
            if (["JTYJ-GD-01LM/BW", "JTQJ-BF-01LM/BW"].includes(meta.mapped.model)) {
                // Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
                const options = { ...manufacturerOptions.lumi, timeout: 35000 };
                await entity.write("ssIasZone", { 65521: { value: 0x03010000, type: 0x23 } }, options);
            }
            else {
                await entity.write("manuSpecificLumi", { 295: { value: true, type: 0x10 } }, manufacturerOptions.lumi);
            }
        },
    },
    lumi_linkage_alarm: {
        key: ["linkage_alarm"],
        convertSet: async (entity, key, value, meta) => {
            await entity.write("manuSpecificLumi", { 331: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { linkage_alarm: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x014b], manufacturerOptions.lumi);
        },
    },
    lumi_state: {
        key: ["state"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x0139], manufacturerOptions.lumi);
        },
    },
    lumi_alarm: {
        key: ["gas", "smoke"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x013a], manufacturerOptions.lumi);
        },
    },
    lumi_density: {
        key: ["gas_density", "smoke_density", "smoke_density_dbm"],
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x013b], manufacturerOptions.lumi);
        },
    },
    lumi_sensitivity: {
        key: ["sensitivity"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toLowerCase();
            const lookup = { low: 0x04010000, medium: 0x04020000, high: 0x04030000 };
            // Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
            const options = { ...manufacturerOptions.lumi, timeout: 35000 };
            await entity.write("ssIasZone", { 65521: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, options);
            return { state: { sensitivity: value } };
        },
    },
    lumi_gas_sensitivity: {
        key: ["gas_sensitivity"],
        convertSet: async (entity, key, value, meta) => {
            (0, utils_1.assertString)(value, key);
            // biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
            value = value.toUpperCase();
            const lookup = { "15%LEL": 1, "10%LEL": 2 };
            await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
            return { state: { gas_sensitivity: value } };
        },
        convertGet: async (entity, key, meta) => {
            await entity.read("manuSpecificLumi", [0x010c], manufacturerOptions.lumi);
        },
    },
    // lumi device specific
    // biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
    lumi_smart_panel_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("manuSpecificLumi", { 533: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { theme: value } };
            }
            if (key === "standby_enabled") {
                await entity.write("manuSpecificLumi", { 531: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
                return { state: { standby_enabled: value } };
            }
            if (key === "beep_volume") {
                const lookup = { mute: 0, low: 1, medium: 2, high: 3 };
                await entity.write("manuSpecificLumi", { 530: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { beep_volume: value } };
            }
            if (key === "lcd_brightness") {
                await entity.write("manuSpecificLumi", { 529: { value: value, type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { lcd_brightness: value } };
            }
            if (key === "language") {
                const lookup = { chinese: 0, english: 1 };
                await entity.write("manuSpecificLumi", { 528: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { language: value } };
            }
            if (key === "screen_saver_style") {
                const lookup = { classic: 1, "analog clock": 2 };
                await entity.write("manuSpecificLumi", { 532: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { screen_saver_style: value } };
            }
            if (key === "standby_time") {
                await entity.write("manuSpecificLumi", { 534: { value: value, type: 0x23 } }, manufacturerOptions.lumi);
                return { state: { standby_time: value } };
            }
            if (key === "font_size") {
                const lookup = { small: 3, medium: 4, large: 5 };
                await entity.write("manuSpecificLumi", { 535: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { font_size: value } };
            }
            if (key === "lcd_auto_brightness_enabled") {
                await entity.write("manuSpecificLumi", { 536: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
                return { state: { lcd_auto_brightness_enabled: value } };
            }
            if (key === "homepage") {
                const lookup = { scene: 0, feel: 1, thermostat: 2, switch: 3 };
                await entity.write("manuSpecificLumi", { 537: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { homepage: value } };
            }
            if (key === "screen_saver_enabled") {
                await entity.write("manuSpecificLumi", { 545: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
                return { state: { screen_saver_enabled: value } };
            }
            if (key === "standby_lcd_brightness") {
                await entity.write("manuSpecificLumi", { 546: { value: value, type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { standby_lcd_brightness: value } };
            }
            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("manuSpecificLumi", { 555: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
                return { state: { available_switches: value } };
            }
            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 = {};
                (0, utils_1.assertObject)(value);
                if (value.switch_1_icon != null) {
                    payload.push((0, utils_1.getFromLookup)(value.switch_1_icon, lookup));
                    statearr.switch_1_icon = value.switch_1_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_1_icon = "1";
                }
                if (value.switch_1_text != null) {
                    payload.push(...value.switch_1_text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_1_text = value.switch_1_text;
                }
                else {
                    // @ts-expect-error ignore
                    payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_1_text = "";
                }
                await entity.write("manuSpecificLumi", { 547: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
                return { state: statearr };
            }
            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 = {};
                (0, utils_1.assertObject)(value);
                if (value.switch_2_icon != null) {
                    payload.push((0, utils_1.getFromLookup)(value.switch_2_icon, lookup));
                    statearr.switch_2_icon = value.switch_2_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_2_icon = "1";
                }
                if (value.switch_2_text != null) {
                    payload.push(...value.switch_2_text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_2_text = value.switch_2_text;
                }
                else {
                    // @ts-expect-error ignore
                    payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_2_text = "";
                }
                await entity.write("manuSpecificLumi", { 548: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
                return { state: statearr };
            }
            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 = {};
                (0, utils_1.assertObject)(value);
                if (value.switch_3_icon != null) {
                    payload.push((0, utils_1.getFromLookup)(value.switch_3_icon, lookup));
                    statearr.switch_3_icon = value.switch_3_icon;
                }
                else {
                    payload.push(1);
                    statearr.switch_3_icon = "1";
                }
                if (value.switch_3_text != null) {
                    payload.push(...value.switch_3_text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_3_text = value.switch_3_text;
                }
                else {
                    // @ts-expect-error ignore
                    payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
                    statearr.switch_3_text = "";
                }
                await entity.write("manuSpecificLumi", { 549: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
                return { state: statearr };
            }
            throw new Error(`Not supported: '${key}'`);
        },
    },
};
//# sourceMappingURL=lumi.js.map