// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {Zdo} from "zigbee-herdsman";
import * as data from "../mocks/data";
import {mockFflateZip, mockFflateZipFailOnce} from "../mocks/fflate";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";

import assert from "node:assert";
import fs from "node:fs";
import {platform} from "node:os";
import path from "node:path";
import {stringify} from "../../lib/util/stringify";
import type {Mock} from "vitest";
import {Controller} from "../../lib/controller";
import Bridge from "../../lib/extension/bridge";
import * as settings from "../../lib/util/settings";
import utils, {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
import {Zcl} from "zigbee-herdsman";

returnDevices.push(devices.coordinator.ieeeAddr);
returnDevices.push(devices.bulb.ieeeAddr);
returnDevices.push(devices.unsupported.ieeeAddr);
returnDevices.push(devices.WXKG11LM.ieeeAddr);
returnDevices.push(devices.remote.ieeeAddr);
returnDevices.push(devices.ZNCZ02LM.ieeeAddr);
returnDevices.push(devices.bulb_color_2.ieeeAddr);
returnDevices.push(devices.WSDCGQ11LM.ieeeAddr);
returnDevices.push(devices.zigfred_plus.ieeeAddr);
returnDevices.push(devices.bulb_custom_cluster.ieeeAddr);

const mocksClear = [
    mockLogger.info,
    mockLogger.warning,
    mockMQTTPublishAsync,
    mockZHController.permitJoin,
    devices.bulb.interview,
    devices.bulb.removeFromDatabase,
    devices.bulb.removeFromNetwork,
];

const deviceIconsDir = path.join(data.mockDir, "device_icons");

vi.mock("node:os", async (importOriginal) => ({
    ...(await importOriginal()),
    version: vi.fn(() => "Linux"),
    release: vi.fn(() => "0.0.1"),
    arch: vi.fn(() => "x64"),
    cpus: vi.fn(() => [{model: "Intel Core i7-9999"}]),
    totalmem: vi.fn(() => 10485760),
}));
vi.mock("node:process", async (importOriginal) => ({
    ...(await importOriginal()),
    version: "v1.2.3",
}));

describe("Extension: Bridge", () => {
    let controller: Controller;
    let mockRestart: Mock;
    let extension: Bridge;

    const resetExtension = async (): Promise<void> => {
        await controller.removeExtension(controller.getExtension("Bridge")!);
        await controller.addExtension(new Bridge(...controller.extensionArgs));
        extension = controller.getExtension("Bridge")! as Bridge;
    };

    beforeAll(async () => {
        vi.useFakeTimers();
        mockRestart = vi.fn();
        controller = new Controller(mockRestart, vi.fn());
        await controller.start();
        await flushPromises();
        extension = controller.getExtension("Bridge")! as Bridge;
    });

    beforeEach(() => {
        // @ts-expect-error private
        controller.mqtt.client.reconnecting = false;
        // @ts-expect-error private
        controller.mqtt.client.disconnecting = false;
        // @ts-expect-error private
        controller.mqtt.client.disconnected = false;
        data.writeDefaultConfiguration();
        settings.reRead();
        data.writeDefaultState();
        for (const mock of mocksClear) mock.mockClear();
        mockLogger.setTransportsEnabled(false);
        // @ts-expect-error private
        extension.lastJoinedDeviceIeeeAddr = undefined;
        // @ts-expect-error private
        extension.restartRequired = false;
        // @ts-expect-error private
        controller.state.state = new Map([[devices.bulb.ieeeAddr, {brightness: 50}]]);
        fs.rmSync(deviceIconsDir, {force: true, recursive: true});
    });

    afterAll(async () => {
        await controller?.stop();
        await flushPromises();
        vi.useRealTimers();
    });

    it("Should publish bridge info on startup", async () => {
        await resetExtension();
        const version = await utils.getZigbee2MQTTVersion();
        const zhVersion = await utils.getDependencyVersion("zigbee-herdsman");
        const zhcVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
        const directory = settings.get().advanced.log_directory;
        // console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "zigbee2mqtt/bridge/info")![1]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/info",
            stringify({
                commit: version.commitHash,
                config: {
                    health: {
                        interval: 10,
                        reset_on_check: false,
                    },
                    advanced: {
                        adapter_concurrent: undefined,
                        adapter_delay: undefined,
                        cache_state: true,
                        cache_state_persistent: true,
                        cache_state_send_on_startup: true,
                        channel: 11,
                        elapsed: false,
                        ext_pan_id: [221, 221, 221, 221, 221, 221, 221, 221],
                        last_seen: "disable",
                        log_debug_namespace_ignore: "",
                        log_debug_to_mqtt_frontend: false,
                        log_directory: directory,
                        log_file: "log.log",
                        log_level: "info",
                        log_namespaced_levels: {},
                        log_output: ["console", "file"],
                        log_console_json: false,
                        log_rotation: true,
                        log_symlink_current: false,
                        log_syslog: {},
                        log_directories_to_keep: 10,
                        output: "json",
                        pan_id: 6754,
                        timestamp_format: "YYYY-MM-DD HH:mm:ss",
                        enable_external_js: true,
                    },
                    blocklist: [],
                    device_options: {},
                    devices: {
                        "0x000b57cdfec6a5b3": {friendly_name: "hue_twilight"},
                        "0x000b57fffec6a5b2": {
                            description: "this is my bulb",
                            friendly_name: "bulb",
                            retain: true,
                        },
                        "0x000b57fffec6a5b3": {friendly_name: "bulb_color", retain: false},
                        "0x000b57fffec6a5b4": {
                            friendly_name: "bulb_color_2",
                            retain: false,
                        },
                        "0x000b57fffec6a5b7": {friendly_name: "bulb_2", retain: false},
                        "0x00124b00cfcf3298": {friendly_name: "fanbee", retain: true},
                        "0x0017880104a44559": {friendly_name: "J1_cover"},
                        "0x0017880104e43559": {friendly_name: "U202DST600ZB"},
                        "0x0017880104e44559": {friendly_name: "3157100_thermostat"},
                        "0x18fc2600000d7ae3": {friendly_name: "bosch_rm230z"},
                        "0x0017880104e45517": {friendly_name: "remote", retain: true},
                        "0x0017880104e45520": {friendly_name: "button", retain: false},
                        "0x0017880104e45521": {
                            friendly_name: "button_double_key",
                            retain: false,
                        },
                        "0x0017880104e45522": {
                            friendly_name: "weather_sensor",
                            qos: 1,
                            retain: false,
                        },
                        "0x0017880104e45523": {
                            friendly_name: "occupancy_sensor",
                            retain: false,
                        },
                        "0x0017880104e45524": {friendly_name: "power_plug", retain: false},
                        "0x0017880104e45526": {friendly_name: "GL-S-007ZS"},
                        "0x0017880104e45529": {
                            friendly_name: "unsupported2",
                            retain: false,
                        },
                        "0x0017880104e45530": {
                            friendly_name: "button_double_key_interviewing",
                            retain: false,
                        },
                        "0x0017880104e45540": {friendly_name: "ikea_onoff"},
                        "0x0017880104e45541": {friendly_name: "wall_switch", retain: false},
                        "0x0017880104e45542": {
                            friendly_name: "wall_switch_double",
                            retain: false,
                        },
                        "0x0017880104e45543": {
                            friendly_name: "led_controller_1",
                            retain: false,
                        },
                        "0x0017880104e45544": {
                            friendly_name: "led_controller_2",
                            retain: false,
                        },
                        "0x0017880104e45545": {
                            friendly_name: "dimmer_wall_switch",
                            retain: false,
                        },
                        "0x0017880104e45547": {friendly_name: "curtain", retain: false},
                        "0x0017880104e45548": {friendly_name: "fan", retain: false},
                        "0x0017880104e45549": {friendly_name: "siren", retain: false},
                        "0x0017880104e45550": {friendly_name: "thermostat", retain: false},
                        "0x0017880104e45551": {friendly_name: "smart vent", retain: false},
                        "0x0017880104e45552": {friendly_name: "j1", retain: false},
                        "0x0017880104e45553": {
                            friendly_name: "bulb_enddevice",
                            retain: false,
                        },
                        "0x0017880104e45559": {
                            friendly_name: "cc2530_router",
                            retain: false,
                        },
                        "0x0017880104e45560": {friendly_name: "livolo", retain: false},
                        "0x0017880104e45561": {friendly_name: "temperature_sensor"},
                        "0x0017880104e45562": {friendly_name: "heating_actuator"},
                        "0x0017880104e45724": {friendly_name: "GLEDOPTO_2ID"},
                        "0x0017882104a44559": {friendly_name: "TS0601_thermostat"},
                        "0x0017882104a44560": {friendly_name: "TS0601_switch"},
                        "0x0017882104a44562": {friendly_name: "TS0601_cover_switch"},
                        "0x0017882194e45543": {friendly_name: "QS-Zigbee-D02-TRIAC-2C-LN"},
                        "0x18fc2600000d7ae2": {friendly_name: "bosch_radiator"},
                        "0x90fd9ffffe4b64aa": {friendly_name: "SP600_OLD"},
                        "0x90fd9ffffe4b64ab": {friendly_name: "SP600_NEW"},
                        "0x90fd9ffffe4b64ac": {friendly_name: "MKS-CM-W5"},
                        "0x90fd9ffffe4b64ae": {
                            friendly_name: "tradfri_remote",
                            retain: false,
                        },
                        "0x90fd9ffffe4b64af": {friendly_name: "roller_shutter"},
                        "0x90fd9ffffe4b64ax": {friendly_name: "ZNLDP12LM"},
                        "0xf4ce368a38be56a1": {
                            cover_1_enabled: "true",
                            cover_1_tilt_enabled: "true",
                            cover_2_enabled: "true",
                            cover_2_tilt_enabled: "true",
                            dimmer_1_dimming_enabled: "true",
                            dimmer_1_enabled: "true",
                            dimmer_2_dimming_enabled: "true",
                            dimmer_2_enabled: "true",
                            dimmer_3_dimming_enabled: "true",
                            dimmer_3_enabled: "true",
                            dimmer_4_dimming_enabled: "true",
                            dimmer_4_enabled: "true",
                            friendly_name: "zigfred_plus",
                            front_surface_enabled: "true",
                            retain: false,
                        },
                    },
                    groups: {
                        1: {friendly_name: "group_1", retain: false},
                        11: {friendly_name: "group_with_tradfri", retain: false},
                        12: {friendly_name: "thermostat_group", retain: false},
                        14: {friendly_name: "switch_group", retain: false},
                        15071: {friendly_name: "group_tradfri_remote", retain: false},
                        19: {friendly_name: "hue_twilight_group"},
                        2: {friendly_name: "group_2", retain: false},
                        21: {friendly_name: "gledopto_group"},
                        9: {friendly_name: "ha_discovery_group"},
                    },
                    homeassistant: {
                        enabled: false,
                        discovery_topic: "homeassistant",
                        status_topic: "homeassistant/status",
                        legacy_action_sensor: false,
                        experimental_event_entities: false,
                    },
                    availability: {
                        enabled: false,
                        active: {
                            timeout: 10,
                            max_jitter: 30000,
                            backoff: true,
                            pause_on_backoff_gt: 0,
                        },
                        passive: {timeout: 1500},
                    },
                    frontend: {
                        enabled: false,
                        package: "zigbee2mqtt-windfront",
                        port: 8080,
                        base_url: "/",
                    },
                    map_options: {
                        graphviz: {
                            colors: {
                                fill: {
                                    coordinator: "#e04e5d",
                                    enddevice: "#fff8ce",
                                    router: "#4ea3e0",
                                },
                                font: {
                                    coordinator: "#ffffff",
                                    enddevice: "#000000",
                                    router: "#ffffff",
                                },
                                line: {active: "#009900", inactive: "#994444"},
                            },
                        },
                    },
                    mqtt: {
                        base_topic: "zigbee2mqtt",
                        force_disable_retain: false,
                        include_device_information: false,
                        maximum_packet_size: 1048576,
                        keepalive: 60,
                        reject_unauthorized: true,
                        version: 4,
                        server: "mqtt://localhost",
                    },
                    ota: {
                        default_maximum_data_size: 50,
                        disable_automatic_update_check: false,
                        image_block_request_timeout: 150000,
                        image_block_response_delay: 250,
                        update_check_interval: 1440,
                    },
                    passlist: [],
                    serial: {disable_led: false, port: "/dev/dummy"},
                },
                config_schema: settings.schemaJson,
                coordinator: {ieee_address: "0x00124b00120144ae", meta: {revision: 20190425, version: 1}, type: "z-Stack"},
                log_level: "info",
                network: {channel: 15, extended_pan_id: "0x64c5fd698daf0c00", pan_id: 5674},
                permit_join: false,
                permit_join_end: undefined,
                restart_required: false,
                version: version.version,
                zigbee_herdsman: zhVersion,
                zigbee_herdsman_converters: zhcVersion,
                os: {
                    version: "Linux - 0.0.1 - x64",
                    node_version: "v1.2.3",
                    cpus: "Intel Core i7-9999 (x1)",
                    memory_mb: 10,
                },
                mqtt: {
                    server: "mqtt://localhost:1883",
                    version: 5,
                },
            }),
            {retain: true},
        );
    });

    it("Should publish devices on startup", async () => {
        await resetExtension();
        // console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "zigbee2mqtt/bridge/devices")[1]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/devices",
            stringify([
                {
                    disabled: false,
                    endpoints: {"1": {bindings: [], clusters: {input: [], output: []}, configured_reportings: [], scenes: []}},
                    friendly_name: "Coordinator",
                    ieee_address: "0x00124b00120144ae",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    network_address: 0,
                    supported: true,
                    type: "Coordinator",
                },
                {
                    definition: {
                        source: "native",
                        description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
                        exposes: [
                            {
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        label: "State",
                                        name: "state",
                                        property: "state",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature of this light",
                                        label: "Color temp",
                                        name: "color_temp",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                        ],
                                        property: "color_temp",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature after cold power on of this light",
                                        label: "Color temp startup",
                                        name: "color_temp_startup",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                            {description: "Restore previous color_temp on cold power on", name: "previous", value: 65535},
                                        ],
                                        property: "color_temp_startup",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Configure genLevelCtrl",
                                        features: [
                                            {
                                                access: 7,
                                                description:
                                                    'this setting can affect the "on_level", "current_level_startup" or "brightness" setting',
                                                label: "Execute if off",
                                                name: "execute_if_off",
                                                property: "execute_if_off",
                                                type: "binary",
                                                value_off: false,
                                                value_on: true,
                                            },
                                            {
                                                access: 7,
                                                description: "Defines the desired startup level for a device when it is supplied with power",
                                                label: "Current level startup",
                                                name: "current_level_startup",
                                                presets: [
                                                    {description: "Use minimum permitted value", name: "minimum", value: "minimum"},
                                                    {description: "Use previous value", name: "previous", value: "previous"},
                                                ],
                                                property: "current_level_startup",
                                                type: "numeric",
                                                value_max: 254,
                                                value_min: 1,
                                            },
                                        ],
                                        label: "Level config",
                                        name: "level_config",
                                        property: "level_config",
                                        type: "composite",
                                    },
                                ],
                                type: "light",
                            },
                            {
                                access: 2,
                                description: "Triggers an effect on the light (e.g. make light blink for a few seconds)",
                                label: "Effect",
                                name: "effect",
                                property: "effect",
                                type: "enum",
                                values: ["blink", "breathe", "okay", "channel_change", "finish_effect", "stop_effect"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Controls the behavior when the device is powered on after power loss",
                                label: "Power-on behavior",
                                name: "power_on_behavior",
                                property: "power_on_behavior",
                                type: "enum",
                                values: ["off", "on", "toggle", "previous"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Advanced color behavior",
                                features: [
                                    {
                                        access: 2,
                                        description: "Controls whether color and color temperature can be set while light is off",
                                        label: "Execute if off",
                                        name: "execute_if_off",
                                        property: "execute_if_off",
                                        type: "binary",
                                        value_off: false,
                                        value_on: true,
                                    },
                                ],
                                label: "Color options",
                                name: "color_options",
                                property: "color_options",
                                type: "composite",
                            },
                            {
                                access: 2,
                                category: "config",
                                description: "Initiate device identification",
                                label: "Identify",
                                name: "identify",
                                property: "identify",
                                type: "enum",
                                values: ["identify"],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "LED1545G12",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).",
                                label: "Transition",
                                name: "transition",
                                property: "transition",
                                type: "numeric",
                                value_min: 0,
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Whether to unfreeze IKEA lights (that are known to be frozen) before issuing a command, false: no unfreeze support, true: unfreeze support (default true).",
                                label: "Unfreeze support",
                                name: "unfreeze_support",
                                property: "unfreeze_support",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
                                label: "Color sync",
                                name: "color_sync",
                                property: "color_sync",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).",
                                label: "Identify timeout",
                                name: "identify_timeout",
                                property: "identify_timeout",
                                type: "numeric",
                                value_max: 30,
                                value_min: 1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: true,
                        vendor: "IKEA",
                        version: "0.0.0",
                    },
                    description: "this is my bulb",
                    disabled: false,
                    endpoints: {
                        "1": {
                            bindings: [],
                            clusters: {
                                input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl", "lightingColorCtrl"],
                                output: ["genScenes", "genOta"],
                            },
                            configured_reportings: [
                                {
                                    attribute: "onOff",
                                    cluster: "genOnOff",
                                    maximum_report_interval: 10,
                                    minimum_report_interval: 1,
                                    reportable_change: 20,
                                },
                            ],
                            scenes: [],
                        },
                    },
                    friendly_name: "bulb",
                    ieee_address: "0x000b57fffec6a5b2",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "TRADFRI bulb E27 WS opal 980lm",
                    network_address: 40369,
                    power_source: "Mains (single phase)",
                    supported: true,
                    type: "Router",
                },
                {
                    date_code: "2019.09",
                    definition: {
                        source: "native",
                        description: "Hue Go",
                        exposes: [
                            {
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        label: "State",
                                        name: "state",
                                        property: "state",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature of this light",
                                        label: "Color temp",
                                        name: "color_temp",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 150},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 500},
                                        ],
                                        property: "color_temp",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 500,
                                        value_min: 150,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature after cold power on of this light",
                                        label: "Color temp startup",
                                        name: "color_temp_startup",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 150},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 500},
                                            {description: "Restore previous color_temp on cold power on", name: "previous", value: 65535},
                                        ],
                                        property: "color_temp_startup",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 500,
                                        value_min: 150,
                                    },
                                    {
                                        access: 7,
                                        description: "Color of this light in the CIE 1931 color space (x/y)",
                                        features: [
                                            {access: 7, label: "X", name: "x", property: "x", type: "numeric"},
                                            {access: 7, label: "Y", name: "y", property: "y", type: "numeric"},
                                        ],
                                        label: "Color (X/Y)",
                                        name: "color_xy",
                                        property: "color",
                                        type: "composite",
                                    },
                                    {
                                        access: 7,
                                        description: "Color of this light expressed as hue/saturation",
                                        features: [
                                            {access: 7, label: "Hue", name: "hue", property: "hue", type: "numeric"},
                                            {access: 7, label: "Saturation", name: "saturation", property: "saturation", type: "numeric"},
                                        ],
                                        label: "Color (HS)",
                                        name: "color_hs",
                                        property: "color",
                                        type: "composite",
                                    },
                                ],
                                type: "light",
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Controls the behavior when the device is powered on after power loss",
                                label: "Power-on behavior",
                                name: "power_on_behavior",
                                property: "power_on_behavior",
                                type: "enum",
                                values: ["off", "on", "toggle", "previous"],
                            },
                            {
                                access: 3,
                                label: "Effect",
                                name: "effect",
                                property: "effect",
                                type: "enum",
                                values: [
                                    "blink",
                                    "breathe",
                                    "okay",
                                    "channel_change",
                                    "candle",
                                    "fireplace",
                                    "colorloop",
                                    "sunset",
                                    "sunrise",
                                    "sparkle",
                                    "opal",
                                    "glisten",
                                    "underwater",
                                    "cosmos",
                                    "sunbeam",
                                    "enchant",
                                    "none",
                                    "finish_effect",
                                    "stop_effect",
                                    "stop_hue_effect",
                                ],
                            },
                            {
                                access: 3,
                                description: "Animation speed for the active effect (0=slowest, 1=fastest)",
                                label: "Effect speed",
                                name: "effect_speed",
                                property: "effect_speed",
                                type: "numeric",
                                value_max: 1,
                                value_min: 0,
                                value_step: 0.01,
                            },
                            {
                                access: 2,
                                description:
                                    'Set the base color of the active effect without stopping it (hex e.g. #FF4400, or JSON {"x":0.6,"y":0.3})',
                                label: "Effect color",
                                name: "effect_color",
                                property: "effect_color",
                                type: "text",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "7146060PH",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Control this light using a Philips-specific protocol instead of standard Zigbee commands. When enabled, on/off, brightness, color, and color temperature are combined into single atomic commands. This is required to use the Effect color update mode. When disabled (default), standard Zigbee commands are used, which preserves the usual behavior, including simulating on/off transitions.",
                                label: "Hue native control",
                                name: "hue_native_control",
                                property: "hue_native_control",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "Controls what happens when color is changed while an effect is active (requires Hue native control). 'stop' (default): color change stops the effect (Hue app behavior). 'update': color change re-sends the effect with the new color.",
                                label: "Effect color mode",
                                name: "effect_color_mode",
                                property: "effect_color_mode",
                                type: "enum",
                                values: ["stop", "update"],
                            },
                            {
                                access: 2,
                                description:
                                    "Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).",
                                label: "Transition",
                                name: "transition",
                                property: "transition",
                                type: "numeric",
                                value_min: 0,
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
                                label: "Color sync",
                                name: "color_sync",
                                property: "color_sync",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: true,
                        vendor: "Philips",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "1": {
                            bindings: [],
                            clusters: {
                                input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl", "lightingColorCtrl"],
                                output: ["genScenes", "genOta"],
                            },
                            configured_reportings: [],
                            scenes: [{id: 1, name: "Chill scene"}],
                        },
                    },
                    friendly_name: "bulb_color_2",
                    ieee_address: "0x000b57fffec6a5b4",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    manufacturer: "Philips",
                    model_id: "LLC020",
                    network_address: 401292,
                    power_source: "Mains (single phase)",
                    software_build_id: "5.127.1.26581",
                    supported: true,
                    type: "Router",
                },
                {
                    definition: {
                        source: "native",
                        description: "Hue dimmer switch gen 1",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Remaining battery in %, can take up to 24 hours before reported",
                                label: "Battery",
                                name: "battery",
                                property: "battery",
                                type: "numeric",
                                unit: "%",
                                value_max: 100,
                                value_min: 0,
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action duration in seconds",
                                label: "Action duration",
                                name: "action_duration",
                                property: "action_duration",
                                type: "numeric",
                                unit: "s",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action (e.g. a button click)",
                                label: "Action",
                                name: "action",
                                property: "action",
                                type: "enum",
                                values: [
                                    "on_press",
                                    "on_press_release",
                                    "on_hold",
                                    "on_hold_release",
                                    "up_press",
                                    "up_press_release",
                                    "up_hold",
                                    "up_hold_release",
                                    "down_press",
                                    "down_press_release",
                                    "down_hold",
                                    "down_hold_release",
                                    "off_press",
                                    "off_press_release",
                                    "off_hold",
                                    "off_hold_release",
                                ],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "324131092621",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.",
                                features: [
                                    {
                                        access: 2,
                                        description: "Delta per interval, 20 by default",
                                        label: "Delta",
                                        name: "delta",
                                        property: "delta",
                                        type: "numeric",
                                        value_min: 0,
                                    },
                                    {
                                        access: 2,
                                        description: "Interval duration",
                                        label: "Interval",
                                        name: "interval",
                                        property: "interval",
                                        type: "numeric",
                                        unit: "ms",
                                        value_min: 0,
                                    },
                                ],
                                label: "Simulated brightness",
                                name: "simulated_brightness",
                                property: "simulated_brightness",
                                type: "composite",
                            },
                        ],
                        supports_ota: true,
                        vendor: "Philips",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "1": {
                            name: "ep1",
                            bindings: [
                                {cluster: "genLevelCtrl", target: {endpoint: 1, ieee_address: "0x000b57fffec6a5b3", type: "endpoint"}},
                                {cluster: "genOnOff", target: {endpoint: 1, ieee_address: "0x000b57fffec6a5b3", type: "endpoint"}},
                                {cluster: "lightingColorCtrl", target: {endpoint: 1, ieee_address: "0x000b57fffec6a5b3", type: "endpoint"}},
                                {cluster: "genOnOff", target: {id: 1, type: "group"}},
                                {cluster: "genLevelCtrl", target: {id: 1, type: "group"}},
                            ],
                            clusters: {input: ["genBasic"], output: ["genBasic", "genOnOff", "genLevelCtrl", "genScenes"]},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "2": {
                            name: "ep2",
                            bindings: [],
                            clusters: {input: ["genBasic"], output: ["genOta", "genOnOff"]},
                            configured_reportings: [],
                            scenes: [],
                        },
                    },
                    friendly_name: "remote",
                    ieee_address: "0x0017880104e45517",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "RWL021",
                    network_address: 6535,
                    power_source: "Battery",
                    supported: true,
                    type: "EndDevice",
                },
                {
                    definition: {
                        source: "generated",
                        description: "Automatically generated definition",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action (e.g. a button click)",
                                label: "Action",
                                name: "action",
                                property: "action",
                                type: "enum",
                                values: [
                                    "on",
                                    "off",
                                    "toggle",
                                    "brightness_move_to_level",
                                    "brightness_move_up",
                                    "brightness_move_down",
                                    "brightness_step_up",
                                    "brightness_step_down",
                                    "brightness_stop",
                                ],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "notSupportedModelID",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.",
                                features: [
                                    {
                                        access: 2,
                                        description: "Delta per interval, 20 by default",
                                        label: "Delta",
                                        name: "delta",
                                        property: "delta",
                                        type: "numeric",
                                        value_min: 0,
                                    },
                                    {
                                        access: 2,
                                        description: "Interval duration",
                                        label: "Interval",
                                        name: "interval",
                                        property: "interval",
                                        type: "numeric",
                                        unit: "ms",
                                        value_min: 0,
                                    },
                                ],
                                label: "Simulated brightness",
                                name: "simulated_brightness",
                                property: "simulated_brightness",
                                type: "composite",
                            },
                        ],
                        supports_ota: false,
                        vendor: "notSupportedMfg",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "1": {
                            bindings: [],
                            clusters: {input: ["genBasic"], output: ["genBasic", "genOnOff", "genLevelCtrl", "genScenes"]},
                            configured_reportings: [],
                            scenes: [],
                        },
                    },
                    friendly_name: "0x0017880104e45518",
                    ieee_address: "0x0017880104e45518",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    manufacturer: "notSupportedMfg",
                    model_id: "notSupportedModelID",
                    network_address: 6536,
                    power_source: "Battery",
                    supported: false,
                    type: "EndDevice",
                },
                {
                    definition: {
                        source: "native",
                        description: "Wireless mini switch",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Remaining battery in %, can take up to 24 hours before reported",
                                label: "Battery",
                                name: "battery",
                                property: "battery",
                                type: "numeric",
                                unit: "%",
                                value_max: 100,
                                value_min: 0,
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Voltage of the battery in millivolts",
                                label: "Voltage",
                                name: "voltage",
                                property: "voltage",
                                type: "numeric",
                                unit: "mV",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Temperature of the device",
                                label: "Device temperature",
                                name: "device_temperature",
                                property: "device_temperature",
                                type: "numeric",
                                unit: "°C",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Number of power outages (since last pairing)",
                                label: "Power outage count",
                                name: "power_outage_count",
                                property: "power_outage_count",
                                type: "numeric",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action (e.g. a button click)",
                                label: "Action",
                                name: "action",
                                property: "action",
                                type: "enum",
                                values: ["single", "double", "triple", "quadruple", "hold", "release"],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "WXKG11LM",
                        options: [
                            {
                                access: 2,
                                description: "Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.",
                                label: "Device temperature calibration",
                                name: "device_temperature_calibration",
                                property: "device_temperature_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                        ],
                        supports_ota: false,
                        vendor: "Aqara",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "1": {
                            bindings: [],
                            clusters: {input: ["genBasic"], output: ["genBasic", "genOnOff", "genLevelCtrl", "genScenes"]},
                            configured_reportings: [
                                {
                                    attribute: 1337,
                                    cluster: "genOnOff",
                                    maximum_report_interval: 10,
                                    minimum_report_interval: 1,
                                    reportable_change: 20,
                                },
                            ],
                            scenes: [],
                        },
                    },
                    friendly_name: "button",
                    ieee_address: "0x0017880104e45520",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "lumi.sensor_switch.aq2",
                    network_address: 6537,
                    power_source: "Battery",
                    supported: true,
                    type: "EndDevice",
                },
                {
                    definition: {
                        source: "native",
                        description: "Temperature and humidity sensor",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Remaining battery in %, can take up to 24 hours before reported",
                                label: "Battery",
                                name: "battery",
                                property: "battery",
                                type: "numeric",
                                unit: "%",
                                value_max: 100,
                                value_min: 0,
                            },
                            {
                                access: 1,
                                description: "Measured temperature value",
                                label: "Temperature",
                                name: "temperature",
                                property: "temperature",
                                type: "numeric",
                                unit: "°C",
                            },
                            {
                                access: 1,
                                description: "Measured relative humidity",
                                label: "Humidity",
                                name: "humidity",
                                property: "humidity",
                                type: "numeric",
                                unit: "%",
                            },
                            {
                                access: 1,
                                description: "The measured atmospheric pressure",
                                label: "Pressure",
                                name: "pressure",
                                property: "pressure",
                                type: "numeric",
                                unit: "hPa",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Voltage of the battery in millivolts",
                                label: "Voltage",
                                name: "voltage",
                                property: "voltage",
                                type: "numeric",
                                unit: "mV",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "WSDCGQ11LM",
                        options: [
                            {
                                access: 2,
                                description: "Calibrates the temperature value (absolute offset), takes into effect on next report of device.",
                                label: "Temperature calibration",
                                name: "temperature_calibration",
                                property: "temperature_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Number of digits after decimal point for temperature, takes into effect on next report of device. This option can only decrease the precision, not increase it.",
                                label: "Temperature precision",
                                name: "temperature_precision",
                                property: "temperature_precision",
                                type: "numeric",
                                value_max: 3,
                                value_min: 0,
                            },
                            {
                                access: 2,
                                description: "Calibrates the humidity value (absolute offset), takes into effect on next report of device.",
                                label: "Humidity calibration",
                                name: "humidity_calibration",
                                property: "humidity_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Number of digits after decimal point for humidity, takes into effect on next report of device. This option can only decrease the precision, not increase it.",
                                label: "Humidity precision",
                                name: "humidity_precision",
                                property: "humidity_precision",
                                type: "numeric",
                                value_max: 3,
                                value_min: 0,
                            },
                            {
                                access: 2,
                                description: "Calibrates the pressure value (absolute offset), takes into effect on next report of device.",
                                label: "Pressure calibration",
                                name: "pressure_calibration",
                                property: "pressure_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Number of digits after decimal point for pressure, takes into effect on next report of device. This option can only decrease the precision, not increase it.",
                                label: "Pressure precision",
                                name: "pressure_precision",
                                property: "pressure_precision",
                                type: "numeric",
                                value_max: 3,
                                value_min: 0,
                            },
                        ],
                        supports_ota: false,
                        vendor: "Aqara",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {"1": {bindings: [], clusters: {input: ["genBasic"], output: []}, configured_reportings: [], scenes: []}},
                    friendly_name: "weather_sensor",
                    ieee_address: "0x0017880104e45522",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "lumi.weather",
                    network_address: 6539,
                    power_source: "Battery",
                    supported: true,
                    type: "EndDevice",
                },
                {
                    definition: {
                        source: "native",
                        description: "Mi smart plug",
                        exposes: [
                            {
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of the switch",
                                        label: "State",
                                        name: "state",
                                        property: "state",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                ],
                                type: "switch",
                            },
                            {
                                access: 5,
                                description: "Instantaneous measured power",
                                label: "Power",
                                name: "power",
                                property: "power",
                                type: "numeric",
                                unit: "W",
                            },
                            {
                                access: 1,
                                description: "Sum of consumed energy",
                                label: "Energy",
                                name: "energy",
                                property: "energy",
                                type: "numeric",
                                unit: "kWh",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Temperature of the device",
                                label: "Device temperature",
                                name: "device_temperature",
                                property: "device_temperature",
                                type: "numeric",
                                unit: "°C",
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Enable/disable the power outage memory, this recovers the on/off mode after power failure",
                                label: "Power outage memory",
                                name: "power_outage_memory",
                                property: "power_outage_memory",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "ZNCZ02LM",
                        options: [
                            {
                                access: 2,
                                description: "Calibrates the power value (percentual offset), takes into effect on next report of device.",
                                label: "Power calibration",
                                name: "power_calibration",
                                property: "power_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Number of digits after decimal point for power, takes into effect on next report of device. This option can only decrease the precision, not increase it.",
                                label: "Power precision",
                                name: "power_precision",
                                property: "power_precision",
                                type: "numeric",
                                value_max: 3,
                                value_min: 0,
                            },
                            {
                                access: 2,
                                description: "Calibrates the energy value (percentual offset), takes into effect on next report of device.",
                                label: "Energy calibration",
                                name: "energy_calibration",
                                property: "energy_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Number of digits after decimal point for energy, takes into effect on next report of device. This option can only decrease the precision, not increase it.",
                                label: "Energy precision",
                                name: "energy_precision",
                                property: "energy_precision",
                                type: "numeric",
                                value_max: 3,
                                value_min: 0,
                            },
                            {
                                access: 2,
                                description: "Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.",
                                label: "Device temperature calibration",
                                name: "device_temperature_calibration",
                                property: "device_temperature_calibration",
                                type: "numeric",
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: true,
                        vendor: "Xiaomi",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {"1": {bindings: [], clusters: {input: ["genBasic", "genOnOff"], output: []}, configured_reportings: [], scenes: []}},
                    friendly_name: "power_plug",
                    ieee_address: "0x0017880104e45524",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "lumi.plug",
                    network_address: 6540,
                    power_source: "Mains (single phase)",
                    supported: true,
                    type: "Router",
                },
                {
                    definition: {
                        source: "native",
                        description: "zigfred plus smart in-wall switch",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action (e.g. a button click)",
                                label: "Action",
                                name: "action",
                                property: "action",
                                type: "enum",
                                values: [
                                    "button_1_single",
                                    "button_1_double",
                                    "button_1_hold",
                                    "button_1_release",
                                    "button_2_single",
                                    "button_2_double",
                                    "button_2_hold",
                                    "button_2_release",
                                    "button_3_single",
                                    "button_3_double",
                                    "button_3_hold",
                                    "button_3_release",
                                    "button_4_single",
                                    "button_4_double",
                                    "button_4_hold",
                                    "button_4_release",
                                ],
                            },
                            {
                                endpoint: "l1",
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        endpoint: "l1",
                                        label: "State",
                                        name: "state",
                                        property: "state_l1",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        endpoint: "l1",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness_l1",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Color of this light in the CIE 1931 color space (x/y)",
                                        endpoint: "l1",
                                        features: [
                                            {access: 7, label: "X", name: "x", property: "x", type: "numeric"},
                                            {access: 7, label: "Y", name: "y", property: "y", type: "numeric"},
                                        ],
                                        label: "Color (X/Y)",
                                        name: "color_xy",
                                        property: "color_l1",
                                        type: "composite",
                                    },
                                ],
                                type: "light",
                            },
                            {
                                endpoint: "l2",
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        endpoint: "l2",
                                        label: "State",
                                        name: "state",
                                        property: "state_l2",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        endpoint: "l2",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness_l2",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                ],
                                type: "light",
                            },
                            {
                                endpoint: "l3",
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        endpoint: "l3",
                                        label: "State",
                                        name: "state",
                                        property: "state_l3",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        endpoint: "l3",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness_l3",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                ],
                                type: "light",
                            },
                            {
                                endpoint: "l4",
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        endpoint: "l4",
                                        label: "State",
                                        name: "state",
                                        property: "state_l4",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        endpoint: "l4",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness_l4",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                ],
                                type: "light",
                            },
                            {
                                endpoint: "l5",
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        endpoint: "l5",
                                        label: "State",
                                        name: "state",
                                        property: "state_l5",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        endpoint: "l5",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness_l5",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                ],
                                type: "light",
                            },
                            {
                                endpoint: "l6",
                                features: [
                                    {
                                        access: 7,
                                        endpoint: "l6",
                                        label: "State",
                                        name: "state",
                                        property: "state_l6",
                                        type: "enum",
                                        values: ["OPEN", "CLOSE", "STOP"],
                                    },
                                    {
                                        access: 7,
                                        description: "Position of this cover",
                                        endpoint: "l6",
                                        label: "Position",
                                        name: "position",
                                        property: "position_l6",
                                        type: "numeric",
                                        unit: "%",
                                        value_max: 100,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Tilt of this cover",
                                        endpoint: "l6",
                                        label: "Tilt",
                                        name: "tilt",
                                        property: "tilt_l6",
                                        type: "numeric",
                                        unit: "%",
                                        value_max: 100,
                                        value_min: 0,
                                    },
                                ],
                                type: "cover",
                            },
                            {
                                endpoint: "l7",
                                features: [
                                    {
                                        access: 7,
                                        endpoint: "l7",
                                        label: "State",
                                        name: "state",
                                        property: "state_l7",
                                        type: "enum",
                                        values: ["OPEN", "CLOSE", "STOP"],
                                    },
                                    {
                                        access: 7,
                                        description: "Position of this cover",
                                        endpoint: "l7",
                                        label: "Position",
                                        name: "position",
                                        property: "position_l7",
                                        type: "numeric",
                                        unit: "%",
                                        value_max: 100,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Tilt of this cover",
                                        endpoint: "l7",
                                        label: "Tilt",
                                        name: "tilt",
                                        property: "tilt_l7",
                                        type: "numeric",
                                        unit: "%",
                                        value_max: 100,
                                        value_min: 0,
                                    },
                                ],
                                type: "cover",
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "ZFP-1A-CH",
                        options: [
                            {
                                access: 2,
                                description: "Front Surface LED enabled",
                                label: "Front surface enabled",
                                name: "front_surface_enabled",
                                property: "front_surface_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 1 enabled",
                                label: "Dimmer 1 enabled",
                                name: "dimmer_1_enabled",
                                property: "dimmer_1_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 1 dimmable",
                                label: "Dimmer 1 dimming enabled",
                                name: "dimmer_1_dimming_enabled",
                                property: "dimmer_1_dimming_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 2 enabled",
                                label: "Dimmer 2 enabled",
                                name: "dimmer_2_enabled",
                                property: "dimmer_2_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 2 dimmable",
                                label: "Dimmer 2 dimming enabled",
                                name: "dimmer_2_dimming_enabled",
                                property: "dimmer_2_dimming_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 3 enabled",
                                label: "Dimmer 3 enabled",
                                name: "dimmer_3_enabled",
                                property: "dimmer_3_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 3 dimmable",
                                label: "Dimmer 3 dimming enabled",
                                name: "dimmer_3_dimming_enabled",
                                property: "dimmer_3_dimming_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 4 enabled",
                                label: "Dimmer 4 enabled",
                                name: "dimmer_4_enabled",
                                property: "dimmer_4_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Dimmer 4 dimmable",
                                label: "Dimmer 4 dimming enabled",
                                name: "dimmer_4_dimming_enabled",
                                property: "dimmer_4_dimming_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Cover 1 enabled",
                                label: "Cover 1 enabled",
                                name: "cover_1_enabled",
                                property: "cover_1_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Cover 1 tiltable",
                                label: "Cover 1 tilt enabled",
                                name: "cover_1_tilt_enabled",
                                property: "cover_1_tilt_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Cover 2 enabled",
                                label: "Cover 2 enabled",
                                name: "cover_2_enabled",
                                property: "cover_2_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description: "Cover 2 tiltable",
                                label: "Cover 2 tilt enabled",
                                name: "cover_2_tilt_enabled",
                                property: "cover_2_tilt_enabled",
                                type: "enum",
                                values: ["auto", "true", "false"],
                            },
                            {
                                access: 2,
                                description:
                                    "When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
                                label: "Color sync",
                                name: "color_sync",
                                property: "color_sync",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).",
                                label: "Transition",
                                name: "transition",
                                property: "transition",
                                type: "numeric",
                                value_min: 0,
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description: "Inverts the cover position, false: open=100,close=0, true: open=0,close=100 (default false).",
                                label: "Invert cover",
                                name: "invert_cover",
                                property: "invert_cover",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: false,
                        vendor: "Siglis",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "10": {
                            name: "l5",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "11": {
                            name: "l6",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "closuresWindowCovering"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "12": {
                            name: "l7",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "closuresWindowCovering"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "5": {
                            name: "l1",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl", "lightingColorCtrl"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "7": {
                            name: "l2",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "8": {
                            name: "l3",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                        "9": {
                            name: "l4",
                            bindings: [],
                            clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
                            configured_reportings: [],
                            scenes: [],
                        },
                    },
                    friendly_name: "zigfred_plus",
                    ieee_address: "0xf4ce368a38be56a1",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    manufacturer: "Siglis",
                    model_id: "zigfred plus",
                    network_address: 6589,
                    power_source: "Mains (single phase)",
                    supported: true,
                    type: "Router",
                },
                {
                    definition: {
                        source: "native",
                        description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
                        exposes: [
                            {
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        label: "State",
                                        name: "state",
                                        property: "state",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature of this light",
                                        label: "Color temp",
                                        name: "color_temp",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                        ],
                                        property: "color_temp",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature after cold power on of this light",
                                        label: "Color temp startup",
                                        name: "color_temp_startup",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                            {description: "Restore previous color_temp on cold power on", name: "previous", value: 65535},
                                        ],
                                        property: "color_temp_startup",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Configure genLevelCtrl",
                                        features: [
                                            {
                                                access: 7,
                                                description:
                                                    'this setting can affect the "on_level", "current_level_startup" or "brightness" setting',
                                                label: "Execute if off",
                                                name: "execute_if_off",
                                                property: "execute_if_off",
                                                type: "binary",
                                                value_off: false,
                                                value_on: true,
                                            },
                                            {
                                                access: 7,
                                                description: "Defines the desired startup level for a device when it is supplied with power",
                                                label: "Current level startup",
                                                name: "current_level_startup",
                                                presets: [
                                                    {description: "Use minimum permitted value", name: "minimum", value: "minimum"},
                                                    {description: "Use previous value", name: "previous", value: "previous"},
                                                ],
                                                property: "current_level_startup",
                                                type: "numeric",
                                                value_max: 254,
                                                value_min: 1,
                                            },
                                        ],
                                        label: "Level config",
                                        name: "level_config",
                                        property: "level_config",
                                        type: "composite",
                                    },
                                ],
                                type: "light",
                            },
                            {
                                access: 2,
                                description: "Triggers an effect on the light (e.g. make light blink for a few seconds)",
                                label: "Effect",
                                name: "effect",
                                property: "effect",
                                type: "enum",
                                values: ["blink", "breathe", "okay", "channel_change", "finish_effect", "stop_effect"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Controls the behavior when the device is powered on after power loss",
                                label: "Power-on behavior",
                                name: "power_on_behavior",
                                property: "power_on_behavior",
                                type: "enum",
                                values: ["off", "on", "toggle", "previous"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Advanced color behavior",
                                features: [
                                    {
                                        access: 2,
                                        description: "Controls whether color and color temperature can be set while light is off",
                                        label: "Execute if off",
                                        name: "execute_if_off",
                                        property: "execute_if_off",
                                        type: "binary",
                                        value_off: false,
                                        value_on: true,
                                    },
                                ],
                                label: "Color options",
                                name: "color_options",
                                property: "color_options",
                                type: "composite",
                            },
                            {
                                access: 2,
                                category: "config",
                                description: "Initiate device identification",
                                label: "Identify",
                                name: "identify",
                                property: "identify",
                                type: "enum",
                                values: ["identify"],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "LED1545G12",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).",
                                label: "Transition",
                                name: "transition",
                                property: "transition",
                                type: "numeric",
                                value_min: 0,
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Whether to unfreeze IKEA lights (that are known to be frozen) before issuing a command, false: no unfreeze support, true: unfreeze support (default true).",
                                label: "Unfreeze support",
                                name: "unfreeze_support",
                                property: "unfreeze_support",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
                                label: "Color sync",
                                name: "color_sync",
                                property: "color_sync",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).",
                                label: "Identify timeout",
                                name: "identify_timeout",
                                property: "identify_timeout",
                                type: "numeric",
                                value_max: 30,
                                value_min: 1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: true,
                        vendor: "IKEA",
                        version: "0.0.0",
                    },
                    disabled: false,
                    endpoints: {
                        "1": {
                            bindings: [],
                            clusters: {
                                input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl", "lightingColorCtrl"],
                                output: ["genScenes", "genOta"],
                            },
                            configured_reportings: [],
                            scenes: [],
                        },
                    },
                    friendly_name: "0x000b57fffec6a5c2",
                    ieee_address: "0x000b57fffec6a5c2",
                    interview_completed: true,
                    interview_state: "SUCCESSFUL",
                    interviewing: false,
                    model_id: "TRADFRI bulb E27 WS opal 980lm",
                    network_address: 40369,
                    power_source: "Mains (single phase)",
                    supported: true,
                    type: "Router",
                },
            ]),
            {retain: true},
        );
    });

    it("Should publish definitions on startup", async () => {
        await resetExtension();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
            retain: true,
        });
    });

    it("Should log to MQTT", () => {
        mockLogger.setTransportsEnabled(true);
        mockMQTTPublishAsync.mockClear();
        mockLogger.info.mockClear();
        mockLogger.info("this is a test");
        mockLogger.info("this is a test"); // Should not publish dupes
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/logging",
            stringify({message: "this is a test", level: "info", namespace: "z2m"}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);

        // Should not publish debug logging
        mockMQTTPublishAsync.mockClear();
        mockLogger.debug("this is a test");
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
    });

    it("Should log to MQTT including debug when enabled", async () => {
        settings.set(["advanced", "log_debug_to_mqtt_frontend"], true);
        await resetExtension();

        mockLogger.setTransportsEnabled(true);
        mockMQTTPublishAsync.mockClear();
        mockLogger.info.mockClear();
        mockLogger.info("this is a test");
        mockLogger.info("this is a test"); // Should not publish dupes
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/logging",
            stringify({message: "this is a test", level: "info", namespace: "z2m"}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);

        // Should publish debug logging
        mockMQTTPublishAsync.mockClear();
        mockLogger.debug("this is a test");
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);

        settings.set(["advanced", "log_debug_to_mqtt_frontend"], false);
        settings.reRead();
    });

    it("Shouldnt log to MQTT when not connected", () => {
        mockLogger.setTransportsEnabled(true);
        // @ts-expect-error private
        controller.mqtt.client.reconnecting = true;
        mockMQTTPublishAsync.mockClear();
        mockLogger.info.mockClear();
        mockLogger.error.mockClear();
        mockLogger.info("this is a test");
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
        expect(mockLogger.info).toHaveBeenCalledTimes(1);
        expect(mockLogger.error).toHaveBeenCalledTimes(0);
    });

    it("Should publish groups on startup", async () => {
        await resetExtension();
        mockLogger.setTransportsEnabled(true);
        // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/groups'));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/groups",
            stringify([
                {friendly_name: "group_1", id: 1, members: [], scenes: []},
                {friendly_name: "group_2", id: 2, members: [], scenes: []},
                {
                    friendly_name: "group_tradfri_remote",
                    id: 15071,
                    members: [
                        {endpoint: 1, ieee_address: "0x000b57fffec6a5b4"},
                        {endpoint: 1, ieee_address: "0x000b57fffec6a5b7"},
                    ],
                    scenes: [],
                },
                {friendly_name: "99", id: 99, members: [], scenes: []},
                {friendly_name: "group_with_tradfri", id: 11, members: [{endpoint: 1, ieee_address: "0x000b57fffec6a5b7"}], scenes: []},
                {friendly_name: "thermostat_group", id: 12, members: [{endpoint: 1, ieee_address: "0x0017882104a44559"}], scenes: []},
                {
                    friendly_name: "switch_group",
                    id: 14,
                    members: [
                        {endpoint: 1, ieee_address: "0x0017880104e45524"},
                        {endpoint: 1, ieee_address: "0x000b57fffec6a5b7"},
                    ],
                    scenes: [],
                },
                {friendly_name: "gledopto_group", id: 21, members: [{endpoint: 15, ieee_address: "0x0017880104e45724"}], scenes: []},
                {friendly_name: "default_bind_group", id: DEFAULT_BIND_GROUP_ID, members: [], scenes: []},
                {
                    friendly_name: "ha_discovery_group",
                    id: 9,
                    members: [
                        {endpoint: 1, ieee_address: "0x000b57fffec6a5b4"},
                        {endpoint: 1, ieee_address: "0x000b57fffec6a5b7"},
                        {endpoint: 3, ieee_address: "0x0017880104e45542"},
                    ],
                    scenes: [{id: 4, name: "Scene 4"}],
                },
                {
                    friendly_name: "hue_twilight_group",
                    id: 19,
                    members: [{endpoint: 11, ieee_address: "0x000b57cdfec6a5b3"}],
                    scenes: [],
                },
            ]),
            {retain: true},
        );
    });

    it("Should publish event when device joined", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceJoined({device: devices.bulb});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({type: "device_joined", data: {friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}}),
            {},
        );
    });

    it("Should publish devices when device joined", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should publish event when device announces", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceAnnounce({device: devices.bulb});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({type: "device_announce", data: {friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}}),
            {},
        );
    });

    it("Should publish event when device interview started", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceInterview({device: devices.bulb, status: "started"});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({type: "device_interview", data: {friendly_name: "bulb", status: "started", ieee_address: "0x000b57fffec6a5b2"}}),
            {},
        );
    });

    it("Should publish event and devices when device interview failed", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceInterview({device: devices.bulb, status: "failed"});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({type: "device_interview", data: {friendly_name: "bulb", status: "failed", ieee_address: "0x000b57fffec6a5b2"}}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should publish event and devices when device interview successful", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceInterview({device: devices.bulb, status: "successful"});
        await mockZHEvents.deviceInterview({device: devices.unsupported, status: "successful"});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(7);
        // console.log(mockMQTTPublishAsync.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event'));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({
                data: {
                    definition: {
                        source: "native",
                        description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
                        exposes: [
                            {
                                features: [
                                    {
                                        access: 7,
                                        description: "On/off state of this light",
                                        label: "State",
                                        name: "state",
                                        property: "state",
                                        type: "binary",
                                        value_off: "OFF",
                                        value_on: "ON",
                                        value_toggle: "TOGGLE",
                                    },
                                    {
                                        access: 7,
                                        description: "Brightness of this light",
                                        label: "Brightness",
                                        name: "brightness",
                                        property: "brightness",
                                        type: "numeric",
                                        value_max: 254,
                                        value_min: 0,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature of this light",
                                        label: "Color temp",
                                        name: "color_temp",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                        ],
                                        property: "color_temp",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Color temperature after cold power on of this light",
                                        label: "Color temp startup",
                                        name: "color_temp_startup",
                                        presets: [
                                            {description: "Coolest temperature supported", name: "coolest", value: 250},
                                            {description: "Cool temperature (250 mireds / 4000 Kelvin)", name: "cool", value: 250},
                                            {description: "Neutral temperature (370 mireds / 2700 Kelvin)", name: "neutral", value: 370},
                                            {description: "Warm temperature (454 mireds / 2200 Kelvin)", name: "warm", value: 454},
                                            {description: "Warmest temperature supported", name: "warmest", value: 454},
                                            {description: "Restore previous color_temp on cold power on", name: "previous", value: 65535},
                                        ],
                                        property: "color_temp_startup",
                                        type: "numeric",
                                        unit: "mired",
                                        value_max: 454,
                                        value_min: 250,
                                    },
                                    {
                                        access: 7,
                                        description: "Configure genLevelCtrl",
                                        features: [
                                            {
                                                access: 7,
                                                description: `this setting can affect the "on_level", "current_level_startup" or "brightness" setting`,
                                                label: "Execute if off",
                                                name: "execute_if_off",
                                                property: "execute_if_off",
                                                type: "binary",
                                                value_off: false,
                                                value_on: true,
                                            },
                                            {
                                                access: 7,
                                                description: "Defines the desired startup level for a device when it is supplied with power",
                                                label: "Current level startup",
                                                name: "current_level_startup",
                                                presets: [
                                                    {description: "Use minimum permitted value", name: "minimum", value: "minimum"},
                                                    {description: "Use previous value", name: "previous", value: "previous"},
                                                ],
                                                property: "current_level_startup",
                                                type: "numeric",
                                                value_max: 254,
                                                value_min: 1,
                                            },
                                        ],
                                        label: "Level config",
                                        name: "level_config",
                                        property: "level_config",
                                        type: "composite",
                                    },
                                ],
                                type: "light",
                            },
                            {
                                access: 2,
                                description: "Triggers an effect on the light (e.g. make light blink for a few seconds)",
                                label: "Effect",
                                name: "effect",
                                property: "effect",
                                type: "enum",
                                values: ["blink", "breathe", "okay", "channel_change", "finish_effect", "stop_effect"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Controls the behavior when the device is powered on after power loss",
                                label: "Power-on behavior",
                                name: "power_on_behavior",
                                property: "power_on_behavior",
                                type: "enum",
                                values: ["off", "on", "toggle", "previous"],
                            },
                            {
                                access: 7,
                                category: "config",
                                description: "Advanced color behavior",
                                features: [
                                    {
                                        access: 2,
                                        description: "Controls whether color and color temperature can be set while light is off",
                                        label: "Execute if off",
                                        name: "execute_if_off",
                                        property: "execute_if_off",
                                        type: "binary",
                                        value_off: false,
                                        value_on: true,
                                    },
                                ],
                                label: "Color options",
                                name: "color_options",
                                property: "color_options",
                                type: "composite",
                            },
                            {
                                access: 2,
                                category: "config",
                                description: "Initiate device identification",
                                label: "Identify",
                                name: "identify",
                                property: "identify",
                                type: "enum",
                                values: ["identify"],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "LED1545G12",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).",
                                label: "Transition",
                                name: "transition",
                                property: "transition",
                                type: "numeric",
                                value_min: 0,
                                value_step: 0.1,
                            },
                            {
                                access: 2,
                                description:
                                    "Whether to unfreeze IKEA lights (that are known to be frozen) before issuing a command, false: no unfreeze support, true: unfreeze support (default true).",
                                label: "Unfreeze support",
                                name: "unfreeze_support",
                                property: "unfreeze_support",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
                                label: "Color sync",
                                name: "color_sync",
                                property: "color_sync",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                            {
                                access: 2,
                                description:
                                    "Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).",
                                label: "Identify timeout",
                                name: "identify_timeout",
                                property: "identify_timeout",
                                type: "numeric",
                                value_max: 30,
                                value_min: 1,
                            },
                            {
                                access: 2,
                                description: "State actions will also be published as 'action' when true (default false).",
                                label: "State action",
                                name: "state_action",
                                property: "state_action",
                                type: "binary",
                                value_off: false,
                                value_on: true,
                            },
                        ],
                        supports_ota: true,
                        vendor: "IKEA",
                        version: "0.0.0",
                    },
                    friendly_name: "bulb",
                    ieee_address: "0x000b57fffec6a5b2",
                    status: "successful",
                    supported: true,
                },
                type: "device_interview",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({
                data: {
                    definition: {
                        source: "generated",
                        description: "Automatically generated definition",
                        exposes: [
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Triggered action (e.g. a button click)",
                                label: "Action",
                                name: "action",
                                property: "action",
                                type: "enum",
                                values: [
                                    "on",
                                    "off",
                                    "toggle",
                                    "brightness_move_to_level",
                                    "brightness_move_up",
                                    "brightness_move_down",
                                    "brightness_step_up",
                                    "brightness_step_down",
                                    "brightness_stop",
                                ],
                            },
                            {
                                access: 1,
                                category: "diagnostic",
                                description: "Link quality (signal strength)",
                                label: "Linkquality",
                                name: "linkquality",
                                property: "linkquality",
                                type: "numeric",
                                unit: "lqi",
                                value_max: 255,
                                value_min: 0,
                            },
                        ],
                        model: "notSupportedModelID",
                        options: [
                            {
                                access: 2,
                                description:
                                    "Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.",
                                features: [
                                    {
                                        access: 2,
                                        description: "Delta per interval, 20 by default",
                                        label: "Delta",
                                        name: "delta",
                                        property: "delta",
                                        type: "numeric",
                                        value_min: 0,
                                    },
                                    {
                                        access: 2,
                                        description: "Interval duration",
                                        label: "Interval",
                                        name: "interval",
                                        property: "interval",
                                        type: "numeric",
                                        unit: "ms",
                                        value_min: 0,
                                    },
                                ],
                                label: "Simulated brightness",
                                name: "simulated_brightness",
                                property: "simulated_brightness",
                                type: "composite",
                            },
                        ],
                        supports_ota: false,
                        vendor: "notSupportedMfg",
                        version: "0.0.0",
                    },
                    friendly_name: "0x0017880104e45518",
                    ieee_address: "0x0017880104e45518",
                    status: "successful",
                    supported: false,
                },
                type: "device_interview",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.any(String), {retain: true});
    });

    it("Should publish event and devices when device leaves", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb.ieeeAddr});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(4);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({type: "device_leave", data: {ieee_address: "0x000b57fffec6a5b2", friendly_name: "bulb"}}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            // Defintitions should be updated on device event
            "zigbee2mqtt/bridge/definitions",
            expect.any(String),
            {retain: true},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), {retain: true});
    });

    it("Should allow permit join on all", async () => {
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 1}));
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
        expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {});
    });

    it("Should disallow permit join on all", async () => {
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 0}));
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
        expect(mockZHController.permitJoin).toHaveBeenCalledWith(0, undefined);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 0}, status: "ok"}), {});
    });

    it("Should allow permit join with number string (automatically on all)", async () => {
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", "1");
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
        expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {});
    });

    it("Should not allow permit join with invalid payload", async () => {
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time_bla: false}));
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/permit_join",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should republish bridge info when permit join changes", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10});
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
    });

    it("Shouldnt republish bridge info when permit join changes and hersman is stopping", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.isStopping.mockImplementationOnce(() => true);
        await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10});
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
    });

    it("Should allow permit join via device", async () => {
        const device = devices.bulb;
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 123, device: "bulb"}));
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
        expect(mockZHController.permitJoin).toHaveBeenCalledWith(123, device);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/permit_join",
            stringify({data: {time: 123, device: "bulb"}, status: "ok"}),
            {},
        );
    });

    it("Should not allow permit join via non-existing device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 123, device: "bulb_not_existing_woeeee"}));
        await flushPromises();
        expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/permit_join",
            stringify({data: {}, status: "error", error: "Device 'bulb_not_existing_woeeee' does not exist"}),
            {},
        );
    });

    it("Should put transaction in response when request is done with transaction", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 0, transaction: 22}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/permit_join",
            stringify({data: {time: 0}, status: "ok", transaction: 22}),
            {},
        );
    });

    it("Should put error in response when request fails", async () => {
        mockZHController.permitJoin.mockImplementationOnce(() => {
            throw new Error("Failed to connect to adapter");
        });
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/permit_join", stringify({time: 0}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/permit_join",
            stringify({data: {}, status: "error", error: "Failed to connect to adapter"}),
            {},
        );
    });

    it("Should put error in response when format is incorrect", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: false}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Coverage satisfaction", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/random", stringify({value: false}));
        const device = devices.bulb;
        await mockZHEvents.message({
            data: {onOff: 1},
            cluster: "genOnOff",
            device,
            endpoint: device.getEndpoint(1),
            type: "attributeReport",
            linkquality: 10,
        });
        await flushPromises();
    });

    it("Should allow a healthcheck", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/health_check", "");
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/health_check",
            stringify({data: {healthy: true}, status: "ok"}),
            {},
        );
    });

    it("Should allow a coordinator check", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.coordinatorCheck.mockReturnValueOnce({missingRouters: [mockZHController.getDeviceByIeeeAddr("0x000b57fffec6a5b2")]});
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/coordinator_check", "");
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/coordinator_check",
            stringify({data: {missing_routers: [{friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}]}, status: "ok"}),
            {},
        );
    });

    it("Should allow to remove device by string", async () => {
        const device = devices.bulb;
        const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", "bulb");
        await flushPromises();
        // @ts-expect-error private
        expect(controller.state.state.get(device.ieeeAddr)).toBeUndefined();
        expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
        expect(device.removeFromDatabase).not.toHaveBeenCalled();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(removeSpy).not.toHaveBeenCalled();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
            {},
        );
        expect(settings.get().blocklist).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
    });

    it("Should allow to remove device by object ID", async () => {
        const device = devices.bulb;
        const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb"}));
        await flushPromises();
        expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
        expect(device.removeFromDatabase).not.toHaveBeenCalled();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(removeSpy).not.toHaveBeenCalled();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
            {},
        );
    });

    it("Should allow to force remove device", async () => {
        const device = devices.bulb;
        const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", force: true}));
        await flushPromises();
        expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
        expect(device.removeFromNetwork).not.toHaveBeenCalled();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(removeSpy).not.toHaveBeenCalled();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {id: "bulb", block: false, force: true, clear_cache: false}, status: "ok"}),
            {},
        );
    });

    it("Should allow to block device", async () => {
        const device = devices.bulb;
        const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", block: true, force: true}));
        await flushPromises();
        expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(removeSpy).not.toHaveBeenCalled();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {id: "bulb", block: true, force: true, clear_cache: false}, status: "ok"}),
            {},
        );
        expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
    });

    it("Should allow to clear cache when removing device", async () => {
        const device = devices.bulb;
        const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", clear_cache: true}));
        await flushPromises();
        expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
        expect(device.removeFromDatabase).not.toHaveBeenCalled();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(removeSpy).toHaveNthReturnedWith(1, true);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {id: "bulb", block: false, force: false, clear_cache: true}, status: "ok"}),
            {},
        );
    });

    it("Should allow to remove group", async () => {
        const group = groups.group_1;
        const removeGroupFromLookup = vi.spyOn(controller.zigbee, "removeGroupFromLookup");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/remove", "group_1");
        await flushPromises();
        expect(group.removeFromNetwork).toHaveBeenCalledTimes(1);
        expect(settings.getGroup("group_1")).toBeUndefined();
        expect(removeGroupFromLookup).toHaveBeenCalledWith(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/remove",
            stringify({data: {id: "group_1", force: false}, status: "ok"}),
            {},
        );
    });

    it("Should allow to force remove group", async () => {
        const group = groups.group_1;
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/remove", stringify({id: "group_1", force: true}));
        await flushPromises();
        expect(group.removeFromDatabase).toHaveBeenCalledTimes(1);
        expect(settings.getGroup("group_1")).toBeUndefined();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/remove",
            stringify({data: {id: "group_1", force: true}, status: "ok"}),
            {},
        );
    });

    it("Should allow to add and remove from blocklist", async () => {
        expect(settings.get().blocklist).toStrictEqual([]);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {blocklist: ["0x123", "0x1234"]}}));
        await flushPromises();
        expect(settings.get().blocklist).toStrictEqual(["0x123", "0x1234"]);

        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {blocklist: ["0x123"]}}));
        await flushPromises();
        expect(settings.get().blocklist).toStrictEqual(["0x123"]);
    });

    it("Should throw error on removing non-existing device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "non-existing-device"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({data: {}, status: "error", error: "Device 'non-existing-device' does not exist"}),
            {},
        );
    });

    it("Should throw error when remove device fails", async () => {
        const device = devices.bulb;
        mockMQTTPublishAsync.mockClear();
        device.removeFromNetwork.mockImplementationOnce(() => {
            throw new Error("device timeout");
        });
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/remove",
            stringify({
                data: {},
                status: "error",
                error: "Failed to remove device 'bulb' (block: false, force: false, clear cache: false) (Error: device timeout)",
            }),
            {},
        );
    });

    it("Should allow rename device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb", to: "bulb_new_name"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(settings.getDevice("bulb_new_name")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb_new_name",
            retain: true,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_new_name", stringify({brightness: 50}), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
            {},
        );
    });

    it("Should trim input when renaming device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb", to: " bulb_new_name  "}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(settings.getDevice("bulb_new_name")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb_new_name",
            retain: true,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
            {},
        );
    });

    it("Shouldnt allow rename device with to not allowed name containing a wildcard", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb", to: "living_room/blinds#"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {}, status: "error", error: "MQTT wildcard (+ and #) not allowed in friendly_name ('living_room/blinds#')"}),
            {},
        );
    });

    it("Should allow rename group", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/rename", stringify({from: "group_1", to: "group_new_name"}));
        await flushPromises();
        expect(settings.getGroup("group_1")).toBeUndefined();
        expect(settings.getGroup("group_new_name")).toStrictEqual({ID: 1, friendly_name: "group_new_name", retain: false});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/rename",
            stringify({data: {from: "group_1", to: "group_new_name", homeassistant_rename: false}, status: "ok"}),
            {},
        );
    });

    it("Should throw error on invalid device rename payload", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from_bla: "bulb", to: "bulb_new_name"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should throw error on non-existing device rename", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb_not_existing", to: "bulb_new_name"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {}, status: "error", error: "Device 'bulb_not_existing' does not exist"}),
            {},
        );
    });

    it("Should allow to rename last joined device", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceJoined({device: devices.bulb});
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({last: true, to: "bulb_new_name"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toBeUndefined();
        expect(settings.getDevice("bulb_new_name")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb_new_name",
            retain: true,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
            {},
        );
    });

    it("Should throw error when renaming device through not allowed friendlyName", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb", to: "bulb_new_name/1"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {}, status: "error", error: `Friendly name cannot end with a "/DIGIT" ('bulb_new_name/1')`}),
            {},
        );
    });

    it("Should throw error when renaming last joined device but none has joined", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({last: true, to: "bulb_new_name"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/rename",
            stringify({data: {}, status: "error", error: "No device has joined since start"}),
            {},
        );
    });

    it("Should allow interviewing a device by friendly name", async () => {
        mockMQTTPublishAsync.mockClear();
        devices.bulb.interview.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "bulb"}));
        await flushPromises();
        expect(devices.bulb.interview).toHaveBeenCalled();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {id: "bulb"}, status: "ok"}),
            {},
        );

        // The following indicates that devices have published.
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should allow interviewing a device by ieeeAddr", async () => {
        const device = controller.zigbee.resolveEntity(devices.bulb)!;
        assert("resolveDefinition" in device);
        device.resolveDefinition = vi.fn();
        mockMQTTPublishAsync.mockClear();
        devices.bulb.interview.mockClear();
        expect(device.resolveDefinition).toHaveBeenCalledTimes(0);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "0x000b57fffec6a5b2"}));
        await flushPromises();
        expect(devices.bulb.interview).toHaveBeenCalledWith(true);
        expect(device.resolveDefinition).toHaveBeenCalledWith(true);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {id: "0x000b57fffec6a5b2"}, status: "ok"}),
            {},
        );

        // The following indicates that devices have published.
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should throw error on invalid device interview payload", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({foo: "bulb"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should throw error on non-existing device interview", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "bulb_not_existing"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {}, status: "error", error: "Device 'bulb_not_existing' does not exist"}),
            {},
        );
    });

    it("Should throw error on id is device endpoint", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "bulb/1"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {}, status: "error", error: "Device 'bulb/1' does not exist"}),
            {},
        );
    });

    it("Should throw error on id is a group", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "group_1"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {}, status: "error", error: "Device 'group_1' does not exist"}),
            {},
        );
    });

    it("Should throw error on when interview fails", async () => {
        mockMQTTPublishAsync.mockClear();
        devices.bulb.interview.mockClear();
        devices.bulb.interview.mockImplementation(() => Promise.reject(new Error("something went wrong")));
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/interview", stringify({id: "bulb"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/interview",
            stringify({data: {}, status: "error", error: "Interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}),
            {},
        );
    });

    it("Should error when generate_external_definition is invalid", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/generate_external_definition", stringify({wrong: devices.ZNCZ02LM.ieeeAddr}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/generate_external_definition",
            stringify({data: {}, error: "Invalid payload", status: "error"}),
            {},
        );
    });

    it("Should error when generate_external_definition requested for unknown device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/generate_external_definition", stringify({id: "non_existing_device"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/generate_external_definition",
            stringify({data: {}, error: "Device 'non_existing_device' does not exist", status: "error"}),
            {},
        );
    });

    it("Should allow to generate device definition", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/generate_external_definition", stringify({id: devices.ZNCZ02LM.ieeeAddr}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/generate_external_definition",
            stringify({
                data: {
                    id: "0x0017880104e45524",
                    source:
                        "import * as m from 'zigbee-herdsman-converters/lib/modernExtend';\n" +
                        "\n" +
                        "export default {\n" +
                        "    zigbeeModel: ['lumi.plug'],\n" +
                        "    model: 'lumi.plug',\n" +
                        "    vendor: '',\n" +
                        "    description: 'Automatically generated definition',\n" +
                        '    extend: [m.onOff({"powerOnBehavior":false})],\n' +
                        "};\n",
                },
                status: "ok",
            }),
            {},
        );
    });

    it("Should allow change device options", async () => {
        mockMQTTPublishAsync.mockClear();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            retain: true,
            description: "this is my bulb",
        });
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options: {retain: false, transition: 1}, id: "bulb"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            retain: false,
            transition: 1,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/options",
            stringify({
                data: {
                    from: {retain: true, description: "this is my bulb"},
                    to: {retain: false, transition: 1, description: "this is my bulb"},
                    restart_required: false,
                    id: "bulb",
                },
                status: "ok",
            }),
            {},
        );
    });

    it.each([
        ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJ", "device_icons/effcad234beeb56ea7c457cf2d36d10b.png", true],
        ["some_icon.png", "some_icon.png", false],
    ])("Should save as image as file when changing device icon", async (mqttIcon, settingsIcon, checkFileExists) => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options: {icon: mqttIcon}, id: "bulb"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            icon: settingsIcon,
            description: "this is my bulb",
            retain: true,
        });
        if (checkFileExists) {
            expect(fs.existsSync(path.join(data.mockDir, settingsIcon))).toBeTruthy();
        }
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/options",
            stringify({
                data: {
                    from: {retain: true, description: "this is my bulb"},
                    to: {retain: true, description: "this is my bulb", icon: settingsIcon},
                    id: "bulb",
                    restart_required: false,
                },
                status: "ok",
            }),
            {},
        );
    });

    it("Should allow to remove device option", async () => {
        mockMQTTPublishAsync.mockClear();
        settings.set(["devices", "0x000b57fffec6a5b2", "qos"], 1);
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            qos: 1,
            retain: true,
            description: "this is my bulb",
        });
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options: {qos: null}, id: "bulb"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            retain: true,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/options",
            stringify({
                data: {
                    from: {retain: true, qos: 1, description: "this is my bulb"},
                    to: {retain: true, description: "this is my bulb"},
                    restart_required: false,
                    id: "bulb",
                },
                status: "ok",
            }),
            {},
        );
    });

    it("Should allow change device options with restart required", async () => {
        mockMQTTPublishAsync.mockClear();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            retain: true,
            description: "this is my bulb",
        });
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options: {disabled: true}, id: "bulb"}));
        await flushPromises();
        expect(settings.getDevice("bulb")).toStrictEqual({
            ID: "0x000b57fffec6a5b2",
            friendly_name: "bulb",
            retain: true,
            disabled: true,
            description: "this is my bulb",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/options",
            stringify({
                data: {
                    from: {retain: true, description: "this is my bulb"},
                    to: {disabled: true, retain: true, description: "this is my bulb"},
                    restart_required: true,
                    id: "bulb",
                },
                status: "ok",
            }),
            {},
        );
    });

    it("Should allow change group options", async () => {
        mockMQTTPublishAsync.mockClear();
        expect(settings.getGroup("group_1")).toStrictEqual({ID: 1, friendly_name: "group_1", retain: false});
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/options", stringify({options: {retain: true, transition: 1}, id: "group_1"}));
        await flushPromises();
        expect(settings.getGroup("group_1")).toStrictEqual({ID: 1, friendly_name: "group_1", retain: true, transition: 1});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/options",
            stringify({data: {from: {retain: false}, to: {retain: true, transition: 1}, restart_required: false, id: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Should allow change group options with restart required", async () => {
        mockMQTTPublishAsync.mockClear();
        expect(settings.getGroup("group_1")).toStrictEqual({ID: 1, friendly_name: "group_1", retain: false});
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/options", stringify({options: {off_state: "all_members_off"}, id: "group_1"}));
        await flushPromises();
        expect(settings.getGroup("group_1")).toStrictEqual({
            ID: 1,
            friendly_name: "group_1",
            retain: false,
            off_state: "all_members_off",
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/options",
            stringify({
                data: {from: {retain: false}, to: {retain: false, off_state: "all_members_off"}, restart_required: true, id: "group_1"},
                status: "ok",
            }),
            {},
        );
    });

    it("Should throw error on invalid device change options payload", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options_: {retain: true, transition: 1}, id: "bulb"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/options",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should allow to add group by string", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/add", "group_193");
        await flushPromises();
        expect(settings.getGroup("group_193")).toStrictEqual({ID: 3, friendly_name: "group_193"});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/add",
            stringify({data: {friendly_name: "group_193", id: 3}, status: "ok"}),
            {},
        );
    });

    it("Should allow to add group with ID", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/add", stringify({friendly_name: "group_193", id: 92}));
        await flushPromises();
        expect(settings.getGroup("group_193")).toStrictEqual({ID: 92, friendly_name: "group_193"});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/add",
            stringify({data: {friendly_name: "group_193", id: 92}, status: "ok"}),
            {},
        );
    });

    it("Shouldnt allow to add group with empty name", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/add", stringify({friendly_name: "", id: 9}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/add",
            stringify({data: {}, status: "error", error: "friendly_name must be at least 1 char long"}),
            {},
        );
    });

    it("Should throw error when add with invalid payload", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/add", stringify({friendly_name9: "group_193"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/add",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should allow to touchlink factory reset (succeeds)", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.factoryResetFirst.mockClear();
        mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(true);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", "");
        await flushPromises();
        expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/factory_reset",
            stringify({data: {}, status: "ok"}),
            {},
        );
    });

    it("Should allow to touchlink factory reset specific device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.factoryReset.mockClear();
        mockZHController.touchlink.factoryReset.mockReturnValueOnce(true);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", stringify({ieee_address: "0x1239", channel: 12}));
        await flushPromises();
        expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledTimes(1);
        expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledWith("0x1239", 12);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/factory_reset",
            stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
            {},
        );
    });

    it("Add install code", async () => {
        mockMQTTPublishAsync.mockClear();

        // By object
        mockZHController.addInstallCode.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/install_code/add", stringify({value: "my-code"}));
        await flushPromises();
        expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1);
        expect(mockZHController.addInstallCode).toHaveBeenCalledWith("my-code");
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/install_code/add",
            stringify({data: {value: "my-code"}, status: "ok"}),
            {},
        );

        // By string
        mockZHController.addInstallCode.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/install_code/add", "my-string-code");
        await flushPromises();
        expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1);
        expect(mockZHController.addInstallCode).toHaveBeenCalledWith("my-string-code");
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/install_code/add",
            stringify({data: {value: "my-code"}, status: "ok"}),
            {},
        );
    });

    it("Add install code error", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.addInstallCode.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/install_code/add", stringify({wrong: "my-code"}));
        await flushPromises();
        expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/install_code/add",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should allow to touchlink identify specific device", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.identify.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239", channel: 12}));
        await flushPromises();
        expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(1);
        expect(mockZHController.touchlink.identify).toHaveBeenCalledWith("0x1239", 12);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/identify",
            stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
            {},
        );
    });

    it("Touchlink identify fails when payload is invalid", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.identify.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239"}));
        await flushPromises();
        expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/identify",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should allow to touchlink factory reset (fails)", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.factoryResetFirst.mockClear();
        mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(false);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", "");
        await flushPromises();
        expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/factory_reset",
            stringify({data: {}, status: "error", error: "Failed to factory reset device through Touchlink"}),
            {},
        );
    });

    it("Should allow to touchlink scan", async () => {
        mockMQTTPublishAsync.mockClear();
        mockZHController.touchlink.scan.mockClear();
        mockZHController.touchlink.scan.mockReturnValueOnce([
            {ieeeAddr: "0x123", channel: 12},
            {ieeeAddr: "0x124", channel: 24},
        ]);
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/scan", "");
        await flushPromises();
        expect(mockZHController.touchlink.scan).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/touchlink/scan",
            stringify({
                data: {
                    found: [
                        {ieee_address: "0x123", channel: 12},
                        {ieee_address: "0x124", channel: 24},
                    ],
                },
                status: "ok",
            }),
            {},
        );
    });

    it("Should allow to configure reporting with endpoint as number", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.bind.mockClear();
        endpoint.configureReporting.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/configure",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: 1,
                cluster: "genLevelCtrl",
                attribute: "currentLevel",
                maximum_report_interval: 10,
                minimum_report_interval: 1,
                reportable_change: 1,
            }),
        );
        await flushPromises();
        expect(endpoint.bind).toHaveBeenCalledTimes(1);
        expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", devices.coordinator.endpoints[0]);
        expect(endpoint.configureReporting).toHaveBeenCalledTimes(1);
        expect(endpoint.configureReporting).toHaveBeenCalledWith(
            "genLevelCtrl",
            [{attribute: "currentLevel", maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}],
            undefined,
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/configure",
            stringify({
                data: {
                    id: "0x000b57fffec6a5b2",
                    endpoint: 1,
                    cluster: "genLevelCtrl",
                    attribute: "currentLevel",
                    maximum_report_interval: 10,
                    minimum_report_interval: 1,
                    reportable_change: 1,
                },
                status: "ok",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should allow to configure reporting with endpoint as string", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.bind.mockClear();
        endpoint.configureReporting.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/configure",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: "1",
                cluster: "genLevelCtrl",
                attribute: "currentLevel",
                maximum_report_interval: 10,
                minimum_report_interval: 1,
                reportable_change: 1,
            }),
        );
        await flushPromises();
        expect(endpoint.bind).toHaveBeenCalledTimes(1);
        expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", devices.coordinator.endpoints[0]);
        expect(endpoint.configureReporting).toHaveBeenCalledTimes(1);
        expect(endpoint.configureReporting).toHaveBeenCalledWith(
            "genLevelCtrl",
            [{attribute: "currentLevel", maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}],
            undefined,
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/configure",
            stringify({
                data: {
                    id: "0x000b57fffec6a5b2",
                    endpoint: "1",
                    cluster: "genLevelCtrl",
                    attribute: "currentLevel",
                    maximum_report_interval: 10,
                    minimum_report_interval: 1,
                    reportable_change: 1,
                },
                status: "ok",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should throw error when configure reporting is called with malformed payload", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.configureReporting.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/configure",
            stringify({
                id: "bulb",
                // endpoint: '1',
                cluster: "genLevelCtrl",
                attribute: "currentLevel",
                maximum_report_interval: 10,
                minimum_report_interval: 1,
                reportable_change: 1,
            }),
        );
        await flushPromises();
        expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/configure",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should throw error when configure reporting is called for non-existing device", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.configureReporting.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/configure",
            stringify({
                id: "non_existing_device",
                endpoint: "1",
                cluster: "genLevelCtrl",
                attribute: "currentLevel",
                maximum_report_interval: 10,
                minimum_report_interval: 1,
                reportable_change: 1,
            }),
        );
        await flushPromises();
        expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/configure",
            stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
            {},
        );
    });

    it("Should throw error when configure reporting is called for non-existing endpoint", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.configureReporting.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/configure",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: "non_existing_endpoint",
                cluster: "genLevelCtrl",
                attribute: "currentLevel",
                maximum_report_interval: 10,
                minimum_report_interval: 1,
                reportable_change: 1,
            }),
        );
        await flushPromises();
        expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/configure",
            stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
            {},
        );
    });

    it("Should allow to read reporting config with endpoint as number", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.bind.mockClear();
        endpoint.readReportingConfig.mockClear();
        endpoint.readReportingConfig.mockResolvedValueOnce([
            {
                status: Zcl.Status.SUCCESS,
                direction: Zcl.Direction.CLIENT_TO_SERVER,
                attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                dataType: Zcl.DataType.UINT8,
                minRepIntval: 10,
                maxRepIntval: 60,
                repChange: 2,
            },
        ]);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: 1,
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
        expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({
                data: {
                    id: "0x000b57fffec6a5b2",
                    endpoint: 1,
                    cluster: "genLevelCtrl",
                    configs: [
                        {
                            status: Zcl.Status.SUCCESS,
                            direction: Zcl.Direction.CLIENT_TO_SERVER,
                            attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                            dataType: Zcl.DataType.UINT8,
                            minRepIntval: 10,
                            maxRepIntval: 60,
                            repChange: 2,
                        },
                    ],
                },
                status: "ok",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should allow to read reporting config with endpoint as string", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.bind.mockClear();
        endpoint.readReportingConfig.mockClear();
        endpoint.readReportingConfig.mockResolvedValueOnce([
            {
                status: Zcl.Status.SUCCESS,
                direction: Zcl.Direction.CLIENT_TO_SERVER,
                attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                dataType: Zcl.DataType.UINT8,
                minRepIntval: 10,
                maxRepIntval: 60,
                repChange: 2,
            },
        ]);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: "1",
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
        expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({
                data: {
                    id: "0x000b57fffec6a5b2",
                    endpoint: "1",
                    cluster: "genLevelCtrl",
                    configs: [
                        {
                            status: Zcl.Status.SUCCESS,
                            direction: Zcl.Direction.CLIENT_TO_SERVER,
                            attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                            dataType: Zcl.DataType.UINT8,
                            minRepIntval: 10,
                            maxRepIntval: 60,
                            repChange: 2,
                        },
                    ],
                },
                status: "ok",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should allow to read reporting config with manufacturer code", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.bind.mockClear();
        endpoint.readReportingConfig.mockClear();
        endpoint.readReportingConfig.mockResolvedValueOnce([
            {
                status: Zcl.Status.SUCCESS,
                direction: Zcl.Direction.CLIENT_TO_SERVER,
                attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                dataType: Zcl.DataType.UINT8,
                minRepIntval: 10,
                maxRepIntval: 60,
                repChange: 2,
            },
        ]);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: 1,
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
                manufacturer_code: 0x1234,
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
        expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {manufacturerCode: 0x1234});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({
                data: {
                    id: "0x000b57fffec6a5b2",
                    endpoint: 1,
                    cluster: "genLevelCtrl",
                    configs: [
                        {
                            status: Zcl.Status.SUCCESS,
                            direction: Zcl.Direction.CLIENT_TO_SERVER,
                            attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
                            dataType: Zcl.DataType.UINT8,
                            minRepIntval: 10,
                            maxRepIntval: 60,
                            repChange: 2,
                        },
                    ],
                    manufacturer_code: 0x1234,
                },
                status: "ok",
            }),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
    });

    it("Should throw error when read reporting config is called with malformed payload", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.readReportingConfig.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "bulb",
                // endpoint: '1',
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should throw error when read reporting config is called for non-existing device", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.readReportingConfig.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "non_existing_device",
                endpoint: "1",
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
            {},
        );
    });

    it("Should throw error when read reporting config is called for non-existing endpoint", async () => {
        const device = devices.bulb;
        const endpoint = device.getEndpoint(1)!;
        endpoint.readReportingConfig.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/device/reporting/read",
            stringify({
                id: "0x000b57fffec6a5b2",
                endpoint: "non_existing_endpoint",
                cluster: "genLevelCtrl",
                configs: [{attribute: "currentLevel"}],
            }),
        );
        await flushPromises();
        expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/device/reporting/read",
            stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
            {},
        );
    });

    it("Should allow to create a backup", async () => {
        fs.mkdirSync(path.join(data.mockDir, "ext_converters"));
        fs.writeFileSync(path.join(data.mockDir, "ext_converters", "afile.js"), "test123");
        fs.mkdirSync(path.join(data.mockDir, "log"));
        fs.writeFileSync(path.join(data.mockDir, "log", "log.log"), "test123");
        fs.mkdirSync(path.join(data.mockDir, "ota"));
        fs.writeFileSync(path.join(data.mockDir, "ota", "my.ota"), "test123");
        fs.mkdirSync(path.join(data.mockDir, "ext_converters", "123"));
        fs.writeFileSync(path.join(data.mockDir, "ext_converters", "123", "myfile.js"), "test123");
        fs.symlinkSync(
            path.join(data.mockDir, "ext_converters"),
            path.join(data.mockDir, "ext_converters_sym"),
            platform() === "win32" ? "junction" : "dir",
        );
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/backup", "");
        await flushPromises();
        expect(mockZHController.backup).toHaveBeenCalledTimes(1);
        expect(mockFflateZip).toHaveBeenCalledTimes(1);
        expect(mockFflateZip).toHaveBeenNthCalledWith(
            1,
            {
                "configuration.yaml": expect.any(Buffer),
                [path.join("ext_converters", "123", "myfile.js")]: expect.any(Buffer),
                [path.join("ext_converters", "afile.js")]: expect.any(Buffer),
                "state.json": expect.any(Buffer),
            },
            {level: 6},
            expect.any(Function),
        );
        expect(Object.keys(mockFflateZip.mock.calls[0][0])).toStrictEqual([
            "configuration.yaml",
            path.join("ext_converters", "123", "myfile.js"),
            path.join("ext_converters", "afile.js"),
            "state.json",
        ]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/backup",
            stringify({data: {zip: "THISISBASE64"}, status: "ok"}),
            {},
        );
    });

    it("Should return an error when the backup archive cannot be created", async () => {
        mockMQTTPublishAsync.mockClear();
        mockFflateZipFailOnce(new Error("invalid zip data"));
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/backup", "");
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/backup",
            stringify({data: {}, status: "error", error: "invalid zip data"}),
            {},
        );
    });

    it("Should allow to restart", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/restart", "");
        await flushPromises();
        vi.runOnlyPendingTimers();
        expect(mockRestart).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/restart", stringify({data: {}, status: "ok"}), {});
    });

    it("Change options and apply - homeassistant", async () => {
        expect(controller.getExtension("HomeAssistant")).toBeUndefined();
        await mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {homeassistant: {enabled: true}}}));
        await expect(vi.waitUntil(() => controller.getExtension("HomeAssistant"))).resolves.toBeDefined();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );

        // Change an option
        await mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/options",
            stringify({options: {homeassistant: {enabled: true, experimental_event_entities: true}}}),
        );
        // @ts-expect-error - private property
        await vi.waitUntil(() => controller.getExtension("HomeAssistant")?.experimentalEventEntities === true);

        // revert
        await mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {homeassistant: {enabled: false}}}));
        await vi.waitUntil(() => controller.getExtension("HomeAssistant") === undefined);
    });

    it("Change options and apply - log_level", async () => {
        mockLogger.setLevel("info");
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_level: "debug"}}}));
        await flushPromises();
        expect(mockLogger.getLevel()).toStrictEqual("debug");
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: false}, status: "ok"}),
            {},
        );
    });

    it("Change options and apply - log_debug_namespace_ignore", async () => {
        mockMQTTPublishAsync.mockClear();
        const nsIgnore = "^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller";
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}}));
        await flushPromises();
        expect(mockLogger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: false}, status: "ok"}),
            {},
        );
    });

    it("Change options and apply - log_namespaced_levels", async () => {
        mockLogger.setLevel("info");
        settings.apply({advanced: {log_namespaced_levels: {"zh:zstack": "warning", "z2m:mqtt": "debug"}}});
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/options",
            stringify({options: {advanced: {log_namespaced_levels: {"z2m:mqtt": "warning", "zh:zstack": null}}}}),
        );
        await flushPromises();
        expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({"z2m:mqtt": "warning"});
        expect(mockLogger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": "warning"});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: false}, status: "ok"}),
            {},
        );

        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_namespaced_levels: {"z2m:mqtt": null}}}}));
        await flushPromises();
        expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({});
        expect(mockLogger.getNamespacedLevels()).toStrictEqual({});
    });

    it("Change options restart required", async () => {
        settings.apply({serial: {port: "123"}});
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {serial: {port: "/dev/newport"}}}));
        await flushPromises();
        expect(settings.get().serial.port).toBe("/dev/newport");
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );
    });

    it("Change options array", async () => {
        expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/options",
            stringify({options: {advanced: {ext_pan_id: [220, 221, 221, 221, 221, 221, 221, 221]}}}),
        );
        await flushPromises();
        expect(settings.get().advanced.ext_pan_id).toStrictEqual([220, 221, 221, 221, 221, 221, 221, 221]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );
    });

    it("Change options with null", async () => {
        expect(settings.get().serial).toStrictEqual({disable_led: false, port: "/dev/dummy"});
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {serial: {disable_led: false, port: null}}}));
        await flushPromises();
        expect(settings.get().serial).toStrictEqual({disable_led: false});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );
    });

    it("Change options invalid payload", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", "I am invalid");
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {}, error: "Invalid payload", status: "error"}),
            {},
        );
    });

    it("Change options not valid against schema", async () => {
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_level: 123}}}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {}, error: "advanced/log_level must be string", status: "error"}),
            {},
        );
    });

    it("Change options consecutively, check restart required", async () => {
        settings.apply({health: {interval: 10, reset_on_check: false}});
        mockMQTTPublishAsync.mockClear();

        // Change option that doesn't require restart
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {reset_on_check: true}}}));
        await flushPromises();

        expect(settings.get().health.reset_on_check).toBe(true);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: false}, status: "ok"}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":false'), {
            retain: true,
        });
        mockMQTTPublishAsync.mockClear();

        // Change option that requires restart
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {interval: 11}}}));
        await flushPromises();

        expect(settings.get().health.interval).toBe(11);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":true'), {
            retain: true,
        });
        mockMQTTPublishAsync.mockClear();

        // Change option that doesn't require restart
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {health: {reset_on_check: false}}}));
        await flushPromises();

        expect(settings.get().health.reset_on_check).toBe(false);

        // System still requires restart
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/options",
            stringify({data: {restart_required: true}, status: "ok"}),
            {},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.stringContaining('"restart_required":true'), {
            retain: true,
        });
        mockMQTTPublishAsync.mockClear();
    });

    it("Icon link handling", () => {
        const bridge = controller.getExtension("Bridge")! as Bridge;
        expect(bridge).toBeDefined();

        const definition = {
            fingerprint: [],
            model: "lumi.plug",
            vendor: "abcd",
            description: "abcd",
            toZigbee: [],
            fromZigbee: [],
            exposes: [],
            icon: "",
        };
        const device = devices.ZNCZ02LM;
        const svg_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo";
        const icon_link = "https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg";
        definition.icon = icon_link;
        // @ts-expect-error bare minimum mock
        let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe(icon_link);

        definition.icon = icon_link;
        // @ts-expect-error bare minimum mock
        payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {icon: svg_icon}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe(svg_icon);

        definition.icon = "_$model_";
        // @ts-expect-error bare minimum mock
        payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe("_lumi.plug_");

        definition.icon = "_$model_$zigbeeModel_";
        // @ts-expect-error bare minimum mock
        payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe("_lumi.plug_lumi.plug_");

        definition.icon = svg_icon;
        // @ts-expect-error bare minimum mock
        payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe(svg_icon);

        device.modelID = "?._Z\\NC+Z02*LM";
        definition.model = "&&&&*+";
        definition.icon = "_$model_$zigbeeModel_";
        // @ts-expect-error bare minimum mock
        payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
        assert(payload);
        expect(payload.icon).not.toBeUndefined();
        expect(payload.icon).toBe("_------_-._Z-NC-Z02-LM_");
    });

    it("Should publish bridge info, devices and definitions when a device with custom_clusters joined", async () => {
        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster});
        await flushPromises();

        // console.log(mockMQTT.publish.mock.calls);
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
            retain: true,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/event",
            stringify({data: {friendly_name: "0x000b57fffec6a5c2", ieee_address: "0x000b57fffec6a5c2"}, type: "device_joined"}),
            {},
        );
    });

    it("Should publish bridge info, devices and definitions when a device with custom_clusters is reconfigured", async () => {
        // Adding a device first
        await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster});
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        // After cleaning, reconfigure it
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", devices.bulb_custom_cluster.ieeeAddr);
        await flushPromises();

        // console.log(mockMQTT.publish.mock.calls);
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(4);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
            retain: true,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/device/configure", expect.any(String), {});
    });

    it("triggers ZHC action by name with params", async () => {
        mockMQTTPublishAsync.mockClear();

        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/action",
            JSON.stringify({
                action: "raw",
                params: {
                    profileId: Zdo.ZDO_PROFILE_ID,
                    ieeeAddress: "0xf1f2f3f4f5f6f7f8",
                    networkAddress: 0x1234,
                    clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
                    zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
                },
            }),
        );
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/action",
            JSON.stringify({data: [0x00, {assocDevList: [], eui64: "", nwkAddress: 0x1234, startIndex: 0}], status: "ok"}),
            {},
        );
    });

    it("triggers ZHC action by name without params", async () => {
        mockMQTTPublishAsync.mockClear();

        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/action",
            JSON.stringify({
                action: "raw",
            }),
        );
        await flushPromises();

        // we're mocking the response, so it's always this as long as the action & payload are valid
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/action",
            JSON.stringify({data: [0x00, {assocDevList: [], eui64: "", nwkAddress: 0x1234, startIndex: 0}], status: "ok"}),
            {},
        );
    });

    it("throws on invalid action payload", async () => {
        mockMQTTPublishAsync.mockClear();

        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/action",
            JSON.stringify({
                params: {
                    profileId: Zdo.ZDO_PROFILE_ID,
                    ieeeAddress: "0xf1f2f3f4f5f6f7f8",
                    networkAddress: 0x1234,
                    clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
                    zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
                },
            }),
        );
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/action",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("throws on invalid action", async () => {
        mockMQTTPublishAsync.mockClear();

        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/action",
            JSON.stringify({
                action: "DOES_NOT_EXIST",
                params: {
                    profileId: Zdo.ZDO_PROFILE_ID,
                    ieeeAddress: "0xf1f2f3f4f5f6f7f8",
                    networkAddress: 0x1234,
                    clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
                    zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
                },
            }),
        );
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/action",
            stringify({data: {}, status: "error", error: "Invalid action"}),
            {},
        );
    });
});
