mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-02 10:01:37 +00:00
f26ade4f93
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
4352 lines
217 KiB
TypeScript
4352 lines
217 KiB
TypeScript
// biome-ignore assist/source/organizeImports: import mocks first
|
|
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
|
import * as data from "../mocks/data";
|
|
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
|
|
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 "json-stable-stringify-without-jsonify";
|
|
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",
|
|
},
|
|
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"},
|
|
"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_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",
|
|
},
|
|
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: 2,
|
|
label: "Effect",
|
|
name: "effect",
|
|
property: "effect",
|
|
type: "enum",
|
|
values: [
|
|
"blink",
|
|
"breathe",
|
|
"okay",
|
|
"channel_change",
|
|
"candle",
|
|
"fireplace",
|
|
"colorloop",
|
|
"finish_effect",
|
|
"stop_effect",
|
|
"stop_hue_effect",
|
|
],
|
|
},
|
|
{
|
|
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:
|
|
"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:
|
|
"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: "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: "Philips",
|
|
},
|
|
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",
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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(3);
|
|
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},
|
|
);
|
|
});
|
|
|
|
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;
|
|
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(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}, 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;
|
|
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(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}, status: "ok"}),
|
|
{},
|
|
);
|
|
});
|
|
|
|
it("Should allow to force remove device", async () => {
|
|
const device = devices.bulb;
|
|
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(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}, status: "ok"}),
|
|
{},
|
|
);
|
|
});
|
|
|
|
it("Should allow to block device", async () => {
|
|
const device = devices.bulb;
|
|
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(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}, status: "ok"}),
|
|
{},
|
|
);
|
|
expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
|
|
});
|
|
|
|
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) (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"}],
|
|
manufacturerCode: 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,
|
|
},
|
|
],
|
|
manufacturerCode: 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, "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(mockJSZipFile).toHaveBeenCalledTimes(4);
|
|
expect(mockJSZipFile).toHaveBeenNthCalledWith(1, "configuration.yaml", expect.any(Object));
|
|
expect(mockJSZipFile).toHaveBeenNthCalledWith(2, path.join("ext_converters", "123", "myfile.js"), expect.any(Object));
|
|
expect(mockJSZipFile).toHaveBeenNthCalledWith(3, path.join("ext_converters", "afile.js"), expect.any(Object));
|
|
expect(mockJSZipFile).toHaveBeenNthCalledWith(4, "state.json", expect.any(Object));
|
|
expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1);
|
|
expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: "base64"});
|
|
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
|
"zigbee2mqtt/bridge/response/backup",
|
|
stringify({data: {zip: "THISISBASE64"}, status: "ok"}),
|
|
{},
|
|
);
|
|
});
|
|
|
|
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"}),
|
|
{},
|
|
);
|
|
// 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("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), {});
|
|
});
|
|
});
|