UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

2,882 lines 138 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.definitions = void 0;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const fz = __importStar(require("../converters/fromZigbee"));
const exposes = __importStar(require("../lib/exposes"));
const logger_1 = require("../lib/logger");
const m = __importStar(require("../lib/modernExtend"));
const utils = __importStar(require("../lib/utils"));
const utils_1 = require("../lib/utils");
const e = exposes.presets;
const ea = exposes.access;
const SHELLY_ENDPOINT_ID = 239;
const SHELLY_OPTIONS = { profileId: zigbee_herdsman_1.ZSpec.CUSTOM_SHELLY_PROFILE_ID };
const SHELLY_PRESENCE_MAX_ZONES = 10;
// The device reports occupancy per tracked person, one endpoint each - not per zone. Shelly's own
// definition exposes all ten unconditionally, and it has to stay that way: a second person only
// shows up on the second endpoint, so gating these on anything would hide people.
const SHELLY_PRESENCE_TARGET_ENDPOINTS = Array.from({ length: 10 }, (_, index) => String(index + 1));
const SHELLY_RPC_DATA_READ_TIMEOUT = 10000;
// The RPC cluster cannot deliver a response over Zigbee: the device acknowledges a read of the
// Data attribute but never sends the Read Attributes Response. Shelly confirmed this as a known
// firmware limitation and it is still present in firmware 2.0.0, so everything that would read
// through the RPC cluster is disabled - it would only produce empty values and a timeout per
// attempt. Flip this once a firmware answers again to restore zone discovery.
const SHELLY_RPC_CAN_READ = false;
// A device leaves the factory with one presence zone, and it carries this id.
const SHELLY_PRESENCE_FIRST_ZONE_ID = 200;
// Shelly.GetComponents answers in pages (7 components per page on a Presence Gen4); this bounds
// the paging loop so a device reporting an inconsistent total cannot spin it forever.
const SHELLY_COMPONENT_PAGE_LIMIT = 10;
// A PresenceZone reports its thresholds as presence_thr/absence_thr, while the API documents them
// as presence_delay/absence_delay. Keep the documented names - they say what the value means - for
// the exposes and translate at the RPC boundary. PresenceZone.SetConfig discards unknown keys
// without reporting an error, so a wrong name here fails silently: prefer whichever name the zone
// config actually carries and fall back to the name observed on the device.
const SHELLY_PRESENCE_ZONE_FIELDS = {
    presence_delay: "presence_thr",
    absence_delay: "absence_thr",
};
// The device reports this once the fine-tuning values no longer match one of the presets. It is
// never accepted on write, but listing it keeps the reported state a value the enum knows.
const SHELLY_PRESENCE_SENSITIVITY_CUSTOM = "custom";
// Merges a value at its path into a config object, so a whole group can be written in one command.
const shellyMergeConfig = (config, path, value) => {
    let cursor = config;
    for (const segment of path.slice(0, -1)) {
        if (typeof cursor[segment] !== "object" || cursor[segment] === null)
            cursor[segment] = {};
        cursor = cursor[segment];
    }
    cursor[path[path.length - 1]] = value;
};
const num = (name, min, max, step, label, description, unit) => {
    const expose = e
        .numeric(name, ea.STATE_SET)
        .withValueMin(min)
        .withValueMax(max)
        .withValueStep(step)
        .withLabel(label)
        .withDescription(description);
    return unit ? expose.withUnit(unit) : expose;
};
const SHELLY_PRESENCE_SETTING_GROUPS = [
    {
        key: "mounting",
        label: "Mounting",
        description: "How and where the sensor is mounted. Write-only: the device cannot report these back over Zigbee, so they show what was last set from here",
        settings: [
            {
                key: "installation_height",
                path: ["sensor", "height"],
                expose: () => num("installation_height", 0, 5, 0.1, "Installation height", "Height the sensor is mounted at", "m"),
            },
            {
                key: "sensor_position",
                path: ["sensor", "position"],
                expose: () => e
                    .enum("sensor_position", ea.STATE_SET, ["center", "left", "right"])
                    .withLabel("Sensor position")
                    .withDescription("Where the sensor is mounted on the wall"),
            },
            {
                key: "sensor_flipped",
                path: ["sensor", "flipped"],
                expose: () => e
                    .binary("sensor_flipped", ea.STATE_SET, true, false)
                    .withLabel("Sensor flipped")
                    .withDescription("Sensor is mounted rotated by 180 degrees"),
            },
        ],
    },
    {
        key: "detection",
        label: "Detection",
        description: "How the radar decides that someone is there. Write-only: the device cannot report these back over Zigbee, so they show what was last set from here",
        settings: [
            {
                key: "sensitivity",
                path: ["sensor", "sensitivity"],
                expose: () => e
                    .enum("sensitivity", ea.STATE_SET, ["low", "medium", "high", SHELLY_PRESENCE_SENSITIVITY_CUSTOM])
                    .withLabel("Sensitivity")
                    .withDescription("Detection responsiveness. Reported as 'custom' while the fine-tuning values differ from a preset"),
            },
            {
                key: "radar_power",
                path: ["sensor", "power"],
                expose: () => e
                    .enum("radar_power", ea.STATE_SET, ["low", "medium", "high"])
                    .withLabel("Radar power")
                    .withDescription("Transmit power of the radar sensor"),
            },
            {
                key: "minimum_range",
                path: ["zmin"],
                expose: () => num("minimum_range", 0, 5, 0.1, "Minimum range", "Lower detection limit", "m"),
            },
            {
                key: "maximum_range",
                path: ["zmax"],
                expose: () => num("maximum_range", 0, 5, 0.1, "Maximum range", "Upper detection limit", "m"),
            },
            {
                key: "tracked_objects",
                path: ["num_tracks"],
                expose: () => num("tracked_objects", 1, 10, 1, "Tracked objects", "How many objects the sensor tracks at once"),
            },
        ],
    },
    {
        key: "leds",
        label: "LEDs",
        description: "Brightness of the status LEDs and their night mode. Write-only: the device cannot report these back over Zigbee, so they show what was last set from here",
        settings: [
            {
                key: "brightness",
                path: ["leds", "brightness"],
                expose: () => num("brightness", 0, 100, 1, "Brightness", "Brightness of the status LEDs, 0 turns them off", "%"),
            },
            {
                key: "night_mode",
                path: ["leds", "night_mode", "enable"],
                expose: () => e
                    .binary("night_mode", ea.STATE_SET, true, false)
                    .withLabel("Night mode")
                    .withDescription("Limit the LED brightness during the configured night hours"),
            },
            {
                key: "night_mode_brightness",
                path: ["leds", "night_mode", "brightness"],
                expose: () => num("night_mode_brightness", 0, 100, 1, "Night mode brightness", "Brightness limit while night mode is active", "%"),
            },
        ],
    },
    {
        key: "tuning",
        label: "Fine-tuning",
        description: "Detection internals. Changing any of these makes the device report its sensitivity as 'custom'. Write-only: the device cannot report these back over Zigbee, so they show what was last set from here",
        settings: [
            {
                key: "detection_points",
                path: ["sensor", "points"],
                expose: () => num("detection_points", 10, 100, 1, "Detection points", "Object recognition threshold"),
            },
            {
                key: "velocity_threshold",
                path: ["sensor", "velocity"],
                expose: () => num("velocity_threshold", 0, 1, 0.01, "Velocity threshold", "Velocity threshold"),
            },
            {
                key: "snr_threshold",
                path: ["sensor", "snr"],
                expose: () => num("snr_threshold", 10, 100, 1, "SNR threshold", "Signal-to-noise threshold"),
            },
            {
                key: "maximum_velocity_difference",
                path: ["sensor", "max_velocity"],
                expose: () => num("maximum_velocity_difference", 1, 50, 1, "Maximum velocity difference", "Largest velocity difference still treated as one object"),
            },
            {
                key: "motion_activation_threshold",
                path: ["sensor", "state", "det_act_thr"],
                expose: () => num("motion_activation_threshold", 1, 100, 1, "Motion activation threshold", "How readily a detected object counts as active"),
            },
            {
                key: "motion_release_threshold",
                path: ["sensor", "state", "det_free_thr"],
                expose: () => num("motion_release_threshold", 1, 100, 1, "Motion release threshold", "How readily a detected object is released again"),
            },
            {
                key: "tracking_loss_threshold",
                path: ["sensor", "state", "act_free_thr"],
                expose: () => num("tracking_loss_threshold", 1, 1000, 1, "Tracking loss threshold", "How long an active object may be lost before it is released"),
            },
            {
                key: "stillness_tracking_threshold",
                path: ["sensor", "state", "stat_free_thr"],
                expose: () => num("stillness_tracking_threshold", 1, 1000, 1, "Stillness tracking threshold", "How long a motionless object stays tracked"),
            },
            {
                key: "stillness_timeout_threshold",
                path: ["sensor", "state", "sleep_free_thr"],
                expose: () => num("stillness_timeout_threshold", 1, 65535, 1, "Stillness timeout threshold", "How long a sleeping object stays tracked before it is released"),
            },
        ],
    },
];
const NS = "zhc:shelly";
const checkOption = (device, options, key, defaultValue = false) => {
    if (options?.[key] === "true")
        return true;
    if (options?.[key] === "false")
        return false;
    if (!utils.isDummyDevice(device) && device.meta[key] !== undefined)
        return !!device.meta[key];
    return defaultValue;
};
const shellySwitchInputEndpoints = (device, endpoints) => {
    if (utils.isDummyDevice(device))
        return endpoints;
    return Object.fromEntries(Object.entries(endpoints).filter(([, endpoint]) => device.getEndpoint(endpoint) !== undefined));
};
const shellySwitchInputExposes = (device, endpoints) => Object.keys(shellySwitchInputEndpoints(device, endpoints)).map((endpoint) => e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint(endpoint));
const shellyDeviceEndpoints = (endpoints) => ({
    meta: { multiEndpoint: true },
    endpoint: (device) => shellySwitchInputEndpoints(device, endpoints),
    isModernExtend: true,
});
const shellyPresenceEndpointNames = (device) => {
    // Without a device (documentation/expose generation) show what the hardware can do. On a real
    // device use the discovered zone count, and before discovery assume the single main zone that
    // every Presence Gen4 has: exposing all ten up front would create nine zones that never report
    // and leave them orphaned once the real count shrinks the list.
    let count = SHELLY_PRESENCE_MAX_ZONES;
    if (!utils.isDummyDevice(device)) {
        count =
            typeof device.meta.presence_zone_count === "number"
                ? Math.min(Math.max(Math.trunc(device.meta.presence_zone_count), 1), SHELLY_PRESENCE_MAX_ZONES)
                : 1;
    }
    return Array.from({ length: count }, (_, index) => String(index + 1));
};
const SHELLY_TRV_EXTERNAL_TEMPERATURE_META_KEY = "shelly_trv_external_temperature_request";
const SHELLY_TRV_EXTERNAL_TEMPERATURE_MIN_RETRY_INTERVAL_MS = 1500;
const SHELLY_TRV_EXTERNAL_TEMPERATURE_MAX_ATTEMPTS = 30;
const SHELLY_TRV_EXTERNAL_TEMPERATURE_MAX_AGE_MS = 60 * 60 * 1000;
// Since RPC messages require multiple writes to complete, requests to the SAME device must not
// interleave. Different devices are independent though - a network with many Gen4 devices must
// not queue every RPC transaction behind one global lock.
const shellyRpcBusy = new Set();
const shellyRpcLock = async (endpoint, callback) => {
    const key = utils.isEndpoint(endpoint) ? endpoint.getDevice().ieeeAddr : "group";
    while (shellyRpcBusy.has(key)) {
        await (0, utils_1.sleep)(200);
    }
    shellyRpcBusy.add(key);
    try {
        return await callback();
    }
    finally {
        shellyRpcBusy.delete(key);
    }
};
const shellyRpcSendRawUnlocked = async (endpoint, message) => {
    const splitBytes = 40;
    logger_1.logger.debug(">>> shellyRPC write TxCtl", NS);
    const txCtl = message.length;
    await endpoint.write("shellyRPCCluster", { txCtl: txCtl }, SHELLY_OPTIONS);
    logger_1.logger.debug(`>>> TxCtl: ${txCtl}`, NS);
    logger_1.logger.debug(">>> shellyRPC write Data", NS);
    let dataToSend = message;
    while (dataToSend.length > 0) {
        const data = dataToSend.substring(0, splitBytes);
        dataToSend = dataToSend.substring(splitBytes);
        await endpoint.write("shellyRPCCluster", { data: data }, SHELLY_OPTIONS);
        logger_1.logger.debug(`>>> Data: ${data}`, NS);
    }
};
const shellyRpcSendRaw = async (endpoint, message) => shellyRpcLock(endpoint, async () => await shellyRpcSendRawUnlocked(endpoint, message));
const shellyRpcSend = async (endpoint, method, params = undefined) => {
    const command = {
        id: 1,
        method: method,
        params: params,
    };
    return await shellyRpcSendRaw(endpoint, JSON.stringify(command));
};
const shellyRpcRequest = async (endpoint, method, params = undefined) => {
    return await shellyRpcLock(endpoint, async () => {
        const command = {
            id: 1,
            method: method,
            params: params,
        };
        await shellyRpcSendRawUnlocked(endpoint, JSON.stringify(command));
        const rxCtlResult = await endpoint.read("shellyRPCCluster", ["rxCtl"], SHELLY_OPTIONS);
        const expectedLen = rxCtlResult.rxCtl;
        if (!expectedLen)
            return undefined;
        let accumulated = "";
        while (accumulated.length < expectedLen) {
            const dataResult = await endpoint.read("shellyRPCCluster", ["data"], {
                ...SHELLY_OPTIONS,
                timeout: SHELLY_RPC_DATA_READ_TIMEOUT,
            });
            if (!dataResult.data)
                break;
            accumulated += dataResult.data;
        }
        if (accumulated.length < expectedLen)
            return undefined;
        return JSON.parse(accumulated.substring(0, expectedLen));
    });
};
/**
 * Get or initialize WS90 meta storage on device
 */
