Restructure settings (#10437)

* -

* deep copy schema

* -

* -

* -

* -

* -

* -

* -

* -

* -

* -

* -

* -
This commit is contained in:
Koen Kanters 2022-01-09 21:28:44 +00:00 committed by GitHub
parent ee6b035108
commit 30177b0db4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 792 additions and 651 deletions

View File

@ -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);
}

View File

@ -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<void> {
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));

View File

@ -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));

View File

@ -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

View File

@ -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) {

View File

@ -65,16 +65,16 @@ export default class HomeAssistant extends Extension {
private discovered: {[s: string]:
{topics: Set<string>, mockProperties: Set<MockProperty>, objectIDs: Set<string>}} = {};
private discoveredTriggers : {[s: string]: Set<string>}= {};
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<void>,
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
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<void> {
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;

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -39,7 +39,7 @@ export default class OTAUpdate extends Extension {
override async start(): Promise<void> {
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();
}

View File

@ -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,

View File

@ -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};
}

View File

@ -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;
}

View File

@ -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;

77
lib/types/types.d.ts vendored
View File

@ -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,

View File

@ -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"],

View File

@ -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<Settings> = {
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<Settings> = {
},
},
},
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<Settings> = {
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<Settings>;
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<Settings>): void {
}
});
};
iterate(schema.properties, []);
iterate(schemaJson.properties, []);
}
function getInternalSettings(): Partial<Settings> {
@ -390,15 +411,7 @@ function getInternalSettings(): Partial<Settings> {
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<string, unknown>): 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`);

View File

@ -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

View File

@ -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<void> => {
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<boolean> {
// 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}'`);

File diff suppressed because one or more lines are too long

View File

@ -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();
});

View File

@ -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 = [];

View File

@ -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 () => {

View File

@ -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());

View File

@ -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'])
});
});