From 30177b0db403ae5998013c603da96aad8f59ad9a Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 9 Jan 2022 21:28:44 +0000 Subject: [PATCH] Restructure settings (#10437) * - * deep copy schema * - * - * - * - * - * - * - * - * - * - * - * - --- lib/controller.ts | 12 +- lib/extension/availability.ts | 16 +- lib/extension/bridge.ts | 8 +- lib/extension/frontend.ts | 6 +- lib/extension/groups.ts | 2 +- lib/extension/homeassistant.ts | 38 +- lib/extension/legacy/bridgeLegacy.ts | 4 +- lib/extension/onEvent.ts | 2 +- lib/extension/otaUpdate.ts | 2 +- lib/extension/publish.ts | 4 +- lib/extension/receive.ts | 8 +- lib/model/device.ts | 6 +- lib/model/group.ts | 4 +- lib/types/types.d.ts | 77 ++- lib/util/settings.schema.json | 810 +++++++++++++++------------ lib/util/settings.ts | 269 ++++----- lib/util/utils.ts | 4 +- lib/zigbee.ts | 16 +- test/bridge.test.js | 16 +- test/controller.test.js | 25 +- test/homeassistant.test.js | 3 +- test/legacy/bridgeLegacy.test.js | 22 +- test/otaUpdate.test.js | 2 +- test/settings.test.js | 87 ++- 24 files changed, 792 insertions(+), 651 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index a88eb16f3..8b6e9abff 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -222,11 +222,11 @@ class Controller { } const options: MQTTOptions = { - retain: utils.getObjectProperty(entity.settings, 'retain', false) as boolean, - qos: utils.getObjectProperty(entity.settings, 'qos', 0) as 0 | 1 | 2, + retain: utils.getObjectProperty(entity.options, 'retain', false) as boolean, + qos: utils.getObjectProperty(entity.options, 'qos', 0) as 0 | 1 | 2, }; - const retention = utils.getObjectProperty(entity.settings, 'retention', false); + const retention = utils.getObjectProperty(entity.options, 'retention', false); if (retention !== false) { options.properties = {messageExpiryInterval: retention as number}; } @@ -259,12 +259,12 @@ class Controller { } // filter mqtt message attributes - if (entity.settings.filtered_attributes) { - entity.settings.filtered_attributes.forEach((a) => delete message[a]); + if (entity.options.filtered_attributes) { + entity.options.filtered_attributes.forEach((a) => delete message[a]); } if (Object.entries(message).length) { - const output = settings.get().experimental.output; + const output = settings.get().advanced.output; if (output === 'attribute_and_json' || output === 'json') { await this.mqtt.publish(entity.name, stringify(message), options); } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index 5519c59fe..040a3b6c0 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -13,17 +13,13 @@ export default class Availability extends Extension { private pingQueueExecuting = false; private getTimeout(device: Device): number { - if (typeof device.settings.availability === 'object' && device.settings.availability?.timeout != null) { - return utils.minutes(device.settings.availability.timeout); + if (typeof device.options.availability === 'object' && device.options.availability?.timeout != null) { + return utils.minutes(device.options.availability.timeout); } const key = this.isActiveDevice(device) ? 'active' : 'passive'; - const availabilitySettings = settings.get().availability; - if (typeof availabilitySettings === 'object' && availabilitySettings[key]?.timeout != null) { - return utils.minutes(availabilitySettings[key]?.timeout); - } - - return key === 'active' ? utils.minutes(10) : utils.hours(25); + const value = settings.get().availability[key]?.timeout; + return key === 'active' ? utils.minutes(value) : utils.hours(value); } private isActiveDevice(device: Device): boolean { @@ -96,7 +92,9 @@ export default class Availability extends Extension { override async start(): Promise { this.eventBus.onEntityRenamed(this, (data) => - data.entity.isDevice() && this.publishAvailability(data.entity, false, true)); + data.entity.isDevice() && + utils.isAvailabilityEnabledForDevice(data.entity, settings.get()) && + this.publishAvailability(data.entity, false, true)); this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device)); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index d0189f3d3..2bbdd4a1d 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -392,9 +392,9 @@ export default class Bridge extends Extension { const ID = message.id; const entity = this.getEntity(entityType, ID); - const oldOptions = objectAssignDeep({}, cleanup(entity.settings)); + const oldOptions = objectAssignDeep({}, cleanup(entity.options)); settings.changeEntityOptions(ID, message.options); - const newOptions = cleanup(entity.settings); + const newOptions = cleanup(entity.options); await this.publishInfo(); logger.info(`Changed config for ${entityType} ${ID}`); @@ -448,7 +448,7 @@ export default class Bridge extends Extension { const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ? message.homeassistant_rename : false; const entity = this.getEntity(entityType, from); - const oldFriendlyName = entity.settings.friendly_name; + const oldFriendlyName = entity.options.friendly_name; settings.changeFriendlyName(from, to); @@ -683,7 +683,7 @@ export default class Bridge extends Extension { getDefinitionPayload(device: Device): DefinitionPayload { if (!device.definition) return null; - let icon = device.settings.icon ? device.settings.icon : device.definition.icon; + let icon = device.options.icon ? device.options.icon : device.definition.icon; if (icon) { icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID)); icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model)); diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 341aa3770..bb74637fa 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -17,9 +17,9 @@ import bind from 'bind-decorator'; */ export default class Frontend extends Extension { private mqttBaseTopic = settings.get().mqtt.base_topic; - private host = settings.get().frontend.host || '0.0.0.0'; - private port = settings.get().frontend.port || 8080; - private authToken = settings.get().frontend.auth_token || false; + private host = settings.get().frontend.host; + private port = settings.get().frontend.port; + private authToken = settings.get().frontend.auth_token; private retainedMessages = new Map(); private server: http.Server; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index 15ac30c29..d2ebe1183 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -123,7 +123,7 @@ export default class Groups extends Extension { if (Object.keys(payload).length) { const entity = data.entity; const groups = this.zigbee.groups().filter((g) => { - return g.settings && (!g.settings.hasOwnProperty('optimistic') || g.settings.optimistic); + return g.options && (!g.options.hasOwnProperty('optimistic') || g.options.optimistic); }); if (entity instanceof Device) { diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index bef32f6d8..44bf09fb0 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -65,16 +65,16 @@ export default class HomeAssistant extends Extension { private discovered: {[s: string]: {topics: Set, mockProperties: Set, objectIDs: Set}} = {}; private discoveredTriggers : {[s: string]: Set}= {}; - private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic; - private statusTopic = settings.get().advanced.homeassistant_status_topic; - private entityAttributes = settings.get().advanced.homeassistant_legacy_entity_attributes; + private discoveryTopic = settings.get().homeassistant.discovery_topic; + private statusTopic = settings.get().homeassistant.status_topic; + private entityAttributes = settings.get().homeassistant.legacy_entity_attributes; private zigbee2MQTTVersion: string; constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState, eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise, restartCallback: () => void, addExtension: (extension: Extension) => Promise) { super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); - if (settings.get().experimental.output === 'attribute') { + if (settings.get().advanced.output === 'attribute') { throw new Error('Home Assistant integration is not possible with attribute output!'); } } @@ -835,7 +835,7 @@ export default class HomeAssistant extends Extension { * can use Home Assistant entities in automations. * https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347 */ - if (settings.get().advanced.homeassistant_legacy_triggers) { + if (settings.get().homeassistant.legacy_triggers) { const keys = ['action', 'click'].filter((k) => data.message[k]); for (const key of keys) { this.publishEntityState(data.entity, {[key]: ''}); @@ -978,19 +978,19 @@ export default class HomeAssistant extends Extension { configs.push(updateAvailableSensor); } - if (isDevice && entity.settings.hasOwnProperty('legacy') && !entity.settings.legacy) { + if (isDevice && entity.options.hasOwnProperty('legacy') && !entity.options.legacy) { configs = configs.filter((c) => c !== sensorClick); } - if (!settings.get().advanced.homeassistant_legacy_triggers) { + if (!settings.get().homeassistant.legacy_triggers) { configs = configs.filter((c) => c.object_id !== 'action' && c.object_id !== 'click'); } // deep clone of the config objects configs = JSON.parse(JSON.stringify(configs)); - if (entity.settings.homeassistant) { - const s = entity.settings.homeassistant; + if (entity.options.homeassistant) { + const s = entity.options.homeassistant; configs = configs.filter((config) => !s.hasOwnProperty(config.object_id) || s[config.object_id] != null); configs.forEach((config) => { const configOverride = s[config.object_id]; @@ -1016,7 +1016,7 @@ export default class HomeAssistant extends Extension { if (entity.isGroup()) { if (!discover || entity.zh.members.length === 0) return; } else if (!discover || !entity.definition || entity.zh.interviewing || - (entity.settings.hasOwnProperty('homeassistant') && !entity.settings.homeassistant)) { + (entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant)) { return; } @@ -1061,7 +1061,7 @@ export default class HomeAssistant extends Extension { } // Set unique_id - payload.unique_id = `${entity.settings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`; + payload.unique_id = `${entity.options.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`; // Attributes for device registry payload.device = this.getDevicePayload(entity); @@ -1179,7 +1179,7 @@ export default class HomeAssistant extends Extension { } // Override configuration with user settings. - if (entity.settings.hasOwnProperty('homeassistant')) { + if (entity.options.hasOwnProperty('homeassistant')) { const add = (obj: KeyValue): void => { Object.keys(obj).forEach((key) => { if (['type', 'object_id'].includes(key)) { @@ -1197,10 +1197,10 @@ export default class HomeAssistant extends Extension { }); }; - add(entity.settings.homeassistant); + add(entity.options.homeassistant); - if (entity.settings.homeassistant.hasOwnProperty(config.object_id)) { - add(entity.settings.homeassistant[config.object_id]); + if (entity.options.homeassistant.hasOwnProperty(config.object_id)) { + add(entity.options.homeassistant[config.object_id]); } } @@ -1260,7 +1260,7 @@ export default class HomeAssistant extends Extension { `${this.discoveryTopic}/${this.getDiscoveryTopic(c, entity)}` === data.topic); } // Device was flagged to be excluded from homeassistant discovery - clear = clear || (entity.settings.hasOwnProperty('homeassistant') && !entity.settings.homeassistant); + clear = clear || (entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant); if (clear) { logger.debug(`Clearing Home Assistant config '${data.topic}'`); @@ -1290,7 +1290,7 @@ export default class HomeAssistant extends Extension { const identifierPostfix = entity.isGroup() ? `zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt'; const payload: KeyValue = { - identifiers: [`${identifierPostfix}_${entity.settings.ID}`], + identifiers: [`${identifierPostfix}_${entity.options.ID}`], name: entity.name, sw_version: `Zigbee2MQTT ${this.zigbee2MQTTVersion}`, }; @@ -1339,8 +1339,8 @@ export default class HomeAssistant extends Extension { } private async publishDeviceTriggerDiscover(device: Device, key: string, value: string, force=false): Promise { - const haConfig = device.settings.homeassistant; - if (device.settings.hasOwnProperty('homeassistant') && (haConfig == null || + const haConfig = device.options.homeassistant; + if (device.options.hasOwnProperty('homeassistant') && (haConfig == null || (haConfig.hasOwnProperty('device_automation') && typeof haConfig === 'object' && haConfig.device_automation == null))) { return; diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index 0cf76124a..a937c49cb 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -51,7 +51,7 @@ export default class BridgeLegacy extends Extension { try { const entity = settings.getDevice(message); assert(entity, `Entity '${message}' does not exist`); - settings.whitelistDevice(entity.ID.toString()); + settings.addDeviceToPasslist(entity.ID.toString()); logger.info(`Whitelisted '${entity.friendly_name}'`); this.mqtt.publish( 'bridge/log', @@ -336,7 +336,7 @@ export default class BridgeLegacy extends Extension { } if (action === 'ban') { - settings.banDevice(ieeeAddr); + settings.blockDevice(ieeeAddr); } } diff --git a/lib/extension/onEvent.ts b/lib/extension/onEvent.ts index 9adf03719..a86083549 100644 --- a/lib/extension/onEvent.ts +++ b/lib/extension/onEvent.ts @@ -43,7 +43,7 @@ export default class OnEvent extends Extension { zhc.onEvent(type, data, device.zh); if (device.definition?.onEvent) { - await device.definition.onEvent(type, data, device.zh, device.settings); + await device.definition.onEvent(type, data, device.zh, device.options); } } } diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 3c9586c70..188d50e4c 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -39,7 +39,7 @@ export default class OTAUpdate extends Extension { override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); this.eventBus.onDeviceMessage(this, this.onZigbeeEvent); - if (settings.get().advanced.ikea_ota_use_test_url) { + if (settings.get().ota.ikea_ota_use_test_url) { tradfriOTA.useTestURL(); } diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index 9fa79e163..a9382418f 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -91,7 +91,7 @@ export default class Publish extends Extension { // Only do this when the retrieve_state option is enabled for this device. // retrieve_state == decprecated if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') && - re.settings.retrieve_state + re.options.retrieve_state ) { setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime); } @@ -134,7 +134,7 @@ export default class Publish extends Extension { return; } const device = re instanceof Device ? re.zh : null; - const entitySettings = re.settings; + const entitySettings = re.options; const entityState = this.state.get(re) || {}; const membersState = re instanceof Group ? Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr, diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index 80d9ef2a0..5b55afa8c 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -128,9 +128,9 @@ export default class Receive extends Extension { } // Check if we have to debounce - if (data.device.settings.debounce) { - this.publishDebounce(data.device, payload, data.device.settings.debounce, - data.device.settings.debounce_ignore); + if (data.device.options.debounce) { + this.publishDebounce(data.device, payload, data.device.options.debounce, + data.device.options.debounce_ignore); } else { this.publishEntityState(data.device, payload); } @@ -141,7 +141,7 @@ export default class Receive extends Extension { for (const converter of converters) { try { const converted = await converter.convert( - data.device.definition, data, publish, data.device.settings, meta); + data.device.definition, data, publish, data.device.options, meta); if (converted) { payload = {...payload, ...converted}; } diff --git a/lib/model/device.ts b/lib/model/device.ts index 8e7c8fbcd..84f40bed8 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -8,9 +8,9 @@ export default class Device { get ieeeAddr(): string {return this.zh.ieeeAddr;} get ID(): string {return this.zh.ieeeAddr;} - get settings(): DeviceSettings {return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)};} + get options(): DeviceOptions {return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)};} get name(): string { - return this.zh.type === 'Coordinator' ? 'Coordinator' : this.settings?.friendly_name || this.ieeeAddr; + return this.zh.type === 'Coordinator' ? 'Coordinator' : this.options?.friendly_name || this.ieeeAddr; } get definition(): zhc.Definition { if (!this._definition && !this.zh.interviewing) { @@ -26,7 +26,7 @@ export default class Device { exposes(): zhc.DefinitionExpose[] { /* istanbul ignore if */ if (typeof this.definition.exposes == 'function') { - return this.definition.exposes(this.zh, this.settings); + return this.definition.exposes(this.zh, this.options); } else { return this.definition.exposes; } diff --git a/lib/model/group.ts b/lib/model/group.ts index 5fdef8c9d..bdb6c3980 100644 --- a/lib/model/group.ts +++ b/lib/model/group.ts @@ -6,8 +6,8 @@ export default class Group { public zh: zh.Group; get ID(): number {return this.zh.groupID;} - get settings(): GroupSettings {return settings.getGroup(this.ID);} - get name(): string {return this.settings?.friendly_name || this.ID.toString();} + get options(): GroupOptions {return settings.getGroup(this.ID);} + get name(): string {return this.options?.friendly_name || this.ID.toString();} constructor(group: zh.Group) { this.zh = group; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 315e840be..90f9aea4b 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -164,24 +164,18 @@ declare global { // Settings // eslint-disable camelcase interface Settings { - homeassistant?: boolean, - devices?: {[s: string]: DeviceSettings}, - groups?: {[s: string]: GroupSettings}, - passlist: string[], - blocklist: string[], - whitelist: string[], - ban: string[], - availability?: boolean | { - active?: {timeout?: number}, - passive?: {timeout?: number} + homeassistant?: { + discovery_topic: string, + status_topic: string, + legacy_entity_attributes: boolean, + legacy_triggers: boolean, }, - permit_join: boolean, - frontend?: { - auth_token?: string, - host?: string, - port?: number, - url?: string, + permit_join?: boolean, + availability?: { + active: {timeout: number}, + passive: {timeout: number} }, + external_converters: string[], mqtt: { base_topic: string, include_device_information: boolean, @@ -198,11 +192,14 @@ declare global { reject_unauthorized?: boolean, }, serial: { - disable_led?: boolean, + disable_led: boolean, port?: string, - adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate' + adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate', + baudrate?: number, + rtscts?: boolean, }, - device_options: KeyValue, + passlist: string[], + blocklist: string[], map_options: { graphviz: { colors: { @@ -223,10 +220,21 @@ declare global { }, }, }, - experimental: { - output: 'json' | 'attribute' | 'attribute_and_json', - transmit_power?: number, + ota: { + update_check_interval: number, + disable_automatic_update_check: boolean, + zigbee_ota_override_index_location?: string, + ikea_ota_use_test_url?: boolean, }, + frontend?: { + auth_token?: string, + host?: string, + port?: number, + url?: string, + }, + devices?: {[s: string]: DeviceOptions}, + groups?: {[s: string]: GroupOptions}, + device_options: KeyValue, advanced: { legacy_api: boolean, log_rotation: boolean, @@ -236,7 +244,6 @@ declare global { log_file: string, log_level: 'debug' | 'info' | 'error' | 'warn', log_syslog: KeyValue, - soft_reset_timeout: number, pan_id: number | 'GENERATE', ext_pan_id: number[], channel: number, @@ -248,31 +255,21 @@ declare global { last_seen: 'disable' | 'ISO_8601' | 'ISO_8601_local' | 'epoch', elapsed: boolean, network_key: number[] | 'GENERATE', - report: boolean, - homeassistant_discovery_topic: string, - homeassistant_status_topic: string, - homeassistant_legacy_entity_attributes: boolean, - homeassistant_legacy_triggers: boolean, timestamp_format: string, - baudrate?: number, - rtscts?: boolean, - ikea_ota_use_test_url?: boolean, - // below are deprecated + output: 'json' | 'attribute' | 'attribute_and_json', + transmit_power?: number, + // Everything below is deprecated availability_timeout?: number, availability_blocklist?: string[], availability_passlist?: string[], availability_blacklist?: string[], availability_whitelist?: string[], + soft_reset_timeout: number, + report: boolean, }, - ota: { - update_check_interval: number, - disable_automatic_update_check: boolean, - zigbee_ota_override_index_location?: string, - }, - external_converters: string[], } - interface DeviceSettings { + interface DeviceOptions { ID?: string, retention?: number, availability?: boolean | {timeout: number}, @@ -289,7 +286,7 @@ declare global { qos?: 0 | 1 | 2, } - interface GroupSettings { + interface GroupOptions { devices?: string[], ID?: number, optimistic?: boolean, diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index cb6621985..e44118462 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -1,14 +1,49 @@ { "type": "object", "properties": { - "device_options": { - "type": "object" - }, "homeassistant": { "title": "Home Assistant integration", - "type": "boolean", + "requiresRestart": true, "description": "Home Assistant integration (MQTT discovery)", - "default": false + "default": false, + "oneOf": [ + { + "type": "boolean", + "title": "Home Assistant (simple)" + }, + { + "type": "object", + "title": "Home Assistant (advanced)", + "properties": { + "legacy_triggers": { + "type": "boolean", + "title": "Home Assistant legacy triggers", + "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discoverd", + "default": true + }, + "discovery_topic": { + "type": "string", + "title": "Homeassistant discovery topic", + "description": "Home Assistant discovery topic", + "requiresRestart": true, + "examples": ["homeassistant"] + }, + "legacy_entity_attributes": { + "type": "boolean", + "title": "Home Assistant legacy entity attributes", + "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", + "default": true + }, + "status_topic": { + "type": "string", + "title": "Home Assistant status topic", + "description": "Home Assistant status topic", + "requiresRestart": true, + "examples": ["homeassistant/status"] + } + } + } + ] }, "permit_join": { "type": "boolean", @@ -16,25 +51,15 @@ "title": "Permit join", "description": "Allow new devices to join (re-applied at restart)" }, - "external_converters": { - "type": "array", - "title": "External converters", - "description": "You can define external converters to e.g. add support for a DiY device", - "requiresRestart": true, - "items": { - "type": "string" - }, - "examples": ["DIYRuZ_FreePad.js"] - }, "availability": { "oneOf": [ { "type": "boolean", - "title": "Availability (boolean)" + "title": "Availability (simple)" }, { "type": "object", - "title": "Availability (object)", + "title": "Availability (advanced)", "properties": { "active": { "type": "object", @@ -73,6 +98,16 @@ "requiresRestart": true, "description": "Checks whether devices are online/offline" }, + "external_converters": { + "type": "array", + "title": "External converters", + "description": "You can define external converters to e.g. add support for a DiY device", + "requiresRestart": true, + "items": { + "type": "string" + }, + "examples": ["DIYRuZ_FreePad.js"] + }, "mqtt": { "type": "object", "title": "MQTT", @@ -80,6 +115,7 @@ "base_topic": { "type": "string", "title": "Base topic", + "default": "zigbee2mqtt", "requiresRestart": true, "description": "MQTT base topic for Zigbee2MQTT MQTT messages", "examples": ["zigbee2mqtt"] @@ -169,7 +205,7 @@ "default": false } }, - "required": ["base_topic", "server"] + "required": ["server"] }, "serial": { "type": "object", @@ -196,6 +232,19 @@ "default": "auto", "requiresRestart": true, "description": "Adapter type, not needed unless you are experiencing problems" + }, + "baudrate": { + "type": "number", + "title": "Baudrate", + "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", + "requiresRestart": true, + "description": "RTS / CTS Hardware Flow Control for serial port" } } }, @@ -217,331 +266,6 @@ "type": "string" } }, - "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" - } - }, - "experimental": { - "type": "object", - "title": "Experimental", - "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)" - } - } - }, - "advanced": { - "type": "object", - "title": "Advanced", - "properties": { - "legacy_api": { - "type": "boolean", - "title": "Legacy API", - "requiresRestart": true, - "description": "Disables the legacy api (false = disable)", - "default": true - }, - "pan_id": { - "oneOf": [{ - "type": "string", - "title": "Pan ID (string)" - }, - { - "type": "number", - "title": "Pan ID (number)" - } - ], - "title": "Pan ID", - "requiresRestart": true, - "description": "ZigBee pan ID, changing requires repairing all devices!" - }, - "ext_pan_id": { - "type": "array", - "items": { - "type": "number" - }, - "title": "Ext Pan ID", - "requiresRestart": true, - "description": "Zigbee extended pan ID, changing requires repairing all devices!" - }, - "channel": { - "type": "number", - "minimum": 11, - "maximum": 26, - "default": 11, - "title": "ZigBee channel", - "requiresRestart": true, - "description": "Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)", - "examples": [15, 20, 25] - }, - "cache_state": { - "type": "boolean", - "title": "Cache state", - "description": "MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant", - "default": true - }, - "cache_state_persistent": { - "type": "boolean", - "title": "Persist cache state", - "description": "Persist cached state, only used when cache_state: true", - "default": true - }, - "cache_state_send_on_startup": { - "type": "boolean", - "title": "Send cached state on startup", - "description": "Send cached state on startup, only used when cache_state: true", - "default": true - }, - "log_rotation": { - "type": "boolean", - "title": "Log rotation", - "requiresRestart": true, - "description": "Log rotation", - "default": true - }, - "log_symlink_current": { - "type": "boolean", - "title": "Log symlink current", - "requiresRestart": true, - "description": "Create symlink to current logs in the log directory", - "default": false - }, - "log_level": { - "type": "string", - "enum": ["info", "warn", "error", "debug"], - "title": "Log level", - "description": "Logging level", - "default": "info" - }, - "log_output": { - "type": "array", - "requiresRestart": true, - "items": { - "type": "string", - "enum": ["console", "file", "syslog"] - }, - "title": "Log output", - "description": "Output location of the log, leave empty to suppress logging" - }, - "log_directory": { - "type": "string", - "title": "Log directory", - "requiresRestart": true, - "description": "Location of log directory", - "examples": ["data/log/%TIMESTAMP%"] - }, - "log_file": { - "type": "string", - "title": "Log file", - "requiresRestart": true, - "description": "Log file name, can also contain timestamp", - "examples": ["zigbee2mqtt_%TIMESTAMP%.log"], - "default": "log.txt" - }, - "baudrate": { - "type": "number", - "title": "Baudrate", - "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", - "requiresRestart": true, - "description": "RTS / CTS Hardware Flow Control for serial port" - }, - "soft_reset_timeout": { - "type": "number", - "minimum": 0, - "requiresRestart": true, - "title": "Soft reset timeout (deprecated)", - "description": "Soft reset ZNP after timeout", - "readOnly": true - }, - "network_key": { - "oneOf": [{ - "type": "string", - "title": "Network key(string)" - }, - { - "type": "array", - "items": { - "type": "number" - }, - "title": "Network key(array)" - } - ], - "title": "Network key", - "requiresRestart": true, - "description": "Network encryption key, changing requires repairing all devices!" - }, - "last_seen": { - "type": "string", - "enum": ["disable", "ISO_8601", "ISO_8601_local", "epoch"], - "title": "Last seen", - "description": "Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message", - "default": "disable" - }, - "elapsed": { - "type": "boolean", - "title": "Elapsed", - "description": "Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg", - "default": false - }, - "report": { - "type": "boolean", - "title": "Reporting", - "requiresRestart": true, - "readOnly": true, - "description": "Enables report feature (deprecated)" - }, - "homeassistant_discovery_topic": { - "type": "string", - "title": "Homeassistant discovery topic", - "description": "Home Assistant discovery topic", - "requiresRestart": true, - "examples": ["homeassistant"] - }, - "homeassistant_legacy_entity_attributes": { - "type": "boolean", - "title": "Home Assistant legacy entity attributes", - "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", - "default": true - }, - "homeassistant_status_topic": { - "type": "string", - "title": "Home Assistant status topic", - "description": "Home Assistant status topic", - "requiresRestart": true, - "examples": ["homeassistant/status"] - }, - "timestamp_format": { - "type": "string", - "title": "Timestamp format", - "requiresRestart": true, - "description": "Log timestamp format", - "examples": ["YYYY-MM-DD HH:mm:ss"] - }, - "adapter_concurrent": { - "title": "Adapter concurrency", - "requiresRestart": true, - "type": ["number", "null"], - "description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)" - }, - "adapter_delay": { - "type": ["number", "null"], - "requiresRestart": true, - "title": "Adapter delay", - "description": "Adapter delay" - }, - "ikea_ota_use_test_url": { - "type": "boolean", - "title": "IKEA TRADFRI OTA use test url", - "requiresRestart": true, - "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", - "default": false - }, - "homeassistant_legacy_triggers": { - "type": "boolean", - "title": "Home Assistant legacy triggers", - "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered", - "default": true - }, - "log_syslog": { - "type": "object", - "title": "syslog", - "properties": { - "host": { - "type": "string", - "title": "Host", - "description": "The host running syslogd, defaults to localhost.", - "default": "localhost" - }, - "port": { - "type": "number", - "title": "Port", - "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", - "default": 123 - }, - "protocol": { - "type": "string", - "title": "Protocol", - "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", - "default": "tcp4", - "examples": [ - "udp4", - "tls4", - "unix", - "unix-connect" - ] - }, - "path": { - "type": "string", - "title": "Path", - "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", - "default": "/dev/log", - "examples": [ - "/var/run/syslog" - ] - }, - "pid": { - "type": "string", - "title": "PID", - "description": "PID of the process that log messages are coming from (Default process.pid).", - "default": "process.pid" - }, - "localhost": { - "type": "string", - "title": "Localhost", - "description": "Host to indicate that log messages are coming from (Default: localhost).", - "default": "localhost" - }, - "type": { - "type": "string", - "title": "Type", - "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", - "default": "5424" - }, - "app_name": { - "type": "string", - "title": "Localhost", - "description": "The name of the application (Default: Zigbee2MQTT).", - "default": "Zigbee2MQTT" - }, - "eol": { - "type": "string", - "title": "eol", - "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", - "default": "/n" - } - } - } - } - }, "map_options": { "type": "object", "title": "Networkmap", @@ -613,6 +337,13 @@ "description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.", "default": false }, + "ikea_ota_use_test_url": { + "type": "boolean", + "title": "IKEA TRADFRI OTA use test url", + "requiresRestart": true, + "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", + "default": false + }, "zigbee_ota_override_index_location": { "type": "string", "title": "OTA index override file name", @@ -622,6 +353,48 @@ } } }, + "frontend": { + "oneOf": [ + { + "type": "boolean", + "title": "Frontend (simple)" + }, + { + "type": "object", + "title": "Frontend (advanced)", + "properties": { + "port": { + "type": "number", + "title": "Port", + "description": "Frontend binding port", + "default": 8080, + "requiresRestart": true + }, + "host": { + "type": "string", + "title": "Bind host", + "description": "Frontend binding host", + "default": "0.0.0.0", + "requiresRestart": true + }, + "auth_token": { + "type": ["string", "null"], + "title": "Auth token", + "description": "Enables authentication, disabled by default", + "requiresRestart": true + }, + "url": { + "type": ["string", "null"], + "title": "URL", + "description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page", + "requiresRestart": true + } + } + } + ], + "title": "Frontend", + "requiresRestart": true + }, "devices": { "type": "object", "propertyNames": { @@ -644,31 +417,346 @@ } } }, - "frontend": { + "device_options": { "type": "object", - "title": "Frontend", + "title": "Options that are applied to all devices" + }, + "advanced": { + "type": "object", + "title": "Advanced", "properties": { - "port": { - "type": "number", - "title": "Port", - "description": "Frontend binding port", - "default": 8080, - "requiresRestart": true + "legacy_api": { + "type": "boolean", + "title": "Legacy API", + "requiresRestart": true, + "description": "Disables the legacy api (false = disable)", + "default": true }, - "host": { + "log_rotation": { + "type": "boolean", + "title": "Log rotation", + "requiresRestart": true, + "description": "Log rotation", + "default": true + }, + "log_symlink_current": { + "type": "boolean", + "title": "Log symlink current", + "requiresRestart": true, + "description": "Create symlink to current logs in the log directory", + "default": false + }, + "log_output": { + "type": "array", + "requiresRestart": true, + "items": { + "type": "string", + "enum": ["console", "file", "syslog"] + }, + "title": "Log output", + "description": "Output location of the log, leave empty to suppress logging" + }, + "log_directory": { "type": "string", - "title": "Bind host", - "description": "Frontend binding host", - "default": "0.0.0.0", - "requiresRestart": true + "title": "Log directory", + "requiresRestart": true, + "description": "Location of log directory", + "examples": ["data/log/%TIMESTAMP%"] }, - "auth_token": { - "type": ["string", "null"], - "title": "Auth token", - "description": "Enables authentication, disabled by default", - "requiresRestart": true + "log_file": { + "type": "string", + "title": "Log file", + "requiresRestart": true, + "description": "Log file name, can also contain timestamp", + "examples": ["zigbee2mqtt_%TIMESTAMP%.log"], + "default": "log.txt" + }, + "log_level": { + "type": "string", + "enum": ["info", "warn", "error", "debug"], + "title": "Log level", + "description": "Logging level", + "default": "info" + }, + "log_syslog": { + "type": "object", + "title": "syslog", + "properties": { + "host": { + "type": "string", + "title": "Host", + "description": "The host running syslogd, defaults to localhost.", + "default": "localhost" + }, + "port": { + "type": "number", + "title": "Port", + "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", + "default": 123 + }, + "protocol": { + "type": "string", + "title": "Protocol", + "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", + "default": "tcp4", + "examples": [ + "udp4", + "tls4", + "unix", + "unix-connect" + ] + }, + "path": { + "type": "string", + "title": "Path", + "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", + "default": "/dev/log", + "examples": [ + "/var/run/syslog" + ] + }, + "pid": { + "type": "string", + "title": "PID", + "description": "PID of the process that log messages are coming from (Default process.pid).", + "default": "process.pid" + }, + "localhost": { + "type": "string", + "title": "Localhost", + "description": "Host to indicate that log messages are coming from (Default: localhost).", + "default": "localhost" + }, + "type": { + "type": "string", + "title": "Type", + "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", + "default": "5424" + }, + "app_name": { + "type": "string", + "title": "Localhost", + "description": "The name of the application (Default: Zigbee2MQTT).", + "default": "Zigbee2MQTT" + }, + "eol": { + "type": "string", + "title": "eol", + "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", + "default": "/n" + } + } + }, + "pan_id": { + "oneOf": [{ + "type": "string", + "title": "Pan ID (string)" + }, + { + "type": "number", + "title": "Pan ID (number)" + } + ], + "title": "Pan ID", + "requiresRestart": true, + "description": "ZigBee pan ID, changing requires repairing all devices!" + }, + "ext_pan_id": { + "type": "array", + "items": { + "type": "number" + }, + "title": "Ext Pan ID", + "requiresRestart": true, + "description": "Zigbee extended pan ID, changing requires repairing all devices!" + }, + "channel": { + "type": "number", + "minimum": 11, + "maximum": 26, + "default": 11, + "title": "ZigBee channel", + "requiresRestart": true, + "description": "Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)", + "examples": [15, 20, 25] + }, + "adapter_concurrent": { + "title": "Adapter concurrency", + "requiresRestart": true, + "type": ["number", "null"], + "description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)" + }, + "adapter_delay": { + "type": ["number", "null"], + "requiresRestart": true, + "title": "Adapter delay", + "description": "Adapter delay" + }, + "cache_state": { + "type": "boolean", + "title": "Cache state", + "description": "MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant", + "default": true + }, + "cache_state_persistent": { + "type": "boolean", + "title": "Persist cache state", + "description": "Persist cached state, only used when cache_state: true", + "default": true + }, + "cache_state_send_on_startup": { + "type": "boolean", + "title": "Send cached state on startup", + "description": "Send cached state on startup, only used when cache_state: true", + "default": true + }, + "last_seen": { + "type": "string", + "enum": ["disable", "ISO_8601", "ISO_8601_local", "epoch"], + "title": "Last seen", + "description": "Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message", + "default": "disable" + }, + "elapsed": { + "type": "boolean", + "title": "Elapsed", + "description": "Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg", + "default": false + }, + "network_key": { + "oneOf": [{ + "type": "string", + "title": "Network key(string)" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "title": "Network key(array)" + } + ], + "title": "Network key", + "requiresRestart": true, + "description": "Network encryption key, changing requires repairing all devices!" + }, + "timestamp_format": { + "type": "string", + "title": "Timestamp format", + "requiresRestart": true, + "description": "Log timestamp format", + "examples": ["YYYY-MM-DD HH:mm:ss"] + }, + "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)" + }, + "homeassistant_discovery_topic": { + "type": "string", + "title": "Homeassistant discovery topic", + "description": "Home Assistant discovery topic", + "requiresRestart": true, + "examples": ["homeassistant"] + }, + "homeassistant_legacy_entity_attributes": { + "type": "boolean", + "title": "Home Assistant legacy entity attributes", + "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", + "default": true + }, + "homeassistant_status_topic": { + "type": "string", + "title": "Home Assistant status topic", + "description": "Home Assistant status topic", + "requiresRestart": true, + "examples": ["homeassistant/status"] + }, + "homeassistant_legacy_triggers": { + "type": "boolean", + "title": "Home Assistant legacy triggers", + "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered", + "default": true + }, + "soft_reset_timeout": { + "type": "number", + "minimum": 0, + "requiresRestart": true, + "title": "Soft reset timeout (deprecated)", + "description": "Soft reset ZNP after timeout", + "readOnly": true + }, + "report": { + "type": "boolean", + "title": "Reporting", + "requiresRestart": true, + "readOnly": true, + "description": "Enables report feature (deprecated)" + }, + "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" + }, + "ikea_ota_use_test_url": { + "type": "boolean", + "title": "IKEA TRADFRI OTA use test url (deprecated)", + "requiresRestart": true, + "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", + "default": false } } + }, + "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"], diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 0c6aa1259..941df3feb 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -5,33 +5,46 @@ import path from 'path'; import yaml from './yaml'; import Ajv from 'ajv'; import schemaJson from './settings.schema.json'; -export const schema = schemaJson; +export let schema = schemaJson; +// @ts-ignore +schema = {}; +objectAssignDeep(schema, schemaJson); + +// Remove legacy settings from schema +{ + delete schema.properties.advanced.properties.homeassistant_discovery_topic; + delete schema.properties.advanced.properties.homeassistant_legacy_entity_attributes; + delete schema.properties.advanced.properties.homeassistant_legacy_triggers; + delete schema.properties.advanced.properties.homeassistant_status_topic; + delete schema.properties.advanced.properties.soft_reset_timeout; + delete schema.properties.advanced.properties.report; + delete schema.properties.advanced.properties.baudrate; + delete schema.properties.advanced.properties.rtscts; + delete schema.properties.advanced.properties.ikea_ota_use_test_url; + delete schema.properties.experimental; + delete schemaJson.properties.whitelist; + delete schemaJson.properties.ban; +} // DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697 const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml'); -const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schema); +const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson); const ajvRestartRequired = new Ajv({allErrors: true}) - .addKeyword({keyword: 'requiresRestart', validate: (schema: unknown) => !schema}).compile(schema); + .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson); const defaults: RecursivePartial = { - passlist: [], - blocklist: [], - // Deprecated: use block/passlist - whitelist: [], - ban: [], permit_join: false, + external_converters: [], mqtt: { + base_topic: 'zigbee2mqtt', include_device_information: false, - /** - * Configurable force disable retain flag on mqtt publish. - * https://github.com/Koenkk/zigbee2mqtt/pull/4948 - */ force_disable_retain: false, }, serial: { disable_led: false, }, - device_options: {}, + passlist: [], + blocklist: [], map_options: { graphviz: { colors: { @@ -52,10 +65,11 @@ const defaults: RecursivePartial = { }, }, }, - experimental: { - // json or attribute or attribute_and_json - output: 'json', + ota: { + update_check_interval: 24 * 60, + disable_automatic_update_check: false, }, + device_options: {}, advanced: { legacy_api: true, log_rotation: true, @@ -65,106 +79,113 @@ const defaults: RecursivePartial = { log_file: 'log.txt', log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info', log_syslog: {}, - soft_reset_timeout: 0, pan_id: 0x1a62, ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD], channel: 11, adapter_concurrent: null, adapter_delay: null, - - // Availability timeout in seconds, disabled by default. - availability_blocklist: [], - availability_passlist: [], - // Deprecated, use block/passlist - availability_blacklist: [], - availability_whitelist: [], - - /** - * Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device. - * https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9 - * - * Therefore Zigbee2MQTT BY DEFAULT caches all values and resend it with every message. - * advanced.cache_state in configuration.yaml allows to configure this. - * https://www.zigbee2mqtt.io/guide/configuration/ - */ cache_state: true, cache_state_persistent: true, cache_state_send_on_startup: true, - - /** - * Add a last_seen attribute to mqtt messages, contains date/time of zigbee message arrival - * "ISO_8601": ISO 8601 format - * "ISO_8601_local": Local ISO 8601 format (instead of UTC-based) - * "epoch": milliseconds elapsed since the UNIX epoch - * "disable": no last_seen attribute (default) - */ last_seen: 'disable', - - // Optional: Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg elapsed: false, - - /** - * https://github.com/Koenkk/zigbee2mqtt/issues/685#issuecomment-449112250 - * - * Network key will serve as the encryption key of your network. - * Changing this will require you to repair your devices. - */ network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], - - /** - * Enables reporting feature - */ - report: false, - - /** - * Home Assistant discovery topic - */ - homeassistant_discovery_topic: 'homeassistant', - - /** - * Home Assistant status topic - */ - homeassistant_status_topic: 'hass/status', - - /** - * Home Assistant legacy entity attributes, when enabled: - * Zigbee2MQTT will send additional states as attributes with each entity. - * For example, A temperature & humidity sensor will have 2 entities for - * the temperature and humidity, with this setting enabled both entities - * will also have an temperature and humidity attribute. - */ - homeassistant_legacy_entity_attributes: true, - - /** - * Home Assistant legacy triggers, when enabled: - * - Zigbee2mqt will send an empty 'action' or 'click' after one has been send - * - A 'sensor_action' and 'sensor_click' will be discovered - */ - homeassistant_legacy_triggers: true, - - /** - * Configurable timestampFormat - * https://github.com/Koenkk/zigbee2mqtt/commit/44db557a0c83f419d66755d14e460cd78bd6204e - */ timestamp_format: 'YYYY-MM-DD HH:mm:ss', + output: 'json', + // Everything below is deprecated + availability_blocklist: [], + availability_passlist: [], + availability_blacklist: [], + availability_whitelist: [], + soft_reset_timeout: 0, + report: false, }, - ota: { - /** - * Minimal time delta in minutes between polling third party server for potential firmware updates - */ - update_check_interval: 24 * 60, - /** - * Completely disallow Zigbee devices to initiate a search for a potential firmware update. - * If set to true, only a user-initiated update search will be possible. - */ - disable_automatic_update_check: false, - }, - external_converters: [], }; let _settings: Partial; let _settingsWithDefaults: Settings; +function loadSettingsWithDefaults(): void { + _settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings; + + if (!_settingsWithDefaults.devices) { + _settingsWithDefaults.devices = {}; + } + + if (!_settingsWithDefaults.groups) { + _settingsWithDefaults.groups = {}; + } + + if (_settingsWithDefaults.homeassistant) { + const defaults = {discovery_topic: 'homeassistant', status_topic: 'hass/status', + legacy_entity_attributes: true, legacy_triggers: true}; + const sLegacy = {}; + if (_settingsWithDefaults.advanced) { + for (const key of ['homeassistant_legacy_triggers', 'homeassistant_discovery_topic', + 'homeassistant_legacy_entity_attributes', 'homeassistant_status_topic']) { + // @ts-ignore + if (_settingsWithDefaults.advanced[key] !== undefined) { + // @ts-ignore + sLegacy[key.replace('homeassistant_', '')] = _settingsWithDefaults.advanced[key]; + } + } + } + + const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {}; + // @ts-ignore + _settingsWithDefaults.homeassistant = {}; + objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s); + } + + if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) { + const defaults = {active: {timeout: 10}, passive: {timeout: 25}}; + const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {}; + // @ts-ignore + _settingsWithDefaults.availability = {}; + objectAssignDeep(_settingsWithDefaults.availability, defaults, s); + } + + if (_settingsWithDefaults.frontend) { + const defaults = {port: 8080, auth_token: false, host: '0.0.0.0'}; + const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {}; + // @ts-ignore + _settingsWithDefaults.frontend = {}; + objectAssignDeep(_settingsWithDefaults.frontend, defaults, s); + } + + if (_settings.advanced?.hasOwnProperty('baudrate') && _settings.serial?.baudrate == null) { + // @ts-ignore + _settingsWithDefaults.serial.baudrate = _settings.advanced.baudrate; + } + + if (_settings.advanced?.hasOwnProperty('rtscts') && _settings.serial?.rtscts == null) { + // @ts-ignore + _settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts; + } + + if (_settings.advanced?.hasOwnProperty('ikea_ota_use_test_url') && _settings.ota?.ikea_ota_use_test_url == null) { + // @ts-ignore + _settingsWithDefaults.ota.ikea_ota_use_test_url = _settings.advanced.ikea_ota_use_test_url; + } + + // @ts-ignore + if (_settings.experimental?.hasOwnProperty('transmit_power') && _settings.advanced?.transmit_power == null) { + // @ts-ignore + _settingsWithDefaults.advanced.transmit_power = _settings.experimental.transmit_power; + } + + // @ts-ignore + if (_settings.experimental?.hasOwnProperty('output') && _settings.advanced?.output == null) { + // @ts-ignore + _settingsWithDefaults.advanced.output = _settings.experimental.output; + } + + // @ts-ignore + _settingsWithDefaults.ban && _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban); + // @ts-ignore + _settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist); +} + function write(): void { const settings = getInternalSettings(); const toWrite: KeyValue = objectAssignDeep({}, settings); @@ -213,7 +234,7 @@ function write(): void { yaml.writeIfChanged(file, toWrite); _settings = read(); - _settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings; + loadSettingsWithDefaults(); } export function validate(): string[] { @@ -247,7 +268,7 @@ export function validate(): string[] { // Verify that all friendly names are unique const names: string[] = []; - const check = (e: DeviceSettings | GroupSettings): void => { + const check = (e: DeviceOptions | GroupOptions): void => { if (names.includes(e.friendly_name)) errors.push(`Duplicate friendly_name '${e.friendly_name}' found`); errors.push(...utils.validateFriendlyName(e.friendly_name)); names.push(e.friendly_name); @@ -376,7 +397,7 @@ function applyEnvironmentVariables(settings: Partial): void { } }); }; - iterate(schema.properties, []); + iterate(schemaJson.properties, []); } function getInternalSettings(): Partial { @@ -390,15 +411,7 @@ function getInternalSettings(): Partial { export function get(): Settings { if (!_settingsWithDefaults) { - _settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings; - } - - if (!_settingsWithDefaults.devices) { - _settingsWithDefaults.devices = {}; - } - - if (!_settingsWithDefaults.groups) { - _settingsWithDefaults.groups = {}; + loadSettingsWithDefaults(); } return _settingsWithDefaults; @@ -443,7 +456,7 @@ export function apply(newSettings: Record): boolean { return restartRequired; } -export function getGroup(IDorName: string | number): GroupSettings { +export function getGroup(IDorName: string | number): GroupOptions { const settings = get(); const byID = settings.groups[IDorName]; if (byID) { @@ -459,14 +472,14 @@ export function getGroup(IDorName: string | number): GroupSettings { return null; } -export function getGroups(): GroupSettings[] { +export function getGroups(): GroupOptions[] { const settings = get(); return Object.entries(settings.groups).map(([ID, group]) => { return {devices: [], ...group, ID: Number(ID)}; }); } -function getGroupThrowIfNotExists(IDorName: string): GroupSettings { +function getGroupThrowIfNotExists(IDorName: string): GroupOptions { const group = getGroup(IDorName); if (!group) { throw new Error(`Group '${IDorName}' does not exist`); @@ -475,7 +488,7 @@ function getGroupThrowIfNotExists(IDorName: string): GroupSettings { return group; } -export function getDevice(IDorName: string): DeviceSettings { +export function getDevice(IDorName: string): DeviceOptions { const settings = get(); const byID = settings.devices[IDorName]; if (byID) { @@ -491,7 +504,7 @@ export function getDevice(IDorName: string): DeviceSettings { return null; } -function getDeviceThrowIfNotExists(IDorName: string): DeviceSettings { +function getDeviceThrowIfNotExists(IDorName: string): DeviceOptions { const device = getDevice(IDorName); if (!device) { throw new Error(`Device '${IDorName}' does not exist`); @@ -500,7 +513,7 @@ function getDeviceThrowIfNotExists(IDorName: string): DeviceSettings { return device; } -export function addDevice(ID: string): DeviceSettings { +export function addDevice(ID: string): DeviceOptions { if (getDevice(ID)) { throw new Error(`Device '${ID}' already exists`); } @@ -516,17 +529,17 @@ export function addDevice(ID: string): DeviceSettings { return getDevice(ID); } -export function whitelistDevice(ID: string): void { +export function addDeviceToPasslist(ID: string): void { const settings = getInternalSettings(); - if (!settings.whitelist) { - settings.whitelist = []; + if (!settings.passlist) { + settings.passlist = []; } - if (settings.whitelist.includes(ID)) { - throw new Error(`Device '${ID}' already whitelisted`); + if (settings.passlist.includes(ID)) { + throw new Error(`Device '${ID}' already in passlist`); } - settings.whitelist.push(ID); + settings.passlist.push(ID); write(); } @@ -540,16 +553,6 @@ export function blockDevice(ID: string): void { write(); } -export function banDevice(ID: string): void { - const settings = getInternalSettings(); - if (!settings.ban) { - settings.ban = []; - } - - settings.ban.push(ID); - write(); -} - export function removeDevice(IDorName: string): void { const device = getDeviceThrowIfNotExists(IDorName); const settings = getInternalSettings(); @@ -567,7 +570,7 @@ export function removeDevice(IDorName: string): void { write(); } -export function addGroup(name: string, ID?: string): GroupSettings { +export function addGroup(name: string, ID?: string): GroupOptions { utils.validateFriendlyName(name, true); if (getGroup(name) || getDevice(name)) { throw new Error(`friendly_name '${name}' is already in use`); diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 7492fe1f4..0286b5c0c 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -246,8 +246,8 @@ function sanitizeImageParameter(parameter: string): string { } function isAvailabilityEnabledForDevice(device: Device, settings: Settings): boolean { - if (device.settings.hasOwnProperty('availability')) { - return !!device.settings.availability; + if (device.options.hasOwnProperty('availability')) { + return !!device.options.availability; } // availability_timeout = deprecated diff --git a/lib/zigbee.ts b/lib/zigbee.ts index ce9bd4ffa..333bd8698 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -36,8 +36,8 @@ export default class Zigbee { databaseBackupPath: data.joinPath('database.db.backup'), backupPath: data.joinPath('coordinator_backup.json'), serialPort: { - baudRate: settings.get().advanced.baudrate, - rtscts: settings.get().advanced.rtscts, + baudRate: settings.get().serial.baudrate, + rtscts: settings.get().serial.rtscts, path: settings.get().serial.port, adapter: settings.get().serial.adapter, }, @@ -112,8 +112,8 @@ export default class Zigbee { for (const device of this.devices(false)) { // If a passlist is used, all other device will be removed from the network. - const passlist = settings.get().passlist.concat(settings.get().whitelist); - const blocklist = settings.get().blocklist.concat(settings.get().ban); + const passlist = settings.get().passlist; + const blocklist = settings.get().blocklist; const remove = async (device: Device): Promise => { try { await device.zh.removeFromNetwork(); @@ -133,8 +133,8 @@ export default class Zigbee { } // Check if we have to set a transmit power - if (settings.get().experimental.hasOwnProperty('transmit_power')) { - const transmitPower = settings.get().experimental.transmit_power; + if (settings.get().advanced.hasOwnProperty('transmit_power')) { + const transmitPower = settings.get().advanced.transmit_power; await this.herdsman.setTransmitPower(transmitPower); logger.info(`Set transmit power to '${transmitPower}'`); } @@ -275,8 +275,8 @@ export default class Zigbee { @bind private async acceptJoiningDeviceHandler(ieeeAddr: string): Promise { // If passlist is set, all devices not on passlist will be rejected to join the network - const passlist = settings.get().passlist.concat(settings.get().whitelist); - const blocklist = settings.get().blocklist.concat(settings.get().ban); + const passlist = settings.get().passlist; + const blocklist = settings.get().blocklist; if (passlist.length > 0) { if (passlist.includes(ieeeAddr)) { logger.info(`Accepting joining device which is on passlist '${ieeeAddr}'`); diff --git a/test/bridge.test.js b/test/bridge.test.js index 7b5ff079e..934d8ab11 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -66,7 +66,7 @@ describe('Bridge', () => { const directory = settings.get().advanced.log_directory; expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', - stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "homeassistant_discovery_topic": "homeassistant", "homeassistant_legacy_entity_attributes": true, "homeassistant_legacy_triggers": true, "homeassistant_status_topic": "hass/status", "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.txt", "log_level": "info", "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "ban": [], "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "experimental": { "output": "json" }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" }, "whitelist": [] }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, config_schema: settings.schema }), + stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.txt", "log_level": "info", "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, config_schema: settings.schema }), { retain: true, qos: 0 }, expect.any(Function) ); @@ -1035,7 +1035,7 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":false},"status":"ok"}), + stringify({"data":{"restart_required":true},"status":"ok"}), {retain: false, qos: 0}, expect.any(Function) ); }); @@ -1129,31 +1129,31 @@ describe('Bridge', () => { const svg_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo'; const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg'; definition.icon = icon_link; - let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}}); + let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe(icon_link); definition.icon = icon_link; - payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {icon: svg_icon}}); + payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {icon: svg_icon}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe(svg_icon); definition.icon = '_${model}_'; - payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}}); + payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe('_lumi.plug_'); definition.icon = '_${model}_${zigbeeModel}_'; - payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}}); + payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe('_lumi.plug_lumi.plug_'); definition.icon = svg_icon; - payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}}); + payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe(svg_icon); @@ -1161,7 +1161,7 @@ describe('Bridge', () => { device.modelID = '?._Z\\NC+Z02*LM'; definition.model = '&&&&*+'; definition.icon = '_${model}_${zigbeeModel}_'; - payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}}); + payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); expect(payload).not.toBeUndefined() expect(payload['icon']).not.toBeUndefined() expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_'); diff --git a/test/controller.test.js b/test/controller.test.js index bfb67bd22..b5a075b09 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -28,9 +28,10 @@ describe('Controller', () => { MQTT.restoreOnMock(); zigbeeHerdsman.returnDevices.splice(0); mockExit = jest.fn(); + data.writeDefaultConfiguration(); + settings.reRead(); controller = new Controller(jest.fn(), mockExit); mocksClear.forEach((m) => m.mockClear()); - data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultState(); }); @@ -180,14 +181,6 @@ describe('Controller', () => { expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); }); - it('Should remove non whitelisted devices on startup', async () => { - settings.set(['whitelist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - await controller.start(); - await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); - }); - it('Should remove device on blocklist on startup', async () => { settings.set(['blocklist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); await controller.start(); @@ -196,14 +189,6 @@ describe('Controller', () => { expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); }); - it('Should remove banned devices on startup', async () => { - settings.set(['ban'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - await controller.start(); - await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); - }); - it('Start controller fails', async () => { zigbeeHerdsman.start.mockImplementationOnce(() => {throw new Error('failed')}); await controller.start(); @@ -630,11 +615,13 @@ describe('Controller', () => { }); it('Should disable legacy options on new network start', async () => { - expect(settings.get().advanced.homeassistant_legacy_entity_attributes).toBeTruthy(); + settings.set(['homeassistant'], true); + settings.reRead(); + expect(settings.get().homeassistant.legacy_entity_attributes).toBeTruthy(); expect(settings.get().advanced.legacy_api).toBeTruthy(); zigbeeHerdsman.start.mockReturnValueOnce('reset'); await controller.start(); - expect(settings.get().advanced.homeassistant_legacy_entity_attributes).toBeFalsy(); + expect(settings.get().homeassistant.legacy_entity_attributes).toBeFalsy(); expect(settings.get().advanced.legacy_api).toBeFalsy(); }); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 5bb941a44..e04a04387 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -24,6 +24,7 @@ describe('HomeAssistant extension', () => { beforeEach(async () => { data.writeDefaultConfiguration(); settings.reRead(); + settings.set(['homeassistant'], true); data.writeEmptyState(); controller.state.load(); await resetExtension(); @@ -50,7 +51,7 @@ describe('HomeAssistant extension', () => { const duplicated = []; require('zigbee-herdsman-converters').devices.forEach((d) => { const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes; - const device = {definition: d, isDevice: () => true, settings: {}, exposes: () => exposes}; + const device = {definition: d, isDevice: () => true, options: {}, exposes: () => exposes}; const configs = extension.getConfigs(device); const cfg_type_object_ids = []; diff --git a/test/legacy/bridgeLegacy.test.js b/test/legacy/bridgeLegacy.test.js index bcd41a3d6..0b7f05071 100644 --- a/test/legacy/bridgeLegacy.test.js +++ b/test/legacy/bridgeLegacy.test.js @@ -56,7 +56,7 @@ describe('Bridge legacy', () => { it('Should allow whitelist', async () => { const bulb_color = zigbeeHerdsman.devices.bulb_color; const bulb = zigbeeHerdsman.devices.bulb; - expect(settings.get().whitelist).toStrictEqual([]); + expect(settings.get().passlist).toStrictEqual([]); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb_color'); await flushPromises(); @@ -68,7 +68,7 @@ describe('Bridge legacy', () => { ); MQTT.publish.mockClear() - expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr]); + expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr]); MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( @@ -79,10 +79,10 @@ describe('Bridge legacy', () => { ); MQTT.publish.mockClear() - expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); + expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); await flushPromises(); - expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); + expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); expect(MQTT.publish).toHaveBeenCalledTimes(0); }); @@ -366,7 +366,7 @@ describe('Bridge legacy', () => { controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}}; const device = zigbeeHerdsman.devices.bulb_color; device.removeFromNetwork.mockClear(); - expect(settings.get().ban.length).toBe(0); + expect(settings.get().blocklist.length).toBe(0); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); @@ -381,14 +381,14 @@ describe('Bridge legacy', () => { expect.any(Function) ); expect(controller.state.state).toStrictEqual({}); - expect(settings.get().ban.length).toBe(0); + expect(settings.get().blocklist.length).toBe(0); }); it('Should allow to force remove device', async () => { controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}}; const device = zigbeeHerdsman.devices.bulb_color; device.removeFromDatabase.mockClear(); - expect(settings.get().ban.length).toBe(0); + expect(settings.get().blocklist.length).toBe(0); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/force_remove', 'bulb_color'); @@ -403,13 +403,13 @@ describe('Bridge legacy', () => { expect.any(Function) ); expect(controller.state.state).toStrictEqual({}); - expect(settings.get().ban.length).toBe(0); + expect(settings.get().blocklist.length).toBe(0); }); - it('Should allow to ban device', async () => { + it('Should allow to block device', async () => { const device = zigbeeHerdsman.devices.bulb_color; device.removeFromNetwork.mockClear(); - expect(settings.get().ban.length).toBe(0); + expect(settings.get().blocklist.length).toBe(0); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); @@ -423,7 +423,7 @@ describe('Bridge legacy', () => { {qos: 0, retain: false}, expect.any(Function) ); - expect(settings.get().ban).toStrictEqual(['0x000b57fffec6a5b3']); + expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b3']); }); it('Shouldnt crash when removing non-existing device', async () => { diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js index 25970ac50..e8991cc67 100644 --- a/test/otaUpdate.test.js +++ b/test/otaUpdate.test.js @@ -28,7 +28,7 @@ describe('OTA update', () => { data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultConfiguration(); - settings.set(['advanced', 'ikea_ota_use_test_url'], true); + settings.set(['ota', 'ikea_ota_use_test_url'], true); settings.reRead(); jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); diff --git a/test/settings.test.js b/test/settings.test.js index a2fb2f77f..d61206987 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -66,7 +66,7 @@ describe('Settings', () => { it('Should apply environment variables', () => { process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1; - process.env['ZIGBEE2MQTT_CONFIG_EXPERIMENTAL_OUTPUT'] = 'csvtest'; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'csvtest'; process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}'; process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic'; @@ -77,7 +77,7 @@ describe('Settings', () => { expected.groups = {}; expected.serial.disable_led = true; expected.advanced.soft_reset_timeout = 1; - expected.experimental.output = 'csvtest'; + expected.advanced.output = 'csvtest'; expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'}; expected.mqtt.base_topic = 'testtopic'; @@ -150,6 +150,7 @@ describe('Settings', () => { write(configurationFile, contentConfiguration); const expected = { + base_topic: 'zigbee2mqtt', include_device_information: false, force_disable_retain: false, password: "mysecretpassword", @@ -672,14 +673,6 @@ describe('Settings', () => { expect(settings.validate()).toEqual(expect.arrayContaining([error])); }); - it('Should ban devices', () => { - write(configurationFile, {}); - settings.banDevice('0x123'); - expect(settings.get().ban).toStrictEqual(['0x123']); - settings.banDevice('0x1234'); - expect(settings.get().ban).toStrictEqual(['0x123', '0x1234']); - }); - it('Should add devices to blocklist', () => { write(configurationFile, {}); settings.blockDevice('0x123'); @@ -842,4 +835,78 @@ describe('Settings', () => { const after = fs.statSync(configurationFile).mtimeMs; expect(before).toBe(after); }); + + it('Frontend config', () => { + write(configurationFile, {...minimalConfig, + frontend: true, + }); + + settings.reRead(); + expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false, host: '0.0.0.0'}) + }); + + it('Baudrate config', () => { + write(configurationFile, {...minimalConfig, + advanced: {baudrate: 20}, + }); + + settings.reRead(); + expect(settings.get().serial.baudrate).toStrictEqual(20) + }); + + it('ikea_ota_use_test_url config', () => { + write(configurationFile, {...minimalConfig, + advanced: {ikea_ota_use_test_url: true}, + }); + + settings.reRead(); + expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true) + }); + + 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', legacy_entity_attributes: true, legacy_triggers: true, 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']) + }); });