function getWS90Meta(device) {
    if (!device.meta.ws90) {
        device.meta.ws90 = {};
    }
    return device.meta.ws90;
}
/**
 * Calculate dew point using Magnus formula
 */
function calculateDewPoint(T, Rh) {
    if (T === undefined || Rh === undefined || Rh <= 0)
        return null;
    const a = 17.27;
    const b = 237.7;
    const alpha = (a * T) / (b + T) + Math.log(Rh / 100);
    return Math.round(((b * alpha) / (a - alpha)) * 10) / 10;
}
/**
 * Calculate humidex (Canadian heat index)
 */
function calculateHumidex(T, Rh) {
    if (T === undefined || Rh === undefined)
        return null;
    const dewPoint = calculateDewPoint(T, Rh);
    if (dewPoint === null)
        return null;
    const ee = 6.11 * Math.exp(5417.753 * (1 / 273.15 - 1 / (273.15 + dewPoint)));
    return Math.round((T + 0.5555 * (ee - 10)) * 10) / 10;
}
/**
 * Calculate wind chill (formula valid for T <= 10°C and wind >= 4.8 km/h)
 */
function calculateWindChill(T, windMs) {
    if (T === undefined || windMs === undefined)
        return null;
    const windKmh = windMs * 3.6;
    if (T > 10 || windKmh < 4.8)
        return Math.round(T * 10) / 10;
    const wc = 13.12 + 0.6215 * T - 11.37 * windKmh ** 0.16 + 0.3965 * T * windKmh ** 0.16;
    return Math.round(wc * 10) / 10;
}
/**
 * Calculate heat stress percentage using sigmoid curve
 */
function calculateHeatStress(T, Rh, lux, windMs, precipitation) {
    if (T === undefined)
        return null;
    const solar = (lux || 0) / 100;
    const base = T + solar / 100 + (Rh || 0) / 10;
    const cooled = base - (windMs || 0) / 2;
    const adjusted = cooled - ((precipitation || 0) > 0 ? 3 : 0);
    const scaled = (adjusted - 18) / (42 - 18);
    const sigmoid = 1 / (1 + Math.E ** (-4 * (scaled - 0.5)));
    return Math.max(Math.round(sigmoid * 100), 0);
}
/**
 * Calculate apparent temperature (wind chill when cold, humidex when warm)
 */
function calculateApparentTemperature(T, Rh, windMs) {
    if (T === undefined)
        return null;
    const windChill = calculateWindChill(T, windMs);
    const humidex = calculateHumidex(T, Rh);
    if (windChill !== null && windChill < T)
        return windChill;
    if (humidex !== null && humidex > T)
        return humidex;
    return Math.round(T * 10) / 10;
}
/**
 * Calculate rain rate from precipitation changes (mm/h)
 */
function calculateRainRate(meta, precipitation) {
    if (precipitation === undefined)
        return null;
    const now = Date.now();
    const history = meta.precipHistory;
    if (!history) {
        meta.precipHistory = { value: precipitation, time: now };
        return 0;
    }
    const timeDeltaMs = now - history.time;
    const precipDelta = precipitation - history.value;
    if (timeDeltaMs < 60000)
        return null;
    if (precipDelta < 0) {
        // The cumulative counter was reset (battery change, restart). Rebase the history on the
        // new counter - otherwise the delta stays negative and the rate reports 0 until the new
        // counter overtakes the old value, which for a cumulative rain counter can take months.
        meta.precipHistory = { value: precipitation, time: now };
        return 0;
    }
    meta.precipHistory = { value: precipitation, time: now };
    const timeDeltaHours = timeDeltaMs / (1000 * 60 * 60);
    const rate = precipDelta / timeDeltaHours;
    return Math.min(Math.round(rate * 10) / 10, 300);
}
/**
 * Calculate pressure trend (hPa/hour)
 */
function calculatePressureTrend(meta, pressure) {
    if (pressure === undefined)
        return null;
    const now = Date.now();
    const history = meta.pressureHistory;
    if (!history) {
        meta.pressureHistory = { value: pressure, time: now };
        return 0;
    }
    const timeDeltaMs = now - history.time;
    const pressureDelta = pressure - history.value;
    if (timeDeltaMs < 1800000)
        return null;
    meta.pressureHistory = { value: pressure, time: now };
    const timeDeltaHours = timeDeltaMs / (1000 * 60 * 60);
    const rate = pressureDelta / timeDeltaHours;
    return Math.round(rate * 10) / 10;
}
/**
 * Determine weather condition based on sensor data
 */
function calculateWeatherCondition(state) {
    const { temperature, illuminance, rain_status, wind_speed, rain_rate, pressure, pressure_trend } = state;
    if (illuminance === undefined)
        return null;
    const isRaining = rain_status === true && rain_rate !== undefined && rain_rate > 0;
    const isPouring = isRaining && rain_rate > 10;
    const isWindy = wind_speed !== undefined && wind_speed > 10;
    const isNight = illuminance < 10;
    const isLowPressure = pressure !== undefined && pressure < 1000;
    const isPressureFalling = pressure_trend !== undefined && pressure_trend < -2;
    const isHail = isRaining &&
        rain_rate > 5 &&
        illuminance < 5000 &&
        wind_speed !== undefined &&
        wind_speed > 5 &&
        (isLowPressure || isPressureFalling);
    const isSnowing = isRaining && temperature !== undefined && temperature < 1 && !isHail;
    if (isHail)
        return "hail";
    if (isSnowing)
        return "snowy";
    if (isPouring)
        return "pouring";
    if (isRaining)
        return "rainy";
    if (isNight) {
        return isWindy ? "windy" : "clear-night";
    }
    if (illuminance > 40000) {
        return isWindy ? "windy" : "sunny";
    }
    if (illuminance > 10000) {
        return isWindy ? "windy-variant" : "partlycloudy";
    }
    return "cloudy";
}
/**
 * The station reports the ZCL non-value marker (all bits set for the attribute type) when a
 * reading is unavailable - a gustSpeed of 0xffff was published as 6553.5 m/s and fed into the
 * calculations (https://github.com/Koenkk/zigbee2mqtt/issues/31048). Scale a raw value only
 * when it is a real reading, otherwise contribute nothing.
 */
const ws90Scaled = (name, raw, invalid) => typeof raw === "number" && raw !== invalid ? { [name]: raw / 10 } : undefined;
/**
 * Update calculated values whenever we get new sensor data (uses device.meta for persistence)
 */
