From fc31e0af0a3c1592c2be9057a66287c60fa28a75 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:02:47 +0200 Subject: [PATCH] fix(ignore): partial cleanup for biome move (#27076) --- lib/controller.ts | 2 +- lib/eventBus.ts | 16 ++- lib/extension/bind.ts | 19 +-- lib/extension/bridge.ts | 59 +++++----- lib/extension/configure.ts | 4 +- lib/extension/externalConverters.ts | 2 +- lib/extension/externalJS.ts | 12 +- lib/extension/frontend.ts | 20 ++-- lib/extension/groups.ts | 28 ++--- lib/extension/homeassistant.ts | 173 +++++++++++++++------------- lib/extension/networkMap.ts | 101 ++++++++-------- lib/extension/otaUpdate.ts | 6 +- lib/extension/publish.ts | 21 ++-- lib/extension/receive.ts | 17 ++- lib/model/device.ts | 13 ++- lib/model/group.ts | 2 +- lib/mqtt.ts | 10 +- lib/state.ts | 11 +- lib/types/types.d.ts | 3 +- lib/types/unix-dgram.d.ts | 3 +- lib/types/zigbee2mqtt-frontend.d.ts | 3 +- lib/util/logger.ts | 27 +++-- lib/util/onboarding.ts | 14 +-- lib/util/sd-notify.ts | 2 +- lib/util/settings.ts | 46 +++----- lib/util/settingsMigration.ts | 92 +++++++-------- lib/util/utils.ts | 80 ++++++------- test/vitest.config.mts | 2 +- 28 files changed, 401 insertions(+), 387 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index 8039e554..7266ed46 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -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(); } diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 33c69594..b781913f 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -35,7 +35,7 @@ type EventBusListener = K extends keyof EventBusMap : never; export default class EventBus { - private callbacksByExtension: {[s: string]: {event: keyof EventBusMap; callback: EventBusListener}[]} = {}; + private callbacksByExtension: Map}[]> = new Map(); private emitter = new events.EventEmitter(); constructor() { @@ -195,8 +195,8 @@ export default class EventBus { } private on(event: K, callback: EventBusListener, 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 => { @@ -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); } 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); + } + } } } diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index e9f897d6..44f4b0c1 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -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 { @@ -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; } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 2c2ff67e..371d3786 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -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> { if (typeof message !== 'object' || typeof message.options !== 'object') { - throw new Error(`Invalid payload`); + throw new Error('Invalid payload'); } const newSettings = message.options as Partial; @@ -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> { 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> { @@ -417,7 +422,7 @@ export default class Bridge extends Extension { message: KeyValue | string, ): Promise> { 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> { 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> { 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})`); } diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 2706c24f..dcf1c213 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -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)); } } diff --git a/lib/extension/externalConverters.ts b/lib/extension/externalConverters.ts index 89c91b92..4659a3a8 100644 --- a/lib/extension/externalConverters.ts +++ b/lib/extension/externalConverters.ts @@ -59,7 +59,7 @@ export default class ExternalConverters extends ExternalJSExtension { `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; diff --git a/lib/extension/externalJS.ts b/lib/extension/externalJS.ts index 5cbbcaeb..7c600152 100644 --- a/lib/extension/externalJS.ts +++ b/lib/extension/externalJS.ts @@ -96,7 +96,7 @@ export default abstract class ExternalJSExtension extends Extension { const message = utils.parseJSON(data.message, data.message); try { - let response; + let response: Awaited>; if (match[1].toLowerCase() === 'save') { response = await this.save( @@ -127,7 +127,7 @@ export default abstract class ExternalJSExtension extends Extension { message: Zigbee2MQTTAPI['bridge/request/converter/remove'] | Zigbee2MQTTAPI['bridge/request/extension/remove'], ): Promise> { 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 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> { 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 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); diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 815a6a15..e231629f 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -123,11 +123,15 @@ export class Frontend extends Extension { override async stop(): Promise { 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/')) { diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index 9cd0a747..e9f46544 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -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 { diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 5d96854e..db88f204 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -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 { + private async discover(entity: Device | Group | Bridge, publish = true): Promise { // 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(/^(?recall|scene)_\*(?:_(?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(/^(?region_)\*_(?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; } diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index 73a9386b..3092a856 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -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 { @@ -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: []}; diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 9c25df50..eb22b0b1 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -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})`; diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index 19e47f0d..19a1c5e7 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -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 { 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; } } diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index a80cd5bd..3867c1cb 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -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)}'`, diff --git a/lib/model/device.ts b/lib/model/device.ts index ccf97577..8a420a58 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -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 { + async resolveDefinition(ignoreCache = false): Promise { 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; } } diff --git a/lib/model/group.ts b/lib/model/group.ts index 2bcab78a..2fbc51c2 100644 --- a/lib/model/group.ts +++ b/lib/model/group.ts @@ -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'; diff --git a/lib/mqtt.ts b/lib/mqtt.ts index 54229751..099cd924 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -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}`); } diff --git a/lib/state.ts b/lib/state.ts index 0c490456..b840ae7d 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -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'); } } diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 65832dff..692b03e2 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -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 = Omit & Partial>; declare global { diff --git a/lib/types/unix-dgram.d.ts b/lib/types/unix-dgram.d.ts index 52b7ff5e..96fc8e7e 100644 --- a/lib/types/unix-dgram.d.ts +++ b/lib/types/unix-dgram.d.ts @@ -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; diff --git a/lib/types/zigbee2mqtt-frontend.d.ts b/lib/types/zigbee2mqtt-frontend.d.ts index b0eeff4f..f9fd0984 100644 --- a/lib/types/zigbee2mqtt-frontend.d.ts +++ b/lib/types/zigbee2mqtt-frontend.d.ts @@ -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): RequestHandler; } diff --git a/lib/util/logger.ts b/lib/util/logger.ts index 7d0e7f37..267c4833 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -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); - }); + } } } diff --git a/lib/util/onboarding.ts b/lib/util/onboarding.ts index eed45c2b..c33872ad 100644 --- a/lib/util/onboarding.ts +++ b/lib/util/onboarding.ts @@ -485,7 +485,7 @@ async function startOnboardingServer(): Promise { } }); - 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 { } }); - 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 { 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 { pErrors += `

- ${escapeHtml(error)}

`; } - 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); diff --git a/lib/util/sd-notify.ts b/lib/util/sd-notify.ts index aea588ff..d47cccc2 100644 --- a/lib/util/sd-notify.ts +++ b/lib/util/sd-notify.ts @@ -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 diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 9efefc93..d387e380 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -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, throwOnError: boolean = true): boolean { +export function apply(settings: Record, 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, 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'; diff --git a/lib/util/settingsMigration.ts b/lib/util/settingsMigration.ts index d7ca8f73..3234eb11 100644 --- a/lib/util/settingsMigration.ts +++ b/lib/util/settingsMigration.ts @@ -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, addition: SettingsAdd): vo function removeValue(currentSettings: Partial, 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, removal: SettingsRemove */ function changeValue(currentSettings: Partial, 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}`); } diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 89fef37e..21487cf3 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -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/(?.+);base64,(?.+)`); +const BASE64_IMAGE_REGEX = /data:image\/(?.+);base64,(?.+)/; 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( response.transaction = request.transaction; } - return response; - } else { - const response: Zigbee2MQTTResponse = { - data, // valid from error check - status: 'ok', - }; - - if (typeof request === 'object' && request.transaction !== undefined) { - response.transaction = request.transaction; - } - return response; } + + const response: Zigbee2MQTTResponse = { + 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 { } 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}`}; } diff --git a/test/vitest.config.mts b/test/vitest.config.mts index 5a9d3347..c15c9359 100644 --- a/test/vitest.config.mts +++ b/test/vitest.config.mts @@ -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: {