mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-28 13:34:24 +00:00
feat: Automatic settings migration (#24871)
* feat: Automatic settings migration * Handle failing on unsupported version. * Handle change not needed. * Test change not wanted. * Cleanup. * Add `external_converters` removal. * Fix schema export.
This commit is contained in:
@@ -129,6 +129,13 @@ async function start() {
|
||||
|
||||
settings.reRead();
|
||||
|
||||
// gc
|
||||
{
|
||||
const settingsMigration = require('./dist/util/settingsMigration');
|
||||
|
||||
settingsMigration.migrateIfNecessary();
|
||||
}
|
||||
|
||||
const errors = settings.validate();
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
@@ -663,7 +663,7 @@ export default class Bridge extends Extension {
|
||||
permit_join_timeout: this.zigbee.getPermitJoinTimeout(),
|
||||
restart_required: this.restartRequired,
|
||||
config,
|
||||
config_schema: settings.schema,
|
||||
config_schema: settings.schemaJson,
|
||||
};
|
||||
|
||||
await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
|
||||
Vendored
+1
-1
@@ -107,6 +107,7 @@ declare global {
|
||||
|
||||
// Settings
|
||||
interface Settings {
|
||||
version?: number;
|
||||
homeassistant?: {
|
||||
discovery_topic: string;
|
||||
status_topic: string;
|
||||
@@ -222,7 +223,6 @@ declare global {
|
||||
filtered_optimistic?: string[];
|
||||
icon?: string;
|
||||
homeassistant?: KeyValue;
|
||||
legacy?: boolean;
|
||||
friendly_name: string;
|
||||
description?: string;
|
||||
qos?: 0 | 1 | 2;
|
||||
|
||||
@@ -725,71 +725,8 @@
|
||||
"enum": ["attribute_and_json", "attribute", "json"],
|
||||
"title": "MQTT output type",
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)"
|
||||
},
|
||||
"homeassistant_discovery_topic": {
|
||||
"type": "string",
|
||||
"title": "Homeassistant discovery topic",
|
||||
"description": "Home Assistant discovery topic",
|
||||
"requiresRestart": true,
|
||||
"examples": ["homeassistant"]
|
||||
},
|
||||
"homeassistant_status_topic": {
|
||||
"type": "string",
|
||||
"title": "Home Assistant status topic",
|
||||
"description": "Home Assistant status topic",
|
||||
"requiresRestart": true,
|
||||
"examples": ["homeassistant/status"]
|
||||
},
|
||||
"baudrate": {
|
||||
"type": "number",
|
||||
"title": "Baudrate (deprecated)",
|
||||
"requiresRestart": true,
|
||||
"description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600",
|
||||
"examples": [38400, 57600, 115200]
|
||||
},
|
||||
"rtscts": {
|
||||
"type": "boolean",
|
||||
"title": "RTS / CTS (deprecated)",
|
||||
"requiresRestart": true,
|
||||
"description": "RTS / CTS Hardware Flow Control for serial port"
|
||||
}
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"type": "object",
|
||||
"title": "Experimental (deprecated)",
|
||||
"properties": {
|
||||
"transmit_power": {
|
||||
"type": ["number", "null"],
|
||||
"title": "Transmit power",
|
||||
"requiresRestart": true,
|
||||
"description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"enum": ["attribute_and_json", "attribute", "json"],
|
||||
"title": "MQTT output type",
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"whitelist": {
|
||||
"readOnly": true,
|
||||
"type": "array",
|
||||
"requiresRestart": true,
|
||||
"title": "Whitelist (deprecated, use passlist)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ban": {
|
||||
"readOnly": true,
|
||||
"type": "array",
|
||||
"requiresRestart": true,
|
||||
"title": "Ban (deprecated, use blocklist)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["mqtt"],
|
||||
|
||||
+14
-63
@@ -8,28 +8,13 @@ import schemaJson from './settings.schema.json';
|
||||
import utils from './utils';
|
||||
import yaml, {YAMLFileException} from './yaml';
|
||||
|
||||
export let schema: KeyValue = schemaJson;
|
||||
|
||||
schema = {};
|
||||
objectAssignDeep(schema, schemaJson);
|
||||
|
||||
// Remove legacy settings from schema
|
||||
{
|
||||
delete schema.properties.advanced.properties.homeassistant_discovery_topic;
|
||||
delete schema.properties.advanced.properties.homeassistant_status_topic;
|
||||
delete schema.properties.advanced.properties.baudrate;
|
||||
delete schema.properties.advanced.properties.rtscts;
|
||||
delete schema.properties.experimental;
|
||||
delete (schemaJson as KeyValue).properties.whitelist;
|
||||
delete (schemaJson as KeyValue).properties.ban;
|
||||
}
|
||||
|
||||
export {schemaJson};
|
||||
export const CURRENT_VERSION = 2;
|
||||
/** NOTE: by order of priority, lower index is lower level (more important) */
|
||||
export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const;
|
||||
export type LogLevel = (typeof LOG_LEVELS)[number];
|
||||
|
||||
// DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697
|
||||
const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml');
|
||||
const CONFIG_FILE_PATH = data.joinPath('configuration.yaml');
|
||||
const NULLABLE_SETTINGS = ['homeassistant'];
|
||||
const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson);
|
||||
const ajvRestartRequired = new Ajv({allErrors: true}).addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson);
|
||||
@@ -114,6 +99,7 @@ function loadSettingsWithDefaults(): void {
|
||||
if (!_settings) {
|
||||
_settings = read();
|
||||
}
|
||||
|
||||
_settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings;
|
||||
|
||||
if (!_settingsWithDefaults.devices) {
|
||||
@@ -131,6 +117,7 @@ function loadSettingsWithDefaults(): void {
|
||||
experimental_event_entities: false,
|
||||
};
|
||||
const sLegacy = {};
|
||||
|
||||
if (_settingsWithDefaults.advanced) {
|
||||
for (const key of ['homeassistant_discovery_topic', 'homeassistant_status_topic']) {
|
||||
// @ts-expect-error ignore typing
|
||||
@@ -144,6 +131,7 @@ function loadSettingsWithDefaults(): void {
|
||||
const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {};
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.homeassistant = {};
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s);
|
||||
}
|
||||
@@ -153,6 +141,7 @@ function loadSettingsWithDefaults(): void {
|
||||
const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {};
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.availability = {};
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
objectAssignDeep(_settingsWithDefaults.availability, defaults, s);
|
||||
}
|
||||
@@ -162,49 +151,10 @@ function loadSettingsWithDefaults(): void {
|
||||
const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {};
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.frontend = {};
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
objectAssignDeep(_settingsWithDefaults.frontend, defaults, s);
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settings.advanced?.baudrate !== undefined && _settings.serial?.baudrate == null) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.serial.baudrate = _settings.advanced.baudrate;
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settings.advanced?.rtscts !== undefined && _settings.serial?.rtscts == null) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts;
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settings.experimental?.transmit_power !== undefined && _settings.advanced?.transmit_power == null) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.advanced.transmit_power = _settings.experimental.transmit_power;
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settings.experimental?.output !== undefined && _settings.advanced?.output == null) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.advanced.output = _settings.experimental.output;
|
||||
}
|
||||
|
||||
if (_settings.advanced?.log_level === 'warn') {
|
||||
_settingsWithDefaults.advanced.log_level = 'warning';
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settingsWithDefaults.ban) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban);
|
||||
}
|
||||
|
||||
// @ts-expect-error ignore typing
|
||||
if (_settingsWithDefaults.whitelist) {
|
||||
// @ts-expect-error ignore typing
|
||||
_settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
|
||||
}
|
||||
}
|
||||
|
||||
function parseValueRef(text: string): {filename: string; key: string} | null {
|
||||
@@ -226,7 +176,7 @@ function write(): void {
|
||||
const toWrite: KeyValue = objectAssignDeep({}, settings);
|
||||
|
||||
// Read settings to check if we have to split devices/groups into separate file.
|
||||
const actual = yaml.read(file);
|
||||
const actual = yaml.read(CONFIG_FILE_PATH);
|
||||
|
||||
// In case the setting is defined in a separate file (e.g. !secret network_key) update it there.
|
||||
for (const path of [
|
||||
@@ -268,10 +218,10 @@ function write(): void {
|
||||
|
||||
writeDevicesOrGroups('devices');
|
||||
writeDevicesOrGroups('groups');
|
||||
|
||||
yaml.writeIfChanged(file, toWrite);
|
||||
yaml.writeIfChanged(CONFIG_FILE_PATH, toWrite);
|
||||
|
||||
_settings = read();
|
||||
|
||||
loadSettingsWithDefaults();
|
||||
}
|
||||
|
||||
@@ -332,6 +282,7 @@ export function validate(): string[] {
|
||||
};
|
||||
|
||||
const settingsWithDefaults = get();
|
||||
|
||||
Object.values(settingsWithDefaults.devices).forEach((d) => check(d));
|
||||
Object.values(settingsWithDefaults.groups).forEach((g) => check(g));
|
||||
|
||||
@@ -347,7 +298,7 @@ export function validate(): string[] {
|
||||
}
|
||||
|
||||
function read(): Settings {
|
||||
const s = yaml.read(file) as Settings;
|
||||
const s = yaml.read(CONFIG_FILE_PATH) as Settings;
|
||||
applyEnvironmentVariables(s);
|
||||
|
||||
// Read !secret MQTT username and password if set
|
||||
@@ -458,7 +409,7 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
|
||||
iterate(schemaJson.properties, []);
|
||||
}
|
||||
|
||||
function getInternalSettings(): Partial<Settings> {
|
||||
export function getInternalSettings(): Partial<Settings> {
|
||||
if (!_settings) {
|
||||
_settings = read();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
import {copyFileSync, writeFileSync} from 'fs';
|
||||
|
||||
import data from './data';
|
||||
import * as settings from './settings';
|
||||
|
||||
interface SettingsMigration {
|
||||
path: string[];
|
||||
note: string;
|
||||
noteIf?: (previousValue: unknown) => boolean;
|
||||
}
|
||||
|
||||
interface SettingsAdd extends Omit<SettingsMigration, 'noteIf'> {
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
type SettingsRemove = SettingsMigration;
|
||||
|
||||
interface SettingsChange extends SettingsMigration {
|
||||
previousValueAnyOf?: unknown[];
|
||||
newValue: unknown;
|
||||
}
|
||||
|
||||
interface SettingsTransfer extends SettingsMigration {
|
||||
newPath: string[];
|
||||
}
|
||||
|
||||
const SUPPORTED_VERSIONS: Settings['version'][] = [undefined, settings.CURRENT_VERSION];
|
||||
|
||||
function backupSettings(version: number): void {
|
||||
const filePath = data.joinPath('configuration.yaml');
|
||||
|
||||
copyFileSync(filePath, filePath.replace('.yaml', `_backup_v${version}.yaml`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given path in given settings to given value. If requested, create path.
|
||||
*
|
||||
* @param currentSettings
|
||||
* @param path
|
||||
* @param value
|
||||
* @param createPathIfNotExist
|
||||
* @returns Returns true if value was set, false if not.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setValue(currentSettings: any, path: string[], value: unknown, createPathIfNotExist: boolean = false): boolean {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
|
||||
if (i === path.length - 1) {
|
||||
currentSettings[key] = value;
|
||||
} else {
|
||||
if (!currentSettings[key]) {
|
||||
/* istanbul ignore else */
|
||||
if (createPathIfNotExist) {
|
||||
currentSettings[key] = {};
|
||||
} else {
|
||||
// invalid path
|
||||
// ignored in test since currently call is always guarded by get-validated path, so this is never reached
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
currentSettings = currentSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the given path in given settings.
|
||||
*
|
||||
* @param currentSettings
|
||||
* @param path
|
||||
* @returns
|
||||
* - true if path was valid
|
||||
* - the value at path
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getValue(currentSettings: any, path: string[]): [validPath: boolean, value: unknown] {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
|
||||
if (i === path.length - 1) {
|
||||
return [true, currentSettings[key]];
|
||||
} else {
|
||||
if (!currentSettings[key]) {
|
||||
// invalid path
|
||||
break;
|
||||
}
|
||||
|
||||
currentSettings = currentSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
return [false, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a value at given path, path is created as needed.
|
||||
* @param currentSettings
|
||||
* @param addition
|
||||
*/
|
||||
function addValue(currentSettings: Partial<Settings>, addition: SettingsAdd): void {
|
||||
setValue(currentSettings, addition.path, addition.value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove value at given path, if path is valid.
|
||||
* Value is actually set to undefined, which triggers removal when `settings.apply` is called.
|
||||
* @param currentSettings
|
||||
* @param removal
|
||||
* @returns
|
||||
*/
|
||||
function removeValue(currentSettings: Partial<Settings>, removal: SettingsRemove): [validPath: boolean, previousValue: unknown] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, removal.path);
|
||||
|
||||
if (validPath && previousValue != undefined) {
|
||||
setValue(currentSettings, removal.path, undefined);
|
||||
}
|
||||
|
||||
return [validPath, previousValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change value at given path, if path is valid, and value matched one of the defined values (if any).
|
||||
* @param currentSettings
|
||||
* @param change
|
||||
* @returns
|
||||
*/
|
||||
function changeValue(currentSettings: Partial<Settings>, change: SettingsChange): [validPath: boolean, previousValue: unknown, changed: boolean] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, change.path);
|
||||
let changed: boolean = false;
|
||||
|
||||
if (validPath && previousValue !== change.newValue) {
|
||||
if (!change.previousValueAnyOf || change.previousValueAnyOf.includes(previousValue)) {
|
||||
setValue(currentSettings, change.path, change.newValue);
|
||||
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [validPath, previousValue, changed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer value at given path, to new path.
|
||||
* Given path must be valid.
|
||||
* New path must not be valid or new path value must be nullish, otherwise given path is removed only.
|
||||
* Value at given path is actually set to undefined, which triggers removal when `settings.apply` is called.
|
||||
* New path is created as needed.
|
||||
* @param currentSettings
|
||||
* @param transfer
|
||||
* @returns
|
||||
*/
|
||||
function transferValue(
|
||||
currentSettings: Partial<Settings>,
|
||||
transfer: SettingsTransfer,
|
||||
): [validPath: boolean, previousValue: unknown, transfered: boolean] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, transfer.path);
|
||||
const [destValidPath, destValue] = getValue(currentSettings, transfer.newPath);
|
||||
const transfered = validPath && previousValue != undefined && (!destValidPath || destValue == undefined || Array.isArray(destValue));
|
||||
|
||||
// no point in set if already undefined
|
||||
if (validPath && previousValue != undefined) {
|
||||
setValue(currentSettings, transfer.path, undefined);
|
||||
}
|
||||
|
||||
if (transfered) {
|
||||
if (Array.isArray(previousValue) && Array.isArray(destValue)) {
|
||||
setValue(currentSettings, transfer.newPath, [...previousValue, ...destValue], true);
|
||||
} else {
|
||||
setValue(currentSettings, transfer.newPath, previousValue, true);
|
||||
}
|
||||
}
|
||||
|
||||
return [validPath, previousValue, transfered];
|
||||
}
|
||||
|
||||
const noteIfWasTrue = (previousValue: unknown): boolean => previousValue === true;
|
||||
const noteIfWasDefined = (previousValue: unknown): boolean => previousValue != undefined;
|
||||
const noteIfWasNonEmptyArray = (previousValue: unknown): boolean => Array.isArray(previousValue) && previousValue.length > 0;
|
||||
|
||||
function migrateToTwo(
|
||||
currentSettings: Partial<Settings>,
|
||||
transfers: SettingsTransfer[],
|
||||
changes: SettingsChange[],
|
||||
additions: SettingsAdd[],
|
||||
removals: SettingsRemove[],
|
||||
): void {
|
||||
transfers.push(
|
||||
{
|
||||
path: ['advanced', 'homeassistant_discovery_topic'],
|
||||
note: `HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['homeassistant', 'discovery_topic'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'homeassistant_status_topic'],
|
||||
note: `HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['homeassistant', 'status_topic'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'baudrate'],
|
||||
note: `Baudrate was moved from advanced.baudrate to serial.baudrate.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['serial', 'baudrate'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'rtscts'],
|
||||
note: `RTSCTS was moved from advanced.rtscts to serial.rtscts.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['serial', 'rtscts'],
|
||||
},
|
||||
{
|
||||
path: ['experimental', 'transmit_power'],
|
||||
note: `Transmit power was moved from experimental.transmit_power to advanced.transmit_power.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['advanced', 'transmit_power'],
|
||||
},
|
||||
{
|
||||
path: ['experimental', 'output'],
|
||||
note: `Output was moved from experimental.output to advanced.output.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['advanced', 'output'],
|
||||
},
|
||||
{
|
||||
path: ['ban'],
|
||||
note: `ban was renamed to passlist.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['blocklist'],
|
||||
},
|
||||
{
|
||||
path: ['whitelist'],
|
||||
note: `whitelist was renamed to passlist.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['passlist'],
|
||||
},
|
||||
);
|
||||
|
||||
changes.push({
|
||||
path: ['advanced', 'log_level'],
|
||||
note: `Log level 'warn' has been renamed to 'warning'.`,
|
||||
noteIf: (previousValue): boolean => previousValue === 'warn',
|
||||
previousValueAnyOf: ['warn'],
|
||||
newValue: 'warning',
|
||||
});
|
||||
|
||||
additions.push({
|
||||
path: ['version'],
|
||||
note: `Migrated settings to version 2`,
|
||||
value: 2,
|
||||
});
|
||||
|
||||
const haLegacyTriggers: SettingsRemove = {
|
||||
path: ['homeassistant', 'legacy_triggers'],
|
||||
note: `Action and click sensors have been removed (homeassistant.legacy_triggers setting). This means all sensor.*_action and sensor.*_click entities are removed. Use the MQTT device trigger instead.`,
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
const haLegacyEntityAttrs: SettingsRemove = {
|
||||
path: ['homeassistant', 'legacy_entity_attributes'],
|
||||
note: `Entity attributes (homeassistant.legacy_entity_attributes setting) has been removed. This means that entities discovered by Zigbee2MQTT will no longer have entity attributes (Home Assistant entity attributes are accessed via e.g. states.binary_sensor.my_sensor.attributes).`,
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
const otaIkeaUseTestUrl: SettingsRemove = {
|
||||
path: ['ota', 'ikea_ota_use_test_url'],
|
||||
note: `Due to the OTA rework, the ota.ikea_ota_use_test_url option has been removed.`,
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
|
||||
removals.push(
|
||||
haLegacyTriggers,
|
||||
haLegacyEntityAttrs,
|
||||
{
|
||||
path: ['advanced', 'homeassistant_legacy_triggers'],
|
||||
note: haLegacyTriggers.note,
|
||||
noteIf: haLegacyTriggers.noteIf,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'homeassistant_legacy_entity_attributes'],
|
||||
note: haLegacyEntityAttrs.note,
|
||||
noteIf: haLegacyEntityAttrs.noteIf,
|
||||
},
|
||||
{
|
||||
path: ['permit_join'],
|
||||
note: `The permit_join setting has been removed, use the frontend or MQTT to permit joining.`,
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
otaIkeaUseTestUrl,
|
||||
{
|
||||
path: ['advanced', 'ikea_ota_use_test_url'],
|
||||
note: otaIkeaUseTestUrl.note,
|
||||
noteIf: otaIkeaUseTestUrl.noteIf,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'legacy_api'],
|
||||
note: `The MQTT legacy API has been removed (advanced.legacy_api setting). See link below for affected topics.`,
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'legacy_availability_payload'],
|
||||
note: `Due to the removal of advanced.legacy_availability_payload, zigbee2mqtt/bridge/state will now always be a JSON object ({"state":"online"} or {"state":"offline"})`,
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'soft_reset_timeout'],
|
||||
note: `Removed deprecated: Soft reset feature (advanced.soft_reset_timeout setting)`,
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'report'],
|
||||
note: `Removed deprecated: Report feature (advanced.report setting)`,
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_timeout'],
|
||||
note: `Removed deprecated: advanced.availability_timeout availability settings`,
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_blocklist'],
|
||||
note: `Removed deprecated: advanced.availability_blocklist availability settings`,
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_passlist'],
|
||||
note: `Removed deprecated: advanced.availability_passlist availability settings`,
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_blacklist'],
|
||||
note: `Removed deprecated: advanced.availability_blacklist availability settings`,
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_whitelist'],
|
||||
note: `Removed deprecated: advanced.availability_whitelist availability settings`,
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['device_options', 'legacy'],
|
||||
note: `Removed everything that was enabled through device_options.legacy. See link below for affected devices.`,
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['experimental'],
|
||||
note: `The entire experimental section was removed.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['external_converters'],
|
||||
note: `External converters are now automatically loaded from the 'data/external_converters' directory without requiring settings to be set. Make sure your external converters are still needed (might be supported out-of-the-box now), and if so, move them to that directory.`,
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
);
|
||||
|
||||
// note only once
|
||||
const noteEntityOptionsRetrieveState = `Retrieve state option ((devices|groups).xyz.retrieve_state setting)`;
|
||||
|
||||
for (const deviceKey in currentSettings.devices) {
|
||||
removals.push({
|
||||
path: ['devices', deviceKey, 'retrieve_state'],
|
||||
note: noteEntityOptionsRetrieveState,
|
||||
noteIf: noteIfWasTrue,
|
||||
});
|
||||
}
|
||||
|
||||
for (const groupKey in currentSettings.groups) {
|
||||
removals.push({
|
||||
path: ['groups', groupKey, 'retrieve_state'],
|
||||
note: noteEntityOptionsRetrieveState,
|
||||
noteIf: noteIfWasTrue,
|
||||
});
|
||||
removals.push({
|
||||
path: ['groups', groupKey, 'devices'],
|
||||
note: `Removed configuring group members through configuration.yaml (groups.xyz.devices setting). This will not impact current group members; however, you will no longer be able to add or remove devices from a group through the configuration.yaml.`,
|
||||
noteIf: noteIfWasDefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Future
|
||||
// function migrateFromTwoToThree(currentSettings: Settings, transfers: SettingsTransfer[], changes: SettingsChange[], additions: SettingsAdd[], removals: SettingsRemove[]): void {
|
||||
// transfers.push();
|
||||
// changes.push(
|
||||
// {
|
||||
// path: ['version'],
|
||||
// note: `Migrated settings to version 3`,
|
||||
// newValue: 3,
|
||||
// }
|
||||
// );
|
||||
// additions.push();
|
||||
// removals.push();
|
||||
// }
|
||||
|
||||
/**
|
||||
* Order of execution:
|
||||
* - Transfer
|
||||
* - Change
|
||||
* - Add
|
||||
* - Remove
|
||||
*
|
||||
* Should allow the most flexibility whenever combination of migrations is necessary (e.g. Transfer + Change)
|
||||
*/
|
||||
export function migrateIfNecessary(): void {
|
||||
const currentSettings = settings.getInternalSettings();
|
||||
|
||||
if (!SUPPORTED_VERSIONS.includes(currentSettings.version)) {
|
||||
throw new Error(
|
||||
`Your configuration.yaml has an unsupported version ${currentSettings.version}, expected one of ${SUPPORTED_VERSIONS.map((v) => String(v)).join(',')}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// when same version as current, nothing left to do
|
||||
while (currentSettings.version !== settings.CURRENT_VERSION) {
|
||||
let migrationNotesFileName: string | undefined;
|
||||
// don't duplicate outputs
|
||||
const migrationNotes: Set<string> = new Set();
|
||||
const transfers: SettingsTransfer[] = [];
|
||||
const changes: SettingsChange[] = [];
|
||||
const additions: SettingsAdd[] = [];
|
||||
const removals: SettingsRemove[] = [];
|
||||
|
||||
backupSettings(currentSettings.version || 1);
|
||||
|
||||
// each version should only bump to the next version so as to gradually migrate if necessary
|
||||
/* istanbul ignore else */
|
||||
if (currentSettings.version == undefined) {
|
||||
// migrating from 1.x.x (`version` did not exist) to 2.0.0
|
||||
migrationNotesFileName = 'migration-1.x.x-to-2.0.0.log';
|
||||
|
||||
migrateToTwo(currentSettings, transfers, changes, additions, removals);
|
||||
} /* else if (currentSettings.version === 2) {
|
||||
// Future
|
||||
// migrating from 2.x.x to 3.0.0
|
||||
migrationNotesFileName = 'migration-2.x.x-to-3.0.0.log';
|
||||
|
||||
migrateFromTwoToThree(currentSettings, transfers, changes, additions, removals);
|
||||
}*/
|
||||
|
||||
for (const transfer of transfers) {
|
||||
const [validPath, previousValue, transfered] = transferValue(currentSettings, transfer);
|
||||
|
||||
if (validPath && (!transfer.noteIf || transfer.noteIf(previousValue))) {
|
||||
migrationNotes.add(`[${transfered ? 'TRANSFER' : 'REMOVAL'}] ${transfer.note}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
const [validPath, previousValue, changed] = changeValue(currentSettings, change);
|
||||
|
||||
if (validPath && changed && (!change.noteIf || change.noteIf(previousValue))) {
|
||||
migrationNotes.add(`[CHANGE] ${change.note}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const addition of additions) {
|
||||
addValue(currentSettings, addition);
|
||||
|
||||
migrationNotes.add(`[ADDITION] ${addition.note}`);
|
||||
}
|
||||
|
||||
for (const removal of removals) {
|
||||
const [validPath, previousValue] = removeValue(currentSettings, removal);
|
||||
|
||||
if (validPath && (!removal.noteIf || removal.noteIf(previousValue))) {
|
||||
migrationNotes.add(`[REMOVAL] ${removal.note}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (migrationNotesFileName && migrationNotes.size > 0) {
|
||||
migrationNotes.add(`For more details, see https://github.com/Koenkk/zigbee2mqtt/discussions/24198`);
|
||||
const migrationNotesFilePath = data.joinPath(migrationNotesFileName);
|
||||
|
||||
writeFileSync(migrationNotesFilePath, Array.from(migrationNotes).join(`\r\n\r\n`), 'utf8');
|
||||
|
||||
console.log(`Migration notes written in ${migrationNotesFilePath}`);
|
||||
}
|
||||
|
||||
settings.apply(currentSettings as unknown as Record<string, unknown>);
|
||||
settings.reRead();
|
||||
}
|
||||
}
|
||||
@@ -636,7 +636,7 @@ describe('Controller', () => {
|
||||
|
||||
it('Publish entity state attribute output', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute');
|
||||
settings.set(['advanced', 'output'], 'attribute');
|
||||
mockMQTT.publishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity('bulb')!;
|
||||
@@ -662,7 +662,7 @@ describe('Controller', () => {
|
||||
|
||||
it('Publish entity state attribute_json output', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute_and_json');
|
||||
settings.set(['advanced', 'output'], 'attribute_and_json');
|
||||
mockMQTT.publishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity('bulb')!;
|
||||
@@ -682,7 +682,7 @@ describe('Controller', () => {
|
||||
|
||||
it('Publish entity state attribute_json output filtered', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute_and_json');
|
||||
settings.set(['advanced', 'output'], 'attribute_and_json');
|
||||
settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']);
|
||||
mockMQTT.publishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
@@ -697,7 +697,7 @@ describe('Controller', () => {
|
||||
|
||||
it('Publish entity state attribute_json output filtered (device_options)', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute_and_json');
|
||||
settings.set(['advanced', 'output'], 'attribute_and_json');
|
||||
settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']);
|
||||
mockMQTT.publishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('Extension: Bridge', () => {
|
||||
passlist: [],
|
||||
serial: {disable_led: false, port: '/dev/dummy'},
|
||||
},
|
||||
config_schema: settings.schema,
|
||||
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: 0x001122, pan_id: 5674},
|
||||
|
||||
@@ -1033,7 +1033,7 @@ describe('Extension: HomeAssistant', () => {
|
||||
});
|
||||
|
||||
it('Should throw error when starting with attributes output', async () => {
|
||||
settings.set(['experimental', 'output'], 'attribute');
|
||||
settings.set(['advanced', 'output'], 'attribute');
|
||||
settings.set(['homeassistant'], true);
|
||||
expect(() => {
|
||||
new Controller(jest.fn(), jest.fn());
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ import yaml from '../../lib/util/yaml';
|
||||
export const mockDir: string = tmp.dirSync().name;
|
||||
const stateFile = path.join(mockDir, 'state.json');
|
||||
|
||||
export function writeDefaultConfiguration(): void {
|
||||
const config = {
|
||||
export function writeDefaultConfiguration(config: unknown = undefined): void {
|
||||
config = config || {
|
||||
homeassistant: false,
|
||||
mqtt: {
|
||||
base_topic: 'zigbee2mqtt',
|
||||
|
||||
+3
-59
@@ -1,3 +1,6 @@
|
||||
// side-effect ensures using mock paths
|
||||
import './mocks/data';
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
@@ -908,63 +911,4 @@ describe('Settings', () => {
|
||||
settings.reRead();
|
||||
expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: null, base_url: '/'});
|
||||
});
|
||||
|
||||
it('Baudrate config', () => {
|
||||
write(configurationFile, {...minimalConfig, advanced: {baudrate: 20}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().serial.baudrate).toStrictEqual(20);
|
||||
});
|
||||
|
||||
it('transmit_power config', () => {
|
||||
write(configurationFile, {...minimalConfig, experimental: {transmit_power: 1337}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().advanced.transmit_power).toStrictEqual(1337);
|
||||
});
|
||||
|
||||
it('output config', () => {
|
||||
write(configurationFile, {...minimalConfig, experimental: {output: 'json'}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().advanced.output).toStrictEqual('json');
|
||||
});
|
||||
|
||||
it('Baudrartsctste config', () => {
|
||||
write(configurationFile, {...minimalConfig, advanced: {rtscts: true}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().serial.rtscts).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('Deprecated: Home Assistant config', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
homeassistant: {discovery_topic: 'new'},
|
||||
advanced: {homeassistant_discovery_topic: 'old', homeassistant_status_topic: 'olds'},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().homeassistant).toStrictEqual({
|
||||
discovery_topic: 'new',
|
||||
experimental_event_entities: false,
|
||||
status_topic: 'olds',
|
||||
});
|
||||
});
|
||||
|
||||
it('Deprecated: ban/whitelist config', () => {
|
||||
write(configurationFile, {...minimalConfig, ban: ['ban'], whitelist: ['whitelist'], passlist: ['passlist'], blocklist: ['blocklist']});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban']);
|
||||
expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist']);
|
||||
});
|
||||
|
||||
it('Deprecated: warn log level', () => {
|
||||
write(configurationFile, {...minimalConfig, advanced: {log_level: 'warn'}});
|
||||
|
||||
settings.reRead();
|
||||
|
||||
expect(settings.get().advanced.log_level).toStrictEqual('warning');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,710 @@
|
||||
// side-effect ensures using mock paths
|
||||
import * as data from './mocks/data';
|
||||
|
||||
import {existsSync, readFileSync, rmSync} from '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';
|
||||
|
||||
describe('Settings Migration', () => {
|
||||
beforeAll(() => {});
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(data.mockDir, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// always validate after each test
|
||||
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.x.x to v2.0.0', () => {
|
||||
const DEFAULT_CONFIG_V2 = {
|
||||
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(() => {
|
||||
data.writeDefaultConfiguration(DEFAULT_CONFIG_V2);
|
||||
settings.reRead();
|
||||
});
|
||||
|
||||
it('no change needed - only add version', () => {
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
afterSettings.version = 2;
|
||||
|
||||
settingsMigration.migrateIfNecessary();
|
||||
|
||||
const migratedSettings = settings.getInternalSettings();
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
});
|
||||
|
||||
it('remove all', () => {
|
||||
// @ts-expect-error workaround
|
||||
const beforeSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
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']);
|
||||
|
||||
// console.log(JSON.stringify(settings.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).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.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
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.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
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.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).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.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
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.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
afterSettings.version = 2;
|
||||
afterSettings.advanced = {log_level: 'warning'};
|
||||
|
||||
settings.set(['advanced', 'log_level'], 'warn');
|
||||
|
||||
// console.log(JSON.stringify(settings.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).toStrictEqual(
|
||||
// @ts-expect-error workaround
|
||||
objectAssignDeep.noMutate(beforeSettings, {
|
||||
advanced: {
|
||||
log_level: 'warn',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsMigration.migrateIfNecessary();
|
||||
|
||||
const migratedSettings = settings.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
expect(existsSync(mockedData.joinPath('configuration_backup_v1.yaml'))).toStrictEqual(true);
|
||||
const migrationNotes = mockedData.joinPath('migration-1.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
afterSettings.version = 2;
|
||||
afterSettings.advanced = {log_level: 'warning'};
|
||||
|
||||
settings.set(['advanced', 'log_level'], 'warning');
|
||||
|
||||
// console.log(JSON.stringify(settings.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).toStrictEqual(
|
||||
// @ts-expect-error workaround
|
||||
objectAssignDeep.noMutate(beforeSettings, {
|
||||
advanced: {
|
||||
log_level: 'warning',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsMigration.migrateIfNecessary();
|
||||
|
||||
const migratedSettings = settings.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
expect(existsSync(mockedData.joinPath('configuration_backup_v1.yaml'))).toStrictEqual(true);
|
||||
const migrationNotes = mockedData.joinPath('migration-1.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
afterSettings.version = 2;
|
||||
afterSettings.advanced = {log_level: 'info'};
|
||||
|
||||
settings.set(['advanced', 'log_level'], 'info');
|
||||
|
||||
// console.log(JSON.stringify(settings.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).toStrictEqual(
|
||||
// @ts-expect-error workaround
|
||||
objectAssignDeep.noMutate(beforeSettings, {
|
||||
advanced: {
|
||||
log_level: 'info',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
settingsMigration.migrateIfNecessary();
|
||||
|
||||
const migratedSettings = settings.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
expect(existsSync(mockedData.joinPath('configuration_backup_v1.yaml'))).toStrictEqual(true);
|
||||
const migrationNotes = mockedData.joinPath('migration-1.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
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.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).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.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
expect(existsSync(mockedData.joinPath('configuration_backup_v1.yaml'))).toStrictEqual(true);
|
||||
const migrationNotes = mockedData.joinPath('migration-1.x.x-to-2.0.0.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.getInternalSettings());
|
||||
// @ts-expect-error workaround
|
||||
const afterSettings = objectAssignDeep.noMutate({}, settings.getInternalSettings());
|
||||
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.getInternalSettings(), undefined, 2));
|
||||
|
||||
expect(settings.getInternalSettings()).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.getInternalSettings();
|
||||
// console.log(JSON.stringify(migratedSettings, undefined, 2));
|
||||
|
||||
expect(migratedSettings).toStrictEqual(afterSettings);
|
||||
expect(existsSync(mockedData.joinPath('configuration_backup_v1.yaml'))).toStrictEqual(true);
|
||||
const migrationNotes = mockedData.joinPath('migration-1.x.x-to-2.0.0.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.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user