function updateWS90CalculatedValues(device, payload) {
    const meta = getWS90Meta(device);
    if (!meta.state)
        meta.state = {};
    Object.assign(meta.state, payload);
    const state = meta.state;
    const result = {};
    const temp = state.temperature;
    const humidity = state.humidity;
    const windSpeed = state.wind_speed;
    const lux = state.illuminance;
    const precip = state.precipitation;
    const pressure = state.pressure;
    if (temp !== undefined && humidity !== undefined) {
        const dewPoint = calculateDewPoint(temp, humidity);
        if (dewPoint !== null)
            result.dew_point = dewPoint;
        const humidex = calculateHumidex(temp, humidity);
        if (humidex !== null)
            result.humidex = humidex;
        const heatStress = calculateHeatStress(temp, humidity, lux, windSpeed, precip);
        if (heatStress !== null)
            result.heat_stress = heatStress;
    }
    if (temp !== undefined && windSpeed !== undefined) {
        const windChill = calculateWindChill(temp, windSpeed);
        if (windChill !== null)
            result.wind_chill = windChill;
    }
    if (temp !== undefined) {
        const apparent = calculateApparentTemperature(temp, humidity, windSpeed);
        if (apparent !== null)
            result.apparent_temperature = apparent;
    }
    if (pressure !== undefined) {
        const trend = calculatePressureTrend(meta, pressure);
        if (trend !== null) {
            result.pressure_trend = trend;
            state.pressure_trend = trend;
        }
        else if (typeof state.pressure_trend === "number") {
            result.pressure_trend = state.pressure_trend;
        }
    }
    const condition = calculateWeatherCondition(state);
    if (condition !== null)
        result.weather_condition = condition;
    // Persist across restarts - but not on every report: the station reports as often as
    // every 10 seconds, and each save writes the whole device database to disk. The histories
    // only advance on the minute scale, so a crash loses at most a minute of history progress.
    const now = Date.now();
    if (meta.lastSave === undefined || now - meta.lastSave >= 60000) {
        meta.lastSave = now;
        device.save();
    }
    return result;
}
// =============================================================================
// Shelly Modern Extend
// =============================================================================
const shellyModernExtend = {
    shellyCustomClusters() {
        return [
            m.deviceAddCustomCluster("shellyRPCCluster", {
                name: "shellyRPCCluster",
                ID: 0xfc01,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    data: { name: "data", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    txCtl: { name: "txCtl", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff },
                    rxCtl: { name: "rxCtl", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.UINT32, write: true, max: 0xffffffff },
                },
                commands: {},
                commandsResponse: {},
            }),
            m.deviceAddCustomCluster("shellyWiFiSetupCluster", {
                name: "shellyWiFiSetupCluster",
                ID: 0xfc02,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    status: { name: "status", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    ip: { name: "ip", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    actionCode: { name: "actionCode", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff },
                    dhcp: { name: "dhcp", ID: 0x0003, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true },
                    enabled: { name: "enabled", ID: 0x0004, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN, write: true },
                    ssid: { name: "ssid", ID: 0x0005, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    password: { name: "password", ID: 0x0006, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    staticIp: { name: "staticIp", ID: 0x0007, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    netMask: { name: "netMask", ID: 0x0008, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    gateway: { name: "gateway", ID: 0x0009, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                    nameServer: { name: "nameServer", ID: 0x000a, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
                },
                commands: {},
                commandsResponse: {},
            }),
        ];
    },
    shellyWindowCovering() {
        const result = m.windowCovering({ controls: ["lift", "tilt"] });
        const tiltOption = e
            .enum("cover_tilt_enabled", ea.SET, ["auto", "true", "false"])
            .withDescription("Expose tilt/slat controls for covers with Shelly slat control enabled");
        const exposesFn = (device, options) => {
            const cover = e.cover().withPosition();
            if (checkOption(device, options, "cover_tilt_enabled", true)) {
                cover.withTilt();
            }
            return [cover];
        };
        result.exposes = [exposesFn];
        result.options = [...(result.options ?? []), tiltOption];
        return result;
    },
    shellyPresenceOccupancy() {
        const exposesFn = () => {
            return SHELLY_PRESENCE_TARGET_ENDPOINTS.map((endpointName) => e.occupancy().withAccess(ea.STATE_GET).withEndpoint(endpointName));
        };
        const fromZigbee = [
            {
                cluster: "msOccupancySensing",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (!("occupancy" in msg.data))
                        return;
                    const endpointName = utils.getEndpointName(msg, model, meta).toString();
                    if (!SHELLY_PRESENCE_TARGET_ENDPOINTS.includes(endpointName))
                        return;
                    return { [utils.postfixWithEndpointName("occupancy", msg, model, meta)]: (msg.data.occupancy & 1) > 0 };
                },
            },
        ];
        const toZigbee = [
            {
                key: ["occupancy"],
                convertGet: async (entity, key, meta) => {
                    await (0, utils_1.determineEndpoint)(entity, meta, "msOccupancySensing").read("msOccupancySensing", ["occupancy"]);
                },
            },
        ];
        // The presence sensor only pushes occupancy autonomously once reporting is configured on
        // the msOccupancySensing cluster of each target endpoint; without this it just answers reads.
        // Every tracked person reports on its own endpoint, so all of them need reporting - a second
        // person would otherwise never arrive. A single endpoint failing must not abort the rest.
        const configure = [
            async (device, coordinatorEndpoint) => {
                for (const endpointName of SHELLY_PRESENCE_TARGET_ENDPOINTS) {
                    const endpoint = device.getEndpoint(Number(endpointName));
                    if (!endpoint)
                        continue;
                    try {
                        await m.setupAttributes(endpoint, coordinatorEndpoint, "msOccupancySensing", [{ attribute: "occupancy", min: "MIN", max: "1_HOUR", change: 0 }], true, false);
                    }
                    catch (e) {
                        logger_1.logger.warning(`Failed to set up occupancy reporting on endpoint ${endpointName}: ${e}`, NS);
                    }
                }
            },
        ];
        return { exposes: [exposesFn], fromZigbee, toZigbee, configure, isModernExtend: true };
    },
    shellyRPCSetup(features = []) {
        // Set helper variables
        const shellyRPCBugFixed = false; // For firmware 20250819-150402/ga0def2d
        const featureDev = features.includes("Dev");
        const featurePowerstripUI = features.includes("PowerstripUI");
        const featurePowerstripPowerOnBehavior = features.includes("PowerstripPowerOnBehavior");
        // The two 2PM variants put their switch inputs on different endpoints: the cover on 2 and 3,
        // the switch-mode device on 3 and 4, because there 1 and 2 are its two relays. A single
        // shared mapping cannot serve both - it would read the second relay as the first input.
        const twoPMInputEndpoints = features.includes("2PMCoverInputMode")
            ? { sw1: 2, sw2: 3 }
            : features.includes("2PMSwitchInputMode")
                ? { sw1: 3, sw2: 4 }
                : undefined;
        const featureOnePMInputMode = features.includes("1PMInputMode");
        const featureCoverTiltAuto = features.includes("CoverTiltAuto");
        const featurePresenceZonesAuto = features.includes("PresenceZonesAuto");
        const featurePresenceZoneConfig = features.includes("PresenceZoneConfig");
        const featurePresenceSensorConfig = features.includes("PresenceSensorConfig");
        const featureEcoMode = features.includes("EcoMode");
        // Generic helper functions
        const validateTime = (value) => {
            const hhmmRegex = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
            if (value === undefined || !value.match(hhmmRegex)) {
                throw new Error(`Invalid time "${value}"`);
            }
        };
        const rpcSendRaw = shellyRpcSendRaw;
        const rpcSend = shellyRpcSend;
        const rpcRequest = shellyRpcRequest;
        // Writes reach the device but a single delivery can still fail, and the presence sensor has
        // no fallback ZCL cluster to fall back on - so a write is retried a few times with a short
        // pause. Reads are a different matter: the firmware never answers one (see
        // SHELLY_RPC_CAN_READ), so retrying a read cannot help. The read variant below stays only
        // for the zone discovery it serves, which is dormant for the same reason.
        const RPC_MAX_ATTEMPTS = 3;
        const RPC_RETRY_PAUSE = 1500;
        const rpcRequestWithRetry = async (endpoint, method, params) => {
            for (let attempt = 0; attempt < RPC_MAX_ATTEMPTS; attempt++) {
                if (attempt > 0)
                    await new Promise((resolve) => setTimeout(resolve, RPC_RETRY_PAUSE));
                const response = await rpcRequest(endpoint, method, params);
                const result = response?.result ?? response?.params ?? response;
                if (result) {
                    (0, utils_1.assertObject)(result);
                    return result;
                }
            }
            return undefined;
        };
        const rpcSendWithRetry = async (endpoint, method, params) => {
            let lastError;
            for (let attempt = 0; attempt < RPC_MAX_ATTEMPTS; attempt++) {
                if (attempt > 0)
                    await new Promise((resolve) => setTimeout(resolve, RPC_RETRY_PAUSE));
                try {
                    await rpcSend(endpoint, method, params);
                    return;
                }
                catch (e) {
                    lastError = e;
                }
            }
            throw lastError;
        };
        const rpcReceive = async (endpoint, key) => {
            logger_1.logger.debug(`||| shellyRPC rpcReceive(${key})`, NS);
            if (key === "rpc_rxctl") {
                logger_1.logger.debug(">>> shellyRPC read RxCtl", NS);
                const result = await endpoint.read("shellyRPCCluster", ["rxCtl"], SHELLY_OPTIONS);
                logger_1.logger.debug(`<<< RxCtl: ${JSON.stringify(result)}`, NS);
            }
            else if (key === "rpc_data") {
                logger_1.logger.debug(">>> shellyRPC read Data", NS);
                const result = await endpoint.read("shellyRPCCluster", ["data"], { ...SHELLY_OPTIONS, timeout: 1000 });
                logger_1.logger.debug(`<<< Data: ${JSON.stringify(result)}`, NS);
            }
        };
        const getRPCEndpoint = (entity) => {
            utils.assertEndpoint(entity);
            const endpoint = entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
            if (!endpoint)
                throw new Error(`Shelly RPC endpoint ${SHELLY_ENDPOINT_ID} not found`);
            return endpoint;
        };
        // Maps a per-zone endpoint name ("1".."N") to the device's RPC PresenceZone id. This
        // numbering counts zones and is unrelated to the occupancy endpoints, which count tracked
        // people - presence_delay_1 is the first zone, occupancy_1 is the first person.
        // Discovery caches the real ids in device.meta, but it needs an RPC read, which the
        // firmware cannot answer over Zigbee (see SHELLY_RPC_CAN_READ). A device leaves the factory
        // with a single zone, so fall back to the id the main zone carries - that keeps the one
        // zone every device has configurable.
        const presenceZoneIdForEndpoint = (device, endpointName) => {
            const index = Number(endpointName) - 1;
            if (!Number.isInteger(index) || index < 0)
                return undefined;
            const ids = device.meta.presence_zone_ids;
            if (Array.isArray(ids) && typeof ids[index] === "number")
                return ids[index];
            return SHELLY_PRESENCE_FIRST_ZONE_ID + index;
        };
        // Features for exposes
        const featurePercentage = (name, label) => {
            return e.numeric(name, ea.STATE_SET).withValueMin(0).withValueMax(100).withValueStep(1).withLabel(label).withUnit("%");
        };
        const featureButtonEnabled = (id) => {
            return e.binary(`switch_${id}`, ea.STATE_SET, "momentary", "detached").withLabel(`Endpoint: ${id + 1}`);
        };
        const exposes = [];
        const exposesDev = [
            e
                .text("rpc_tx", ea.STATE_SET)
                .withLabel("TX Data")
                .withDescription("See https://shelly-api-docs.shelly.cloud/gen2/Devices/Gen4/ShellyPowerStripG4"),
            e.text("rpc_rxctl", ea.STATE_GET).withLabel("RxCtl").withDescription("RX bytes available").withCategory("diagnostic"),
            e.text("rpc_data", ea.STATE_GET).withLabel("Data").withDescription("RX Data").withCategory("diagnostic"),
        ];
        // The device applies these but cannot report any of them back over Zigbee, so what a value
        // shows is what was last written from here - never a reading off the device. Say so on every
        // one of them: a configuration field that quietly means something else than the user assumes
        // is worse than no field at all.
        const WRITE_ONLY = "The device cannot report this back over Zigbee, so it shows what was last set from here";
        const exposesPowerstripUI = [
            e
                .enum("led_mode", ea.STATE_SET, ["off", "switch", "power"])
                .withLabel("LED Mode")
                .withDescription(`Controls the behaviour of the LED rings around the sockets. ${WRITE_ONLY}`)
                .withCategory("config"),
            e
                .composite("led_colors", "led_colors", ea.STATE_SET)
                .withFeature(featurePercentage("on_r", "Red (on)"))
                .withFeature(featurePercentage("on_g", "Green (on)"))
                .withFeature(featurePercentage("on_b", "Blue (on)"))
                .withFeature(featurePercentage("on_brightness", "Brightness (on)"))
                .withFeature(featurePercentage("off_r", "Red (off)"))
                .withFeature(featurePercentage("off_g", "Green (off)"))
                .withFeature(featurePercentage("off_b", "Blue (off)"))
                .withFeature(featurePercentage("off_brightness", "Brightness (off)"))
                .withLabel("LED colors in 'switch' mode")
                .withDescription(`Colors of the LED rings while the LED mode is 'switch'. ${WRITE_ONLY}`)
                .withCategory("config"),
            featurePercentage("led_power_brightness", "LED brightness in 'power' mode")
                .withDescription(`Brightness of the LED rings while the LED mode is 'power'. ${WRITE_ONLY}`)
                .withCategory("config"),
            e
                .composite("led_night_mode", "led_night_mode", ea.STATE_SET)
                .withFeature(e.binary("enable", ea.STATE_SET, true, false))
                .withFeature(featurePercentage("brightness", "Brightness"))
                .withFeature(e.text("from", ea.STATE_SET).withLabel("Active from").withDescription("hh:mm"))
                .withFeature(e.text("until", ea.STATE_SET).withLabel("Active until").withDescription("hh:mm"))
                .withLabel("LED night mode")
                .withDescription(`Adjust LED brightness during night time. ${WRITE_ONLY}`)
                .withCategory("config"),
            e
                .composite("buttons_enabled", "buttons_enabled", ea.STATE_SET)
                .withFeature(featureButtonEnabled(0))
                .withFeature(featureButtonEnabled(1))
                .withFeature(featureButtonEnabled(2))
                .withFeature(featureButtonEnabled(3))
                .withLabel("Buttons enabled")
                .withDescription(`Whether each socket button switches its own socket or is detached. ${WRITE_ONLY}`)
                .withCategory("config"),
        ];
        // The RPC receive accumulator only serves the Dev diagnostics: nothing but their reads can
        // produce shellyRPCCluster responses (the firmware answers no RPC read, see
        // SHELLY_RPC_CAN_READ), and registering it unconditionally would publish the undeclared
        // rpc_rxctl/rpc_data state keys on devices that expose no such fields.
        const fromZigbee = [];
        const fromZigbeeDev = [
            {
                cluster: "shellyRPCCluster",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const state = {};
                    // Diagnostic data
                    if (msg.data.rxCtl !== undefined) {
                        state.rpc_rxctl = msg.data.rxCtl;
                        state.rpc_data = "";
                    }
                    if (msg.data.data !== undefined) {
                        const accumulated = (meta.state.rpc_data ?? "") + msg.data.data;
                        state.rpc_data = accumulated;
                        const expectedLen = meta.state.rpc_rxctl;
                        if (expectedLen > 0 && accumulated.length >= expectedLen) {
                            try {
                                const response = JSON.parse(accumulated);
                                if (response.result?.in_mode !== undefined) {
                                    const epName = response.result.id === 0 ? "sw1" : "sw2";
                                    state[`switch_mode_${epName}`] = response.result.in_mode;
                                }
                            }
                            catch { }
                        }
                    }
                    return state;
                },
            },
        ];
        const toZigbee = [];
        const configure = [];
        const toZigbeeDev = [
            {
                key: ["rpc_rxctl", "rpc_data"],
                convertGet: async (entity, key, meta) => {
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcReceive(ep, key);
                },
            },
            {
                key: ["rpc_tx"],
                convertSet: async (entity, key, value, meta) => {
                    logger_1.logger.debug(`>>> toZigbee.convertSet(${key}): ${value}`, NS);
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSendRaw(ep, value);
                    await rpcReceive(ep, "rpc_rxctl");
                    if (shellyRPCBugFixed) {
                        await rpcReceive(ep, "rpc_data");
                    }
                    else {
                        return { state: { rpc_data: "[Refresh for response]" } };
                    }
                },
            },
        ];
        const toZigbeePowerstripUI = [
            {
                key: ["led_mode"],
                convertSet: async (entity, key, value, meta) => {
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSend(ep, "POWERSTRIP_UI.SetConfig", {
                        config: {
                            leds: {
                                mode: value,
                            },
                        },
                    });
                },
            },
            {
                key: ["led_colors"],
                convertSet: async (entity, key, value, meta) => {
                    (0, utils_1.assertObject)(value);
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSend(ep, "POWERSTRIP_UI.SetConfig", {
                        config: {
                            leds: {
                                colors: {
                                    "switch:0": {
                                        on: {
                                            rgb: [value.on_r ?? 0, value.on_g ?? 0, value.on_b ?? 0],
                                            brightness: value.on_brightness ?? 0,
                                        },
                                        off: {
                                            rgb: [value.off_r ?? 0, value.off_g ?? 0, value.off_b ?? 0],
                                            brightness: value.off_brightness ?? 0,
                                        },
                                    },
                                },
                            },
                        },
                    });
                },
            },
            {
                key: ["led_power_brightness"],
                convertSet: async (entity, key, value, meta) => {
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSend(ep, "POWERSTRIP_UI.SetConfig", {
                        config: {
                            leds: {
                                colors: {
                                    power: {
                                        brightness: value ?? 0,
                                    },
                                },
                            },
                        },
                    });
                },
            },
            {
                key: ["led_night_mode"],
                convertSet: async (entity, key, value, meta) => {
                    (0, utils_1.assertObject)(value);
                    validateTime(value.from);
                    validateTime(value.until);
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSend(ep, "POWERSTRIP_UI.SetConfig", {
                        config: {
                            leds: {
                                night_mode: {
                                    enable: value.enable,
                                    brightness: value.brightness,
                                    active_between: [value.from, value.until],
                                },
                            },
                        },
                    });
                },
            },
            {
                key: ["buttons_enabled"],
                convertSet: async (entity, key, value, meta) => {
                    (0, utils_1.assertObject)(value);
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyRPCCluster");
                    await rpcSend(ep, "POWERSTRIP_UI.SetConfig", {
                        config: {
                            controls: {
                                "switch:0": {
                                    in_mode: value.switch_0,
                                },
                                "switch:1": {
                                    in_mode: value.switch_1,
                                },
                                "switch:2": {
                                    in_mode: value.switch_2,
                                },
                                "switch:3": {
                                    in_mode: value.switch_3,
                                },
                            },
                        },
                    });
                },
            },
        ];
        if (featureDev) {
            exposes.push(...exposesDev);
            fromZigbee.push(...fromZigbeeDev);
            toZigbee.push(...toZigbeeDev);
        }
        if (featurePowerstripUI) {
            exposes.push(...exposesPowerstripUI);
            toZigbee.push(...toZigbeePowerstripUI);
        }
        if (featurePowerstripPowerOnBehavior) {
            const powerOnBehaviorValues = ["off", "on", "previous", "match_input"];
            for (const channel of [1, 2, 3, 4]) {
                exposes.push(e
                    .enum("power_on_behavior", ea.STATE_SET, powerOnBehaviorValues)
                    .withDescription("Behavior of the socket after a power outage. 'previous' restores the last known state.")
                    .withCategory("config")
                    .withEndpoint(String(channel)));
            }
            toZigbee.push({
                key: ["power_on_behavior"],
                convertSet: async (entity, key, value, meta) => {
                    utils.assertString(value, key);
                    utils.assertString(meta.endpoint_name, "endpoint_name");
                    utils.assertEndpoint(entity);
                    const switchId = Number(meta.endpoint_name) - 1;
                    // shellyRPCCluster lives on a dedicated endpoint (239), but this expose is per-channel.
                    // determineEndpoint() would return the per-channel endpoint when endpoint_name is set,
                    // so we explicitly resolve the RPC endpoint via the device.
                    const ep = entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
                    if (!ep)
                        throw new Error(`Shelly RPC endpoint ${SHELLY_ENDPOINT_ID} not found`);
                    const shellyValue = value === "previous" ? "restore_last" : value;
                    await rpcSend(ep, "Switch.SetConfig", { id: switchId, config: { initial_state: shellyValue } });
                    return { state: { power_on_behavior: value } };
                },
            });
        }
        // switch_mode travels over the RPC cluster, which the firmware cannot answer over Zigbee
        // (see SHELLY_RPC_CAN_READ): no convertGet - a device query walks every converter that has
        // one regardless of the access flags - and the exposes say so instead of announcing GET.
        if (twoPMInputEndpoints) {
            const inModeValues = ["follow", "flip", "detached", "cycle", "activation"];
            exposes.push((device, _options) => {
                if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID))
                    return [];
                return Object.keys(shellySwitchInputEndpoints(device, twoPMInputEndpoints)).map((endpoint) => e
                    .enum("switch_mode", ea.STATE_SET, inModeValues)
                    .withDescription(`Switch input mode. ${WRITE_ONLY}`)
                    .withCategory("config")
                    .withEndpoint(endpoint));
            });
            toZigbee.push({
                key: ["switch_mode"],
                convertSet: async (entity, key, value, meta) => {
                    const switchId = meta.endpoint_name === "sw1" ? 0 : 1;
                    const ep = getRPCEndpoint(entity);
                    await rpcSend(ep, "Switch.SetConfig", { id: switchId, config: { in_mode: value } });
                    return { state: { switch_mode: value } };
                },
            });
        }
        if (featureOnePMInputMode) {
            const inModeValues = ["follow", "flip", "detached", "cycle", "activation"];
            exposes.push((device, _options) => {
                if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID))
                    return [];
                return Object.keys(shellySwitchInputEndpoints(device, { sw1: 2 })).map((endpoint) => e
                    .enum("switch_mode", ea.STATE_SET, inModeValues)
                    .withDescription(`Switch input mode. ${WRITE_ONLY}`)
                    .withCategory("config")
                    .withEndpoint(endpoint));
            });
            toZigbee.push({
                key: ["switch_mode"],
                convertSet: async (entity, key, value, meta) => {
                    const ep = getRPCEndpoint(entity);
                    await rpcSend(ep, "Switch.SetConfig", { id: 0, config: { in_mode: value } });
                    return { state: { switch_mode: value } };
                },
            });
        }
        if (featureCoverTiltAuto && SHELLY_RPC_CAN_READ) {
            configure.push(async (device) => {
                const ep = device.getEndpoint(SHELLY_ENDPOINT_ID);
                if (!ep)
                    return;
                try {
                    const response = await rpcRequest(ep, "Cover.GetConfig", { id: 0 });
                    const result = response?.result ?? response?.params ?? response;
                    if (!result)
                        return;
                    (0, utils_1.assertObject)(result);
                    if (!result.slat)
                        return;
                    (0, utils_1.assertObject)(result.slat);
                    const enabled = result.slat.enable;
                    if (typeof enabled === "boolean" && device.meta.cover_tilt_enabled !== enabled) {
                        device.meta.cover_tilt_enabled = enabled;
                        device.save();
                    }
                }
                catch (e) {
                    logger_1.logger.warning(`Failed to read cover_tilt_enabled during configure, use manual option to override: ${e}`, NS);
                }
            });
        }
        if (featurePresenceZonesAuto && SHELLY_RPC_CAN_READ) {
            configure.push(async (device) => {
                const ep = device.getEndpoint(SHELLY_ENDPOINT_ID);
                if (!ep)
                    return;
                try {
                    // PresenceZones are dynamic components: Shelly.GetConfig never lists them, only
                    // Shelly.GetComponents does. `dynamic_only` keeps the answer short, but it also
                    // covers virtual components, so filter for the presencezone prefix. The answer
                    // is paginated, so keep asking until every announced component has been seen.
                    const zoneIds = [];
                    let offset = 0;
                    let total = Number.POSITIVE_INFINITY;
                    for (let page = 0; page < SHELLY_COMPONENT_PAGE_LIMIT && offset < total; page++) {
                        const result = await rpcRequestWithRetry(ep, "Shelly.GetComponents", { dynamic_only: true, offset });
                        if (!result)
                            return;
                        const components = result.components;
                        if (!Array.isArray(components) || components.length === 0)
                            break;
                        for (const component of components) {
                            const key = component?.key;
                            const match = typeof key === "string" ? /^presencezone:(\d+)$/.exec(key) : null;
                            if (match)
                                zoneIds.push(Number(match[1]));
                        }
                        total = typeof result.total === "number" ? result.total : components.length;
                        offset += components.length;
                    }
                    if (offset < total) {
                        logger_1.logger.warning(`Stopped reading presence components after ${offset} of ${total}; some zones may be missing`, NS);
                    }
                    zoneIds.sort((a, b) => a - b);
                    const zoneCount = zoneIds.length;
                    if (zoneCount < 1 || zoneCount > SHELLY_PRESENCE_MAX_ZONES)
                        return;
                    let metaChanged = false;
                    if (device.meta.presence_zone_count !== zoneCount) {
                        device.meta.presence_zone_count = zoneCount;
                        metaChanged = true;
                    }
                    if (JSON.stringify(device.meta.presence_zone_ids) !== JSON.stringify(zoneIds)) {
                        device.meta.presence_zone_ids = zoneIds;
                        metaChanged = true;
                    }
                    if (metaChanged)
                        device.save();
                }
                catch (e) {
                    logger_1.logger.warning(`Failed to read presence zones during configure, using last known or default exposed zones: ${e}`, NS);
                }
            });
        }
        if (featurePresenceZoneConfig) {
            const presenceZoneKeys = ["presence_delay", "absence_delay"];
            exposes.push((device, _options) => {
                if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID))
                    return [];
                return shellyPresenceEndpointNames(device).flatMap((endpointName) => [
                    e
                        .numeric("presence_delay", ea.STATE_SET)
                        .withValueMin(0)
                        .withValueMax(3600)
                        .withValueStep(1)
                        .withUnit("s")
                        .withLabel("Presence delay")
                        .withDescription("Time presence must be continuously detected before this zone is reported occupied. " +
                        "The device cannot report this value back over Zigbee, so it shows what was last set")
                        .withCategory("config")
                        .withEndpoint(endpointName),
                    e
                        .numeric("absence_delay", ea.STATE_SET)
                        .withValueMin(0)
                        .withValueMax(3600)
                        .withValueStep(1)
                        .withUnit("s")
                        .withLabel("Absence delay")
                        .withDescription("Time without presence before this zone is reported empty. " +
                        "The device cannot report this value back over Zigbee, so it shows what was last set")
                        .withCategory("config")
                        .withEndpoint(endpointName),
                ]);
            });
            toZigbee.push({
                key: presenceZoneKeys,
                convertSet: async (entity, key, value, meta) => {
                    utils.assertEndpoint(entity);
                    utils.assertString(meta.endpoint_name, "endpoint_name");
                    const zoneId = presenceZoneIdForEndpoint(entity.getDevice(), meta.endpoint_name);
                    if (zoneId === undefined)
                        throw new Error(`No Shelly presence zone for endpoint ${meta.endpoint_name}`);
                    const seconds = Math.min(3600, Math.max(0, Math.round(value)));
                    const ep = getRPCEndpoint(entity);
                    await rpcSendWithRetry(ep, "PresenceZone.SetConfig", { id: zoneId, config: { [SHELLY_PRESENCE_ZONE_FIELDS[key]]: seconds } });
                    return { state: { [key]: seconds } };
                },
            });
        }
        if (featurePresenceSensorConfig) {
            exposes.push((device, _options) => {
                if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID))
                    return [];
                return SHELLY_PRESENCE_SETTING_GROUPS.map((group) => {
                    const composite = e.composite(group.key, group.key, ea.STATE_SET).withLabel(group.label).withDescription(group.description);
                    for (const setting of group.settings)
                        composite.withFeature(setting.expose());
                    return composite.withCategory("config");
                });
            });
            toZigbee.push({
                key: SHELLY_PRESENCE_SETTING_GROUPS.map((group) => group.key),
                convertSet: async (entity, key, value, meta) => {
                    const group = SHELLY_PRESENCE_SETTING_GROUPS.find((candidate) => candidate.key === key);
                    if (!group)
                        return;
                    (0, utils_1.assertObject)(value);
                    // Only the values actually given travel; the rest of the group stays as it is.
                    const config = {};
                    const state = {};
                    for (const setting of group.settings) {
                        const given = value[setting.key];
                        if (given === undefined)
                            continue;
                        if (setting.key === "sensitivity" && given === SHELLY_PRESENCE_SENSITIVITY_CUSTOM) {
                            throw new Error("Sensitivity 'custom' is reported by the device, not set: adjust the individual detection thresholds instead");
                        }
                        shellyMergeConfig(config, setting.path, given);
                        state[setting.key] = given;
                    }
                    if (Object.keys(state).length === 0)
                        return;
                    utils.assertEndpoint(entity);
                    await rpcSendWithRetry(getRPCEndpoint(entity), "Presence.SetConfig", { config });
                    return { state: { [key]: state } };
                },
            });
        }
        if (featureEcoMode) {
            exposes.push((device, _options) => {
                if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID))
                    return [];
                return [
                    e
                        .binary("eco_mode", ea.STATE_SET, true, false)
                        .withLabel("Eco mode")
                        .withDescription("Reduce CPU and radio activity to lower power consumption (may slightly increase response latency). Write-only: the device cannot report this back over Zigbee, so it shows what was last set from here")
                        .withCategory("config"),
                ];
            });
            toZigbee.push({
                key: ["eco_mode"],
                convertSet: async (entity, key, value, meta) => {
                    const ep = getRPCEndpoint(entity);
                    await rpcSendWithRetry(ep, "Sys.SetConfig", { config: { device: { eco_mode: value === true } } });
                    return { state: { eco_mode: value === true } };
                },
            });
        }
        return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
    },
    shellyWiFiSetup() {
        const normalizeWifiString = (value) => (typeof value === "string" && value !== "" ? value : undefined);
        const getKnownFullWifiSsid = (device, options) => {
            if (typeof options?.shelly_wifi_ssid === "string" && options.shelly_wifi_ssid !== "")
                return options.shelly_wifi_ssid;
            if (typeof device.meta.shelly_wifi_ssid === "string" && device.meta.shelly_wifi_ssid !== "")
                return device.meta.shelly_wifi_ssid;
            return undefined;
        };
        const cacheFullWifiSsid = (device, ssid) => {
            if (typeof ssid === "string" && ssid !== "" && device.meta.shelly_wifi_ssid !== ssid) {
                device.meta.shelly_wifi_ssid = ssid;
                device.save();
            }
        };
        const rpcResult = (response) => {
            const result = response?.result ?? response?.params ?? response;
            if (!result)
                return undefined;
            (0, utils_1.assertObject)(result);
            return result;
        };
        const readWifiStateViaRpc = async (endpoint) => {
            // Reading through the RPC cluster only gains the untruncated SSID here, and the firmware
            // cannot answer an RPC read at all - it would just cost two timeouts before the setup
            // cluster, which does answer, gets its turn. The reported SSID is completed from the
            // cached one anyway.
            if (!SHELLY_RPC_CAN_READ)
                return undefined;
            const config = rpcResult(await shellyRpcRequest(endpoint, "Wifi.GetConfig"));
            const status = rpcResult(await shellyRpcRequest(endpoint, "Wifi.GetStatus"));
            const state = {};
            const wifiConfig = {};
            const sta = config?.sta;
            if (sta) {
                (0, utils_1.assertObject)(sta);
                if (typeof sta.enable === "boolean")
                    wifiConfig.enabled = sta.enable;
                if (typeof sta.ssid === "string")
                    wifiConfig.ssid = sta.ssid;
                if (typeof sta.ipv4mode === "string")
                    state.dhcp_enabled = sta.ipv4mode === "dhcp";
                if (typeof sta.ip === "string")
                    wifiConfig.static_ip = normalizeWifiString(sta.ip);
                if (typeof sta.netmask === "string")
                    wifiConfig.net_mask = normalizeWifiString(sta.netmask);
                if (typeof sta.gw === "string")
                    wifiConfig.gateway = normalizeWifiString(sta.gw);
                if (typeof sta.nameserver === "string")
                    wifiConfig.name_server = normalizeWifiString(sta.nameserver);
            }
            if (status) {
                if (typeof status.status === "string")
                    state.wifi_status = status.status;
                if (typeof status.sta_ip === "string")
                    state.ip_address = normalizeWifiString(status.sta_ip);
                if (wifiConfig.ssid === undefined && typeof status.ssid === "string")
                    wifiConfig.ssid = status.ssid;
            }
            if (Object.keys(wifiConfig).length > 0) {
                state.wifi_config = wifiConfig;
            }
            return Object.keys(state).length > 0 ? state : undefined;
        };
        const refresh = async (endpoint, meta) => {
            let published = false;
            if (meta) {
                try {
                    const rpcState = await readWifiStateViaRpc(endpoint);
                    const ssid = rpcState?.wifi_config;
                    if (ssid && utils.isObject(ssid)) {
                        cacheFullWifiSsid(endpoint.getDevice(), ssid.ssid);
                    }
                    if (rpcState) {
                        meta.publish(rpcState);
                        published = true;
                    }
                }
                catch (e) {
                    logger_1.logger.debug(`Failed to read Wi-Fi state through Shelly RPC, falling back to setup cluster: ${e}`, NS);
                    const ssid = getKnownFullWifiSsid(endpoint.getDevice(), meta.options);
                    if (ssid) {
                        meta.publish({ wifi_config: { ssid } });
                        published = true;
                    }
                }
            }
            if (published) {
                return;
            }
            try {
                await endpoint.write("shellyWiFiSetupCluster", { actionCode: 0 }, SHELLY_OPTIONS);
                await endpoint.read("shellyWiFiSetupCluster", ["status", "ip", "enabled", "dhcp", "ssid"], SHELLY_OPTIONS);
                await endpoint.read("shellyWiFiSetupCluster", ["staticIp", "netMask"], SHELLY_OPTIONS);
                await endpoint.read("shellyWiFiSetupCluster", ["gateway", "nameServer"], SHELLY_OPTIONS);
            }
            catch (e) {
                logger_1.logger.warning(`Failed to read Wi-Fi state through Shelly setup cluster; leaving previous state unchanged: ${e}`, NS);
            }
        };
        const exposes = [
            e.text("wifi_status", ea.STATE_GET).withLabel("Wi-Fi status").withDescription("Current connection status").withCategory("diagnostic"),
            e
                .text("ip_address", ea.STATE_GET)
                .withLabel("IP address")
                .withDescription("IP address currently assigned to the device")
                .withCategory("diagnostic"),
            e
                .binary("dhcp_enabled", ea.STATE_GET, true, false)
                .withLabel("DHCP enabled")
                .withDescription("Indicates whether DHCP is used to automatically assign network settings")
                .withCategory("diagnostic"),
            e
                .composite("wifi_config", "wifi_config", ea.ALL)
                .withFeature(e.binary("enabled", ea.STATE_SET, true, false).withLabel("Wi-Fi enabled").withDescription("Enable/disable Wi-Fi connectivity"))
                .withFeature(e.text("ssid", ea.STATE_SET).withLabel("Network").withDescription("Name (SSID) of the Wi-Fi network to connect to"))
                .withFeature(e.text("password", ea.SET).withLabel("Password").withDescription("Password for the selected Wi-Fi network"))
                .withFeature(e
                .text("static_ip", ea.STATE_SET)
                .withLabel("IPv4 address")
                .withDescription("Manually assigned IP address (used when DHCP is disabled)"))
                .withFeature(e.text("net_mask", ea.STATE_SET).withLabel("Network mask").withDescription("Subnet mask for the static IP configuration"))
                .withFeature(e.text("gateway", ea.STATE_SET).withLabel("Gateway").withDescription("Default gateway address for static IP configuration"))
                .withFeature(e.text("name_server", ea.STATE_SET).withLabel("DNS").withDescription("Name server address for static IP configuration"))
                .withLabel("Wi-Fi Configuration")
                .withCategory("config"),
        ];
        // biome-ignore lint/suspicious/noExplicitAny: generic
        const fromZigbee = [
            {
                cluster: "shellyWiFiSetupCluster",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const wifi_config = {};
                    const state = { wifi_config };
                    // Diagnostic data
                    if (msg.data.status !== undefined)
                        state.wifi_status = msg.data.status;
                    if (msg.data.ip !== undefined)
                        state.ip_address = msg.data.ip;
                    if (msg.data.dhcp !== undefined)
                        state.dhcp_enabled = msg.data.dhcp === 1;
                    // Wi-Fi config
                    if (msg.data.enabled !== undefined)
                        wifi_config.enabled = msg.data.enabled === 1;
                    if (msg.data.ssid !== undefined) {
                        const reportedSsid = normalizeWifiString(msg.data.ssid);
                        const knownFullSsid = getKnownFullWifiSsid(meta.device, options);
                        wifi_config.ssid = knownFullSsid && reportedSsid && knownFullSsid.startsWith(reportedSsid) ? knownFullSsid : reportedSsid;
                        if (reportedSsid && (!knownFullSsid || reportedSsid.length > knownFullSsid.length)) {
                            cacheFullWifiSsid(meta.device, reportedSsid);
                        }
                    }
                    if (msg.data.staticIp !== undefined)
                        wifi_config.static_ip = msg.data.staticIp;
                    if (msg.data.netMask !== undefined)
                        wifi_config.net_mask = msg.data.netMask;
                    if (msg.data.gateway !== undefined)
                        wifi_config.gateway = msg.data.gateway;
                    if (msg.data.nameServer !== undefined)
                        wifi_config.name_server = msg.data.nameServer;
                    // Cleanup empty keys
                    for (const key in wifi_config) {
                        if (wifi_config[key] === "") {
                            wifi_config[key] = undefined;
                        }
                    }
                    return state;
                },
            },
        ];
        const toZigbee = [
            {
                key: ["wifi_status", "ip_address", "dhcp_enabled"],
                convertGet: async (entity, key, meta) => {
                    utils.assertEndpoint(entity);
                    const ep = entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
                    if (!ep)
                        throw new Error(`Shelly endpoint ${SHELLY_ENDPOINT_ID} not found`);
                    await refresh(ep, meta);
                },
            },
            {
                key: ["wifi_config"],
                convertGet: async (entity, key, meta) => {
                    utils.assertEndpoint(entity);
                    const ep = entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
                    if (!ep)
                        throw new Error(`Shelly endpoint ${SHELLY_ENDPOINT_ID} not found`);
                    await refresh(ep, meta);
                },
                convertSet: async (entity, key, value, meta) => {
                    (0, utils_1.assertObject)(value);
                    const ep = (0, utils_1.determineEndpoint)(entity, meta, "shellyWiFiSetupCluster");
                    // Stage only the fields actually given: every staged attribute is applied by
                    // actionCode 1, so an absent field must not travel as its empty default - a
                    // payload of only {enabled} used to wipe the stored credentials, and one of
                    // only {ssid, password} silently wrote enabled=false (#12617). The ssid and
                    // password never travel empty: clearing credentials over Zigbee is exactly
                    // the accident this fixes. The static network fields may travel empty on
                    // purpose (clearing them when switching back to DHCP).
                    const staged = {};
                    if (typeof value.enabled === "boolean")
                        staged.enabled = value.enabled;
                    if (typeof value.ssid === "string" && value.ssid !== "")
                        staged.ssid = value.ssid;
                    if (typeof value.password === "string" && value.password !== "")
                        staged.password = value.password;
                    if (typeof value.static_ip === "string")
                        staged.staticIp = value.static_ip;
                    if (typeof value.net_mask === "string")
                        staged.netMask = value.net_mask;
                    if (typeof value.gateway === "string")
                        staged.gateway = value.gateway;
                    if (typeof value.name_server === "string")
                        staged.nameServer = value.name_server;
                    if (Object.keys(staged).length === 0)
                        return;
                    // Keep the proven write grouping (small multi-attribute frames, password on
                    // its own), but skip groups with nothing to write.
                    const groups = [
                        { enabled: staged.enabled, ssid: staged.ssid },
                        { password: staged.password },
                        { staticIp: staged.staticIp, netMask: staged.netMask },
                        { gateway: staged.gateway, nameServer: staged.nameServer },
                    ];
                    for (const group of groups) {
                        const attrs = Object.fromEntries(Object.entries(group).filter(([, attrValue]) => attrValue !== undefined));
                        if (Object.keys(attrs).length === 0)
                            continue;
                        await ep.write("shellyWiFiSetupCluster", attrs, SHELLY_OPTIONS);
                    }
                    await ep.write("shellyWiFiSetupCluster", { actionCode: 1 }, SHELLY_OPTIONS);
                    // Optimistic state: only what was written; the password stays write-only.
                    const written = {};
                    if (staged.enabled !== undefined)
                        written.enabled = staged.enabled;
                    if (staged.ssid !== undefined)
                        written.ssid = staged.ssid;
                    if (staged.staticIp !== undefined)
                        written.static_ip = staged.staticIp;
                    if (staged.netMask !== undefined)
                        written.net_mask = staged.netMask;
                    if (staged.gateway !== undefined)
                        written.gateway = staged.gateway;
                    if (staged.nameServer !== undefined)
                        written.name_server = staged.nameServer;
                    return { state: { wifi_config: written } };
                },
            },
        ];
        const configure = [
            async (device, coordinatorEndpoint, definition) => {
                const ep = device.getEndpoint(SHELLY_ENDPOINT_ID);
                if (!ep)
                    return;
                await refresh(ep);
            },
        ];
        const options = [
            e
                .text("shelly_wifi_ssid", ea.SET)
                .withDescription("Full Wi-Fi SSID to use when the Shelly Wi-Fi setup cluster reports a shortened network name"),
        ];
        return { exposes, fromZigbee, toZigbee, configure, options, isModernExtend: true };
    },
    ws90CalculatedValues() {
        const exposes = [
            // Calculated values only
            e.numeric("dew_point", ea.STATE).withUnit("°C").withDescription("Calculated dew point temperature"),
            e.numeric("wind_chill", ea.STATE).withUnit("°C").withDescription("Calculated wind chill temperature"),
            e.numeric("humidex", ea.STATE).withUnit("°C").withDescription("Calculated humidex (feels-like for warm conditions)"),
            e.numeric("apparent_temperature", ea.STATE).withUnit("°C").withDescription("Calculated apparent temperature"),
            e.numeric("heat_stress", ea.STATE).withUnit("%").withDescription("Calculated heat stress percentage (0-100%)"),
            e.numeric("rain_rate", ea.STATE).withUnit("mm/h").withDescription("Calculated rainfall rate"),
            e.numeric("pressure_trend", ea.STATE).withUnit("hPa/h").withDescription("Pressure change rate (negative = falling)"),
            e.text("weather_condition", ea.STATE).withDescription("Weather condition (sunny, rainy, snowy, cloudy, etc.)"),
        ];
        // biome-ignore lint/suspicious/noExplicitAny: custom clusters not in type registry
        const fromZigbee = [
            {
                cluster: "msTemperatureMeasurement",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.measuredValue !== undefined) {
                        const temperature = msg.data.measuredValue / 100;
                        const calculated = updateWS90CalculatedValues(msg.device, { temperature });
                        return calculated; // Only calculated values; m.temperature() handles base temperature
                    }
                },
            },
            {
                cluster: "msRelativeHumidity",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.measuredValue !== undefined) {
                        const humidity = msg.data.measuredValue / 100;
                        const calculated = updateWS90CalculatedValues(msg.device, { humidity });
                        return calculated; // Only calculated values; m.humidity() handles base humidity
                    }
                },
            },
            {
                cluster: "msPressureMeasurement",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.measuredValue !== undefined) {
                        const pressure = msg.data.measuredValue / 10;
                        const calculated = updateWS90CalculatedValues(msg.device, { pressure });
                        return calculated; // Only calculated values; m.pressure() handles base pressure
                    }
                },
            },
            {
                cluster: "msIlluminanceMeasurement",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.measuredValue !== undefined) {
                        const measuredValue = msg.data.measuredValue;
                        const illuminance = measuredValue > 0 ? Math.round(10 ** ((measuredValue - 1) / 10000)) : 0;
                        const calculated = updateWS90CalculatedValues(msg.device, { illuminance });
                        return calculated; // Only calculated values; m.illuminance() handles base illuminance
                    }
                },
            },
            {
                cluster: "shellyWS90UV",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const data = msg.data;
                    const payload = ws90Scaled("uv_index", data.uvIndex, 0xff);
                    if (payload) {
                        return updateWS90CalculatedValues(msg.device, payload);
                    }
                },
            },
            {
                cluster: "shellyWS90Wind",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const data = msg.data;
                    const payload = {
                        ...(ws90Scaled("wind_speed", data.windSpeed, 0xffff) ?? {}),
                        ...(ws90Scaled("wind_direction", data.windDirection, 0xffff) ?? {}),
                        ...(ws90Scaled("gust_speed", data.gustSpeed, 0xffff) ?? {}),
                    };
                    const calculated = updateWS90CalculatedValues(msg.device, payload);
                    return calculated;
                },
            },
            {
                cluster: "shellyWS90Rain",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    const data = msg.data;
                    const payload = {
                        ...(ws90Scaled("precipitation", data.precipitation, 0xffffff) ?? {}),
                    };
                    if (data.rainStatus !== undefined)
                        payload.rain_status = Boolean(data.rainStatus);
                    // Calculate rain_rate (it's a calculated value, not a base sensor value)
                    const ws90Meta = getWS90Meta(msg.device);
                    const rainRate = calculateRainRate(ws90Meta, payload.precipitation);
                    const rain_rate = rainRate !== null ? rainRate : 0;
                    // Update state with precipitation and rain_rate
                    const stateUpdate = { ...payload, rain_rate };
                    const calculated = updateWS90CalculatedValues(msg.device, stateUpdate);
                    // Include rain_rate in calculated values
                    calculated.rain_rate = rain_rate;
                    return calculated; // Only calculated values; m.binary()/m.numeric() handle base rain values
                },
            },
        ];
        return { exposes, fromZigbee, isModernExtend: true };
    },
    shellyLightLevel(args) {
        const reporting = args?.reporting ?? { min: "1_MINUTE", max: 900, change: 0 };
        return [
            m.deviceAddCustomCluster("shellyLightLevel", {
                name: "shellyLightLevel",
                ID: 0xfc21,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    lightLevel: { name: "lightLevel", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                    darkThreshold: { name: "darkThreshold", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT24, write: true },
                    brightThreshold: { name: "brightThreshold", ID: 0x0002, type: zigbee_herdsman_1.Zcl.DataType.UINT24, write: true },
                },
                commands: {},
                commandsResponse: {},
            }),
            m.enumLookup({
                name: "light_level",
                cluster: "shellyLightLevel",
                attribute: "lightLevel",
                lookup: { dark: 0, twilight: 1, bright: 2 },
                description: "Coarse light level",
                reporting,
                access: "STATE_GET",
            }),
            m.numeric({
                name: "dark_threshold",
                cluster: "shellyLightLevel",
                attribute: "darkThreshold",
                valueMin: 0,
                valueMax: 65535,
                reporting: false,
                description: "Lux threshold below which light level is dark",
                unit: "lx",
                access: "ALL",
            }),
            m.numeric({
                name: "bright_threshold",
                cluster: "shellyLightLevel",
                attribute: "brightThreshold",
                valueMin: 0,
                valueMax: 65535,
                reporting: false,
                description: "Lux threshold above which light level is bright",
                unit: "lx",
                access: "ALL",
            }),
        ];
    },
};
// =============================================================================
// Local From Zigbee Converters
// =============================================================================
const handlePosition = e
    .enum("handle_position", ea.STATE, ["closed", "tilted", "open"])
    .withDescription("Handle position: closed, tilted (partly open), or open");
