mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-20 02:21:20 +00:00
fix(ignore): partial cleanup for biome move (#27076)
This commit is contained in:
+1
-1
@@ -160,7 +160,7 @@ export class Controller {
|
||||
|
||||
this.eventBus.onLastSeenChanged(this, (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
|
||||
|
||||
logger.info(`Zigbee2MQTT started!`);
|
||||
logger.info('Zigbee2MQTT started!');
|
||||
|
||||
this.sdNotify = await initSdNotify();
|
||||
}
|
||||
|
||||
+11
-5
@@ -35,7 +35,7 @@ type EventBusListener<K> = K extends keyof EventBusMap
|
||||
: never;
|
||||
|
||||
export default class EventBus {
|
||||
private callbacksByExtension: {[s: string]: {event: keyof EventBusMap; callback: EventBusListener<keyof EventBusMap>}[]} = {};
|
||||
private callbacksByExtension: Map<string, {event: keyof EventBusMap; callback: EventBusListener<keyof EventBusMap>}[]> = new Map();
|
||||
private emitter = new events.EventEmitter<EventBusMap>();
|
||||
|
||||
constructor() {
|
||||
@@ -195,8 +195,8 @@ export default class EventBus {
|
||||
}
|
||||
|
||||
private on<K extends keyof EventBusMap>(event: K, callback: EventBusListener<K>, key: ListenerKey): void {
|
||||
if (!this.callbacksByExtension[key.constructor.name]) {
|
||||
this.callbacksByExtension[key.constructor.name] = [];
|
||||
if (!this.callbacksByExtension.has(key.constructor.name)) {
|
||||
this.callbacksByExtension.set(key.constructor.name, []);
|
||||
}
|
||||
|
||||
const wrappedCallback = async (...args: never[]): Promise<void> => {
|
||||
@@ -208,11 +208,17 @@ export default class EventBus {
|
||||
}
|
||||
};
|
||||
|
||||
this.callbacksByExtension[key.constructor.name].push({event, callback: wrappedCallback});
|
||||
this.callbacksByExtension.get(key.constructor.name)!.push({event, callback: wrappedCallback});
|
||||
this.emitter.on(event, wrappedCallback as EventBusListener<K>);
|
||||
}
|
||||
|
||||
public removeListeners(key: ListenerKey): void {
|
||||
this.callbacksByExtension[key.constructor.name]?.forEach((e) => this.emitter.removeListener(e.event, e.callback));
|
||||
const callbacks = this.callbacksByExtension.get(key.constructor.name);
|
||||
|
||||
if (callbacks) {
|
||||
for (const cb of callbacks) {
|
||||
this.emitter.removeListener(cb.event, cb.callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -1,3 +1,5 @@
|
||||
import type {ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from '../types/api';
|
||||
|
||||
import assert from 'node:assert';
|
||||
@@ -7,7 +9,6 @@ import debounce from 'debounce';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import {ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
@@ -224,8 +225,8 @@ export default class Bind extends Extension {
|
||||
let skipDisableReporting = false;
|
||||
const message = JSON.parse(data.message) as Zigbee2MQTTAPI['bridge/request/device/bind'];
|
||||
|
||||
if (typeof message !== 'object' || message.from == undefined || message.to == undefined) {
|
||||
return [message, {type, skipDisableReporting}, `Invalid payload`];
|
||||
if (typeof message !== 'object' || message.from == null || message.to == null) {
|
||||
return [message, {type, skipDisableReporting}, 'Invalid payload'];
|
||||
}
|
||||
|
||||
const sourceKey = message.from;
|
||||
@@ -233,7 +234,7 @@ export default class Bind extends Extension {
|
||||
const targetKey = message.to;
|
||||
const targetEndpointKey = message.to_endpoint;
|
||||
const clusters = message.clusters;
|
||||
skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false;
|
||||
skipDisableReporting = message.skip_disable_reporting != null ? message.skip_disable_reporting : false;
|
||||
const resolvedSource = this.zigbee.resolveEntity(message.from) as Device;
|
||||
|
||||
if (!resolvedSource || !(resolvedSource instanceof Device)) {
|
||||
@@ -289,9 +290,9 @@ export default class Bind extends Extension {
|
||||
},
|
||||
undefined,
|
||||
];
|
||||
} else {
|
||||
return [undefined, undefined, undefined];
|
||||
}
|
||||
|
||||
return [undefined, undefined, undefined];
|
||||
}
|
||||
|
||||
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
@@ -323,7 +324,7 @@ export default class Bind extends Extension {
|
||||
assert(resolvedSource, '`resolvedSource` is missing');
|
||||
assert(resolvedTarget, '`resolvedTarget` is missing');
|
||||
assert(resolvedSourceEndpoint, '`resolvedSourceEndpoint` is missing');
|
||||
assert(resolvedBindTarget != undefined, '`resolvedBindTarget` is missing');
|
||||
assert(resolvedBindTarget !== undefined, '`resolvedBindTarget` is missing');
|
||||
|
||||
const successfulClusters: string[] = [];
|
||||
const failedClusters = [];
|
||||
@@ -374,7 +375,9 @@ export default class Bind extends Extension {
|
||||
logger.error(`Nothing to ${type} from '${resolvedSource.name}' to '${resolvedTarget.name}'`);
|
||||
await this.publishResponse(parsed.type, raw, {}, `Nothing to ${type}`);
|
||||
return;
|
||||
} else if (failedClusters.length === attemptedClusters.length) {
|
||||
}
|
||||
|
||||
if (failedClusters.length === attemptedClusters.length) {
|
||||
await this.publishResponse(parsed.type, raw, {}, `Failed to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
+32
-27
@@ -1,3 +1,6 @@
|
||||
import type winston from 'winston';
|
||||
|
||||
import type Group from '../model/group';
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from '../types/api';
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -6,14 +9,12 @@ import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import JSZip from 'jszip';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import winston from 'winston';
|
||||
import Transport from 'winston-transport';
|
||||
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import data from '../util/data';
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
@@ -63,7 +64,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
if (payload !== this.lastBridgeLoggingPayload) {
|
||||
this.lastBridgeLoggingPayload = payload;
|
||||
void this.mqtt.publish(`bridge/logging`, payload, {}, baseTopic, true);
|
||||
void this.mqtt.publish('bridge/logging', payload, {}, baseTopic, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,7 +228,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
@bind async bridgeOptions(message: KeyValue | string): Promise<Zigbee2MQTTResponse<'bridge/response/options'>> {
|
||||
if (typeof message !== 'object' || typeof message.options !== 'object') {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const newSettings = message.options as Partial<Settings>;
|
||||
@@ -238,15 +239,15 @@ export default class Bridge extends Extension {
|
||||
await this.enableDisableExtension(settings.get().homeassistant.enabled, 'HomeAssistant');
|
||||
}
|
||||
|
||||
if (newSettings.advanced?.log_level != undefined) {
|
||||
if (newSettings.advanced?.log_level != null) {
|
||||
logger.setLevel(settings.get().advanced.log_level);
|
||||
}
|
||||
|
||||
if (newSettings.advanced?.log_namespaced_levels != undefined) {
|
||||
if (newSettings.advanced?.log_namespaced_levels != null) {
|
||||
logger.setNamespacedLevels(settings.get().advanced.log_namespaced_levels);
|
||||
}
|
||||
|
||||
if (newSettings.advanced?.log_debug_namespace_ignore != undefined) {
|
||||
if (newSettings.advanced?.log_debug_namespace_ignore != null) {
|
||||
logger.setDebugNamespaceIgnore(settings.get().advanced.log_debug_namespace_ignore);
|
||||
}
|
||||
|
||||
@@ -277,7 +278,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
@bind async groupAdd(message: string | KeyValue): Promise<Zigbee2MQTTResponse<'bridge/response/group/add'>> {
|
||||
if (typeof message === 'object' && message.friendly_name === undefined) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const friendlyName = typeof message === 'object' ? message.friendly_name : message;
|
||||
@@ -311,7 +312,11 @@ export default class Bridge extends Extension {
|
||||
.map((f) => [f, f.substring(dataPath.length + 1)])
|
||||
.filter((f) => !f[1].startsWith('log'));
|
||||
const zip = new JSZip();
|
||||
files.forEach((f) => zip.file(f[1], fs.readFileSync(f[0])));
|
||||
|
||||
for (const f of files) {
|
||||
zip.file(f[1], fs.readFileSync(f[0]));
|
||||
}
|
||||
|
||||
const base64Zip = await zip.generateAsync({type: 'base64'});
|
||||
return utils.getResponse(message, {zip: base64Zip});
|
||||
}
|
||||
@@ -392,10 +397,10 @@ export default class Bridge extends Extension {
|
||||
if (result) {
|
||||
logger.info('Successfully factory reset device through Touchlink');
|
||||
return utils.getResponse(message, payload);
|
||||
} else {
|
||||
logger.error('Failed to factory reset device through Touchlink');
|
||||
throw new Error('Failed to factory reset device through Touchlink');
|
||||
}
|
||||
|
||||
logger.error('Failed to factory reset device through Touchlink');
|
||||
throw new Error('Failed to factory reset device through Touchlink');
|
||||
}
|
||||
|
||||
@bind async touchlinkScan(message: KeyValue | string): Promise<Zigbee2MQTTResponse<'bridge/response/touchlink/scan'>> {
|
||||
@@ -417,7 +422,7 @@ export default class Bridge extends Extension {
|
||||
message: KeyValue | string,
|
||||
): Promise<Zigbee2MQTTResponse<T extends 'device' ? 'bridge/response/device/options' : 'bridge/response/group/options'>> {
|
||||
if (typeof message !== 'object' || message.id === undefined || message.options === undefined) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const cleanup = (o: KeyValue): KeyValue => {
|
||||
@@ -464,7 +469,7 @@ export default class Bridge extends Extension {
|
||||
message.reportable_change === undefined ||
|
||||
message.attribute === undefined
|
||||
) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const device = this.getEntity('device', message.id);
|
||||
@@ -507,7 +512,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
@bind async deviceInterview(message: string | KeyValue): Promise<Zigbee2MQTTResponse<'bridge/response/device/interview'>> {
|
||||
if (typeof message !== 'object' || message.id === undefined) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const device = this.getEntity('device', message.id);
|
||||
@@ -532,7 +537,7 @@ export default class Bridge extends Extension {
|
||||
message: string | KeyValue,
|
||||
): Promise<Zigbee2MQTTResponse<'bridge/response/device/generate_external_definition'>> {
|
||||
if (typeof message !== 'object' || message.id === undefined) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const device = this.getEntity('device', message.id);
|
||||
@@ -548,7 +553,7 @@ export default class Bridge extends Extension {
|
||||
const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true;
|
||||
|
||||
if (typeof message !== 'object' || (message.from === undefined && !deviceAndHasLast) || message.to === undefined) {
|
||||
throw new Error(`Invalid payload`);
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
if (deviceAndHasLast && !this.lastJoinedDeviceIeeeAddr) {
|
||||
@@ -648,17 +653,17 @@ export default class Bridge extends Extension {
|
||||
const responseData: Zigbee2MQTTAPI['bridge/response/device/remove'] = {id: ID, block, force};
|
||||
|
||||
return utils.getResponse(message, responseData);
|
||||
} else {
|
||||
await this.publishGroups();
|
||||
|
||||
const responseData: Zigbee2MQTTAPI['bridge/response/group/remove'] = {id: ID, force};
|
||||
|
||||
return utils.getResponse(
|
||||
message,
|
||||
// @ts-expect-error typing infer does not work here
|
||||
responseData,
|
||||
);
|
||||
}
|
||||
|
||||
await this.publishGroups();
|
||||
|
||||
const responseData: Zigbee2MQTTAPI['bridge/response/group/remove'] = {id: ID, force};
|
||||
|
||||
return utils.getResponse(
|
||||
message,
|
||||
// @ts-expect-error typing infer does not work here
|
||||
responseData,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class Configure extends Extension {
|
||||
let error: string | undefined;
|
||||
|
||||
if (ID === undefined) {
|
||||
error = `Invalid payload`;
|
||||
error = 'Invalid payload';
|
||||
} else {
|
||||
const device = this.zigbee.resolveEntity(ID);
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class Configure extends Extension {
|
||||
|
||||
const response = utils.getResponse<'bridge/response/device/configure'>(message, {id: ID}, error);
|
||||
|
||||
await this.mqtt.publish(`bridge/response/device/configure`, stringify(response));
|
||||
await this.mqtt.publish('bridge/response/device/configure', stringify(response));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class ExternalConverters extends ExternalJSExtension<TModule> {
|
||||
`Failed to load external converter '${newName ?? name}'. Check the code for syntax error and make sure it is up to date with the current Zigbee2MQTT version.`,
|
||||
);
|
||||
logger.warning(
|
||||
`External converters are not meant for long term usage, but for local testing after which a pull request should be created to add out-of-the-box support for the device`,
|
||||
'External converters are not meant for long term usage, but for local testing after which a pull request should be created to add out-of-the-box support for the device',
|
||||
);
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -96,7 +96,7 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
const message = utils.parseJSON(data.message, data.message);
|
||||
|
||||
try {
|
||||
let response;
|
||||
let response: Awaited<ReturnType<typeof this.save | typeof this.remove>>;
|
||||
|
||||
if (match[1].toLowerCase() === 'save') {
|
||||
response = await this.save(
|
||||
@@ -127,7 +127,7 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
message: Zigbee2MQTTAPI['bridge/request/converter/remove'] | Zigbee2MQTTAPI['bridge/request/extension/remove'],
|
||||
): Promise<Zigbee2MQTTResponse<'bridge/response/converter/remove' | 'bridge/response/extension/remove'>> {
|
||||
if (!message.name) {
|
||||
return utils.getResponse(message, {}, `Invalid payload`);
|
||||
return utils.getResponse(message, {}, 'Invalid payload');
|
||||
}
|
||||
|
||||
const {name} = message;
|
||||
@@ -144,16 +144,16 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
await this.publishExternalJS();
|
||||
|
||||
return utils.getResponse(message, {});
|
||||
} else {
|
||||
return utils.getResponse(message, {}, `${name} (${srcToBeRemoved}) doesn't exists`);
|
||||
}
|
||||
|
||||
return utils.getResponse(message, {}, `${name} (${srcToBeRemoved}) doesn't exists`);
|
||||
}
|
||||
|
||||
@bind private async save(
|
||||
message: Zigbee2MQTTAPI['bridge/request/converter/save'] | Zigbee2MQTTAPI['bridge/request/extension/save'],
|
||||
): Promise<Zigbee2MQTTResponse<'bridge/response/converter/save' | 'bridge/response/extension/save'>> {
|
||||
if (!message.name || !message.code) {
|
||||
return utils.getResponse(message, {}, `Invalid payload`);
|
||||
return utils.getResponse(message, {}, 'Invalid payload');
|
||||
}
|
||||
|
||||
const {name, code} = message;
|
||||
@@ -165,7 +165,7 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
const versionMatch = name.match(/\.(\d+)\.(c|m)?js$/);
|
||||
|
||||
if (versionMatch) {
|
||||
const version = parseInt(versionMatch[1], 10);
|
||||
const version = Number.parseInt(versionMatch[1], 10);
|
||||
newName = name.replace(`.${version}.`, `.${version + 1}.`);
|
||||
} else {
|
||||
const ext = path.extname(name);
|
||||
|
||||
@@ -123,11 +123,15 @@ export class Frontend extends Extension {
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
await super.stop();
|
||||
this.wss?.clients.forEach((client) => {
|
||||
client.send(stringify({topic: 'bridge/state', payload: {state: 'offline'}}));
|
||||
client.terminate();
|
||||
});
|
||||
this.wss?.close();
|
||||
|
||||
if (this.wss) {
|
||||
for (const client of this.wss.clients) {
|
||||
client.send(stringify({topic: 'bridge/state', payload: {state: 'offline'}}));
|
||||
client.terminate();
|
||||
}
|
||||
|
||||
this.wss.close();
|
||||
}
|
||||
|
||||
await new Promise((resolve) => this.server?.close(resolve));
|
||||
}
|
||||
@@ -138,13 +142,15 @@ export class Frontend extends Extension {
|
||||
|
||||
// The request url is not within the frontend base url, so the relative path starts with '..'
|
||||
if (newUrl.startsWith('.')) {
|
||||
return fin();
|
||||
fin();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach originalUrl so that static-server can perform a redirect to '/' when serving the root directory.
|
||||
// This is necessary for the browser to resolve relative assets paths correctly.
|
||||
request.originalUrl = request.url;
|
||||
request.url = '/' + newUrl;
|
||||
request.url = `/${newUrl}`;
|
||||
request.path = request.url;
|
||||
|
||||
if (newUrl.startsWith('device_icons/')) {
|
||||
|
||||
+15
-13
@@ -1,3 +1,5 @@
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from '../types/api';
|
||||
|
||||
import assert from 'node:assert';
|
||||
@@ -6,8 +8,6 @@ import bind from 'bind-decorator';
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import logger from '../util/logger';
|
||||
@@ -81,7 +81,7 @@ export default class Groups extends Extension {
|
||||
const groups = [];
|
||||
|
||||
for (const group of this.zigbee.groupsIterator()) {
|
||||
if (group.options && (group.options.optimistic == undefined || group.options.optimistic)) {
|
||||
if (group.options && (group.options.optimistic == null || group.options.optimistic)) {
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
@@ -191,30 +191,32 @@ export default class Groups extends Extension {
|
||||
|
||||
if (topicRegexMatch) {
|
||||
const type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all';
|
||||
let resolvedGroup;
|
||||
let groupKey;
|
||||
let resolvedGroup: Group | undefined;
|
||||
let groupKey: string | undefined;
|
||||
let skipDisableReporting = false;
|
||||
const message = JSON.parse(data.message) as Zigbee2MQTTAPI['bridge/request/group/members/add'];
|
||||
|
||||
if (typeof message !== 'object' || message.device == undefined) {
|
||||
if (typeof message !== 'object' || message.device == null) {
|
||||
return [message, {type, skipDisableReporting}, 'Invalid payload'];
|
||||
}
|
||||
|
||||
const deviceKey = message.device;
|
||||
skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false;
|
||||
skipDisableReporting = message.skip_disable_reporting != null ? message.skip_disable_reporting : false;
|
||||
|
||||
if (type !== 'remove_all') {
|
||||
groupKey = message.group;
|
||||
|
||||
if (message.group == undefined) {
|
||||
return [message, {type, skipDisableReporting}, `Invalid payload`];
|
||||
if (message.group == null) {
|
||||
return [message, {type, skipDisableReporting}, 'Invalid payload'];
|
||||
}
|
||||
|
||||
resolvedGroup = this.zigbee.resolveEntity(message.group);
|
||||
const group = this.zigbee.resolveEntity(message.group);
|
||||
|
||||
if (!resolvedGroup || !(resolvedGroup instanceof Group)) {
|
||||
if (!group || !(group instanceof Group)) {
|
||||
return [message, {type, skipDisableReporting}, `Group '${message.group}' does not exist`];
|
||||
}
|
||||
|
||||
resolvedGroup = group;
|
||||
}
|
||||
|
||||
const resolvedDevice = this.zigbee.resolveEntity(message.device);
|
||||
@@ -244,9 +246,9 @@ export default class Groups extends Extension {
|
||||
},
|
||||
undefined,
|
||||
];
|
||||
} else {
|
||||
return [undefined, undefined, undefined];
|
||||
}
|
||||
|
||||
return [undefined, undefined, undefined];
|
||||
}
|
||||
|
||||
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
|
||||
@@ -293,9 +293,9 @@ const LIST_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
|
||||
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
|
||||
if (feature.endpoint) {
|
||||
return feature.property.slice(0, -1 + -1 * feature.endpoint.length);
|
||||
} else {
|
||||
return feature.property;
|
||||
}
|
||||
|
||||
return feature.property;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -337,7 +337,7 @@ class Bridge {
|
||||
this.options = {
|
||||
ID: `bridge_${ieeeAdress}`,
|
||||
homeassistant: {
|
||||
name: `Zigbee2MQTT Bridge`,
|
||||
name: 'Zigbee2MQTT Bridge',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -357,7 +357,7 @@ export class HomeAssistant extends Extension {
|
||||
private discovered: {[s: string]: Discovered} = {};
|
||||
private discoveryTopic: string;
|
||||
private discoveryRegex: RegExp;
|
||||
private discoveryRegexWoTopic = new RegExp(`(.*)/(.*)/(.*)/config`);
|
||||
private discoveryRegexWoTopic = /(.*)\/(.*)\/(.*)\/config/;
|
||||
private statusTopic: string;
|
||||
private legacyActionSensor: boolean;
|
||||
private experimentalEventEntities: boolean;
|
||||
@@ -442,7 +442,7 @@ export class HomeAssistant extends Extension {
|
||||
await this.mqtt.subscribe(`${this.discoveryTopic}/#`);
|
||||
setTimeout(async () => {
|
||||
await this.mqtt.unsubscribe(`${this.discoveryTopic}/#`);
|
||||
logger.debug(`Discovering entities to Home Assistant`);
|
||||
logger.debug('Discovering entities to Home Assistant');
|
||||
|
||||
await this.discover(this.bridge);
|
||||
|
||||
@@ -630,10 +630,7 @@ export class HomeAssistant extends Extension {
|
||||
if (state) {
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template =
|
||||
`{% set values = ` +
|
||||
`{None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.${state.property}] }}`;
|
||||
}
|
||||
|
||||
const coolingSetpoint = (firstExpose as zhc.Climate).features.find((f) => f.name === 'occupied_cooling_setpoint');
|
||||
@@ -780,10 +777,7 @@ export class HomeAssistant extends Extension {
|
||||
// The movement direction is calculated (assumed) in this case.
|
||||
if (running) {
|
||||
assert(position, `Cover must have 'position' when it has 'running'`);
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{% if "${featurePropertyWithoutEndpoint(running)}" in value_json ` +
|
||||
`and value_json.${featurePropertyWithoutEndpoint(running)} %} {% if value_json.${featurePropertyWithoutEndpoint(position)} > 0 %} closing ` +
|
||||
`{% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json.${featurePropertyWithoutEndpoint(running)} %} {% if value_json.${featurePropertyWithoutEndpoint(position)} > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
}
|
||||
|
||||
// If curtains have `motor_state` or `moving` property, lookup for possible
|
||||
@@ -797,10 +791,7 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
discoveryEntry.discovery_payload.state_closing = closingState;
|
||||
discoveryEntry.discovery_payload.state_stopped = stoppedState;
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json ` +
|
||||
`and value_json.${featurePropertyWithoutEndpoint(motorState)} %} {{ value_json.${featurePropertyWithoutEndpoint(motorState)} }} {% else %} ` +
|
||||
`${stoppedState} {% endif %}`;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json.${featurePropertyWithoutEndpoint(motorState)} %} {{ value_json.${featurePropertyWithoutEndpoint(motorState)} }} {% else %} ${stoppedState} {% endif %}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,7 +832,7 @@ export class HomeAssistant extends Extension {
|
||||
break;
|
||||
}
|
||||
case 'fan': {
|
||||
assert(!endpoint, `Endpoint not supported for fan type`);
|
||||
assert(!endpoint, 'Endpoint not supported for fan type');
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'fan',
|
||||
object_id: 'fan',
|
||||
@@ -857,7 +848,7 @@ export class HomeAssistant extends Extension {
|
||||
const nativeSpeed = (firstExpose as zhc.Fan).features.filter(isNumericExpose).find((e) => e.name === 'speed');
|
||||
|
||||
// Exactly one mode needs to be active (logical xor)
|
||||
assert(!modeEmulatedSpeed != !nativeSpeed, 'Fans need to be either mode- or speed-controlled');
|
||||
assert(!modeEmulatedSpeed !== !nativeSpeed, 'Fans need to be either mode- or speed-controlled');
|
||||
|
||||
if (modeEmulatedSpeed) {
|
||||
// A fan entity in Home Assistant 2021.3 and above may have a speed,
|
||||
@@ -887,7 +878,11 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const allowed = [...speeds, ...presets];
|
||||
modeEmulatedSpeed.values.forEach((s) => assert(allowed.includes(s.toString())));
|
||||
|
||||
for (const val of modeEmulatedSpeed.values) {
|
||||
assert(allowed.includes(val.toString()));
|
||||
}
|
||||
|
||||
const percentValues = speeds.map((s, i) => `'${s}':${i}`).join(', ');
|
||||
const percentCommands = speeds.map((s, i) => `${i}:'${s}'`).join(', ');
|
||||
const presetList = presets.map((s) => `'${s}'`).join(', ');
|
||||
@@ -1069,7 +1064,7 @@ export class HomeAssistant extends Extension {
|
||||
this.experimentalEventEntities &&
|
||||
firstExpose.access & ACCESS_STATE &&
|
||||
!(firstExpose.access & ACCESS_SET) &&
|
||||
firstExpose.property == 'action'
|
||||
firstExpose.property === 'action'
|
||||
) {
|
||||
discoveryEntries.push({
|
||||
type: 'event',
|
||||
@@ -1196,30 +1191,30 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
// Exposes with category 'config' or 'diagnostic' are always added to the respective category.
|
||||
// This takes precedence over definitions in this file.
|
||||
if (firstExpose.category === 'config') {
|
||||
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'config'));
|
||||
} else if (firstExpose.category === 'diagnostic') {
|
||||
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'diagnostic'));
|
||||
if (firstExpose.category === 'config' || firstExpose.category === 'diagnostic') {
|
||||
for (const entry of discoveryEntries) {
|
||||
entry.discovery_payload.entity_category = firstExpose.category;
|
||||
}
|
||||
}
|
||||
|
||||
discoveryEntries.forEach((d) => {
|
||||
for (const entry of discoveryEntries) {
|
||||
// If a sensor has entity category `config`, then change
|
||||
// it to `diagnostic`. Sensors have no input, so can't be configured.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/pull/19474
|
||||
if (['binary_sensor', 'sensor'].includes(d.type) && d.discovery_payload.entity_category === 'config') {
|
||||
d.discovery_payload.entity_category = 'diagnostic';
|
||||
if (['binary_sensor', 'sensor'].includes(entry.type) && entry.discovery_payload.entity_category === 'config') {
|
||||
entry.discovery_payload.entity_category = 'diagnostic';
|
||||
}
|
||||
|
||||
// Event entities cannot have an entity_category set.
|
||||
if (d.type === 'event' && d.discovery_payload.entity_category) {
|
||||
delete d.discovery_payload.entity_category;
|
||||
if (entry.type === 'event' && entry.discovery_payload.entity_category) {
|
||||
delete entry.discovery_payload.entity_category;
|
||||
}
|
||||
|
||||
// Let Home Assistant generate entity name when device_class is present
|
||||
if (d.discovery_payload.device_class) {
|
||||
delete d.discovery_payload.name;
|
||||
if (entry.discovery_payload.device_class) {
|
||||
delete entry.discovery_payload.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return discoveryEntries;
|
||||
}
|
||||
@@ -1298,7 +1293,7 @@ export class HomeAssistant extends Extension {
|
||||
* and republish it to zigbee2mqtt/my_device/action
|
||||
*/
|
||||
if (settings.get().advanced.output === 'json' && entity.isDevice() && entity.definition && data.message.action) {
|
||||
const value = data.message['action'].toString();
|
||||
const value = data.message.action.toString();
|
||||
await this.publishDeviceTriggerDiscover(entity, 'action', value);
|
||||
await this.mqtt.publish(`${data.entity.name}/action`, value, {});
|
||||
}
|
||||
@@ -1420,9 +1415,8 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
// Discover scenes.
|
||||
const endpointsOrGroups = isDevice ? entity.zh.endpoints : isGroup ? [entity.zh] : [];
|
||||
endpointsOrGroups.forEach((endpointOrGroup) => {
|
||||
utils.getScenes(endpointOrGroup).forEach((scene) => {
|
||||
for (const endpointOrGroup of isDevice ? entity.zh.endpoints : isGroup ? [entity.zh] : []) {
|
||||
for (const scene of utils.getScenes(endpointOrGroup)) {
|
||||
const sceneEntry: DiscoveryEntry = {
|
||||
type: 'scene',
|
||||
object_id: `scene_${scene.id}`,
|
||||
@@ -1437,8 +1431,8 @@ export class HomeAssistant extends Extension {
|
||||
};
|
||||
|
||||
configs.push(sceneEntry);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// deep clone of the config objects
|
||||
configs = JSON.parse(JSON.stringify(configs));
|
||||
@@ -1446,26 +1440,29 @@ export class HomeAssistant extends Extension {
|
||||
if (entity.options.homeassistant) {
|
||||
const s = entity.options.homeassistant;
|
||||
configs = configs.filter((config) => s[config.object_id] === undefined || s[config.object_id] != null);
|
||||
configs.forEach((config) => {
|
||||
|
||||
for (const config of configs) {
|
||||
const configOverride = s[config.object_id];
|
||||
if (configOverride) {
|
||||
config.object_id = configOverride.object_id || config.object_id;
|
||||
config.type = configOverride.type || config.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
private async discover(entity: Device | Group | Bridge, publish: boolean = true): Promise<void> {
|
||||
private async discover(entity: Device | Group | Bridge, publish = true): Promise<void> {
|
||||
// Handle type differences.
|
||||
const isDevice = entity.isDevice();
|
||||
const isGroup = entity.isGroup();
|
||||
|
||||
if (isGroup && entity.zh.members.length === 0) {
|
||||
return;
|
||||
} else if (
|
||||
}
|
||||
|
||||
if (
|
||||
isDevice &&
|
||||
(!entity.definition || entity.zh.interviewing || (entity.options.homeassistant !== undefined && !entity.options.homeassistant))
|
||||
) {
|
||||
@@ -1540,9 +1537,13 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
if (isDevice && entity.options.disabled) {
|
||||
// Mark disabled device always as unavailable
|
||||
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ "offline" }}'));
|
||||
for (const entry of payload.availability) {
|
||||
entry.value_template = '{{ "offline" }}';
|
||||
}
|
||||
} else {
|
||||
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ value_json.state }}'));
|
||||
for (const entry of payload.availability) {
|
||||
entry.value_template = '{{ value_json.state }}';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete payload.availability;
|
||||
@@ -1631,7 +1632,7 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.preset_mode_command_topic) {
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/` + payload.preset_mode_command_topic;
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.preset_mode_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.action_topic) {
|
||||
@@ -1639,28 +1640,32 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
// Override configuration with user settings.
|
||||
if (entity.options.homeassistant != undefined) {
|
||||
if (entity.options.homeassistant != null) {
|
||||
const add = (obj: KeyValue, ignoreName: boolean): void => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (['type', 'object_id'].includes(key)) {
|
||||
return;
|
||||
} else if (ignoreName && key === 'name') {
|
||||
return;
|
||||
} else if (['number', 'string', 'boolean'].includes(typeof obj[key]) || Array.isArray(obj[key])) {
|
||||
for (const key in obj) {
|
||||
if (key === 'type' || key === 'object_id') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoreName && key === 'name') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (['number', 'string', 'boolean'].includes(typeof obj[key]) || Array.isArray(obj[key])) {
|
||||
payload[key] = obj[key];
|
||||
} else if (obj[key] === null) {
|
||||
delete payload[key];
|
||||
} else if (key === 'device' && typeof obj[key] === 'object') {
|
||||
Object.keys(obj['device']).forEach((key) => {
|
||||
payload['device'][key] = obj['device'][key];
|
||||
});
|
||||
for (const devKey in obj.device) {
|
||||
payload.device[devKey] = obj.device[devKey];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
add(entity.options.homeassistant, true);
|
||||
|
||||
if (entity.options.homeassistant[config.object_id] != undefined) {
|
||||
if (entity.options.homeassistant[config.object_id] != null) {
|
||||
add(entity.options.homeassistant[config.object_id], false);
|
||||
}
|
||||
}
|
||||
@@ -1683,7 +1688,12 @@ export class HomeAssistant extends Extension {
|
||||
} else {
|
||||
logger.debug(`Skipping discovery of '${topic}', already discovered`);
|
||||
}
|
||||
config.mockProperties?.forEach((mockProperty) => discovered.mockProperties.add(mockProperty));
|
||||
|
||||
if (config.mockProperties) {
|
||||
for (const mockProperty of config.mockProperties) {
|
||||
discovered.mockProperties.add(mockProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const topic of lastDiscoveredTopics) {
|
||||
@@ -1703,7 +1713,7 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
try {
|
||||
message = JSON.parse(data.message);
|
||||
const baseTopic = settings.get().mqtt.base_topic + '/';
|
||||
const baseTopic = `${settings.get().mqtt.base_topic}/`;
|
||||
if (isDeviceAutomation && (!message.topic || !message.topic.startsWith(baseTopic))) {
|
||||
return;
|
||||
}
|
||||
@@ -1779,11 +1789,11 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
// Make sure Home Assistant deletes the old entity first otherwise another one (_2) is created
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/12610
|
||||
logger.debug(`Finished clearing scene discovery topics, waiting for Home Assistant.`);
|
||||
logger.debug('Finished clearing scene discovery topics, waiting for Home Assistant.');
|
||||
await utils.sleep(2);
|
||||
|
||||
// Re-discover entity (including any new scenes).
|
||||
logger.debug(`Re-discovering entities with their scenes.`);
|
||||
logger.debug('Re-discovering entities with their scenes.');
|
||||
await this.discover(data.entity);
|
||||
}
|
||||
|
||||
@@ -1836,11 +1846,11 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
override adjustMessageBeforePublish(entity: Device | Group | Bridge, message: KeyValue): void {
|
||||
this.getDiscovered(entity).mockProperties.forEach((mockProperty) => {
|
||||
for (const mockProperty of this.getDiscovered(entity).mockProperties) {
|
||||
if (message[mockProperty.property] === undefined) {
|
||||
message[mockProperty.property] = mockProperty.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy hue -> h, saturation -> s to make homeassistant happy
|
||||
if (message.color !== undefined) {
|
||||
@@ -2059,13 +2069,13 @@ export class HomeAssistant extends Extension {
|
||||
// Handle wildcard actions.
|
||||
let m = action.match(/^(?<action>recall|scene)_\*(?:_(?<endpoint>e1|e2|s1|s2))?$/);
|
||||
if (m?.groups?.action) {
|
||||
logger.debug('Found scene wildcard action ' + m.groups.action);
|
||||
logger.debug(`Found scene wildcard action ${m.groups.action}`);
|
||||
return this.buildAction(m.groups, {scene: 'wildcard'});
|
||||
}
|
||||
|
||||
m = action.match(/^(?<actionPrefix>region_)\*_(?<action>enter|leave|occupied|unoccupied)$/);
|
||||
if (m?.groups?.action) {
|
||||
logger.debug('Found region wildcard action ' + m.groups.action);
|
||||
logger.debug(`Found region wildcard action ${m.groups.action}`);
|
||||
return this.buildAction(m.groups, {region: 'wildcard'});
|
||||
}
|
||||
|
||||
@@ -2100,22 +2110,21 @@ export class HomeAssistant extends Extension {
|
||||
.join(', ')}]}`;
|
||||
}).join(',\n');
|
||||
|
||||
const value_template =
|
||||
`{% set patterns = [\n${patterns}\n] %}\n` +
|
||||
`{% set action_value = value_json.action|default('') %}\n` +
|
||||
`{% set ns = namespace(r=[('action', action_value)]) %}\n` +
|
||||
`{% for p in patterns %}\n` +
|
||||
` {% set m = action_value|regex_findall(p.pattern) %}\n` +
|
||||
` {% if m[0] is undefined %}{% continue %}{% endif %}\n` +
|
||||
` {% for key, value in zip(p.groups, m[0]) %}\n` +
|
||||
` {% set ns.r = ns.r|rejectattr(0, 'eq', key)|list + [(key, value)] %}\n` +
|
||||
` {% endfor %}\n` +
|
||||
`{% endfor %}\n` +
|
||||
`{% if (ns.r|selectattr(0, 'eq', 'actionPrefix')|first) is defined %}\n` +
|
||||
` {% set ns.r = ns.r|rejectattr(0, 'eq', 'action')|list + [('action', ns.r|selectattr(0, 'eq', 'actionPrefix')|map(attribute=1)|first + ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}\n` +
|
||||
`{% endif %}\n` +
|
||||
`{% set ns.r = ns.r + [('event_type', ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}\n` +
|
||||
`{{dict.from_keys(ns.r|rejectattr(0, 'in', ('action', 'actionPrefix'))|reject('eq', ('event_type', None))|reject('eq', ('event_type', '')))|to_json}}`;
|
||||
const value_template = `{% set patterns = [\n${patterns}\n] %}
|
||||
{% set action_value = value_json.action|default('') %}
|
||||
{% set ns = namespace(r=[('action', action_value)]) %}
|
||||
{% for p in patterns %}
|
||||
{% set m = action_value|regex_findall(p.pattern) %}
|
||||
{% if m[0] is undefined %}{% continue %}{% endif %}
|
||||
{% for key, value in zip(p.groups, m[0]) %}
|
||||
{% set ns.r = ns.r|rejectattr(0, 'eq', key)|list + [(key, value)] %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if (ns.r|selectattr(0, 'eq', 'actionPrefix')|first) is defined %}
|
||||
{% set ns.r = ns.r|rejectattr(0, 'eq', 'action')|list + [('action', ns.r|selectattr(0, 'eq', 'actionPrefix')|map(attribute=1)|first + ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}
|
||||
{% endif %}
|
||||
{% set ns.r = ns.r + [('event_type', ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}
|
||||
{{dict.from_keys(ns.r|rejectattr(0, 'in', ('action', 'actionPrefix'))|reject('eq', ('event_type', None))|reject('eq', ('event_type', '')))|to_json}}`;
|
||||
|
||||
return value_template;
|
||||
}
|
||||
|
||||
+51
-50
@@ -67,7 +67,7 @@ export default class NetworkMap extends Extension {
|
||||
let text = 'digraph G {\nnode[shape=record];\n';
|
||||
let style = '';
|
||||
|
||||
topology.nodes.forEach((node) => {
|
||||
for (const node of topology.nodes) {
|
||||
const labels = [];
|
||||
|
||||
// Add friendly name
|
||||
@@ -75,8 +75,7 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
labels.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})` +
|
||||
(node.failed && node.failed.length ? `failed: ${node.failed.join(',')}` : ''),
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})${node.failed?.length ? `failed: ${node.failed.join(',')}` : ''}`,
|
||||
);
|
||||
|
||||
// Add the device model
|
||||
@@ -94,9 +93,9 @@ export default class NetworkMap extends Extension {
|
||||
labels.push(lastSeen);
|
||||
|
||||
// Shape the record according to device type
|
||||
if (node.type == 'Coordinator') {
|
||||
if (node.type === 'Coordinator') {
|
||||
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", fontcolor="${colors.font.coordinator}"`;
|
||||
} else if (node.type == 'Router') {
|
||||
} else if (node.type === 'Router') {
|
||||
style = `style="rounded, filled", fillcolor="${colors.fill.router}", fontcolor="${colors.font.router}"`;
|
||||
} else {
|
||||
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", fontcolor="${colors.font.enddevice}"`;
|
||||
@@ -110,17 +109,21 @@ export default class NetworkMap extends Extension {
|
||||
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
|
||||
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
|
||||
*/
|
||||
topology.links
|
||||
.filter((e) => e.source.ieeeAddr === node.ieeeAddr)
|
||||
.forEach((e) => {
|
||||
const lineStyle = node.type == 'EndDevice' ? 'penwidth=1, ' : !e.routes.length ? 'penwidth=0.5, ' : 'penwidth=2, ';
|
||||
const lineWeight = !e.routes.length ? `weight=0, color="${colors.line.inactive}", ` : `weight=1, color="${colors.line.active}", `;
|
||||
const textRoutes = e.routes.map((r) => utils.toNetworkAddressHex(r.destinationAddress));
|
||||
const lineLabels = !e.routes.length ? `label="${e.linkquality}"` : `label="${e.linkquality} (routes: ${textRoutes.join(',')})"`;
|
||||
text += ` "${node.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
|
||||
for (const link of topology.links) {
|
||||
if (link.source.ieeeAddr === node.ieeeAddr) {
|
||||
const lineStyle = node.type === 'EndDevice' ? 'penwidth=1, ' : !link.routes.length ? 'penwidth=0.5, ' : 'penwidth=2, ';
|
||||
const lineWeight = !link.routes.length
|
||||
? `weight=0, color="${colors.line.inactive}", `
|
||||
: `weight=1, color="${colors.line.active}", `;
|
||||
const textRoutes = link.routes.map((r) => utils.toNetworkAddressHex(r.destinationAddress));
|
||||
const lineLabels = !link.routes.length
|
||||
? `label="${link.linkquality}"`
|
||||
: `label="${link.linkquality} (routes: ${textRoutes.join(',')})"`;
|
||||
text += ` "${node.ieeeAddr}" -> "${link.target.ieeeAddr}"`;
|
||||
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text += '}';
|
||||
|
||||
@@ -131,55 +134,53 @@ export default class NetworkMap extends Extension {
|
||||
const text = [];
|
||||
|
||||
text.push(`' paste into: https://www.planttext.com/`);
|
||||
text.push(``);
|
||||
text.push('');
|
||||
text.push('@startuml');
|
||||
|
||||
topology.nodes
|
||||
.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName))
|
||||
.forEach((node) => {
|
||||
// Add friendly name
|
||||
text.push(`card ${node.ieeeAddr} [`);
|
||||
text.push(`${node.friendlyName}`);
|
||||
text.push(`---`);
|
||||
for (const node of topology.nodes.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName))) {
|
||||
// Add friendly name
|
||||
text.push(`card ${node.ieeeAddr} [`);
|
||||
text.push(`${node.friendlyName}`);
|
||||
text.push('---');
|
||||
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
text.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})` +
|
||||
(node.failed && node.failed.length ? ` failed: ${node.failed.join(',')}` : ''),
|
||||
);
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
text.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})${node.failed?.length ? ` failed: ${node.failed.join(',')}` : ''}`,
|
||||
);
|
||||
|
||||
// Add the device model
|
||||
if (node.type !== 'Coordinator') {
|
||||
text.push(`---`);
|
||||
const definition = (this.zigbee.resolveEntity(node.ieeeAddr) as Device).definition;
|
||||
text.push(`${definition?.vendor} ${definition?.description} (${definition?.model})`);
|
||||
}
|
||||
// Add the device model
|
||||
if (node.type !== 'Coordinator') {
|
||||
text.push('---');
|
||||
const definition = (this.zigbee.resolveEntity(node.ieeeAddr) as Device).definition;
|
||||
text.push(`${definition?.vendor} ${definition?.description} (${definition?.model})`);
|
||||
}
|
||||
|
||||
// Add the device last_seen timestamp
|
||||
let lastSeen = 'unknown';
|
||||
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
|
||||
if (date) {
|
||||
lastSeen = utils.formatDate(date, 'relative') as string;
|
||||
}
|
||||
text.push(`---`);
|
||||
text.push(lastSeen);
|
||||
text.push(`]`);
|
||||
text.push(``);
|
||||
});
|
||||
// Add the device last_seen timestamp
|
||||
let lastSeen = 'unknown';
|
||||
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
|
||||
if (date) {
|
||||
lastSeen = utils.formatDate(date, 'relative') as string;
|
||||
}
|
||||
text.push('---');
|
||||
text.push(lastSeen);
|
||||
text.push(']');
|
||||
text.push('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add edges between the devices
|
||||
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
|
||||
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
|
||||
*/
|
||||
topology.links.forEach((link) => {
|
||||
for (const link of topology.links) {
|
||||
text.push(`${link.sourceIeeeAddr} --> ${link.targetIeeeAddr}: ${link.lqi}`);
|
||||
});
|
||||
}
|
||||
|
||||
text.push('');
|
||||
|
||||
text.push(`@enduml`);
|
||||
text.push('@enduml');
|
||||
|
||||
return text.join(`\n`);
|
||||
return text.join('\n');
|
||||
}
|
||||
|
||||
async networkScan(includeRoutes: boolean): Promise<Zigbee2MQTTNetworkMap> {
|
||||
@@ -230,7 +231,7 @@ export default class NetworkMap extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Network scan finished`);
|
||||
logger.info('Network scan finished');
|
||||
|
||||
const topology: Zigbee2MQTTNetworkMap = {nodes: [], links: []};
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ export default class OTAUpdate extends Extension {
|
||||
| Zigbee2MQTTAPI['bridge/request/device/ota_update/check/downgrade']
|
||||
| Zigbee2MQTTAPI['bridge/request/device/ota_update/update']
|
||||
| Zigbee2MQTTAPI['bridge/request/device/ota_update/update/downgrade'];
|
||||
const ID = (typeof message === 'object' && message['id'] !== undefined ? message.id : message) as string;
|
||||
const ID = (typeof message === 'object' && message.id !== undefined ? message.id : message) as string;
|
||||
const device = this.zigbee.resolveEntity(ID);
|
||||
const type = topicMatch[1];
|
||||
const downgrade = Boolean(topicMatch[2]);
|
||||
@@ -209,7 +209,7 @@ export default class OTAUpdate extends Extension {
|
||||
update_available: availableResult.available,
|
||||
});
|
||||
|
||||
await this.mqtt.publish(`bridge/response/device/ota_update/check`, stringify(response));
|
||||
await this.mqtt.publish('bridge/response/device/ota_update/check', stringify(response));
|
||||
} catch (e) {
|
||||
error = `Failed to check if update available for '${device.name}' (${(e as Error).message})`;
|
||||
errorStack = (e as Error).stack;
|
||||
@@ -257,7 +257,7 @@ export default class OTAUpdate extends Extension {
|
||||
to: firmwareTo ? {software_build_id: firmwareTo.softwareBuildID, date_code: firmwareTo.dateCode} : undefined,
|
||||
});
|
||||
|
||||
await this.mqtt.publish(`bridge/response/device/ota_update/update`, stringify(response));
|
||||
await this.mqtt.publish('bridge/response/device/ota_update/update', stringify(response));
|
||||
} catch (e) {
|
||||
logger.debug(`Update of '${device.name}' failed (${e})`);
|
||||
error = `Update of '${device.name}' failed (${(e as Error).message})`;
|
||||
|
||||
@@ -67,11 +67,7 @@ export default class Publish extends Extension {
|
||||
try {
|
||||
return JSON.parse(data.message);
|
||||
} catch {
|
||||
if (STATE_VALUES.includes(data.message.toLowerCase())) {
|
||||
return {state: data.message};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return STATE_VALUES.includes(data.message.toLowerCase()) ? {state: data.message} : undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,8 +82,7 @@ export default class Publish extends Extension {
|
||||
const hasColorTemp = message.color_temp !== undefined;
|
||||
const hasColor = message.color !== undefined;
|
||||
const hasBrightness = message.brightness !== undefined;
|
||||
const isOn = entityState.state === 'ON' ? true : false;
|
||||
if (isOn && (hasColorTemp || hasColor) && !hasBrightness) {
|
||||
if (entityState.state === 'ON' && (hasColorTemp || hasColor) && !hasBrightness) {
|
||||
delete message.state;
|
||||
logger.debug('Skipping state because of Home Assistant');
|
||||
}
|
||||
@@ -217,7 +212,7 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
// If the endpoint_name name is a number, try to map it to a friendlyName
|
||||
if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isZHEndpoint(localTarget) && re.endpointName(localTarget)) {
|
||||
if (!Number.isNaN(Number(endpointName)) && re.isDevice() && utils.isZHEndpoint(localTarget) && re.endpointName(localTarget)) {
|
||||
endpointName = re.endpointName(localTarget);
|
||||
}
|
||||
|
||||
@@ -252,7 +247,7 @@ export default class Publish extends Extension {
|
||||
const result = await converter.convertSet(localTarget, key, value, meta);
|
||||
const optimistic = entitySettings.optimistic === undefined || entitySettings.optimistic;
|
||||
|
||||
if (result && result.state && optimistic) {
|
||||
if (result?.state && optimistic) {
|
||||
const msg = result.state;
|
||||
|
||||
if (endpointName) {
|
||||
@@ -268,7 +263,7 @@ export default class Publish extends Extension {
|
||||
addToToPublish(re, msg);
|
||||
}
|
||||
|
||||
if (result && result.membersState && optimistic) {
|
||||
if (result?.membersState && optimistic) {
|
||||
for (const [ieeeAddr, state] of Object.entries(result.membersState)) {
|
||||
addToToPublish(this.zigbee.resolveEntity(ieeeAddr)!, state);
|
||||
}
|
||||
@@ -306,9 +301,9 @@ export default class Publish extends Extension {
|
||||
|
||||
private getDefinitionConverters(definition: zhc.Definition | zhc.Definition[]): ReadonlyArray<zhc.Tz.Converter> {
|
||||
if (Array.isArray(definition)) {
|
||||
return definition.length ? Array.from(new Set(definition.map((d) => d.toZigbee).flat())) : [];
|
||||
} else {
|
||||
return definition?.toZigbee;
|
||||
return definition.length ? Array.from(new Set(definition.flatMap((d) => d.toZigbee))) : [];
|
||||
}
|
||||
|
||||
return definition?.toZigbee;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,13 +90,12 @@ export default class Receive extends Extension {
|
||||
// otherwise payload is conflicted
|
||||
isPayloadConflicted(newPayload: KeyValue, oldPayload: KeyValue, debounceIgnore: string[] | undefined): boolean {
|
||||
let result = false;
|
||||
Object.keys(oldPayload)
|
||||
.filter((key) => (debounceIgnore || []).includes(key))
|
||||
.forEach((key) => {
|
||||
if (typeof newPayload[key] !== 'undefined' && newPayload[key] !== oldPayload[key]) {
|
||||
result = true;
|
||||
}
|
||||
});
|
||||
|
||||
for (const key in oldPayload) {
|
||||
if (debounceIgnore?.includes(key) && typeof newPayload[key] !== 'undefined' && newPayload[key] !== oldPayload[key]) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -106,7 +105,7 @@ export default class Receive extends Extension {
|
||||
if (!data.device) return;
|
||||
|
||||
if (!data.device.definition || data.device.zh.interviewing) {
|
||||
logger.debug(`Skipping message, still interviewing`);
|
||||
logger.debug('Skipping message, still interviewing');
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +117,7 @@ export default class Receive extends Extension {
|
||||
|
||||
// Check if there is an available converter, genOta messages are not interesting.
|
||||
const ignoreClusters: (string | number)[] = ['genOta', 'genTime', 'genBasic', 'genPollCtrl'];
|
||||
if (converters.length == 0 && !ignoreClusters.includes(data.cluster)) {
|
||||
if (converters.length === 0 && !ignoreClusters.includes(data.cluster)) {
|
||||
logger.debug(
|
||||
`No converter available for '${data.device.definition.model}' with ` +
|
||||
`cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`,
|
||||
|
||||
+7
-6
@@ -1,8 +1,9 @@
|
||||
import type {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import assert from 'node:assert';
|
||||
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import {access, Numeric} from 'zigbee-herdsman-converters';
|
||||
import {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
|
||||
@@ -48,7 +49,7 @@ export default class Device {
|
||||
exposes(): zhc.Expose[] {
|
||||
const exposes: zhc.Expose[] = [];
|
||||
assert(this.definition, 'Cannot retreive exposes before definition is resolved');
|
||||
if (typeof this.definition.exposes == 'function') {
|
||||
if (typeof this.definition.exposes === 'function') {
|
||||
const options: KeyValue = this.options;
|
||||
exposes.push(...this.definition.exposes(this.zh, options));
|
||||
} else {
|
||||
@@ -58,7 +59,7 @@ export default class Device {
|
||||
return exposes;
|
||||
}
|
||||
|
||||
async resolveDefinition(ignoreCache: boolean = false): Promise<void> {
|
||||
async resolveDefinition(ignoreCache = false): Promise<void> {
|
||||
if (!this.zh.interviewing && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) {
|
||||
this.definition = await zhc.findByDevice(this.zh, true);
|
||||
this._definitionModelID = this.zh.modelID;
|
||||
@@ -74,11 +75,11 @@ export default class Device {
|
||||
endpoint(key?: string | number): zh.Endpoint | undefined {
|
||||
let endpoint: zh.Endpoint | undefined;
|
||||
|
||||
if (key == null || key == '') {
|
||||
if (!key) {
|
||||
key = 'default';
|
||||
}
|
||||
|
||||
if (!isNaN(Number(key))) {
|
||||
if (!Number.isNaN(Number(key))) {
|
||||
endpoint = this.zh.getEndpoint(Number(key));
|
||||
} else if (this.definition?.endpoint) {
|
||||
const ID = this.definition?.endpoint?.(this.zh)[key];
|
||||
@@ -107,7 +108,7 @@ export default class Device {
|
||||
if (this.definition?.endpoint) {
|
||||
const mapping = this.definition?.endpoint(this.zh);
|
||||
for (const [name, id] of Object.entries(mapping)) {
|
||||
if (id == endpoint.ID) {
|
||||
if (id === endpoint.ID) {
|
||||
epName = name;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
|
||||
|
||||
+5
-5
@@ -36,7 +36,7 @@ export default class MQTT {
|
||||
will: {
|
||||
topic: `${settings.get().mqtt.base_topic}/bridge/state`,
|
||||
payload: Buffer.from(JSON.stringify({state: 'offline'})),
|
||||
retain: settings.get().mqtt.force_disable_retain ? false : true,
|
||||
retain: !settings.get().mqtt.force_disable_retain,
|
||||
qos: 1,
|
||||
},
|
||||
properties: {maximumPacketSize: mqttSettings.maximum_packet_size},
|
||||
@@ -71,7 +71,7 @@ export default class MQTT {
|
||||
logger.debug(`Using MQTT login with username only: ${mqttSettings.user}`);
|
||||
options.username = mqttSettings.user;
|
||||
} else {
|
||||
logger.debug(`Using MQTT anonymous login`);
|
||||
logger.debug('Using MQTT anonymous login');
|
||||
}
|
||||
|
||||
if (mqttSettings.client_id) {
|
||||
@@ -80,7 +80,7 @@ export default class MQTT {
|
||||
}
|
||||
|
||||
if (mqttSettings.reject_unauthorized !== undefined && !mqttSettings.reject_unauthorized) {
|
||||
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`);
|
||||
logger.debug('MQTT reject_unauthorized set false, ignoring certificate warnings.');
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export default class MQTT {
|
||||
logger.error(`MQTT error: ${err.message}`);
|
||||
});
|
||||
|
||||
if (mqttSettings.version != undefined && mqttSettings.version >= 5) {
|
||||
if (mqttSettings.version != null && mqttSettings.version >= 5) {
|
||||
this.client.on('disconnect', (packet) => {
|
||||
logger.error(`MQTT disconnect: reason ${packet.reasonCode} (${packet.properties?.reasonString})`);
|
||||
});
|
||||
@@ -201,7 +201,7 @@ export default class MQTT {
|
||||
|
||||
if (!this.isConnected()) {
|
||||
if (!skipLog) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
logger.error('Not connected to MQTT server!');
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -53,9 +53,12 @@ class State {
|
||||
|
||||
stop(): void {
|
||||
// Remove any invalid states (ie when the device has left the network) when the system is stopped
|
||||
Object.keys(this.state)
|
||||
.filter((k) => typeof k === 'string' && !this.zigbee.resolveEntity(k)) // string key = ieeeAddr
|
||||
.forEach((k) => delete this.state[k]);
|
||||
for (const key in this.state) {
|
||||
if (typeof key === 'string' && !this.zigbee.resolveEntity(key)) {
|
||||
// string key = ieeeAddr
|
||||
delete this.state[key];
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(this.timer);
|
||||
this.save();
|
||||
@@ -84,7 +87,7 @@ class State {
|
||||
logger.error(`Failed to write state to '${this.file}' (${error})`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Not saving state`);
|
||||
logger.debug('Not saving state');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -7,10 +7,9 @@ import type TypeDevice from '../model/device';
|
||||
import type TypeGroup from '../model/group';
|
||||
import type TypeMQTT from '../mqtt';
|
||||
import type TypeState from '../state';
|
||||
import type {LogLevel} from '../util/settings';
|
||||
import type TypeZigbee from '../zigbee';
|
||||
|
||||
import {LogLevel} from '../util/settings';
|
||||
|
||||
type OptionalProps<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
declare global {
|
||||
|
||||
Vendored
+1
-2
@@ -1,6 +1,5 @@
|
||||
declare module 'unix-dgram' {
|
||||
import {EventEmitter} from 'events';
|
||||
import {Buffer} from 'buffer';
|
||||
import {EventEmitter} from 'node:events';
|
||||
|
||||
export class UnixDgramSocket extends EventEmitter {
|
||||
send(buf: Buffer, callback?: (err?: Error) => void): void;
|
||||
|
||||
Vendored
+2
-1
@@ -10,7 +10,8 @@ declare module 'http' {
|
||||
}
|
||||
|
||||
declare module 'express-static-gzip' {
|
||||
import {IncomingMessage, ServerResponse} from 'node:http';
|
||||
import type {IncomingMessage, ServerResponse} from 'node:http';
|
||||
|
||||
export type RequestHandler = (req: IncomingMessage, res: ServerResponse, finalhandler: (err: unknown) => void) => void;
|
||||
export default function expressStaticGzip(root: string, options?: Record<string, unknown>): RequestHandler;
|
||||
}
|
||||
|
||||
+15
-12
@@ -76,7 +76,7 @@ class Logger {
|
||||
|
||||
if (settings.get().advanced.log_symlink_current) {
|
||||
const current = settings.get().advanced.log_directory.replace('%TIMESTAMP%', 'current');
|
||||
const actual = './' + timestamp;
|
||||
const actual = `./${timestamp}`;
|
||||
|
||||
/* v8 ignore start */
|
||||
if (fs.existsSync(current)) {
|
||||
@@ -109,7 +109,7 @@ class Logger {
|
||||
|
||||
/* v8 ignore start */
|
||||
if (this.output.includes('syslog')) {
|
||||
logging += `, syslog`;
|
||||
logging += ', syslog';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unused-expressions
|
||||
require('winston-syslog').Syslog;
|
||||
|
||||
@@ -119,7 +119,7 @@ class Logger {
|
||||
...settings.get().advanced.log_syslog,
|
||||
};
|
||||
|
||||
if (options['type'] !== undefined) {
|
||||
if (options.type !== undefined) {
|
||||
options.type = options.type.toString();
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class Logger {
|
||||
}
|
||||
|
||||
public setDebugNamespaceIgnore(value: string): void {
|
||||
this.debugNamespaceIgnoreRegex = value != '' ? new RegExp(value) : undefined;
|
||||
this.debugNamespaceIgnoreRegex = value !== '' ? new RegExp(value) : undefined;
|
||||
}
|
||||
|
||||
public getLevel(): settings.LogLevel {
|
||||
@@ -178,11 +178,13 @@ class Logger {
|
||||
private cacheNamespacedLevel(namespace: string): string {
|
||||
let cached = namespace;
|
||||
|
||||
while (this.cachedNamespacedLevels[namespace] == undefined) {
|
||||
while (this.cachedNamespacedLevels[namespace] === undefined) {
|
||||
const sep = cached.lastIndexOf(NAMESPACE_SEPARATOR);
|
||||
|
||||
if (sep === -1) {
|
||||
return (this.cachedNamespacedLevels[namespace] = this.level);
|
||||
this.cachedNamespacedLevels[namespace] = this.level;
|
||||
|
||||
return this.level;
|
||||
}
|
||||
|
||||
cached = cached.slice(0, sep);
|
||||
@@ -201,19 +203,19 @@ class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
public error(messageOrLambda: string | (() => string), namespace: string = 'z2m'): void {
|
||||
public error(messageOrLambda: string | (() => string), namespace = 'z2m'): void {
|
||||
this.log('error', messageOrLambda, namespace);
|
||||
}
|
||||
|
||||
public warning(messageOrLambda: string | (() => string), namespace: string = 'z2m'): void {
|
||||
public warning(messageOrLambda: string | (() => string), namespace = 'z2m'): void {
|
||||
this.log('warning', messageOrLambda, namespace);
|
||||
}
|
||||
|
||||
public info(messageOrLambda: string | (() => string), namespace: string = 'z2m'): void {
|
||||
public info(messageOrLambda: string | (() => string), namespace = 'z2m'): void {
|
||||
this.log('info', messageOrLambda, namespace);
|
||||
}
|
||||
|
||||
public debug(messageOrLambda: string | (() => string), namespace: string = 'z2m'): void {
|
||||
public debug(messageOrLambda: string | (() => string), namespace = 'z2m'): void {
|
||||
if (this.debugNamespaceIgnoreRegex?.test(namespace)) {
|
||||
return;
|
||||
}
|
||||
@@ -233,10 +235,11 @@ class Logger {
|
||||
|
||||
directories.sort((a: KeyValue, b: KeyValue) => b.birth - a.birth);
|
||||
directories = directories.slice(settings.get().advanced.log_directories_to_keep, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
|
||||
for (const dir of directories) {
|
||||
this.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimrafSync(dir.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ async function startOnboardingServer(): Promise<boolean> {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
console.log(`Onboarding page is available at ${serverUrl.href}`);
|
||||
});
|
||||
});
|
||||
@@ -512,7 +512,7 @@ async function startFailureServer(errors: string): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
console.error(`Failure page is available at ${serverUrl.href}`);
|
||||
});
|
||||
});
|
||||
@@ -553,11 +553,11 @@ export async function onboard(): Promise<boolean> {
|
||||
const errors = settings.validate();
|
||||
|
||||
if (errors.length > 0) {
|
||||
let pErrors: string = '';
|
||||
let pErrors = '';
|
||||
|
||||
console.error(`\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
|
||||
console.error('\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.error(' READ THIS CAREFULLY\n');
|
||||
console.error(`Refusing to start because configuration is not valid, found the following errors:`);
|
||||
console.error('Refusing to start because configuration is not valid, found the following errors:');
|
||||
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
@@ -565,8 +565,8 @@ export async function onboard(): Promise<boolean> {
|
||||
pErrors += `<p>- ${escapeHtml(error)}</p>`;
|
||||
}
|
||||
|
||||
console.error(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration`);
|
||||
console.error(`\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n`);
|
||||
console.error("\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration");
|
||||
console.error('\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n');
|
||||
|
||||
if (!process.env.Z2M_ONBOARD_NO_SERVER && !process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
|
||||
await startFailureServer(pErrors);
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function initSdNotify(): Promise<{notifyStopping: () => void; stop:
|
||||
|
||||
sendToSystemd('READY=1');
|
||||
|
||||
const wdUSec = process.env.WATCHDOG_USEC !== undefined ? Math.max(0, parseInt(process.env.WATCHDOG_USEC, 10)) : -1;
|
||||
const wdUSec = process.env.WATCHDOG_USEC !== undefined ? Math.max(0, Number.parseInt(process.env.WATCHDOG_USEC, 10)) : -1;
|
||||
|
||||
if (wdUSec > 0) {
|
||||
// Convert us to ms, send twice as frequently as the timeout
|
||||
|
||||
+19
-27
@@ -1,6 +1,8 @@
|
||||
import type {ValidateFunction} from 'ajv';
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
import Ajv, {ValidateFunction} from 'ajv';
|
||||
import Ajv from 'ajv';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
|
||||
import data from './data';
|
||||
@@ -140,9 +142,9 @@ function parseValueRef(text: string): {filename: string; key: string} | null {
|
||||
filename += '.yaml';
|
||||
}
|
||||
return {filename, key: match[2]};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writeMinimalDefaults(): void {
|
||||
@@ -192,7 +194,7 @@ export function write(): void {
|
||||
['advanced', 'network_key'],
|
||||
['frontend', 'auth_token'],
|
||||
]) {
|
||||
if (actual[ns] && actual[ns][key]) {
|
||||
if (actual[ns]?.[key]) {
|
||||
const ref = parseValueRef(actual[ns][key]);
|
||||
if (ref) {
|
||||
yaml.updateIfChanged(data.joinPath(ref.filename), ref.key, toWrite[ns][key]);
|
||||
@@ -252,30 +254,15 @@ export function validate(): string[] {
|
||||
|
||||
const errors = [];
|
||||
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.network_key &&
|
||||
typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE'
|
||||
) {
|
||||
if (_settings.advanced?.network_key && typeof _settings.advanced.network_key === 'string' && _settings.advanced.network_key !== 'GENERATE') {
|
||||
errors.push(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
||||
}
|
||||
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.pan_id &&
|
||||
typeof _settings.advanced.pan_id === 'string' &&
|
||||
_settings.advanced.pan_id !== 'GENERATE'
|
||||
) {
|
||||
if (_settings.advanced?.pan_id && typeof _settings.advanced.pan_id === 'string' && _settings.advanced.pan_id !== 'GENERATE') {
|
||||
errors.push(`advanced.pan_id: should be number or 'GENERATE' (is '${_settings.advanced.pan_id}')`);
|
||||
}
|
||||
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.ext_pan_id &&
|
||||
typeof _settings.advanced.ext_pan_id === 'string' &&
|
||||
_settings.advanced.ext_pan_id !== 'GENERATE'
|
||||
) {
|
||||
if (_settings.advanced?.ext_pan_id && typeof _settings.advanced.ext_pan_id === 'string' && _settings.advanced.ext_pan_id !== 'GENERATE') {
|
||||
errors.push(`advanced.ext_pan_id: should be array or 'GENERATE' (is '${_settings.advanced.ext_pan_id}')`);
|
||||
}
|
||||
|
||||
@@ -295,8 +282,13 @@ export function validate(): string[] {
|
||||
|
||||
const settingsWithDefaults = get();
|
||||
|
||||
Object.values(settingsWithDefaults.devices).forEach((d) => check(d));
|
||||
Object.values(settingsWithDefaults.groups).forEach((g) => check(g));
|
||||
for (const key in settingsWithDefaults.devices) {
|
||||
check(settingsWithDefaults.devices[key]);
|
||||
}
|
||||
|
||||
for (const key in settingsWithDefaults.groups) {
|
||||
check(settingsWithDefaults.groups[key]);
|
||||
}
|
||||
|
||||
if (settingsWithDefaults.mqtt.version !== 5) {
|
||||
for (const device of Object.values(settingsWithDefaults.devices)) {
|
||||
@@ -459,7 +451,7 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
|
||||
write();
|
||||
}
|
||||
|
||||
export function apply(settings: Record<string, unknown>, throwOnError: boolean = true): boolean {
|
||||
export function apply(settings: Record<string, unknown>, throwOnError = true): boolean {
|
||||
getPersistedSettings(); // Ensure _settings is initialized.
|
||||
// @ts-expect-error noMutate not typed properly
|
||||
const newSettings = objectAssignDeep.noMutate(_settings, settings);
|
||||
@@ -468,7 +460,7 @@ export function apply(settings: Record<string, unknown>, throwOnError: boolean =
|
||||
ajvSetting(newSettings);
|
||||
|
||||
if (throwOnError) {
|
||||
const errors = ajvSetting.errors && ajvSetting.errors.filter((e) => e.keyword !== 'required');
|
||||
const errors = ajvSetting.errors?.filter((e) => e.keyword !== 'required');
|
||||
|
||||
if (errors?.length) {
|
||||
const error = errors[0];
|
||||
@@ -585,7 +577,7 @@ export function addGroup(name: string, ID?: string): GroupOptions {
|
||||
settings.groups = {};
|
||||
}
|
||||
|
||||
if (ID == undefined) {
|
||||
if (ID == null) {
|
||||
// look for free ID
|
||||
ID = '1';
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function backupSettings(version: number): void {
|
||||
* @returns Returns true if value was set, false if not.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setValue(currentSettings: any, path: string[], value: unknown, createPathIfNotExist: boolean = false): boolean {
|
||||
function setValue(currentSettings: any, path: string[], value: unknown, createPathIfNotExist = false): boolean {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
|
||||
@@ -90,14 +90,14 @@ function getValue(currentSettings: any, path: string[]): [validPath: boolean, va
|
||||
|
||||
if (i === path.length - 1) {
|
||||
return [value !== undefined, value];
|
||||
} else {
|
||||
if (!value) {
|
||||
// invalid path
|
||||
break;
|
||||
}
|
||||
|
||||
currentSettings = value;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
// invalid path
|
||||
break;
|
||||
}
|
||||
|
||||
currentSettings = value;
|
||||
}
|
||||
|
||||
return [false, undefined];
|
||||
@@ -122,7 +122,7 @@ function addValue(currentSettings: Partial<Settings>, addition: SettingsAdd): vo
|
||||
function removeValue(currentSettings: Partial<Settings>, removal: SettingsRemove): [validPath: boolean, previousValue: unknown] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, removal.path);
|
||||
|
||||
if (validPath && previousValue != undefined) {
|
||||
if (validPath && previousValue != null) {
|
||||
setValue(currentSettings, removal.path, undefined);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ function removeValue(currentSettings: Partial<Settings>, removal: SettingsRemove
|
||||
*/
|
||||
function changeValue(currentSettings: Partial<Settings>, change: SettingsChange): [validPath: boolean, previousValue: unknown, changed: boolean] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, change.path);
|
||||
let changed: boolean = false;
|
||||
let changed = false;
|
||||
|
||||
if (validPath && previousValue !== change.newValue) {
|
||||
if (!change.previousValueAnyOf || change.previousValueAnyOf.includes(previousValue)) {
|
||||
@@ -166,10 +166,10 @@ function transferValue(
|
||||
): [validPath: boolean, previousValue: unknown, transfered: boolean] {
|
||||
const [validPath, previousValue] = getValue(currentSettings, transfer.path);
|
||||
const [destValidPath, destValue] = getValue(currentSettings, transfer.newPath);
|
||||
const transfered = validPath && previousValue != undefined && (!destValidPath || destValue == undefined || Array.isArray(destValue));
|
||||
const transfered = validPath && previousValue != null && (!destValidPath || destValue == null || Array.isArray(destValue));
|
||||
|
||||
// no point in set if already undefined
|
||||
if (validPath && previousValue != undefined) {
|
||||
if (validPath && previousValue != null) {
|
||||
setValue(currentSettings, transfer.path, undefined);
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ function transferValue(
|
||||
}
|
||||
|
||||
const noteIfWasTrue = (previousValue: unknown): boolean => previousValue === true;
|
||||
const noteIfWasDefined = (previousValue: unknown): boolean => previousValue != undefined;
|
||||
const noteIfWasDefined = (previousValue: unknown): boolean => previousValue != null;
|
||||
const noteIfWasNonEmptyArray = (previousValue: unknown): boolean => Array.isArray(previousValue) && previousValue.length > 0;
|
||||
|
||||
function migrateToTwo(
|
||||
@@ -199,49 +199,49 @@ function migrateToTwo(
|
||||
transfers.push(
|
||||
{
|
||||
path: ['advanced', 'homeassistant_discovery_topic'],
|
||||
note: `HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.`,
|
||||
note: 'HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['homeassistant', 'discovery_topic'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'homeassistant_status_topic'],
|
||||
note: `HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.`,
|
||||
note: 'HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['homeassistant', 'status_topic'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'baudrate'],
|
||||
note: `Baudrate was moved from advanced.baudrate to serial.baudrate.`,
|
||||
note: 'Baudrate was moved from advanced.baudrate to serial.baudrate.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['serial', 'baudrate'],
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'rtscts'],
|
||||
note: `RTSCTS was moved from advanced.rtscts to serial.rtscts.`,
|
||||
note: 'RTSCTS was moved from advanced.rtscts to serial.rtscts.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['serial', 'rtscts'],
|
||||
},
|
||||
{
|
||||
path: ['experimental', 'transmit_power'],
|
||||
note: `Transmit power was moved from experimental.transmit_power to advanced.transmit_power.`,
|
||||
note: 'Transmit power was moved from experimental.transmit_power to advanced.transmit_power.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['advanced', 'transmit_power'],
|
||||
},
|
||||
{
|
||||
path: ['experimental', 'output'],
|
||||
note: `Output was moved from experimental.output to advanced.output.`,
|
||||
note: 'Output was moved from experimental.output to advanced.output.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['advanced', 'output'],
|
||||
},
|
||||
{
|
||||
path: ['ban'],
|
||||
note: `ban was renamed to passlist.`,
|
||||
note: 'ban was renamed to passlist.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['blocklist'],
|
||||
},
|
||||
{
|
||||
path: ['whitelist'],
|
||||
note: `whitelist was renamed to passlist.`,
|
||||
note: 'whitelist was renamed to passlist.',
|
||||
noteIf: noteIfWasDefined,
|
||||
newPath: ['passlist'],
|
||||
},
|
||||
@@ -257,23 +257,23 @@ function migrateToTwo(
|
||||
|
||||
additions.push({
|
||||
path: ['version'],
|
||||
note: `Migrated settings to version 2`,
|
||||
note: 'Migrated settings to version 2',
|
||||
value: 2,
|
||||
});
|
||||
|
||||
const haLegacyTriggers: SettingsRemove = {
|
||||
path: ['homeassistant', 'legacy_triggers'],
|
||||
note: `Action and click sensors have been removed (homeassistant.legacy_triggers setting). This means all sensor.*_action and sensor.*_click entities are removed. Use the MQTT device trigger instead.`,
|
||||
note: 'Action and click sensors have been removed (homeassistant.legacy_triggers setting). This means all sensor.*_action and sensor.*_click entities are removed. Use the MQTT device trigger instead.',
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
const haLegacyEntityAttrs: SettingsRemove = {
|
||||
path: ['homeassistant', 'legacy_entity_attributes'],
|
||||
note: `Entity attributes (homeassistant.legacy_entity_attributes setting) has been removed. This means that entities discovered by Zigbee2MQTT will no longer have entity attributes (Home Assistant entity attributes are accessed via e.g. states.binary_sensor.my_sensor.attributes).`,
|
||||
note: 'Entity attributes (homeassistant.legacy_entity_attributes setting) has been removed. This means that entities discovered by Zigbee2MQTT will no longer have entity attributes (Home Assistant entity attributes are accessed via e.g. states.binary_sensor.my_sensor.attributes).',
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
const otaIkeaUseTestUrl: SettingsRemove = {
|
||||
path: ['ota', 'ikea_ota_use_test_url'],
|
||||
note: `Due to the OTA rework, the ota.ikea_ota_use_test_url option has been removed.`,
|
||||
note: 'Due to the OTA rework, the ota.ikea_ota_use_test_url option has been removed.',
|
||||
noteIf: noteIfWasTrue,
|
||||
};
|
||||
|
||||
@@ -292,7 +292,7 @@ function migrateToTwo(
|
||||
},
|
||||
{
|
||||
path: ['permit_join'],
|
||||
note: `The permit_join setting has been removed, use the frontend or MQTT to permit joining.`,
|
||||
note: 'The permit_join setting has been removed, use the frontend or MQTT to permit joining.',
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
otaIkeaUseTestUrl,
|
||||
@@ -303,68 +303,68 @@ function migrateToTwo(
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'legacy_api'],
|
||||
note: `The MQTT legacy API has been removed (advanced.legacy_api setting). See link below for affected topics.`,
|
||||
note: 'The MQTT legacy API has been removed (advanced.legacy_api setting). See link below for affected topics.',
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'legacy_availability_payload'],
|
||||
note: `Due to the removal of advanced.legacy_availability_payload, zigbee2mqtt/bridge/state will now always be a JSON object ({"state":"online"} or {"state":"offline"})`,
|
||||
note: 'Due to the removal of advanced.legacy_availability_payload, zigbee2mqtt/bridge/state will now always be a JSON object ({"state":"online"} or {"state":"offline"})',
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'soft_reset_timeout'],
|
||||
note: `Removed deprecated: Soft reset feature (advanced.soft_reset_timeout setting)`,
|
||||
note: 'Removed deprecated: Soft reset feature (advanced.soft_reset_timeout setting)',
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'report'],
|
||||
note: `Removed deprecated: Report feature (advanced.report setting)`,
|
||||
note: 'Removed deprecated: Report feature (advanced.report setting)',
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_timeout'],
|
||||
note: `Removed deprecated: advanced.availability_timeout availability settings`,
|
||||
note: 'Removed deprecated: advanced.availability_timeout availability settings',
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_blocklist'],
|
||||
note: `Removed deprecated: advanced.availability_blocklist availability settings`,
|
||||
note: 'Removed deprecated: advanced.availability_blocklist availability settings',
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_passlist'],
|
||||
note: `Removed deprecated: advanced.availability_passlist availability settings`,
|
||||
note: 'Removed deprecated: advanced.availability_passlist availability settings',
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_blacklist'],
|
||||
note: `Removed deprecated: advanced.availability_blacklist availability settings`,
|
||||
note: 'Removed deprecated: advanced.availability_blacklist availability settings',
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['advanced', 'availability_whitelist'],
|
||||
note: `Removed deprecated: advanced.availability_whitelist availability settings`,
|
||||
note: 'Removed deprecated: advanced.availability_whitelist availability settings',
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
{
|
||||
path: ['device_options', 'legacy'],
|
||||
note: `Removed everything that was enabled through device_options.legacy. See link below for affected devices.`,
|
||||
note: 'Removed everything that was enabled through device_options.legacy. See link below for affected devices.',
|
||||
noteIf: noteIfWasTrue,
|
||||
},
|
||||
{
|
||||
path: ['experimental'],
|
||||
note: `The entire experimental section was removed.`,
|
||||
note: 'The entire experimental section was removed.',
|
||||
noteIf: noteIfWasDefined,
|
||||
},
|
||||
{
|
||||
path: ['external_converters'],
|
||||
note: `External converters are now automatically loaded from the 'data/external_converters' directory without requiring settings to be set. Make sure your external converters are still needed (might be supported out-of-the-box now), and if so, move them to that directory.`,
|
||||
note: "External converters are now automatically loaded from the 'data/external_converters' directory without requiring settings to be set. Make sure your external converters are still needed (might be supported out-of-the-box now), and if so, move them to that directory.",
|
||||
noteIf: noteIfWasNonEmptyArray,
|
||||
},
|
||||
);
|
||||
|
||||
// note only once
|
||||
const noteEntityOptionsRetrieveState = `Retrieve state option ((devices|groups).xyz.retrieve_state setting)`;
|
||||
const noteEntityOptionsRetrieveState = 'Retrieve state option ((devices|groups).xyz.retrieve_state setting)';
|
||||
|
||||
for (const deviceKey in currentSettings.devices) {
|
||||
removals.push({
|
||||
@@ -382,7 +382,7 @@ function migrateToTwo(
|
||||
});
|
||||
removals.push({
|
||||
path: ['groups', groupKey, 'devices'],
|
||||
note: `Removed configuring group members through configuration.yaml (groups.xyz.devices setting). This will not impact current group members; however, you will no longer be able to add or remove devices from a group through the configuration.yaml.`,
|
||||
note: 'Removed configuring group members through configuration.yaml (groups.xyz.devices setting). This will not impact current group members; however, you will no longer be able to add or remove devices from a group through the configuration.yaml.',
|
||||
noteIf: noteIfWasDefined,
|
||||
});
|
||||
}
|
||||
@@ -401,7 +401,7 @@ function migrateToThree(
|
||||
transfers.push();
|
||||
changes.push({
|
||||
path: ['version'],
|
||||
note: `Migrated settings to version 3`,
|
||||
note: 'Migrated settings to version 3',
|
||||
newValue: 3,
|
||||
});
|
||||
additions.push();
|
||||
@@ -451,7 +451,7 @@ function migrateToFour(
|
||||
transfers.push();
|
||||
changes.push({
|
||||
path: ['version'],
|
||||
note: `Migrated settings to version 4`,
|
||||
note: 'Migrated settings to version 4',
|
||||
newValue: 4,
|
||||
});
|
||||
additions.push();
|
||||
@@ -475,7 +475,7 @@ function migrateToFour(
|
||||
};
|
||||
|
||||
customHandlers.push({
|
||||
note: `Device icons are now saved as images.`,
|
||||
note: 'Device icons are now saved as images.',
|
||||
noteIf: () => true,
|
||||
execute: (currentSettings) => saveBase64DeviceIconsAsImage(currentSettings),
|
||||
});
|
||||
@@ -520,7 +520,7 @@ export function migrateIfNecessary(): void {
|
||||
backupSettings(currentSettings.version || 1);
|
||||
|
||||
// each version should only bump to the next version so as to gradually migrate if necessary
|
||||
if (currentSettings.version == undefined) {
|
||||
if (currentSettings.version == null) {
|
||||
// migrating from 1 (`version` did not exist) to 2
|
||||
migrationNotesFileName = 'migration-1-to-2.log';
|
||||
|
||||
@@ -574,10 +574,10 @@ export function migrateIfNecessary(): void {
|
||||
}
|
||||
|
||||
if (migrationNotesFileName && migrationNotes.size > 0) {
|
||||
migrationNotes.add(`For more details, see https://github.com/Koenkk/zigbee2mqtt/discussions/24198`);
|
||||
migrationNotes.add('For more details, see https://github.com/Koenkk/zigbee2mqtt/discussions/24198');
|
||||
const migrationNotesFilePath = data.joinPath(migrationNotesFileName);
|
||||
|
||||
writeFileSync(migrationNotesFilePath, Array.from(migrationNotes).join(`\r\n\r\n`), 'utf8');
|
||||
writeFileSync(migrationNotesFilePath, Array.from(migrationNotes).join('\r\n\r\n'), 'utf8');
|
||||
|
||||
console.log(`Migration notes written in ${migrationNotesFilePath}`);
|
||||
}
|
||||
|
||||
+35
-45
@@ -2,8 +2,8 @@ import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from '../types/api';
|
||||
|
||||
import {exec} from 'child_process';
|
||||
import assert from 'node:assert';
|
||||
import {exec} from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -13,7 +13,7 @@ import humanizeDuration from 'humanize-duration';
|
||||
|
||||
import data from './data';
|
||||
|
||||
const BASE64_IMAGE_REGEX = new RegExp(`data:image/(?<extension>.+);base64,(?<data>.+)`);
|
||||
const BASE64_IMAGE_REGEX = /data:image\/(?<extension>.+);base64,(?<data>.+)/;
|
||||
|
||||
function pad(num: number): string {
|
||||
const norm = Math.floor(Math.abs(num));
|
||||
@@ -28,23 +28,7 @@ function toLocalISOString(date: Date): string {
|
||||
const tzOffset = -date.getTimezoneOffset();
|
||||
const plusOrMinus = tzOffset >= 0 ? '+' : '-';
|
||||
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
pad(date.getMonth() + 1) +
|
||||
'-' +
|
||||
pad(date.getDate()) +
|
||||
'T' +
|
||||
pad(date.getHours()) +
|
||||
':' +
|
||||
pad(date.getMinutes()) +
|
||||
':' +
|
||||
pad(date.getSeconds()) +
|
||||
plusOrMinus +
|
||||
pad(tzOffset / 60) +
|
||||
':' +
|
||||
pad(tzOffset % 60)
|
||||
);
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${plusOrMinus}${pad(tzOffset / 60)}:${pad(tzOffset % 60)}`;
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
@@ -84,12 +68,21 @@ async function getDependencyVersion(depend: string): Promise<{version: string}>
|
||||
}
|
||||
|
||||
function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch' | 'relative'): string | number {
|
||||
if (type === 'ISO_8601') return new Date(time).toISOString();
|
||||
else if (type === 'ISO_8601_local') return toLocalISOString(new Date(time));
|
||||
else if (type === 'epoch') return time;
|
||||
else {
|
||||
// relative
|
||||
return humanizeDuration(Date.now() - time, {language: 'en', largest: 2, round: true}) + ' ago';
|
||||
switch (type) {
|
||||
case 'ISO_8601':
|
||||
// ISO8601 (UTC) = 2019-03-01T15:32:45.941Z
|
||||
return new Date(time).toISOString();
|
||||
|
||||
case 'ISO_8601_local':
|
||||
// ISO8601 (local) = 2019-03-01T16:32:45.941+01:00 (for timezone GMT+1)
|
||||
return toLocalISOString(new Date(time));
|
||||
|
||||
case 'epoch':
|
||||
return time;
|
||||
|
||||
default:
|
||||
// relative
|
||||
return `${humanizeDuration(Date.now() - time, {language: 'en', largest: 2, round: true})} ago`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,19 +132,19 @@ function getResponse<T extends Zigbee2MQTTResponseEndpoints>(
|
||||
response.transaction = request.transaction;
|
||||
}
|
||||
|
||||
return response;
|
||||
} else {
|
||||
const response: Zigbee2MQTTResponse<T> = {
|
||||
data, // valid from error check
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
if (typeof request === 'object' && request.transaction !== undefined) {
|
||||
response.transaction = request.transaction;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const response: Zigbee2MQTTResponse<T> = {
|
||||
data, // valid from error check
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
if (typeof request === 'object' && request.transaction !== undefined) {
|
||||
response.transaction = request.transaction;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function parseJSON(value: string, fallback: string): KeyValue | string {
|
||||
@@ -225,9 +218,9 @@ function getAllFiles(path_: string): string[] {
|
||||
function validateFriendlyName(name: string, throwFirstError = false): string[] {
|
||||
const errors = [];
|
||||
|
||||
if (name.length === 0) errors.push(`friendly_name must be at least 1 char long`);
|
||||
if (name.endsWith('/') || name.startsWith('/')) errors.push(`friendly_name is not allowed to end or start with /`);
|
||||
if (containsControlCharacter(name)) errors.push(`friendly_name is not allowed to contain control char`);
|
||||
if (name.length === 0) errors.push('friendly_name must be at least 1 char long');
|
||||
if (name.endsWith('/') || name.startsWith('/')) errors.push('friendly_name is not allowed to end or start with /');
|
||||
if (containsControlCharacter(name)) errors.push('friendly_name is not allowed to contain control char');
|
||||
if (name.match(/.*\/\d*$/)) errors.push(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
||||
if (name.includes('#') || name.includes('+')) {
|
||||
errors.push(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
|
||||
@@ -245,10 +238,7 @@ function sleep(seconds: number): Promise<void> {
|
||||
}
|
||||
|
||||
function sanitizeImageParameter(parameter: string): string {
|
||||
const replaceByDash = [/\?/g, /&/g, /[^a-z\d\- _./:]/gi];
|
||||
let sanitized = parameter;
|
||||
replaceByDash.forEach((r) => (sanitized = sanitized.replace(r, '-')));
|
||||
return sanitized;
|
||||
return parameter.replace(/\?|&|[^a-z\d\- _./:]/gi, '-');
|
||||
}
|
||||
|
||||
function isAvailabilityEnabledForEntity(entity: Device | Group, settings: Settings): boolean {
|
||||
@@ -356,8 +346,8 @@ function getScenes(entity: zh.Endpoint | zh.Group): Zigbee2MQTTScene[] {
|
||||
for (const endpoint of endpoints) {
|
||||
for (const [key, data] of Object.entries(endpoint.meta?.scenes || {})) {
|
||||
const split = key.split('_');
|
||||
const sceneID = parseInt(split[0], 10);
|
||||
const sceneGroupID = parseInt(split[1], 10);
|
||||
const sceneID = Number.parseInt(split[0], 10);
|
||||
const sceneGroupID = Number.parseInt(split[1], 10);
|
||||
if (sceneGroupID === groupID) {
|
||||
scenes[sceneID] = {id: sceneID, name: (data as KeyValue).name || `Scene ${sceneID}`};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | undefined {
|
||||
return false;
|
||||
},
|
||||
coverage: {
|
||||
|
||||
Reference in New Issue
Block a user