Files
zigbee2mqtt/test/settings.test.ts
2025-09-09 20:05:41 +02:00

1053 lines
34 KiB
TypeScript

// side-effect ensures using mock paths
import {beforeEach, describe, expect, it} from "vitest";
import "./mocks/data";
import fs from "node:fs";
import yaml from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import * as settings from "../lib/util/settings";
const configurationFile = mockedData.joinPath("configuration.yaml");
const devicesFile = mockedData.joinPath("devices.yaml");
const devicesFile2 = mockedData.joinPath("devices2.yaml");
const groupsFile = mockedData.joinPath("groups.yaml");
const secretFile = mockedData.joinPath("secret.yaml");
const minimalConfig = {
homeassistant: {enabled: true},
mqtt: {base_topic: "zigbee2mqtt", server: "localhost"},
};
describe("Settings", () => {
const write = (file: string, json: Record<string, unknown>, reread = true): void => {
fs.writeFileSync(file, yaml.dump(json));
if (reread) {
settings.reRead();
}
};
const read = (file: string): unknown => yaml.load(fs.readFileSync(file, "utf8"));
const remove = (file: string): void => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
};
const clearEnvironmentVariables = (): void => {
for (const key in process.env) {
if (key.startsWith("ZIGBEE2MQTT_CONFIG_")) {
delete process.env[key];
}
}
};
beforeEach(() => {
remove(configurationFile);
remove(secretFile);
remove(devicesFile);
remove(groupsFile);
clearEnvironmentVariables();
});
it("Ensures configuration.example.yaml is never out of sync with settings", () => {
let path = "./data/configuration.example.yaml";
// allow running in single-test mode
if (!fs.existsSync("./data")) {
path = "../data/configuration.example.yaml";
}
const exampleYaml = read(path) as Record<string, unknown>;
// force keeping an eye on example yaml whenever CURRENT_VERSION changes
expect(exampleYaml).toStrictEqual({
version: settings.CURRENT_VERSION,
homeassistant: {
enabled: false,
},
frontend: {
enabled: true,
},
mqtt: {
base_topic: "zigbee2mqtt",
server: "mqtt://localhost",
},
advanced: {
network_key: "GENERATE",
pan_id: "GENERATE",
ext_pan_id: "GENERATE",
},
});
});
it("Should return default settings", () => {
write(configurationFile, {});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expect(s).toStrictEqual(expected);
});
it("Should return settings", () => {
write(configurationFile, {serial: {disable_led: true}});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expected.serial = {disable_led: true};
expect(s).toStrictEqual(expected);
});
it("Should apply environment variables as overrides", () => {
write(secretFile, {password: "the-password"}, false);
process.env.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD = "!secret.yaml password";
process.env.ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED = "true";
process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL = "15";
process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT = "attribute_and_json";
process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT = '["console"]';
process.env.ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}';
process.env.ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC = "testtopic";
process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER = "testserver";
process.env.ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY = "GENERATE";
process.env.ZIGBEE2MQTT_CONFIG_DEVICES = "devices.yaml";
const contentDevices = {
"0x00158d00018255df": {
friendly_name: "0x00158d00018255df",
retain: false,
},
};
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
expected.devices = {
"0x00158d00018255df": {
friendly_name: "0x00158d00018255df",
retain: false,
},
};
expected.groups = {};
expected.serial.disable_led = true;
expected.advanced.channel = 15;
expected.advanced.log_output = ["console"];
expected.advanced.output = "attribute_and_json";
expected.map_options.graphviz.colors.fill = {enddevice: "#ff0000", coordinator: "#00ff00", router: "#0000ff"};
expected.mqtt.base_topic = "testtopic";
expected.mqtt.server = "testserver";
expected.mqtt.password = "the-password";
expected.advanced.network_key = "GENERATE";
write(configurationFile, {mqtt: {password: "config-password"}});
write(devicesFile, contentDevices);
const writeAndCheck = (): void => {
expect(settings.write()); // trigger writing of ENVs
expect(settings.validate()).toStrictEqual([]);
expect(settings.get()).toStrictEqual(expected);
settings.set(["advanced", "channel"], 25);
expect(settings.get().advanced.channel).toStrictEqual(15);
expect(read(configurationFile)).toMatchObject({advanced: {channel: 15}});
expect(read(secretFile)).toMatchObject({password: "the-password"});
expect(read(configurationFile)).toHaveProperty("mqtt.password", "!secret.yaml password");
};
// Write trice to ensure there are no side effects.
writeAndCheck();
writeAndCheck();
writeAndCheck();
});
it("Should apply Home Assistant environment variables", () => {
// should be kept in sync with envs in https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/blob/master/common/rootfs/docker-entrypoint.sh
process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_ENABLED = "true";
process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_PORT = "8099";
process.env.ZIGBEE2MQTT_CONFIG_HOMEASSISTANT_ENABLED = "true";
process.env.ZIGBEE2MQTT_CONFIG_SERIAL_PORT =
"/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_48be7a7468d8ed11bfea786ff2c613ac-if00-port0";
process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER = "mqtt://core-mosquitto:1883";
process.env.ZIGBEE2MQTT_CONFIG_MQTT_USER = "addons";
process.env.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD = "7RbR4NBSskS8TKG4ugzdRilXw6TPQ4NKFs259j2DxeultOFtl5JwciNZFd6feT5o";
write(configurationFile, {});
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
expected.frontend.enabled = true;
expected.frontend.port = 8099;
expected.homeassistant.enabled = true;
expected.serial.port = "/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_48be7a7468d8ed11bfea786ff2c613ac-if00-port0";
expected.mqtt.server = "mqtt://core-mosquitto:1883";
expected.mqtt.user = "addons";
expected.mqtt.password = "7RbR4NBSskS8TKG4ugzdRilXw6TPQ4NKFs259j2DxeultOFtl5JwciNZFd6feT5o";
expected.devices = {};
expected.groups = {};
const writeAndCheck = (): void => {
expect(settings.write()); // trigger writing of ENVs
expect(settings.validate()).toStrictEqual([]);
expect(settings.get()).toStrictEqual(expected);
};
// Write trice to ensure there are no side effects.
writeAndCheck();
writeAndCheck();
writeAndCheck();
});
it("Should write environment variables as overrides to configuration.yaml, not in the ref file", () => {
write(secretFile, {password: "password-in-secret-file"}, false);
write(configurationFile, {mqtt: {password: "!secret password", server: "server"}});
process.env.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD = "password-in-env-var";
const writeAndCheck = (): void => {
expect(settings.write()); // trigger writing of ENVs
expect(settings.validate()).toStrictEqual([]);
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({groups: {}, devices: {}}, settings.testing.defaults);
expected.mqtt.password = "password-in-env-var";
expected.mqtt.server = "server";
expect(s).toStrictEqual(expected);
expect(read(secretFile)).toMatchObject({password: "password-in-secret-file"});
expect(read(configurationFile)).toMatchObject({mqtt: {password: "password-in-env-var", server: "server"}});
};
// Write trice to ensure there are no side effects.
writeAndCheck();
writeAndCheck();
writeAndCheck();
});
it("Should add devices", () => {
write(configurationFile, {});
settings.addDevice("0x12345678");
const actual = read(configurationFile);
const expected = {
devices: {
"0x12345678": {
friendly_name: "0x12345678",
},
},
};
expect(actual).toStrictEqual(expected);
});
it("Should add devices even when devices exist empty", () => {
write(configurationFile, {devices: []});
settings.addDevice("0x12345678");
const actual = read(configurationFile);
const expected = {
devices: {
"0x12345678": {
friendly_name: "0x12345678",
},
},
};
expect(actual).toStrictEqual(expected);
});
it("Should read devices", () => {
const content = {
devices: {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
},
};
write(configurationFile, content);
const device = settings.getDevice("0x12345678");
const expected = {
ID: "0x12345678",
friendly_name: "0x12345678",
retain: false,
};
expect(device).toStrictEqual(expected);
});
it("Should not throw error when devices is null", () => {
const content = {devices: null};
write(configurationFile, content);
settings.getDevice("0x12345678");
});
it("Should read MQTT username and password form a separate file", () => {
const contentConfiguration = {
mqtt: {
server: "my.mqtt.server",
user: "!secret username",
password: "!secret password",
},
advanced: {
network_key: "!secret network_key",
},
};
const contentSecret = {
username: "mysecretusername",
password: "mysecretpassword",
network_key: [1, 2, 3],
};
write(secretFile, contentSecret, false);
write(configurationFile, contentConfiguration);
const expected = {
base_topic: "zigbee2mqtt",
include_device_information: false,
maximum_packet_size: 1048576,
keepalive: 60,
reject_unauthorized: true,
version: 4,
force_disable_retain: false,
password: "mysecretpassword",
server: "my.mqtt.server",
user: "mysecretusername",
};
expect(settings.get().mqtt).toStrictEqual(expected);
expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]);
settings.testing.write();
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
expect(read(secretFile)).toStrictEqual(contentSecret);
settings.set(["mqtt", "user"], "test123");
settings.set(["advanced", "network_key"], [1, 2, 3, 4]);
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
expect(read(secretFile)).toStrictEqual({...contentSecret, username: "test123", network_key: [1, 2, 3, 4]});
});
it("Should read ALL secrets form a separate file", () => {
const contentConfiguration = {
mqtt: {
server: "!secret server",
user: "!secret username",
password: "!secret.yaml password",
},
advanced: {
network_key: "!secret network_key",
},
};
const contentSecret = {
server: "my.mqtt.server",
username: "mysecretusername",
password: "mysecretpassword",
network_key: [1, 2, 3],
};
write(secretFile, contentSecret, false);
write(configurationFile, contentConfiguration);
const expected = {
base_topic: "zigbee2mqtt",
include_device_information: false,
maximum_packet_size: 1048576,
keepalive: 60,
reject_unauthorized: true,
version: 4,
force_disable_retain: false,
password: "mysecretpassword",
server: "my.mqtt.server",
user: "mysecretusername",
};
expect(settings.get().mqtt).toStrictEqual(expected);
expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]);
settings.testing.write();
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
expect(read(secretFile)).toStrictEqual(contentSecret);
settings.set(["mqtt", "server"], "not.secret.server");
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
expect(read(secretFile)).toStrictEqual({...contentSecret, server: "not.secret.server"});
});
it("Should read devices form a separate file", () => {
const contentConfiguration = {
devices: "devices.yaml",
};
const contentDevices = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
};
write(configurationFile, contentConfiguration);
write(devicesFile, contentDevices);
const device = settings.getDevice("0x12345678");
const expected = {
ID: "0x12345678",
friendly_name: "0x12345678",
retain: false,
};
expect(device).toStrictEqual(expected);
});
it("Should read devices form 2 separate files", () => {
const contentConfiguration = {
devices: ["devices.yaml", "devices2.yaml"],
};
const contentDevices = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
};
const contentDevices2 = {
"0x87654321": {
friendly_name: "0x87654321",
retain: false,
},
};
write(configurationFile, contentConfiguration);
write(devicesFile, contentDevices);
write(devicesFile2, contentDevices2);
expect(settings.getDevice("0x12345678").friendly_name).toStrictEqual("0x12345678");
expect(settings.getDevice("0x87654321").friendly_name).toStrictEqual("0x87654321");
});
it("Should add devices to a separate file", () => {
const contentConfiguration = {
devices: "devices.yaml",
};
const contentDevices = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
};
write(configurationFile, contentConfiguration);
write(devicesFile, contentDevices);
settings.addDevice("0x1234");
expect(read(configurationFile)).toStrictEqual({devices: "devices.yaml"});
const expected = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
"0x1234": {
friendly_name: "0x1234",
},
};
expect(read(devicesFile)).toStrictEqual(expected);
});
function extractFromMultipleDeviceConfigs(contentDevices2: Record<string, unknown>): void {
const contentConfiguration = {
devices: ["devices.yaml", "devices2.yaml"],
};
const contentDevices = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
};
write(configurationFile, contentConfiguration);
write(devicesFile, contentDevices);
write(devicesFile2, contentDevices2);
settings.addDevice("0x1234");
expect(read(configurationFile)).toStrictEqual({devices: ["devices.yaml", "devices2.yaml"]});
const expected = {
"0x12345678": {
friendly_name: "0x12345678",
retain: false,
},
"0x1234": {
friendly_name: "0x1234",
},
};
expect(read(devicesFile)).toStrictEqual(expected);
expect(read(devicesFile2)).toStrictEqual(contentDevices2);
}
it("Should add devices for first file when using 2 separates file", () => {
extractFromMultipleDeviceConfigs({
"0x87654321": {
friendly_name: "0x87654321",
retain: false,
},
});
});
it("Should add devices for first file when using 2 separates file and the second file is empty", () => {
extractFromMultipleDeviceConfigs({});
});
it("Should add devices to a separate file if devices.yaml doesnt exist", () => {
const contentConfiguration = {
devices: "devices.yaml",
};
write(configurationFile, contentConfiguration);
settings.addDevice("0x1234");
expect(read(configurationFile)).toStrictEqual({devices: "devices.yaml"});
const expected = {
"0x1234": {
friendly_name: "0x1234",
},
};
expect(read(devicesFile)).toStrictEqual(expected);
});
it("Should add and remove devices to a separate file if devices.yaml doesnt exist", () => {
const contentConfiguration = {
devices: "devices.yaml",
};
write(configurationFile, contentConfiguration);
settings.addDevice("0x1234");
expect(read(configurationFile)).toStrictEqual({devices: "devices.yaml"});
settings.removeDevice("0x1234");
expect(read(configurationFile)).toStrictEqual({devices: "devices.yaml"});
expect(read(devicesFile)).toStrictEqual({});
});
it("Should read groups", () => {
const content = {
groups: {
"1": {
friendly_name: "123",
},
},
};
write(configurationFile, content);
const group = settings.getGroup("1");
expect(group).toStrictEqual({
ID: 1,
friendly_name: "123",
});
});
it("Throw if removing non-existing group", () => {
const content = {
groups: {
"1": {
friendly_name: "123",
},
},
};
write(configurationFile, content);
expect(() => settings.removeGroup("2")).toThrow(`Group '2' does not exist`);
});
it("Should read groups from a separate file", () => {
const contentConfiguration = {
groups: "groups.yaml",
};
const contentGroups = {
"1": {
friendly_name: "123",
},
};
write(configurationFile, contentConfiguration);
write(groupsFile, contentGroups);
const group = settings.getGroup("1");
expect(group).toStrictEqual({
ID: 1,
friendly_name: "123",
});
});
it("Combine everything! groups and devices from separate file :)", () => {
const contentConfiguration = {
devices: "devices.yaml",
groups: "groups.yaml",
};
const contentGroups = {
"1": {
friendly_name: "123",
},
};
write(configurationFile, contentConfiguration);
write(groupsFile, contentGroups);
const expectedConfiguration = {
devices: "devices.yaml",
groups: "groups.yaml",
};
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
settings.addDevice("0x1234");
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
expect(read(devicesFile)).toStrictEqual({
"0x1234": {
friendly_name: "0x1234",
},
});
const group = settings.getGroup("1");
expect(group).toStrictEqual({
ID: 1,
friendly_name: "123",
});
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
expect(settings.getDevice("0x1234")).toStrictEqual({
ID: "0x1234",
friendly_name: "0x1234",
});
});
it("Should add groups", () => {
write(configurationFile, {});
settings.addGroup("test123");
expect(settings.get().groups).toStrictEqual({
"1": {
friendly_name: "test123",
},
});
settings.addGroup("test456", "");
expect(settings.get().groups).toStrictEqual({
"1": {
friendly_name: "test123",
},
"2": {
friendly_name: "test456",
},
});
});
it("Should allow username without password", () => {
const contentConfiguration = {
mqtt: {
server: "my.mqtt.server",
user: "myusername",
},
};
const expected = {
base_topic: "zigbee2mqtt",
include_device_information: false,
maximum_packet_size: 1048576,
keepalive: 60,
reject_unauthorized: true,
version: 4,
force_disable_retain: false,
server: "my.mqtt.server",
user: "myusername",
};
write(configurationFile, contentConfiguration);
expect(settings.get().mqtt).toStrictEqual(expected);
});
it("Should add groups with specific ID", () => {
write(configurationFile, {});
settings.addGroup("test123", "123");
const expected = {
"123": {
friendly_name: "test123",
},
};
expect(settings.get().groups).toStrictEqual(expected);
});
it("Should throw error when changing entity options of non-existing device", () => {
write(configurationFile, {});
expect(() => {
settings.changeEntityOptions("not_existing_123", {});
}).toThrow(`Device or group 'not_existing_123' does not exist`);
});
it("Should throw error when changing entity friendlyName of non-existing device", () => {
write(configurationFile, {});
expect(() => {
settings.changeFriendlyName("not_existing_123", "non_existing_456");
}).toThrow(`Device or group 'not_existing_123' does not exist`);
});
it("Should not add duplicate groups", () => {
write(configurationFile, {});
settings.addGroup("test123");
expect(() => {
settings.addGroup("test123");
}).toThrow(`friendly_name 'test123' is already in use`);
const expected = {
"1": {
friendly_name: "test123",
},
};
expect(settings.get().groups).toStrictEqual(expected);
});
it("Should not add duplicate groups with specific ID", () => {
write(configurationFile, {});
settings.addGroup("test123", "123");
expect(() => {
settings.addGroup("test_id_123", "123");
}).toThrow(new Error("Group ID '123' is already in use"));
const expected = {
"123": {
friendly_name: "test123",
},
};
expect(settings.get().groups).toStrictEqual(expected);
});
it("Should throw when adding device which already exists", () => {
write(configurationFile, {
devices: {
"0x123": {
friendly_name: "bulb",
retain: true,
},
},
});
expect(() => {
settings.addDevice("0x123");
}).toThrow(new Error("Device '0x123' already exists"));
});
it("Should not allow any string values for ext_pan_id", () => {
write(configurationFile, {
...minimalConfig,
advanced: {ext_pan_id: "NOT_GENERATE"},
});
settings.reRead();
const error = `advanced.ext_pan_id: should be array or 'GENERATE' (is 'NOT_GENERATE')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Should not allow any string values for network_key", () => {
write(configurationFile, {
...minimalConfig,
advanced: {network_key: "NOT_GENERATE"},
});
settings.reRead();
const error = `advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Should not allow any string values for pan_id", () => {
write(configurationFile, {
...minimalConfig,
advanced: {pan_id: "NOT_GENERATE"},
});
settings.reRead();
const error = `advanced.pan_id: should be number or 'GENERATE' (is 'NOT_GENERATE')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Should allow retention configuration with MQTT v5", () => {
write(configurationFile, {
...minimalConfig,
mqtt: {base_topic: "zigbee2mqtt", server: "localhost", version: 5},
devices: {"0x0017880104e45519": {friendly_name: "tain", retention: 900}},
});
settings.reRead();
expect(settings.validate()).toEqual([]);
});
it("Should not allow retention configuration without MQTT v5", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "tain", retention: 900}},
});
settings.reRead();
const error = "MQTT retention requires protocol version 5";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Should validate if settings do not conform to scheme", () => {
write(configurationFile, {
...minimalConfig,
advanced: null,
});
settings.reRead();
const error = "advanced must be object";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Should add devices to blocklist", () => {
write(configurationFile, {});
settings.blockDevice("0x123");
expect(settings.get().blocklist).toStrictEqual(["0x123"]);
settings.blockDevice("0x1234");
expect(settings.get().blocklist).toStrictEqual(["0x123", "0x1234"]);
});
it("Configuration shouldnt be valid when invalid QOS value is used", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "myname", retain: false, qos: 3}},
});
settings.reRead();
const error = "devices/0x0017880104e45519/qos must be equal to one of the allowed values";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration shouldnt be valid when duplicate friendly_name are used", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "myname", retain: false}},
groups: {"1": {friendly_name: "myname", retain: false}},
});
settings.reRead();
const error = `Duplicate friendly_name 'myname' found`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it.each([
["bla.png", `Device icon of 'myname' should start with 'device_icons/', got 'bla.png'`],
["device_icons/bla.png", undefined],
["https://www.example.org/my-device.png", undefined],
["http://www.example.org/my-device.png", undefined],
])("Device icons should be in `device_icons` directory or a url", (icon, error) => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "myname", icon}},
});
settings.reRead();
if (error) {
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
} else {
expect(settings.validate()).toEqual([]);
}
});
it("Configuration friendly name cannot be empty", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "", retain: false}},
});
settings.reRead();
const error = "friendly_name must be at least 1 char long";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration friendly name cannot end with /", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "blaa/", retain: false}},
});
settings.reRead();
const error = "friendly_name is not allowed to end or start with /";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration friendly name cannot contain control char", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "blaa/blaa\u009f", retain: false}},
});
settings.reRead();
const error = "friendly_name is not allowed to contain control char";
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration shouldnt be valid when friendly_name ends with /DIGIT", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "myname/123", retain: false}},
});
settings.reRead();
const error = `Friendly name cannot end with a "/DIGIT" ('myname/123')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration shouldnt be valid when friendly_name contains a MQTT wildcard", () => {
write(configurationFile, {
...minimalConfig,
devices: {"0x0017880104e45519": {friendly_name: "myname#", retain: false}},
});
settings.reRead();
const error = `MQTT wildcard (+ and #) not allowed in friendly_name ('myname#')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it("Configuration shouldnt be valid when duplicate friendly_name are used", () => {
write(configurationFile, {
devices: {
"0x0017880104e45519": {friendly_name: "myname", retain: false},
"0x0017880104e45511": {friendly_name: "myname1", retain: false},
},
});
settings.reRead();
expect(() => {
settings.changeFriendlyName("myname1", "myname");
}).toThrowError(`friendly_name 'myname' is already in use`);
});
it("Should throw when removing device which doesnt exist", () => {
write(configurationFile, {
devices: {
"0x0017880104e45519": {friendly_name: "myname", retain: false},
"0x0017880104e45511": {friendly_name: "myname1", retain: false},
},
});
settings.reRead();
expect(() => {
settings.removeDevice("myname33");
}).toThrowError(`Device 'myname33' does not exist`);
});
it("Shouldnt write to configuration.yaml when there are no changes in it", () => {
const contentConfiguration = {devices: "devices.yaml"};
const contentDevices = {};
write(configurationFile, contentConfiguration);
const before = fs.statSync(configurationFile).mtimeMs;
write(devicesFile, contentDevices);
settings.addDevice("0x1234");
const after = fs.statSync(configurationFile).mtimeMs;
expect(before).toBe(after);
});
it("Should keep homeassistant null property on device setting change", () => {
write(configurationFile, {
devices: {
"0x12345678": {
friendly_name: "custom discovery",
homeassistant: {
entityXYZ: {
entity_category: null,
},
},
},
},
});
settings.changeEntityOptions("0x12345678", {disabled: true});
const actual = read(configurationFile);
const expected = {
devices: {
"0x12345678": {
friendly_name: "custom discovery",
disabled: true,
homeassistant: {
entityXYZ: {
entity_category: null,
},
},
},
},
};
expect(actual).toStrictEqual(expected);
});
it("Should keep homeassistant null properties on apply", () => {
write(configurationFile, {
device_options: {
homeassistant: {temperature: null},
},
devices: {
"0x1234567812345678": {
friendly_name: "custom discovery",
homeassistant: {humidity: null},
},
},
});
settings.reRead();
settings.apply({external_converters: []});
expect(settings.get().device_options.homeassistant).toStrictEqual({temperature: null});
expect(settings.get().devices["0x1234567812345678"].homeassistant).toStrictEqual({humidity: null});
});
it("Frontend config", () => {
write(configurationFile, {frontend: {enabled: true}});
settings.reRead();
expect(settings.get().frontend).toStrictEqual({enabled: true, package: "zigbee2mqtt-windfront", port: 8080, base_url: "/"});
});
});