// Resolves which switch input (1 or 2) sent a message, from the definition's own endpoint map:
// the cover-mode 2PM has its inputs on endpoints 2/3, the switch-mode 2PM on 3/4 and the
// single-channel devices on 2 - a fixed endpoint list cannot serve them all. Messages from any
// other endpoint return undefined and must be ignored, not mislabeled as input 1.
const shellyInputNumber = (model, msg) => {
    const endpoints = model.endpoint?.(msg.device) ?? {};
    if (endpoints.sw1 === msg.endpoint.ID)
        return 1;
    if (endpoints.sw2 === msg.endpoint.ID)
        return 2;
    return undefined;
};
const fzLocal = {
    one_button_events: {
        cluster: "genOnOff",
        type: ["commandToggle"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const event = utils.getFromLookup(msg.endpoint.ID, { 1: "single", 2: "double", 3: "triple" });
            return { action: event };
        },
    },
    one_button_scene_events: {
        cluster: "genScenes",
        type: ["commandRecall"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const event = utils.getFromLookup(`${msg.endpoint.ID}`, { "1": "single_long", "2": "double_long", "3": "triple_long" });
            return { action: event };
        },
    },
    four_buttons_single_events: {
        cluster: "genOnOff",
        type: ["commandOn", "commandOff", "commandToggle"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const event = utils.getFromLookup(`${msg.endpoint.ID}_${msg.type}`, {
                "1_commandOn": "1_single",
                "1_commandOff": "2_single",
                "2_commandOn": "3_single",
                "2_commandOff": "4_single",
                "1_commandToggle": "1_single",
                "2_commandToggle": "2_single",
                "3_commandToggle": "3_single",
                "4_commandToggle": "4_single",
            });
            return { action: event };
        },
    },
    four_buttons_hold_events: {
        cluster: "genLevelCtrl",
        type: ["commandStep"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const event = utils.getFromLookup(`${msg.endpoint.ID}_${msg.data.stepmode}`, {
                "1_0": "1_hold",
                "1_1": "2_hold",
                "2_0": "3_hold",
                "2_1": "4_hold",
            });
            return { action: event };
        },
    },
    four_buttons_scene_events: {
        cluster: "genScenes",
        type: ["commandRecall"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const event = utils.getFromLookup(`${msg.endpoint.ID}_${msg.data.sceneid}`, {
                "1_1": "1_double",
                "2_1": "2_double",
                "3_1": "3_double",
                "4_1": "4_double",
                "1_2": "1_triple",
                "2_2": "2_triple",
                "3_2": "3_triple",
                "4_2": "4_triple",
                "1_11": "1_single_long",
                "2_11": "2_single_long",
                "3_11": "3_single_long",
                "4_11": "4_single_long",
                "1_12": "1_double_long",
                "2_12": "2_double_long",
                "3_12": "3_double_long",
                "4_12": "4_double_long",
                "1_13": "1_triple_long",
                "2_13": "2_triple_long",
                "3_13": "3_triple_long",
                "4_13": "4_triple_long",
            });
            return { action: event };
        },
    },
    // The input numbering comes from the definition's endpoint map (see shellyInputNumber): a
    // fixed endpoint lookup broke the cover-mode 2PM, whose inputs live on endpoints 2/3 while
    // the switch-mode device has them on 3/4 - input 1 threw and input 2 was reported as input 1.
    two_switch_inputs_events: {
        cluster: "genOnOff",
        type: ["commandOn", "commandOff", "commandToggle"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const input = shellyInputNumber(model, msg);
            if (input === undefined)
                return;
            const event = utils.getFromLookup(msg.type, { commandOn: "on", commandOff: "off", commandToggle: "toggle" });
            return { action: `input_${input}_${event}` };
        },
    },
    two_switch_inputs_scene_events: {
        cluster: "genScenes",
        type: ["commandRecall"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            const input = shellyInputNumber(model, msg);
            if (input === undefined)
                return;
            const event = utils.getFromLookup(`${msg.data.sceneid}`, {
                "1": "single",
                "2": "double",
                "3": "triple",
                "4": "hold",
                "5": "toggle",
                "11": "hold",
            });
            return { action: `input_${input}_${event}` };
        },
    },
    one_switch_input_events: {
        cluster: "genOnOff",
        type: ["commandOn", "commandOff", "commandToggle"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            if (shellyInputNumber(model, msg) !== 1)
                return;
            const event = utils.getFromLookup(msg.type, {
                commandOn: "input_1_on",
                commandOff: "input_1_off",
                commandToggle: "input_1_toggle",
            });
            return { action: event };
        },
    },
    one_switch_input_scene_events: {
        cluster: "genScenes",
        type: ["commandRecall"],
        convert: (model, msg, publish, options, meta) => {
            if (utils.hasAlreadyProcessedMessage(msg, model))
                return;
            if (shellyInputNumber(model, msg) !== 1)
                return;
            const event = utils.getFromLookup(`${msg.data.sceneid}`, {
                "1": "input_1_single",
                "2": "input_1_double",
                "3": "input_1_triple",
                "4": "input_1_hold",
                "5": "input_1_toggle",
                "11": "input_1_hold",
            });
            return { action: event };
        },
    },
    switch_input_type: {
        cluster: "genOnOffSwitchCfg",
        type: ["attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            if (!Object.hasOwn(msg.data, "switchType"))
                return;
            const property = utils.postfixWithEndpointName("switch_type", msg, model, meta);
            return { [property]: utils.getFromLookup(msg.data.switchType, { 0: "toggle", 1: "momentary" }) };
        },
    },
    blu_door_window: {
        cluster: "ssIasZone",
        type: ["commandStatusChangeNotification", "attributeReport", "readResponse"],
        convert: (model, msg, publish, options, meta) => {
            const zoneStatus = "zonestatus" in msg.data ? msg.data.zonestatus : msg.data.zoneStatus;
            if (zoneStatus === undefined)
                return;
            const alarm1 = (zoneStatus & 1) > 0;
            const alarm2 = (zoneStatus & (1 << 1)) > 0;
            // Zone type 0x0016 (door/window handle) per ZigBee spec Table 8-7:
            //   alarm1=0, alarm2=0 -> closed
            //   alarm1=1, alarm2=0 -> tilted (partly open)
            //   alarm1=1, alarm2=1 -> open
            let position;
            if (!alarm1 && !alarm2)
                position = "closed";
            else if (alarm1 && !alarm2)
                position = "tilted";
            else
                position = "open";
            return {
                contact: position === "closed",
                handle_position: position,
                battery_low: (zoneStatus & (1 << 3)) > 0,
            };
        },
    },
};
const tzLocal = {
    switch_input_type: {
        key: ["switch_type"],
        convertSet: async (entity, key, value, meta) => {
            const lookup = { toggle: 0, momentary: 1 };
            const ep = (0, utils_1.determineEndpoint)(entity, meta, "genOnOffSwitchCfg");
            await ep.write("genOnOffSwitchCfg", { switchType: utils.getFromLookup(value, lookup) });
            return { state: { switch_type: value } };
        },
        convertGet: async (entity, key, meta) => {
            const ep = (0, utils_1.determineEndpoint)(entity, meta, "genOnOffSwitchCfg");
            await ep.read("genOnOffSwitchCfg", ["switchType"]);
        },
    },
};
/**
 * Returns the persisted pending external temperature request for a TRV.
 */
