mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-06-21 12:41:47 +00:00
dd1c449796
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
996 lines
44 KiB
TypeScript
996 lines
44 KiB
TypeScript
// biome-ignore assist/source/organizeImports: import mocks first
|
|
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
|
import * as data from "./mocks/data";
|
|
|
|
import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs";
|
|
import objectAssignDeep from "object-assign-deep";
|
|
import mockedData from "../lib/util/data";
|
|
import * as settings from "../lib/util/settings";
|
|
import * as settingsMigration from "../lib/util/settingsMigration";
|
|
import path from "node:path";
|
|
|
|
describe("Settings Migration", () => {
|
|
beforeAll(() => {});
|
|
|
|
afterAll(() => {
|
|
rmSync(data.mockDir, {recursive: true, force: true});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
data.writeDefaultConfiguration();
|
|
settings.reRead();
|
|
});
|
|
|
|
afterEach(() => {
|
|
settings.testing.CURRENT_VERSION = settings.CURRENT_VERSION;
|
|
|
|
if (settings.get().version === settings.CURRENT_VERSION) {
|
|
// always validate after each test when up to current version (matching current schema)
|
|
expect(settings.validate()).toStrictEqual([]);
|
|
}
|
|
});
|
|
|
|
it("Fails on unsupported version", () => {
|
|
settings.set(["version"], 0);
|
|
|
|
expect(() => settingsMigration.migrateIfNecessary()).toThrow(
|
|
"Your configuration.yaml has an unsupported version 0, expected one of undefined,2",
|
|
);
|
|
|
|
settings.set(["version"], 99999);
|
|
|
|
expect(() => settingsMigration.migrateIfNecessary()).toThrow(
|
|
"Your configuration.yaml has an unsupported version 99999, expected one of undefined,2",
|
|
);
|
|
});
|
|
|
|
describe("Migrates v1 to v2", () => {
|
|
const BASE_CONFIG = {
|
|
homeassistant: false,
|
|
mqtt: {
|
|
base_topic: "zigbee2mqtt",
|
|
server: "mqtt://localhost",
|
|
},
|
|
serial: {
|
|
port: "/dev/dummy",
|
|
},
|
|
devices: {
|
|
"0x18fc2600000d7ae2": {
|
|
friendly_name: "bosch_radiator",
|
|
},
|
|
"0x000b57fffec6a5b2": {
|
|
retain: true,
|
|
friendly_name: "bulb",
|
|
description: "this is my bulb",
|
|
},
|
|
"0x0017880104e45517": {
|
|
retain: true,
|
|
friendly_name: "remote",
|
|
},
|
|
"0x0017880104e45520": {
|
|
retain: false,
|
|
friendly_name: "button",
|
|
},
|
|
"0x0017880104e45521": {
|
|
retain: false,
|
|
friendly_name: "button_double_key",
|
|
},
|
|
"0x0017880104e45522": {
|
|
qos: 1,
|
|
retain: false,
|
|
friendly_name: "weather_sensor",
|
|
},
|
|
"0x0017880104e45523": {
|
|
retain: false,
|
|
friendly_name: "occupancy_sensor",
|
|
},
|
|
"0x0017880104e45524": {
|
|
retain: false,
|
|
friendly_name: "power_plug",
|
|
},
|
|
"0x0017880104e45530": {
|
|
retain: false,
|
|
friendly_name: "button_double_key_interviewing",
|
|
},
|
|
"0x0017880104e45540": {
|
|
friendly_name: "ikea_onoff",
|
|
},
|
|
"0x000b57fffec6a5b7": {
|
|
retain: false,
|
|
friendly_name: "bulb_2",
|
|
},
|
|
"0x000b57fffec6a5b3": {
|
|
retain: false,
|
|
friendly_name: "bulb_color",
|
|
},
|
|
"0x000b57fffec6a5b4": {
|
|
retain: false,
|
|
friendly_name: "bulb_color_2",
|
|
},
|
|
"0x0017880104e45541": {
|
|
retain: false,
|
|
friendly_name: "wall_switch",
|
|
},
|
|
"0x0017880104e45542": {
|
|
retain: false,
|
|
friendly_name: "wall_switch_double",
|
|
},
|
|
"0x0017880104e45543": {
|
|
retain: false,
|
|
friendly_name: "led_controller_1",
|
|
},
|
|
"0x0017880104e45544": {
|
|
retain: false,
|
|
friendly_name: "led_controller_2",
|
|
},
|
|
"0x0017880104e45545": {
|
|
retain: false,
|
|
friendly_name: "dimmer_wall_switch",
|
|
},
|
|
"0x0017880104e45547": {
|
|
retain: false,
|
|
friendly_name: "curtain",
|
|
},
|
|
"0x0017880104e45548": {
|
|
retain: false,
|
|
friendly_name: "fan",
|
|
},
|
|
"0x0017880104e45549": {
|
|
retain: false,
|
|
friendly_name: "siren",
|
|
},
|
|
"0x0017880104e45529": {
|
|
retain: false,
|
|
friendly_name: "unsupported2",
|
|
},
|
|
"0x0017880104e45550": {
|
|
retain: false,
|
|
friendly_name: "thermostat",
|
|
},
|
|
"0x0017880104e45551": {
|
|
retain: false,
|
|
friendly_name: "smart vent",
|
|
},
|
|
"0x0017880104e45552": {
|
|
retain: false,
|
|
friendly_name: "j1",
|
|
},
|
|
"0x0017880104e45553": {
|
|
retain: false,
|
|
friendly_name: "bulb_enddevice",
|
|
},
|
|
"0x0017880104e45559": {
|
|
retain: false,
|
|
friendly_name: "cc2530_router",
|
|
},
|
|
"0x0017880104e45560": {
|
|
retain: false,
|
|
friendly_name: "livolo",
|
|
},
|
|
"0x90fd9ffffe4b64ae": {
|
|
retain: false,
|
|
friendly_name: "tradfri_remote",
|
|
},
|
|
"0x90fd9ffffe4b64af": {
|
|
friendly_name: "roller_shutter",
|
|
},
|
|
"0x90fd9ffffe4b64ax": {
|
|
friendly_name: "ZNLDP12LM",
|
|
},
|
|
"0x90fd9ffffe4b64aa": {
|
|
friendly_name: "SP600_OLD",
|
|
},
|
|
"0x90fd9ffffe4b64ab": {
|
|
friendly_name: "SP600_NEW",
|
|
},
|
|
"0x90fd9ffffe4b64ac": {
|
|
friendly_name: "MKS-CM-W5",
|
|
},
|
|
"0x0017880104e45526": {
|
|
friendly_name: "GL-S-007ZS",
|
|
},
|
|
"0x0017880104e43559": {
|
|
friendly_name: "U202DST600ZB",
|
|
},
|
|
"0xf4ce368a38be56a1": {
|
|
retain: false,
|
|
friendly_name: "zigfred_plus",
|
|
front_surface_enabled: "true",
|
|
dimmer_1_enabled: "true",
|
|
dimmer_1_dimming_enabled: "true",
|
|
dimmer_2_enabled: "true",
|
|
dimmer_2_dimming_enabled: "true",
|
|
dimmer_3_enabled: "true",
|
|
dimmer_3_dimming_enabled: "true",
|
|
dimmer_4_enabled: "true",
|
|
dimmer_4_dimming_enabled: "true",
|
|
cover_1_enabled: "true",
|
|
cover_1_tilt_enabled: "true",
|
|
cover_2_enabled: "true",
|
|
cover_2_tilt_enabled: "true",
|
|
},
|
|
"0x0017880104e44559": {
|
|
friendly_name: "3157100_thermostat",
|
|
},
|
|
"0x0017880104a44559": {
|
|
friendly_name: "J1_cover",
|
|
},
|
|
"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",
|
|
},
|
|
"0x0017880104e45724": {
|
|
friendly_name: "GLEDOPTO_2ID",
|
|
},
|
|
"0x0017880104e45561": {
|
|
friendly_name: "temperature_sensor",
|
|
},
|
|
"0x0017880104e45562": {
|
|
friendly_name: "heating_actuator",
|
|
},
|
|
},
|
|
groups: {
|
|
1: {
|
|
friendly_name: "group_1",
|
|
retain: false,
|
|
},
|
|
2: {
|
|
friendly_name: "group_2",
|
|
retain: false,
|
|
},
|
|
15071: {
|
|
friendly_name: "group_tradfri_remote",
|
|
retain: false,
|
|
},
|
|
11: {
|
|
friendly_name: "group_with_tradfri",
|
|
retain: false,
|
|
},
|
|
12: {
|
|
friendly_name: "thermostat_group",
|
|
retain: false,
|
|
},
|
|
14: {
|
|
friendly_name: "switch_group",
|
|
retain: false,
|
|
},
|
|
21: {
|
|
friendly_name: "gledopto_group",
|
|
},
|
|
9: {
|
|
friendly_name: "ha_discovery_group",
|
|
},
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
settings.testing.CURRENT_VERSION = 2; // stop update after this version
|
|
data.writeDefaultConfiguration(BASE_CONFIG);
|
|
settings.reRead();
|
|
});
|
|
|
|
it("no change needed - only add version", () => {
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
});
|
|
|
|
it("remove all", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
|
|
settings.set(["homeassistant", "legacy_triggers"], true);
|
|
settings.set(["homeassistant", "legacy_entity_attributes"], true);
|
|
settings.set(["ota", "ikea_ota_use_test_url"], true);
|
|
settings.set(["advanced", "homeassistant_legacy_triggers"], true);
|
|
settings.set(["advanced", "homeassistant_legacy_entity_attributes"], true);
|
|
settings.set(["permit_join"], true);
|
|
settings.set(["advanced", "ikea_ota_use_test_url"], true);
|
|
settings.set(["advanced", "legacy_api"], true);
|
|
settings.set(["advanced", "legacy_availability_payload"], true);
|
|
settings.set(["advanced", "soft_reset_timeout"], 12);
|
|
settings.set(["advanced", "report"], true);
|
|
settings.set(["advanced", "availability_timeout"], 65);
|
|
settings.set(["advanced", "availability_blocklist"], ["abcd", "efgh"]);
|
|
settings.set(["advanced", "availability_passlist"], ["abcd"]);
|
|
settings.set(["advanced", "availability_blacklist"], ["abcd", "efgh"]);
|
|
settings.set(["advanced", "availability_whitelist"], ["abcd", "efgh"]);
|
|
settings.set(["device_options", "legacy"], true);
|
|
settings.set(["devices", "0x18fc2600000d7ae2", "retrieve_state"], true);
|
|
settings.set(["devices", "0x000b57fffec6a5b2", "retrieve_state"], true);
|
|
settings.set(["groups", "15071", "retrieve_state"], true);
|
|
settings.set(["groups", "12", "devices"], ["0x0017880104e45521", "0x0017880104e45524"]);
|
|
settings.set(["external_converters"], ["zyx.js"]);
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
permit_join: true,
|
|
homeassistant: {
|
|
legacy_triggers: true,
|
|
legacy_entity_attributes: true,
|
|
},
|
|
ota: {ikea_ota_use_test_url: true},
|
|
advanced: {
|
|
homeassistant_legacy_triggers: true,
|
|
homeassistant_legacy_entity_attributes: true,
|
|
ikea_ota_use_test_url: true,
|
|
legacy_api: true,
|
|
legacy_availability_payload: true,
|
|
soft_reset_timeout: 12,
|
|
report: true,
|
|
availability_timeout: 65,
|
|
availability_blocklist: ["abcd", "efgh"],
|
|
availability_passlist: ["abcd"],
|
|
availability_blacklist: ["abcd", "efgh"],
|
|
availability_whitelist: ["abcd", "efgh"],
|
|
},
|
|
device_options: {legacy: true},
|
|
devices: {
|
|
"0x18fc2600000d7ae2": {retrieve_state: true},
|
|
"0x000b57fffec6a5b2": {retrieve_state: true},
|
|
},
|
|
groups: {
|
|
15071: {retrieve_state: true},
|
|
12: {devices: ["0x0017880104e45521", "0x0017880104e45524"]},
|
|
},
|
|
external_converters: ["zyx.js"],
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings.advanced).toStrictEqual({});
|
|
expect(migratedSettings.device_options).toStrictEqual({});
|
|
expect(migratedSettings.ota).toStrictEqual({});
|
|
expect(migratedSettings.homeassistant).toStrictEqual({});
|
|
|
|
// defaults added automatically when pushing to these keys, remove to match against default (verified by above expects)
|
|
migratedSettings.homeassistant = false;
|
|
delete migratedSettings.advanced;
|
|
delete migratedSettings.device_options;
|
|
delete migratedSettings.ota;
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain("homeassistant.legacy_triggers");
|
|
expect(migrationNotesContent).toContain("homeassistant.legacy_entity_attributes");
|
|
expect(migrationNotesContent).toContain("ota.ikea_ota_use_test_url");
|
|
expect(migrationNotesContent).toContain("permit_join");
|
|
expect(migrationNotesContent).toContain("advanced.legacy_api");
|
|
expect(migrationNotesContent).toContain("advanced.legacy_availability_payload");
|
|
expect(migrationNotesContent).toContain("advanced.soft_reset_timeout");
|
|
expect(migrationNotesContent).toContain("advanced.report");
|
|
expect(migrationNotesContent).toContain("advanced.availability_timeout");
|
|
expect(migrationNotesContent).toContain("advanced.availability_blocklist");
|
|
expect(migrationNotesContent).toContain("advanced.availability_passlist");
|
|
expect(migrationNotesContent).toContain("advanced.availability_blacklist");
|
|
expect(migrationNotesContent).toContain("advanced.availability_whitelist");
|
|
expect(migrationNotesContent).toContain("device_options.legacy");
|
|
expect(migrationNotesContent).toContain("(devices|groups).xyz.retrieve_state");
|
|
expect(migrationNotesContent).toContain("groups.xyz.devices");
|
|
expect(migrationNotesContent).toContain("External converters are now automatically loaded");
|
|
});
|
|
|
|
it("remove partial", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
|
|
settings.set(["advanced", "homeassistant_legacy_triggers"], true);
|
|
settings.set(["advanced", "homeassistant_legacy_entity_attributes"], true);
|
|
settings.set(["permit_join"], true);
|
|
settings.set(["advanced", "ikea_ota_use_test_url"], true);
|
|
settings.set(["advanced", "legacy_api"], true);
|
|
settings.set(["advanced", "legacy_availability_payload"], false);
|
|
settings.set(["advanced", "soft_reset_timeout"], 16);
|
|
settings.set(["advanced", "report"], true);
|
|
settings.set(["advanced", "availability_timeout"], 64);
|
|
settings.set(["advanced", "availability_passlist"], []);
|
|
settings.set(["device_options", "legacy"], true);
|
|
settings.set(["groups", "12", "devices"], ["0x0017880104e45521", "0x0017880104e45524"]);
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
permit_join: true,
|
|
advanced: {
|
|
homeassistant_legacy_triggers: true,
|
|
homeassistant_legacy_entity_attributes: true,
|
|
ikea_ota_use_test_url: true,
|
|
legacy_api: true,
|
|
legacy_availability_payload: false,
|
|
soft_reset_timeout: 16,
|
|
report: true,
|
|
availability_timeout: 64,
|
|
availability_passlist: [],
|
|
},
|
|
device_options: {legacy: true},
|
|
groups: {12: {devices: ["0x0017880104e45521", "0x0017880104e45524"]}},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings.advanced).toStrictEqual({});
|
|
expect(migratedSettings.device_options).toStrictEqual({});
|
|
|
|
// defaults added automatically when pushing to these keys, remove to match against default (verified by above expects)
|
|
migratedSettings.homeassistant = false;
|
|
delete migratedSettings.advanced;
|
|
delete migratedSettings.device_options;
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain("homeassistant.legacy_triggers");
|
|
expect(migrationNotesContent).toContain("homeassistant.legacy_entity_attributes");
|
|
expect(migrationNotesContent).toContain("ota.ikea_ota_use_test_url");
|
|
expect(migrationNotesContent).toContain("permit_join");
|
|
expect(migrationNotesContent).toContain("advanced.legacy_api");
|
|
expect(migrationNotesContent).not.toContain("advanced.legacy_availability_payload"); // was false, no impact
|
|
expect(migrationNotesContent).toContain("advanced.soft_reset_timeout");
|
|
expect(migrationNotesContent).toContain("advanced.report");
|
|
expect(migrationNotesContent).toContain("advanced.availability_timeout");
|
|
expect(migrationNotesContent).not.toContain("advanced.availability_blocklist");
|
|
expect(migrationNotesContent).not.toContain("advanced.availability_passlist"); // empty array
|
|
expect(migrationNotesContent).not.toContain("advanced.availability_blacklist");
|
|
expect(migrationNotesContent).not.toContain("advanced.availability_whitelist");
|
|
expect(migrationNotesContent).toContain("device_options.legacy");
|
|
expect(migrationNotesContent).not.toContain("(devices|groups).xyz.retrieve_state");
|
|
expect(migrationNotesContent).toContain("groups.xyz.devices");
|
|
});
|
|
|
|
it("changes log_level", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
afterSettings.advanced = {log_level: "warning"};
|
|
|
|
settings.set(["advanced", "log_level"], "warn");
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
advanced: {
|
|
log_level: "warn",
|
|
},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain(`Log level 'warn' has been renamed to 'warning'.`);
|
|
});
|
|
|
|
it("does not changes already migrated log_level", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
afterSettings.advanced = {log_level: "warning"};
|
|
|
|
settings.set(["advanced", "log_level"], "warning");
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
advanced: {
|
|
log_level: "warning",
|
|
},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).not.toContain(`Log level 'warn' has been renamed to 'warning'.`);
|
|
});
|
|
|
|
it("does not changes other log_level", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
afterSettings.advanced = {log_level: "info"};
|
|
|
|
settings.set(["advanced", "log_level"], "info");
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
advanced: {
|
|
log_level: "info",
|
|
},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).not.toContain(`Log level 'warn' has been renamed to 'warning'.`);
|
|
});
|
|
|
|
it("transfer all", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
afterSettings.advanced = {
|
|
transmit_power: 12,
|
|
output: "attribute",
|
|
};
|
|
afterSettings.serial.baudrate = 115200;
|
|
afterSettings.serial.rtscts = true;
|
|
afterSettings.blocklist = ["abcd"];
|
|
afterSettings.passlist = ["efgh"];
|
|
afterSettings.homeassistant = {
|
|
discovery_topic: "ha_disc",
|
|
status_topic: "ha_stat",
|
|
};
|
|
// Here, the `experimental` section is also explicitly removed after the transfer, so the empty object is gone
|
|
// afterSettings.experimental = {}; // caused by pushing to key and removing all
|
|
|
|
settings.set(["advanced", "homeassistant_discovery_topic"], "ha_disc");
|
|
settings.set(["advanced", "homeassistant_status_topic"], "ha_stat");
|
|
settings.set(["advanced", "baudrate"], 115200);
|
|
settings.set(["advanced", "rtscts"], true); // only deleted since also below
|
|
settings.set(["serial", "rtscts"], true);
|
|
settings.set(["experimental", "transmit_power"], 12);
|
|
settings.set(["experimental", "output"], "attribute");
|
|
settings.set(["ban"], ["abcd"]);
|
|
settings.set(["whitelist"], ["efgh"]);
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
advanced: {
|
|
homeassistant_discovery_topic: "ha_disc",
|
|
homeassistant_status_topic: "ha_stat",
|
|
baudrate: 115200,
|
|
rtscts: true,
|
|
},
|
|
serial: {
|
|
rtscts: true,
|
|
},
|
|
experimental: {
|
|
transmit_power: 12,
|
|
output: "attribute",
|
|
},
|
|
ban: ["abcd"],
|
|
whitelist: ["efgh"],
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain(
|
|
"HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.",
|
|
);
|
|
expect(migrationNotesContent).toContain(
|
|
"HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.",
|
|
);
|
|
expect(migrationNotesContent).toContain("Baudrate was moved from advanced.baudrate to serial.baudrate.");
|
|
expect(migrationNotesContent).toContain("RTSCTS was moved from advanced.rtscts to serial.rtscts.");
|
|
expect(migrationNotesContent).toContain("Transmit power was moved from experimental.transmit_power to advanced.transmit_power.");
|
|
expect(migrationNotesContent).toContain("Output was moved from experimental.output to advanced.output.");
|
|
expect(migrationNotesContent).toContain("ban was renamed to passlist.");
|
|
expect(migrationNotesContent).toContain("whitelist was renamed to passlist.");
|
|
expect(migrationNotesContent).toContain("The entire experimental section was removed.");
|
|
});
|
|
|
|
it("transfer partial", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 2;
|
|
afterSettings.advanced = {}; // caused by pushing to key and removing all
|
|
afterSettings.serial.baudrate = 115200;
|
|
afterSettings.serial.rtscts = true;
|
|
afterSettings.blocklist = ["abcd", "efgh"];
|
|
afterSettings.homeassistant = {
|
|
discovery_topic: "ha_disc_newer", // keeps the newer value, just removes the old path
|
|
};
|
|
|
|
settings.set(["homeassistant", "discovery_topic"], "ha_disc_newer");
|
|
settings.set(["advanced", "homeassistant_discovery_topic"], "ha_disc");
|
|
settings.set(["advanced", "baudrate"], 115200);
|
|
settings.set(["advanced", "rtscts"], true); // only deleted since also below
|
|
settings.set(["serial", "rtscts"], true);
|
|
settings.set(["ban"], ["abcd"]);
|
|
settings.set(["blocklist"], ["efgh"]);
|
|
|
|
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
homeassistant: {discovery_topic: "ha_disc_newer"},
|
|
advanced: {
|
|
homeassistant_discovery_topic: "ha_disc",
|
|
baudrate: 115200,
|
|
rtscts: true,
|
|
},
|
|
serial: {
|
|
rtscts: true,
|
|
},
|
|
ban: ["abcd"],
|
|
blocklist: ["efgh"],
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(existsSync(mockedData.joinPath("configuration_backup_v1.yaml"))).toStrictEqual(true);
|
|
const migrationNotes = mockedData.joinPath("migration-1-to-2.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain("[TRANSFER] Baudrate was moved from advanced.baudrate to serial.baudrate.");
|
|
expect(migrationNotesContent).toContain("[REMOVAL] RTSCTS was moved from advanced.rtscts to serial.rtscts.");
|
|
expect(migrationNotesContent).toContain("[TRANSFER] ban was renamed to passlist.");
|
|
});
|
|
});
|
|
|
|
describe("Migrates v1 to v3", () => {
|
|
const BASE_CONFIG = {
|
|
mqtt: {
|
|
server: "mqtt://localhost",
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
settings.testing.CURRENT_VERSION = 3; // stop update after this version
|
|
data.writeDefaultConfiguration(BASE_CONFIG);
|
|
settings.reRead();
|
|
});
|
|
|
|
it("Update", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 3;
|
|
afterSettings.homeassistant = {enabled: false};
|
|
afterSettings.frontend = {enabled: true};
|
|
afterSettings.availability = {enabled: true, active: {timeout: 15}};
|
|
afterSettings.advanced = {
|
|
log_level: "warning",
|
|
transmit_power: 12,
|
|
};
|
|
|
|
settings.set(["homeassistant"], false);
|
|
settings.set(["frontend"], true);
|
|
settings.set(["availability"], {active: {timeout: 15}});
|
|
settings.set(["permit_join"], true);
|
|
settings.set(["advanced", "log_level"], "warn");
|
|
settings.set(["experimental", "transmit_power"], 12);
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
homeassistant: false,
|
|
frontend: true,
|
|
availability: {active: {timeout: 15}},
|
|
permit_join: true,
|
|
advanced: {log_level: "warn"},
|
|
experimental: {transmit_power: 12},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
});
|
|
});
|
|
|
|
describe("Migrates v2 to v3", () => {
|
|
const BASE_CONFIG = {
|
|
version: 2,
|
|
mqtt: {
|
|
server: "mqtt://localhost",
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
settings.testing.CURRENT_VERSION = 3; // stop update after this version
|
|
data.writeDefaultConfiguration(BASE_CONFIG);
|
|
settings.reRead();
|
|
});
|
|
|
|
it("Update", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 3;
|
|
afterSettings.homeassistant = {enabled: false};
|
|
afterSettings.frontend = {enabled: true};
|
|
afterSettings.availability = {enabled: true, active: {timeout: 15}};
|
|
|
|
settings.set(["homeassistant"], false);
|
|
settings.set(["frontend"], true);
|
|
settings.set(["availability"], {active: {timeout: 15}});
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
homeassistant: false,
|
|
frontend: true,
|
|
availability: {active: {timeout: 15}},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
const migrationNotes = mockedData.joinPath("migration-2-to-3.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain(`[SPECIAL] Property 'homeassistant' is now always an object.`);
|
|
expect(migrationNotesContent).toContain(`[SPECIAL] Property 'frontend' is now always an object.`);
|
|
expect(migrationNotesContent).toContain(`[SPECIAL] Property 'availability' is now always an object.`);
|
|
});
|
|
|
|
it("Update when not set, tests that frontend/availability is not added when not set", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 3;
|
|
afterSettings.homeassistant = {enabled: false};
|
|
|
|
settings.set(["homeassistant"], false);
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
homeassistant: false,
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
const migrationNotes = mockedData.joinPath("migration-2-to-3.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain(`[SPECIAL] Property 'homeassistant' is now always an object.`);
|
|
expect(migrationNotesContent).not.toContain(`[SPECIAL] Property 'frontend' is now always an object.`);
|
|
expect(migrationNotesContent).not.toContain(`[SPECIAL] Property 'availability' is now always an object.`);
|
|
});
|
|
});
|
|
|
|
describe("Migrates v3 to v4", () => {
|
|
const BASE_CONFIG = {
|
|
version: 3,
|
|
mqtt: {
|
|
server: "mqtt://localhost",
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
settings.testing.CURRENT_VERSION = 4; // stop update after this version
|
|
data.writeDefaultConfiguration(BASE_CONFIG);
|
|
settings.reRead();
|
|
});
|
|
|
|
it("Update", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 4;
|
|
afterSettings.devices = {
|
|
"0x123127fffe8d96bc": {
|
|
friendly_name: "0x847127fffe8d96bc",
|
|
icon: "device_icons/08a9016bbc0657cf5f581ae9c19c31a5.png",
|
|
},
|
|
"0x223127fffe8d96bc": {
|
|
friendly_name: "0x223127fffe8d96bc",
|
|
icon: "device_icons/effcad234beeb56ea7c457cf2d36d10b.png",
|
|
},
|
|
"0x323127fffe8d96bc": {
|
|
friendly_name: "0x323127fffe8d96bc",
|
|
},
|
|
};
|
|
|
|
settings.set(["devices"], {
|
|
"0x123127fffe8d96bc": {
|
|
friendly_name: "0x847127fffe8d96bc",
|
|
icon: "device_icons/08a9016bbc0657cf5f581ae9c19c31a5.png",
|
|
},
|
|
"0x223127fffe8d96bc": {
|
|
friendly_name: "0x223127fffe8d96bc",
|
|
icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJ",
|
|
},
|
|
"0x323127fffe8d96bc": {
|
|
friendly_name: "0x323127fffe8d96bc",
|
|
},
|
|
});
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(
|
|
// @ts-expect-error workaround
|
|
objectAssignDeep.noMutate(beforeSettings, {
|
|
devices: {
|
|
"0x123127fffe8d96bc": {
|
|
friendly_name: "0x847127fffe8d96bc",
|
|
icon: "device_icons/08a9016bbc0657cf5f581ae9c19c31a5.png",
|
|
},
|
|
"0x223127fffe8d96bc": {
|
|
friendly_name: "0x223127fffe8d96bc",
|
|
icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJ",
|
|
},
|
|
"0x323127fffe8d96bc": {
|
|
friendly_name: "0x323127fffe8d96bc",
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
const migrationNotes = mockedData.joinPath("migration-3-to-4.log");
|
|
expect(existsSync(migrationNotes)).toStrictEqual(true);
|
|
const migrationNotesContent = readFileSync(migrationNotes, "utf8");
|
|
expect(migrationNotesContent).toContain("[SPECIAL] Device icons are now saved as images.");
|
|
});
|
|
});
|
|
|
|
describe("Migrates v4 to v5", () => {
|
|
const BASE_CONFIG = {
|
|
version: 4,
|
|
mqtt: {
|
|
server: "mqtt://localhost",
|
|
},
|
|
};
|
|
const DEFAULT_STATE: Record<string, unknown> = {
|
|
"0x000b57fffec6a5b2": {
|
|
state: "ON",
|
|
brightness: 50,
|
|
color_temp: 370,
|
|
linkquality: 99,
|
|
},
|
|
"0x0017880104e45517": {
|
|
brightness: 255,
|
|
update: {state: "idle"},
|
|
},
|
|
1: {
|
|
state: "ON",
|
|
update: {state: "available", installed_version: 1, latest_version: 2},
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
settings.testing.CURRENT_VERSION = 5; // stop update after this version
|
|
data.writeDefaultConfiguration(BASE_CONFIG);
|
|
data.writeDefaultState(DEFAULT_STATE);
|
|
settings.reRead();
|
|
});
|
|
|
|
it("Update", () => {
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 5;
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
|
|
expect(data.readState()).toStrictEqual(DEFAULT_STATE);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
|
|
// @ts-expect-error workaround
|
|
const migratedState = objectAssignDeep.noMutate({}, DEFAULT_STATE);
|
|
delete (migratedState["0x0017880104e45517"] as Record<string, unknown>).update;
|
|
delete (migratedState[1] as Record<string, unknown>).update;
|
|
|
|
expect(data.readState()).toStrictEqual(migratedState);
|
|
});
|
|
|
|
it("handles errors gracefully", () => {
|
|
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
writeFileSync(path.join(data.mockDir, "state.json"), "notjson", "utf8");
|
|
|
|
// @ts-expect-error workaround
|
|
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
// @ts-expect-error workaround
|
|
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
|
|
afterSettings.version = 5;
|
|
|
|
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
|
|
|
|
settingsMigration.migrateIfNecessary();
|
|
|
|
const migratedSettings = settings.getPersistedSettings();
|
|
expect(migratedSettings).toStrictEqual(afterSettings);
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to write state"));
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("is not valid JSON"));
|
|
});
|
|
});
|
|
});
|