function shellyTRVGetExternalTemperatureRequest(device) {
    const request = device.meta[SHELLY_TRV_EXTERNAL_TEMPERATURE_META_KEY];
    if (typeof request !== "object" || request === null)
        return undefined;
    return request;
}
/**
 * Stores the pending external temperature request in the device metadata.
 *
 * The request itself is persisted immediately so delivery can resume after a
 * Zigbee2MQTT restart. Retry bookkeeping may stay in memory because createdAt
 * remains persisted and still bounds the request lifetime across restarts.
 */
function shellyTRVPutExternalTemperatureRequest(device, request, persist = true) {
    device.meta[SHELLY_TRV_EXTERNAL_TEMPERATURE_META_KEY] = request;
    if (persist)
        device.save();
}
/**
 * Clears the persisted pending external temperature request for a TRV.
 */
function shellyTRVClearExternalTemperatureRequest(device) {
    if (!(SHELLY_TRV_EXTERNAL_TEMPERATURE_META_KEY in device.meta))
        return;
    delete device.meta[SHELLY_TRV_EXTERNAL_TEMPERATURE_META_KEY];
    device.save();
}
/**
 * Creates a new confirmation-tracked external temperature request.
 */
function shellyTRVCreateExternalTemperatureRequest(temperature) {
    return {
        temperature,
        measuredValue: Math.round(temperature * 100),
        createdAt: Date.now(),
        lastAttemptAt: 0,
        attempts: 0,
    };
}
/**
 * Reserves one delivery attempt before asynchronous Zigbee traffic starts.
 *
 * Updating the store synchronously prevents several reports from the same wake window
 * from scheduling duplicate retries.
 */
function shellyTRVReserveExternalTemperatureAttempt(device, request) {
    const reserved = {
        ...request,
        lastAttemptAt: Date.now(),
        attempts: request.attempts + 1,
    };
    shellyTRVPutExternalTemperatureRequest(device, reserved, false);
    return reserved;
}
/**
 * Reconstructs the thermostat RemoteSensing bit mask from the cached state.
 */
function shellyTRVRemoteSensingFromState(state) {
    if (typeof state.remote_sensing_raw === "number")
        return state.remote_sensing_raw;
    if (typeof state.remote_sensing !== "object" || state.remote_sensing === null)
        return 0;
    const remoteSensing = state.remote_sensing;
    let value = 0;
    if (remoteSensing.local_temperature === "remotely")
        value |= 1;
    if (remoteSensing.outdoor_temperature === "remotely")
        value |= 1 << 1;
    if (remoteSensing.occupancy === "remotely")
        value |= 1 << 2;
    return value;
}
/**
 * Returns the best available RemoteSensing bit mask without querying the sleeping device.
 */
function shellyTRVGetRemoteSensing(endpoint, state = {}, reportedValue) {
    if (reportedValue !== undefined)
        return reportedValue;
    const cached = endpoint.getClusterAttributeValue("hvacThermostat", "remoteSensing");
    if (typeof cached === "number")
        return cached;
    return shellyTRVRemoteSensingFromState(state);
}
/**
 * Enables the external local-temperature source while preserving the other RemoteSensing bits.
 */
async function shellyTRVEnableRemoteTemperature(endpoint, remoteSensing) {
    const enabled = remoteSensing | 1;
    if (enabled !== remoteSensing) {
        await endpoint.write("hvacThermostat", { remoteSensing: enabled }, { disableResponse: true, disableDefaultResponse: true });
    }
    return enabled;
}
/**
 * Sends an external temperature as the attribute report of a bound Temperature Measurement server.
 */
async function shellyTRVSendExternalTemperature(endpoint, request) {
    await endpoint.report("msTemperatureMeasurement", { measuredValue: request.measuredValue }, {
        direction: zigbee_herdsman_1.Zcl.Direction.SERVER_TO_CLIENT,
        srcEndpoint: 1,
        disableResponse: true,
        disableDefaultResponse: true,
    });
}
/**
 * Enables remote sensing and sends the currently reserved external temperature request.
 */
async function shellyTRVDeliverExternalTemperature(endpoint, request, remoteSensing) {
    await shellyTRVEnableRemoteTemperature(endpoint, remoteSensing);
    await shellyTRVSendExternalTemperature(endpoint, request);
}
/**
 * Checks whether the TRV has confirmed the requested value while using the remote source.
 */
function shellyTRVExternalTemperatureIsConfirmed(request, localTemperature, remoteSensing) {
    return localTemperature === request.measuredValue && (remoteSensing & 1) !== 0;
}
/**
 * Checks whether a request reached its bounded retry limit.
 */
function shellyTRVExternalTemperatureIsExpired(request) {
    return (request.attempts >= SHELLY_TRV_EXTERNAL_TEMPERATURE_MAX_ATTEMPTS ||
        Date.now() - request.createdAt >= SHELLY_TRV_EXTERNAL_TEMPERATURE_MAX_AGE_MS);
}
/**
 * Confirms an external temperature from incoming thermostat reports and retries an unconfirmed value during a wake window.
 */
const fzShellyTRVExternalTemperature = {
    cluster: "hvacThermostat",
    type: ["attributeReport", "readResponse"],
    convert: (model, msg, publish, options, meta) => {
        const request = shellyTRVGetExternalTemperatureRequest(msg.device);
        if (!request)
            return;
        const remoteSensing = shellyTRVGetRemoteSensing(msg.endpoint, meta.state, typeof msg.data.remoteSensing === "number" ? msg.data.remoteSensing : undefined);
        if (shellyTRVExternalTemperatureIsConfirmed(request, msg.data.localTemp, remoteSensing)) {
            shellyTRVClearExternalTemperatureRequest(msg.device);
            logger_1.logger.debug(`External temperature ${request.temperature} °C was confirmed by '${msg.device.ieeeAddr}'`, NS);
            return;
        }
        if (shellyTRVExternalTemperatureIsExpired(request)) {
            shellyTRVClearExternalTemperatureRequest(msg.device);
            logger_1.logger.warning(`External temperature ${request.temperature} °C was not confirmed by '${msg.device.ieeeAddr}'`, NS);
            return;
        }
        if (Date.now() - request.lastAttemptAt < SHELLY_TRV_EXTERNAL_TEMPERATURE_MIN_RETRY_INTERVAL_MS)
            return;
        const reserved = shellyTRVReserveExternalTemperatureAttempt(msg.device, request);
        shellyTRVDeliverExternalTemperature(msg.endpoint, reserved, remoteSensing).catch((error) => logger_1.logger.debug(`Failed to retry external temperature for '${msg.device.ieeeAddr}': ${error}`, NS));
    },
};
/**
 * Stores and sends an external temperature while marking the request as pending until the TRV confirms it.
 */
const tzShellyTRVExternalTemperature = {
    key: ["external_temperature"],
    convertSet: async (entity, key, value, meta) => {
        utils.assertEndpoint(entity);
        utils.assertNumber(value, key);
        if (value < -40 || value > 85) {
            throw new Error(`External temperature must be between -40 and 85 °C, got ${value}`);
        }
        const temperature = Math.round(value * 100) / 100;
        const request = shellyTRVCreateExternalTemperatureRequest(temperature);
        shellyTRVPutExternalTemperatureRequest(entity.getDevice(), request);
        const remoteSensing = shellyTRVGetRemoteSensing(entity, meta.state);
        const reserved = shellyTRVReserveExternalTemperatureAttempt(entity.getDevice(), request);
        try {
            await shellyTRVDeliverExternalTemperature(entity, reserved, remoteSensing);
        }
        catch (error) {
            logger_1.logger.debug(`Initial external temperature delivery to '${entity.getDevice().ieeeAddr}' failed: ${error}`, NS);
        }
        return { state: { external_temperature: temperature } };
    },
};
/**
 * Retries a persisted pending external temperature when the TRV announces itself.
 */
const shellyTRVExternalTemperatureOnEvent = (event) => {
    if (event.type !== "deviceAnnounce")
        return;
    const endpoint = event.data.device.getEndpoint(1);
    const request = shellyTRVGetExternalTemperatureRequest(event.data.device);
    if (!endpoint || !request)
        return;
    if (shellyTRVExternalTemperatureIsExpired(request)) {
        shellyTRVClearExternalTemperatureRequest(event.data.device);
        logger_1.logger.warning(`External temperature ${request.temperature} °C was not confirmed by '${event.data.device.ieeeAddr}'`, NS);
        return;
    }
    if (Date.now() - request.lastAttemptAt < SHELLY_TRV_EXTERNAL_TEMPERATURE_MIN_RETRY_INTERVAL_MS)
        return;
    const remoteSensing = shellyTRVGetRemoteSensing(endpoint, event.data.state);
    const reserved = shellyTRVReserveExternalTemperatureAttempt(event.data.device, request);
    shellyTRVDeliverExternalTemperature(endpoint, reserved, remoteSensing).catch((error) => logger_1.logger.debug(`Failed to deliver external temperature after announce from '${event.data.device.ieeeAddr}': ${error}`, NS));
};
// =============================================================================
// Device Definitions
// =============================================================================
exports.definitions = [
    {
        zigbeeModel: ["Mini1", "1 Mini"],
        fingerprint: [{ modelID: "1", manufacturerName: "Shelly" }],
        model: "S4SW-001X8EU",
        vendor: "Shelly",
        description: "1 Mini Gen 4",
        whiteLabel: [
            {
                vendor: "Shelly",
                model: "S4SW-001X16EU",
                description: "1 Gen 4",
                fingerprint: [{ modelID: "1", manufacturerName: "Shelly" }],
            },
        ],
        ota: true,
        // The genOnOff/genScenes bindings and the switchType read in configure were added after
        // this device was first released; bump the patch version so already paired devices get
        // re-configured and their input events start arriving (same rule as the BLU remotes).
        version: "0.0.1",
        fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
        toZigbee: [tzLocal.switch_input_type],
        // The switch input endpoint only exists when an input is actually wired. Without it the
        // setting has nothing to address, and a state that can never hold a value is worse than
        // none - so expose it the same way the 2PM already does, conditional on the endpoint.
        exposes: (device) => [
            e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
            ...shellySwitchInputExposes(device, { sw1: 2 }),
        ],
        extend: [
            // The endpoint map must only name endpoints the device actually has: the application
            // builds its property parser from these names and resolves them with a non-null
            // assertion, so naming sw1 on a device without a wired input crashes every
            // switch_type_sw1/switch_mode_sw1 set with "Cannot read properties of undefined
            // (reading 'ID')" (Koenkk/zigbee2mqtt#31951).
            shellyDeviceEndpoints({ sw1: 2 }),
            m.onOff({ powerOnBehavior: false }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["1PMInputMode"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
        configure: async (device, coordinatorEndpoint) => {
            const ep = device.getEndpoint(2);
            if (ep) {
                await ep.bind("genOnOff", coordinatorEndpoint);
                await ep.bind("genScenes", coordinatorEndpoint);
                await ep.read("genOnOffSwitchCfg", ["switchType"]);
            }
        },
    },
    {
        zigbeeModel: ["Mini1PM", "1PM Mini", "1PM"],
        model: "S4SW-001P8EU",
        vendor: "Shelly",
        description: "1PM Mini Gen 4",
        whiteLabel: [
            {
                vendor: "Shelly",
                model: "S4SW-001P16EU",
                description: "1PM Gen 4",
                fingerprint: [{ modelID: "1PM" }],
            },
        ],
        ota: true,
        // The genOnOff/genScenes bindings and the switchType read in configure were added after
        // this device was first released; bump the patch version so already paired devices get
        // re-configured and their input events start arriving (same rule as the BLU remotes).
        version: "0.0.1",
        fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
        toZigbee: [tzLocal.switch_input_type],
        // The switch input endpoint only exists when an input is actually wired. Without it the
        // setting has nothing to address, and a state that can never hold a value is worse than
        // none - so expose it the same way the 2PM already does, conditional on the endpoint.
        exposes: (device) => [
            e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
            ...shellySwitchInputExposes(device, { sw1: 2 }),
        ],
        extend: [
            // The endpoint map must only name endpoints the device actually has: the application
            // builds its property parser from these names and resolves them with a non-null
            // assertion, so naming sw1 on a device without a wired input crashes every
            // switch_type_sw1/switch_mode_sw1 set with "Cannot read properties of undefined
            // (reading 'ID')" (Koenkk/zigbee2mqtt#31951).
            shellyDeviceEndpoints({ sw1: 2 }),
            m.onOff({ powerOnBehavior: false }),
            m.electricityMeter({ producedEnergy: true, acFrequency: true }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["1PMInputMode"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
        configure: async (device, coordinatorEndpoint) => {
            const ep = device.getEndpoint(2);
            if (ep) {
                await ep.bind("genOnOff", coordinatorEndpoint);
                await ep.bind("genScenes", coordinatorEndpoint);
                await ep.read("genOnOffSwitchCfg", ["switchType"]);
            }
        },
    },
    {
        zigbeeModel: ["EM Mini"],
        model: "S4EM-001PXCEU16",
        vendor: "Shelly",
        description: "EM Mini Gen4",
        extend: [
            m.electricityMeter({ producedEnergy: true, acFrequency: true }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyWiFiSetup(),
            m.forcePowerSource({ powerSource: "Mains (single phase)" }),
        ],
    },
    {
        fingerprint: [{ modelID: "EM", manufacturerName: "Shelly" }],
        model: "S4EM-002CXCEU",
        vendor: "Shelly",
        description: "EM Gen4",
        extend: [
            m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3 } }),
            m.onOff({ powerOnBehavior: false, endpointNames: ["1"] }),
            m.electricityMeter({
                endpointNames: ["2", "3"],
                producedEnergy: true,
                acFrequency: true,
            }),
            m.forcePowerSource({ powerSource: "Mains (single phase)" }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [
            {
                type: "Router",
                manufacturerName: "Shelly",
                modelID: "2PM",
                endpoints: [
                    { ID: 1, profileID: 260, deviceID: 514, inputClusters: [0, 3, 4, 5, 258], outputClusters: [] },
                    { ID: 239, profileID: 49153, deviceID: 8193, inputClusters: [64513, 64514], outputClusters: [] },
                    { ID: 242, profileID: 41440, deviceID: 97, inputClusters: [], outputClusters: [33] },
                ],
            },
            {
                type: "Router",
                manufacturerName: "Shelly",
                modelID: "2PM",
                endpoints: [
                    { ID: 1, profileID: 260, deviceID: 514, inputClusters: [0, 3, 4, 5, 258], outputClusters: [25] },
                    { ID: 2, inputClusters: [7], outputClusters: [3, 4, 5, 6] },
                    { ID: 3, inputClusters: [7], outputClusters: [3, 4, 5, 6] },
                    { ID: 4, inputClusters: [], outputClusters: [3, 4, 6, 8, 258] },
                    { ID: 239, profileID: 49153, deviceID: 8193, inputClusters: [64513, 64514], outputClusters: [] },
                    { ID: 242, profileID: 41440, deviceID: 97, inputClusters: [], outputClusters: [33] },
                ],
            },
        ],
        model: "S4SW-002P16EU-COVER",
        vendor: "Shelly",
        description: "2PM Gen4 (Cover mode)",
        ota: true,
        // The genOnOff/genScenes bindings and the switchType read in configure were added after
        // this device was first released; bump the patch version so already paired devices get
        // re-configured and their input events start arriving (same rule as the BLU remotes).
        version: "0.0.1",
        fromZigbee: [fzLocal.two_switch_inputs_events, fzLocal.two_switch_inputs_scene_events, fzLocal.switch_input_type],
        toZigbee: [tzLocal.switch_input_type],
        exposes: (device) => [
            e.action([
                "input_1_on",
                "input_1_off",
                "input_1_toggle",
                "input_1_single",
                "input_1_double",
                "input_1_triple",
                "input_1_hold",
                "input_2_on",
                "input_2_off",
                "input_2_toggle",
                "input_2_single",
                "input_2_double",
                "input_2_triple",
                "input_2_hold",
            ]),
            ...shellySwitchInputExposes(device, { sw1: 2, sw2: 3 }),
        ],
        extend: [
            shellyDeviceEndpoints({ sw1: 2, sw2: 3 }),
            shellyModernExtend.shellyWindowCovering(),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["2PMCoverInputMode", "CoverTiltAuto"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
        configure: async (device, coordinatorEndpoint) => {
            for (const epID of [2, 3]) {
                const ep = device.getEndpoint(epID);
                if (ep) {
                    await ep.bind("genOnOff", coordinatorEndpoint);
                    await ep.bind("genScenes", coordinatorEndpoint);
                    await ep.read("genOnOffSwitchCfg", ["switchType"]);
                }
            }
        },
    },
    {
        fingerprint: [
            {
                type: "Router",
                manufacturerName: "Shelly",
                modelID: "2PM",
                endpoints: [
                    { ID: 1, profileID: 260, deviceID: 266, inputClusters: [0, 3, 4, 5, 6, 2820, 1794], outputClusters: [] },
                    { ID: 2, profileID: 260, deviceID: 266, inputClusters: [4, 5, 6, 2820, 1794], outputClusters: [] },
                    { ID: 239, profileID: 49153, deviceID: 8193, inputClusters: [64513, 64514], outputClusters: [] },
                    { ID: 242, profileID: 41440, deviceID: 97, inputClusters: [], outputClusters: [33] },
                ],
            },
            {
                type: "Router",
                manufacturerName: "Shelly",
                modelID: "2PM",
                endpoints: [
                    { ID: 1, profileID: 260, deviceID: 266, inputClusters: [0, 3, 4, 5, 6, 2820, 1794], outputClusters: [25] },
                    { ID: 2, profileID: 260, deviceID: 266, inputClusters: [4, 5, 6, 2820, 1794], outputClusters: [] },
                    { ID: 3, inputClusters: [7], outputClusters: [3, 4, 5, 6] },
                    { ID: 4, inputClusters: [7], outputClusters: [3, 4, 5, 6] },
                    { ID: 5, inputClusters: [], outputClusters: [3, 4, 6, 8, 258] },
                    { ID: 239, profileID: 49153, deviceID: 8193, inputClusters: [64513, 64514], outputClusters: [] },
                    { ID: 242, profileID: 41440, deviceID: 97, inputClusters: [], outputClusters: [33] },
                ],
            },
        ],
        model: "S4SW-002P16EU-SWITCH",
        vendor: "Shelly",
        description: "2PM Gen4 (Switch mode)",
        ota: true,
        // The genOnOff/genScenes bindings and the switchType read in configure were added after
        // this device was first released; bump the patch version so already paired devices get
        // re-configured and their input events start arriving (same rule as the BLU remotes).
        version: "0.0.1",
        fromZigbee: [fzLocal.two_switch_inputs_events, fzLocal.two_switch_inputs_scene_events, fzLocal.switch_input_type],
        toZigbee: [tzLocal.switch_input_type],
        exposes: (device) => [
            e.action([
                "input_1_on",
                "input_1_off",
                "input_1_toggle",
                "input_1_single",
                "input_1_double",
                "input_1_triple",
                "input_1_hold",
                "input_2_on",
                "input_2_off",
                "input_2_toggle",
                "input_2_single",
                "input_2_double",
                "input_2_triple",
                "input_2_hold",
            ]),
            ...shellySwitchInputExposes(device, { sw1: 3, sw2: 4 }),
        ],
        extend: [
            shellyDeviceEndpoints({ l1: 1, l2: 2, sw1: 3, sw2: 4 }),
            m.onOff({ powerOnBehavior: false, endpointNames: ["l1", "l2"] }),
            m.electricityMeter({ producedEnergy: true, acFrequency: true, endpointNames: ["l1", "l2"] }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["2PMSwitchInputMode"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
        configure: async (device, coordinatorEndpoint) => {
            for (const epID of [3, 4]) {
                const ep = device.getEndpoint(epID);
                if (ep) {
                    await ep.bind("genOnOff", coordinatorEndpoint);
                    await ep.bind("genScenes", coordinatorEndpoint);
                    await ep.read("genOnOffSwitchCfg", ["switchType"]);
                }
            }
        },
    },
    {
        fingerprint: [{ modelID: "Plug US", manufacturerName: "Shelly" }],
        model: "S4PL-00116US",
        vendor: "Shelly",
        description: "Plug US Gen4",
        extend: [
            m.onOff({ powerOnBehavior: false }),
            m.electricityMeter(),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [{ modelID: "Power Strip", manufacturerName: "Shelly" }],
        model: "S4PL-00416EU",
        vendor: "Shelly",
        description: "Power strip 4 Gen4",
        version: "0.0.1",
        extend: [
            m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3, "4": 4 } }),
            m.onOff({ powerOnBehavior: false, endpointNames: ["1", "2", "3", "4"] }),
            m.electricityMeter({
                endpointNames: ["1", "2", "3", "4"],
                // Reduce reporting to prevent crashes
                // https://github.com/Koenkk/zigbee2mqtt/issues/31183
                acFrequency: { change: 125 },
                current: { change: 60 },
                voltage: { change: 625 },
                power: { change: 6 },
                energy: { change: 125000 },
            }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["PowerstripUI", "PowerstripPowerOnBehavior"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [
            { modelID: "Flood", manufacturerName: "Shelly" },
            { modelID: "Flood S", manufacturerName: "Shelly" },
        ],
        model: "S4SN-0071A",
        vendor: "Shelly",
        description: "Flood Gen 4",
        whiteLabel: [
            {
                vendor: "Shelly",
                model: "S4SN-0071Z",
                description: "Flood S Gen 4",
                fingerprint: [{ modelID: "Flood S", manufacturerName: "Shelly" }],
            },
        ],
        extend: [
            m.battery({ percentageReportingConfig: false }),
            m.iasZoneAlarm({ zoneType: "water_leak", zoneAttributes: ["alarm_1", "tamper", "battery_low", "trouble"] }),
            ...shellyModernExtend.shellyCustomClusters(),
        ],
    },
    {
        fingerprint: [{ modelID: "Ecowitt WS90", manufacturerName: "Shelly" }],
        model: "WS90",
        vendor: "Shelly",
        description: "Weather station",
        extend: [
            m.battery({ voltage: true }),
            m.numeric({
                name: "capacitor_voltage",
                cluster: "genPowerCfg",
                attribute: "battery2Voltage",
                description: "Capacitor Voltage",
                unit: "V",
                scale: 10,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                access: "STATE_GET",
                entityCategory: "diagnostic",
            }),
            m.illuminance(),
            m.temperature(),
            m.pressure(),
            m.humidity(),
            m.deviceAddCustomCluster("shellyWS90Wind", {
                name: "shellyWS90Wind",
                ID: 0xfc01,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    windSpeed: { name: "windSpeed", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT16 },
                    windDirection: { name: "windDirection", ID: 0x0004, type: zigbee_herdsman_1.Zcl.DataType.UINT16 },
                    gustSpeed: { name: "gustSpeed", ID: 0x0007, type: zigbee_herdsman_1.Zcl.DataType.UINT16 },
                },
                commands: {},
                commandsResponse: {},
            }),
            m.numeric({
                name: "wind_speed",
                cluster: "shellyWS90Wind",
                attribute: "windSpeed",
                fzConvert: (model, msg) => ws90Scaled("wind_speed", msg.data.windSpeed, 0xffff),
                valueMin: 0,
                valueMax: 140,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Wind speed in m/s",
                scale: 10,
                unit: "m/s",
                access: "STATE_GET",
            }),
            m.numeric({
                name: "wind_direction",
                cluster: "shellyWS90Wind",
                attribute: "windDirection",
                fzConvert: (model, msg) => ws90Scaled("wind_direction", msg.data.windDirection, 0xffff),
                valueMin: 0,
                valueMax: 360,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Wind direction in degrees",
                scale: 10,
                unit: "°",
                access: "STATE_GET",
            }),
            m.numeric({
                name: "gust_speed",
                cluster: "shellyWS90Wind",
                attribute: "gustSpeed",
                fzConvert: (model, msg) => ws90Scaled("gust_speed", msg.data.gustSpeed, 0xffff),
                valueMin: 0,
                valueMax: 140,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Gust speed in m/s",
                scale: 10,
                unit: "m/s",
                access: "STATE_GET",
            }),
            m.deviceAddCustomCluster("shellyWS90UV", {
                name: "shellyWS90UV",
                ID: 0xfc02,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    uvIndex: { name: "uvIndex", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                },
                commands: {},
                commandsResponse: {},
            }),
            m.numeric({
                name: "uv_index",
                cluster: "shellyWS90UV",
                attribute: "uvIndex",
                fzConvert: (model, msg) => ws90Scaled("uv_index", msg.data.uvIndex, 0xff),
                valueMin: 0,
                valueMax: 11,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "UV index",
                scale: 10,
                access: "STATE_GET",
            }),
            m.deviceAddCustomCluster("shellyWS90Rain", {
                name: "shellyWS90Rain",
                ID: 0xfc03,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    rainStatus: { name: "rainStatus", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN },
                    precipitation: { name: "precipitation", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT24 },
                },
                commands: {},
                commandsResponse: {},
            }),
            m.binary({
                name: "rain_status",
                cluster: "shellyWS90Rain",
                attribute: "rainStatus",
                valueOn: [true, 1],
                valueOff: [false, 0],
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Rain status",
                access: "STATE_GET",
            }),
            m.numeric({
                name: "precipitation",
                cluster: "shellyWS90Rain",
                attribute: "precipitation",
                fzConvert: (model, msg) => ws90Scaled("precipitation", msg.data.precipitation, 0xffffff),
                valueMin: 0,
                valueMax: 100000,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Precipitation",
                unit: "mm",
                scale: 10,
                access: "STATE_GET",
            }),
            // Calculated values (added by PR #11437)
            shellyModernExtend.ws90CalculatedValues(),
        ],
    },
    {
        fingerprint: [
            { modelID: "Dimmer", manufacturerName: "Shelly" },
            { modelID: "Dimmer US", manufacturerName: "Shelly" },
        ],
        model: "S4DM-0A101WWL",
        vendor: "Shelly",
        description: "Dimmer Gen4",
        extend: [
            m.light({ configureReporting: true }),
            m.electricityMeter(),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [{ modelID: "Dimmer 0-1/10", manufacturerName: "Shelly" }],
        model: "S4DM-0010WW",
        vendor: "Shelly",
        description: "Dimmer 0/1-10V PM Gen4",
        extend: [
            m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3, "4": 4, "239": 239 } }),
            m.light(),
            m.electricityMeter(),
            m.commandsOnOff({ endpointNames: ["2", "3", "4"] }),
            m.commandsWindowCovering({ endpointNames: ["4"] }),
            m.commandsLevelCtrl({ endpointNames: ["4"] }),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [{ modelID: "BLU H&T ZB", manufacturerName: "Shelly" }],
        model: "SBHT-203C",
        vendor: "Shelly",
        description: "Humidity & temperature sensor",
        extend: [m.battery(), m.temperature(), m.humidity()],
    },
    {
        fingerprint: [{ modelID: "BLU H&T Display ZB", manufacturerName: "Shelly" }],
        model: "SBHT-103C",
        vendor: "Shelly",
        description: "BLU H&T display Zigbee",
        extend: [m.battery(), m.temperature(), m.humidity(), ...shellyModernExtend.shellyLightLevel()],
    },
    {
        fingerprint: [{ modelID: "BLU Remote Control ZB", manufacturerName: "Shelly" }],
        model: "SBRC-005B-B",
        vendor: "Shelly",
        description: "BLU Remote Control ZB",
        exposes: [
            e.action(["on", "off", "brightness_step_up", "brightness_step_down"]),
            e.numeric("action_group", ea.STATE).withDescription("Group ID associated with the action command."),
            e.numeric("action_step_size", ea.STATE).withDescription("Step size value used for brightness step actions."),
            e.numeric("action_transition_time", ea.STATE).withDescription("Transition time in seconds for the action."),
        ],
        extend: [
            m.battery(),
            m.commandsOnOff({ commands: ["on", "off"] }),
            m.commandsLevelCtrl({ commands: ["brightness_step_up", "brightness_step_down"] }),
            m.identify(),
        ],
    },
    {
        fingerprint: [{ modelID: "BLU Button Tough 1 ZB", manufacturerName: "Shelly" }],
        model: "SBBT-102C",
        vendor: "Shelly",
        description: "BLU Button Tough 1 ZB",
        fromZigbee: [fzLocal.one_button_events, fzLocal.one_button_scene_events],
        exposes: [e.action(["single", "double", "triple", "single_long", "double_long", "triple_long"])],
        extend: [m.battery(), m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3 } }), m.identify()],
        version: "0.0.2",
        configure: async (device, coordinatorEndpoint, definition) => {
            for (const endpoint of device.endpoints) {
                await endpoint.bind("genOnOff", coordinatorEndpoint);
                await endpoint.bind("genScenes", coordinatorEndpoint);
            }
        },
    },
    {
        zigbeeModel: ["BLU RC Button 4 ZB", "BLU Wall Switch 4 ZB", "BLU Wall Switch 4 ZB DK"],
        model: "SBBT-104CUS",
        vendor: "Shelly",
        description: "BLU RC Button 4 ZB",
        whiteLabel: [
            { vendor: "Shelly", model: "SBBT-004CEU", fingerprint: [{ modelID: "BLU Wall Switch 4 ZB" }], description: "BLU Wall Switch 4 ZB" },
            { vendor: "Shelly", model: "SBBT-104CEU", fingerprint: [{ modelID: "BLU Wall Switch 4 ZB DK" }], description: "BLU Wall Switch 4 ZB DK" },
        ],
        fromZigbee: [fzLocal.four_buttons_single_events, fzLocal.four_buttons_hold_events, fzLocal.four_buttons_scene_events],
        exposes: [
            e.action([
                "1_single",
                "2_single",
                "3_single",
                "4_single",
                "1_double",
                "2_double",
                "3_double",
                "4_double",
                "1_triple",
                "2_triple",
                "3_triple",
                "4_triple",
                "1_single_long",
                "2_single_long",
                "3_single_long",
                "4_single_long",
                "1_double_long",
                "2_double_long",
                "3_double_long",
                "4_double_long",
                "1_triple_long",
                "2_triple_long",
                "3_triple_long",
                "4_triple_long",
                "1_hold",
                "2_hold",
                "3_hold",
                "4_hold",
            ]),
        ],
        extend: [m.battery(), m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3, "4": 4 } }), m.identify()],
        version: "0.0.2",
        configure: async (device, coordinatorEndpoint, definition) => {
            for (const endpoint of device.endpoints) {
                await endpoint.bind("genOnOff", coordinatorEndpoint);
                await endpoint.bind("genLevelCtrl", coordinatorEndpoint);
                await endpoint.bind("genScenes", coordinatorEndpoint);
            }
        },
    },
    {
        zigbeeModel: ["BLU TRV"],
        model: "SBTR-001AEU",
        vendor: "Shelly",
        description: "Thermostatic radiator valve",
        fromZigbee: [
            fz.thermostat,
            fzShellyTRVExternalTemperature,
            {
                cluster: "hvacThermostat",
                type: ["attributeReport", "readResponse"],
                convert: (model, msg, publish, options, meta) => {
                    if (msg.data.alarmMask === undefined)
                        return;
                    return { calibration_ok: !((msg.data.alarmMask & (1 << 2)) > 0) };
                },
            },
        ],
        toZigbee: [
            tzShellyTRVExternalTemperature,
            {
                key: ["calibrate"],
                convertSet: async (entity, key, value, meta) => {
                    await entity.command("shellyTRVManualMode", "calibrate", {}, { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY });
                },
            },
        ],
        exposes: [
            e.binary("calibration_ok", ea.STATE, true, false).withDescription("Calibration OK").withCategory("diagnostic"),
            e.enum("calibrate", ea.SET, ["trigger"]).withDescription("Trigger valve calibration").withCategory("config"),
            e
                .numeric("external_temperature", ea.STATE_SET)
                .withUnit("°C")
                .withValueMin(-40)
                .withValueMax(85)
                .withValueStep(0.01)
                .withDescription("External room temperature used by the thermostat"),
        ],
        extend: [
            m.battery(),
            m.thermostat({
                localTemperatureCalibration: { values: { min: -10, max: 10, step: 0.1 } },
                setpoints: {
                    values: {
                        occupiedHeatingSetpoint: { min: 4, max: 30, step: 0.1 },
                        unoccupiedHeatingSetpoint: { min: 4, max: 30, step: 0.1 },
                    },
                },
                setpointsLimit: {
                    minHeatSetpointLimit: { min: 4, max: 30, step: 0.1 },
                    maxHeatSetpointLimit: { min: 4, max: 30, step: 0.1 },
                },
                systemMode: { values: ["off", "auto", "heat"] },
                piHeatingDemand: { values: true },
            }),
            m.deviceAddCustomCluster("shellyTRVManualMode", {
                name: "shellyTRVManualMode",
                ID: 0xfc24,
                manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHELLY,
                attributes: {
                    manualMode: { name: "manualMode", ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                    position: { name: "position", ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                },
                commands: {
                    calibrate: { name: "calibrate", ID: 0x0000, parameters: [] },
                },
                commandsResponse: {},
            }),
            m.binary({
                name: "manual_mode",
                cluster: "shellyTRVManualMode",
                attribute: { ID: 0x0000, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                valueOn: [true, 1],
                valueOff: [false, 0],
                description: "Manual mode (0 = auto, 1 = manual)",
                access: "ALL",
            }),
            m.numeric({
                name: "valve_position",
                cluster: "shellyTRVManualMode",
                attribute: { ID: 0x0001, type: zigbee_herdsman_1.Zcl.DataType.UINT8 },
                valueMin: 0,
                valueMax: 100,
                reporting: { min: "10_SECONDS", max: "1_HOUR", change: 1 },
                description: "Valve position (0-100%)",
                unit: "%",
                access: "ALL",
            }),
            m.identify(),
        ],
        onEvent: shellyTRVExternalTemperatureOnEvent,
    },
    {
        fingerprint: [{ modelID: "Presence", manufacturerName: "Shelly" }],
        model: "S4SN-0U61X",
        vendor: "Shelly",
        description: "Presence Gen4 Zigbee",
        // The occupancy endpoints only report once reporting is configured on all ten of them, which
        // this definition sets up. Devices paired before this version have no such configuration, so
        // bump the patch version to have the application re-configure them.
        version: "0.0.1",
        extend: [
            m.deviceEndpoints({ endpoints: { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10 } }),
            shellyModernExtend.shellyPresenceOccupancy(),
            ...shellyModernExtend.shellyLightLevel(),
            m.identify(),
            ...shellyModernExtend.shellyCustomClusters(),
            shellyModernExtend.shellyRPCSetup(["PresenceZonesAuto", "PresenceZoneConfig", "PresenceSensorConfig", "EcoMode"]),
            shellyModernExtend.shellyWiFiSetup(),
        ],
    },
    {
        fingerprint: [{ modelID: "BLU DoorWindow ZB", manufacturerName: "Shelly" }],
        model: "SBDW-103C",
        vendor: "Shelly",
        description: "BLU DoorWindow ZB",
        fromZigbee: [fzLocal.blu_door_window],
        toZigbee: [],
        exposes: [e.contact(), handlePosition, e.battery_low()],
        extend: [m.battery(), ...shellyModernExtend.shellyLightLevel(), m.identify()],
        configure: async (device, coordinatorEndpoint) => {
            const endpoint = device.getEndpoint(1);
            if (endpoint) {
                await endpoint.read("ssIasZone", ["zoneStatus"]);
            }
        },
    },
    {
        fingerprint: [{ modelID: "BLU Motion ZB", manufacturerName: "Shelly" }],
        model: "SBMO-103Z",
        vendor: "Shelly",
        description: "BLU Motion ZB",
        extend: [
            m.iasZoneAlarm({ zoneType: "occupancy", zoneAttributes: ["alarm_1", "battery_low"] }),
            m.battery(),
            ...shellyModernExtend.shellyLightLevel(),
            m.enumLookup({
                name: "motion_sensitivity",
                cluster: "ssIasZone",
                attribute: "currentZoneSensitivityLevel",
                lookup: { low: 1, medium: 2, high: 3 },
                description: "Motion sensor sensitivity",
                access: "ALL",
                reporting: false,
                entityCategory: "config",
            }),
            m.identify(),
        ],
        configure: async (device, coordinatorEndpoint) => {
            const endpoint = device.getEndpoint(1);
            if (endpoint) {
                await endpoint.read("ssIasZone", ["currentZoneSensitivityLevel"]);
            }
        },
    },
];
//# sourceMappingURL=shelly.js.map