diff --git a/.eslintrc.js b/.eslintrc.js index f2abf40f..87af9ca8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,21 +5,19 @@ module.exports = { 'es6': true, 'node': true, }, - 'extends': ['eslint:recommended', 'google', 'plugin:jest/recommended', 'plugin:jest/style'], + 'extends': ['eslint:recommended', 'plugin:jest/recommended', 'plugin:jest/style', 'prettier'], 'parserOptions': { 'ecmaVersion': 2018, 'sourceType': 'module', }, 'rules': { 'require-jsdoc': 'off', - 'indent': ['error', 4], - 'max-len': ['error', {'code': 150}], 'no-prototype-builtins': 'off', - 'linebreak-style': ['error', (process.platform === 'win32' ? 'windows' : 'unix')], // https://stackoverflow.com/q/39114446/2771889 '@typescript-eslint/no-floating-promises': 'error', }, 'plugins': [ 'jest', + 'perfectionist', ], 'overrides': [{ files: ['*.ts'], @@ -35,14 +33,46 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/semi': ['error'], - 'array-bracket-spacing': ['error', 'never'], - 'indent': ['error', 4], - 'max-len': ['error', {'code': 150}], - 'no-return-await': 'error', - 'object-curly-spacing': ['error', 'never'], '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-unused-vars': 'error', + 'no-return-await': 'error', + "perfectionist/sort-imports": [ + "error", + { + "groups": [ + "type", + [ + "builtin", + "external" + ], + "internal-type", + "internal", + [ + "parent-type", + "sibling-type", + "index-type" + ], + [ + "parent", + "sibling", + "index" + ], + "object", + "unknown" + ], + "custom-groups": { + "value": {}, + "type": {} + }, + "newlines-between": "always", + "internal-pattern": [ + "~/**" + ], + "type": "natural", + "order": "asc", + "ignore-case": false + } + ], }, }], }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40f0dcb8..3f58e7d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,12 @@ jobs: run: npm ci - name: Build run: npm run build + - name: Lint + run: | + npm run pretty:check + npm run eslint - name: Test run: npm run test-with-coverage - - name: Lint - run: npm run eslint - name: Docker login if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..47d29d4e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 150, + "bracketSpacing": false, + "endOfLine": "lf", + "tabWidth": 4 +} \ No newline at end of file diff --git a/lib/controller.ts b/lib/controller.ts index d3503dbb..c84c9bfa 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -1,46 +1,67 @@ -import MQTT from './mqtt'; -import Zigbee from './zigbee'; +import assert from 'assert'; +import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; +import {setLogger as zhSetLogger} from 'zigbee-herdsman'; +import {setLogger as zhcSetLogger} from 'zigbee-herdsman-converters'; + import EventBus from './eventBus'; +import ExtensionAvailability from './extension/availability'; +import ExtensionBind from './extension/bind'; +import ExtensionBridge from './extension/bridge'; +import ExtensionConfigure from './extension/configure'; +import ExtensionExternalConverters from './extension/externalConverters'; +import ExtensionExternalExtension from './extension/externalExtension'; +// Extensions +import ExtensionFrontend from './extension/frontend'; +import ExtensionGroups from './extension/groups'; +import ExtensionHomeAssistant from './extension/homeassistant'; +import ExtensionBridgeLegacy from './extension/legacy/bridgeLegacy'; +import ExtensionDeviceGroupMembership from './extension/legacy/deviceGroupMembership'; +import ExtensionReport from './extension/legacy/report'; +import ExtensionSoftReset from './extension/legacy/softReset'; +import ExtensionNetworkMap from './extension/networkMap'; +import ExtensionOnEvent from './extension/onEvent'; +import ExtensionOTAUpdate from './extension/otaUpdate'; +import ExtensionPublish from './extension/publish'; +import ExtensionReceive from './extension/receive'; +import MQTT from './mqtt'; import State from './state'; import logger from './util/logger'; import * as settings from './util/settings'; import utils from './util/utils'; -import stringify from 'json-stable-stringify-without-jsonify'; -import assert from 'assert'; -import bind from 'bind-decorator'; -import {setLogger as zhcSetLogger} from 'zigbee-herdsman-converters'; -import {setLogger as zhSetLogger} from 'zigbee-herdsman'; - -// Extensions -import ExtensionFrontend from './extension/frontend'; -import ExtensionPublish from './extension/publish'; -import ExtensionReceive from './extension/receive'; -import ExtensionNetworkMap from './extension/networkMap'; -import ExtensionSoftReset from './extension/legacy/softReset'; -import ExtensionHomeAssistant from './extension/homeassistant'; -import ExtensionConfigure from './extension/configure'; -import ExtensionDeviceGroupMembership from './extension/legacy/deviceGroupMembership'; -import ExtensionBridgeLegacy from './extension/legacy/bridgeLegacy'; -import ExtensionBridge from './extension/bridge'; -import ExtensionGroups from './extension/groups'; -import ExtensionAvailability from './extension/availability'; -import ExtensionBind from './extension/bind'; -import ExtensionReport from './extension/legacy/report'; -import ExtensionOnEvent from './extension/onEvent'; -import ExtensionOTAUpdate from './extension/otaUpdate'; -import ExtensionExternalConverters from './extension/externalConverters'; -import ExtensionExternalExtension from './extension/externalExtension'; +import Zigbee from './zigbee'; const AllExtensions = [ - ExtensionPublish, ExtensionReceive, ExtensionNetworkMap, ExtensionSoftReset, ExtensionHomeAssistant, - ExtensionConfigure, ExtensionDeviceGroupMembership, ExtensionBridgeLegacy, ExtensionBridge, ExtensionGroups, - ExtensionBind, ExtensionReport, ExtensionOnEvent, ExtensionOTAUpdate, - ExtensionExternalConverters, ExtensionFrontend, ExtensionExternalExtension, ExtensionAvailability, + ExtensionPublish, + ExtensionReceive, + ExtensionNetworkMap, + ExtensionSoftReset, + ExtensionHomeAssistant, + ExtensionConfigure, + ExtensionDeviceGroupMembership, + ExtensionBridgeLegacy, + ExtensionBridge, + ExtensionGroups, + ExtensionBind, + ExtensionReport, + ExtensionOnEvent, + ExtensionOTAUpdate, + ExtensionExternalConverters, + ExtensionFrontend, + ExtensionExternalExtension, + ExtensionAvailability, ]; -type ExtensionArgs = [Zigbee, MQTT, State, PublishEntityState, EventBus, - enableDisableExtension: (enable: boolean, name: string) => Promise, restartCallback: () => Promise, - addExtension: (extension: Extension) => Promise]; +type ExtensionArgs = [ + Zigbee, + MQTT, + State, + PublishEntityState, + EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, +]; // eslint-disable-next-line @typescript-eslint/no-explicit-any let sdNotify: any = null; @@ -72,8 +93,16 @@ export class Controller { this.exitCallback = exitCallback; // Initialize extensions. - this.extensionArgs = [this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus, - this.enableDisableExtension, this.restartCallback, this.addExtension]; + this.extensionArgs = [ + this.zigbee, + this.mqtt, + this.state, + this.publishEntityState, + this.eventBus, + this.enableDisableExtension, + this.restartCallback, + this.addExtension, + ]; this.extensions = [ new ExtensionOnEvent(...this.extensionArgs), @@ -111,7 +140,7 @@ export class Controller { this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected); } catch (error) { logger.error('Failed to start zigbee'); - logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions'); /* eslint-disable-line max-len */ + logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions'); logger.error('Exiting...'); logger.error(error.stack); return this.exit(1); @@ -130,9 +159,9 @@ export class Controller { const devices = this.zigbee.devices(false); logger.info(`Currently ${devices.length} devices are joined:`); for (const device of devices) { - const model = device.isSupported ? - `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}` : - 'Not supported'; + const model = device.isSupported + ? `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}` + : 'Not supported'; logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`); } @@ -170,8 +199,7 @@ export class Controller { } } - this.eventBus.onLastSeenChanged(this, - (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState)); + this.eventBus.onLastSeenChanged(this, (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState)); logger.info(`Zigbee2MQTT started!`); @@ -237,8 +265,7 @@ export class Controller { await this.stop(); } - @bind async publishEntityState(entity: Group | Device, payload: KeyValue, - stateChangeReason?: StateChangeReason): Promise { + @bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise { let message = {...payload}; // Update state cache with new state. @@ -261,12 +288,18 @@ export class Controller { if (entity.isDevice() && settings.get().mqtt.include_device_information) { message.device = { - friendlyName: entity.name, model: entity.definition?.model, - ieeeAddr: entity.ieeeAddr, networkAddress: entity.zh.networkAddress, type: entity.zh.type, + friendlyName: entity.name, + model: entity.definition?.model, + ieeeAddr: entity.ieeeAddr, + networkAddress: entity.zh.networkAddress, + type: entity.zh.type, manufacturerID: entity.zh.manufacturerID, - powerSource: entity.zh.powerSource, applicationVersion: entity.zh.applicationVersion, - stackVersion: entity.zh.stackVersion, zclVersion: entity.zh.zclVersion, - hardwareVersion: entity.zh.hardwareVersion, dateCode: entity.zh.dateCode, + powerSource: entity.zh.powerSource, + applicationVersion: entity.zh.applicationVersion, + stackVersion: entity.zh.stackVersion, + zclVersion: entity.zh.zclVersion, + hardwareVersion: entity.zh.hardwareVersion, + dateCode: entity.zh.dateCode, softwareBuildID: entity.zh.softwareBuildID, // Manufacturer name can contain \u0000, remove this. // https://github.com/home-assistant/core/issues/85691 diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 71716d9b..aef2c95b 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -1,11 +1,12 @@ import events from 'events'; + import logger from './util/logger'; // eslint-disable-next-line type ListenerKey = object; export default class EventBus { - private callbacksByExtension: { [s: string]: { event: string, callback: (...args: unknown[]) => void }[] } = {}; + private callbacksByExtension: {[s: string]: {event: string; callback: (...args: unknown[]) => void}[]} = {}; private emitter = new events.EventEmitter(); constructor() { @@ -57,8 +58,7 @@ export default class EventBus { public emitDeviceNetworkAddressChanged(data: eventdata.DeviceNetworkAddressChanged): void { this.emitter.emit('deviceNetworkAddressChanged', data); } - public onDeviceNetworkAddressChanged( - key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void { + public onDeviceNetworkAddressChanged(key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void { this.on('deviceNetworkAddressChanged', callback, key); } @@ -167,7 +167,7 @@ export default class EventBus { this.on('stateChange', callback, key); } - private on(event: string, callback: (...args: unknown[]) => (Promise | void), key: ListenerKey): void { + private on(event: string, callback: (...args: unknown[]) => Promise | void, key: ListenerKey): void { if (!this.callbacksByExtension[key.constructor.name]) this.callbacksByExtension[key.constructor.name] = []; const wrappedCallback = async (...args: unknown[]): Promise => { try { @@ -182,7 +182,6 @@ export default class EventBus { } public removeListeners(key: ListenerKey): void { - this.callbacksByExtension[key.constructor.name]?.forEach( - (e) => this.emitter.removeListener(e.event, e.callback)); + this.callbacksByExtension[key.constructor.name]?.forEach((e) => this.emitter.removeListener(e.event, e.callback)); } } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index cbb2566b..ebce749d 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -1,12 +1,13 @@ -import Extension from './extension'; -import logger from '../util/logger'; -import utils from '../util/utils'; -import * as settings from '../util/settings'; -import debounce from 'debounce'; import bind from 'bind-decorator'; +import debounce from 'debounce'; import * as zhc from 'zigbee-herdsman-converters'; -const RETRIEVE_ON_RECONNECT: readonly {keys: string[], condition?: (state: KeyValue) => boolean}[] = [ +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; + +const RETRIEVE_ON_RECONNECT: readonly {keys: string[]; condition?: (state: KeyValue) => boolean}[] = [ {keys: ['state']}, {keys: ['brightness'], condition: (state: KeyValue): boolean => state.state === 'ON'}, {keys: ['color', 'color_temp'], condition: (state: KeyValue): boolean => state.state === 'ON'}, @@ -40,8 +41,9 @@ export default class Availability extends Extension { } private isAvailable(entity: Device | Group): boolean { - return entity.isDevice() ? (Date.now() - entity.zh.lastSeen) < this.getTimeout(entity) : - entity.membersDevices().length === 0 || entity.membersDevices().some((d) => this.availabilityCache[d.ieeeAddr]); + return entity.isDevice() + ? Date.now() - entity.zh.lastSeen < this.getTimeout(entity) + : entity.membersDevices().length === 0 || entity.membersDevices().some((d) => this.availabilityCache[d.ieeeAddr]); } private resetTimer(device: Device): void { @@ -80,7 +82,7 @@ export default class Availability extends Extension { for (let i = 1; i <= attempts; i++) { try { // Enable recovery if device is marked as available and first ping fails. - await device.zh.ping(!available || (i !== 2)); + await device.zh.ping(!available || i !== 2); pingedSuccessfully = true; @@ -152,7 +154,7 @@ export default class Availability extends Extension { } } - private async publishAvailability(entity: Device | Group, logLastSeen: boolean, forcePublish=false, skipGroups=false): Promise { + private async publishAvailability(entity: Device | Group, logLastSeen: boolean, forcePublish = false, skipGroups = false): Promise { if (logLastSeen && entity.isDevice()) { const ago = Date.now() - entity.zh.lastSeen; if (this.isActiveDevice(entity)) { @@ -226,8 +228,12 @@ export default class Availability extends Extension { const options: KeyValue = device.options; const state = this.state.get(device); const meta: zhc.Tz.Meta = { - message: this.state.get(device), mapped: device.definition, endpoint_name: null, - options, state, device: device.zh, + message: this.state.get(device), + mapped: device.definition, + endpoint_name: null, + options, + state, + device: device.zh, }; try { diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 08e6e123..5553b44c 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -1,28 +1,38 @@ -import * as settings from '../util/settings'; -import logger from '../util/logger'; -import utils from '../util/utils'; -import Extension from './extension'; -import stringify from 'json-stable-stringify-without-jsonify'; -import debounce from 'debounce'; -import {Zcl} from 'zigbee-herdsman'; import bind from 'bind-decorator'; +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'; -import {ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; const LEGACY_API = settings.get().advanced.legacy_api; const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`); const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`); const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [ - 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering', 'hvacThermostat', 'msIlluminanceMeasurement', - 'msTemperatureMeasurement', 'msRelativeHumidity', 'msSoilMoisture', 'msCO2', + 'genScenes', + 'genOnOff', + 'genLevelCtrl', + 'lightingColorCtrl', + 'closuresWindowCovering', + 'hvacThermostat', + 'msIlluminanceMeasurement', + 'msTemperatureMeasurement', + 'msRelativeHumidity', + 'msSoilMoisture', + 'msCO2', ]; // See zigbee-herdsman-converters const DEFAULT_BIND_GROUP = {type: 'group_number', ID: 901, name: 'default_bind_group'}; const DEFAULT_REPORT_CONFIG = {minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}; -const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean, colorXY: boolean}> => { +const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => { if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') == null) { await endpoint.read('lightingColorCtrl', ['colorCapabilities']); } @@ -30,47 +40,53 @@ const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemper const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') as number; return { - colorTemperature: (value & 1 << 4) > 0, - colorXY: (value & 1 << 3) > 0, + colorTemperature: (value & (1 << 4)) > 0, + colorXY: (value & (1 << 3)) > 0, }; }; -const REPORT_CLUSTERS: Readonly Promise; -}>[]>>> = { - 'genOnOff': [ - {attribute: 'onOff', ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}, - ], - 'genLevelCtrl': [ - {attribute: 'currentLevel', ...DEFAULT_REPORT_CONFIG}, - ], - 'lightingColorCtrl': [ +const REPORT_CLUSTERS: Readonly< + Partial< + Record< + ClusterName, + Readonly<{ + attribute: string; + minimumReportInterval: number; + maximumReportInterval: number; + reportableChange: number; + condition?: (endpoint: zh.Endpoint) => Promise; + }>[] + > + > +> = { + genOnOff: [{attribute: 'onOff', ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}], + genLevelCtrl: [{attribute: 'currentLevel', ...DEFAULT_REPORT_CONFIG}], + lightingColorCtrl: [ { - attribute: 'colorTemperature', ...DEFAULT_REPORT_CONFIG, + attribute: 'colorTemperature', + ...DEFAULT_REPORT_CONFIG, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorTemperature, }, { - attribute: 'currentX', ...DEFAULT_REPORT_CONFIG, + attribute: 'currentX', + ...DEFAULT_REPORT_CONFIG, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, { - attribute: 'currentY', ...DEFAULT_REPORT_CONFIG, + attribute: 'currentY', + ...DEFAULT_REPORT_CONFIG, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, ], - 'closuresWindowCovering': [ + closuresWindowCovering: [ {attribute: 'currentPositionLiftPercentage', ...DEFAULT_REPORT_CONFIG}, {attribute: 'currentPositionTiltPercentage', ...DEFAULT_REPORT_CONFIG}, ], }; type PollOnMessage = { - cluster: Readonly>>; - read: Readonly<{cluster: string, attributes: string[], attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise}>; + cluster: Readonly>>; + read: Readonly<{cluster: string; attributes: string[]; attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise}>; manufacturerIDs: readonly number[]; manufacturerNames: readonly string[]; }[]; @@ -92,9 +108,7 @@ const POLL_ON_MESSAGE: Readonly = [ {type: 'commandMove', data: {}}, {type: 'commandMoveToLevelWithOnOff', data: {}}, ], - genScenes: [ - {type: 'commandRecall', data: {}}, - ], + genScenes: [{type: 'commandRecall', data: {}}], }, // Read the following attributes read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']}, @@ -107,10 +121,7 @@ const POLL_ON_MESSAGE: Readonly = [ Zcl.ManufacturerCode.TELINK_MICRO, Zcl.ManufacturerCode.BUSCH_JAEGER_ELEKTRO, ], - manufacturerNames: [ - 'GLEDOPTO', - 'Trust International B.V.\u0000', - ], + manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'], }, { cluster: { @@ -126,9 +137,7 @@ const POLL_ON_MESSAGE: Readonly = [ {type: 'commandOffWithEffect', data: {}}, {type: 'commandToggle', data: {}}, ], - genScenes: [ - {type: 'commandRecall', data: {}}, - ], + genScenes: [{type: 'commandRecall', data: {}}], manuSpecificPhilips: [ {type: 'commandHueNotification', data: {button: 1}}, {type: 'commandHueNotification', data: {button: 4}}, @@ -143,16 +152,11 @@ const POLL_ON_MESSAGE: Readonly = [ Zcl.ManufacturerCode.TELINK_MICRO, Zcl.ManufacturerCode.BUSCH_JAEGER_ELEKTRO, ], - manufacturerNames: [ - 'GLEDOPTO', - 'Trust International B.V.\u0000', - ], + manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'], }, { cluster: { - genScenes: [ - {type: 'commandRecall', data: {}}, - ], + genScenes: [{type: 'commandRecall', data: {}}], }, read: { cluster: 'lightingColorCtrl', @@ -184,15 +188,16 @@ const POLL_ON_MESSAGE: Readonly = [ Zcl.ManufacturerCode.TELINK_MICRO, // Note: ManufacturerCode.BUSCH_JAEGER is left out intentionally here as their devices don't support colors ], - manufacturerNames: [ - 'GLEDOPTO', - 'Trust International B.V.\u0000', - ], + manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'], }, ]; interface ParsedMQTTMessage { - type: 'bind' | 'unbind', sourceKey: string, targetKey: string, clusters: string[], skipDisableReporting: boolean + type: 'bind' | 'unbind'; + sourceKey: string; + targetKey: string; + clusters: string[]; + skipDisableReporting: boolean; } export default class Bind extends Extension { @@ -258,8 +263,8 @@ export default class Bind extends Extension { const attemptedClusters = []; const bindSource: zh.Endpoint = parsedSource.endpoint; - const bindTarget: number | zh.Group | zh.Endpoint = (target instanceof Device) ? parsedTarget.endpoint : - ((target instanceof Group) ? target.zh : Number(target.ID)); + const bindTarget: number | zh.Group | zh.Endpoint = + target instanceof Device ? parsedTarget.endpoint : target instanceof Group ? target.zh : Number(target.ID); // Find which clusters are supported by both the source and target. // Groups are assumed to support all clusters. const clusterCandidates = clusters ?? ALL_CLUSTER_CANDIDATES; @@ -270,8 +275,9 @@ export default class Bind extends Extension { const anyClusterValid = utils.isZHGroup(bindTarget) || typeof bindTarget === 'number' || (target as Device).zh.type === 'Coordinator'; if (!anyClusterValid && utils.isEndpoint(bindTarget)) { - matchingClusters = ((bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) || - (bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster))); + matchingClusters = + (bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) || + (bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster)); } const sourceValid = bindSource.supportsInputCluster(cluster) || bindSource.supportsOutputCluster(cluster); @@ -332,7 +338,7 @@ export default class Bind extends Extension { if (successfulClusters.length !== 0) { if (type === 'bind') { await this.setupReporting(bindSource.binds.filter((b) => successfulClusters.includes(b.cluster.name) && b.target === bindTarget)); - } else if ((typeof bindTarget !== 'number') && !skipDisableReporting) { + } else if (typeof bindTarget !== 'number' && !skipDisableReporting) { await this.disableUnnecessaryReportings(bindTarget); } } @@ -368,7 +374,8 @@ export default class Bind extends Extension { } await this.setupReporting(bindsToGroup); - } else { // action === remove/remove_all + } else { + // action === remove/remove_all if (!data.skipDisableReporting) { await this.disableUnnecessaryReportings(data.endpoint); } @@ -411,7 +418,7 @@ export default class Bind extends Extension { for (const c of REPORT_CLUSTERS[bind.cluster.name as ClusterName]) { /* istanbul ignore else */ - if (!c.condition || await c.condition(endpoint)) { + if (!c.condition || (await c.condition(endpoint))) { const i = {...c}; delete i.condition; @@ -458,7 +465,7 @@ export default class Bind extends Extension { for (const b of endpoint.binds) { /* istanbul ignore else */ - if (b.target === coordinator && !requiredClusters.includes(b.cluster.name) && (b.cluster.name in REPORT_CLUSTERS)) { + if (b.target === coordinator && !requiredClusters.includes(b.cluster.name) && b.cluster.name in REPORT_CLUSTERS) { boundClusters.push(b.cluster.name); } } @@ -471,11 +478,11 @@ export default class Bind extends Extension { for (const item of REPORT_CLUSTERS[cluster as ClusterName]) { /* istanbul ignore else */ - if (!item.condition || await item.condition(endpoint)) { + if (!item.condition || (await item.condition(endpoint))) { const i = {...item}; delete i.condition; - items.push({...i, maximumReportInterval: 0xFFFF}); + items.push({...i, maximumReportInterval: 0xffff}); } } @@ -499,8 +506,8 @@ export default class Bind extends Extension { * When dimming the bulb via the dimmer switch the state is therefore not reported. * When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound). */ - const polls = POLL_ON_MESSAGE.filter( - (p) => p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)), + const polls = POLL_ON_MESSAGE.filter((p) => + p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)), ); if (polls.length) { @@ -526,9 +533,11 @@ export default class Bind extends Extension { for (const endpoint of toPoll) { for (const poll of polls) { - if ((!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) && - !poll.manufacturerNames.includes(endpoint.getDevice().manufacturerName)) || - !endpoint.supportsInputCluster(poll.read.cluster)) { + if ( + (!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) && + !poll.manufacturerNames.includes(endpoint.getDevice().manufacturerName)) || + !endpoint.supportsInputCluster(poll.read.cluster) + ) { continue; } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index ca766bc3..caee3da2 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -1,31 +1,37 @@ /* eslint-disable camelcase */ -import logger from '../util/logger'; -import utils from '../util/utils'; -import * as settings from '../util/settings'; -import Transport from 'winston-transport'; import bind from 'bind-decorator'; +import fs from 'fs'; import stringify from 'json-stable-stringify-without-jsonify'; +import JSZip from 'jszip'; import objectAssignDeep from 'object-assign-deep'; -import Extension from './extension'; +import winston from 'winston'; +import Transport from 'winston-transport'; +import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster'; +import {CustomClusters, ClusterDefinition, ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; +import * as zhc from 'zigbee-herdsman-converters'; + import Device from '../model/device'; import Group from '../model/group'; import data from '../util/data'; -import JSZip from 'jszip'; -import fs from 'fs'; -import * as zhc from 'zigbee-herdsman-converters'; -import {CustomClusters, ClusterDefinition, ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; -import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster'; -import winston from 'winston'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); type DefinitionPayload = { - model: string, vendor: string, description: string, exposes: zhc.Expose[], supports_ota: - boolean, icon: string, options: zhc.Expose[], + model: string; + vendor: string; + description: string; + exposes: zhc.Expose[]; + supports_ota: boolean; + icon: string; + options: zhc.Expose[]; }; export default class Bridge extends Extension { - private zigbee2mqttVersion: {commitHash: string, version: string}; + private zigbee2mqttVersion: {commitHash: string; version: string}; private zigbeeHerdsmanVersion: {version: string}; private zigbeeHerdsmanConvertersVersion: {version: string}; private coordinatorVersion: zh.CoordinatorVersion; @@ -47,16 +53,16 @@ export default class Bridge extends Extension { 'group/options': this.groupOptions, 'group/remove': this.groupRemove, 'group/rename': this.groupRename, - 'permit_join': this.permitJoin, - 'restart': this.restart, - 'backup': this.backup, + permit_join: this.permitJoin, + restart: this.restart, + backup: this.backup, 'touchlink/factory_reset': this.touchlinkFactoryReset, 'touchlink/identify': this.touchlinkIdentify, 'install_code/add': this.installCodeAdd, 'touchlink/scan': this.touchlinkScan, - 'health_check': this.healthCheck, - 'coordinator_check': this.coordinatorCheck, - 'options': this.bridgeOptions, + health_check: this.healthCheck, + coordinator_check: this.coordinatorCheck, + options: this.bridgeOptions, // Below are deprecated 'config/last_seen': this.configLastSeen, 'config/homeassistant': this.configHomeAssistant, @@ -78,7 +84,7 @@ export default class Bridge extends Extension { if (debugToMQTTFrontend) { class DebugEventTransport extends Transport { - log(info: {message: string, level: string, namespace: string}, next: () => void): void { + log(info: {message: string; level: string; namespace: string}, next: () => void): void { bridgeLogging(info.message, info.level, info.namespace); next(); } @@ -87,7 +93,7 @@ export default class Bridge extends Extension { this.logTransport = new DebugEventTransport(); } else { class EventTransport extends Transport { - log(info: {message: string, level: string, namespace: string}, next: () => void): void { + log(info: {message: string; level: string; namespace: string}, next: () => void): void { if (info.level !== 'debug') { bridgeLogging(info.message, info.level, info.namespace); } @@ -107,9 +113,7 @@ export default class Bridge extends Extension { this.eventBus.onEntityRenamed(this, () => this.publishInfo()); this.eventBus.onGroupMembersChanged(this, () => this.publishGroups()); - this.eventBus.onDevicesChanged(this, () => this.publishDevices() && - this.publishInfo() && - this.publishDefinitions()); + this.eventBus.onDevicesChanged(this, () => this.publishDevices() && this.publishInfo() && this.publishDefinitions()); this.eventBus.onPermitJoinChanged(this, () => !this.zigbee.isStopping() && this.publishInfo()); this.eventBus.onScenesChanged(this, async () => { await this.publishDevices(); @@ -132,8 +136,7 @@ export default class Bridge extends Extension { this.eventBus.onDeviceNetworkAddressChanged(this, () => this.publishDevices()); this.eventBus.onDeviceInterview(this, async (data) => { await this.publishDevices(); - const payload: KeyValue = - {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr}; + const payload: KeyValue = {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr}; if (data.status === 'successful') { payload.supported = data.device.isSupported; payload.definition = this.getDefinitionPayload(data.device); @@ -274,7 +277,9 @@ export default class Bridge extends Extension { @bind async backup(message: string | KeyValue): Promise { await this.zigbee.backup(); const dataPath = data.getPath(); - const files = utils.getAllFiles(dataPath).map((f) => [f, f.substring(dataPath.length + 1)]) + const files = utils + .getAllFiles(dataPath) + .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]))); @@ -321,7 +326,7 @@ export default class Bridge extends Extension { } await this.zigbee.permitJoin(value, device, time); - const response: {value: boolean, device?: string, time?: number} = {value}; + const response: {value: boolean; device?: string; time?: number} = {value}; if (device && typeof message === 'object') response.device = message.device; if (time && typeof message === 'object') response.time = message.time; return utils.getResponse(message, response, null); @@ -380,8 +385,7 @@ export default class Bridge extends Extension { } @bind async touchlinkIdentify(message: KeyValue | string): Promise { - if (typeof message !== 'object' || !message.hasOwnProperty('ieee_address') || - !message.hasOwnProperty('channel')) { + if (typeof message !== 'object' || !message.hasOwnProperty('ieee_address') || !message.hasOwnProperty('channel')) { throw new Error('Invalid payload'); } @@ -392,9 +396,8 @@ export default class Bridge extends Extension { @bind async touchlinkFactoryReset(message: KeyValue | string): Promise { let result = false; - const payload: {ieee_address?: string, channel?: number} = {}; - if (typeof message === 'object' && message.hasOwnProperty('ieee_address') && - message.hasOwnProperty('channel')) { + const payload: {ieee_address?: string; channel?: number} = {}; + if (typeof message === 'object' && message.hasOwnProperty('ieee_address') && message.hasOwnProperty('channel')) { logger.info(`Start Touchlink factory reset of '${message.ieee_address}' on channel ${message.channel}`); result = await this.zigbee.touchlinkFactoryReset(message.ieee_address, message.channel); payload.ieee_address = message.ieee_address; @@ -445,7 +448,11 @@ export default class Bridge extends Extension { } const cleanup = (o: KeyValue): KeyValue => { - delete o.friendlyName; delete o.friendly_name; delete o.ID; delete o.type; delete o.devices; + delete o.friendlyName; + delete o.friendly_name; + delete o.ID; + delete o.type; + delete o.devices; return o; }; @@ -460,17 +467,19 @@ export default class Bridge extends Extension { logger.info(`Changed config for ${entityType} ${ID}`); this.eventBus.emitEntityOptionsChanged({from: oldOptions, to: newOptions, entity}); - return utils.getResponse( - message, - {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired}, - null, - ); + return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired}, null); } @bind async deviceConfigureReporting(message: string | KeyValue): Promise { - if (typeof message !== 'object' || !message.hasOwnProperty('id') || !message.hasOwnProperty('cluster') || - !message.hasOwnProperty('maximum_report_interval') || !message.hasOwnProperty('minimum_report_interval') || - !message.hasOwnProperty('reportable_change') || !message.hasOwnProperty('attribute')) { + if ( + typeof message !== 'object' || + !message.hasOwnProperty('id') || + !message.hasOwnProperty('cluster') || + !message.hasOwnProperty('maximum_report_interval') || + !message.hasOwnProperty('minimum_report_interval') || + !message.hasOwnProperty('reportable_change') || + !message.hasOwnProperty('attribute') + ) { throw new Error(`Invalid payload`); } @@ -485,20 +494,35 @@ export default class Bridge extends Extension { const coordinatorEndpoint = this.zigbee.firstCoordinatorEndpoint(); await endpoint.bind(message.cluster, coordinatorEndpoint); - await endpoint.configureReporting(message.cluster, [{ - attribute: message.attribute, minimumReportInterval: message.minimum_report_interval, - maximumReportInterval: message.maximum_report_interval, reportableChange: message.reportable_change, - }], message.options); + await endpoint.configureReporting( + message.cluster, + [ + { + attribute: message.attribute, + minimumReportInterval: message.minimum_report_interval, + maximumReportInterval: message.maximum_report_interval, + reportableChange: message.reportable_change, + }, + ], + message.options, + ); await this.publishDevices(); logger.info(`Configured reporting for '${message.id}', '${message.cluster}.${message.attribute}'`); - return utils.getResponse(message, { - id: message.id, cluster: message.cluster, maximum_report_interval: message.maximum_report_interval, - minimum_report_interval: message.minimum_report_interval, reportable_change: message.reportable_change, - attribute: message.attribute, - }, null); + return utils.getResponse( + message, + { + id: message.id, + cluster: message.cluster, + maximum_report_interval: message.maximum_report_interval, + minimum_report_interval: message.minimum_report_interval, + reportable_change: message.reportable_change, + attribute: message.attribute, + }, + null, + ); } @bind async deviceInterview(message: string | KeyValue): Promise { @@ -537,8 +561,7 @@ export default class Bridge extends Extension { async renameEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise { const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true; - if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) || - !message.hasOwnProperty('to')) { + if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) || !message.hasOwnProperty('to')) { throw new Error(`Invalid payload`); } @@ -548,8 +571,7 @@ export default class Bridge extends Extension { const from = deviceAndHasLast ? this.lastJoinedDeviceIeeeAddr : message.from; const to = message.to; - const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ? - message.homeassistant_rename : false; + const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ? message.homeassistant_rename : false; const entity = this.getEntity(entityType, from); const oldFriendlyName = entity.options.friendly_name; @@ -570,11 +592,7 @@ export default class Bridge extends Extension { // Republish entity state await this.publishEntityState(entity, {}); - return utils.getResponse( - message, - {from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename}, - null, - ); + return utils.getResponse(message, {from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename}, null); } async removeEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise { @@ -650,9 +668,7 @@ export default class Bridge extends Extension { return utils.getResponse(message, {id: ID, force: force}, null); } } catch (error) { - throw new Error( - `Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`, - ); + throw new Error(`Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`); } } @@ -687,16 +703,21 @@ export default class Bridge extends Extension { config_schema: settings.schema, }; - await this.mqtt.publish( - 'bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); + await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } async publishDevices(): Promise { interface Data { - bindings: {cluster: string, target: {type: string, endpoint?: number, ieee_address?: string, id?: number}}[] - configured_reportings: {cluster: string, attribute: string | number, - minimum_report_interval: number, maximum_report_interval: number, reportable_change: number}[], - clusters: {input: string[], output: string[]}, scenes: Scene[] + bindings: {cluster: string; target: {type: string; endpoint?: number; ieee_address?: string; id?: number}}[]; + configured_reportings: { + cluster: string; + attribute: string | number; + minimum_report_interval: number; + maximum_report_interval: number; + reportable_change: number; + }[]; + clusters: {input: string[]; output: string[]}; + scenes: Scene[]; } const devices = this.zigbee.devices().map((device) => { @@ -713,9 +734,9 @@ export default class Bridge extends Extension { }; for (const bind of endpoint.binds) { - const target = utils.isEndpoint(bind.target) ? - {type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID} : - {type: 'group', id: bind.target.groupID}; + const target = utils.isEndpoint(bind.target) + ? {type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID} + : {type: 'group', id: bind.target.groupID}; data.bindings.push({cluster: bind.cluster.name, target}); } @@ -752,8 +773,7 @@ export default class Bridge extends Extension { }; }); - await this.mqtt.publish('bridge/devices', stringify(devices), - {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); + await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } async publishGroups(): Promise { @@ -768,14 +788,13 @@ export default class Bridge extends Extension { }), }; }); - await this.mqtt.publish( - 'bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); + await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } async publishDefinitions(): Promise { interface ClusterDefinitionPayload { - clusters: Readonly>>, - custom_clusters: {[key: string] : CustomClusters} + clusters: Readonly>>; + custom_clusters: {[key: string]: CustomClusters}; } const data: ClusterDefinitionPayload = { @@ -789,8 +808,7 @@ export default class Bridge extends Extension { } } - await this.mqtt.publish('bridge/definitions', stringify(data), - {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); + await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } getDefinitionPayload(device: Device): DefinitionPayload { diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 84c22cd4..2f1e0c99 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -1,11 +1,12 @@ -import * as settings from '../util/settings'; -import utils from '../util/utils'; -import logger from '../util/logger'; +import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; import * as zhc from 'zigbee-herdsman-converters'; -import Extension from './extension'; -import bind from 'bind-decorator'; + import Device from '../model/device'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; /** * This extension calls the zigbee-herdsman-converters definition configure() method @@ -89,8 +90,12 @@ export default class Configure extends Extension { this.eventBus.onReconfigure(this, this.onReconfigure); } - private async configure(device: Device, event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message', - force=false, throwError=false): Promise { + private async configure( + device: Device, + event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message', + force = false, + throwError = false, + ): Promise { if (!force) { if (device.options.disabled || !device.definition?.configure || !device.zh.interviewCompleted) { return; diff --git a/lib/extension/extension.ts b/lib/extension/extension.ts index d5ae7a79..3f09f3ed 100644 --- a/lib/extension/extension.ts +++ b/lib/extension/extension.ts @@ -20,9 +20,16 @@ abstract class Extension { * @param {restartCallback} restartCallback Restart Zigbee2MQTT * @param {addExtension} addExtension Add an extension */ - constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState, - eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise, - restartCallback: () => Promise, addExtension: (extension: Extension) => Promise) { + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + ) { this.zigbee = zigbee; this.mqtt = mqtt; this.state = state; diff --git a/lib/extension/externalConverters.ts b/lib/extension/externalConverters.ts index df137506..1b7d412a 100644 --- a/lib/extension/externalConverters.ts +++ b/lib/extension/externalConverters.ts @@ -1,13 +1,21 @@ import * as zhc from 'zigbee-herdsman-converters'; + +import logger from '../util/logger'; import * as settings from '../util/settings'; import {loadExternalConverter} from '../util/utils'; import Extension from './extension'; -import logger from '../util/logger'; export default class ExternalConverters extends Extension { - constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState, - eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise, - restartCallback: () => Promise, addExtension: (extension: Extension) => Promise) { + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + ) { super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); for (const file of settings.get().external_converters) { @@ -21,10 +29,11 @@ export default class ExternalConverters extends Extension { logger.error(`Failed to load external converter file '${file}' (${error.message})`); logger.error( `Probably there is a syntax error in the file or the external converter is not ` + - `compatible with the current Zigbee2MQTT version`); + `compatible with the current Zigbee2MQTT version`, + ); logger.error( `Note that external converters are not meant for long term usage, it's meant for local ` + - `testing after which a pull request should be created to add out-of-the-box support for the device`, + `testing after which a pull request should be created to add out-of-the-box support for the device`, ); } } diff --git a/lib/extension/externalExtension.ts b/lib/extension/externalExtension.ts index a65c664c..6fcffbd7 100644 --- a/lib/extension/externalExtension.ts +++ b/lib/extension/externalExtension.ts @@ -1,11 +1,12 @@ +import bind from 'bind-decorator'; +import fs from 'fs'; +import stringify from 'json-stable-stringify-without-jsonify'; +import path from 'path'; + import * as settings from '../util/settings'; import utils from '../util/utils'; -import fs from 'fs'; import data from './../util/data'; -import path from 'path'; import logger from './../util/logger'; -import stringify from 'json-stable-stringify-without-jsonify'; -import bind from 'bind-decorator'; import Extension from './extension'; const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`); @@ -15,7 +16,7 @@ export default class ExternalExtension extends Extension { override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); - this.requestLookup = {'save': this.saveExtension, 'remove': this.removeExtension}; + this.requestLookup = {save: this.saveExtension, remove: this.removeExtension}; await this.loadUserDefinedExtensions(); await this.publishExtensions(); } @@ -24,13 +25,16 @@ export default class ExternalExtension extends Extension { return data.joinPath('extension'); } - private getListOfUserDefinedExtensions(): {name: string, code: string}[] { + private getListOfUserDefinedExtensions(): {name: string; code: string}[] { const basePath = this.getExtensionsBasePath(); if (fs.existsSync(basePath)) { - return fs.readdirSync(basePath).filter((f) => f.endsWith('.js')).map((fileName) => { - const extensionFilePath = path.join(basePath, fileName); - return {'name': fileName, 'code': fs.readFileSync(extensionFilePath, 'utf-8')}; - }); + return fs + .readdirSync(basePath) + .filter((f) => f.endsWith('.js')) + .map((fileName) => { + const extensionFilePath = path.join(basePath, fileName); + return {name: fileName, code: fs.readFileSync(extensionFilePath, 'utf-8')}; + }); } else { return []; } @@ -88,8 +92,7 @@ export default class ExternalExtension extends Extension { @bind private async loadExtension(ConstructorClass: typeof Extension): Promise { await this.enableDisableExtension(false, ConstructorClass.name); // @ts-ignore - await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState, - this.eventBus, settings, logger)); + await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus, settings, logger)); } private async loadUserDefinedExtensions(): Promise { @@ -100,9 +103,15 @@ export default class ExternalExtension extends Extension { private async publishExtensions(): Promise { const extensions = this.getListOfUserDefinedExtensions(); - await this.mqtt.publish('bridge/extensions', stringify(extensions), { - retain: true, - qos: 0, - }, settings.get().mqtt.base_topic, true); + await this.mqtt.publish( + 'bridge/extensions', + stringify(extensions), + { + retain: true, + qos: 0, + }, + settings.get().mqtt.base_topic, + true, + ); } } diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index a85ebb8a..7dea0802 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -1,18 +1,19 @@ -import http from 'http'; -import https from 'https'; +import bind from 'bind-decorator'; import gzipStatic, {RequestHandler} from 'connect-gzip-static'; import finalhandler from 'finalhandler'; -import logger from '../util/logger'; -import frontend from 'zigbee2mqtt-frontend'; -import WebSocket from 'ws'; +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import stringify from 'json-stable-stringify-without-jsonify'; import net from 'net'; import url from 'url'; -import fs from 'fs'; +import WebSocket from 'ws'; +import frontend from 'zigbee2mqtt-frontend'; + +import logger from '../util/logger'; import * as settings from '../util/settings'; import utils from '../util/utils'; -import stringify from 'json-stable-stringify-without-jsonify'; import Extension from './extension'; -import bind from 'bind-decorator'; /** * This extension servers the frontend @@ -30,17 +31,24 @@ export default class Frontend extends Extension { private fileServer: RequestHandler; private wss: WebSocket.Server = null; - constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState, - eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise, - restartCallback: () => Promise, addExtension: (extension: Extension) => Promise) { + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + ) { super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage); } - private isHttpsConfigured():boolean { + private isHttpsConfigured(): boolean { if (this.sslCert && this.sslKey) { if (!fs.existsSync(this.sslCert) || !fs.existsSync(this.sslKey)) { - logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`); /* eslint-disable-line max-len */ + logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`); return false; } return true; @@ -48,12 +56,12 @@ export default class Frontend extends Extension { return false; } - override async start(): Promise { if (this.isHttpsConfigured()) { const serverOptions = { key: fs.readFileSync(this.sslKey), - cert: fs.readFileSync(this.sslCert)}; + cert: fs.readFileSync(this.sslCert), + }; this.server = https.createServer(serverOptions, this.onRequest); } else { this.server = http.createServer(this.onRequest); diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index 15a8f0b0..ea60ad82 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -1,31 +1,42 @@ -import * as settings from '../util/settings'; -import logger from '../util/logger'; -import utils from '../util/utils'; -import stringify from 'json-stable-stringify-without-jsonify'; -import equals from 'fast-deep-equal/es6'; import bind from 'bind-decorator'; -import Extension from './extension'; +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 * as zhc from 'zigbee-herdsman-converters'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`); const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`); const LEGACY_TOPIC_REGEX_REMOVE_ALL = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`); const STATE_PROPERTIES: Readonly boolean>> = { - 'state': () => true, - 'brightness': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')), - 'color_temp': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')), - 'color': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')), - 'color_mode': (value, exposes) => exposes.some((e) => e.type === 'light' && - ((e.features.some((f) => f.name === `color_${value}`)) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp')))), + state: () => true, + brightness: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')), + color_temp: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')), + color: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')), + color_mode: (value, exposes) => + exposes.some( + (e) => + e.type === 'light' && + (e.features.some((f) => f.name === `color_${value}`) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp'))), + ), }; interface ParsedMQTTMessage { - type: 'remove' | 'add' | 'remove_all', resolvedEntityGroup: Group, resolvedEntityDevice: Device, - error: string, groupKey: string, deviceKey: string, triggeredViaLegacyApi: boolean, - skipDisableReporting: boolean, resolvedEntityEndpoint: zh.Endpoint, + type: 'remove' | 'add' | 'remove_all'; + resolvedEntityGroup: Group; + resolvedEntityDevice: Device; + error: string; + groupKey: string; + deviceKey: string; + triggeredViaLegacyApi: boolean; + skipDisableReporting: boolean; + resolvedEntityEndpoint: zh.Endpoint; } export default class Groups extends Extension { @@ -42,8 +53,13 @@ export default class Groups extends Extension { const settingsGroups = settings.getGroups(); const zigbeeGroups = this.zigbee.groups(); - const addRemoveFromGroup = async (action: 'add' | 'remove', deviceName: string, groupName: string | number, - endpoint: zh.Endpoint, group: Group): Promise => { + const addRemoveFromGroup = async ( + action: 'add' | 'remove', + deviceName: string, + groupName: string | number, + endpoint: zh.Endpoint, + group: Group, + ): Promise => { try { logger.info(`${action === 'add' ? 'Adding' : 'Removing'} '${deviceName}' to group '${groupName}'`); @@ -119,7 +135,8 @@ export default class Groups extends Extension { let endpointName: string = null; const endpointNames: string[] = data.entity instanceof Device ? data.entity.getEndpointNames() : []; - for (let [prop, value] of Object.entries(data.update)) { + for (let prop of Object.keys(data.update)) { + const value = data.update[prop]; const endpointNameMatch = endpointNames.find((n) => prop.endsWith(`_${n}`)); if (endpointNameMatch) { @@ -140,8 +157,11 @@ export default class Groups extends Extension { if (entity instanceof Device) { for (const group of groups) { - if (group.zh.hasMember(entity.endpoint(endpointName)) && !equals(this.lastOptimisticState[group.ID], payload) && - this.shouldPublishPayloadForGroup(group, payload)) { + if ( + group.zh.hasMember(entity.endpoint(endpointName)) && + !equals(this.lastOptimisticState[group.ID], payload) && + this.shouldPublishPayloadForGroup(group, payload) + ) { this.lastOptimisticState[group.ID] = payload; await this.publishEntityState(group, payload, reason); @@ -197,7 +217,7 @@ export default class Groups extends Extension { } private shouldPublishPayloadForGroup(group: Group, payload: KeyValue): boolean { - return ((group.options.off_state === 'last_member_state') || (!payload || payload.state !== 'OFF') || this.areAllMembersOff(group)); + return group.options.off_state === 'last_member_state' || !payload || payload.state !== 'OFF' || this.areAllMembersOff(group); } private areAllMembersOff(group: Group): boolean { @@ -263,7 +283,7 @@ export default class Groups extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'entity doesn\'t exists'}; + const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: "entity doesn't exists"}; await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); } @@ -309,8 +329,15 @@ export default class Groups extends Extension { } return { - resolvedEntityGroup, resolvedEntityDevice, type, error, groupKey, deviceKey, - triggeredViaLegacyApi, skipDisableReporting, resolvedEntityEndpoint, + resolvedEntityGroup, + resolvedEntityDevice, + type, + error, + groupKey, + deviceKey, + triggeredViaLegacyApi, + skipDisableReporting, + resolvedEntityEndpoint, }; } @@ -321,10 +348,17 @@ export default class Groups extends Extension { return; } - let { - resolvedEntityGroup, resolvedEntityDevice, type, error, triggeredViaLegacyApi, - groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint, + const { + resolvedEntityGroup, + resolvedEntityDevice, + type, + triggeredViaLegacyApi, + groupKey, + deviceKey, + skipDisableReporting, + resolvedEntityEndpoint, } = parsed; + let error = parsed.error; let changedGroups: Group[] = []; if (!error) { @@ -369,7 +403,8 @@ export default class Groups extends Extension { await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove`, message})); } - } else { // remove_all + } else { + // remove_all logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`); changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint)); await resolvedEntityEndpoint.removeFromAllGroups(); diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 5b900bb2..966cf99b 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1,16 +1,25 @@ -import * as settings from '../util/settings'; -import logger from '../util/logger'; -import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils'; -import stringify from 'json-stable-stringify-without-jsonify'; import assert from 'assert'; -import Extension from './extension'; import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; import * as zhc from 'zigbee-herdsman-converters'; -interface MockProperty {property: string, value: KeyValue | string} +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils'; +import Extension from './extension'; + +interface MockProperty { + property: string; + value: KeyValue | string; +} // eslint-disable-next-line camelcase -interface DiscoveryEntry {mockProperties: MockProperty[], type: string, object_id: string, discovery_payload: KeyValue} +interface DiscoveryEntry { + mockProperties: MockProperty[]; + type: string; + object_id: string; + discovery_payload: KeyValue; +} const sensorClick: DiscoveryEntry = { type: 'sensor', @@ -24,10 +33,10 @@ const sensorClick: DiscoveryEntry = { }; interface Discovered { - mockProperties: Set, - messages: {[s: string]: {payload: string, published: boolean}}, - triggers: Set, - discovered: boolean, + mockProperties: Set; + messages: {[s: string]: {payload: string; published: boolean}}; + triggers: Set; + discovered: boolean; } const ACCESS_STATE = 0b001; @@ -37,11 +46,36 @@ const defaultStatusTopic = 'homeassistant/status'; const legacyMapping = [ { - models: ['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S', - 'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045', - 'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2', - 'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', - 'E1743'], + models: [ + 'WXKG01LM', + 'HS1EB/HS1EB-E', + 'ICZB-KPD14S', + 'TERNCY-SD01', + 'TERNCY-PP01', + 'ICZB-KPD18S', + 'E1766', + 'ZWallRemote0', + 'ptvo.switch', + '2AJZ4KPKEY', + 'ZGRC-KEY-013', + 'HGZB-02S', + 'HGZB-045', + 'HGZB-1S', + 'AV2010/34', + 'IM6001-BTP01', + 'WXKG11LM', + 'WXKG03LM', + 'WXKG02LM_rev1', + 'WXKG02LM_rev2', + 'QBKG04LM', + 'QBKG03LM', + 'QBKG11LM', + 'QBKG21LM', + 'QBKG22LM', + 'WXKG12LM', + 'QBKG12LM', + 'E1743', + ], discovery: sensorClick, }, { @@ -78,16 +112,26 @@ class Bridge { private discoveryEntries: DiscoveryEntry[]; readonly options: { - ID?: string, - homeassistant?: KeyValue, + ID?: string; + homeassistant?: KeyValue; }; /* eslint-disable brace-style */ - get ID(): string {return this.coordinatorIeeeAddress;} - get name(): string {return 'bridge';} - get hardwareVersion(): string {return this.coordinatorType;} - get firmwareVersion(): string {return this.coordinatorFirmwareVersion;} - get configs(): DiscoveryEntry[] {return this.discoveryEntries;} + get ID(): string { + return this.coordinatorIeeeAddress; + } + get name(): string { + return 'bridge'; + } + get hardwareVersion(): string { + return this.coordinatorType; + } + get firmwareVersion(): string { + return this.coordinatorFirmwareVersion; + } + get configs(): DiscoveryEntry[] { + return this.discoveryEntries; + } constructor(ieeeAdress: string, version: zh.CoordinatorVersion, discovery: DiscoveryEntry[]) { this.coordinatorIeeeAddress = ieeeAdress; @@ -104,8 +148,12 @@ class Bridge { }; } - isDevice(): this is Device {return false;} - isGroup(): this is Group {return false;} + isDevice(): this is Device { + return false; + } + isGroup(): this is Group { + return false; + } /* eslint-enable brace-style */ } @@ -120,13 +168,20 @@ export default class HomeAssistant extends Extension { private statusTopic = settings.get().homeassistant.status_topic; private entityAttributes = settings.get().homeassistant.legacy_entity_attributes; private zigbee2MQTTVersion: string; - private discoveryOrigin: {name: string, sw: string, url: string}; + private discoveryOrigin: {name: string; sw: string; url: string}; private bridge: Bridge; private bridgeIdentifier: string; - constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState, - eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise, - restartCallback: () => Promise, addExtension: (extension: Extension) => Promise) { + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + ) { super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); if (settings.get().advanced.output === 'attribute') { throw new Error('Home Assistant integration is not possible with attribute output!'); @@ -193,19 +248,21 @@ export default class HomeAssistant extends Extension { return this.discovered[ID]; } - private exposeToConfig(exposes: zhc.Expose[], entityType: 'device' | 'group', - allExposes: zhc.Expose[], definition?: zhc.Definition): DiscoveryEntry[] { + private exposeToConfig( + exposes: zhc.Expose[], + entityType: 'device' | 'group', + allExposes: zhc.Expose[], + definition?: zhc.Definition, + ): DiscoveryEntry[] { // For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features // to use for a bulb (e.g. color_xy/color_temp) assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed'); const firstExpose = exposes[0]; - assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type), - `Unsupported expose type ${firstExpose.type} for group`); + assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type), `Unsupported expose type ${firstExpose.type} for group`); const discoveryEntries: DiscoveryEntry[] = []; const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined; - const getProperty = (feature: zhc.Feature): string => entityType === 'group' ? - featurePropertyWithoutEndpoint(feature) : feature.property; + const getProperty = (feature: zhc.Feature): string => (entityType === 'group' ? featurePropertyWithoutEndpoint(feature) : feature.property); /* istanbul ignore else */ if (firstExpose.type === 'light') { @@ -216,9 +273,10 @@ export default class HomeAssistant extends Extension { const state = firstExpose.features.find((f) => f.name === 'state'); // Prefer HS over XY when at least one of the lights in the group prefers HS over XY. // A light prefers HS over XY when HS is earlier in the feature array than HS. - const preferHS = exposes.map((e) => [e.features.findIndex((ee) => ee.name === 'color_xy'), - e.features.findIndex((ee) => ee.name === 'color_hs')]) - .filter((d) => d[0] !== -1 && d[1] !== -1 && d[1] < d[0]).length !== 0; + const preferHS = + exposes + .map((e) => [e.features.findIndex((ee) => ee.name === 'color_xy'), e.features.findIndex((ee) => ee.name === 'color_hs')]) + .filter((d) => d[0] !== -1 && d[1] !== -1 && d[1] < d[0]).length !== 0; const discoveryEntry: DiscoveryEntry = { type: 'light', @@ -246,16 +304,24 @@ export default class HomeAssistant extends Extension { } if (hasColorTemp) { - const colorTemps = exposes.map((expose) => expose.features.find((e) => e.name === 'color_temp')) - .filter((e) => e).filter(isNumericExposeFeature); + const colorTemps = exposes + .map((expose) => expose.features.find((e) => e.name === 'color_temp')) + .filter((e) => e) + .filter(isNumericExposeFeature); const max = Math.min(...colorTemps.map((e) => e.value_max)); const min = Math.max(...colorTemps.map((e) => e.value_min)); discoveryEntry.discovery_payload.max_mireds = max; discoveryEntry.discovery_payload.min_mireds = min; } - const effects = utils.arrayUnique(utils.flatten( - allExposes.filter(isEnumExposeFeature).filter((e) => e.name === 'effect').map((e) => e.values))); + const effects = utils.arrayUnique( + utils.flatten( + allExposes + .filter(isEnumExposeFeature) + .filter((e) => e.name === 'effect') + .map((e) => e.values), + ), + ); if (effects.length) { discoveryEntry.discovery_payload.effect = true; discoveryEntry.discovery_payload.effect_list = effects; @@ -295,8 +361,7 @@ export default class HomeAssistant extends Extension { discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'climate') { const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint']; - const setpoint = firstExpose.features.filter(isNumericExposeFeature) - .find((f) => setpointProperties.includes(f.name)); + const setpoint = firstExpose.features.filter(isNumericExposeFeature).find((f) => setpointProperties.includes(f.name)); assert(setpoint, 'No setpoint found'); const temperature = firstExpose.features.find((f) => f.name === 'local_temperature'); assert(temperature, 'No temperature found'); @@ -339,45 +404,39 @@ export default 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.features.find((f) => f.name === 'occupied_cooling_setpoint'); if (coolingSetpoint) { discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_low_state_template = - `{{ value_json.${setpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json.${setpoint.property} }}`; discoveryEntry.discovery_payload.temperature_low_state_topic = true; discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name; - discoveryEntry.discovery_payload.temperature_high_state_template = - `{{ value_json.${coolingSetpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json.${coolingSetpoint.property} }}`; discoveryEntry.discovery_payload.temperature_high_state_topic = true; } else { discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_state_template = - `{{ value_json.${setpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json.${setpoint.property} }}`; discoveryEntry.discovery_payload.temperature_state_topic = true; } - const fanMode = firstExpose.features.filter(isEnumExposeFeature) - .find((f) => f.name === 'fan_mode'); + const fanMode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'fan_mode'); if (fanMode) { discoveryEntry.discovery_payload.fan_modes = fanMode.values; discoveryEntry.discovery_payload.fan_mode_command_topic = true; - discoveryEntry.discovery_payload.fan_mode_state_template = - `{{ value_json.${fanMode.property} }}`; + discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json.${fanMode.property} }}`; discoveryEntry.discovery_payload.fan_mode_state_topic = true; } - const swingMode = firstExpose.features.filter(isEnumExposeFeature) - .find((f) => f.name === 'swing_mode'); + const swingMode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'swing_mode'); if (swingMode) { discoveryEntry.discovery_payload.swing_modes = swingMode.values; discoveryEntry.discovery_payload.swing_mode_command_topic = true; - discoveryEntry.discovery_payload.swing_mode_state_template = - `{{ value_json.${swingMode.property} }}`; + discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json.${swingMode.property} }}`; discoveryEntry.discovery_payload.swing_mode_state_topic = true; } @@ -385,13 +444,11 @@ export default class HomeAssistant extends Extension { if (preset) { discoveryEntry.discovery_payload.preset_modes = preset.values; discoveryEntry.discovery_payload.preset_mode_command_topic = 'preset'; - discoveryEntry.discovery_payload.preset_mode_value_template = - `{{ value_json.${preset.property} }}`; + discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${preset.property} }}`; discoveryEntry.discovery_payload.preset_mode_state_topic = true; } - const tempCalibration = firstExpose.features.filter(isNumericExposeFeature) - .find((f) => f.name === 'local_temperature_calibration'); + const tempCalibration = firstExpose.features.filter(isNumericExposeFeature).find((f) => f.name === 'local_temperature_calibration'); if (tempCalibration) { const discoveryEntry: DiscoveryEntry = { type: 'number', @@ -418,8 +475,7 @@ export default class HomeAssistant extends Extension { discoveryEntries.push(discoveryEntry); } - const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature) - .find((f) => f.name === 'pi_heating_demand'); + const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature).find((f) => f.name === 'pi_heating_demand'); if (piHeatingDemand) { const discoveryEntry: DiscoveryEntry = { type: 'sensor', @@ -480,13 +536,13 @@ export default class HomeAssistant extends Extension { discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'cover') { - const state = exposes.find((expose) => expose.features.find((e) => e.name === 'state')) - ?.features.find((f) => f.name === 'state'); - const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position')) + const state = exposes.find((expose) => expose.features.find((e) => e.name === 'state'))?.features.find((f) => f.name === 'state'); + const position = exposes + .find((expose) => expose.features.find((e) => e.name === 'position')) ?.features.find((f) => f.name === 'position'); - const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt')) - ?.features.find((f) => f.name === 'tilt'); - const motorState = allExposes?.filter(isEnumExposeFeature) + const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))?.features.find((f) => f.name === 'tilt'); + const motorState = allExposes + ?.filter(isEnumExposeFeature) .find((e) => ['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE); const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running'); @@ -506,7 +562,8 @@ export default class HomeAssistant extends Extension { // If curtains have `running` property, use this in discovery. // The movement direction is calculated (assumed) in this case. if (running) { - discoveryEntry.discovery_payload.value_template = `{% if "${running.property}" in value_json ` + + discoveryEntry.discovery_payload.value_template = + `{% if "${running.property}" in value_json ` + `and value_json.${running.property} %} {% if value_json.${position.property} > 0 %} closing ` + `{% else %} opening {% endif %} {% else %} stopped {% endif %}`; } @@ -526,7 +583,8 @@ export default 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 "${motorState.property}" in value_json ` + + discoveryEntry.discovery_payload.value_template = + `{% if "${motorState.property}" in value_json ` + `and value_json.${motorState.property} %} {{ value_json.${motorState.property} }} {% else %} ` + `${stoppedState} {% endif %}`; } @@ -534,9 +592,8 @@ export default class HomeAssistant extends Extension { // If curtains do not have `running`, `motor_state` or `moving` properties. if (!discoveryEntry.discovery_payload.value_template) { - discoveryEntry.discovery_payload.value_template = - `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`, - discoveryEntry.discovery_payload.state_open = 'OPEN'; + (discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`), + (discoveryEntry.discovery_payload.state_open = 'OPEN'); discoveryEntry.discovery_payload.state_closed = 'CLOSE'; discoveryEntry.discovery_payload.state_stopped = 'STOP'; } @@ -546,7 +603,8 @@ export default class HomeAssistant extends Extension { } if (position) { - discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, + discoveryEntry.discovery_payload = { + ...discoveryEntry.discovery_payload, position_template: `{{ value_json.${featurePropertyWithoutEndpoint(position)} }}`, set_position_template: `{ "${getProperty(position)}": {{ position }} }`, set_position_topic: true, @@ -555,7 +613,8 @@ export default class HomeAssistant extends Extension { } if (tilt) { - discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, + discoveryEntry.discovery_payload = { + ...discoveryEntry.discovery_payload, tilt_command_topic: true, tilt_status_topic: true, tilt_status_template: `{{ value_json.${featurePropertyWithoutEndpoint(tilt)} }}`, @@ -592,8 +651,9 @@ export default class HomeAssistant extends Extension { // presets "on", "auto" and "smart" to cover the remaining modes in // ZCL. This supports a generic ZCL HVAC Fan Control fan. "Off" is // always a valid speed. - let speeds = ['off'].concat(['low', 'medium', 'high', '1', '2', '3', '4', '5', - '6', '7', '8', '9'].filter((s) => speed.values.includes(s))); + let speeds = ['off'].concat( + ['low', 'medium', 'high', '1', '2', '3', '4', '5', '6', '7', '8', '9'].filter((s) => speed.values.includes(s)), + ); let presets = ['on', 'auto', 'smart'].filter((s) => speed.values.includes(s)); if (['99432'].includes(definition.model)) { @@ -613,24 +673,21 @@ export default class HomeAssistant extends Extension { discoveryEntry.discovery_payload.percentage_state_topic = true; discoveryEntry.discovery_payload.percentage_command_topic = true; - discoveryEntry.discovery_payload.percentage_value_template = - `{{ {${percentValues}}[value_json.${speed.property}] | default('None') }}`; - discoveryEntry.discovery_payload.percentage_command_template = - `{{ {${percentCommands}}[value] | default('') }}`; + discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json.${speed.property}] | default('None') }}`; + discoveryEntry.discovery_payload.percentage_command_template = `{{ {${percentCommands}}[value] | default('') }}`; discoveryEntry.discovery_payload.speed_range_min = 1; discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1; assert(presets.length !== 0); discoveryEntry.discovery_payload.preset_mode_state_topic = true; discoveryEntry.discovery_payload.preset_mode_command_topic = 'fan_mode'; discoveryEntry.discovery_payload.preset_mode_value_template = - `{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}]` + - ` else 'None' | default('None') }}`; + `{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}]` + ` else 'None' | default('None') }}`; discoveryEntry.discovery_payload.preset_modes = presets; } discoveryEntries.push(discoveryEntry); } else if (isBinaryExposeFeature(firstExpose)) { - const lookup: {[s: string]: KeyValue}= { + const lookup: {[s: string]: KeyValue} = { activity_led_indicator: {icon: 'mdi:led-on'}, auto_off: {icon: 'mdi:flash-auto'}, battery_low: {entity_category: 'diagnostic', device_class: 'battery'}, @@ -699,14 +756,13 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'switch', mockProperties: [{property: firstExpose.property, value: null}], - object_id: endpoint ? - `switch_${firstExpose.name}_${endpoint}` : - `switch_${firstExpose.name}`, + object_id: endpoint ? `switch_${firstExpose.name}_${endpoint}` : `switch_${firstExpose.name}`, discovery_payload: { name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, - value_template: typeof firstExpose.value_on === 'boolean' ? - `{% if value_json.${firstExpose.property} %} true {% else %} false {% endif %}` : - `{{ value_json.${firstExpose.property} }}`, + value_template: + typeof firstExpose.value_on === 'boolean' + ? `{% if value_json.${firstExpose.property} %} true {% else %} false {% endif %}` + : `{{ value_json.${firstExpose.property} }}`, payload_on: firstExpose.value_on.toString(), payload_off: firstExpose.value_off.toString(), command_topic: true, @@ -735,15 +791,12 @@ export default class HomeAssistant extends Extension { } } else if (isNumericExposeFeature(firstExpose)) { const lookup: {[s: string]: KeyValue} = { - ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic', - state_class: 'measurement'}, + ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic', state_class: 'measurement'}, action_duration: {icon: 'mdi:timer', device_class: 'duration'}, alarm_humidity_max: {device_class: 'humidity', entity_category: 'config', icon: 'mdi:water-plus'}, alarm_humidity_min: {device_class: 'humidity', entity_category: 'config', icon: 'mdi:water-minus'}, - alarm_temperature_max: {device_class: 'temperature', entity_category: 'config', - icon: 'mdi:thermometer-high'}, - alarm_temperature_min: {device_class: 'temperature', entity_category: 'config', - icon: 'mdi:thermometer-low'}, + alarm_temperature_max: {device_class: 'temperature', entity_category: 'config', icon: 'mdi:thermometer-high'}, + alarm_temperature_min: {device_class: 'temperature', entity_category: 'config', icon: 'mdi:thermometer-low'}, angle: {icon: 'angle-acute'}, angle_axis: {icon: 'angle-acute'}, aqi: {device_class: 'aqi', state_class: 'measurement'}, @@ -756,8 +809,7 @@ export default class HomeAssistant extends Extension { ballast_physical_minimum_level: {entity_category: 'diagnostic'}, battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'}, battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'}, - battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement', - enabled_by_default: true}, + battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement', enabled_by_default: true}, boost_heating_countdown: {device_class: 'duration'}, boost_heating_countdown_time_set: {entity_category: 'config', icon: 'mdi:timer'}, boost_time: {entity_category: 'config', icon: 'mdi:timer'}, @@ -766,7 +818,9 @@ export default class HomeAssistant extends Extension { co2: {device_class: 'carbon_dioxide', state_class: 'measurement'}, comfort_temperature: {entity_category: 'config', icon: 'mdi:thermometer'}, cpu_temperature: { - device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement', + device_class: 'temperature', + entity_category: 'diagnostic', + state_class: 'measurement', }, cube_side: {icon: 'mdi:cube'}, current: { @@ -790,7 +844,9 @@ export default class HomeAssistant extends Extension { deadzone_temperature: {entity_category: 'config', icon: 'mdi:thermometer'}, detection_interval: {icon: 'mdi:timer'}, device_temperature: { - device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement', + device_class: 'temperature', + entity_category: 'diagnostic', + state_class: 'measurement', }, duration: {entity_category: 'config', icon: 'mdi:timer'}, eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'}, @@ -832,18 +888,21 @@ export default class HomeAssistant extends Extension { people: {state_class: 'measurement', icon: 'mdi:account-multiple'}, position: {icon: 'mdi:valve', state_class: 'measurement'}, power: {device_class: 'power', entity_category: 'diagnostic', state_class: 'measurement'}, - power_factor: {device_class: 'power_factor', enabled_by_default: false, - entity_category: 'diagnostic', state_class: 'measurement'}, + power_factor: {device_class: 'power_factor', enabled_by_default: false, entity_category: 'diagnostic', state_class: 'measurement'}, power_outage_count: {icon: 'mdi:counter', enabled_by_default: false}, precision: {entity_category: 'config', icon: 'mdi:decimal-comma-increase'}, pressure: {device_class: 'atmospheric_pressure', state_class: 'measurement'}, presence_timeout: {entity_category: 'config', icon: 'mdi:timer'}, reporting_time: {entity_category: 'config', icon: 'mdi:clock-time-one-outline'}, requested_brightness_level: { - enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5', + enabled_by_default: false, + entity_category: 'diagnostic', + icon: 'mdi:brightness-5', }, requested_brightness_percent: { - enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5', + enabled_by_default: false, + entity_category: 'diagnostic', + icon: 'mdi:brightness-5', }, smoke_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'}, soil_moisture: {device_class: 'moisture', state_class: 'measurement'}, @@ -892,7 +951,6 @@ export default class HomeAssistant extends Extension { Object.assign(extraAttrs, {device_class: 'energy', state_class: 'total_increasing'}); } - const allowsSet = firstExpose.access & ACCESS_SET; let key = firstExpose.name; @@ -918,8 +976,7 @@ export default class HomeAssistant extends Extension { // When a device_class is set, unit_of_measurement must be set, otherwise warnings are generated. // https://github.com/Koenkk/zigbee2mqtt/issues/15958#issuecomment-1377483202 - if (discoveryEntry.discovery_payload.device_class && - !discoveryEntry.discovery_payload.unit_of_measurement) { + if (discoveryEntry.discovery_payload.device_class && !discoveryEntry.discovery_payload.unit_of_measurement) { delete discoveryEntry.discovery_payload.device_class; } @@ -1008,8 +1065,7 @@ export default class HomeAssistant extends Extension { week: {entity_category: 'config', icon: 'mdi:calendar-clock'}, }; - const valueTemplate = firstExpose.access & ACCESS_STATE ? - `{{ value_json.${firstExpose.property} }}` : undefined; + const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; if (firstExpose.access & ACCESS_STATE) { discoveryEntries.push({ @@ -1078,8 +1134,7 @@ export default class HomeAssistant extends Extension { color_options: {icon: 'mdi:palette'}, level_config: {entity_category: 'diagnostic'}, programming_mode: {icon: 'mdi:calendar-clock'}, - program: {value_template: `{{ value_json.${firstExpose.property}|default('',True) ` + - `| truncate(254, True, '', 0) }}`}, + program: {value_template: `{{ value_json.${firstExpose.property}|default('',True) ` + `| truncate(254, True, '', 0) }}`}, schedule_settings: {icon: 'mdi:calendar-clock'}, }; if (firstExpose.access & ACCESS_STATE) { @@ -1119,9 +1174,9 @@ export default 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'); + discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'config')); } else if (firstExpose.category === 'diagnostic') { - discoveryEntries.forEach((d) => d.discovery_payload.entity_category = 'diagnostic'); + discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'diagnostic')); } discoveryEntries.forEach((d) => { @@ -1273,12 +1328,15 @@ export default class HomeAssistant extends Extension { // @ts-ignore configs.push(entity.definition.homeassistant); } - } else if (isGroup) { // group + } else if (isGroup) { + // group const exposesByType: {[s: string]: zhc.Expose[]} = {}; const allExposes: zhc.Expose[] = []; - entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device) - .filter((d) => d.definition).forEach((device) => { + entity.zh.members + .map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device) + .filter((d) => d.definition) + .forEach((device) => { const exposes = device.exposes(); allExposes.push(...exposes); for (const expose of exposes.filter((e) => groupSupportedTypes.includes(e.type))) { @@ -1295,8 +1353,7 @@ export default class HomeAssistant extends Extension { } }); - configs = [].concat(...Object.values(exposesByType) - .map((exposes) => this.exposeToConfig(exposes, 'group', allExposes))); + configs = [].concat(...Object.values(exposesByType).map((exposes) => this.exposeToConfig(exposes, 'group', allExposes))); } else { // Discover bridge config. configs.push(...entity.configs); @@ -1370,8 +1427,7 @@ export default class HomeAssistant extends Extension { value_template: `{{ value_json['update']['installed_version'] }}`, latest_version_template: `{{ value_json['update']['latest_version'] }}`, json_attributes_topic: `${settings.get().mqtt.base_topic}/${entity.name}`, // state topic - json_attributes_template: - `{"in_progress": {{ iif(value_json['update']['state'] == 'updating', 'true', 'false') }} }`, + json_attributes_template: `{"in_progress": {{ iif(value_json['update']['state'] == 'updating', 'true', 'false') }} }`, }, }; configs.push(updateSensor); @@ -1431,8 +1487,10 @@ export default class HomeAssistant extends Extension { if (isGroup && entity.zh.members.length === 0) { return; - } else if (isDevice && (!entity.definition || entity.zh.interviewing || - (entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant))) { + } else if ( + isDevice && + (!entity.definition || entity.zh.interviewing || (entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant)) + ) { return; } @@ -1467,7 +1525,7 @@ export default class HomeAssistant extends Extension { payload.tilt_status_topic = stateTopic; } - if (this.entityAttributes && (isDevice || isGroup) ) { + if (this.entityAttributes && (isDevice || isGroup)) { payload.json_attributes_topic = stateTopic; } @@ -1494,23 +1552,24 @@ export default class HomeAssistant extends Extension { payload.origin = this.discoveryOrigin; // Availability payload (can be disabled by setting `payload.availability = false`). - if (! payload.hasOwnProperty('availability') || payload.availability) { + if (!payload.hasOwnProperty('availability') || payload.availability) { payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}]; - if (isDevice||isGroup) { + if (isDevice || isGroup) { if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) { payload.availability_mode = 'all'; payload.availability.push({topic: `${baseTopic}/availability`}); } - } else { // Bridge availability is different. + } else { + // Bridge availability is different. payload.availability_mode = 'all'; } if (isDevice && entity.options.disabled) { // Mark disabled device always as unavailable - payload.availability.forEach((a: KeyValue) => a.value_template = '{{ "offline" }}'); + payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ "offline" }}')); } else if (!settings.get().advanced.legacy_availability_payload) { - payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}'); + payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ value_json.state }}')); } } else { delete payload.availability; @@ -1559,18 +1618,15 @@ export default class HomeAssistant extends Extension { } if (payload.temperature_command_topic) { - payload.temperature_command_topic = - `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_command_topic}`; + payload.temperature_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_command_topic}`; } if (payload.temperature_low_command_topic) { - payload.temperature_low_command_topic = - `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_low_command_topic}`; + payload.temperature_low_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_low_command_topic}`; } if (payload.temperature_high_command_topic) { - payload.temperature_high_command_topic = - `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_high_command_topic}`; + payload.temperature_high_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_high_command_topic}`; } if (payload.fan_mode_state_topic) { @@ -1606,8 +1662,7 @@ export default 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) { @@ -1622,8 +1677,7 @@ export default class HomeAssistant extends Extension { return; } else if (ignoreName && key === 'name') { return; - } else if (['number', 'string', 'boolean'].includes(typeof obj[key]) || - Array.isArray(obj[key])) { + } else if (['number', 'string', 'boolean'].includes(typeof obj[key]) || Array.isArray(obj[key])) { payload[key] = obj[key]; } else if (obj[key] === null) { delete payload[key]; @@ -1680,8 +1734,7 @@ export default class HomeAssistant extends Extension { return; } - if (!isDeviceAutomation && - (!message.availability || !message.availability[0].topic.startsWith(baseTopic))) { + if (!isDeviceAutomation && (!message.availability || !message.availability[0].topic.startsWith(baseTopic))) { return; } } catch (e) { @@ -1691,7 +1744,7 @@ export default class HomeAssistant extends Extension { // Group discovery topic uses "ENCODEDBASETOPIC_GROUPID", device use ieeeAddr const ID = discoveryMatch[2].includes('_') ? discoveryMatch[2].split('_')[1] : discoveryMatch[2]; const entity = ID === this.bridge.ID ? this.bridge : this.zigbee.resolveEntity(ID); - let clear = !entity || entity.isDevice() && !entity.definition; + let clear = !entity || (entity.isDevice() && !entity.definition); // Only save when topic matches otherwise config is not updated when renamed by editing configuration.yaml if (entity) { @@ -1716,8 +1769,7 @@ export default class HomeAssistant extends Extension { } else { this.getDiscovered(entity).messages[topic] = {payload: stringify(message), published: true}; } - } else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && - data.message.toLowerCase() === 'online') { + } else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') { const timer = setTimeout(async () => { // Publish all device states. for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) { @@ -1762,8 +1814,7 @@ export default class HomeAssistant extends Extension { } private getDevicePayload(entity: Device | Group | Bridge): KeyValue { - const identifierPostfix = entity.isGroup() ? - `zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt'; + const identifierPostfix = entity.isGroup() ? `zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt'; // Allow device name to be overridden by homeassistant config let deviceName = entity.name; @@ -1830,7 +1881,11 @@ export default class HomeAssistant extends Extension { } private getEncodedBaseTopic(): string { - return settings.get().mqtt.base_topic.split('').map((s) => s.charCodeAt(0).toString()).join(''); + return settings + .get() + .mqtt.base_topic.split('') + .map((s) => s.charCodeAt(0).toString()) + .join(''); } private getDiscoveryTopic(config: DiscoveryEntry, entity: Device | Group | Bridge): string { @@ -1838,11 +1893,12 @@ export default class HomeAssistant extends Extension { return `${config.type}/${key}/${config.object_id}/config`; } - private async publishDeviceTriggerDiscover(device: Device, key: string, value: string, force=false): Promise { + private async publishDeviceTriggerDiscover(device: Device, key: string, value: string, force = false): Promise { const haConfig = device.options.homeassistant; - if (device.options.hasOwnProperty('homeassistant') && (haConfig == null || - (haConfig.hasOwnProperty('device_automation') && typeof haConfig === 'object' && - haConfig.device_automation == null))) { + if ( + device.options.hasOwnProperty('homeassistant') && + (haConfig == null || (haConfig.hasOwnProperty('device_automation') && typeof haConfig === 'object' && haConfig.device_automation == null)) + ) { return; } @@ -1944,8 +2000,7 @@ export default class HomeAssistant extends Extension { state_topic_postfix: 'info', value_template: '{{ value_json.log_level | lower }}', command_topic: `${baseTopic}/request/options`, - command_template: - '{"options": {"advanced": {"log_level": "{{ value }}" } } }', + command_template: '{"options": {"advanced": {"log_level": "{{ value }}" } } }', options: settings.LOG_LEVELS, }, }, @@ -1962,7 +2017,6 @@ export default class HomeAssistant extends Extension { state_topic_postfix: 'info', value_template: '{{ value_json.version }}', }, - }, { type: 'sensor', @@ -1977,7 +2031,6 @@ export default class HomeAssistant extends Extension { state_topic_postfix: 'info', value_template: '{{ value_json.coordinator.meta.revision }}', }, - }, { type: 'sensor', @@ -1989,7 +2042,7 @@ export default class HomeAssistant extends Extension { enabled_by_default: false, state_topic: true, state_topic_postfix: 'response/networkmap', - value_template: '{{ now().strftime(\'%Y-%m-%d %H:%M:%S\') }}', + value_template: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}", json_attributes_topic: `${baseTopic}/response/networkmap`, json_attributes_template: '{{ value_json.data.value | tojson }}', }, @@ -2005,8 +2058,7 @@ export default class HomeAssistant extends Extension { entity_category: 'diagnostic', state_topic: true, state_topic_postfix: 'info', - value_template: - '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', + value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', }, }, diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index d46b0f16..e8240b4c 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -1,13 +1,13 @@ -import * as settings from '../../util/settings'; -import logger from '../../util/logger'; -import utils from '../../util/utils'; import assert from 'assert'; -import Extension from '../extension'; -import stringify from 'json-stable-stringify-without-jsonify'; import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; -const configRegex = - new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`); +import logger from '../../util/logger'; +import * as settings from '../../util/settings'; +import utils from '../../util/utils'; +import Extension from '../extension'; + +const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`); export default class BridgeLegacy extends Extension { private lastJoinedDeviceName: string = null; @@ -15,24 +15,24 @@ export default class BridgeLegacy extends Extension { override async start(): Promise { this.supportedOptions = { - 'permit_join': this.permitJoin, - 'last_seen': this.lastSeen, - 'elapsed': this.elapsed, - 'reset': this.reset, - 'log_level': this.logLevel, - 'devices': this.devices, - 'groups': this.groups, + permit_join: this.permitJoin, + last_seen: this.lastSeen, + elapsed: this.elapsed, + reset: this.reset, + log_level: this.logLevel, + devices: this.devices, + groups: this.groups, 'devices/get': this.devices, - 'rename': this.rename, - 'rename_last': this.renameLast, - 'remove': this.remove, - 'force_remove': this.forceRemove, - 'ban': this.ban, - 'device_options': this.deviceOptions, - 'add_group': this.addGroup, - 'remove_group': this.removeGroup, - 'force_remove_group': this.removeGroup, - 'whitelist': this.whitelist, + rename: this.rename, + rename_last: this.renameLast, + remove: this.remove, + force_remove: this.forceRemove, + ban: this.ban, + device_options: this.deviceOptions, + add_group: this.addGroup, + remove_group: this.removeGroup, + force_remove_group: this.removeGroup, + whitelist: this.whitelist, 'touchlink/factory_reset': this.touchlinkFactoryReset, }; @@ -51,10 +51,7 @@ export default class BridgeLegacy extends Extension { assert(entity, `Entity '${message}' does not exist`); settings.addDeviceToPasslist(entity.ID.toString()); logger.info(`Whitelisted '${entity.friendly_name}'`); - await this.mqtt.publish( - 'bridge/log', - stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendly_name}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendly_name}})); } catch (error) { logger.error(`Failed to whitelist '${message}' '${error}'`); } @@ -162,9 +159,7 @@ export default class BridgeLegacy extends Extension { }); if (topic.split('/').pop() == 'get') { - await this.mqtt.publish( - `bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false, - ); + await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false); } else { await this.mqtt.publish('bridge/log', stringify({type: 'devices', message: devices})); } @@ -179,8 +174,7 @@ export default class BridgeLegacy extends Extension { } @bind async rename(topic: string, message: string): Promise { - const invalid = - `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`; + const invalid = `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`; let json = null; try { @@ -218,10 +212,7 @@ export default class BridgeLegacy extends Extension { this.eventBus.emitEntityRenamed({homeAssisantRename: false, from, to, entity}); } - await this.mqtt.publish( - 'bridge/log', - stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}})); } catch (error) { logger.error(`Failed to rename - ${from} to ${to}`); } @@ -377,40 +368,25 @@ export default class BridgeLegacy extends Extension { } if (type === 'deviceJoined') { - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}})); } else if (type === 'deviceInterview') { if (data.status === 'successful') { if (resolvedEntity.isSupported) { const {vendor, description, model} = resolvedEntity.definition; const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `pairing`, message: 'interview_successful', meta: log}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta: log})); } else { const meta = {friendly_name: resolvedEntity.name, supported: false}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `pairing`, message: 'interview_successful', meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta})); } } else if (data.status === 'failed') { const meta = {friendly_name: resolvedEntity.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `pairing`, message: 'interview_failed', meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_failed', meta})); } else { /* istanbul ignore else */ if (data.status === 'started') { const meta = {friendly_name: resolvedEntity.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `pairing`, message: 'interview_started', meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_started', meta})); } } } else if (type === 'deviceAnnounce') { @@ -421,34 +397,22 @@ export default class BridgeLegacy extends Extension { if (type === 'deviceLeave') { const name = data.ieeeAddr; const meta = {friendly_name: name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_removed`, message: 'left_network', meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `device_removed`, message: 'left_network', meta})); } } } @bind async touchlinkFactoryReset(): Promise { logger.info('Starting touchlink factory reset...'); - await this.mqtt.publish( - 'bridge/log', - stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}})); const result = await this.zigbee.touchlinkFactoryResetFirst(); if (result) { logger.info('Successfully factory reset device through Touchlink'); - await this.mqtt.publish( - 'bridge/log', - stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}})); } else { logger.warning('Failed to factory reset device through Touchlink'); - await this.mqtt.publish( - 'bridge/log', - stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}})); } } } diff --git a/lib/extension/legacy/deviceGroupMembership.ts b/lib/extension/legacy/deviceGroupMembership.ts index 2697940d..39ab5352 100644 --- a/lib/extension/legacy/deviceGroupMembership.ts +++ b/lib/extension/legacy/deviceGroupMembership.ts @@ -1,9 +1,10 @@ /* istanbul ignore file */ -import * as settings from '../../util/settings'; -import logger from '../../util/logger'; -import Extension from '../extension'; import bind from 'bind-decorator'; + import Device from '../../model/device'; +import logger from '../../util/logger'; +import * as settings from '../../util/settings'; +import Extension from '../extension'; const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`); @@ -31,16 +32,14 @@ export default class DeviceGroupMembership extends Extension { return; } - const response = await endpoint.command( - `genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {}, - ); + const response = await endpoint.command(`genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {}); if (!response) { logger.warning(`Couldn't get group membership of ${device.ieeeAddr}`); return; } - let {grouplist, capacity} = response; + let {grouplist} = response; grouplist = grouplist.map((gid: string) => { const g = settings.getGroup(gid); @@ -49,13 +48,13 @@ export default class DeviceGroupMembership extends Extension { const msgGroupList = `${device.ieeeAddr} is in groups [${grouplist}]`; let msgCapacity; - if (capacity === 254) { + if (response.capacity === 254) { msgCapacity = 'it can be a part of at least 1 more group'; } else { - msgCapacity = `its remaining group capacity is ${capacity === 255 ? 'unknown' : capacity}`; + msgCapacity = `its remaining group capacity is ${response.capacity === 255 ? 'unknown' : response.capacity}`; } logger.info(`${msgGroupList} and ${msgCapacity}`); - await this.publishEntityState(device, {group_list: grouplist, group_capacity: capacity}); + await this.publishEntityState(device, {group_list: grouplist, group_capacity: response.capacity}); } } diff --git a/lib/extension/legacy/report.ts b/lib/extension/legacy/report.ts index 9ac4666e..c27e2831 100644 --- a/lib/extension/legacy/report.ts +++ b/lib/extension/legacy/report.ts @@ -1,10 +1,13 @@ import * as zhc from 'zigbee-herdsman-converters'; + import logger from '../../util/logger'; import * as settings from '../../util/settings'; import Extension from '../extension'; const defaultConfiguration = { - minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1, + minimumReportInterval: 3, + maximumReportInterval: 300, + reportableChange: 1, }; const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM'); @@ -19,43 +22,47 @@ const devicesNotSupportingReporting = [ const reportKey = 1; -const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean, colorXY: boolean}> => { +const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => { if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') === undefined) { await endpoint.read('lightingColorCtrl', ['colorCapabilities']); } const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') as number; return { - colorTemperature: (value & 1<<4) > 0, - colorXY: (value & 1<<3) > 0, + colorTemperature: (value & (1 << 4)) > 0, + colorXY: (value & (1 << 3)) > 0, }; }; -const clusters: {[s: string]: - {attribute: string, minimumReportInterval: number, maximumReportInterval: number, reportableChange: number - condition?: (endpoint: zh.Endpoint) => Promise}[]} = -{ - 'genOnOff': [ - {attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0}, - ], - 'genLevelCtrl': [ - {attribute: 'currentLevel', ...defaultConfiguration}, - ], - 'lightingColorCtrl': [ +const clusters: { + [s: string]: { + attribute: string; + minimumReportInterval: number; + maximumReportInterval: number; + reportableChange: number; + condition?: (endpoint: zh.Endpoint) => Promise; + }[]; +} = { + genOnOff: [{attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0}], + genLevelCtrl: [{attribute: 'currentLevel', ...defaultConfiguration}], + lightingColorCtrl: [ { - attribute: 'colorTemperature', ...defaultConfiguration, + attribute: 'colorTemperature', + ...defaultConfiguration, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorTemperature, }, { - attribute: 'currentX', ...defaultConfiguration, + attribute: 'currentX', + ...defaultConfiguration, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, { - attribute: 'currentY', ...defaultConfiguration, + attribute: 'currentY', + ...defaultConfiguration, condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, ], - 'closuresWindowCovering': [ + closuresWindowCovering: [ {attribute: 'currentPositionLiftPercentage', ...defaultConfiguration}, {attribute: 'currentPositionTiltPercentage', ...defaultConfiguration}, ], @@ -86,28 +93,25 @@ export default class Report extends Extension { try { for (const ep of device.zh.endpoints) { for (const [cluster, configuration] of Object.entries(clusters)) { - if (ep.supportsInputCluster(cluster) && - !this.shouldIgnoreClusterForDevice(cluster, device.definition)) { + if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, device.definition)) { logger.debug(`${term1} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); const items = []; for (const entry of configuration) { if (!entry.hasOwnProperty('condition') || (await entry.condition(ep))) { const toAdd = {...entry}; - if (!this.enabled) toAdd.maximumReportInterval = 0xFFFF; + if (!this.enabled) toAdd.maximumReportInterval = 0xffff; items.push(toAdd); delete items[items.length - 1].condition; } } - this.enabled ? - await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint()) : - await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint()); + this.enabled + ? await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint()) + : await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint()); await ep.configureReporting(cluster, items); - logger.info( - `Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`, - ); + logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); } } } @@ -121,9 +125,7 @@ export default class Report extends Extension { this.eventBus.emitDevicesChanged(); } catch (error) { - logger.error( - `Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`, - ); + logger.error(`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`); this.failed.add(device.ieeeAddr); } @@ -143,26 +145,26 @@ export default class Report extends Extension { // else reconfigure is done in zigbee-herdsman-converters ikea.js/bulbOnEvent // configuredReportings are saved since Zigbee2MQTT 1.17.0 // https://github.com/Koenkk/zigbee2mqtt/issues/966 - if (this.enabled && messageType === 'deviceAnnounce' && device.isIkeaTradfri() && - device.zh.endpoints.filter((e) => e.configuredReportings.length === 0).length === - device.zh.endpoints.length) { + if ( + this.enabled && + messageType === 'deviceAnnounce' && + device.isIkeaTradfri() && + device.zh.endpoints.filter((e) => e.configuredReportings.length === 0).length === device.zh.endpoints.length + ) { return true; } // These do not support reproting. // https://github.com/Koenkk/zigbee-herdsman/issues/110 const philipsIgnoreSw = ['5.127.1.26581', '5.130.1.30000']; - if (device.zh.manufacturerName === 'Philips' && - philipsIgnoreSw.includes(device.zh.softwareBuildID)) return false; + if (device.zh.manufacturerName === 'Philips' && philipsIgnoreSw.includes(device.zh.softwareBuildID)) return false; if (device.zh.interviewing === true) return false; if (device.zh.type !== 'Router' || device.zh.powerSource === 'Battery') return false; // Gledopto devices don't support reporting. - if (devicesNotSupportingReporting.includes(device.definition) || - device.definition.vendor === 'Gledopto') return false; + if (devicesNotSupportingReporting.includes(device.definition) || device.definition.vendor === 'Gledopto') return false; - if (this.enabled && device.zh.meta.hasOwnProperty('reporting') && - device.zh.meta.reporting === reportKey) { + if (this.enabled && device.zh.meta.hasOwnProperty('reporting') && device.zh.meta.reporting === reportKey) { return false; } diff --git a/lib/extension/legacy/softReset.ts b/lib/extension/legacy/softReset.ts index 5522ee80..cf1139e3 100644 --- a/lib/extension/legacy/softReset.ts +++ b/lib/extension/legacy/softReset.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ +import logger from '../../util/logger'; // DEPRECATED import * as settings from '../../util/settings'; -import logger from '../../util/logger'; import utils from '../../util/utils'; import Extension from '../extension'; diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index 2370c051..7e154e05 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -1,22 +1,37 @@ +import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; + +import logger from '../util/logger'; import * as settings from '../util/settings'; import utils from '../util/utils'; -import logger from '../util/logger'; -import stringify from 'json-stable-stringify-without-jsonify'; import Extension from './extension'; -import bind from 'bind-decorator'; interface Link { - source: {ieeeAddr: string, networkAddress: number}, target: {ieeeAddr: string, networkAddress: number}, - linkquality: number, depth: number, routes: zh.RoutingTableEntry[], - sourceIeeeAddr: string, targetIeeeAddr: string, sourceNwkAddr: number, lqi: number, relationship: number, + source: {ieeeAddr: string; networkAddress: number}; + target: {ieeeAddr: string; networkAddress: number}; + linkquality: number; + depth: number; + routes: zh.RoutingTableEntry[]; + sourceIeeeAddr: string; + targetIeeeAddr: string; + sourceNwkAddr: number; + lqi: number; + relationship: number; } interface Topology { nodes: { - ieeeAddr: string, friendlyName: string, type: string, networkAddress: number, manufacturerName: string, - modelID: string, failed: string[], lastSeen: number, - definition: {model: string, vendor: string, supports: string, description: string}}[], - links: Link[], + ieeeAddr: string; + friendlyName: string; + type: string; + networkAddress: number; + manufacturerName: string; + modelID: string; + failed: string[]; + lastSeen: number; + definition: {model: string; vendor: string; supports: string; description: string}; + }[]; + links: Link[]; } /** @@ -32,17 +47,16 @@ export default class NetworkMap extends Extension { override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); this.supportedFormats = { - 'raw': this.raw, - 'graphviz': this.graphviz, - 'plantuml': this.plantuml, + raw: this.raw, + graphviz: this.graphviz, + plantuml: this.plantuml, }; } @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { /* istanbul ignore else */ if (this.legacyApi) { - if ((data.topic === this.legacyTopic || data.topic === this.legacyTopicRoutes) && - this.supportedFormats.hasOwnProperty(data.message)) { + if ((data.topic === this.legacyTopic || data.topic === this.legacyTopicRoutes) && this.supportedFormats.hasOwnProperty(data.message)) { const includeRoutes = data.topic === this.legacyTopicRoutes; const topology = await this.networkScan(includeRoutes); let converted = this.supportedFormats[data.message](topology); @@ -62,15 +76,9 @@ export default class NetworkMap extends Extension { const routes = typeof message === 'object' && message.routes; const topology = await this.networkScan(routes); const value = this.supportedFormats[type](topology); - await this.mqtt.publish( - 'bridge/response/networkmap', - stringify(utils.getResponse(message, {routes, type, value}, null)), - ); + await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {routes, type, value}, null))); } catch (error) { - await this.mqtt.publish( - 'bridge/response/networkmap', - stringify(utils.getResponse(message, {}, error.message)), - ); + await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {}, error.message))); } } } @@ -94,7 +102,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.failed && node.failed.length ? `failed: ${node.failed.join(',')}` : ''), ); // Add the device model @@ -113,35 +121,31 @@ export default class NetworkMap extends Extension { // Shape the record according to device type if (node.type == 'Coordinator') { - style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + - `fontcolor="${colors.font.coordinator}"`; + style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + `fontcolor="${colors.font.coordinator}"`; } else if (node.type == 'Router') { - style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + - `fontcolor="${colors.font.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}"`; + style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` + `fontcolor="${colors.font.enddevice}"`; } // Add the device with its labels to the graph as a node. - text += ` "${node.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`; + text += ` "${node.ieeeAddr}" [${style}, label="{${labels.join('|')}}"];\n`; /** * Add an edge between the device and its child to the graph * 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}"`; - text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`; - }); + 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}"`; + text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`; + }); }); text += '}'; @@ -156,36 +160,38 @@ export default class NetworkMap extends Extension { 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(`---`); - - // 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 model - if (node.type !== 'Coordinator') { + 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(`---`); - 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 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 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 edges between the devices @@ -254,19 +260,30 @@ export default class NetworkMap extends Extension { const topology: Topology = {nodes: [], links: []}; // Add nodes for (const device of devices) { - const definition = device.definition ? { - model: device.definition.model, - vendor: device.definition.vendor, - description: device.definition.description, - supports: Array.from(new Set((device.exposes()).map((e) => { - return e.name ?? `${e.type} (${e.features.map((f) => f.name).join(', ')})`; - }))).join(', '), - } : null; + const definition = device.definition + ? { + model: device.definition.model, + vendor: device.definition.vendor, + description: device.definition.description, + supports: Array.from( + new Set( + device.exposes().map((e) => { + return e.name ?? `${e.type} (${e.features.map((f) => f.name).join(', ')})`; + }), + ), + ).join(', '), + } + : null; topology.nodes.push({ - ieeeAddr: device.ieeeAddr, friendlyName: device.name, type: device.zh.type, - networkAddress: device.zh.networkAddress, manufacturerName: device.zh.manufacturerName, - modelID: device.zh.modelID, failed: failed.get(device), lastSeen: device.zh.lastSeen, + ieeeAddr: device.ieeeAddr, + friendlyName: device.name, + type: device.zh.type, + networkAddress: device.zh.networkAddress, + manufacturerName: device.zh.manufacturerName, + modelID: device.zh.modelID, + failed: failed.get(device), + lastSeen: device.zh.lastSeen, definition, }); } @@ -289,17 +306,20 @@ export default class NetworkMap extends Extension { const link: Link = { source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress}, target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress}, - linkquality: neighbor.linkquality, depth: neighbor.depth, routes: [], + linkquality: neighbor.linkquality, + depth: neighbor.depth, + routes: [], // DEPRECATED: - sourceIeeeAddr: neighbor.ieeeAddr, targetIeeeAddr: device.ieeeAddr, - sourceNwkAddr: neighbor.networkAddress, lqi: neighbor.linkquality, + sourceIeeeAddr: neighbor.ieeeAddr, + targetIeeeAddr: device.ieeeAddr, + sourceNwkAddr: neighbor.networkAddress, + lqi: neighbor.linkquality, relationship: neighbor.relationship, }; const routingTable = routingTables.get(device); if (routingTable) { - link.routes = routingTable.table - .filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress); + link.routes = routingTable.table.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress); } topology.links.push(link); diff --git a/lib/extension/onEvent.ts b/lib/extension/onEvent.ts index a3554a6d..f8d10071 100644 --- a/lib/extension/onEvent.ts +++ b/lib/extension/onEvent.ts @@ -1,4 +1,5 @@ import * as zhc from 'zigbee-herdsman-converters'; + import Extension from './extension'; /** @@ -11,21 +12,17 @@ export default class OnEvent extends Extension { } this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data))); - this.eventBus.onDeviceJoined(this, - (data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data))); - this.eventBus.onDeviceInterview(this, - (data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data))); - this.eventBus.onDeviceAnnounce(this, - (data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data))); - this.eventBus.onDeviceNetworkAddressChanged(this, - (data) => this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data))); - this.eventBus.onEntityOptionsChanged(this, - async (data) => { - if (data.entity.isDevice()) { - await this.callOnEvent(data.entity, 'deviceOptionsChanged', data) - .then(() => this.eventBus.emitDevicesChanged()); - } - }); + this.eventBus.onDeviceJoined(this, (data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data))); + this.eventBus.onDeviceInterview(this, (data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data))); + this.eventBus.onDeviceAnnounce(this, (data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data))); + this.eventBus.onDeviceNetworkAddressChanged(this, (data) => + this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)), + ); + this.eventBus.onEntityOptionsChanged(this, async (data) => { + if (data.entity.isDevice()) { + await this.callOnEvent(data.entity, 'deviceOptionsChanged', data).then(() => this.eventBus.emitDevicesChanged()); + } + }); } private convertData(data: KeyValue): KeyValue { diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 6fd360f4..0a4cab8c 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -1,15 +1,16 @@ -import * as settings from '../util/settings'; -import logger from '../util/logger'; -import stringify from 'json-stable-stringify-without-jsonify'; -import utils from '../util/utils'; -import Extension from './extension'; import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; +import path from 'path'; +import * as URI from 'uri-js'; +import {Zcl} from 'zigbee-herdsman'; +import * as zhc from 'zigbee-herdsman-converters'; + import Device from '../model/device'; import dataDir from '../util/data'; -import * as URI from 'uri-js'; -import path from 'path'; -import * as zhc from 'zigbee-herdsman-converters'; -import {Zcl} from 'zigbee-herdsman'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; function isValidUrl(url: string): boolean { let parsed; @@ -24,17 +25,19 @@ function isValidUrl(url: string): boolean { type UpdateState = 'updating' | 'idle' | 'available'; interface UpdatePayload { - update_available?: boolean + update_available?: boolean; // eslint-disable-next-line camelcase update: { - progress?: number, remaining?: number, state: UpdateState, - installed_version: number | null, latest_version: number | null - } + progress?: number; + remaining?: number; + state: UpdateState; + installed_version: number | null; + latest_version: number | null; + }; } const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`); -const topicRegex = - new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)`, 'i'); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)`, 'i'); export default class OTAUpdate extends Extension { private inProgress = new Set(); @@ -79,8 +82,7 @@ export default class OTAUpdate extends Extension { } @bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise { - if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition || - this.inProgress.has(data.device.ieeeAddr)) return; + if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition || this.inProgress.has(data.device.ieeeAddr)) return; logger.debug(`Device '${data.device.name}' requested OTA`); const automaticOTACheckDisabled = settings.get().ota.disable_automatic_update_check; @@ -90,8 +92,9 @@ export default class OTAUpdate extends Extension { // with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update // each time, so this interval can be set by the user. The default is 1,440 minutes (one day). const updateCheckInterval = settings.get().ota.update_check_interval * 1000 * 60; - const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ? - (Date.now() - this.lastChecked[data.device.ieeeAddr]) > updateCheckInterval : true; + const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) + ? Date.now() - this.lastChecked[data.device.ieeeAddr] > updateCheckInterval + : true; if (!check) return; this.lastChecked[data.device.ieeeAddr] = Date.now(); @@ -113,23 +116,24 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: 'available', device: data.device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message, meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta})); } } } // Respond to stop the client from requesting OTAs const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster('genOta')) || data.endpoint; - await endpoint.commandResponse('genOta', 'queryNextImageResponse', - {status: Zcl.Status.NO_IMAGE_AVAILABLE}, undefined, data.meta.zclTransactionSequenceNumber); + await endpoint.commandResponse( + 'genOta', + 'queryNextImageResponse', + {status: Zcl.Status.NO_IMAGE_AVAILABLE}, + undefined, + data.meta.zclTransactionSequenceNumber, + ); logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`); } - private async readSoftwareBuildIDAndDateCode(device: Device, sendPolicy?: 'immediate'): - Promise<{softwareBuildID: string, dateCode: string}> { + private async readSoftwareBuildIDAndDateCode(device: Device, sendPolicy?: 'immediate'): Promise<{softwareBuildID: string; dateCode: string}> { try { const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster('genBasic')); const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId'], {sendPolicy}); @@ -139,16 +143,20 @@ export default class OTAUpdate extends Extension { } } - private getEntityPublishPayload(device: Device, state: zhc.OtaUpdateAvailableResult | UpdateState, - progress: number=null, remaining: number=null): UpdatePayload { + private getEntityPublishPayload( + device: Device, + state: zhc.OtaUpdateAvailableResult | UpdateState, + progress: number = null, + remaining: number = null, + ): UpdatePayload { const deviceUpdateState = this.state.get(device).update; - const payload: UpdatePayload = {update: { - state: typeof state === 'string' ? state : (state.available ? 'available' : 'idle'), - installed_version: typeof state === 'string' ? - deviceUpdateState?.installed_version : state.currentFileVersion, - latest_version: typeof state === 'string' ? - deviceUpdateState?.latest_version : state.otaFileVersion, - }}; + const payload: UpdatePayload = { + update: { + state: typeof state === 'string' ? state : state.available ? 'available' : 'idle', + installed_version: typeof state === 'string' ? deviceUpdateState?.installed_version : state.currentFileVersion, + latest_version: typeof state === 'string' ? deviceUpdateState?.latest_version : state.otaFileVersion, + }, + }; if (progress !== null) payload.update.progress = progress; if (remaining !== null) payload.update.remaining = Math.round(remaining); @@ -169,7 +177,7 @@ export default class OTAUpdate extends Extension { const ID = (typeof message === 'object' && message.hasOwnProperty('id') ? message.id : message) as string; const device = this.zigbee.resolveEntity(ID); const type = data.topic.substring(data.topic.lastIndexOf('/') + 1); - const responseData: {id: string, updateAvailable?: boolean, from?: string, to?: string}= {id: ID}; + const responseData: {id: string; updateAvailable?: boolean; from?: string; to?: string} = {id: ID}; let error = null; let errorStack = null; @@ -181,10 +189,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `not_supported`, device: device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message: error, meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); } } else if (this.inProgress.has(device.ieeeAddr)) { error = `Update or check for update already in progress for '${device.name}'`; @@ -198,10 +203,7 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `checking_if_available`, device: device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message: msg, meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); } try { @@ -212,11 +214,10 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = { - status: availableResult.available ? 'available' : 'not_available', device: device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message: msg, meta}), - ); + status: availableResult.available ? 'available' : 'not_available', + device: device.name, + }; + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); } const payload = this.getEntityPublishPayload(device, availableResult); @@ -230,23 +231,18 @@ export default class OTAUpdate extends Extension { /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `check_failed`, device: device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message: error, meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); } } - } else { // type === 'update' + } else { + // type === 'update' const msg = `Updating '${device.name}' to latest firmware`; logger.info(msg); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `update_in_progress`, device: device.name}; - await this.mqtt.publish( - 'bridge/log', - stringify({type: `ota_update`, message: msg, meta}), - ); + await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); } try { @@ -272,8 +268,11 @@ export default class OTAUpdate extends Extension { const fileVersion = await device.definition.ota.updateToLatest(device.zh, onProgress); logger.info(`Finished update of '${device.name}'`); this.removeProgressAndRemainingFromState(device); - const payload = this.getEntityPublishPayload(device, - {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}); + const payload = this.getEntityPublishPayload(device, { + available: false, + currentFileVersion: fileVersion, + otaFileVersion: fileVersion, + }); await this.publishEntityState(device, payload); const to = await this.readSoftwareBuildIDAndDateCode(device); const [fromS, toS] = [stringify(from_), stringify(to)]; diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index a5d60f3f..fe6480fe 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -1,14 +1,14 @@ - -import * as settings from '../util/settings'; +import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; import * as zhc from 'zigbee-herdsman-converters'; import * as philips from 'zigbee-herdsman-converters/lib/philips'; + +import Device from '../model/device'; +import Group from '../model/group'; import logger from '../util/logger'; +import * as settings from '../util/settings'; import utils from '../util/utils'; import Extension from './extension'; -import stringify from 'json-stable-stringify-without-jsonify'; -import Group from '../model/group'; -import Device from '../model/device'; -import bind from 'bind-decorator'; let topicGetSetRegex: RegExp; // Used by `publish.test.js` to reload regex when changing `mqtt.base_topic`. @@ -37,7 +37,12 @@ const defaultGroupConverters = [ zhc.toZigbee.light_hue_saturation_step, ]; -interface ParsedTopic {ID: string, endpoint: string, attribute: string, type: 'get' | 'set'} +interface ParsedTopic { + ID: string; + endpoint: string; + attribute: string; + type: 'get' | 'set'; +} export default class Publish extends Extension { async start(): Promise { @@ -92,8 +97,14 @@ export default class Publish extends Extension { } } - legacyRetrieveState(re: Device | Group, converter: zhc.Tz.Converter, result: zhc.Tz.ConvertSetResult, - target: zh.Endpoint | zh.Group, key: string, meta: zhc.Tz.Meta): void { + legacyRetrieveState( + re: Device | Group, + converter: zhc.Tz.Converter, + result: zhc.Tz.ConvertSetResult, + target: zh.Endpoint | zh.Group, + key: string, + meta: zhc.Tz.Meta, + ): void { // It's possible for devices to get out of sync when writing an attribute that's not reportable. // So here we re-read the value after a specified timeout, this timeout could for example be the // transition time of a color change or for forcing a state read for devices that don't @@ -102,9 +113,7 @@ export default class Publish extends Extension { // ever issue a read here, as we assume the device will properly report changes. // Only do this when the retrieve_state option is enabled for this device. // retrieve_state == deprecated - if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') && - re.options.retrieve_state - ) { + if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') && re.options.retrieve_state) { setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime); } } @@ -148,9 +157,12 @@ export default class Publish extends Extension { const device = re instanceof Device ? re.zh : null; const entitySettings = re.options; const entityState = this.state.get(re); - const membersState = re instanceof Group ? - Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr, - this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))])) : null; + const membersState = + re instanceof Group + ? Object.fromEntries( + re.zh.members.map((e) => [e.getDevice().ieeeAddr, this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))]), + ) + : null; let converters: zhc.Tz.Converter[]; { if (Array.isArray(definition)) { @@ -199,7 +211,9 @@ export default class Publish extends Extension { const endpointNames = re instanceof Device ? re.getEndpointNames() : []; const propertyEndpointRegex = new RegExp(`^(.*?)_(${endpointNames.join('|')})$`); - for (let [key, value] of entries) { + for (const entry of entries) { + let key = entry[0]; + const value = entry[1]; let endpointName = parsedTopic.endpoint; let localTarget = target; let endpointOrGroupID = utils.isEndpoint(target) ? target.ID : target.groupID; @@ -215,8 +229,7 @@ export default class Publish extends Extension { if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = []; /* istanbul ignore next */ - const converter = converters.find((c) => - c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName)); + const converter = converters.find((c) => c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName)); if (parsedTopic.type === 'set' && usedConverters[endpointOrGroupID].includes(converter)) { // Use a converter for set only once @@ -230,16 +243,21 @@ 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.isEndpoint(localTarget) && - re.endpointName(localTarget)) { + if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isEndpoint(localTarget) && re.endpointName(localTarget)) { endpointName = re.endpointName(localTarget); } // Converter didn't return a result, skip const entitySettingsKeyValue: KeyValue = entitySettings; const meta = { - endpoint_name: endpointName, options: entitySettingsKeyValue, - message: {...message}, logger, device, state: entityState, membersState, mapped: definition, + endpoint_name: endpointName, + options: entitySettingsKeyValue, + message: {...message}, + logger, + device, + state: entityState, + membersState, + mapped: definition, }; // Strip endpoint name from meta.message properties. @@ -289,8 +307,7 @@ export default class Publish extends Extension { continue; } } catch (error) { - const message = - `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`; + const message = `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`; logger.error(message); logger.debug(error.stack); await this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}}); @@ -305,8 +322,7 @@ export default class Publish extends Extension { } } - const scenesChanged = Object.values(usedConverters) - .some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k)))); + const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k)))); if (scenesChanged) { this.eventBus.emitScenesChanged({entity: re}); } diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index 84d80655..9e65bf00 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -1,17 +1,18 @@ -import * as settings from '../util/settings'; -import logger from '../util/logger'; -import debounce from 'debounce'; -import Extension from './extension'; -import stringify from 'json-stable-stringify-without-jsonify'; import bind from 'bind-decorator'; -import utils from '../util/utils'; +import debounce from 'debounce'; +import stringify from 'json-stable-stringify-without-jsonify'; import * as zhc from 'zigbee-herdsman-converters'; -type DebounceFunction = (() => void) & { clear(): void; } & { flush(): void; }; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; + +type DebounceFunction = (() => void) & {clear(): void} & {flush(): void}; export default class Receive extends Extension { private elapsed: {[s: string]: number} = {}; - private debouncers: {[s: string]: {payload: KeyValue, publish: DebounceFunction }} = {}; + private debouncers: {[s: string]: {payload: KeyValue; publish: DebounceFunction}} = {}; async start(): Promise { this.eventBus.onPublishEntityState(this, this.onPublishEntityState); @@ -24,8 +25,12 @@ export default class Receive extends Extension { * In case that e.g. the state is currently held back by a debounce and a new state is published * remove it from the to be send debounced message. */ - if (data.entity.isDevice() && this.debouncers[data.entity.ieeeAddr] && - data.stateChangeReason !== 'publishDebounce' && data.stateChangeReason !== 'lastSeenChanged') { + if ( + data.entity.isDevice() && + this.debouncers[data.entity.ieeeAddr] && + data.stateChangeReason !== 'publishDebounce' && + data.stateChangeReason !== 'lastSeenChanged' + ) { for (const key of Object.keys(data.payload)) { delete this.debouncers[data.entity.ieeeAddr].payload[key]; } @@ -91,8 +96,7 @@ export default class Receive extends Extension { if (!data.device) return; if (!this.shouldProcess(data)) { - await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, - settings.get(), true, this.publishEntityState); + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); return; } @@ -104,10 +108,11 @@ 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)) { - logger.debug(`No converter available for '${data.device.definition.model}' with ` + - `cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`); - await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, - settings.get(), true, this.publishEntityState); + logger.debug( + `No converter available for '${data.device.definition.model}' with ` + + `cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`, + ); + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); return; } @@ -131,8 +136,7 @@ export default class Receive extends Extension { // Check if we have to debounce if (data.device.options.debounce) { - this.publishDebounce(data.device, payload, data.device.options.debounce, - data.device.options.debounce_ignore); + this.publishDebounce(data.device, payload, data.device.options.debounce, data.device.options.debounce_ignore); } else { await this.publishEntityState(data.device, payload); } @@ -143,15 +147,13 @@ export default class Receive extends Extension { this.eventBus.emitExposesChanged({device: data.device}); }; - const meta = {device: data.device.zh, logger, state: this.state.get(data.device), - deviceExposesChanged: deviceExposesChanged}; + const meta = {device: data.device.zh, logger, state: this.state.get(data.device), deviceExposesChanged: deviceExposesChanged}; let payload: KeyValue = {}; for (const converter of converters) { try { const convertData = {...data, device: data.device.zh}; const options: KeyValue = data.device.options; - const converted = await converter.convert( - data.device.definition, convertData, publish, options, meta); + const converted = await converter.convert(data.device.definition, convertData, publish, options, meta); if (converted) { payload = {...payload, ...converted}; } @@ -164,8 +166,7 @@ export default class Receive extends Extension { if (Object.keys(payload).length) { await publish(payload); } else { - await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, - settings.get(), true, this.publishEntityState); + await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState); } } } diff --git a/lib/model/device.ts b/lib/model/device.ts index bba16275..dca52f8c 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -1,16 +1,23 @@ /* eslint-disable brace-style */ -import * as settings from '../util/settings'; -import * as zhc from 'zigbee-herdsman-converters'; import {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; +import * as zhc from 'zigbee-herdsman-converters'; + +import * as settings from '../util/settings'; export default class Device { public zh: zh.Device; public definition: zhc.Definition; private _definitionModelID: string; - get ieeeAddr(): string {return this.zh.ieeeAddr;} - get ID(): string {return this.zh.ieeeAddr;} - get options(): DeviceOptions {return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)};} + get ieeeAddr(): string { + return this.zh.ieeeAddr; + } + get ID(): string { + return this.zh.ieeeAddr; + } + get options(): DeviceOptions { + return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)}; + } get name(): string { return this.zh.type === 'Coordinator' ? 'Coordinator' : this.options?.friendly_name || this.ieeeAddr; } @@ -86,9 +93,15 @@ export default class Device { return Object.keys(this.definition?.endpoint?.(this.zh) ?? {}).filter((name) => name !== 'default'); } - isIkeaTradfri(): boolean {return this.zh.manufacturerID === 4476;} + isIkeaTradfri(): boolean { + return this.zh.manufacturerID === 4476; + } - isDevice(): this is Device {return true;} + isDevice(): this is Device { + return true; + } /* istanbul ignore next */ - isGroup(): this is Group {return false;} + isGroup(): this is Group { + return false; + } } diff --git a/lib/model/group.ts b/lib/model/group.ts index 0e0f43d8..0eaac89c 100644 --- a/lib/model/group.ts +++ b/lib/model/group.ts @@ -1,14 +1,21 @@ /* eslint-disable brace-style */ -import * as settings from '../util/settings'; import * as zhc from 'zigbee-herdsman-converters'; +import * as settings from '../util/settings'; + export default class Group { public zh: zh.Group; private resolveDevice: (ieeeAddr: string) => Device; - get ID(): number {return this.zh.groupID;} - get options(): GroupOptions {return {...settings.getGroup(this.ID)};} - get name(): string {return this.options?.friendly_name || this.ID.toString();} + get ID(): number { + return this.zh.groupID; + } + get options(): GroupOptions { + return {...settings.getGroup(this.ID)}; + } + get name(): string { + return this.options?.friendly_name || this.ID.toString(); + } constructor(group: zh.Group, resolveDevice: (ieeeAddr: string) => Device) { this.zh = group; @@ -24,9 +31,15 @@ export default class Group { } membersDefinitions(): zhc.Definition[] { - return this.membersDevices().map((d) => d.definition).filter((d) => d); + return this.membersDevices() + .map((d) => d.definition) + .filter((d) => d); } - isDevice(): this is Device {return false;} - isGroup(): this is Group {return true;} + isDevice(): this is Device { + return false; + } + isGroup(): this is Group { + return true; + } } diff --git a/lib/mqtt.ts b/lib/mqtt.ts index ad6e2f22..9d09fd0f 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -1,10 +1,12 @@ +import type {QoS} from 'mqtt-packet'; + +import bind from 'bind-decorator'; +import fs from 'fs'; import * as mqtt from 'mqtt'; + import logger from './util/logger'; import * as settings from './util/settings'; import utils from './util/utils'; -import fs from 'fs'; -import bind from 'bind-decorator'; -import type {QoS} from 'mqtt-packet'; const NS = 'z2m:mqtt'; @@ -15,8 +17,9 @@ export default class MQTT { private eventBus: EventBus; private initialConnect = true; private republishRetainedTimer: NodeJS.Timeout; - private retainedMessages: {[s: string]: {payload: string, options: MQTTOptions, - skipLog: boolean, skipReceive: boolean, topic: string, base: string}} = {}; + private retainedMessages: { + [s: string]: {payload: string; options: MQTTOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string}; + } = {}; constructor(eventBus: EventBus) { this.eventBus = eventBus; @@ -155,9 +158,15 @@ export default class MQTT { return this.client && !this.client.reconnecting; } - async publish(topic: string, payload: string, options: MQTTOptions={}, base=settings.get().mqtt.base_topic, skipLog=false, skipReceive=true) - : Promise { - const defaultOptions: {qos: QoS, retain: boolean} = {qos: 0, retain: false}; + async publish( + topic: string, + payload: string, + options: MQTTOptions = {}, + base = settings.get().mqtt.base_topic, + skipLog = false, + skipReceive = true, + ): Promise { + const defaultOptions: {qos: QoS; retain: boolean} = {qos: 0, retain: false}; topic = `${base}/${topic}`; if (skipReceive) { diff --git a/lib/state.ts b/lib/state.ts index 9ea64b9b..f8db931d 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -1,16 +1,33 @@ -import logger from './util/logger'; -import data from './util/data'; -import * as settings from './util/settings'; -import utils from './util/utils'; import fs from 'fs'; import objectAssignDeep from 'object-assign-deep'; +import data from './util/data'; +import logger from './util/logger'; +import * as settings from './util/settings'; +import utils from './util/utils'; + const saveInterval = 1000 * 60 * 5; // 5 minutes const dontCacheProperties = [ - 'action', 'action_.*', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror', - 'step_size', 'transition_time', 'group_list', 'group_capacity', 'no_occupancy_since', - 'step_mode', 'transition_time', 'duration', 'elapsed', 'from_side', 'to_side', + 'action', + 'action_.*', + 'button', + 'button_left', + 'button_right', + 'click', + 'forgotten', + 'keyerror', + 'step_size', + 'transition_time', + 'group_list', + 'group_capacity', + 'no_occupancy_since', + 'step_mode', + 'transition_time', + 'duration', + 'elapsed', + 'from_side', + 'to_side', ]; class State { @@ -18,7 +35,10 @@ class State { private file = data.joinPath('state.json'); private timer: NodeJS.Timeout = null; - constructor(private readonly eventBus: EventBus, private readonly zigbee: Zigbee) { + constructor( + private readonly eventBus: EventBus, + private readonly zigbee: Zigbee, + ) { this.eventBus = eventBus; this.zigbee = zigbee; } @@ -75,7 +95,7 @@ class State { return this.state[entity.ID] || {}; } - set(entity: Group | Device, update: KeyValue, reason: string=null): KeyValue { + set(entity: Group | Device, update: KeyValue, reason: string = null): KeyValue { const fromState = this.state[entity.ID] || {}; const toState = objectAssignDeep({}, fromState, update); const newCache = {...toState}; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 4020d7e1..ff23add5 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -1,11 +1,12 @@ /* eslint-disable camelcase */ -import {LogLevel} from 'lib/util/settings'; -import type { - Device as ZHDevice, - Group as ZHGroup, - Endpoint as ZHEndpoint, -} from 'zigbee-herdsman/dist/controller/model'; - +import type TypeEventBus from 'lib/eventBus'; +import type TypeExtension from 'lib/extension/extension'; +import type TypeDevice from 'lib/model/device'; +import type TypeGroup from 'lib/model/group'; +import type TypeMQTT from 'lib/mqtt'; +import type TypeState from 'lib/state'; +import type TypeZigbee from 'lib/zigbee'; +import type {QoS} from 'mqtt-packet'; import type { NetworkParameters as ZHNetworkParameters, CoordinatorVersion as ZHCoordinatorVersion, @@ -13,25 +14,12 @@ import type { RoutingTable as ZHRoutingTable, RoutingTableEntry as ZHRoutingTableEntry, } from 'zigbee-herdsman/dist/adapter/tstype'; - -import type { - Cluster as ZHCluster, - FrameControl as ZHFrameControl, -} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; - +import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events'; +import type {Device as ZHDevice, Group as ZHGroup, Endpoint as ZHEndpoint} from 'zigbee-herdsman/dist/controller/model'; +import type {Cluster as ZHCluster, FrameControl as ZHFrameControl} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; import type * as zhc from 'zigbee-herdsman-converters'; -import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events'; - -import type TypeEventBus from 'lib/eventBus'; -import type TypeMQTT from 'lib/mqtt'; -import type TypeState from 'lib/state'; -import type TypeZigbee from 'lib/zigbee'; -import type TypeDevice from 'lib/model/device'; -import type TypeGroup from 'lib/model/group'; -import type TypeExtension from 'lib/extension/extension'; - -import type {QoS} from 'mqtt-packet'; +import {LogLevel} from 'lib/util/settings'; declare global { // Define some class types as global @@ -46,15 +34,25 @@ declare global { // Types // eslint-disable-next-line @typescript-eslint/no-explicit-any type ExternalDefinition = zhc.Definition & {homeassistant: any}; - interface MQTTResponse {data: KeyValue, status: 'error' | 'ok', error?: string, transaction?: string} - interface MQTTOptions {qos?: QoS, retain?: boolean, properties?: {messageExpiryInterval: number}} - type Scene = {id: number, name: string}; + interface MQTTResponse { + data: KeyValue; + status: 'error' | 'ok'; + error?: string; + transaction?: string; + } + interface MQTTOptions { + qos?: QoS; + retain?: boolean; + properties?: {messageExpiryInterval: number}; + } + type Scene = {id: number; name: string}; type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged' | 'publishCached'; - type PublishEntityState = (entity: Device | Group, payload: KeyValue, - stateChangeReason?: StateChangeReason) => Promise; - type RecursivePartial = {[P in keyof T]?: RecursivePartial;}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - interface KeyValue {[s: string]: any} + type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise; + type RecursivePartial = {[P in keyof T]?: RecursivePartial}; + interface KeyValue { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [s: string]: any; + } // zigbee-herdsman namespace zh { @@ -74,27 +72,32 @@ declare global { } namespace eventdata { - type EntityRenamed = { entity: Device | Group, homeAssisantRename: boolean, from: string, to: string }; - type DeviceRemoved = { ieeeAddr: string, name: string }; - type MQTTMessage = { topic: string, message: string }; - type MQTTMessagePublished = { topic: string, payload: string, options: {retain: boolean, qos: number} }; + type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string}; + type DeviceRemoved = {ieeeAddr: string; name: string}; + type MQTTMessage = {topic: string; message: string}; + type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}}; type StateChange = { - entity: Device | Group, from: KeyValue, to: KeyValue, reason: string | null, update: KeyValue }; + entity: Device | Group; + from: KeyValue; + to: KeyValue; + reason: string | null; + update: KeyValue; + }; type PermitJoinChanged = ZHEvents.PermitJoinChangedPayload; - type LastSeenChanged = { device: Device, - reason: 'deviceAnnounce' | 'networkAddress' | 'deviceJoined' | 'messageEmitted' | 'messageNonEmitted'; }; - type DeviceNetworkAddressChanged = { device: Device }; - type DeviceAnnounce = { device: Device }; - type DeviceInterview = { device: Device, status: 'started' | 'successful' | 'failed' }; - type DeviceJoined = { device: Device }; - type EntityOptionsChanged = { entity: Device | Group, from: KeyValue, to: KeyValue }; - type ExposesChanged = { device: Device }; - type Reconfigure = { device: Device }; - type DeviceLeave = { ieeeAddr: string, name: string }; - type GroupMembersChanged = {group: Group, action: 'remove' | 'add' | 'remove_all', - endpoint: zh.Endpoint, skipDisableReporting: boolean }; - type PublishEntityState = {entity: Group | Device, message: KeyValue, stateChangeReason: StateChangeReason, - payload: KeyValue}; + type LastSeenChanged = { + device: Device; + reason: 'deviceAnnounce' | 'networkAddress' | 'deviceJoined' | 'messageEmitted' | 'messageNonEmitted'; + }; + type DeviceNetworkAddressChanged = {device: Device}; + type DeviceAnnounce = {device: Device}; + type DeviceInterview = {device: Device; status: 'started' | 'successful' | 'failed'}; + type DeviceJoined = {device: Device}; + type EntityOptionsChanged = {entity: Device | Group; from: KeyValue; to: KeyValue}; + type ExposesChanged = {device: Device}; + type Reconfigure = {device: Device}; + type DeviceLeave = {ieeeAddr: string; name: string}; + type GroupMembersChanged = {group: Group; action: 'remove' | 'add' | 'remove_all'; endpoint: zh.Endpoint; skipDisableReporting: boolean}; + type PublishEntityState = {entity: Group | Device; message: KeyValue; stateChangeReason: StateChangeReason; payload: KeyValue}; type DeviceMessage = { type: ZHEvents.MessagePayloadType; device: Device; @@ -103,157 +106,157 @@ declare global { groupID: number; cluster: string | number; data: KeyValue | Array; - meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl;}; + meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl}; }; - type ScenesChanged = { entity: Device | Group }; + type ScenesChanged = {entity: Device | Group}; } // Settings // eslint-disable camelcase interface Settings { homeassistant?: { - discovery_topic: string, - status_topic: string, - legacy_entity_attributes: boolean, - legacy_triggers: boolean, - }, - permit_join?: boolean, + discovery_topic: string; + status_topic: string; + legacy_entity_attributes: boolean; + legacy_triggers: boolean; + }; + permit_join?: boolean; availability?: { - active: {timeout: number}, - passive: {timeout: number} - }, - external_converters: string[], + active: {timeout: number}; + passive: {timeout: number}; + }; + external_converters: string[]; mqtt: { - base_topic: string, - include_device_information: boolean, - force_disable_retain: boolean - version?: 3 | 4 | 5, - user?: string, - password?: string, - server: string, - ca?: string, - keepalive?: number, - key?: string, - cert?: string, - client_id?: string, - reject_unauthorized?: boolean, - }, + base_topic: string; + include_device_information: boolean; + force_disable_retain: boolean; + version?: 3 | 4 | 5; + user?: string; + password?: string; + server: string; + ca?: string; + keepalive?: number; + key?: string; + cert?: string; + client_id?: string; + reject_unauthorized?: boolean; + }; serial: { - disable_led: boolean, - port?: string, - adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate' | 'ember', - baudrate?: number, - rtscts?: boolean, - }, - passlist: string[], - blocklist: string[], + disable_led: boolean; + port?: string; + adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate' | 'ember'; + baudrate?: number; + rtscts?: boolean; + }; + passlist: string[]; + blocklist: string[]; map_options: { graphviz: { colors: { fill: { - enddevice: string, - coordinator: string, - router: string, - }, + enddevice: string; + coordinator: string; + router: string; + }; font: { - coordinator: string, - router: string, - enddevice: string, - }, + coordinator: string; + router: string; + enddevice: string; + }; line: { - active: string, - inactive: string, - }, - }, - }, - }, + active: string; + inactive: string; + }; + }; + }; + }; ota: { - update_check_interval: number, - disable_automatic_update_check: boolean, - zigbee_ota_override_index_location?: string, - ikea_ota_use_test_url?: boolean, - }, + update_check_interval: number; + disable_automatic_update_check: boolean; + zigbee_ota_override_index_location?: string; + ikea_ota_use_test_url?: boolean; + }; frontend?: { - auth_token?: string, - host?: string, - port?: number, - url?: string, - ssl_cert?: string, - ssl_key?: string, - }, - devices?: {[s: string]: DeviceOptions}, - groups?: {[s: string]: GroupOptions}, - device_options: KeyValue, + auth_token?: string; + host?: string; + port?: number; + url?: string; + ssl_cert?: string; + ssl_key?: string; + }; + devices?: {[s: string]: DeviceOptions}; + groups?: {[s: string]: GroupOptions}; + device_options: KeyValue; advanced: { - legacy_api: boolean, - legacy_availability_payload: boolean, - log_rotation: boolean, - log_symlink_current: boolean, - log_output: ('console' | 'file' | 'syslog')[], - log_directory: string, - log_file: string, - log_level: LogLevel, - log_namespaced_levels: Record, - log_syslog: KeyValue, - log_debug_to_mqtt_frontend: boolean, - log_debug_namespace_ignore: string, - pan_id: number | 'GENERATE', - ext_pan_id: number[] | 'GENERATE', - channel: number, - adapter_concurrent: number | null, - adapter_delay: number | null, - cache_state: boolean, - cache_state_persistent: boolean, - cache_state_send_on_startup: boolean, - last_seen: 'disable' | 'ISO_8601' | 'ISO_8601_local' | 'epoch', - elapsed: boolean, - network_key: number[] | 'GENERATE', - timestamp_format: string, - output: 'json' | 'attribute' | 'attribute_and_json', - transmit_power?: number, + legacy_api: boolean; + legacy_availability_payload: boolean; + log_rotation: boolean; + log_symlink_current: boolean; + log_output: ('console' | 'file' | 'syslog')[]; + log_directory: string; + log_file: string; + log_level: LogLevel; + log_namespaced_levels: Record; + log_syslog: KeyValue; + log_debug_to_mqtt_frontend: boolean; + log_debug_namespace_ignore: string; + pan_id: number | 'GENERATE'; + ext_pan_id: number[] | 'GENERATE'; + channel: number; + adapter_concurrent: number | null; + adapter_delay: number | null; + cache_state: boolean; + cache_state_persistent: boolean; + cache_state_send_on_startup: boolean; + last_seen: 'disable' | 'ISO_8601' | 'ISO_8601_local' | 'epoch'; + elapsed: boolean; + network_key: number[] | 'GENERATE'; + timestamp_format: string; + output: 'json' | 'attribute' | 'attribute_and_json'; + transmit_power?: number; // Everything below is deprecated - availability_timeout?: number, - availability_blocklist?: string[], - availability_passlist?: string[], - availability_blacklist?: string[], - availability_whitelist?: string[], - soft_reset_timeout: number, - report: boolean, - }, + availability_timeout?: number; + availability_blocklist?: string[]; + availability_passlist?: string[]; + availability_blacklist?: string[]; + availability_whitelist?: string[]; + soft_reset_timeout: number; + report: boolean; + }; } interface DeviceOptions { - ID?: string, - disabled?: boolean, - retention?: number, - availability?: boolean | {timeout: number}, - optimistic?: boolean, - retrieve_state?: boolean, - debounce?: number, - debounce_ignore?: string[], - filtered_attributes?: string[], - filtered_cache?: string[], - filtered_optimistic?: string[], - icon?: string, - homeassistant?: KeyValue, - legacy?: boolean, - friendly_name: string, - description?: string, - qos?: 0 | 1 | 2, + ID?: string; + disabled?: boolean; + retention?: number; + availability?: boolean | {timeout: number}; + optimistic?: boolean; + retrieve_state?: boolean; + debounce?: number; + debounce_ignore?: string[]; + filtered_attributes?: string[]; + filtered_cache?: string[]; + filtered_optimistic?: string[]; + icon?: string; + homeassistant?: KeyValue; + legacy?: boolean; + friendly_name: string; + description?: string; + qos?: 0 | 1 | 2; } interface GroupOptions { - devices?: string[], - ID?: number, - optimistic?: boolean, - off_state?: 'all_members_off' | 'last_member_state' - filtered_attributes?: string[], - filtered_cache?: string[], - filtered_optimistic?: string[], - retrieve_state?: boolean, - homeassistant?: KeyValue, - friendly_name: string, - description?: string, - qos?: 0 | 1 | 2, + devices?: string[]; + ID?: number; + optimistic?: boolean; + off_state?: 'all_members_off' | 'last_member_state'; + filtered_attributes?: string[]; + filtered_cache?: string[]; + filtered_optimistic?: string[]; + retrieve_state?: boolean; + homeassistant?: KeyValue; + friendly_name: string; + description?: string; + qos?: 0 | 1 | 2; } } diff --git a/lib/util/logger.ts b/lib/util/logger.ts index 1ecd5fdc..399f4ded 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -1,11 +1,12 @@ -import winston from 'winston'; -import moment from 'moment'; -import * as settings from './settings'; -import path from 'path'; +import assert from 'assert'; import fs from 'fs'; import fx from 'mkdir-recursive'; +import moment from 'moment'; +import path from 'path'; import {rimrafSync} from 'rimraf'; -import assert from 'assert'; +import winston from 'winston'; + +import * as settings from './settings'; const NAMESPACE_SEPARATOR = ':'; @@ -30,19 +31,13 @@ class Logger { this.namespacedLevels = settings.get().advanced.log_namespaced_levels; this.cachedNamespacedLevels = Object.assign({}, this.namespacedLevels); - assert( - settings.LOG_LEVELS.includes(this.level), - `'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`, - ); + assert(settings.LOG_LEVELS.includes(this.level), `'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`); const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format); this.logger = winston.createLogger({ level: 'debug', - format: winston.format.combine( - winston.format.errors({stack: true}), - winston.format.timestamp({format: timestampFormat}), - ), + format: winston.format.combine(winston.format.errors({stack: true}), winston.format.timestamp({format: timestampFormat})), levels: winston.config.syslog.levels, }); @@ -51,16 +46,20 @@ class Logger { let logging = `Logging to console${consoleSilenced ? ' (silenced)' : ''}`; // Setup default console logger - this.logger.add(new winston.transports.Console({ - silent: consoleSilenced, - // winston.config.syslog.levels sets 'warning' as 'red' - format: winston.format.combine( - winston.format.colorize({colors: {debug: 'blue', info: 'green', warning: 'yellow', error: 'red'}}), - winston.format.printf(/* istanbul ignore next */(info) => { - return `[${info.timestamp}] ${info.level}: \t${info.message}`; - }), - ), - })); + this.logger.add( + new winston.transports.Console({ + silent: consoleSilenced, + // winston.config.syslog.levels sets 'warning' as 'red' + format: winston.format.combine( + winston.format.colorize({colors: {debug: 'blue', info: 'green', warning: 'yellow', error: 'red'}}), + winston.format.printf( + /* istanbul ignore next */ (info) => { + return `[${info.timestamp}] ${info.level}: \t${info.message}`; + }, + ), + ), + }), + ); if (this.output.includes('file')) { logging += `, file (filename: ${logFilename})`; @@ -79,13 +78,14 @@ class Logger { } // Add file logger when enabled - // eslint-disable-next-line max-len // NOTE: the initiation of the logger even when not added as transport tries to create the logging directory const transportFileOptions: winston.transports.FileTransportOptions = { filename: path.join(this.directory, logFilename), - format: winston.format.printf(/* istanbul ignore next */(info) => { - return `[${info.timestamp}] ${info.level}: \t${info.message}`; - }), + format: winston.format.printf( + /* istanbul ignore next */ (info) => { + return `[${info.timestamp}] ${info.level}: \t${info.message}`; + }, + ), }; if (settings.get().advanced.log_rotation) { @@ -135,7 +135,7 @@ class Logger { } public getDebugNamespaceIgnore(): string { - return this.debugNamespaceIgnoreRegex?.toString().slice(1, -1)/* remove slashes */ ?? ''; + return this.debugNamespaceIgnoreRegex?.toString().slice(1, -1) /* remove slashes */ ?? ''; } public setDebugNamespaceIgnore(value: string): void { @@ -164,14 +164,14 @@ class Logger { this.cachedNamespacedLevels = Object.assign({}, this.namespacedLevels); } - private cacheNamespacedLevel(namespace: string) : string { + private cacheNamespacedLevel(namespace: string): string { let cached = namespace; while (this.cachedNamespacedLevels[namespace] == undefined) { const sep = cached.lastIndexOf(NAMESPACE_SEPARATOR); if (sep === -1) { - return this.cachedNamespacedLevels[namespace] = this.level; + return (this.cachedNamespacedLevels[namespace] = this.level); } cached = cached.slice(0, sep); diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index b3b44a1d..f9baaa18 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -1,969 +1,959 @@ { - "type": "object", - "properties": { - "homeassistant": { - "title": "Home Assistant integration", - "requiresRestart": true, - "description": "Home Assistant integration (MQTT discovery)", - "default": false, - "oneOf": [ - { - "type": "boolean", - "title": "Home Assistant (simple)" - }, - { - "type": "object", - "title": "Home Assistant (advanced)", - "properties": { - "legacy_triggers": { - "type": "boolean", - "title": "Home Assistant legacy triggers", - "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discoverd", - "default": true - }, - "discovery_topic": { - "type": "string", - "title": "Homeassistant discovery topic", - "description": "Home Assistant discovery topic", - "requiresRestart": true, - "examples": ["homeassistant"] - }, - "legacy_entity_attributes": { - "type": "boolean", - "title": "Home Assistant legacy entity attributes", - "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", - "default": true - }, - "status_topic": { - "type": "string", - "title": "Home Assistant status topic", - "description": "Home Assistant status topic", - "requiresRestart": true, - "examples": ["homeassistant/status"] - } - } - } - ] - }, - "permit_join": { - "type": "boolean", - "default": false, - "title": "Permit join", - "description": "Allow new devices to join (re-applied at restart)" - }, - "availability": { - "oneOf": [ - { - "type": "boolean", - "title": "Availability (simple)" - }, - { - "type": "object", - "title": "Availability (advanced)", - "properties": { - "active": { - "type": "object", - "title": "Active", - "requiresRestart": true, - "description": "Options for active devices (routers/mains powered)", - "properties": { - "timeout": { - "type": "number", - "title": "Timeout", - "requiresRestart": true, - "default": 10, - "description": "Time after which an active device will be marked as offline in minutes" - } - } - }, - "passive": { - "type": "object", - "title": "Passive", - "requiresRestart": true, - "description": "Options for passive devices (mostly battery powered)", - "properties": { - "timeout": { - "type": "number", - "title": "Timeout", - "requiresRestart": true, - "default": 1500, - "description": "Time after which an passive device will be marked as offline in minutes" - } - } - } - } - } - ], - "title": "Availability", - "requiresRestart": true, - "description": "Checks whether devices are online/offline" - }, - "external_converters": { - "type": "array", - "title": "External converters", - "description": "You can define external converters to e.g. add support for a DiY device", - "requiresRestart": true, - "items": { - "type": "string" - }, - "examples": ["DIYRuZ_FreePad.js"] - }, - "mqtt": { - "type": "object", - "title": "MQTT", - "properties": { - "base_topic": { - "type": "string", - "title": "Base topic", - "default": "zigbee2mqtt", - "requiresRestart": true, - "description": "MQTT base topic for Zigbee2MQTT MQTT messages", - "examples": ["zigbee2mqtt"] - }, - "server": { - "type": "string", - "title": "MQTT server", - "requiresRestart": true, - "description": "MQTT server URL (use mqtts:// for SSL/TLS connection)", - "examples": ["mqtt://localhost:1883"] - }, - "keepalive": { - "type": "number", - "title": "Keepalive", - "requiresRestart": true, - "description": "MQTT keepalive in second", - "default": 60 - }, - "ca": { - "type": "string", - "title": "Certificate authority", - "requiresRestart": true, - "description": "Absolute path to SSL/TLS certificate of CA used to sign server and client certificates", - "examples": ["/etc/ssl/mqtt-ca.crt"] - }, - "key": { - "type": "string", - "title": "SSL/TLS key", - "requiresRestart": true, - "description": "Absolute path to SSL/TLS key for client-authentication", - "examples": ["/etc/ssl/mqtt-client.key"] - }, - "cert": { - "type": "string", - "title": "SSL/TLS certificate", - "description": "Absolute path to SSL/TLS certificate for client-authentication", - "requiresRestart": true, - "examples": ["/etc/ssl/mqtt-client.crt"] - }, - "user": { - "type": "string", - "title": "User", - "requiresRestart": true, - "description": "MQTT server authentication user", - "examples": ["johnnysilverhand"] - }, - "password": { - "type": "string", - "title": "Password", - "requiresRestart": true, - "description": "MQTT server authentication password", - "examples": ["ILOVEPELMENI"] - }, - "client_id": { - "type": "string", - "title": "Client ID", - "requiresRestart": true, - "description": "MQTT client ID", - "examples": ["MY_CLIENT_ID"] - }, - "reject_unauthorized": { - "type": "boolean", - "title": "Reject unauthorized", - "requiresRestart": true, - "description": "Disable self-signed SSL certificate", - "default": true - }, - "include_device_information": { - "type": "boolean", - "title": "Include device information", - "description": "Include device information to mqtt messages", - "default": false - }, - "version": { - "type": ["number", "null"], - "title": "Version", - "requiresRestart": true, - "description": "MQTT protocol version", - "default": 4, - "examples": [5] - }, - "force_disable_retain": { - "type": "boolean", - "title": "Force disable retain", - "requiresRestart": true, - "description": "Disable retain for all send messages. ONLY enable if you MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", - "default": false - } - }, - "required": ["server"] - }, - "serial": { - "type": "object", - "title": "Serial", - "properties": { - "port": { - "type": ["string", "null"], - "title": "Port", - "requiresRestart": true, - "description": "Location of the adapter. To autodetect the port, set null", - "examples": ["/dev/ttyACM0"] - }, - "disable_led": { - "type": "boolean", - "title": "Disable led", - "requiresRestart": true, - "description": "Disable LED of the adapter if supported", - "default": false - }, - "adapter": { - "type": ["string"], - "enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember"], - "title": "Adapter", - "default": "auto", - "requiresRestart": true, - "description": "Adapter type, not needed unless you are experiencing problems" - }, - "baudrate": { - "type": "number", - "title": "Baudrate", - "requiresRestart": true, - "description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600", - "examples": [38400, 57600, 115200] - }, - "rtscts": { - "type": "boolean", - "title": "RTS / CTS", - "requiresRestart": true, - "description": "RTS / CTS Hardware Flow Control for serial port" - } - } - }, - "blocklist": { - "title": "Blocklist", - "requiresRestart": true, - "description": "Block devices from the network (by ieeeAddr)", - "type": "array", - "items": { - "type": "string" - } - }, - "passlist": { - "title": "Passlist", - "requiresRestart": true, - "description": "Allow only certain devices to join the network (by ieeeAddr). Note that all devices not on the passlist will be removed from the network!", - "type": "array", - "items": { - "type": "string" - } - }, - "map_options": { - "type": "object", - "title": "Networkmap", - "properties": { - "graphviz": { - "type": "object", - "properties": { - "colors": { - "type": "object", - "properties": { - "fill": { - "type": "object", - "properties": { - "enddevice": { - "type": "string" - }, - "coordinator": { - "type": "string" - }, - "router": { - "type": "string" - } - } - }, - "font": { - "type": "object", - "properties": { - "enddevice": { - "type": "string" - }, - "coordinator": { - "type": "string" - }, - "router": { - "type": "string" - } - } - }, - "line": { - "type": "object", - "properties": { - "active": { - "type": "string" - }, - "inactive": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "ota": { - "type": "object", - "title": "OTA updates", - "properties": { - "update_check_interval": { - "type": "number", - "title": "Update check interval", - "description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.", - "default": 1440 - }, - "disable_automatic_update_check": { - "type": "boolean", - "title": "Disable automatic update check", - "description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.", - "default": false - }, - "ikea_ota_use_test_url": { - "type": "boolean", - "title": "IKEA TRADFRI OTA use test url", - "requiresRestart": true, - "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", - "default": false - }, - "zigbee_ota_override_index_location": { - "type": ["string", "null"], - "title": "OTA index override file name", - "requiresRestart": true, - "description": "Location of override OTA index file", - "examples": ["index.json"] - } - } - }, - "frontend": { - "oneOf": [ - { - "type": "boolean", - "title": "Frontend (simple)" - }, - { - "type": "object", - "title": "Frontend (advanced)", - "properties": { - "port": { - "type": "number", - "title": "Port", - "description": "Frontend binding port. Ignored when using a unix domain socket", - "default": 8080, - "requiresRestart": true - }, - "host": { - "type": ["string", "null"], - "title": "Bind host", - "description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.", - "examples": ["127.0.0.1", "::1", "/run/zigbee2mqtt/zigbee2mqtt.sock"], - "requiresRestart": true - }, - "auth_token": { - "type": ["string", "null"], - "title": "Auth token", - "description": "Enables authentication, disabled by default", - "requiresRestart": true - }, - "url": { - "type": ["string", "null"], - "title": "URL", - "description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page", - "requiresRestart": true - }, - "ssl_cert" : { - "type": ["string", "null"], - "title": "Certificate file path", - "description": "SSL Certificate file path for exposing HTTPS. The sibling property 'ssl_key' must be set for HTTPS to be activated.", - "requiresRestart": true - }, - "ssl_key" : { - "type": ["string", "null"], - "title": "key file path", - "description": "SSL key file path for exposing HTTPS. The sibling property 'ssl_cert' must be set for HTTPS to be activated.", - "requiresRestart": true - } - } - } - ], - "title": "Frontend", - "requiresRestart": true - }, - "devices": { - "type": "object", - "propertyNames": { - "pattern": "^0x[\\d\\w]{16}$" - }, - "patternProperties": { - "^.*$": { - "$ref": "#/definitions/device" - } - } - }, - "groups": { - "type": "object", - "propertyNames": { - "pattern": "^[\\w].*$" - }, - "patternProperties": { - "^.*$": { - "$ref": "#/definitions/group" - } - } - }, - "device_options": { - "type": "object", - "title": "Options that are applied to all devices" - }, - "advanced": { - "type": "object", - "title": "Advanced", - "properties": { - "legacy_api": { - "type": "boolean", - "title": "Legacy API", - "requiresRestart": true, - "description": "Disables the legacy api (false = disable)", - "default": true - }, - "legacy_availability_payload": { - "type": "boolean", - "title": "Legacy availability payload", - "requiresRestart": true, - "description": "Payload to be used for device availability and bridge/state topics. true = text, false = JSON", - "default": true - }, - "log_rotation": { - "type": "boolean", - "title": "Log rotation", - "requiresRestart": true, - "description": "Log rotation", - "default": true - }, - "log_symlink_current": { - "type": "boolean", - "title": "Log symlink current", - "requiresRestart": true, - "description": "Create symlink to current logs in the log directory", - "default": false - }, - "log_output": { - "type": "array", - "requiresRestart": true, - "items": { - "type": "string", - "enum": ["console", "file", "syslog"] - }, - "title": "Log output", - "description": "Output location of the log, leave empty to suppress logging" - }, - "log_directory": { - "type": "string", - "title": "Log directory", - "requiresRestart": true, - "description": "Location of log directory", - "examples": ["data/log/%TIMESTAMP%"] - }, - "log_file": { - "type": "string", - "title": "Log file", - "requiresRestart": true, - "description": "Log file name, can also contain timestamp", - "examples": ["zigbee2mqtt_%TIMESTAMP%.log"], - "default": "log.txt" - }, - "log_level": { - "type": "string", - "enum": ["error", "warning", "info", "debug", "warn"], - "title": "Log level", - "description": "Logging level", - "default": "info" - }, - "log_namespaced_levels": { - "type": "object", - "propertyNames": { - "pattern": "^(z2m|zhc|zh)(:[a-z0-9]{1,})*$" - }, - "additionalProperties": { - "type": "string", - "enum": ["error", "warning", "info", "debug"] - }, - "title": "Log Namespaced Levels", - "description": "Set individual log levels for certain namespaces", - "default": {}, - "examples": [ - {"z2m:mqtt": "warning"}, - {"zh:ember:uart:ash": "info"} - ] - }, - "log_debug_to_mqtt_frontend": { - "type": "boolean", - "title": "Log debug to MQTT and frontend", - "description": "Log debug level to MQTT and frontend (may decrease overall performance)", - "requiresRestart": true, - "default": false - }, - "log_debug_namespace_ignore": { - "type": "string", - "title": "Log debug namespace ignore", - "description": "Do not log these namespaces (regex-based) for debug level", - "default": "", - "examples": [ - "^zhc:legacy:fz:(tuya|moes)", - "^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller" - ] - }, - "log_syslog": { - "requiresRestart": true, - "oneOf": [ - { - "title": "syslog (disabled)", - "type": "null" - }, - { - "title": "syslog (enabled)", - "type": "object", - "properties": { - "host": { - "type": "string", - "title": "Host", - "description": "The host running syslogd, defaults to localhost.", - "default": "localhost" - }, - "port": { - "type": "number", - "title": "Port", - "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", - "default": 514 - }, - "protocol": { - "type": "string", - "title": "Protocol", - "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", - "default": "udp4", - "examples": [ - "udp4", - "tls4", - "unix", - "unix-connect" - ] - }, - "path": { - "type": "string", - "title": "Path", - "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", - "default": "/dev/log", - "examples": [ - "/var/run/syslog" - ] - }, - "pid": { - "type": "string", - "title": "PID", - "description": "PID of the process that log messages are coming from (Default process.pid).", - "default": "process.pid" - }, - "localhost": { - "type": "string", - "title": "Localhost", - "description": "Host to indicate that log messages are coming from (Default: localhost).", - "default": "localhost" - }, - "type": { - "type": "string", - "title": "Type", - "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", - "default": "5424" - }, - "app_name": { - "type": "string", - "title": "Localhost", - "description": "The name of the application (Default: Zigbee2MQTT).", - "default": "Zigbee2MQTT" - }, - "eol": { - "type": "string", - "title": "eol", - "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", - "default": "/n" - } - } - } - ] - }, - "pan_id": { - "oneOf": [{ - "type": "string", - "title": "Pan ID (string)" - }, - { - "type": "number", - "title": "Pan ID (number)" - } - ], - "title": "Pan ID", - "requiresRestart": true, - "description": "ZigBee pan ID, changing requires repairing all devices!" - }, - "ext_pan_id": { - "oneOf": [{ - "type": "string", - "title": "Extended pan ID (string)" - }, - { - "type": "array", - "items": { - "type": "number" - }, - "title": "Extended pan ID (array)" - } - ], - "title": "Ext Pan ID", - "requiresRestart": true, - "description": "Zigbee extended pan ID, changing requires repairing all devices!" - }, - "channel": { - "type": "number", - "minimum": 11, - "maximum": 26, - "default": 11, - "title": "ZigBee channel", - "requiresRestart": true, - "description": "Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)", - "examples": [15, 20, 25] - }, - "adapter_concurrent": { - "title": "Adapter concurrency", - "requiresRestart": true, - "type": ["number", "null"], - "description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)" - }, - "adapter_delay": { - "type": ["number", "null"], - "requiresRestart": true, - "title": "Adapter delay", - "description": "Adapter delay" - }, - "cache_state": { - "type": "boolean", - "title": "Cache state", - "description": "MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant", - "default": true - }, - "cache_state_persistent": { - "type": "boolean", - "title": "Persist cache state", - "description": "Persist cached state, only used when cache_state: true", - "default": true - }, - "cache_state_send_on_startup": { - "type": "boolean", - "title": "Send cached state on startup", - "description": "Send cached state on startup, only used when cache_state: true", - "default": true - }, - "last_seen": { - "type": "string", - "enum": ["disable", "ISO_8601", "ISO_8601_local", "epoch"], - "title": "Last seen", - "description": "Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message", - "default": "disable" - }, - "elapsed": { - "type": "boolean", - "title": "Elapsed", - "description": "Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg", - "default": false - }, - "network_key": { - "oneOf": [{ - "type": "string", - "title": "Network key(string)" - }, - { - "type": "array", - "items": { - "type": "number" - }, - "title": "Network key(array)" - } - ], - "title": "Network key", - "requiresRestart": true, - "description": "Network encryption key, changing requires repairing all devices!" - }, - "timestamp_format": { - "type": "string", - "title": "Timestamp format", - "requiresRestart": true, - "description": "Log timestamp format", - "examples": ["YYYY-MM-DD HH:mm:ss"] - }, - "transmit_power": { - "type": ["number", "null"], - "title": "Transmit power", - "requiresRestart": true, - "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" - }, - "output": { - "type": "string", - "enum": ["attribute_and_json", "attribute", "json"], - "title": "MQTT output type", - "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" - }, - "homeassistant_discovery_topic": { - "type": "string", - "title": "Homeassistant discovery topic", - "description": "Home Assistant discovery topic", - "requiresRestart": true, - "examples": ["homeassistant"] - }, - "homeassistant_legacy_entity_attributes": { - "type": "boolean", - "title": "Home Assistant legacy entity attributes", - "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", - "default": true - }, - "homeassistant_status_topic": { - "type": "string", - "title": "Home Assistant status topic", - "description": "Home Assistant status topic", - "requiresRestart": true, - "examples": ["homeassistant/status"] - }, - "homeassistant_legacy_triggers": { - "type": "boolean", - "title": "Home Assistant legacy triggers", - "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered", - "default": true - }, - "soft_reset_timeout": { - "type": "number", - "minimum": 0, - "requiresRestart": true, - "title": "Soft reset timeout (deprecated)", - "description": "Soft reset ZNP after timeout", - "readOnly": true - }, - "report": { - "type": "boolean", - "title": "Reporting", - "requiresRestart": true, - "readOnly": true, - "description": "Enables report feature (deprecated)" - }, - "baudrate": { - "type": "number", - "title": "Baudrate (deprecated)", - "requiresRestart": true, - "description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600", - "examples": [38400, 57600, 115200] - }, - "rtscts": { - "type": "boolean", - "title": "RTS / CTS (deprecated)", - "requiresRestart": true, - "description": "RTS / CTS Hardware Flow Control for serial port" - }, - "ikea_ota_use_test_url": { - "type": "boolean", - "title": "IKEA TRADFRI OTA use test url (deprecated)", - "requiresRestart": true, - "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", - "default": false - } - } - }, - "experimental": { - "type": "object", - "title": "Experimental (deprecated)", - "properties": { - "transmit_power": { - "type": ["number", "null"], - "title": "Transmit power", - "requiresRestart": true, - "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" - }, - "output": { - "type": "string", - "enum": ["attribute_and_json", "attribute", "json"], - "title": "MQTT output type", - "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" - } - } - }, - "whitelist": { - "readOnly": true, - "type": "array", - "requiresRestart": true, - "title": "Whitelist (deprecated, use passlist)", - "items": { - "type": "string" - } - }, - "ban": { - "readOnly": true, - "type": "array", - "requiresRestart": true, - "title": "Ban (deprecated, use blocklist)", - "items": { - "type": "string" - } - } - }, - "required": ["mqtt"], - "definitions": { - "device": { - "type": "object", - "properties": { - "friendly_name": { - "type": "string", - "title": "Friendly name", - "description": "Used in the MQTT topic of a device. By default this is the device ID", - "readOnly": true - }, - "retain": { - "type": "boolean", - "title": "Retain", - "description": "Retain MQTT messages of this device" - }, - "disabled": { - "type": "boolean", - "title": "Disabled", - "description": "Disables the device (excludes device from network scans, availability and group state updates)", - "requiresRestart": true - }, - "retention": { - "type": "number", - "title": "Retention", - "description": "Sets the MQTT Message Expiry in seconds, Make sure to set mqtt.version to 5" - }, - "qos": { - "type": "number", - "title": "QoS", - "description": "QoS level for MQTT messages of this device" - }, - "debounce": { - "type": "number", - "title": "Debounce", - "description": "Debounces messages of this device" - }, - "debounce_ignore": { - "type": "array", - "items": { - "type": "string" - }, - "examples": ["action"], - "title": "Ignore debounce", - "description": "Protects unique payload values of specified payload properties from overriding within debounce time" - }, - "optimistic": { - "type": "boolean", - "title": "Optimistic", - "description": "Publish optimistic state after set", - "default": true - }, - "filtered_attributes": { - "type": "array", - "items": { - "type": "string" - }, - "examples": ["^temperature$", "^battery$", "^action$"], - "title": "Filtered publish attributes", - "description": "Filter attributes with regex from published payload." - }, - "filtered_cache": { - "type": "array", - "items": { - "type": "string" - }, - "examples": ["^input_actions$"], - "title": "Filtered attributes from cache", - "description": "Filter attributes with regex from being added to the cache, this prevents the attribute from being in the published payload when the value didn't change." - }, - "filtered_optimistic": { - "type": "array", - "items": { - "type": "string" - }, - "examples": ["^color_(mode|temp)$", "color"], - "title": "Filtered optimistic attributes", - "description": "Filter attributes with regex from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)." - }, - "icon": { - "type": "string", - "title": "Icon", - "description": "The user-defined device icon for the frontend. It can be a full URL link to an image (e.g. https://SOME.SITE/MODEL123.jpg) (you cannot use a path to a local file) or base64 encoded data URL (e.g. image/svg+xml;base64,PHN2ZyB3aW....R0aD)" - }, + "type": "object", + "properties": { "homeassistant": { - "type": ["object", "null"], - "title": "Home Assistant", - "properties": { - "name": { - "type": "string", - "title": "Home Assistant name", - "description": "Name of the device in Home Assistant" - } - } - } - }, - "required": ["friendly_name"] - }, - "group": { - "type": "object", - "properties": { - "friendly_name": { - "type": "string" + "title": "Home Assistant integration", + "requiresRestart": true, + "description": "Home Assistant integration (MQTT discovery)", + "default": false, + "oneOf": [ + { + "type": "boolean", + "title": "Home Assistant (simple)" + }, + { + "type": "object", + "title": "Home Assistant (advanced)", + "properties": { + "legacy_triggers": { + "type": "boolean", + "title": "Home Assistant legacy triggers", + "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discoverd", + "default": true + }, + "discovery_topic": { + "type": "string", + "title": "Homeassistant discovery topic", + "description": "Home Assistant discovery topic", + "requiresRestart": true, + "examples": ["homeassistant"] + }, + "legacy_entity_attributes": { + "type": "boolean", + "title": "Home Assistant legacy entity attributes", + "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", + "default": true + }, + "status_topic": { + "type": "string", + "title": "Home Assistant status topic", + "description": "Home Assistant status topic", + "requiresRestart": true, + "examples": ["homeassistant/status"] + } + } + } + ] }, - "retain": { - "type": "boolean" + "permit_join": { + "type": "boolean", + "default": false, + "title": "Permit join", + "description": "Allow new devices to join (re-applied at restart)" + }, + "availability": { + "oneOf": [ + { + "type": "boolean", + "title": "Availability (simple)" + }, + { + "type": "object", + "title": "Availability (advanced)", + "properties": { + "active": { + "type": "object", + "title": "Active", + "requiresRestart": true, + "description": "Options for active devices (routers/mains powered)", + "properties": { + "timeout": { + "type": "number", + "title": "Timeout", + "requiresRestart": true, + "default": 10, + "description": "Time after which an active device will be marked as offline in minutes" + } + } + }, + "passive": { + "type": "object", + "title": "Passive", + "requiresRestart": true, + "description": "Options for passive devices (mostly battery powered)", + "properties": { + "timeout": { + "type": "number", + "title": "Timeout", + "requiresRestart": true, + "default": 1500, + "description": "Time after which an passive device will be marked as offline in minutes" + } + } + } + } + } + ], + "title": "Availability", + "requiresRestart": true, + "description": "Checks whether devices are online/offline" + }, + "external_converters": { + "type": "array", + "title": "External converters", + "description": "You can define external converters to e.g. add support for a DiY device", + "requiresRestart": true, + "items": { + "type": "string" + }, + "examples": ["DIYRuZ_FreePad.js"] + }, + "mqtt": { + "type": "object", + "title": "MQTT", + "properties": { + "base_topic": { + "type": "string", + "title": "Base topic", + "default": "zigbee2mqtt", + "requiresRestart": true, + "description": "MQTT base topic for Zigbee2MQTT MQTT messages", + "examples": ["zigbee2mqtt"] + }, + "server": { + "type": "string", + "title": "MQTT server", + "requiresRestart": true, + "description": "MQTT server URL (use mqtts:// for SSL/TLS connection)", + "examples": ["mqtt://localhost:1883"] + }, + "keepalive": { + "type": "number", + "title": "Keepalive", + "requiresRestart": true, + "description": "MQTT keepalive in second", + "default": 60 + }, + "ca": { + "type": "string", + "title": "Certificate authority", + "requiresRestart": true, + "description": "Absolute path to SSL/TLS certificate of CA used to sign server and client certificates", + "examples": ["/etc/ssl/mqtt-ca.crt"] + }, + "key": { + "type": "string", + "title": "SSL/TLS key", + "requiresRestart": true, + "description": "Absolute path to SSL/TLS key for client-authentication", + "examples": ["/etc/ssl/mqtt-client.key"] + }, + "cert": { + "type": "string", + "title": "SSL/TLS certificate", + "description": "Absolute path to SSL/TLS certificate for client-authentication", + "requiresRestart": true, + "examples": ["/etc/ssl/mqtt-client.crt"] + }, + "user": { + "type": "string", + "title": "User", + "requiresRestart": true, + "description": "MQTT server authentication user", + "examples": ["johnnysilverhand"] + }, + "password": { + "type": "string", + "title": "Password", + "requiresRestart": true, + "description": "MQTT server authentication password", + "examples": ["ILOVEPELMENI"] + }, + "client_id": { + "type": "string", + "title": "Client ID", + "requiresRestart": true, + "description": "MQTT client ID", + "examples": ["MY_CLIENT_ID"] + }, + "reject_unauthorized": { + "type": "boolean", + "title": "Reject unauthorized", + "requiresRestart": true, + "description": "Disable self-signed SSL certificate", + "default": true + }, + "include_device_information": { + "type": "boolean", + "title": "Include device information", + "description": "Include device information to mqtt messages", + "default": false + }, + "version": { + "type": ["number", "null"], + "title": "Version", + "requiresRestart": true, + "description": "MQTT protocol version", + "default": 4, + "examples": [5] + }, + "force_disable_retain": { + "type": "boolean", + "title": "Force disable retain", + "requiresRestart": true, + "description": "Disable retain for all send messages. ONLY enable if you MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", + "default": false + } + }, + "required": ["server"] + }, + "serial": { + "type": "object", + "title": "Serial", + "properties": { + "port": { + "type": ["string", "null"], + "title": "Port", + "requiresRestart": true, + "description": "Location of the adapter. To autodetect the port, set null", + "examples": ["/dev/ttyACM0"] + }, + "disable_led": { + "type": "boolean", + "title": "Disable led", + "requiresRestart": true, + "description": "Disable LED of the adapter if supported", + "default": false + }, + "adapter": { + "type": ["string"], + "enum": ["deconz", "zstack", "zigate", "ezsp", "auto", "ember"], + "title": "Adapter", + "default": "auto", + "requiresRestart": true, + "description": "Adapter type, not needed unless you are experiencing problems" + }, + "baudrate": { + "type": "number", + "title": "Baudrate", + "requiresRestart": true, + "description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600", + "examples": [38400, 57600, 115200] + }, + "rtscts": { + "type": "boolean", + "title": "RTS / CTS", + "requiresRestart": true, + "description": "RTS / CTS Hardware Flow Control for serial port" + } + } + }, + "blocklist": { + "title": "Blocklist", + "requiresRestart": true, + "description": "Block devices from the network (by ieeeAddr)", + "type": "array", + "items": { + "type": "string" + } + }, + "passlist": { + "title": "Passlist", + "requiresRestart": true, + "description": "Allow only certain devices to join the network (by ieeeAddr). Note that all devices not on the passlist will be removed from the network!", + "type": "array", + "items": { + "type": "string" + } + }, + "map_options": { + "type": "object", + "title": "Networkmap", + "properties": { + "graphviz": { + "type": "object", + "properties": { + "colors": { + "type": "object", + "properties": { + "fill": { + "type": "object", + "properties": { + "enddevice": { + "type": "string" + }, + "coordinator": { + "type": "string" + }, + "router": { + "type": "string" + } + } + }, + "font": { + "type": "object", + "properties": { + "enddevice": { + "type": "string" + }, + "coordinator": { + "type": "string" + }, + "router": { + "type": "string" + } + } + }, + "line": { + "type": "object", + "properties": { + "active": { + "type": "string" + }, + "inactive": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "ota": { + "type": "object", + "title": "OTA updates", + "properties": { + "update_check_interval": { + "type": "number", + "title": "Update check interval", + "description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.", + "default": 1440 + }, + "disable_automatic_update_check": { + "type": "boolean", + "title": "Disable automatic update check", + "description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.", + "default": false + }, + "ikea_ota_use_test_url": { + "type": "boolean", + "title": "IKEA TRADFRI OTA use test url", + "requiresRestart": true, + "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", + "default": false + }, + "zigbee_ota_override_index_location": { + "type": ["string", "null"], + "title": "OTA index override file name", + "requiresRestart": true, + "description": "Location of override OTA index file", + "examples": ["index.json"] + } + } + }, + "frontend": { + "oneOf": [ + { + "type": "boolean", + "title": "Frontend (simple)" + }, + { + "type": "object", + "title": "Frontend (advanced)", + "properties": { + "port": { + "type": "number", + "title": "Port", + "description": "Frontend binding port. Ignored when using a unix domain socket", + "default": 8080, + "requiresRestart": true + }, + "host": { + "type": ["string", "null"], + "title": "Bind host", + "description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.", + "examples": ["127.0.0.1", "::1", "/run/zigbee2mqtt/zigbee2mqtt.sock"], + "requiresRestart": true + }, + "auth_token": { + "type": ["string", "null"], + "title": "Auth token", + "description": "Enables authentication, disabled by default", + "requiresRestart": true + }, + "url": { + "type": ["string", "null"], + "title": "URL", + "description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page", + "requiresRestart": true + }, + "ssl_cert": { + "type": ["string", "null"], + "title": "Certificate file path", + "description": "SSL Certificate file path for exposing HTTPS. The sibling property 'ssl_key' must be set for HTTPS to be activated.", + "requiresRestart": true + }, + "ssl_key": { + "type": ["string", "null"], + "title": "key file path", + "description": "SSL key file path for exposing HTTPS. The sibling property 'ssl_cert' must be set for HTTPS to be activated.", + "requiresRestart": true + } + } + } + ], + "title": "Frontend", + "requiresRestart": true }, "devices": { - "type": "array", - "items": { - "type": "string" - } + "type": "object", + "propertyNames": { + "pattern": "^0x[\\d\\w]{16}$" + }, + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/device" + } + } }, - "optimistic": { - "type": "boolean" + "groups": { + "type": "object", + "propertyNames": { + "pattern": "^[\\w].*$" + }, + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/group" + } + } }, - "qos": { - "type": "number" + "device_options": { + "type": "object", + "title": "Options that are applied to all devices" }, - "off_state": { - "type": ["string"], - "enum": ["all_members_off", "last_member_state"], - "title": "Group off state", - "default": "auto", - "requiresRestart": true, - "description": "Control when to publish state OFF for a group. 'all_members_off': only publish state OFF when all group members are in state OFF, 'last_member_state': publish state OFF whenever one of its members changes to OFF" + "advanced": { + "type": "object", + "title": "Advanced", + "properties": { + "legacy_api": { + "type": "boolean", + "title": "Legacy API", + "requiresRestart": true, + "description": "Disables the legacy api (false = disable)", + "default": true + }, + "legacy_availability_payload": { + "type": "boolean", + "title": "Legacy availability payload", + "requiresRestart": true, + "description": "Payload to be used for device availability and bridge/state topics. true = text, false = JSON", + "default": true + }, + "log_rotation": { + "type": "boolean", + "title": "Log rotation", + "requiresRestart": true, + "description": "Log rotation", + "default": true + }, + "log_symlink_current": { + "type": "boolean", + "title": "Log symlink current", + "requiresRestart": true, + "description": "Create symlink to current logs in the log directory", + "default": false + }, + "log_output": { + "type": "array", + "requiresRestart": true, + "items": { + "type": "string", + "enum": ["console", "file", "syslog"] + }, + "title": "Log output", + "description": "Output location of the log, leave empty to suppress logging" + }, + "log_directory": { + "type": "string", + "title": "Log directory", + "requiresRestart": true, + "description": "Location of log directory", + "examples": ["data/log/%TIMESTAMP%"] + }, + "log_file": { + "type": "string", + "title": "Log file", + "requiresRestart": true, + "description": "Log file name, can also contain timestamp", + "examples": ["zigbee2mqtt_%TIMESTAMP%.log"], + "default": "log.txt" + }, + "log_level": { + "type": "string", + "enum": ["error", "warning", "info", "debug", "warn"], + "title": "Log level", + "description": "Logging level", + "default": "info" + }, + "log_namespaced_levels": { + "type": "object", + "propertyNames": { + "pattern": "^(z2m|zhc|zh)(:[a-z0-9]{1,})*$" + }, + "additionalProperties": { + "type": "string", + "enum": ["error", "warning", "info", "debug"] + }, + "title": "Log Namespaced Levels", + "description": "Set individual log levels for certain namespaces", + "default": {}, + "examples": [{"z2m:mqtt": "warning"}, {"zh:ember:uart:ash": "info"}] + }, + "log_debug_to_mqtt_frontend": { + "type": "boolean", + "title": "Log debug to MQTT and frontend", + "description": "Log debug level to MQTT and frontend (may decrease overall performance)", + "requiresRestart": true, + "default": false + }, + "log_debug_namespace_ignore": { + "type": "string", + "title": "Log debug namespace ignore", + "description": "Do not log these namespaces (regex-based) for debug level", + "default": "", + "examples": ["^zhc:legacy:fz:(tuya|moes)", "^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller"] + }, + "log_syslog": { + "requiresRestart": true, + "oneOf": [ + { + "title": "syslog (disabled)", + "type": "null" + }, + { + "title": "syslog (enabled)", + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Host", + "description": "The host running syslogd, defaults to localhost.", + "default": "localhost" + }, + "port": { + "type": "number", + "title": "Port", + "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", + "default": 514 + }, + "protocol": { + "type": "string", + "title": "Protocol", + "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", + "default": "udp4", + "examples": ["udp4", "tls4", "unix", "unix-connect"] + }, + "path": { + "type": "string", + "title": "Path", + "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", + "default": "/dev/log", + "examples": ["/var/run/syslog"] + }, + "pid": { + "type": "string", + "title": "PID", + "description": "PID of the process that log messages are coming from (Default process.pid).", + "default": "process.pid" + }, + "localhost": { + "type": "string", + "title": "Localhost", + "description": "Host to indicate that log messages are coming from (Default: localhost).", + "default": "localhost" + }, + "type": { + "type": "string", + "title": "Type", + "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", + "default": "5424" + }, + "app_name": { + "type": "string", + "title": "Localhost", + "description": "The name of the application (Default: Zigbee2MQTT).", + "default": "Zigbee2MQTT" + }, + "eol": { + "type": "string", + "title": "eol", + "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", + "default": "/n" + } + } + } + ] + }, + "pan_id": { + "oneOf": [ + { + "type": "string", + "title": "Pan ID (string)" + }, + { + "type": "number", + "title": "Pan ID (number)" + } + ], + "title": "Pan ID", + "requiresRestart": true, + "description": "ZigBee pan ID, changing requires repairing all devices!" + }, + "ext_pan_id": { + "oneOf": [ + { + "type": "string", + "title": "Extended pan ID (string)" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "title": "Extended pan ID (array)" + } + ], + "title": "Ext Pan ID", + "requiresRestart": true, + "description": "Zigbee extended pan ID, changing requires repairing all devices!" + }, + "channel": { + "type": "number", + "minimum": 11, + "maximum": 26, + "default": 11, + "title": "ZigBee channel", + "requiresRestart": true, + "description": "Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)", + "examples": [15, 20, 25] + }, + "adapter_concurrent": { + "title": "Adapter concurrency", + "requiresRestart": true, + "type": ["number", "null"], + "description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)" + }, + "adapter_delay": { + "type": ["number", "null"], + "requiresRestart": true, + "title": "Adapter delay", + "description": "Adapter delay" + }, + "cache_state": { + "type": "boolean", + "title": "Cache state", + "description": "MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant", + "default": true + }, + "cache_state_persistent": { + "type": "boolean", + "title": "Persist cache state", + "description": "Persist cached state, only used when cache_state: true", + "default": true + }, + "cache_state_send_on_startup": { + "type": "boolean", + "title": "Send cached state on startup", + "description": "Send cached state on startup, only used when cache_state: true", + "default": true + }, + "last_seen": { + "type": "string", + "enum": ["disable", "ISO_8601", "ISO_8601_local", "epoch"], + "title": "Last seen", + "description": "Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message", + "default": "disable" + }, + "elapsed": { + "type": "boolean", + "title": "Elapsed", + "description": "Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg", + "default": false + }, + "network_key": { + "oneOf": [ + { + "type": "string", + "title": "Network key(string)" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "title": "Network key(array)" + } + ], + "title": "Network key", + "requiresRestart": true, + "description": "Network encryption key, changing requires repairing all devices!" + }, + "timestamp_format": { + "type": "string", + "title": "Timestamp format", + "requiresRestart": true, + "description": "Log timestamp format", + "examples": ["YYYY-MM-DD HH:mm:ss"] + }, + "transmit_power": { + "type": ["number", "null"], + "title": "Transmit power", + "requiresRestart": true, + "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" + }, + "output": { + "type": "string", + "enum": ["attribute_and_json", "attribute", "json"], + "title": "MQTT output type", + "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" + }, + "homeassistant_discovery_topic": { + "type": "string", + "title": "Homeassistant discovery topic", + "description": "Home Assistant discovery topic", + "requiresRestart": true, + "examples": ["homeassistant"] + }, + "homeassistant_legacy_entity_attributes": { + "type": "boolean", + "title": "Home Assistant legacy entity attributes", + "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", + "default": true + }, + "homeassistant_status_topic": { + "type": "string", + "title": "Home Assistant status topic", + "description": "Home Assistant status topic", + "requiresRestart": true, + "examples": ["homeassistant/status"] + }, + "homeassistant_legacy_triggers": { + "type": "boolean", + "title": "Home Assistant legacy triggers", + "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered", + "default": true + }, + "soft_reset_timeout": { + "type": "number", + "minimum": 0, + "requiresRestart": true, + "title": "Soft reset timeout (deprecated)", + "description": "Soft reset ZNP after timeout", + "readOnly": true + }, + "report": { + "type": "boolean", + "title": "Reporting", + "requiresRestart": true, + "readOnly": true, + "description": "Enables report feature (deprecated)" + }, + "baudrate": { + "type": "number", + "title": "Baudrate (deprecated)", + "requiresRestart": true, + "description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600", + "examples": [38400, 57600, 115200] + }, + "rtscts": { + "type": "boolean", + "title": "RTS / CTS (deprecated)", + "requiresRestart": true, + "description": "RTS / CTS Hardware Flow Control for serial port" + }, + "ikea_ota_use_test_url": { + "type": "boolean", + "title": "IKEA TRADFRI OTA use test url (deprecated)", + "requiresRestart": true, + "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", + "default": false + } + } }, - "filtered_attributes": { - "type": "array", - "items": { - "type": "string" - } + "experimental": { + "type": "object", + "title": "Experimental (deprecated)", + "properties": { + "transmit_power": { + "type": ["number", "null"], + "title": "Transmit power", + "requiresRestart": true, + "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" + }, + "output": { + "type": "string", + "enum": ["attribute_and_json", "attribute", "json"], + "title": "MQTT output type", + "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" + } + } + }, + "whitelist": { + "readOnly": true, + "type": "array", + "requiresRestart": true, + "title": "Whitelist (deprecated, use passlist)", + "items": { + "type": "string" + } + }, + "ban": { + "readOnly": true, + "type": "array", + "requiresRestart": true, + "title": "Ban (deprecated, use blocklist)", + "items": { + "type": "string" + } + } + }, + "required": ["mqtt"], + "definitions": { + "device": { + "type": "object", + "properties": { + "friendly_name": { + "type": "string", + "title": "Friendly name", + "description": "Used in the MQTT topic of a device. By default this is the device ID", + "readOnly": true + }, + "retain": { + "type": "boolean", + "title": "Retain", + "description": "Retain MQTT messages of this device" + }, + "disabled": { + "type": "boolean", + "title": "Disabled", + "description": "Disables the device (excludes device from network scans, availability and group state updates)", + "requiresRestart": true + }, + "retention": { + "type": "number", + "title": "Retention", + "description": "Sets the MQTT Message Expiry in seconds, Make sure to set mqtt.version to 5" + }, + "qos": { + "type": "number", + "title": "QoS", + "description": "QoS level for MQTT messages of this device" + }, + "debounce": { + "type": "number", + "title": "Debounce", + "description": "Debounces messages of this device" + }, + "debounce_ignore": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["action"], + "title": "Ignore debounce", + "description": "Protects unique payload values of specified payload properties from overriding within debounce time" + }, + "optimistic": { + "type": "boolean", + "title": "Optimistic", + "description": "Publish optimistic state after set", + "default": true + }, + "filtered_attributes": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["^temperature$", "^battery$", "^action$"], + "title": "Filtered publish attributes", + "description": "Filter attributes with regex from published payload." + }, + "filtered_cache": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["^input_actions$"], + "title": "Filtered attributes from cache", + "description": "Filter attributes with regex from being added to the cache, this prevents the attribute from being in the published payload when the value didn't change." + }, + "filtered_optimistic": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["^color_(mode|temp)$", "color"], + "title": "Filtered optimistic attributes", + "description": "Filter attributes with regex from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)." + }, + "icon": { + "type": "string", + "title": "Icon", + "description": "The user-defined device icon for the frontend. It can be a full URL link to an image (e.g. https://SOME.SITE/MODEL123.jpg) (you cannot use a path to a local file) or base64 encoded data URL (e.g. image/svg+xml;base64,PHN2ZyB3aW....R0aD)" + }, + "homeassistant": { + "type": ["object", "null"], + "title": "Home Assistant", + "properties": { + "name": { + "type": "string", + "title": "Home Assistant name", + "description": "Name of the device in Home Assistant" + } + } + } + }, + "required": ["friendly_name"] + }, + "group": { + "type": "object", + "properties": { + "friendly_name": { + "type": "string" + }, + "retain": { + "type": "boolean" + }, + "devices": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimistic": { + "type": "boolean" + }, + "qos": { + "type": "number" + }, + "off_state": { + "type": ["string"], + "enum": ["all_members_off", "last_member_state"], + "title": "Group off state", + "default": "auto", + "requiresRestart": true, + "description": "Control when to publish state OFF for a group. 'all_members_off': only publish state OFF when all group members are in state OFF, 'last_member_state': publish state OFF whenever one of its members changes to OFF" + }, + "filtered_attributes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["friendly_name"] } - }, - "required": ["friendly_name"] } - } } diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 9341c10b..b98c1c9c 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -1,10 +1,11 @@ -import data from './data'; -import utils from './utils'; +import Ajv, {ValidateFunction} from 'ajv'; import objectAssignDeep from 'object-assign-deep'; import path from 'path'; -import yaml from './yaml'; -import Ajv, {ValidateFunction} from 'ajv'; + +import data from './data'; import schemaJson from './settings.schema.json'; +import utils from './utils'; +import yaml from './yaml'; export let schema = schemaJson; // @ts-ignore schema = {}; @@ -28,18 +29,19 @@ objectAssignDeep(schema, schemaJson); /** NOTE: by order of priority, lower index is lower level (more important) */ export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const; -export type LogLevel = typeof LOG_LEVELS[number]; +export type LogLevel = (typeof LOG_LEVELS)[number]; // DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697 const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml'); const NULLABLE_SETTINGS = ['homeassistant']; const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson); -const ajvRestartRequired = new Ajv({allErrors: true}) - .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson); +const ajvRestartRequired = new Ajv({allErrors: true}).addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson); const ajvRestartRequiredDeviceOptions = new Ajv({allErrors: true}) - .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.device); + .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}) + .compile(schemaJson.definitions.device); const ajvRestartRequiredGroupOptions = new Ajv({allErrors: true}) - .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.group); + .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}) + .compile(schemaJson.definitions.group); const defaults: RecursivePartial = { permit_join: false, external_converters: [], @@ -92,7 +94,7 @@ const defaults: RecursivePartial = { log_debug_to_mqtt_frontend: false, log_debug_namespace_ignore: '', pan_id: 0x1a62, - ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD], + ext_pan_id: [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd], channel: 11, adapter_concurrent: null, adapter_delay: null, @@ -129,12 +131,15 @@ function loadSettingsWithDefaults(): void { } if (_settingsWithDefaults.homeassistant) { - const defaults = {discovery_topic: 'homeassistant', status_topic: 'hass/status', - legacy_entity_attributes: true, legacy_triggers: true}; + const defaults = {discovery_topic: 'homeassistant', status_topic: 'hass/status', legacy_entity_attributes: true, legacy_triggers: true}; const sLegacy = {}; if (_settingsWithDefaults.advanced) { - for (const key of ['homeassistant_legacy_triggers', 'homeassistant_discovery_topic', - 'homeassistant_legacy_entity_attributes', 'homeassistant_status_topic']) { + for (const key of [ + 'homeassistant_legacy_triggers', + 'homeassistant_discovery_topic', + 'homeassistant_legacy_entity_attributes', + 'homeassistant_status_topic', + ]) { // @ts-ignore if (_settingsWithDefaults.advanced[key] !== undefined) { // @ts-ignore @@ -202,7 +207,7 @@ function loadSettingsWithDefaults(): void { _settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist); } -function parseValueRef(text: string): {filename: string, key: string} | null { +function parseValueRef(text: string): {filename: string; key: string} | null { const match = /!(.*) (.*)/g.exec(text); if (match) { let filename = match[1]; @@ -248,7 +253,8 @@ function write(): void { // If an array, only write to first file and only devices which are not in the other files. if (Array.isArray(actual[type])) { - actual[type].filter((f: string, i: number) => i !== 0) + actual[type] + .filter((f: string, i: number) => i !== 0) .map((f: string) => yaml.readIfExists(data.joinPath(f), {})) .map((c: KeyValue) => Object.keys(c)) // @ts-ignore @@ -274,10 +280,7 @@ export function validate(): string[] { getInternalSettings(); } catch (error) { if (error.name === 'YAMLException') { - return [ - `Your YAML file: '${error.file}' is invalid ` + - `(use https://jsonformatter.org/yaml-validator to find and fix the issue)`, - ]; + return [`Your YAML file: '${error.file}' is invalid ` + `(use https://jsonformatter.org/yaml-validator to find and fix the issue)`]; } return [error.message]; @@ -288,18 +291,30 @@ 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 && + _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 && + _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 && + _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}')`); } @@ -404,7 +419,7 @@ function applyEnvironmentVariables(settings: Partial): void { if (key !== 'properties' && obj[key]) { const type = (obj[key].type || 'object').toString(); const envPart = path.reduce((acc, val) => `${acc}${val}_`, ''); - const envVariableName = (`ZIGBEE2MQTT_CONFIG_${envPart}${key}`).toUpperCase(); + const envVariableName = `ZIGBEE2MQTT_CONFIG_${envPart}${key}`.toUpperCase(); if (process.env[envVariableName]) { const setting = path.reduce((acc, val) => { /* eslint-disable-line */ // @ts-ignore @@ -498,8 +513,7 @@ export function apply(settings: Record): boolean { write(); ajvRestartRequired(settings); - const restartRequired = ajvRestartRequired.errors && - !!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart'); + const restartRequired = ajvRestartRequired.errors && !!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart'); return restartRequired; } @@ -607,8 +621,7 @@ export function removeDevice(IDorName: string): void { // Remove device from groups if (settings.groups) { - const regex = - new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`); + const regex = new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`); for (const group of Object.values(settings.groups).filter((g) => g.devices)) { group.devices = group.devices.filter((device) => !device.match(regex)); } @@ -701,7 +714,7 @@ export function changeEntityOptions(IDorName: string, newOptions: KeyValue): boo validator = ajvRestartRequiredDeviceOptions; } else if (getGroup(IDorName)) { objectAssignDeep(settings.groups[getGroup(IDorName).ID], newOptions); - utils.removeNullPropertiesFromObject(settings.groups[getGroup(IDorName).ID], NULLABLE_SETTINGS ); + utils.removeNullPropertiesFromObject(settings.groups[getGroup(IDorName).ID], NULLABLE_SETTINGS); validator = ajvRestartRequiredGroupOptions; } else { throw new Error(`Device or group '${IDorName}' does not exist`); diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 790b73e2..9c6bfd3d 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -1,11 +1,13 @@ -import equals from 'fast-deep-equal/es6'; -import humanizeDuration from 'humanize-duration'; -import data from './data'; -import vm from 'vm'; -import fs from 'fs'; -import path from 'path'; import type * as zhc from 'zigbee-herdsman-converters'; +import equals from 'fast-deep-equal/es6'; +import fs from 'fs'; +import humanizeDuration from 'humanize-duration'; +import path from 'path'; +import vm from 'vm'; + +import data from './data'; + // construct a local ISO8601 string (instead of UTC-based) // Example: // - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000 @@ -18,21 +20,30 @@ function toLocalISOString(date: Date): string { return (norm < 10 ? '0' : '') + norm; }; - 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 { return s[0].toUpperCase() + s.slice(1); } -async function getZigbee2MQTTVersion(includeCommitHash=true): Promise<{commitHash: string, version: string}> { +async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitHash: string; version: string}> { const git = await import('git-last-commit'); const packageJSON = await import('../..' + '/package.json'); @@ -75,7 +86,8 @@ function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch' 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 + else { + // relative return humanizeDuration(Date.now() - time, {language: 'en', largest: 2, round: true}) + ' ago'; } } @@ -168,7 +180,7 @@ export function* loadExternalConverter(moduleName: string): Generator '_' + y.toLowerCase()).replace(/^_/, '').replace('_i_d', '_id'); + return value + .replace(/\.?([A-Z])/g, (x, y) => '_' + y.toLowerCase()) + .replace(/^_/, '') + .replace('_i_d', '_id'); } } function charRange(start: string, stop: string): number[] { const result = []; - for (let idx=start.charCodeAt(0), end=stop.charCodeAt(0); idx <=end; ++idx) { + for (let idx = start.charCodeAt(0), end = stop.charCodeAt(0); idx <= end; ++idx) { result.push(idx); } return result; } -const controlCharacters = [ - ...charRange('\u0000', '\u001F'), - ...charRange('\u007f', '\u009F'), - ...charRange('\ufdd0', '\ufdef'), -]; +const controlCharacters = [...charRange('\u0000', '\u001F'), ...charRange('\u007f', '\u009F'), ...charRange('\ufdd0', '\ufdef')]; function containsControlCharacter(str: string): boolean { for (let i = 0; i < str.length; i++) { const ch = str.charCodeAt(i); - if (controlCharacters.includes(ch) || [0xFFFE, 0xFFFF].includes(ch & 0xFFFF)) { + if (controlCharacters.includes(ch) || [0xfffe, 0xffff].includes(ch & 0xffff)) { return true; } } @@ -239,7 +250,7 @@ function getAllFiles(path_: string): string[] { return result; } -function validateFriendlyName(name: string, throwFirstError=false): 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`); @@ -264,7 +275,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, '-')); + replaceByDash.forEach((r) => (sanitized = sanitized.replace(r, '-'))); return sanitized; } @@ -325,8 +336,12 @@ const hours = (hours: number): number => 1000 * 60 * 60 * hours; const minutes = (minutes: number): number => 1000 * 60 * minutes; const seconds = (seconds: number): number => 1000 * seconds; -async function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean, - publishEntityState: PublishEntityState): Promise { +async function publishLastSeen( + data: eventdata.LastSeenChanged, + settings: Settings, + allowMessageEmitted: boolean, + publishEntityState: PublishEntityState, +): Promise { /** * Prevent 2 MQTT publishes when 1 message event is received; * - In case reason == messageEmitted, receive.ts will only call this when it did not publish a @@ -382,10 +397,34 @@ function getScenes(entity: zh.Endpoint | zh.Group): Scene[] { } export default { - capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties, - equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile, - removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase, - isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep, - sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload, - getAllFiles, filterProperties, flatten, arrayUnique, getScenes, + capitalize, + getZigbee2MQTTVersion, + getDependencyVersion, + formatDate, + objectHasProperties, + equalsPartial, + getObjectProperty, + getResponse, + parseJSON, + loadModuleFromText, + loadModuleFromFile, + removeNullPropertiesFromObject, + toNetworkAddressHex, + toSnakeCase, + isEndpoint, + isZHGroup, + hours, + minutes, + seconds, + validateFriendlyName, + sleep, + sanitizeImageParameter, + isAvailabilityEnabledForEntity, + publishLastSeen, + availabilityPayload, + getAllFiles, + filterProperties, + flatten, + arrayUnique, + getScenes, }; diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index 5d1e33ee..82d4d717 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -1,11 +1,11 @@ -import yaml from 'js-yaml'; -import fs from 'fs'; import equals from 'fast-deep-equal/es6'; +import fs from 'fs'; +import yaml from 'js-yaml'; function read(file: string): KeyValue { try { const result = yaml.load(fs.readFileSync(file, 'utf8')); - return result as KeyValue ?? {}; + return (result as KeyValue) ?? {}; } catch (error) { if (error.name === 'YAMLException') { error.file = file; diff --git a/lib/zigbee.ts b/lib/zigbee.ts index 22f1c28a..043889d4 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -1,14 +1,15 @@ -import {Controller} from 'zigbee-herdsman'; -import logger from './util/logger'; -import * as settings from './util/settings'; -import data from './util/data'; -import utils from './util/utils'; -import stringify from 'json-stable-stringify-without-jsonify'; -import Device from './model/device'; -import Group from './model/group'; -import * as ZHEvents from 'zigbee-herdsman/dist/controller/events'; import bind from 'bind-decorator'; import {randomInt} from 'crypto'; +import stringify from 'json-stable-stringify-without-jsonify'; +import {Controller} from 'zigbee-herdsman'; +import * as ZHEvents from 'zigbee-herdsman/dist/controller/events'; + +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'; +import utils from './util/utils'; const entityIDRegex = new RegExp(`^(.+?)(?:/([^/]+))?$`); @@ -27,13 +28,14 @@ export default class Zigbee { logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`); const herdsmanSettings = { network: { - panID: settings.get().advanced.pan_id === 'GENERATE' ? - this.generatePanID() : settings.get().advanced.pan_id as number, - extendedPanID: settings.get().advanced.ext_pan_id === 'GENERATE' ? - this.generateExtPanID() : settings.get().advanced.ext_pan_id as number[], + panID: settings.get().advanced.pan_id === 'GENERATE' ? this.generatePanID() : (settings.get().advanced.pan_id as number), + extendedPanID: + settings.get().advanced.ext_pan_id === 'GENERATE' ? this.generateExtPanID() : (settings.get().advanced.ext_pan_id as number[]), channelList: [settings.get().advanced.channel], - networkKey: settings.get().advanced.network_key === 'GENERATE' ? - this.generateNetworkKey() : settings.get().advanced.network_key as number[], + networkKey: + settings.get().advanced.network_key === 'GENERATE' + ? this.generateNetworkKey() + : (settings.get().advanced.network_key as number[]), }, databasePath: data.joinPath('database.db'), databaseBackupPath: data.joinPath('database.db.backup'), @@ -108,10 +110,12 @@ export default class Zigbee { this.herdsman.on('message', async (data: ZHEvents.MessagePayload) => { const device = this.resolveDevice(data.device.ieeeAddr); await device.resolveDefinition(); - logger.debug(`Received Zigbee message from '${device.name}', type '${data.type}', ` + - `cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` + - (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) + - (device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``)); + logger.debug( + `Received Zigbee message from '${device.name}', type '${data.type}', ` + + `cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` + + (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) + + (device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``), + ); if (device.zh.type === 'Coordinator') return; this.eventBus.emitDeviceMessage({...data, device}); }); @@ -161,14 +165,16 @@ export default class Zigbee { const {vendor, description, model} = data.device.definition; logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`); } else { - logger.warning(`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` + - `'${data.device.zh.manufacturerName}' is NOT supported, ` + - // eslint-disable-next-line max-len - `please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`); + logger.warning( + `Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` + + `'${data.device.zh.manufacturerName}' is NOT supported, ` + + `please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`, + ); } } else if (data.status === 'failed') { logger.error(`Failed to interview '${name}', device has not successfully been paired`); - } else { // data.status === 'started' + } else { + // data.status === 'started' logger.info(`Starting interview of '${name}'`); } } @@ -186,7 +192,7 @@ export default class Zigbee { } private generatePanID(): number { - const panID = randomInt(1, 0xFFFF - 1); + const panID = randomInt(1, 0xffff - 1); settings.set(['advanced', 'pan_id'], panID); return panID; } @@ -230,7 +236,7 @@ export default class Zigbee { return this.herdsman.getPermitJoinTimeout(); } - async permitJoin(permit: boolean, device?: Device, time: number=undefined): Promise { + async permitJoin(permit: boolean, device?: Device, time: number = undefined): Promise { if (permit) { logger.info(`Zigbee: allowing new devices to join${device ? ` via ${device.name}` : ''}.`); } else { @@ -284,8 +290,7 @@ export default class Zigbee { } } - resolveEntityAndEndpoint(ID: string) - : {ID: string, entity: Device | Group, endpointID: string, endpoint: zh.Endpoint} { + resolveEntityAndEndpoint(ID: string): {ID: string; entity: Device | Group; endpointID: string; endpoint: zh.Endpoint} { // This function matches the following entity formats: // device_name (just device name) // device_name/ep_name (device name and endpoint numeric ID or name) @@ -324,8 +329,9 @@ export default class Zigbee { return this.herdsman.getGroups().map((g) => this.resolveGroup(g.groupID)); } - devices(includeCoordinator=true): Device[] { - return this.herdsman.getDevices() + devices(includeCoordinator = true): Device[] { + return this.herdsman + .getDevices() .map((d) => this.resolveDevice(d.ieeeAddr)) .filter((d) => includeCoordinator || d.zh.type !== 'Coordinator'); } @@ -371,7 +377,7 @@ export default class Zigbee { await this.herdsman.touchlinkIdentify(ieeeAddr, channel); } - async touchlinkScan(): Promise<{ieeeAddr: string, channel: number}[]> { + async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> { return this.herdsman.touchlinkScan(); } diff --git a/package-lock.json b/package-lock.json index bd5c8df1..e82d2b6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,9 +56,11 @@ "@typescript-eslint/parser": "^7.13.1", "babel-jest": "^29.7.0", "eslint": "^8.57.0", - "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", + "eslint-plugin-perfectionist": "^2.11.0", "jest": "^29.7.0", + "prettier": "^3.3.2", "tmp": "^0.2.3", "typescript": "^5.5.2" }, @@ -4671,16 +4673,16 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-google": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "bin": { + "eslint-config-prettier": "bin/cli.js" }, "peerDependencies": { - "eslint": ">=5.16.0" + "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-jest": { @@ -4708,6 +4710,38 @@ } } }, + "node_modules/eslint-plugin-perfectionist": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.11.0.tgz", + "integrity": "sha512-XrtBtiu5rbQv88gl+1e2RQud9te9luYNvKIgM9emttQ2zutHPzY/AQUucwxscDKV4qlTkvLTxjOFvxqeDpPorw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.13.0 || ^7.0.0", + "minimatch": "^9.0.3", + "natural-compare-lite": "^1.4.0" + }, + "peerDependencies": { + "astro-eslint-parser": "^1.0.2", + "eslint": ">=8.0.0", + "svelte": ">=3.0.0", + "svelte-eslint-parser": "^0.37.0", + "vue-eslint-parser": ">=9.0.0" + }, + "peerDependenciesMeta": { + "astro-eslint-parser": { + "optional": true + }, + "svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + }, + "vue-eslint-parser": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -7939,6 +7973,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/node-addon-api": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", @@ -8283,6 +8323,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 841eab70..1c5acfc4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "build": "tsc && node index.js writehash", "build-watch": "tsc --watch", "eslint": "eslint lib/ --max-warnings=0", + "pretty:write": "prettier --write lib test", + "pretty:check": "prettier --check lib test", "start": "node index.js", "test-with-coverage": "jest test --silent --maxWorkers=50% --coverage", "test": "jest test --silent --maxWorkers=50%", @@ -79,9 +81,11 @@ "@typescript-eslint/parser": "^7.13.1", "babel-jest": "^29.7.0", "eslint": "^8.57.0", - "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", + "eslint-plugin-perfectionist": "^2.11.0", "jest": "^29.7.0", + "prettier": "^3.3.2", "tmp": "^0.2.3", "typescript": "^5.5.2" }, diff --git a/scripts/generateChangelog.js b/scripts/generateChangelog.js index 218cb459..daf100a4 100644 --- a/scripts/generateChangelog.js +++ b/scripts/generateChangelog.js @@ -1,4 +1,3 @@ -/* eslint max-len: 0 */ const path = require('path'); const fs = require('fs'); const process = require('process'); diff --git a/test/assets/exampleExtension.js b/test/assets/exampleExtension.js index 5b85e826..149cc96f 100644 --- a/test/assets/exampleExtension.js +++ b/test/assets/exampleExtension.js @@ -1,11 +1,11 @@ class Example { constructor(zigbee, mqtt, state, publishEntityState, eventBus) { this.mqtt = mqtt; - this.mqtt.publish('example/extension', 'call from constructor') + this.mqtt.publish('example/extension', 'call from constructor'); } start() { - this.mqtt.publish('example/extension', 'test') + this.mqtt.publish('example/extension', 'test'); } } diff --git a/test/assets/mock-external-converter-multiple.js b/test/assets/mock-external-converter-multiple.js index 4bf7568f..d48543f7 100644 --- a/test/assets/mock-external-converter-multiple.js +++ b/test/assets/mock-external-converter-multiple.js @@ -9,25 +9,28 @@ const homeassistantSwitch = { }, }; -const mockDevices = [{ - mock: 1, - model: 'external_converters_device_1', - homeassistant: [homeassistantSwitch], - zigbeeModel: ['external_converter_device_1'], - vendor: 'external_1', - description: 'external_1', - fromZigbee: [], - toZigbee: [], - exposes: [], -}, { - mock: 2, - model: 'external_converters_device_2', - zigbeeModel: ['external_converter_device_2'], - vendor: 'external_2', - description: 'external_2', - fromZigbee: [], - toZigbee: [], - exposes: [], -}]; +const mockDevices = [ + { + mock: 1, + model: 'external_converters_device_1', + homeassistant: [homeassistantSwitch], + zigbeeModel: ['external_converter_device_1'], + vendor: 'external_1', + description: 'external_1', + fromZigbee: [], + toZigbee: [], + exposes: [], + }, + { + mock: 2, + model: 'external_converters_device_2', + zigbeeModel: ['external_converter_device_2'], + vendor: 'external_2', + description: 'external_2', + fromZigbee: [], + toZigbee: [], + exposes: [], + }, +]; -module.exports = mockDevices; \ No newline at end of file +module.exports = mockDevices; diff --git a/test/assets/mock-external-converter.js b/test/assets/mock-external-converter.js index 493c1b97..c2f04377 100644 --- a/test/assets/mock-external-converter.js +++ b/test/assets/mock-external-converter.js @@ -9,4 +9,4 @@ const mockDevice = { exposes: [], }; -module.exports = mockDevice; \ No newline at end of file +module.exports = mockDevice; diff --git a/test/availability.test.js b/test/availability.test.js index 6a7facb3..cf93d6b8 100644 --- a/test/availability.test.js +++ b/test/availability.test.js @@ -13,7 +13,8 @@ import stringify from 'json-stable-stringify-without-jsonify'; const mocks = [MQTT.publish, logger.warning, logger.info]; const devices = zigbeeHerdsman.devices; zigbeeHerdsman.returnDevices.push( - ...[devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr, devices.remote.ieeeAddr]) + ...[devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr, devices.remote.ieeeAddr], +); describe('Availability', () => { let controller; @@ -21,12 +22,12 @@ describe('Availability', () => { let resetExtension = async () => { await controller.enableDisableExtension(false, 'Availability'); await controller.enableDisableExtension(true, 'Availability'); - } + }; const setTimeAndAdvanceTimers = async (value) => { jest.setSystemTime(Date.now() + value); await jest.advanceTimersByTimeAsync(value); - } + }; beforeAll(async () => { jest.spyOn(utils, 'sleep').mockImplementation(async (seconds) => {}); @@ -44,24 +45,28 @@ describe('Availability', () => { settings.reRead(); settings.set(['availability'], true); settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], false); - Object.values(devices).forEach(d => d.lastSeen = utils.minutes(1)); + Object.values(devices).forEach((d) => (d.lastSeen = utils.minutes(1))); mocks.forEach((m) => m.mockClear()); await resetExtension(); Object.values(devices).forEach((d) => d.ping.mockClear()); }); - afterEach(async () => { - }) + afterEach(async () => {}); afterAll(async () => { await controller.stop(); jest.useRealTimers(); - }) + }); it('Should publish availability on startup for device where it is enabled for', async () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).not.toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color_2/availability', + 'online', + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Should ping on startup for enabled and unavailable devices', async () => { @@ -71,8 +76,8 @@ describe('Availability', () => { await resetExtension(); await setTimeAndAdvanceTimers(utils.minutes(1)); - expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);// enabled/unavailable - expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0);// enabled/available + expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); // enabled/unavailable + expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); // enabled/available }); it('Should not ping on startup for available or disabled devices', async () => { @@ -83,8 +88,8 @@ describe('Availability', () => { await resetExtension(); await setTimeAndAdvanceTimers(utils.minutes(1)); - expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);// enabled/available - expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0);// disabled/unavailable + expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); // enabled/available + expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); // disabled/unavailable }); it('Should publish offline for active device when not seen for 10 minutes', async () => { @@ -134,7 +139,9 @@ describe('Availability', () => { await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); - devices.bulb_color.ping.mockImplementationOnce(() => {throw new Error('failed')}); + devices.bulb_color.ping.mockImplementationOnce(() => { + throw new Error('failed'); + }); await setTimeAndAdvanceTimers(utils.minutes(15)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(2); @@ -218,7 +225,7 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(9)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb_color"})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb_color'})); await flushPromises(); await setTimeAndAdvanceTimers(utils.minutes(3)); @@ -249,7 +256,7 @@ describe('Availability', () => { //@ts-expect-error private const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); //@ts-expect-error private - controller.state.set(device, {state: 'OFF'}) + controller.state.set(device, {state: 'OFF'}); const endpoint = devices.bulb_color.getEndpoint(1); endpoint.read.mockClear(); @@ -269,7 +276,9 @@ describe('Availability', () => { endpoint.read.mockClear(); await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); - endpoint.read.mockImplementationOnce(() => {throw new Error('')}); + endpoint.read.mockImplementationOnce(() => { + throw new Error(''); + }); await setTimeAndAdvanceTimers(utils.seconds(3)); expect(endpoint.read).toHaveBeenCalledTimes(1); }); @@ -293,7 +302,12 @@ describe('Availability', () => { MQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Deprecated - should allow to block via advanced.availability_blocklist', async () => { @@ -332,15 +346,30 @@ describe('Availability', () => { await resetExtension(); devices.bulb_color_2.ping.mockClear(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_tradfri_remote/availability', + 'online', + {retain: true, qos: 1}, + expect.any(Function), + ); MQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_tradfri_remote/availability', + 'offline', + {retain: true, qos: 1}, + expect.any(Function), + ); MQTT.publish.mockClear(); devices.bulb_color_2.lastSeen = Date.now(); await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_tradfri_remote/availability', + 'online', + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Should clear the ping queue on stop', async () => { @@ -358,7 +387,7 @@ describe('Availability', () => { expect(availability.pingQueue).toEqual([]); // Validate the stop-interrupt implicitly by checking that it prevents further function invocations expect(publishAvailabilitySpy).not.toHaveBeenCalled(); - devices.bulb_color.ping = jest.fn();// ensure reset + devices.bulb_color.ping = jest.fn(); // ensure reset }); it('Should prevent instance restart', async () => { diff --git a/test/bind.test.js b/test/bind.test.js index 896fe532..35924b68 100644 --- a/test/bind.test.js +++ b/test/bind.test.js @@ -6,7 +6,7 @@ const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); const flushPromises = require('./lib/flushPromises'); const stringify = require('json-stable-stringify-without-jsonify'); -jest.mock('debounce', () => jest.fn(fn => fn)); +jest.mock('debounce', () => jest.fn((fn) => fn)); const debounce = require('debounce'); describe('Bind', () => { @@ -21,12 +21,12 @@ describe('Bind', () => { endpoint.bind.mockClear(); endpoint.unbind.mockClear(); } - } + }; let resetExtension = async () => { await controller.enableDisableExtension(false, 'Bind'); await controller.enableDisableExtension(true, 'Bind'); - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -49,7 +49,7 @@ describe('Bind', () => { afterAll(async () => { jest.useRealTimers(); - }) + }); it('Should bind to device and configure reporting', async () => { const device = zigbeeHerdsman.devices.remote; @@ -61,29 +61,44 @@ describe('Bind', () => { device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; const originalTargetBinds = target.binds; target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); mockClear(device); - target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")}); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); - expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]); + expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); - expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); expect(target.configureReporting).toHaveBeenCalledTimes(3); - expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + transaction: '1234', + data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; @@ -102,35 +117,48 @@ describe('Bind', () => { device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; const originalTargetInputClusters = target.inputClusters; target.inputClusters = [...originalTargetInputClusters]; - target.inputClusters.splice(originalTargetInputClusters.indexOf(8), 1);// remove genLevelCtrl + target.inputClusters.splice(originalTargetInputClusters.indexOf(8), 1); // remove genLevelCtrl const originalTargetOutputClusters = target.outputClusters; target.outputClusters = [...target.outputClusters, 8]; const originalTargetBinds = target.binds; target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); mockClear(device); - target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")}); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); - expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]); + expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); - expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); expect(target.configureReporting).toHaveBeenCalledTimes(2); - expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + transaction: '1234', + data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; @@ -150,9 +178,11 @@ describe('Bind', () => { device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; const originalTargetBinds = target.binds; target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); mockClear(device); - target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")}); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); const originalTargetCR = target.configuredReportings; target.configuredReportings = [ { @@ -161,28 +191,39 @@ describe('Bind', () => { minimumReportInterval: 0, maximumReportInterval: 3600, reportableChange: 0, - } + }, ]; - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); - expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]); + expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); - expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); expect(target.configureReporting).toHaveBeenCalledTimes(2); - expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + transaction: '1234', + data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.configuredReportings = originalTargetCR; @@ -195,14 +236,15 @@ describe('Bind', () => { const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); const endpoint = device.getEndpoint(1); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ["genOnOff"]})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ['genOnOff']})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genOnOff"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -216,8 +258,9 @@ describe('Bind', () => { expect(endpoint.bind).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"button","clusters":[],"failed":[]},"status":"error","error":"Nothing to bind"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'button', clusters: [], failed: []}, status: 'error', error: 'Nothing to bind'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -226,14 +269,16 @@ describe('Bind', () => { const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); // setup - target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")}); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); const originalRemoteBinds = device.getEndpoint(1).binds; device.getEndpoint(1).binds = []; const originalTargetBinds = target.binds; target.binds = [ {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'lightingColorCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)} + {cluster: {name: 'lightingColorCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, ]; const endpoint = device.getEndpoint(1); @@ -243,20 +288,29 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); // Disable reporting expect(target.configureReporting).toHaveBeenCalledTimes(3); - expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1}]); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + ]); expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(332242049); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // Teardown @@ -273,13 +327,14 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'Coordinator'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({"data":{"from":"remote","to":"Coordinator","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -294,16 +349,21 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'group_1'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(target1Member.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"group_1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // Should configure reproting for device added to group @@ -311,8 +371,12 @@ describe('Bind', () => { await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb'); await flushPromises(); expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(target1Member.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); }); it('Should unbind from group', async () => { @@ -326,13 +390,14 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({"data":{"from":"remote","to":"group_1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -347,7 +412,10 @@ describe('Bind', () => { const originalBinds = endpoint.binds; endpoint.binds = []; - target1Member.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; + target1Member.binds = [ + {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + ]; target1Member.configureReporting.mockClear(); mockClear(device); MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: true})); @@ -369,7 +437,10 @@ describe('Bind', () => { const originalBinds = endpoint.binds; endpoint.binds = []; - target1Member.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; + target1Member.binds = [ + {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + ]; target1Member.configureReporting.mockClear(); mockClear(device); MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: false})); @@ -377,8 +448,12 @@ describe('Bind', () => { expect(endpoint.unbind).toHaveBeenCalledTimes(3); // with skip_disable_reporting set, we expect it to reconfigure reporting expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 65535, "minimumReportInterval": 5, "reportableChange": 1}]) - expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 65535, "minimumReportInterval": 0, "reportableChange": 0}]) + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 65535, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 65535, minimumReportInterval: 0, reportableChange: 0}, + ]); endpoint.binds = originalBinds; }); @@ -390,13 +465,14 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '1'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -405,14 +481,21 @@ describe('Bind', () => { const device = zigbeeHerdsman.devices.remote; const endpoint = device.getEndpoint(1); mockClear(device); - endpoint.bind.mockImplementation(() => {throw new Error('failed')}); + endpoint.bind.mockImplementation(() => { + throw new Error('failed'); + }); MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"bulb_color","clusters":[],"failed":["genScenes","genOnOff","genLevelCtrl"]},"status":"error","error":"Failed to bind"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote', to: 'bulb_color', clusters: [], failed: ['genScenes', 'genOnOff', 'genLevelCtrl']}, + status: 'error', + error: 'Failed to bind', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -424,11 +507,12 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote/ep2","to":"wall_switch_double/right","clusters":["genOnOff"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote/ep2', to: 'wall_switch_double/right', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -440,11 +524,12 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'temperature_sensor', to: 'heating_actuator'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("msTemperatureMeasurement", target); + expect(endpoint.bind).toHaveBeenCalledWith('msTemperatureMeasurement', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"temperature_sensor","to":"heating_actuator","clusters":["msTemperatureMeasurement"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -456,11 +541,12 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote/ep2","to":"wall_switch","clusters":["genOnOff"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -472,13 +558,17 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: target})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({"data":{"from":"remote","to":"default_bind_group","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -491,8 +581,13 @@ describe('Bind', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote_not_existing","to":"bulb_color"},"status":"error","error":"Source device 'remote_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote_not_existing', to: 'bulb_color'}, + status: 'error', + error: "Source device 'remote_not_existing' does not exist", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -505,8 +600,13 @@ describe('Bind', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote/not_existing_endpoint","to":"bulb_color"},"status":"error","error":"Source device 'remote' does not have endpoint 'not_existing_endpoint'"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote/not_existing_endpoint', to: 'bulb_color'}, + status: 'error', + error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -519,8 +619,13 @@ describe('Bind', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"bulb_color_not_existing"},"status":"error","error":"Target device or group 'bulb_color_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote', to: 'bulb_color_not_existing'}, + status: 'error', + error: "Target device or group 'bulb_color_not_existing' does not exist", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -533,8 +638,13 @@ describe('Bind', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({"data":{"from":"remote","to":"bulb_color/not_existing_endpoint"},"status":"error","error":"Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: 'remote', to: 'bulb_color/not_existing_endpoint'}, + status: 'error', + error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -546,15 +656,24 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}, + }); }); it('Legacy api: Should log error when there is nothing to bind', async () => { @@ -576,15 +695,24 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'bulb_color'); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}, + }); }); it('Legacy api: Should unbind coordinator', async () => { @@ -596,15 +724,24 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'Coordinator'); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'Coordinator', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'Coordinator', cluster: 'genLevelCtrl'}, + }); }); it('Legacy api: Should bind to groups', async () => { @@ -615,15 +752,24 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'group_1'); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}, + }); }); it('Legacy api: Should bind to group by number', async () => { @@ -634,15 +780,24 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/bind/remote', '1'); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); - expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); - expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_bind', + message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}, + }); }); it('Legacy api: Should log when bind fails', async () => { @@ -650,7 +805,9 @@ describe('Bind', () => { const device = zigbeeHerdsman.devices.remote; const endpoint = device.getEndpoint(1); mockClear(device); - endpoint.bind.mockImplementationOnce(() => {throw new Error('failed')}); + endpoint.bind.mockImplementationOnce(() => { + throw new Error('failed'); + }); MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); await flushPromises(); expect(logger.error).toHaveBeenCalledWith("Failed to bind cluster 'genScenes' from 'remote' to 'bulb_color' (Error: failed)"); @@ -665,7 +822,7 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch_double/right'); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); }); it('Legacy api: Should bind to default endpoint returned by endpoints()', async () => { @@ -676,7 +833,7 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch'); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); }); it('Legacy api: Should unbind from default_bind_group', async () => { @@ -687,53 +844,92 @@ describe('Bind', () => { MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', target); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901); - expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901); - expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}, + }); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'}}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'}, + }); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'}}); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ + type: 'device_unbind', + message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'}, + }); }); it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { const remote = zigbeeHerdsman.devices.remote; - const data = {"button":3,"unknown1":3145728,"type":2,"unknown2":0,"time":1}; - const payload = {data, cluster: 'manuSpecificPhilips', device: remote, endpoint: remote.getEndpoint(2), type: 'commandHueNotification', linkquality: 10, groupID: 0}; + const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; + const payload = { + data, + cluster: 'manuSpecificPhilips', + device: remote, + endpoint: remote.getEndpoint(2), + type: 'commandHueNotification', + linkquality: 10, + groupID: 0, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(debounce).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); }); it('Should poll bounded Hue bulb when receiving message from scene controller', async () => { const remote = zigbeeHerdsman.devices.bj_scene_switch; - const data = {"action": "recall_2_row_1"}; - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => {throw new Error('failed')}); - const payload = {data, cluster: 'genScenes', device: remote, endpoint: remote.getEndpoint(10), type: 'commandRecall', linkquality: 10, groupID: 0}; + const data = {action: 'recall_2_row_1'}; + zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => { + throw new Error('failed'); + }); + const payload = { + data, + cluster: 'genScenes', + device: remote, + endpoint: remote.getEndpoint(10), + type: 'commandRecall', + linkquality: 10, + groupID: 0, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); // Calls to three clusters are expected in this case expect(debounce).toHaveBeenCalledTimes(3); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genOnOff", ["onOff"]); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('lightingColorCtrl', [ + 'currentX', + 'currentY', + 'colorTemperature', + ]); }); it('Should poll grouped Hue bulb when receiving message from TRADFRI remote', async () => { zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockClear(); zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read.mockClear(); const remote = zigbeeHerdsman.devices.tradfri_remote; - const data = {"stepmode":0,"stepsize":43,"transtime":5}; - const payload = {data, cluster: 'genLevelCtrl', device: remote, endpoint: remote.getEndpoint(1), type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071}; + const data = {stepmode: 0, stepsize: 43, transtime: 5}; + const payload = { + data, + cluster: 'genLevelCtrl', + device: remote, + endpoint: remote.getEndpoint(1), + type: 'commandStepWithOnOff', + linkquality: 10, + groupID: 15071, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(debounce).toHaveBeenCalledTimes(2); expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genOnOff", ["onOff"]); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']); // Should also only debounce once await zigbeeHerdsman.events.message(payload); diff --git a/test/bridge.test.js b/test/bridge.test.js index f5061722..18991555 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -11,18 +11,19 @@ const utils = require('../lib/util/utils').default; const stringify = require('json-stable-stringify-without-jsonify'); const mockJSZipFile = jest.fn(); -const mockJSZipGenerateAsync = jest.fn().mockReturnValue("THISISBASE64"); +const mockJSZipGenerateAsync = jest.fn().mockReturnValue('THISISBASE64'); -jest.mock("jszip", () => +jest.mock('jszip', () => jest.fn().mockImplementation((path) => { return { file: mockJSZipFile, generateAsync: mockJSZipGenerateAsync, - } - }) + }; + }), ); -const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM, zigfred_plus, bulb_custom_cluster} = zigbeeHerdsman.devices; +const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM, zigfred_plus, bulb_custom_cluster} = + zigbeeHerdsman.devices; zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr); @@ -43,7 +44,7 @@ describe('Bridge', () => { await controller.enableDisableExtension(false, 'Bridge'); await controller.enableDisableExtension(true, 'Bridge'); extension = controller.extensions.find((e) => e.constructor.name === 'Bridge'); - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -76,7 +77,7 @@ describe('Bridge', () => { afterAll(async () => { jest.useRealTimers(); - }) + }); it('Should publish bridge info on startup', async () => { await resetExtension(); @@ -87,9 +88,145 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', - stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "legacy_availability_payload": true,"output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.log", "log_level": "info", "log_namespaced_levels": {}, "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "log_debug_namespace_ignore": "", "log_debug_to_mqtt_frontend": false, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "description": "this is my bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0xf4ce368a38be56a1":{"cover_1_enabled":"true","cover_1_tilt_enabled":"true","cover_2_enabled":"true","cover_2_tilt_enabled":"true","dimmer_1_dimming_enabled":"true","dimmer_1_enabled":"true","dimmer_2_dimming_enabled":"true","dimmer_2_enabled":"true","dimmer_3_dimming_enabled":"true","dimmer_3_enabled":"true","dimmer_4_dimming_enabled":"true","dimmer_4_enabled":"true","friendly_name":"zigfred_plus","front_surface_enabled":"true","retain":false}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882104a44560":{"friendly_name":"TS0601_switch"}, "0x0017882104a44562":{"friendly_name":"TS0601_cover_switch"}, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, "zigbee_herdsman_converters": zhcVersion, "zigbee_herdsman": zhVersion, config_schema: settings.schema }), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + restart_required: false, + commit: version.commitHash, + config: { + advanced: { + legacy_availability_payload: true, + output: 'json', + adapter_concurrent: null, + adapter_delay: null, + availability_blacklist: [], + availability_blocklist: [], + availability_passlist: [], + availability_whitelist: [], + cache_state: true, + cache_state_persistent: true, + cache_state_send_on_startup: true, + channel: 11, + elapsed: false, + ext_pan_id: [221, 221, 221, 221, 221, 221, 221, 221], + last_seen: 'disable', + legacy_api: false, + log_directory: directory, + log_file: 'log.log', + log_level: 'info', + log_namespaced_levels: {}, + log_output: ['console', 'file'], + log_rotation: true, + log_symlink_current: false, + log_syslog: {}, + log_debug_namespace_ignore: '', + log_debug_to_mqtt_frontend: false, + pan_id: 6754, + report: false, + soft_reset_timeout: 0, + timestamp_format: 'YYYY-MM-DD HH:mm:ss', + }, + blocklist: [], + device_options: {}, + devices: { + '0x000b57fffec6a5b2': {friendly_name: 'bulb', description: 'this is my bulb', retain: true}, + '0x000b57fffec6a5b3': {friendly_name: 'bulb_color', retain: false}, + '0x000b57fffec6a5b4': {friendly_name: 'bulb_color_2', retain: false}, + '0x000b57fffec6a5b7': {friendly_name: 'bulb_2', retain: false}, + '0x0017880104a44559': {friendly_name: 'J1_cover'}, + '0x0017880104e43559': {friendly_name: 'U202DST600ZB'}, + '0x0017880104e44559': {friendly_name: '3157100_thermostat'}, + '0x0017880104e45517': {friendly_name: 'remote', retain: true}, + '0x0017880104e45520': {friendly_name: 'button', retain: false}, + '0x0017880104e45521': {friendly_name: 'button_double_key', retain: false}, + '0x0017880104e45522': {friendly_name: 'weather_sensor', qos: 1, retain: false}, + '0x0017880104e45523': {friendly_name: 'occupancy_sensor', retain: false}, + '0x0017880104e45524': {friendly_name: 'power_plug', retain: false}, + '0x0017880104e45526': {friendly_name: 'GL-S-007ZS'}, + '0x0017880104e45529': {friendly_name: 'unsupported2', retain: false}, + '0x0017880104e45530': {friendly_name: 'button_double_key_interviewing', retain: false}, + '0x0017880104e45540': {friendly_name: 'ikea_onoff'}, + '0x0017880104e45541': {friendly_name: 'wall_switch', retain: false}, + '0x0017880104e45542': {friendly_name: 'wall_switch_double', retain: false}, + '0x0017880104e45543': {friendly_name: 'led_controller_1', retain: false}, + '0x0017880104e45544': {friendly_name: 'led_controller_2', retain: false}, + '0x0017880104e45545': {friendly_name: 'dimmer_wall_switch', retain: false}, + '0x0017880104e45547': {friendly_name: 'curtain', retain: false}, + '0x0017880104e45548': {friendly_name: 'fan', retain: false}, + '0x0017880104e45549': {friendly_name: 'siren', retain: false}, + '0x0017880104e45550': {friendly_name: 'thermostat', retain: false}, + '0x0017880104e45551': {friendly_name: 'smart vent', retain: false}, + '0x0017880104e45552': {friendly_name: 'j1', retain: false}, + '0x0017880104e45553': {friendly_name: 'bulb_enddevice', retain: false}, + '0x0017880104e45559': {friendly_name: 'cc2530_router', retain: false}, + '0x0017880104e45560': {friendly_name: 'livolo', retain: false}, + '0x0017880104e45724': {friendly_name: 'GLEDOPTO_2ID'}, + '0xf4ce368a38be56a1': { + cover_1_enabled: 'true', + cover_1_tilt_enabled: 'true', + cover_2_enabled: 'true', + cover_2_tilt_enabled: 'true', + dimmer_1_dimming_enabled: 'true', + dimmer_1_enabled: 'true', + dimmer_2_dimming_enabled: 'true', + dimmer_2_enabled: 'true', + dimmer_3_dimming_enabled: 'true', + dimmer_3_enabled: 'true', + dimmer_4_dimming_enabled: 'true', + dimmer_4_enabled: 'true', + friendly_name: 'zigfred_plus', + front_surface_enabled: 'true', + retain: false, + }, + '0x0017882104a44559': {friendly_name: 'TS0601_thermostat'}, + '0x0017882104a44560': {friendly_name: 'TS0601_switch'}, + '0x0017882104a44562': {friendly_name: 'TS0601_cover_switch'}, + '0x0017882194e45543': {friendly_name: 'QS-Zigbee-D02-TRIAC-2C-LN'}, + '0x90fd9ffffe4b64aa': {friendly_name: 'SP600_OLD'}, + '0x90fd9ffffe4b64ab': {friendly_name: 'SP600_NEW'}, + '0x90fd9ffffe4b64ac': {friendly_name: 'MKS-CM-W5'}, + '0x90fd9ffffe4b64ae': {friendly_name: 'tradfri_remote', retain: false}, + '0x90fd9ffffe4b64af': {friendly_name: 'roller_shutter'}, + '0x90fd9ffffe4b64ax': {friendly_name: 'ZNLDP12LM'}, + '0x0017880104e45561': {friendly_name: 'temperature_sensor'}, + '0x0017880104e45562': {friendly_name: 'heating_actuator'}, + }, + external_converters: [], + groups: { + 1: {friendly_name: 'group_1', retain: false}, + 9: {devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'}, + 11: {devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false}, + 14: {devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false}, + 12: {devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false}, + 15071: {devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false}, + 2: {friendly_name: 'group_2', retain: false}, + 21: {devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'}, + }, + homeassistant: false, + map_options: { + graphviz: { + colors: { + fill: {coordinator: '#e04e5d', enddevice: '#fff8ce', router: '#4ea3e0'}, + font: {coordinator: '#ffffff', enddevice: '#000000', router: '#ffffff'}, + line: {active: '#009900', inactive: '#994444'}, + }, + }, + }, + mqtt: {base_topic: 'zigbee2mqtt', include_device_information: false, server: 'mqtt://localhost', force_disable_retain: false}, + ota: {disable_automatic_update_check: false, update_check_interval: 1440}, + passlist: [], + permit_join: true, + serial: {disable_led: false, port: '/dev/dummy'}, + }, + coordinator: {ieee_address: '0x00124b00120144ae', meta: {revision: 20190425, version: 1}, type: 'z-Stack'}, + log_level: 'info', + network: {channel: 15, extended_pan_id: [0, 11, 22], pan_id: 5674}, + permit_join: false, + version: version.version, + zigbee_herdsman_converters: zhcVersion, + zigbee_herdsman: zhVersion, + config_schema: settings.schema, + }), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -98,9 +235,1790 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]) expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/devices', - stringify([{"date_code":null,"definition":null,"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":[],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"Coordinator","ieee_address":"0x00124b00120144ae","interview_completed":false,"interviewing":false,"model_id":null,"network_address":0,"power_source":null,"software_build_id":null,"supported":true,"type":"Coordinator"},{"date_code":null,"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454}],"property":"color_temp","type":"numeric","unit":"mired","value_max":454,"value_min":250},{"access":7,"description":"Color temperature after cold power on of this light","label":"Color temp startup","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":454,"value_min":250}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":7,"category":"config","description":"Advanced color behavior","features":[{"access":2,"description":"Controls whether color and color temperature can be set while light is off","label":"Execute if off","name":"execute_if_off","property":"execute_if_off","type":"binary","value_off":false,"value_on":true}],"label":"Color options","name":"color_options","property":"color_options","type":"composite"},{"access":2,"category":"config","description":"Initiate device identification","label":"Identify","name":"identify","property":"identify","type":"enum","values":["identify"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).","label":"Identify timeout","name":"identify_timeout","property":"identify_timeout","type":"numeric","value_max":30,"value_min":1},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"IKEA"},"description":"this is my bulb","disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[{"attribute":"onOff","cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}],"scenes":[]}},"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2","interview_completed":true,"interviewing":false,"model_id":"TRADFRI bulb E27 WS opal 980lm","network_address":40369,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"},{"date_code":"2019.09","definition":{"description":"Hue Go","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":150},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":500}],"property":"color_temp","type":"numeric","unit":"mired","value_max":500,"value_min":150},{"access":7,"description":"Color temperature after cold power on of this light","label":"Color temp startup","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":150},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":500},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":500,"value_min":150},{"access":7,"description":"Color of this light in the CIE 1931 color space (x/y)","features":[{"access":7,"label":"X","name":"x","property":"x","type":"numeric"},{"access":7,"label":"Y","name":"y","property":"y","type":"numeric"}],"label":"Color (X/Y)","name":"color_xy","property":"color","type":"composite"},{"access":7,"description":"Color of this light expressed as hue/saturation","features":[{"access":7,"label":"Hue","name":"hue","property":"hue","type":"numeric"},{"access":7,"label":"Saturation","name":"saturation","property":"saturation","type":"numeric"}],"label":"Color (HS)","name":"color_hs","property":"color","type":"composite"}],"type":"light"},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":2,"label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","candle","fireplace","colorloop","finish_effect","stop_effect","stop_hue_effect"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"7146060PH","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"Philips"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[],"scenes":[{"id":1,"name":"Chill scene"}]}},"friendly_name":"bulb_color_2","ieee_address":"0x000b57fffec6a5b4","interview_completed":true,"interviewing":false,"manufacturer":"Philips","model_id":"LLC020","network_address":401292,"power_source":"Mains (single phase)","software_build_id":"5.127.1.26581","supported":true,"type":"Router"},{"date_code":null,"definition":{"description":"Hue dimmer switch","exposes":[{"access":1,"category":"diagnostic","description":"Remaining battery in %, can take up to 24 hours before reported","label":"Battery","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"category":"diagnostic","description":"Triggered action duration in seconds","label":"Action duration","name":"action_duration","property":"action_duration","type":"numeric","unit":"s"},{"access":1,"category":"diagnostic","description":"Triggered action (e.g. a button click)","label":"Action","name":"action","property":"action","type":"enum","values":["on_press","on_press_release","on_hold","on_hold_release","up_press","up_press_release","up_hold","up_hold_release","down_press","down_press_release","down_hold","down_hold_release","off_press","off_press_release","off_hold","off_hold_release"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"324131092621","options":[{"access":2,"description":"Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).","label":"Legacy","name":"legacy","property":"legacy","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. Only works when legacy is false.","features":[{"access":2,"description":"Delta per interval, 20 by default","label":"Delta","name":"delta","property":"delta","type":"numeric","value_min":0},{"access":2,"description":"Interval duration","label":"Interval","name":"interval","property":"interval","type":"numeric","unit":"ms","value_min":0}],"label":"Simulated brightness","name":"simulated_brightness","property":"simulated_brightness","type":"composite"}],"supports_ota":true,"vendor":"Philips"},"disabled":false,"endpoints":{"1":{"bindings":[{"cluster":"genLevelCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"lightingColorCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"id":1,"type":"group"}},{"cluster":"genLevelCtrl","target":{"id":1,"type":"group"}}],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[],"scenes":[]},"2":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genOta","genOnOff"]},"configured_reportings":[],"scenes":[]}},"friendly_name":"remote","ieee_address":"0x0017880104e45517","interview_completed":true,"interviewing":false,"model_id":"RWL021","network_address":6535,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Automatically generated definition","exposes":[{"access":1,"category":"diagnostic","description":"Triggered action (e.g. a button click)","label":"Action","name":"action","property":"action","type":"enum","values":["on","off","toggle","brightness_move_to_level","brightness_move_up","brightness_move_down","brightness_step_up","brightness_step_down","brightness_stop"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"notSupportedModelID","options":[{"access":2,"description":"Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ","features":[{"access":2,"description":"Delta per interval, 20 by default","label":"Delta","name":"delta","property":"delta","type":"numeric","value_min":0},{"access":2,"description":"Interval duration","label":"Interval","name":"interval","property":"interval","type":"numeric","unit":"ms","value_min":0}],"label":"Simulated brightness","name":"simulated_brightness","property":"simulated_brightness","type":"composite"}],"supports_ota":false,"vendor":"notSupportedMfg"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[],"scenes":[]}},"friendly_name":"0x0017880104e45518","ieee_address":"0x0017880104e45518","interview_completed":true,"interviewing":false,"manufacturer":"notSupportedMfg","model_id":"notSupportedModelID","network_address":6536,"power_source":"Battery","software_build_id":null,"supported":false,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Wireless mini switch","exposes":[{"access":1,"category":"diagnostic","description":"Remaining battery in %, can take up to 24 hours before reported","label":"Battery","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"category":"diagnostic","description":"Voltage of the battery in millivolts","label":"Voltage","name":"voltage","property":"voltage","type":"numeric","unit":"mV"},{"access":1,"category":"diagnostic","description":"Temperature of the device","label":"Device temperature","name":"device_temperature","property":"device_temperature","type":"numeric","unit":"°C"},{"access":1,"category":"diagnostic","description":"Number of power outages (since last pairing)","label":"Power outage count","name":"power_outage_count","property":"power_outage_count","type":"numeric"},{"access":1,"category":"diagnostic","description":"Triggered action (e.g. a button click)","label":"Action","name":"action","property":"action","type":"enum","values":["single","double","triple","quadruple","hold","release"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"WXKG11LM","options":[{"access":2,"description":"Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.","label":"Device temperature calibration","name":"device_temperature_calibration","property":"device_temperature_calibration","type":"numeric"},{"access":2,"description":"Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).","label":"Legacy","name":"legacy","property":"legacy","type":"binary","value_off":false,"value_on":true}],"supports_ota":false,"vendor":"Aqara"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[{"attribute":1337,"cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}],"scenes":[]}},"friendly_name":"button","ieee_address":"0x0017880104e45520","interview_completed":true,"interviewing":false,"model_id":"lumi.sensor_switch.aq2","network_address":6537,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Temperature and humidity sensor","exposes":[{"access":1,"category":"diagnostic","description":"Remaining battery in %, can take up to 24 hours before reported","label":"Battery","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"description":"Measured temperature value","label":"Temperature","name":"temperature","property":"temperature","type":"numeric","unit":"°C"},{"access":1,"description":"Measured relative humidity","label":"Humidity","name":"humidity","property":"humidity","type":"numeric","unit":"%"},{"access":1,"description":"The measured atmospheric pressure","label":"Pressure","name":"pressure","property":"pressure","type":"numeric","unit":"hPa"},{"access":1,"category":"diagnostic","description":"Voltage of the battery in millivolts","label":"Voltage","name":"voltage","property":"voltage","type":"numeric","unit":"mV"},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"WSDCGQ11LM","options":[{"access":2,"description":"Calibrates the temperature value (absolute offset), takes into effect on next report of device.","label":"Temperature calibration","name":"temperature_calibration","property":"temperature_calibration","type":"numeric"},{"access":2,"description":"Number of digits after decimal point for temperature, takes into effect on next report of device. This option can only decrease the precision, not increase it.","label":"Temperature precision","name":"temperature_precision","property":"temperature_precision","type":"numeric","value_max":3,"value_min":0},{"access":2,"description":"Calibrates the humidity value (absolute offset), takes into effect on next report of device.","label":"Humidity calibration","name":"humidity_calibration","property":"humidity_calibration","type":"numeric"},{"access":2,"description":"Number of digits after decimal point for humidity, takes into effect on next report of device. This option can only decrease the precision, not increase it.","label":"Humidity precision","name":"humidity_precision","property":"humidity_precision","type":"numeric","value_max":3,"value_min":0},{"access":2,"description":"Calibrates the pressure value (absolute offset), takes into effect on next report of device.","label":"Pressure calibration","name":"pressure_calibration","property":"pressure_calibration","type":"numeric"},{"access":2,"description":"Number of digits after decimal point for pressure, takes into effect on next report of device. This option can only decrease the precision, not increase it.","label":"Pressure precision","name":"pressure_precision","property":"pressure_precision","type":"numeric","value_max":3,"value_min":0}],"supports_ota":false,"vendor":"Aqara"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"weather_sensor","ieee_address":"0x0017880104e45522","interview_completed":true,"interviewing":false,"model_id":"lumi.weather","network_address":6539,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Mi smart plug","exposes":[{"features":[{"access":7,"description":"On/off state of the switch","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"}],"type":"switch"},{"access":5,"category":"diagnostic","description":"Instantaneous measured power","label":"Power","name":"power","property":"power","type":"numeric","unit":"W"},{"access":1,"description":"Sum of consumed energy","label":"Energy","name":"energy","property":"energy","type":"numeric","unit":"kWh"},{"access":1,"category":"diagnostic","description":"Temperature of the device","label":"Device temperature","name":"device_temperature","property":"device_temperature","type":"numeric","unit":"°C"},{"access":7,"category":"config","description":"Enable/disable the power outage memory, this recovers the on/off mode after power failure","label":"Power outage memory","name":"power_outage_memory","property":"power_outage_memory","type":"binary","value_off":false,"value_on":true},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"ZNCZ02LM","options":[{"access":2,"description":"Calibrates the power value (percentual offset), takes into effect on next report of device.","label":"Power calibration","name":"power_calibration","property":"power_calibration","type":"numeric"},{"access":2,"description":"Number of digits after decimal point for power, takes into effect on next report of device. This option can only decrease the precision, not increase it.","label":"Power precision","name":"power_precision","property":"power_precision","type":"numeric","value_max":3,"value_min":0},{"access":2,"description":"Calibrates the energy value (percentual offset), takes into effect on next report of device.","label":"Energy calibration","name":"energy_calibration","property":"energy_calibration","type":"numeric"},{"access":2,"description":"Number of digits after decimal point for energy, takes into effect on next report of device. This option can only decrease the precision, not increase it.","label":"Energy precision","name":"energy_precision","property":"energy_precision","type":"numeric","value_max":3,"value_min":0},{"access":2,"description":"Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.","label":"Device temperature calibration","name":"device_temperature_calibration","property":"device_temperature_calibration","type":"numeric"},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"Xiaomi"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genOnOff"],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"power_plug","ieee_address":"0x0017880104e45524","interview_completed":true,"interviewing":false,"model_id":"lumi.plug","network_address":6540,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"},{"date_code":null,"definition":{"description":"zigfred plus smart in-wall switch","exposes":[{"access":1,"category":"diagnostic","description":"Triggered action (e.g. a button click)","label":"Action","name":"action","property":"action","type":"enum","values":["button_1_single","button_1_double","button_1_hold","button_1_release","button_2_single","button_2_double","button_2_hold","button_2_release","button_3_single","button_3_double","button_3_hold","button_3_release","button_4_single","button_4_double","button_4_hold","button_4_release"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0},{"endpoint":"l1","features":[{"access":7,"description":"On/off state of this light","endpoint":"l1","label":"State","name":"state","property":"state_l1","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","endpoint":"l1","label":"Brightness","name":"brightness","property":"brightness_l1","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color of this light in the CIE 1931 color space (x/y)","endpoint":"l1","features":[{"access":7,"label":"X","name":"x","property":"x","type":"numeric"},{"access":7,"label":"Y","name":"y","property":"y","type":"numeric"}],"label":"Color (X/Y)","name":"color_xy","property":"color_l1","type":"composite"}],"type":"light"},{"endpoint":"l2","features":[{"access":7,"description":"On/off state of this light","endpoint":"l2","label":"State","name":"state","property":"state_l2","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","endpoint":"l2","label":"Brightness","name":"brightness","property":"brightness_l2","type":"numeric","value_max":254,"value_min":0}],"type":"light"},{"endpoint":"l3","features":[{"access":7,"description":"On/off state of this light","endpoint":"l3","label":"State","name":"state","property":"state_l3","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","endpoint":"l3","label":"Brightness","name":"brightness","property":"brightness_l3","type":"numeric","value_max":254,"value_min":0}],"type":"light"},{"endpoint":"l4","features":[{"access":7,"description":"On/off state of this light","endpoint":"l4","label":"State","name":"state","property":"state_l4","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","endpoint":"l4","label":"Brightness","name":"brightness","property":"brightness_l4","type":"numeric","value_max":254,"value_min":0}],"type":"light"},{"endpoint":"l5","features":[{"access":7,"description":"On/off state of this light","endpoint":"l5","label":"State","name":"state","property":"state_l5","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","endpoint":"l5","label":"Brightness","name":"brightness","property":"brightness_l5","type":"numeric","value_max":254,"value_min":0}],"type":"light"},{"endpoint":"l6","features":[{"access":7,"endpoint":"l6","label":"State","name":"state","property":"state_l6","type":"enum","values":["OPEN","CLOSE","STOP"]},{"access":7,"description":"Position of this cover","endpoint":"l6","label":"Position","name":"position","property":"position_l6","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":7,"description":"Tilt of this cover","endpoint":"l6","label":"Tilt","name":"tilt","property":"tilt_l6","type":"numeric","unit":"%","value_max":100,"value_min":0}],"type":"cover"},{"endpoint":"l7","features":[{"access":7,"endpoint":"l7","label":"State","name":"state","property":"state_l7","type":"enum","values":["OPEN","CLOSE","STOP"]},{"access":7,"description":"Position of this cover","endpoint":"l7","label":"Position","name":"position","property":"position_l7","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":7,"description":"Tilt of this cover","endpoint":"l7","label":"Tilt","name":"tilt","property":"tilt_l7","type":"numeric","unit":"%","value_max":100,"value_min":0}],"type":"cover"}],"model":"ZFP-1A-CH","options":[{"access":2,"description":"Front Surface LED enabled","label":"Front surface enabled","name":"front_surface_enabled","property":"front_surface_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 1 enabled","label":"Dimmer 1 enabled","name":"dimmer_1_enabled","property":"dimmer_1_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 1 dimmable","label":"Dimmer 1 dimming enabled","name":"dimmer_1_dimming_enabled","property":"dimmer_1_dimming_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 2 enabled","label":"Dimmer 2 enabled","name":"dimmer_2_enabled","property":"dimmer_2_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 2 dimmable","label":"Dimmer 2 dimming enabled","name":"dimmer_2_dimming_enabled","property":"dimmer_2_dimming_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 3 enabled","label":"Dimmer 3 enabled","name":"dimmer_3_enabled","property":"dimmer_3_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 3 dimmable","label":"Dimmer 3 dimming enabled","name":"dimmer_3_dimming_enabled","property":"dimmer_3_dimming_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 4 enabled","label":"Dimmer 4 enabled","name":"dimmer_4_enabled","property":"dimmer_4_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Dimmer 4 dimmable","label":"Dimmer 4 dimming enabled","name":"dimmer_4_dimming_enabled","property":"dimmer_4_dimming_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Cover 1 enabled","label":"Cover 1 enabled","name":"cover_1_enabled","property":"cover_1_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Cover 1 tiltable","label":"Cover 1 tilt enabled","name":"cover_1_tilt_enabled","property":"cover_1_tilt_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Cover 2 enabled","label":"Cover 2 enabled","name":"cover_2_enabled","property":"cover_2_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"Cover 2 tiltable","label":"Cover 2 tilt enabled","name":"cover_2_tilt_enabled","property":"cover_2_tilt_enabled","type":"enum","values":["auto","true","false"]},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Inverts the cover position, false: open=100,close=0, true: open=0,close=100 (default false).","label":"Invert cover","name":"invert_cover","property":"invert_cover","type":"binary","value_off":false,"value_on":true}],"supports_ota":false,"vendor":"Siglis"},"disabled":false,"endpoints":{"10":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl"],"output":[]},"configured_reportings":[],"scenes":[]},"11":{"bindings":[],"clusters":{"input":["genBasic","genScenes","closuresWindowCovering"],"output":[]},"configured_reportings":[],"scenes":[]},"12":{"bindings":[],"clusters":{"input":["genBasic","genScenes","closuresWindowCovering"],"output":[]},"configured_reportings":[],"scenes":[]},"5":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":[]},"configured_reportings":[],"scenes":[]},"7":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl"],"output":[]},"configured_reportings":[],"scenes":[]},"8":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl"],"output":[]},"configured_reportings":[],"scenes":[]},"9":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl"],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"zigfred_plus","ieee_address":"0xf4ce368a38be56a1","interview_completed":true,"interviewing":false,"manufacturer":"Siglis","model_id":"zigfred plus","network_address":6589,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"},{"date_code":null,"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454}],"property":"color_temp","type":"numeric","unit":"mired","value_max":454,"value_min":250},{"access":7,"description":"Color temperature after cold power on of this light","label":"Color temp startup","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":454,"value_min":250}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":7,"category":"config","description":"Advanced color behavior","features":[{"access":2,"description":"Controls whether color and color temperature can be set while light is off","label":"Execute if off","name":"execute_if_off","property":"execute_if_off","type":"binary","value_off":false,"value_on":true}],"label":"Color options","name":"color_options","property":"color_options","type":"composite"},{"access":2,"category":"config","description":"Initiate device identification","label":"Identify","name":"identify","property":"identify","type":"enum","values":["identify"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).","label":"Identify timeout","name":"identify_timeout","property":"identify_timeout","type":"numeric","value_max":30,"value_min":1},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"IKEA"},"disabled":false,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[],"scenes":[]}},"friendly_name":"0x000b57fffec6a5c2","ieee_address":"0x000b57fffec6a5c2","interview_completed":true,"interviewing":false,"manufacturer":null,"model_id":"TRADFRI bulb E27 WS opal 980lm","network_address":40369,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"}]), - { retain: true, qos: 0 }, - expect.any(Function) + stringify([ + { + date_code: null, + definition: null, + disabled: false, + endpoints: {1: {bindings: [], clusters: {input: [], output: []}, configured_reportings: [], scenes: []}}, + friendly_name: 'Coordinator', + ieee_address: '0x00124b00120144ae', + interview_completed: false, + interviewing: false, + model_id: null, + network_address: 0, + power_source: null, + software_build_id: null, + supported: true, + type: 'Coordinator', + }, + { + date_code: null, + definition: { + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + exposes: [ + { + features: [ + { + access: 7, + description: 'On/off state of this light', + label: 'State', + name: 'state', + property: 'state', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + label: 'Brightness', + name: 'brightness', + property: 'brightness', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + { + access: 7, + description: 'Color temperature of this light', + label: 'Color temp', + name: 'color_temp', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + ], + property: 'color_temp', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + { + access: 7, + description: 'Color temperature after cold power on of this light', + label: 'Color temp startup', + name: 'color_temp_startup', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + {description: 'Restore previous color_temp on cold power on', name: 'previous', value: 65535}, + ], + property: 'color_temp_startup', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + ], + type: 'light', + }, + { + access: 2, + description: 'Triggers an effect on the light (e.g. make light blink for a few seconds)', + label: 'Effect', + name: 'effect', + property: 'effect', + type: 'enum', + values: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], + }, + { + access: 7, + category: 'config', + description: + 'Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.', + label: 'Power-on behavior', + name: 'power_on_behavior', + property: 'power_on_behavior', + type: 'enum', + values: ['off', 'on', 'toggle', 'previous'], + }, + { + access: 7, + category: 'config', + description: 'Advanced color behavior', + features: [ + { + access: 2, + description: 'Controls whether color and color temperature can be set while light is off', + label: 'Execute if off', + name: 'execute_if_off', + property: 'execute_if_off', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + label: 'Color options', + name: 'color_options', + property: 'color_options', + type: 'composite', + }, + { + access: 2, + category: 'config', + description: 'Initiate device identification', + label: 'Identify', + name: 'identify', + property: 'identify', + type: 'enum', + values: ['identify'], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'LED1545G12', + options: [ + { + access: 2, + description: + 'Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).', + label: 'Transition', + name: 'transition', + property: 'transition', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: + 'When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).', + label: 'Color sync', + name: 'color_sync', + property: 'color_sync', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: + 'Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).', + label: 'Identify timeout', + name: 'identify_timeout', + property: 'identify_timeout', + type: 'numeric', + value_max: 30, + value_min: 1, + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: true, + vendor: 'IKEA', + }, + description: 'this is my bulb', + disabled: false, + endpoints: { + 1: { + bindings: [], + clusters: { + input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + output: ['genScenes', 'genOta'], + }, + configured_reportings: [ + { + attribute: 'onOff', + cluster: 'genOnOff', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 20, + }, + ], + scenes: [], + }, + }, + friendly_name: 'bulb', + ieee_address: '0x000b57fffec6a5b2', + interview_completed: true, + interviewing: false, + model_id: 'TRADFRI bulb E27 WS opal 980lm', + network_address: 40369, + power_source: 'Mains (single phase)', + software_build_id: null, + supported: true, + type: 'Router', + }, + { + date_code: '2019.09', + definition: { + description: 'Hue Go', + exposes: [ + { + features: [ + { + access: 7, + description: 'On/off state of this light', + label: 'State', + name: 'state', + property: 'state', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + label: 'Brightness', + name: 'brightness', + property: 'brightness', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + { + access: 7, + description: 'Color temperature of this light', + label: 'Color temp', + name: 'color_temp', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 150}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 500}, + ], + property: 'color_temp', + type: 'numeric', + unit: 'mired', + value_max: 500, + value_min: 150, + }, + { + access: 7, + description: 'Color temperature after cold power on of this light', + label: 'Color temp startup', + name: 'color_temp_startup', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 150}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 500}, + {description: 'Restore previous color_temp on cold power on', name: 'previous', value: 65535}, + ], + property: 'color_temp_startup', + type: 'numeric', + unit: 'mired', + value_max: 500, + value_min: 150, + }, + { + access: 7, + description: 'Color of this light in the CIE 1931 color space (x/y)', + features: [ + {access: 7, label: 'X', name: 'x', property: 'x', type: 'numeric'}, + {access: 7, label: 'Y', name: 'y', property: 'y', type: 'numeric'}, + ], + label: 'Color (X/Y)', + name: 'color_xy', + property: 'color', + type: 'composite', + }, + { + access: 7, + description: 'Color of this light expressed as hue/saturation', + features: [ + {access: 7, label: 'Hue', name: 'hue', property: 'hue', type: 'numeric'}, + {access: 7, label: 'Saturation', name: 'saturation', property: 'saturation', type: 'numeric'}, + ], + label: 'Color (HS)', + name: 'color_hs', + property: 'color', + type: 'composite', + }, + ], + type: 'light', + }, + { + access: 7, + category: 'config', + description: + 'Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.', + label: 'Power-on behavior', + name: 'power_on_behavior', + property: 'power_on_behavior', + type: 'enum', + values: ['off', 'on', 'toggle', 'previous'], + }, + { + access: 2, + label: 'Effect', + name: 'effect', + property: 'effect', + type: 'enum', + values: [ + 'blink', + 'breathe', + 'okay', + 'channel_change', + 'candle', + 'fireplace', + 'colorloop', + 'finish_effect', + 'stop_effect', + 'stop_hue_effect', + ], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: '7146060PH', + options: [ + { + access: 2, + description: + 'Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).', + label: 'Transition', + name: 'transition', + property: 'transition', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: + 'When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).', + label: 'Color sync', + name: 'color_sync', + property: 'color_sync', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: true, + vendor: 'Philips', + }, + disabled: false, + endpoints: { + 1: { + bindings: [], + clusters: { + input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + output: ['genScenes', 'genOta'], + }, + configured_reportings: [], + scenes: [{id: 1, name: 'Chill scene'}], + }, + }, + friendly_name: 'bulb_color_2', + ieee_address: '0x000b57fffec6a5b4', + interview_completed: true, + interviewing: false, + manufacturer: 'Philips', + model_id: 'LLC020', + network_address: 401292, + power_source: 'Mains (single phase)', + software_build_id: '5.127.1.26581', + supported: true, + type: 'Router', + }, + { + date_code: null, + definition: { + description: 'Hue dimmer switch', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Remaining battery in %, can take up to 24 hours before reported', + label: 'Battery', + name: 'battery', + property: 'battery', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + { + access: 1, + category: 'diagnostic', + description: 'Triggered action duration in seconds', + label: 'Action duration', + name: 'action_duration', + property: 'action_duration', + type: 'numeric', + unit: 's', + }, + { + access: 1, + category: 'diagnostic', + description: 'Triggered action (e.g. a button click)', + label: 'Action', + name: 'action', + property: 'action', + type: 'enum', + values: [ + 'on_press', + 'on_press_release', + 'on_hold', + 'on_hold_release', + 'up_press', + 'up_press_release', + 'up_hold', + 'up_hold_release', + 'down_press', + 'down_press_release', + 'down_hold', + 'down_hold_release', + 'off_press', + 'off_press_release', + 'off_hold', + 'off_hold_release', + ], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: '324131092621', + options: [ + { + access: 2, + description: + 'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).', + label: 'Legacy', + name: 'legacy', + property: 'legacy', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. Only works when legacy is false.', + features: [ + { + access: 2, + description: 'Delta per interval, 20 by default', + label: 'Delta', + name: 'delta', + property: 'delta', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: 'Interval duration', + label: 'Interval', + name: 'interval', + property: 'interval', + type: 'numeric', + unit: 'ms', + value_min: 0, + }, + ], + label: 'Simulated brightness', + name: 'simulated_brightness', + property: 'simulated_brightness', + type: 'composite', + }, + ], + supports_ota: true, + vendor: 'Philips', + }, + disabled: false, + endpoints: { + 1: { + bindings: [ + {cluster: 'genLevelCtrl', target: {endpoint: 1, ieee_address: '0x000b57fffec6a5b3', type: 'endpoint'}}, + {cluster: 'genOnOff', target: {endpoint: 1, ieee_address: '0x000b57fffec6a5b3', type: 'endpoint'}}, + {cluster: 'lightingColorCtrl', target: {endpoint: 1, ieee_address: '0x000b57fffec6a5b3', type: 'endpoint'}}, + {cluster: 'genOnOff', target: {id: 1, type: 'group'}}, + {cluster: 'genLevelCtrl', target: {id: 1, type: 'group'}}, + ], + clusters: {input: ['genBasic'], output: ['genBasic', 'genOnOff', 'genLevelCtrl', 'genScenes']}, + configured_reportings: [], + scenes: [], + }, + 2: {bindings: [], clusters: {input: ['genBasic'], output: ['genOta', 'genOnOff']}, configured_reportings: [], scenes: []}, + }, + friendly_name: 'remote', + ieee_address: '0x0017880104e45517', + interview_completed: true, + interviewing: false, + model_id: 'RWL021', + network_address: 6535, + power_source: 'Battery', + software_build_id: null, + supported: true, + type: 'EndDevice', + }, + { + date_code: null, + definition: { + description: 'Automatically generated definition', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Triggered action (e.g. a button click)', + label: 'Action', + name: 'action', + property: 'action', + type: 'enum', + values: [ + 'on', + 'off', + 'toggle', + 'brightness_move_to_level', + 'brightness_move_up', + 'brightness_move_down', + 'brightness_step_up', + 'brightness_step_down', + 'brightness_stop', + ], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'notSupportedModelID', + options: [ + { + access: 2, + description: + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ', + features: [ + { + access: 2, + description: 'Delta per interval, 20 by default', + label: 'Delta', + name: 'delta', + property: 'delta', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: 'Interval duration', + label: 'Interval', + name: 'interval', + property: 'interval', + type: 'numeric', + unit: 'ms', + value_min: 0, + }, + ], + label: 'Simulated brightness', + name: 'simulated_brightness', + property: 'simulated_brightness', + type: 'composite', + }, + ], + supports_ota: false, + vendor: 'notSupportedMfg', + }, + disabled: false, + endpoints: { + 1: { + bindings: [], + clusters: {input: ['genBasic'], output: ['genBasic', 'genOnOff', 'genLevelCtrl', 'genScenes']}, + configured_reportings: [], + scenes: [], + }, + }, + friendly_name: '0x0017880104e45518', + ieee_address: '0x0017880104e45518', + interview_completed: true, + interviewing: false, + manufacturer: 'notSupportedMfg', + model_id: 'notSupportedModelID', + network_address: 6536, + power_source: 'Battery', + software_build_id: null, + supported: false, + type: 'EndDevice', + }, + { + date_code: null, + definition: { + description: 'Wireless mini switch', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Remaining battery in %, can take up to 24 hours before reported', + label: 'Battery', + name: 'battery', + property: 'battery', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + { + access: 1, + category: 'diagnostic', + description: 'Voltage of the battery in millivolts', + label: 'Voltage', + name: 'voltage', + property: 'voltage', + type: 'numeric', + unit: 'mV', + }, + { + access: 1, + category: 'diagnostic', + description: 'Temperature of the device', + label: 'Device temperature', + name: 'device_temperature', + property: 'device_temperature', + type: 'numeric', + unit: '°C', + }, + { + access: 1, + category: 'diagnostic', + description: 'Number of power outages (since last pairing)', + label: 'Power outage count', + name: 'power_outage_count', + property: 'power_outage_count', + type: 'numeric', + }, + { + access: 1, + category: 'diagnostic', + description: 'Triggered action (e.g. a button click)', + label: 'Action', + name: 'action', + property: 'action', + type: 'enum', + values: ['single', 'double', 'triple', 'quadruple', 'hold', 'release'], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'WXKG11LM', + options: [ + { + access: 2, + description: 'Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.', + label: 'Device temperature calibration', + name: 'device_temperature_calibration', + property: 'device_temperature_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).', + label: 'Legacy', + name: 'legacy', + property: 'legacy', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: false, + vendor: 'Aqara', + }, + disabled: false, + endpoints: { + 1: { + bindings: [], + clusters: {input: ['genBasic'], output: ['genBasic', 'genOnOff', 'genLevelCtrl', 'genScenes']}, + configured_reportings: [ + { + attribute: 1337, + cluster: 'genOnOff', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 20, + }, + ], + scenes: [], + }, + }, + friendly_name: 'button', + ieee_address: '0x0017880104e45520', + interview_completed: true, + interviewing: false, + model_id: 'lumi.sensor_switch.aq2', + network_address: 6537, + power_source: 'Battery', + software_build_id: null, + supported: true, + type: 'EndDevice', + }, + { + date_code: null, + definition: { + description: 'Temperature and humidity sensor', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Remaining battery in %, can take up to 24 hours before reported', + label: 'Battery', + name: 'battery', + property: 'battery', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + { + access: 1, + description: 'Measured temperature value', + label: 'Temperature', + name: 'temperature', + property: 'temperature', + type: 'numeric', + unit: '°C', + }, + { + access: 1, + description: 'Measured relative humidity', + label: 'Humidity', + name: 'humidity', + property: 'humidity', + type: 'numeric', + unit: '%', + }, + { + access: 1, + description: 'The measured atmospheric pressure', + label: 'Pressure', + name: 'pressure', + property: 'pressure', + type: 'numeric', + unit: 'hPa', + }, + { + access: 1, + category: 'diagnostic', + description: 'Voltage of the battery in millivolts', + label: 'Voltage', + name: 'voltage', + property: 'voltage', + type: 'numeric', + unit: 'mV', + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'WSDCGQ11LM', + options: [ + { + access: 2, + description: 'Calibrates the temperature value (absolute offset), takes into effect on next report of device.', + label: 'Temperature calibration', + name: 'temperature_calibration', + property: 'temperature_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Number of digits after decimal point for temperature, takes into effect on next report of device. This option can only decrease the precision, not increase it.', + label: 'Temperature precision', + name: 'temperature_precision', + property: 'temperature_precision', + type: 'numeric', + value_max: 3, + value_min: 0, + }, + { + access: 2, + description: 'Calibrates the humidity value (absolute offset), takes into effect on next report of device.', + label: 'Humidity calibration', + name: 'humidity_calibration', + property: 'humidity_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Number of digits after decimal point for humidity, takes into effect on next report of device. This option can only decrease the precision, not increase it.', + label: 'Humidity precision', + name: 'humidity_precision', + property: 'humidity_precision', + type: 'numeric', + value_max: 3, + value_min: 0, + }, + { + access: 2, + description: 'Calibrates the pressure value (absolute offset), takes into effect on next report of device.', + label: 'Pressure calibration', + name: 'pressure_calibration', + property: 'pressure_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Number of digits after decimal point for pressure, takes into effect on next report of device. This option can only decrease the precision, not increase it.', + label: 'Pressure precision', + name: 'pressure_precision', + property: 'pressure_precision', + type: 'numeric', + value_max: 3, + value_min: 0, + }, + ], + supports_ota: false, + vendor: 'Aqara', + }, + disabled: false, + endpoints: {1: {bindings: [], clusters: {input: ['genBasic'], output: []}, configured_reportings: [], scenes: []}}, + friendly_name: 'weather_sensor', + ieee_address: '0x0017880104e45522', + interview_completed: true, + interviewing: false, + model_id: 'lumi.weather', + network_address: 6539, + power_source: 'Battery', + software_build_id: null, + supported: true, + type: 'EndDevice', + }, + { + date_code: null, + definition: { + description: 'Mi smart plug', + exposes: [ + { + features: [ + { + access: 7, + description: 'On/off state of the switch', + label: 'State', + name: 'state', + property: 'state', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + ], + type: 'switch', + }, + { + access: 5, + category: 'diagnostic', + description: 'Instantaneous measured power', + label: 'Power', + name: 'power', + property: 'power', + type: 'numeric', + unit: 'W', + }, + { + access: 1, + description: 'Sum of consumed energy', + label: 'Energy', + name: 'energy', + property: 'energy', + type: 'numeric', + unit: 'kWh', + }, + { + access: 1, + category: 'diagnostic', + description: 'Temperature of the device', + label: 'Device temperature', + name: 'device_temperature', + property: 'device_temperature', + type: 'numeric', + unit: '°C', + }, + { + access: 7, + category: 'config', + description: 'Enable/disable the power outage memory, this recovers the on/off mode after power failure', + label: 'Power outage memory', + name: 'power_outage_memory', + property: 'power_outage_memory', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'ZNCZ02LM', + options: [ + { + access: 2, + description: 'Calibrates the power value (percentual offset), takes into effect on next report of device.', + label: 'Power calibration', + name: 'power_calibration', + property: 'power_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Number of digits after decimal point for power, takes into effect on next report of device. This option can only decrease the precision, not increase it.', + label: 'Power precision', + name: 'power_precision', + property: 'power_precision', + type: 'numeric', + value_max: 3, + value_min: 0, + }, + { + access: 2, + description: 'Calibrates the energy value (percentual offset), takes into effect on next report of device.', + label: 'Energy calibration', + name: 'energy_calibration', + property: 'energy_calibration', + type: 'numeric', + }, + { + access: 2, + description: + 'Number of digits after decimal point for energy, takes into effect on next report of device. This option can only decrease the precision, not increase it.', + label: 'Energy precision', + name: 'energy_precision', + property: 'energy_precision', + type: 'numeric', + value_max: 3, + value_min: 0, + }, + { + access: 2, + description: 'Calibrates the device_temperature value (absolute offset), takes into effect on next report of device.', + label: 'Device temperature calibration', + name: 'device_temperature_calibration', + property: 'device_temperature_calibration', + type: 'numeric', + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: true, + vendor: 'Xiaomi', + }, + disabled: false, + endpoints: {1: {bindings: [], clusters: {input: ['genBasic', 'genOnOff'], output: []}, configured_reportings: [], scenes: []}}, + friendly_name: 'power_plug', + ieee_address: '0x0017880104e45524', + interview_completed: true, + interviewing: false, + model_id: 'lumi.plug', + network_address: 6540, + power_source: 'Mains (single phase)', + software_build_id: null, + supported: true, + type: 'Router', + }, + { + date_code: null, + definition: { + description: 'zigfred plus smart in-wall switch', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Triggered action (e.g. a button click)', + label: 'Action', + name: 'action', + property: 'action', + type: 'enum', + values: [ + 'button_1_single', + 'button_1_double', + 'button_1_hold', + 'button_1_release', + 'button_2_single', + 'button_2_double', + 'button_2_hold', + 'button_2_release', + 'button_3_single', + 'button_3_double', + 'button_3_hold', + 'button_3_release', + 'button_4_single', + 'button_4_double', + 'button_4_hold', + 'button_4_release', + ], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + { + endpoint: 'l1', + features: [ + { + access: 7, + description: 'On/off state of this light', + endpoint: 'l1', + label: 'State', + name: 'state', + property: 'state_l1', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + endpoint: 'l1', + label: 'Brightness', + name: 'brightness', + property: 'brightness_l1', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + { + access: 7, + description: 'Color of this light in the CIE 1931 color space (x/y)', + endpoint: 'l1', + features: [ + {access: 7, label: 'X', name: 'x', property: 'x', type: 'numeric'}, + {access: 7, label: 'Y', name: 'y', property: 'y', type: 'numeric'}, + ], + label: 'Color (X/Y)', + name: 'color_xy', + property: 'color_l1', + type: 'composite', + }, + ], + type: 'light', + }, + { + endpoint: 'l2', + features: [ + { + access: 7, + description: 'On/off state of this light', + endpoint: 'l2', + label: 'State', + name: 'state', + property: 'state_l2', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + endpoint: 'l2', + label: 'Brightness', + name: 'brightness', + property: 'brightness_l2', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + ], + type: 'light', + }, + { + endpoint: 'l3', + features: [ + { + access: 7, + description: 'On/off state of this light', + endpoint: 'l3', + label: 'State', + name: 'state', + property: 'state_l3', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + endpoint: 'l3', + label: 'Brightness', + name: 'brightness', + property: 'brightness_l3', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + ], + type: 'light', + }, + { + endpoint: 'l4', + features: [ + { + access: 7, + description: 'On/off state of this light', + endpoint: 'l4', + label: 'State', + name: 'state', + property: 'state_l4', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + endpoint: 'l4', + label: 'Brightness', + name: 'brightness', + property: 'brightness_l4', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + ], + type: 'light', + }, + { + endpoint: 'l5', + features: [ + { + access: 7, + description: 'On/off state of this light', + endpoint: 'l5', + label: 'State', + name: 'state', + property: 'state_l5', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + endpoint: 'l5', + label: 'Brightness', + name: 'brightness', + property: 'brightness_l5', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + ], + type: 'light', + }, + { + endpoint: 'l6', + features: [ + { + access: 7, + endpoint: 'l6', + label: 'State', + name: 'state', + property: 'state_l6', + type: 'enum', + values: ['OPEN', 'CLOSE', 'STOP'], + }, + { + access: 7, + description: 'Position of this cover', + endpoint: 'l6', + label: 'Position', + name: 'position', + property: 'position_l6', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + { + access: 7, + description: 'Tilt of this cover', + endpoint: 'l6', + label: 'Tilt', + name: 'tilt', + property: 'tilt_l6', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + ], + type: 'cover', + }, + { + endpoint: 'l7', + features: [ + { + access: 7, + endpoint: 'l7', + label: 'State', + name: 'state', + property: 'state_l7', + type: 'enum', + values: ['OPEN', 'CLOSE', 'STOP'], + }, + { + access: 7, + description: 'Position of this cover', + endpoint: 'l7', + label: 'Position', + name: 'position', + property: 'position_l7', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + { + access: 7, + description: 'Tilt of this cover', + endpoint: 'l7', + label: 'Tilt', + name: 'tilt', + property: 'tilt_l7', + type: 'numeric', + unit: '%', + value_max: 100, + value_min: 0, + }, + ], + type: 'cover', + }, + ], + model: 'ZFP-1A-CH', + options: [ + { + access: 2, + description: 'Front Surface LED enabled', + label: 'Front surface enabled', + name: 'front_surface_enabled', + property: 'front_surface_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 1 enabled', + label: 'Dimmer 1 enabled', + name: 'dimmer_1_enabled', + property: 'dimmer_1_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 1 dimmable', + label: 'Dimmer 1 dimming enabled', + name: 'dimmer_1_dimming_enabled', + property: 'dimmer_1_dimming_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 2 enabled', + label: 'Dimmer 2 enabled', + name: 'dimmer_2_enabled', + property: 'dimmer_2_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 2 dimmable', + label: 'Dimmer 2 dimming enabled', + name: 'dimmer_2_dimming_enabled', + property: 'dimmer_2_dimming_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 3 enabled', + label: 'Dimmer 3 enabled', + name: 'dimmer_3_enabled', + property: 'dimmer_3_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 3 dimmable', + label: 'Dimmer 3 dimming enabled', + name: 'dimmer_3_dimming_enabled', + property: 'dimmer_3_dimming_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 4 enabled', + label: 'Dimmer 4 enabled', + name: 'dimmer_4_enabled', + property: 'dimmer_4_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Dimmer 4 dimmable', + label: 'Dimmer 4 dimming enabled', + name: 'dimmer_4_dimming_enabled', + property: 'dimmer_4_dimming_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Cover 1 enabled', + label: 'Cover 1 enabled', + name: 'cover_1_enabled', + property: 'cover_1_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Cover 1 tiltable', + label: 'Cover 1 tilt enabled', + name: 'cover_1_tilt_enabled', + property: 'cover_1_tilt_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Cover 2 enabled', + label: 'Cover 2 enabled', + name: 'cover_2_enabled', + property: 'cover_2_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: 'Cover 2 tiltable', + label: 'Cover 2 tilt enabled', + name: 'cover_2_tilt_enabled', + property: 'cover_2_tilt_enabled', + type: 'enum', + values: ['auto', 'true', 'false'], + }, + { + access: 2, + description: + 'When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).', + label: 'Color sync', + name: 'color_sync', + property: 'color_sync', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: + 'Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).', + label: 'Transition', + name: 'transition', + property: 'transition', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: 'Inverts the cover position, false: open=100,close=0, true: open=0,close=100 (default false).', + label: 'Invert cover', + name: 'invert_cover', + property: 'invert_cover', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: false, + vendor: 'Siglis', + }, + disabled: false, + endpoints: { + 10: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl'], output: []}, + configured_reportings: [], + scenes: [], + }, + 11: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'closuresWindowCovering'], output: []}, + configured_reportings: [], + scenes: [], + }, + 12: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'closuresWindowCovering'], output: []}, + configured_reportings: [], + scenes: [], + }, + 5: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], output: []}, + configured_reportings: [], + scenes: [], + }, + 7: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl'], output: []}, + configured_reportings: [], + scenes: [], + }, + 8: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl'], output: []}, + configured_reportings: [], + scenes: [], + }, + 9: { + bindings: [], + clusters: {input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl'], output: []}, + configured_reportings: [], + scenes: [], + }, + }, + friendly_name: 'zigfred_plus', + ieee_address: '0xf4ce368a38be56a1', + interview_completed: true, + interviewing: false, + manufacturer: 'Siglis', + model_id: 'zigfred plus', + network_address: 6589, + power_source: 'Mains (single phase)', + software_build_id: null, + supported: true, + type: 'Router', + }, + { + date_code: null, + definition: { + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + exposes: [ + { + features: [ + { + access: 7, + description: 'On/off state of this light', + label: 'State', + name: 'state', + property: 'state', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + label: 'Brightness', + name: 'brightness', + property: 'brightness', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + { + access: 7, + description: 'Color temperature of this light', + label: 'Color temp', + name: 'color_temp', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + ], + property: 'color_temp', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + { + access: 7, + description: 'Color temperature after cold power on of this light', + label: 'Color temp startup', + name: 'color_temp_startup', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + {description: 'Restore previous color_temp on cold power on', name: 'previous', value: 65535}, + ], + property: 'color_temp_startup', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + ], + type: 'light', + }, + { + access: 2, + description: 'Triggers an effect on the light (e.g. make light blink for a few seconds)', + label: 'Effect', + name: 'effect', + property: 'effect', + type: 'enum', + values: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], + }, + { + access: 7, + category: 'config', + description: + 'Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.', + label: 'Power-on behavior', + name: 'power_on_behavior', + property: 'power_on_behavior', + type: 'enum', + values: ['off', 'on', 'toggle', 'previous'], + }, + { + access: 7, + category: 'config', + description: 'Advanced color behavior', + features: [ + { + access: 2, + description: 'Controls whether color and color temperature can be set while light is off', + label: 'Execute if off', + name: 'execute_if_off', + property: 'execute_if_off', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + label: 'Color options', + name: 'color_options', + property: 'color_options', + type: 'composite', + }, + { + access: 2, + category: 'config', + description: 'Initiate device identification', + label: 'Identify', + name: 'identify', + property: 'identify', + type: 'enum', + values: ['identify'], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'LED1545G12', + options: [ + { + access: 2, + description: + 'Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).', + label: 'Transition', + name: 'transition', + property: 'transition', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: + 'When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).', + label: 'Color sync', + name: 'color_sync', + property: 'color_sync', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: + 'Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).', + label: 'Identify timeout', + name: 'identify_timeout', + property: 'identify_timeout', + type: 'numeric', + value_max: 30, + value_min: 1, + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: true, + vendor: 'IKEA', + }, + disabled: false, + endpoints: { + 1: { + bindings: [], + clusters: { + input: ['genBasic', 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + output: ['genScenes', 'genOta'], + }, + configured_reportings: [], + scenes: [], + }, + }, + friendly_name: '0x000b57fffec6a5c2', + ieee_address: '0x000b57fffec6a5c2', + interview_completed: true, + interviewing: false, + manufacturer: null, + model_id: 'TRADFRI bulb E27 WS opal 980lm', + network_address: 40369, + power_source: 'Mains (single phase)', + software_build_id: null, + supported: true, + type: 'Router', + }, + ]), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -109,8 +2027,8 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - { retain: true, qos: 0 }, - expect.any(Function) + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -118,19 +2036,19 @@ describe('Bridge', () => { logger.setTransportsEnabled(true); MQTT.publish.mockClear(); logger.info.mockClear(); - logger.info("this is a test"); - logger.info("this is a test"); // Should not publish dupes + logger.info('this is a test'); + logger.info('this is a test'); // Should not publish dupes expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledTimes(1); // Should not publish debug logging MQTT.publish.mockClear(); - logger.debug("this is a test"); + logger.debug('this is a test'); expect(MQTT.publish).toHaveBeenCalledTimes(0); }); @@ -141,19 +2059,19 @@ describe('Bridge', () => { logger.setTransportsEnabled(true); MQTT.publish.mockClear(); logger.info.mockClear(); - logger.info("this is a test"); - logger.info("this is a test"); // Should not publish dupes + logger.info('this is a test'); + logger.info('this is a test'); // Should not publish dupes expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledTimes(1); // Should publish debug logging MQTT.publish.mockClear(); - logger.debug("this is a test"); + logger.debug('this is a test'); expect(MQTT.publish).toHaveBeenCalledTimes(1); settings.set(['advanced', 'log_debug_to_mqtt_frontend'], false); @@ -166,7 +2084,7 @@ describe('Bridge', () => { MQTT.publish.mockClear(); logger.info.mockClear(); logger.error.mockClear(); - logger.info("this is a test"); + logger.info('this is a test'); expect(MQTT.publish).toHaveBeenCalledTimes(0); expect(logger.info).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledTimes(0); @@ -177,9 +2095,28 @@ describe('Bridge', () => { logger.setTransportsEnabled(true); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', - stringify([{"friendly_name":"group_1","id":1,"members":[],"scenes":[]},{"friendly_name":"group_tradfri_remote","id":15071,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b4"}],"scenes":[]},{"friendly_name":"99","id":99,"members":[],"scenes":[]},{"friendly_name":"group_with_tradfri","id":11,"members":[],"scenes":[]},{"friendly_name":"thermostat_group","id":12,"members":[],"scenes":[]},{"friendly_name":"switch_group","id":14,"members":[{"endpoint":1,"ieee_address":"0x0017880104e45524"}],"scenes":[]},{"friendly_name":"gledopto_group","id":21,"members":[],"scenes":[]},{"friendly_name":"default_bind_group","id":901,"members":[],"scenes":[]},{"friendly_name":"ha_discovery_group","id":9,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b4"},{"endpoint":2,"ieee_address":"0x0017880104e45542"}],"scenes":[{"id":4,"name":"Scene 4"}]},{"friendly_name":"group_2","id":2,"members":[],"scenes":[]}]), - { retain: true, qos: 0 }, - expect.any(Function) + stringify([ + {friendly_name: 'group_1', id: 1, members: [], scenes: []}, + {friendly_name: 'group_tradfri_remote', id: 15071, members: [{endpoint: 1, ieee_address: '0x000b57fffec6a5b4'}], scenes: []}, + {friendly_name: '99', id: 99, members: [], scenes: []}, + {friendly_name: 'group_with_tradfri', id: 11, members: [], scenes: []}, + {friendly_name: 'thermostat_group', id: 12, members: [], scenes: []}, + {friendly_name: 'switch_group', id: 14, members: [{endpoint: 1, ieee_address: '0x0017880104e45524'}], scenes: []}, + {friendly_name: 'gledopto_group', id: 21, members: [], scenes: []}, + {friendly_name: 'default_bind_group', id: 901, members: [], scenes: []}, + { + friendly_name: 'ha_discovery_group', + id: 9, + members: [ + {endpoint: 1, ieee_address: '0x000b57fffec6a5b4'}, + {endpoint: 2, ieee_address: '0x0017880104e45542'}, + ], + scenes: [{id: 4, name: 'Scene 4'}], + }, + {friendly_name: 'group_2', id: 2, members: [], scenes: []}, + ]), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -189,9 +2126,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"type":"device_joined","data":{"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -199,12 +2136,7 @@ describe('Bridge', () => { MQTT.publish.mockClear(); await zigbeeHerdsman.events.deviceNetworkAddressChanged({device: zigbeeHerdsman.devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event when device announces', async () => { @@ -214,9 +2146,9 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledTimes(2); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"type":"device_announce","data":{"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -227,9 +2159,9 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledTimes(2); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"type":"device_interview","data":{"friendly_name":"bulb","status":"started","ieee_address":"0x000b57fffec6a5b2"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -240,16 +2172,11 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledTimes(2); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"type":"device_interview","data":{"friendly_name":"bulb","status":"failed","ieee_address":"0x000b57fffec6a5b2"}}), - { retain: false, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event and devices when device interview successful', async () => { @@ -261,28 +2188,284 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')) expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"data":{"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454}],"property":"color_temp","type":"numeric","unit":"mired","value_max":454,"value_min":250},{"access":7,"description":"Color temperature after cold power on of this light","label":"Color temp startup","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":454,"value_min":250}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":7,"category":"config","description":"Advanced color behavior","features":[{"access":2,"description":"Controls whether color and color temperature can be set while light is off","label":"Execute if off","name":"execute_if_off","property":"execute_if_off","type":"binary","value_off":false,"value_on":true}],"label":"Color options","name":"color_options","property":"color_options","type":"composite"},{"access":2,"category":"config","description":"Initiate device identification","label":"Identify","name":"identify","property":"identify","type":"enum","values":["identify"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).","label":"Identify timeout","name":"identify_timeout","property":"identify_timeout","type":"numeric","value_max":30,"value_min":1},{"access":2,"description":"State actions will also be published as \'action\' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"IKEA"},"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2","status":"successful","supported":true},"type":"device_interview"}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({ + data: { + definition: { + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + exposes: [ + { + features: [ + { + access: 7, + description: 'On/off state of this light', + label: 'State', + name: 'state', + property: 'state', + type: 'binary', + value_off: 'OFF', + value_on: 'ON', + value_toggle: 'TOGGLE', + }, + { + access: 7, + description: 'Brightness of this light', + label: 'Brightness', + name: 'brightness', + property: 'brightness', + type: 'numeric', + value_max: 254, + value_min: 0, + }, + { + access: 7, + description: 'Color temperature of this light', + label: 'Color temp', + name: 'color_temp', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + ], + property: 'color_temp', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + { + access: 7, + description: 'Color temperature after cold power on of this light', + label: 'Color temp startup', + name: 'color_temp_startup', + presets: [ + {description: 'Coolest temperature supported', name: 'coolest', value: 250}, + {description: 'Cool temperature (250 mireds / 4000 Kelvin)', name: 'cool', value: 250}, + {description: 'Neutral temperature (370 mireds / 2700 Kelvin)', name: 'neutral', value: 370}, + {description: 'Warm temperature (454 mireds / 2200 Kelvin)', name: 'warm', value: 454}, + {description: 'Warmest temperature supported', name: 'warmest', value: 454}, + {description: 'Restore previous color_temp on cold power on', name: 'previous', value: 65535}, + ], + property: 'color_temp_startup', + type: 'numeric', + unit: 'mired', + value_max: 454, + value_min: 250, + }, + ], + type: 'light', + }, + { + access: 2, + description: 'Triggers an effect on the light (e.g. make light blink for a few seconds)', + label: 'Effect', + name: 'effect', + property: 'effect', + type: 'enum', + values: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], + }, + { + access: 7, + category: 'config', + description: + 'Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.', + label: 'Power-on behavior', + name: 'power_on_behavior', + property: 'power_on_behavior', + type: 'enum', + values: ['off', 'on', 'toggle', 'previous'], + }, + { + access: 7, + category: 'config', + description: 'Advanced color behavior', + features: [ + { + access: 2, + description: 'Controls whether color and color temperature can be set while light is off', + label: 'Execute if off', + name: 'execute_if_off', + property: 'execute_if_off', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + label: 'Color options', + name: 'color_options', + property: 'color_options', + type: 'composite', + }, + { + access: 2, + category: 'config', + description: 'Initiate device identification', + label: 'Identify', + name: 'identify', + property: 'identify', + type: 'enum', + values: ['identify'], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'LED1545G12', + options: [ + { + access: 2, + description: + 'Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).', + label: 'Transition', + name: 'transition', + property: 'transition', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: + 'When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).', + label: 'Color sync', + name: 'color_sync', + property: 'color_sync', + type: 'binary', + value_off: false, + value_on: true, + }, + { + access: 2, + description: + 'Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).', + label: 'Identify timeout', + name: 'identify_timeout', + property: 'identify_timeout', + type: 'numeric', + value_max: 30, + value_min: 1, + }, + { + access: 2, + description: "State actions will also be published as 'action' when true (default false).", + label: 'State action', + name: 'state_action', + property: 'state_action', + type: 'binary', + value_off: false, + value_on: true, + }, + ], + supports_ota: true, + vendor: 'IKEA', + }, + friendly_name: 'bulb', + ieee_address: '0x000b57fffec6a5b2', + status: 'successful', + supported: true, + }, + type: 'device_interview', + }), + {retain: false, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/event', - stringify({"data":{"definition":{"description":"Automatically generated definition","exposes":[{"access":1,"category":"diagnostic","description":"Triggered action (e.g. a button click)","label":"Action","name":"action","property":"action","type":"enum","values":["on","off","toggle","brightness_move_to_level","brightness_move_up","brightness_move_down","brightness_step_up","brightness_step_down","brightness_stop"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"notSupportedModelID","options":[{"access":2,"description":"Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ","features":[{"access":2,"description":"Delta per interval, 20 by default","label":"Delta","name":"delta","property":"delta","type":"numeric","value_min":0},{"access":2,"description":"Interval duration","label":"Interval","name":"interval","property":"interval","type":"numeric","unit":"ms","value_min":0}],"label":"Simulated brightness","name":"simulated_brightness","property":"simulated_brightness","type":"composite"}],"supports_ota":false,"vendor":"notSupportedMfg"},"friendly_name":"0x0017880104e45518","ieee_address":"0x0017880104e45518","status":"successful","supported":false},"type":"device_interview"}), - { retain: false, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/definitions', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) + 'zigbee2mqtt/bridge/event', + stringify({ + data: { + definition: { + description: 'Automatically generated definition', + exposes: [ + { + access: 1, + category: 'diagnostic', + description: 'Triggered action (e.g. a button click)', + label: 'Action', + name: 'action', + property: 'action', + type: 'enum', + values: [ + 'on', + 'off', + 'toggle', + 'brightness_move_to_level', + 'brightness_move_up', + 'brightness_move_down', + 'brightness_step_up', + 'brightness_step_down', + 'brightness_stop', + ], + }, + { + access: 1, + category: 'diagnostic', + description: 'Link quality (signal strength)', + label: 'Linkquality', + name: 'linkquality', + property: 'linkquality', + type: 'numeric', + unit: 'lqi', + value_max: 255, + value_min: 0, + }, + ], + model: 'notSupportedModelID', + options: [ + { + access: 2, + description: + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ', + features: [ + { + access: 2, + description: 'Delta per interval, 20 by default', + label: 'Delta', + name: 'delta', + property: 'delta', + type: 'numeric', + value_min: 0, + }, + { + access: 2, + description: 'Interval duration', + label: 'Interval', + name: 'interval', + property: 'interval', + type: 'numeric', + unit: 'ms', + value_min: 0, + }, + ], + label: 'Simulated brightness', + name: 'simulated_brightness', + property: 'simulated_brightness', + type: 'composite', + }, + ], + supports_ota: false, + vendor: 'notSupportedMfg', + }, + friendly_name: '0x0017880104e45518', + ieee_address: '0x0017880104e45518', + status: 'successful', + supported: false, + }, + type: 'device_interview', + }), + {retain: false, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event and devices when device leaves', async () => { @@ -292,21 +2475,17 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"type":"device_leave","data":{"ieee_address":"0x000b57fffec6a5b2","friendly_name":"bulb"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_leave', data: {ieee_address: '0x000b57fffec6a5b2', friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( // Defintitions should be updated on device event + // Defintitions should be updated on device event 'zigbee2mqtt/bridge/definitions', expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -319,58 +2498,63 @@ describe('Bridge', () => { expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value": false})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value": "False"})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: 'False'})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // Invalid payload zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value_bla": false})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value_bla: false})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow permit join for certain time', async () => { zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value": false, "time": 10})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false, time: 10})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, 10); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":false,"time": 10},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false, time: 10}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -378,7 +2562,7 @@ describe('Bridge', () => { MQTT.publish.mockClear(); await zigbeeHerdsman.events.permitJoinChanged({permitted: false, time: 10}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Shouldnt republish bridge info when permit join changes and hersman is stopping', async () => { @@ -386,7 +2570,7 @@ describe('Bridge', () => { zigbeeHerdsman.isStopping.mockImplementationOnce(() => true); await zigbeeHerdsman.events.permitJoinChanged({permitted: false, time: 10}); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should allow permit join via device', async () => { @@ -399,8 +2583,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, device, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":true,"device":"bulb"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: true, device: 'bulb'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // Device does not exist @@ -411,50 +2596,63 @@ describe('Bridge', () => { expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing_woeeee' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing_woeeee' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should put transaction in response when request is done with transaction', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value": false, "transaction": 22})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false, transaction: 22})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{"value":false},"status":"ok", "transaction": 22}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false}, status: 'ok', transaction: 22}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should put error in response when request fails', async () => { - zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {throw new Error('Failed to connect to adapter')}); + zigbeeHerdsman.permitJoin.mockImplementationOnce(() => { + throw new Error('Failed to connect to adapter'); + }); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({"value": false})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({"data":{},"status":"error","error": "Failed to connect to adapter"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Failed to connect to adapter'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should put error in response when format is incorrect', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', stringify({"value_not_good": false})); + MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', stringify({value_not_good: false})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({"data":{},"status":"error","error": "No value given"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'No value given'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Coverage satisfaction', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/random', stringify({"value": false})); + MQTT.events.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); const device = zigbeeHerdsman.devices.bulb; - await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {onOff: 1}, + cluster: 'genOnOff', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); }); @@ -464,26 +2662,43 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/health_check', - stringify({"data":{"healthy": true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {healthy: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow a coordinator check', async () => { MQTT.publish.mockClear(); - zigbeeHerdsman.coordinatorCheck.mockReturnValueOnce({missingRouters: [zigbeeHerdsman.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}) + zigbeeHerdsman.coordinatorCheck.mockReturnValueOnce({missingRouters: [zigbeeHerdsman.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); MQTT.events.message('zigbee2mqtt/bridge/request/coordinator_check', ''); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/coordinator_check', - stringify({"data":{"missing_routers":[{"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2"}]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {missing_routers: [{friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}]}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to remove device by string', async () => { const device = zigbeeHerdsman.devices.bulb; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['0x999b57fffec6a5b9/1', '0x000b57fffec6a5b2/1', 'bulb', 'bulb/right', 'other_bulb', 'bulb_1', '0x000b57fffec6a5b2', 'bulb/room/2']}}); + settings.set(['groups'], { + 1: { + friendly_name: 'group_1', + retain: false, + devices: [ + '0x999b57fffec6a5b9/1', + '0x000b57fffec6a5b2/1', + 'bulb', + 'bulb/right', + 'other_bulb', + 'bulb_1', + '0x000b57fffec6a5b2', + 'bulb/room/2', + ], + }, + }); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); await flushPromises(); @@ -495,8 +2710,9 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{"id": "bulb","block":false,"force":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); expect(settings.get().blocklist).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual(['0x999b57fffec6a5b9/1', 'other_bulb', 'bulb_1', 'bulb/room/2']); @@ -507,7 +2723,7 @@ describe('Bridge', () => { it('Should allow to remove device by object ID', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb"})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(device.removeFromDatabase).not.toHaveBeenCalled(); @@ -515,15 +2731,16 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{"id": "bulb","block":false,"force":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to force remove device', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb", force: true})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(device.removeFromNetwork).not.toHaveBeenCalled(); @@ -531,25 +2748,27 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{"id": "bulb","block":false,"force":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', block: false, force: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to block device', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb", block: true, force: true})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', block: true, force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getDevice('bulb')).toBeNull(); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{"id": "bulb","block":true,"force":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', block: true, force: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]); + expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b2']); }); it('Should allow to remove group', async () => { @@ -562,69 +2781,75 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', - stringify({"data":{"id": "group_1", "force": false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'group_1', force: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to force remove group', async () => { const group = zigbeeHerdsman.groups.group_1; MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: "group_1", force: true})); + MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'group_1', force: true})); await flushPromises(); expect(group.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getGroup('group_1')).toBeNull(); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', - stringify({"data":{"id": "group_1", "force": true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'group_1', force: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to add and remove from blocklist', async () => { expect(settings.get().blocklist).toStrictEqual([]); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"blocklist":["0x123","0x1234"]}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123', '0x1234']}})); await flushPromises(); - expect(settings.get().blocklist).toStrictEqual(["0x123","0x1234"]); + expect(settings.get().blocklist).toStrictEqual(['0x123', '0x1234']); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"blocklist":["0x123"]}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123']}})); await flushPromises(); - expect(settings.get().blocklist).toStrictEqual(["0x123"]); + expect(settings.get().blocklist).toStrictEqual(['0x123']); }); it('Should allow to add and remove from availabliltiy blocklist', async () => { expect(settings.get().blocklist).toStrictEqual([]); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"advanced":{"availability_blocklist": ["0x123","0x1234"]}}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123', '0x1234']}}})); await flushPromises(); - expect(settings.get().advanced.availability_blocklist).toStrictEqual(["0x123","0x1234"]); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"advanced":{"availability_blocklist": ["0x123"]}}})); + expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123', '0x1234']); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123']}}})); await flushPromises(); - expect(settings.get().advanced.availability_blocklist).toStrictEqual(["0x123"]); + expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123']); }); it('Should throw error on removing non-existing device', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "non-existing-device"})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{},"status":"error","error":"Device 'non-existing-device' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'non-existing-device' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should throw error when remove device fails', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); - device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('device timeout')}) - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb"})); + device.removeFromNetwork.mockImplementationOnce(() => { + throw new Error('device timeout'); + }); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', - stringify({"data":{},"status":"error","error":"Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -633,14 +2858,20 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeNull(); - expect(settings.getDevice('bulb_new_name')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb_new_name", "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb_new_name')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb_new_name', + retain: true, + description: 'this is my bulb', + }); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name', stringify({"brightness":50}), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name', stringify({brightness: 50}), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{"from":"bulb","to":"bulb_new_name","homeassistant_rename":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -650,8 +2881,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{},"status":"error","error":"MQTT wildcard (+ and #) not allowed in friendly_name ('living_room/blinds#')"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "MQTT wildcard (+ and #) not allowed in friendly_name ('living_room/blinds#')"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -660,12 +2892,13 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); await flushPromises(); expect(settings.getGroup('group_1')).toBeNull(); - expect(settings.getGroup('group_new_name')).toStrictEqual({"ID": 1, "devices": [], "friendly_name": "group_new_name", "retain": false}); + expect(settings.getGroup('group_new_name')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_new_name', retain: false}); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/rename', - stringify({"data":{"from":"group_1","to":"group_new_name","homeassistant_rename":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'group_1', to: 'group_new_name', homeassistant_rename: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -675,8 +2908,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -686,8 +2920,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -697,12 +2932,18 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeNull(); - expect(settings.getDevice('bulb_new_name')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb_new_name", "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb_new_name')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb_new_name', + retain: true, + description: 'this is my bulb', + }); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{"from":"bulb","to":"bulb_new_name","homeassistant_rename":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -712,8 +2953,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{},"status":"error","error":`Friendly name cannot end with a "/DIGIT" ('bulb_new_name/1')`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: `Friendly name cannot end with a "/DIGIT" ('bulb_new_name/1')`}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -723,8 +2965,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', - stringify({"data":{},"status":"error","error":"No device has joined since start"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'No device has joined since start'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -736,36 +2979,30 @@ describe('Bridge', () => { expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{"id":"bulb"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // The following indicates that devices have published. - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - {retain: true, qos: 0}, expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should allow interviewing a device by ieeeAddr', async () => { MQTT.publish.mockClear(); zigbeeHerdsman.devices.bulb.interview.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: "0x000b57fffec6a5b2"})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'})); await flushPromises(); expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{"id":"0x000b57fffec6a5b2"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: '0x000b57fffec6a5b2'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // The following indicates that devices have published. - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - {retain: true, qos: 0}, expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should throw error on invalid device interview payload', async () => { @@ -774,8 +3011,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -785,8 +3023,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -796,8 +3035,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{},"status":"error","error":"Device 'bulb/1' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'bulb/1' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -807,21 +3047,23 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{},"status":"error","error":"Device 'group_1' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'group_1' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should throw error on when interview fails', async () => { MQTT.publish.mockClear(); zigbeeHerdsman.devices.bulb.interview.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))) + zigbeeHerdsman.devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))); MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', - stringify({"data":{},"status":"error","error":"interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -831,8 +3073,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', - stringify({ data: {}, error: 'Invalid payload', status: 'error' }), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, error: 'Invalid payload', status: 'error'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -842,8 +3085,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', - stringify({ data: {}, error: "Device 'non_existing_device' does not exist", status: 'error' }), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, error: "Device 'non_existing_device' does not exist", status: 'error'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -855,89 +3099,162 @@ describe('Bridge', () => { 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({ data: { - id: '0x0017880104e45524', - source: "const {onOff} = require('zigbee-herdsman-converters/lib/modernExtend');\n" + - '\n' + - 'const definition = {\n' + - " zigbeeModel: ['lumi.plug'],\n" + - " model: 'lumi.plug',\n" + - " vendor: '',\n" + - " description: 'Automatically generated definition',\n" + - ' extend: [onOff({"powerOnBehavior":false})],\n' + - ' meta: {},\n' + - '};\n' + - '\n' + - 'module.exports = definition;' + id: '0x0017880104e45524', + source: + "const {onOff} = require('zigbee-herdsman-converters/lib/modernExtend');\n" + + '\n' + + 'const definition = {\n' + + " zigbeeModel: ['lumi.plug'],\n" + + " model: 'lumi.plug',\n" + + " vendor: '',\n" + + " description: 'Automatically generated definition',\n" + + ' extend: [onOff({"powerOnBehavior":false})],\n' + + ' meta: {},\n' + + '};\n' + + '\n' + + 'module.exports = definition;', }, - status: 'ok' + status: 'ok', }), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow change device options', async () => { MQTT.publish.mockClear(); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + retain: true, + description: 'this is my bulb', + }); MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {retain: false, transition: 1}, id: 'bulb'})); await flushPromises(); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "retain": false, "transition": 1, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + retain: false, + transition: 1, + description: 'this is my bulb', + }); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', - stringify({"data":{"from":{"retain": true, "description": "this is my bulb"},"to":{"retain": false,"transition":1, "description": "this is my bulb"}, "restart_required": false, "id":"bulb"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: { + from: {retain: true, description: 'this is my bulb'}, + to: {retain: false, transition: 1, description: 'this is my bulb'}, + restart_required: false, + id: 'bulb', + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to remove device option', async () => { MQTT.publish.mockClear(); settings.set(['devices', '0x000b57fffec6a5b2', 'qos'], 1); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "qos": 1, "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + qos: 1, + retain: true, + description: 'this is my bulb', + }); MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {qos: null}, id: 'bulb'})); await flushPromises(); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + retain: true, + description: 'this is my bulb', + }); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', - stringify({"data":{"from":{"retain": true, "qos": 1, "description": "this is my bulb"},"to":{"retain": true, "description": "this is my bulb"}, "restart_required": false, "id":"bulb"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: { + from: {retain: true, qos: 1, description: 'this is my bulb'}, + to: {retain: true, description: 'this is my bulb'}, + restart_required: false, + id: 'bulb', + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow change device options with restart required', async () => { MQTT.publish.mockClear(); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "retain": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + retain: true, + description: 'this is my bulb', + }); MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {disabled: true}, id: 'bulb'})); await flushPromises(); - expect(settings.getDevice('bulb')).toStrictEqual({"ID": "0x000b57fffec6a5b2", "friendly_name": "bulb", "retain": true, "disabled": true, "description": "this is my bulb"}); + expect(settings.getDevice('bulb')).toStrictEqual({ + ID: '0x000b57fffec6a5b2', + friendly_name: 'bulb', + retain: true, + disabled: true, + description: 'this is my bulb', + }); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', - stringify({"data":{"from":{"retain": true, "description": "this is my bulb"},"to":{"disabled": true, "retain": true, "description": "this is my bulb"}, "restart_required": true, "id":"bulb"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: { + from: {retain: true, description: 'this is my bulb'}, + to: {disabled: true, retain: true, description: 'this is my bulb'}, + restart_required: true, + id: 'bulb', + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow change group options', async () => { MQTT.publish.mockClear(); - expect(settings.getGroup('group_1')).toStrictEqual({"ID": 1, "devices": [], "friendly_name": "group_1", "retain": false}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); MQTT.events.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {retain: true, transition: 1}, id: 'group_1'})); await flushPromises(); - expect(settings.getGroup('group_1')).toStrictEqual({"ID": 1, "devices": [], "friendly_name": "group_1", "retain": true, "transition": 1}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: true, transition: 1}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/options', - stringify({"data":{"from":{"retain": false},"to":{"retain": true,"transition":1}, "restart_required": false, "id":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {from: {retain: false}, to: {retain: true, transition: 1}, restart_required: false, id: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow change group options with restart required', async () => { MQTT.publish.mockClear(); - expect(settings.getGroup('group_1')).toStrictEqual({"ID": 1, "devices": [], "friendly_name": "group_1", "retain": false}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); MQTT.events.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {off_state: 'all_members_off'}, id: 'group_1'})); await flushPromises(); - expect(settings.getGroup('group_1')).toStrictEqual({"ID": 1, "devices": [], "friendly_name": "group_1", "retain": false, "off_state": "all_members_off"}); + expect(settings.getGroup('group_1')).toStrictEqual({ + ID: 1, + devices: [], + friendly_name: 'group_1', + retain: false, + off_state: 'all_members_off', + }); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/options', - stringify({"data":{"from":{"retain": false},"to":{"retain": false,"off_state":"all_members_off"}, "restart_required": true, "id":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {from: {retain: false}, to: {retain: false, off_state: 'all_members_off'}, restart_required: true, id: 'group_1'}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -947,8 +3264,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -956,47 +3274,51 @@ describe('Bridge', () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); await flushPromises(); - expect(settings.getGroup('group_193')).toStrictEqual({"ID": 3, "devices": [], "friendly_name": "group_193"}); + expect(settings.getGroup('group_193')).toStrictEqual({ID: 3, devices: [], friendly_name: 'group_193'}); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', - stringify({"data":{"friendly_name":"group_193","id": 3},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {friendly_name: 'group_193', id: 3}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to add group with ID', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: "group_193", id: 92})); + MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: 'group_193', id: 92})); await flushPromises(); - expect(settings.getGroup('group_193')).toStrictEqual({"ID": 92, "devices": [], "friendly_name": "group_193"}); + expect(settings.getGroup('group_193')).toStrictEqual({ID: 92, devices: [], friendly_name: 'group_193'}); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', - stringify({"data":{"friendly_name":"group_193","id": 92},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {friendly_name: 'group_193', id: 92}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Shouldnt allow to add group with empty name', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: "", id: 9})); + MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: '', id: 9})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', - stringify({"data":{},"status":"error","error":"friendly_name must be at least 1 char long"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'friendly_name must be at least 1 char long'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should throw error when add with invalid payload', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: "group_193"})); + MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: 'group_193'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1014,8 +3336,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({"data":{"value":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); expect(settings.get().homeassistant).toBeFalsy(); @@ -1024,8 +3347,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({"data":{"value":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); expect(settings.get().homeassistant).toBeTruthy(); MQTT.publish.mockClear(); @@ -1038,8 +3362,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({"data":{"value":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); expect(settings.get().homeassistant).toBeFalsy(); MQTT.publish.mockClear(); @@ -1055,8 +3380,9 @@ describe('Bridge', () => { expect(settings.get().homeassistant).toBeFalsy(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({"data":{},"status":"error","error":"'invalid_one' is not an allowed value, allowed: true,false"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: true,false"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1067,8 +3393,9 @@ describe('Bridge', () => { expect(settings.get().advanced.last_seen).toBe('ISO_8601'); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({"data":{"value":"ISO_8601"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: 'ISO_8601'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1079,8 +3406,9 @@ describe('Bridge', () => { expect(settings.get().advanced.last_seen).toBe('disable'); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({"data":{},"status":"error","error":"'invalid_one' is not an allowed value, allowed: disable,ISO_8601,epoch,ISO_8601_local"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: disable,ISO_8601,epoch,ISO_8601_local"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1091,8 +3419,9 @@ describe('Bridge', () => { expect(settings.get().advanced.elapsed).toBe(true); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/elapsed', - stringify({"data":{"value":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1103,8 +3432,9 @@ describe('Bridge', () => { expect(settings.get().advanced.elapsed).toBe(false); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/elapsed', - stringify({"data":{},"status":"error","error":"'not_valid' is not an allowed value, allowed: true,false"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "'not_valid' is not an allowed value, allowed: true,false"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1116,8 +3446,9 @@ describe('Bridge', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/log_level', - stringify({"data":{"value":'debug'},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: 'debug'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1127,8 +3458,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/config/log_level', - stringify({"data":{},"status":"error","error":`'not_valid' is not an allowed value, allowed: ${settings.LOG_LEVELS.join(',')}`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: `'not_valid' is not an allowed value, allowed: ${settings.LOG_LEVELS.join(',')}`}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1141,8 +3473,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', - stringify({"data":{},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1156,8 +3489,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledWith('0x1239', 12); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', - stringify({"data":{"ieee_address":'0x1239',"channel":12},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1172,8 +3506,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-code'); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', - stringify({"data":{"value":'my-code'},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: 'my-code'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); // By string @@ -1184,8 +3519,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-string-code'); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', - stringify({"data":{"value":'my-code'},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {value: 'my-code'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1197,8 +3533,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1211,8 +3548,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledWith('0x1239', 12); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', - stringify({"data":{"ieee_address":'0x1239',"channel":12},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1224,8 +3562,9 @@ describe('Bridge', () => { expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1238,22 +3577,35 @@ describe('Bridge', () => { expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', - stringify({"data":{},"status":"error","error":"Failed to factory reset device through Touchlink"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Failed to factory reset device through Touchlink'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to touchlink scan', async () => { MQTT.publish.mockClear(); zigbeeHerdsman.touchlinkScan.mockClear(); - zigbeeHerdsman.touchlinkScan.mockReturnValueOnce([{ieeeAddr: '0x123', channel: 12}, {ieeeAddr: '0x124', channel: 24}]); + zigbeeHerdsman.touchlinkScan.mockReturnValueOnce([ + {ieeeAddr: '0x123', channel: 12}, + {ieeeAddr: '0x124', channel: 24}, + ]); MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/scan', ''); await flushPromises(); expect(zigbeeHerdsman.touchlinkScan).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/scan', - stringify({"data":{"found":[{ieee_address: '0x123', channel: 12}, {ieee_address: '0x124', channel: 24}]},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: { + found: [ + {ieee_address: '0x123', channel: 12}, + {ieee_address: '0x124', channel: 24}, + ], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1263,23 +3615,43 @@ describe('Bridge', () => { endpoint.configureReporting.mockClear(); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure_reporting', stringify({id: '0x000b57fffec6a5b2/1', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/configure_reporting', + stringify({ + id: '0x000b57fffec6a5b2/1', + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }), + ); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinator.endpoints[0]); expect(endpoint.configureReporting).toHaveBeenCalledTimes(1); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 10, "minimumReportInterval": 1, "reportableChange": 1}], undefined); + expect(endpoint.configureReporting).toHaveBeenCalledWith( + 'genLevelCtrl', + [{attribute: 'currentLevel', maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}], + undefined, + ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', - stringify({"data":{id: '0x000b57fffec6a5b2/1', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + data: { + id: '0x000b57fffec6a5b2/1', + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should throw error when configure reporting is called with malformed payload', async () => { @@ -1288,13 +3660,24 @@ describe('Bridge', () => { endpoint.configureReporting.mockClear(); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure_reporting', stringify({id: 'bulb', cluster: 'genLevelCtrl', attribute_lala: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/configure_reporting', + stringify({ + id: 'bulb', + cluster: 'genLevelCtrl', + attribute_lala: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }), + ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', - stringify({"data":{},"status":"error","error":"Invalid payload"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1304,13 +3687,24 @@ describe('Bridge', () => { endpoint.configureReporting.mockClear(); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure_reporting', stringify({id: 'non_existing_device', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/configure_reporting', + stringify({ + id: 'non_existing_device', + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }), + ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', - stringify({"data":{},"status":"error","error":"Device 'non_existing_device' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device 'non_existing_device' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1320,38 +3714,50 @@ describe('Bridge', () => { endpoint.configureReporting.mockClear(); zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure_reporting', stringify({id: '0x000b57fffec6a5b2/non_existing_endpoint', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/configure_reporting', + stringify({ + id: '0x000b57fffec6a5b2/non_existing_endpoint', + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }), + ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', - stringify({"data":{},"status":"error","error":"Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should allow to create a backup', async () => { fs.mkdirSync(path.join(data.mockDir, 'ext_converters')); - fs.writeFileSync(path.join(data.mockDir, 'ext_converters', 'afile.js'), 'test123') + fs.writeFileSync(path.join(data.mockDir, 'ext_converters', 'afile.js'), 'test123'); fs.mkdirSync(path.join(data.mockDir, 'log')); - fs.writeFileSync(path.join(data.mockDir, 'log', 'log.log'), 'test123') + fs.writeFileSync(path.join(data.mockDir, 'log', 'log.log'), 'test123'); fs.mkdirSync(path.join(data.mockDir, 'ext_converters', '123')); - fs.writeFileSync(path.join(data.mockDir, 'ext_converters', '123', 'myfile.js'), 'test123') + fs.writeFileSync(path.join(data.mockDir, 'ext_converters', '123', 'myfile.js'), 'test123'); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/backup', ''); - await flushPromises(); + await flushPromises(); expect(zigbeeHerdsman.backup).toHaveBeenCalledTimes(1); expect(mockJSZipFile).toHaveBeenCalledTimes(4); expect(mockJSZipFile).toHaveBeenNthCalledWith(1, 'configuration.yaml', expect.any(Object)); - expect(mockJSZipFile).toHaveBeenNthCalledWith(2, path.join('ext_converters', '123','myfile.js'), expect.any(Object)); + expect(mockJSZipFile).toHaveBeenNthCalledWith(2, path.join('ext_converters', '123', 'myfile.js'), expect.any(Object)); expect(mockJSZipFile).toHaveBeenNthCalledWith(3, path.join('ext_converters', 'afile.js'), expect.any(Object)); expect(mockJSZipFile).toHaveBeenNthCalledWith(4, 'state.json', expect.any(Object)); expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1); expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: 'base64'}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/backup', - stringify({"data":{"zip":"THISISBASE64"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {zip: 'THISISBASE64'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1364,8 +3770,9 @@ describe('Bridge', () => { expect(mockRestart).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/restart', - stringify({"data":{},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1378,11 +3785,12 @@ describe('Bridge', () => { expect(settings.get().permit_join).toBe(true); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1392,11 +3800,12 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: true}})); await flushPromises(); expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).not.toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1406,11 +3815,12 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 'debug'}}})); await flushPromises(); expect(logger.getLevel()).toStrictEqual('debug'); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1420,11 +3830,12 @@ describe('Bridge', () => { MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}})); await flushPromises(); expect(logger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1432,18 +3843,22 @@ describe('Bridge', () => { logger.setLevel('info'); settings.apply({advanced: {log_namespaced_levels: {'zh:zstack': 'warning', 'z2m:mqtt': 'debug'}}}); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": 'warning', "zh:zstack": null }}}})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/options', + stringify({options: {advanced: {log_namespaced_levels: {'z2m:mqtt': 'warning', 'zh:zstack': null}}}}), + ); await flushPromises(); - expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({"z2m:mqtt": "warning"}); - expect(logger.getNamespacedLevels()).toStrictEqual({ "z2m:mqtt": 'warning' }); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({'z2m:mqtt': 'warning'}); + expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": null }}}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: {'z2m:mqtt': null}}}})); await flushPromises(); expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({}); expect(logger.getNamespacedLevels()).toStrictEqual({}); @@ -1458,36 +3873,42 @@ describe('Bridge', () => { expect(settings.get().serial.port).toBe('/dev/newport'); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Change options array', async () => { zigbeeHerdsman.permitJoin.mockClear(); - expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]) + expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {ext_pan_id: [220, 221, 221, 221, 221, 221, 221, 221]}}})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/options', + stringify({options: {advanced: {ext_pan_id: [220, 221, 221, 221, 221, 221, 221, 221]}}}), + ); await flushPromises(); expect(settings.get().advanced.ext_pan_id).toStrictEqual([220, 221, 221, 221, 221, 221, 221, 221]); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Change options with null', async () => { zigbeeHerdsman.permitJoin.mockClear(); - expect(settings.get().serial).toStrictEqual({"disable_led": false, "port": "/dev/dummy"}) + expect(settings.get().serial).toStrictEqual({disable_led: false, port: '/dev/dummy'}); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"serial":{"disable_led":false,"port":null}}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {disable_led: false, port: null}}})); await flushPromises(); - expect(settings.get().serial).toStrictEqual({"disable_led": false}); + expect(settings.get().serial).toStrictEqual({disable_led: false}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{"restart_required":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {restart_required: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1498,8 +3919,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{},"error": "Invalid payload", "status":"error"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, error: 'Invalid payload', status: 'error'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1510,8 +3932,9 @@ describe('Bridge', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({"data":{},"error": "permit_join must be boolean", "status":"error"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, error: 'permit_join must be boolean', status: 'error'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1519,46 +3942,46 @@ describe('Bridge', () => { const bridge = controller.extensions.find((e) => e.constructor.name === 'Bridge'); expect(bridge).not.toBeUndefined(); - const definition = {'model': 'lumi.plug', fromZigbee: []}; + const definition = {model: 'lumi.plug', fromZigbee: []}; const device = zigbeeHerdsman.devices.ZNCZ02LM; const svg_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo'; - const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg'; + const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg'; definition.icon = icon_link; let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(icon_link); definition.icon = icon_link; payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {icon: svg_icon}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); definition.icon = '_${model}_'; payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_'); definition.icon = '_${model}_${zigbeeModel}_'; payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_lumi.plug_'); definition.icon = svg_icon; payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); device.modelID = '?._Z\\NC+Z02*LM'; definition.model = '&&&&*+'; definition.icon = '_${model}_${zigbeeModel}_'; payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined() - expect(payload['icon']).not.toBeUndefined() + expect(payload).not.toBeUndefined(); + expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_'); }); @@ -1569,29 +3992,19 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls); expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/info', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - { retain: true, qos: 0 }, - expect.any(Function) + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', - stringify({"data":{"friendly_name":"0x000b57fffec6a5c2","ieee_address":"0x000b57fffec6a5c2"},"type":"device_joined"}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({data: {friendly_name: '0x000b57fffec6a5c2', ieee_address: '0x000b57fffec6a5c2'}, type: 'device_joined'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1607,29 +4020,19 @@ describe('Bridge', () => { // console.log(MQTT.publish.mock.calls); expect(MQTT.publish).toHaveBeenCalledTimes(4); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/info', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - { retain: true, qos: 0 }, - expect.any(Function) + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', expect.any(String), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); }); diff --git a/test/configure.test.js b/test/configure.test.js index 55bd1a67..da7a0e25 100644 --- a/test/configure.test.js +++ b/test/configure.test.js @@ -23,29 +23,29 @@ describe('Configure', () => { const endpoint2 = device.getEndpoint(2); expect(endpoint2.write).toHaveBeenCalledTimes(1); - expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {"49": {"type": 25, "value": 11}}, {"disableDefaultResponse": true, "manufacturerCode": 4107}); + expect(endpoint2.write).toHaveBeenCalledWith('genBasic', {49: {type: 25, value: 11}}, {disableDefaultResponse: true, manufacturerCode: 4107}); expect(device.meta.configured).toBe(332242049); - } + }; const expectBulbConfigured = () => { const device = zigbeeHerdsman.devices.bulb; const endpoint1 = device.getEndpoint(1); expect(endpoint1.read).toHaveBeenCalledTimes(2); expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); - expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorTempPhysicalMin', 'colorTempPhysicalMax' ]); - } + expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorTempPhysicalMin', 'colorTempPhysicalMax']); + }; const expectBulbNotConfigured = () => { const device = zigbeeHerdsman.devices.bulb; const endpoint1 = device.getEndpoint(1); expect(endpoint1.read).toHaveBeenCalledTimes(0); - } + }; const expectRemoteNotConfigured = () => { const device = zigbeeHerdsman.devices.remote; const endpoint1 = device.getEndpoint(1); expect(endpoint1.bind).toHaveBeenCalledTimes(0); - } + }; const mockClear = (device) => { for (const endpoint of device.endpoints) { @@ -54,12 +54,12 @@ describe('Configure', () => { endpoint.configureReporting.mockClear(); endpoint.bind.mockClear(); } - } + }; let resetExtension = async () => { await controller.enableDisableExtension(false, 'Configure'); await controller.enableDisableExtension(true, 'Configure'); - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -80,7 +80,7 @@ describe('Configure', () => { afterAll(async () => { jest.useRealTimers(); - }) + }); it('Should configure Router on startup', async () => { expectBulbConfigured(); @@ -149,39 +149,45 @@ describe('Configure', () => { expectRemoteConfigured(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({"data":{"id": "remote"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'remote'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Fail to configure via MQTT when device does not exist', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "not_existing_device"})); + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({"data":{"id": "not_existing_device"},"status":"error","error": "Device 'not_existing_device' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'not_existing_device'}, status: 'error', error: "Device 'not_existing_device' does not exist"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Fail to configure via MQTT when configure fails', async () => { - zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => {throw new Error('Bind timeout after 10s')}); - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "remote"})); + zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => { + throw new Error('Bind timeout after 10s'); + }); + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({"data":{"id": "remote"},"status":"error","error": "Failed to configure (Bind timeout after 10s)"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'remote'}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Fail to configure via MQTT when device has no configure', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "0x0017882104a44559", transaction: 20})); + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({"data":{"id": "0x0017882104a44559"},"status":"error","error": "Device 'TS0601_thermostat' cannot be configured","transaction":20}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: '0x0017882104a44559'}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -202,7 +208,7 @@ describe('Configure', () => { it('Legacy api: Should skip reconfigure when device does not require this', async () => { await MQTT.events.message('zigbee2mqtt/bridge/configure', '0x0017882104a44559'); await flushPromises(); - expect(logger.warning).toHaveBeenCalledWith(`Skipping configure of 'TS0601_thermostat', device does not require this.`) + expect(logger.warning).toHaveBeenCalledWith(`Skipping configure of 'TS0601_thermostat', device does not require this.`); }); it('Should not configure when interview not completed', async () => { @@ -237,19 +243,29 @@ describe('Configure', () => { delete device.meta.configured; const endpoint = device.getEndpoint(1); mockClear(device); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('BLA'); + }); await zigbeeHerdsman.events.lastSeenChanged({device}); await flushPromises(); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('BLA'); + }); await zigbeeHerdsman.events.lastSeenChanged({device}); await flushPromises(); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('BLA'); + }); await zigbeeHerdsman.events.lastSeenChanged({device}); await flushPromises(); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('BLA'); + }); await zigbeeHerdsman.events.lastSeenChanged({device}); await flushPromises(); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('BLA'); + }); await zigbeeHerdsman.events.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); diff --git a/test/controller.test.js b/test/controller.test.js index 26520bf7..3cda62f0 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -1,4 +1,4 @@ -process.env.NOTIFY_SOCKET = "mocked"; +process.env.NOTIFY_SOCKET = 'mocked'; const data = require('./stub/data'); const logger = require('./stub/logger'); const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); @@ -10,24 +10,36 @@ const stringify = require('json-stable-stringify-without-jsonify'); const flushPromises = require('./lib/flushPromises'); const tmp = require('tmp'); const mocksClear = [ - zigbeeHerdsman.permitJoin, MQTT.end, zigbeeHerdsman.stop, logger.debug, - MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error, + zigbeeHerdsman.permitJoin, + MQTT.end, + zigbeeHerdsman.stop, + logger.debug, + MQTT.publish, + MQTT.connect, + zigbeeHerdsman.devices.bulb_color.removeFromNetwork, + zigbeeHerdsman.devices.bulb.removeFromNetwork, + logger.error, ]; const fs = require('fs'); const LOG_MQTT_NS = 'z2m:mqtt'; -jest.mock('sd-notify', () => { - return { - watchdogInterval: () => {return 3000;}, - startWatchdogMode: (interval) => {}, - stopWatchdogMode: () => {}, - ready: () => {}, - stopping: () => {}, - }; -}, {virtual: true}); +jest.mock( + 'sd-notify', + () => { + return { + watchdogInterval: () => { + return 3000; + }, + startWatchdogMode: (interval) => {}, + stopWatchdogMode: () => {}, + ready: () => {}, + stopping: () => {}, + }; + }, + {virtual: true}, +); describe('Controller', () => { let controller; @@ -51,27 +63,51 @@ describe('Controller', () => { afterAll(async () => { jest.useRealTimers(); - }) + }); it('Start controller', async () => { await controller.start(); - expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),adapter: {concurrent: null, delay: null, disableLED: false}, "serialPort":{"baudRate":undefined,"rtscts":undefined,"path":"/dev/dummy"}}); + expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({ + network: { + panID: 6754, + extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], + channelList: [11], + networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], + }, + databasePath: path.join(data.mockDir, 'database.db'), + databaseBackupPath: path.join(data.mockDir, 'database.db.backup'), + backupPath: path.join(data.mockDir, 'coordinator_backup.json'), + acceptJoiningDeviceHandler: expect.any(Function), + adapter: {concurrent: null, delay: null, disableLED: false}, + serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'}, + }); expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); - expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`) - expect(logger.info).toHaveBeenCalledWith('bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)'); + expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`); + expect(logger.info).toHaveBeenCalledWith( + 'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)', + ); expect(logger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); expect(logger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); expect(MQTT.connect).toHaveBeenCalledTimes(1); - expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", {"will": {"payload": Buffer.from("offline"), "retain": true, "topic": "zigbee2mqtt/bridge/state", "qos": 1}}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}),{ retain: true, qos: 0 }, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({"brightness":255}), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', { + will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + }); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0}, expect.any(Function)); }); it('Start controller when permit join fails', async () => { - zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {throw new Error("failed!")}); + zigbeeHerdsman.permitJoin.mockImplementationOnce(() => { + throw new Error('failed!'); + }); await controller.start(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); expect(MQTT.connect).toHaveBeenCalledTimes(1); @@ -79,29 +115,31 @@ describe('Controller', () => { it('Start controller with specific MQTT settings', async () => { const ca = tmp.fileSync().name; - fs.writeFileSync(ca, "ca"); + fs.writeFileSync(ca, 'ca'); const key = tmp.fileSync().name; - fs.writeFileSync(key, "key"); + fs.writeFileSync(key, 'key'); const cert = tmp.fileSync().name; - fs.writeFileSync(cert, "cert"); + fs.writeFileSync(cert, 'cert'); const configuration = { - base_topic: "zigbee2mqtt", - server: "mqtt://localhost", + base_topic: 'zigbee2mqtt', + server: 'mqtt://localhost', keepalive: 30, - ca, cert, key, + ca, + cert, + key, password: 'pass', user: 'user1', client_id: 'my_client_id', reject_unauthorized: false, version: 5, - } - settings.set(['mqtt'], configuration) + }; + settings.set(['mqtt'], configuration); await controller.start(); await flushPromises(); expect(MQTT.connect).toHaveBeenCalledTimes(1); const expected = { - "will": {"payload": Buffer.from("offline"), "retain": true, "topic": "zigbee2mqtt/bridge/state", "qos": 1}, + will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, keepalive: 30, ca: Buffer.from([99, 97]), key: Buffer.from([107, 101, 121]), @@ -111,9 +149,8 @@ describe('Controller', () => { clientId: 'my_client_id', rejectUnauthorized: false, protocolVersion: 5, - - } - expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected); + }; + expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => { @@ -134,9 +171,14 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/remote", stringify({"brightness":255}), {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":'ON'}), {"qos": 0, "retain":false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false}, expect.any(Function)); }); it('Start controller should not publish cached states when disabled', async () => { @@ -144,8 +186,8 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - const publishedTopics = MQTT.publish.mock.calls.map(m => m[0]); - expect(publishedTopics).toEqual(expect.not.arrayContaining(["zigbee2mqtt/bulb", "zigbee2mqtt/remote"])); + const publishedTopics = MQTT.publish.mock.calls.map((m) => m[0]); + expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote'])); }); it('Start controller should not publish cached states when cache_state is false', async () => { @@ -153,8 +195,13 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith("zigbee2mqtt/bulb", `{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`, {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).not.toHaveBeenCalledWith("zigbee2mqtt/remote", `{"brightness":255}`, {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).not.toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + `{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`, + {qos: 0, retain: true}, + expect.any(Function), + ); + expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}, expect.any(Function)); }); it('Log when MQTT client is unavailable', async () => { @@ -163,7 +210,7 @@ describe('Controller', () => { logger.error.mockClear(); controller.mqtt.client.reconnecting = true; jest.advanceTimersByTime(11 * 1000); - expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); + expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); controller.mqtt.client.reconnecting = false; }); @@ -173,11 +220,19 @@ describe('Controller', () => { logger.error.mockClear(); controller.mqtt.client.reconnecting = true; const device = controller.zigbee.resolveEntity('bulb'); - await controller.publishEntityState(device, {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}}); + await controller.publishEntityState(device, { + state: 'ON', + brightness: 50, + color_temp: 370, + color: {r: 100, g: 50, b: 10}, + dummy: {1: 'yes', 2: 'no'}, + }); await flushPromises(); expect(logger.error).toHaveBeenCalledTimes(2); - expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); - expect(logger.error).toHaveBeenCalledWith("Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"brightness\":50,\"color\":{\"b\":10,\"g\":50,\"r\":100},\"color_temp\":370,\"dummy\":{\"1\":\"yes\",\"2\":\"no\"},\"linkquality\":99,\"state\":\"ON\"}"); + expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + expect(logger.error).toHaveBeenCalledWith( + 'Cannot send message: topic: \'zigbee2mqtt/bulb\', payload: \'{"brightness":50,"color":{"b":10,"g":50,"r":100},"color_temp":370,"dummy":{"1":"yes","2":"no"},"linkquality":99,"state":"ON"}', + ); controller.mqtt.client.reconnecting = false; }); @@ -190,7 +245,9 @@ describe('Controller', () => { it('Should remove device not on passlist on startup', async () => { settings.set(['passlist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => {throw new Error("dummy")}); + zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => { + throw new Error('dummy'); + }); await controller.start(); await flushPromises(); expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); @@ -206,7 +263,9 @@ describe('Controller', () => { }); it('Start controller fails', async () => { - zigbeeHerdsman.start.mockImplementationOnce(() => {throw new Error('failed')}); + zigbeeHerdsman.start.mockImplementationOnce(() => { + throw new Error('failed'); + }); await controller.start(); expect(mockExit).toHaveBeenCalledTimes(1); }); @@ -247,7 +306,9 @@ describe('Controller', () => { }); it('Start controller and stop', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')}) + zigbeeHerdsman.stop.mockImplementationOnce(() => { + throw new Error('failed'); + }); await controller.start(); await controller.stop(); expect(MQTT.end).toHaveBeenCalledTimes(1); @@ -257,7 +318,9 @@ describe('Controller', () => { }); it('Start controller adapter disconnects', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')}) + zigbeeHerdsman.stop.mockImplementationOnce(() => { + throw new Error('failed'); + }); await controller.start(); await zigbeeHerdsman.events.adapterDisconnected(); await flushPromises(); @@ -271,14 +334,14 @@ describe('Controller', () => { await controller.start(); logger.debug.mockClear(); await MQTT.events.message('dummytopic', 'dummymessage'); - expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS) + expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS); }); it('Skip MQTT messages on topic we published to', async () => { await controller.start(); logger.debug.mockClear(); await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'", LOG_MQTT_NS) + expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'", LOG_MQTT_NS); logger.debug.mockClear(); await controller.mqtt.publish('skip-this-topic', '', {}); await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); @@ -288,19 +351,38 @@ describe('Controller', () => { it('On zigbee event message', async () => { await controller.start(); const device = zigbeeHerdsman.devices.bulb; - const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}}; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); - expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`); + expect(logger.debug).toHaveBeenCalledWith( + `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`, + ); }); it('On zigbee event message with group ID', async () => { await controller.start(); const device = zigbeeHerdsman.devices.bulb; - const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 0, cluster: 'genBasic', data: {modelId: device.modelID}}; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + groupID: 0, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); - expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`); + expect(logger.debug).toHaveBeenCalledWith( + `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`, + ); }); it('Should add entities which are missing from configuration but are in database to configuration', async () => { @@ -315,7 +397,12 @@ describe('Controller', () => { const payload = {device}; await zigbeeHerdsman.events.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_connected","message":{"friendly_name":"bulb"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('acceptJoiningDeviceHandler reject device on blocklist', async () => { @@ -373,7 +460,12 @@ describe('Controller', () => { zigbeeHerdsman.events.deviceJoined(payload); zigbeeHerdsman.events.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_connected","message":{"friendly_name":"bulb"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee deviceInterview started', async () => { @@ -382,7 +474,12 @@ describe('Controller', () => { const payload = {device, status: 'started'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_started","meta":{"friendly_name":"bulb"}}), { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'pairing', message: 'interview_started', meta: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee deviceInterview failed', async () => { @@ -391,7 +488,12 @@ describe('Controller', () => { const payload = {device, status: 'failed'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_failed","meta":{"friendly_name":"bulb"}}), { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'pairing', message: 'interview_failed', meta: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee deviceInterview successful supported', async () => { @@ -400,7 +502,22 @@ describe('Controller', () => { const payload = {device, status: 'successful'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_successful","meta":{"friendly_name":"bulb","model":"LED1545G12","vendor":"IKEA","description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","supported":true}}), { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({ + type: 'pairing', + message: 'interview_successful', + meta: { + friendly_name: 'bulb', + model: 'LED1545G12', + vendor: 'IKEA', + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + supported: true, + }, + }), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee deviceInterview successful not supported', async () => { @@ -409,7 +526,12 @@ describe('Controller', () => { const payload = {device, status: 'successful'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_successful","meta":{"friendly_name":"0x0017880104e45518","supported":false}}), { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'pairing', message: 'interview_successful', meta: {friendly_name: '0x0017880104e45518', supported: false}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee event device announce', async () => { @@ -419,19 +541,29 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceAnnounce(payload); await flushPromises(); expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"device_announced","message":"announce","meta":{"friendly_name":"bulb"}}), { retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_announced', message: 'announce', meta: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee event device leave (removed from database and settings)', async () => { await controller.start(); zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); - settings.set(['devices'], {}) + settings.set(['devices'], {}); MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; await zigbeeHerdsman.events.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"device_removed","message":"left_network","meta":{"friendly_name":"0x000b57fffec6a5b2"}}), { retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('On zigbee event device leave (removed from database and NOT settings)', async () => { @@ -442,7 +574,12 @@ describe('Controller', () => { const payload = {ieeeAddr: device.ieeeAddr}; await zigbeeHerdsman.events.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"device_removed","message":"left_network","meta":{"friendly_name":"0x000b57fffec6a5b2"}}), { retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Publish entity state attribute output', async () => { @@ -450,16 +587,24 @@ describe('Controller', () => { settings.set(['experimental', 'output'], 'attribute'); MQTT.publish.mockClear(); const device = controller.zigbee.resolveEntity('bulb'); - await controller.publishEntityState(device, {dummy: {1: 'yes', 2: 'no'}, color: {r: 100, g: 50, b: 10}, state: 'ON', test: undefined, test1: null, color_temp: 370, brightness: 50}); + await controller.publishEntityState(device, { + dummy: {1: 'yes', 2: 'no'}, + color: {r: 100, g: 50, b: 10}, + state: 'ON', + test: undefined, + test1: null, + color_temp: 370, + brightness: 50, + }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "50", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", '100,50,10', {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-1", "yes", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", "no", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test1", '', {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test", '', {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}, expect.any(Function)); }); it('Publish entity state attribute_json output', async () => { @@ -470,14 +615,18 @@ describe('Controller', () => { await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "99", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); - it('Publish entity state attribute_json output filtered', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); @@ -487,9 +636,14 @@ describe('Controller', () => { await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Publish entity state attribute_json output filtered (device_options)', async () => { @@ -501,9 +655,14 @@ describe('Controller', () => { await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Publish entity state attribute_json output filtered cache', async () => { @@ -513,17 +672,22 @@ describe('Controller', () => { MQTT.publish.mockClear(); const device = controller.zigbee.resolveEntity('bulb'); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"}); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); await flushPromises(); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"}); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'}); expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Publish entity state attribute_json output filtered cache (device_options)', async () => { @@ -533,17 +697,22 @@ describe('Controller', () => { MQTT.publish.mockClear(); const device = controller.zigbee.resolveEntity('bulb'); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"}); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); await flushPromises(); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"}); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'}); expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Publish entity state with device information', async () => { @@ -553,13 +722,52 @@ describe('Controller', () => { let device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","model":"LED1545G12","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({ + state: 'ON', + brightness: 50, + color_temp: 370, + linkquality: 99, + device: { + friendlyName: 'bulb', + model: 'LED1545G12', + ieeeAddr: '0x000b57fffec6a5b2', + networkAddress: 40369, + type: 'Router', + manufacturerID: 4476, + powerSource: 'Mains (single phase)', + dateCode: null, + softwareBuildID: null, + }, + }), + {qos: 0, retain: true}, + expect.any(Function), + ); // Unsupported device should have model "unknown" device = controller.zigbee.resolveEntity('unsupported2'); await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', stringify({"state":"ON","device":{"friendlyName":"unsupported2","model":"notSupportedModelID","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/unsupported2', + stringify({ + state: 'ON', + device: { + friendlyName: 'unsupported2', + model: 'notSupportedModelID', + ieeeAddr: '0x0017880104e45529', + networkAddress: 6536, + type: 'EndDevice', + manufacturerID: 0, + powerSource: 'Battery', + dateCode: null, + softwareBuildID: null, + }, + }), + {qos: 0, retain: false}, + expect.any(Function), + ); }); it('Should publish entity state without retain', async () => { @@ -569,7 +777,12 @@ describe('Controller', () => { const device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: false}, + expect.any(Function), + ); }); it('Should publish entity state with retain', async () => { @@ -579,7 +792,12 @@ describe('Controller', () => { const device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Should publish entity state with expiring retention', async () => { @@ -591,7 +809,12 @@ describe('Controller', () => { const device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true, "properties": {messageExpiryInterval: 37}}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true, properties: {messageExpiryInterval: 37}}, + expect.any(Function), + ); }); it('Publish entity state no empty messages', async () => { @@ -614,8 +837,13 @@ describe('Controller', () => { await controller.publishEntityState(device, {brightness: 200}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "ON"}), {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "ON", brightness: 200}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200}), + {qos: 0, retain: true}, + expect.any(Function), + ); await controller.stop(); expect(data.stateExists()).toBeFalsy(); }); @@ -624,7 +852,7 @@ describe('Controller', () => { data.removeState(); await controller.start(); logger.error.mockClear(); - controller.state.file = "/"; + controller.state.file = '/'; await controller.state.save(); expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to \'\/\'/)); }); @@ -639,8 +867,8 @@ describe('Controller', () => { await controller.publishEntityState(device, {brightness: 200}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON"}), {"qos": 0, "retain": true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"brightness":200}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}, expect.any(Function)); }); it('Should start when state is corrupted', async () => { @@ -656,18 +884,18 @@ describe('Controller', () => { await flushPromises(); expect(MQTT.connect).toHaveBeenCalledTimes(1); const expected = { - "will": { "payload": Buffer.from("offline"), "retain": false, "topic": "zigbee2mqtt/bridge/state", "qos": 1 }, - } - expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected); + will: {payload: Buffer.from('offline'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + }; + expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should republish retained messages on MQTT reconnect', async () => { await controller.start(); MQTT.publish.mockClear(); MQTT.events['connect'](); - await jest.advanceTimersByTimeAsync(2500);// before any startup configure triggers + await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers expect(MQTT.publish).toHaveBeenCalledTimes(14); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should not republish retained messages on MQTT reconnect when retained message are sent', async () => { @@ -676,19 +904,19 @@ describe('Controller', () => { MQTT.events['connect'](); await flushPromises(); await MQTT.events.message('zigbee2mqtt/bridge/info', 'dummy'); - await jest.advanceTimersByTimeAsync(2500);// before any startup configure triggers + await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => { settings.set(['mqtt', 'force_disable_retain'], true); - await controller.mqtt.connect() + await controller.mqtt.connect(); MQTT.publish.mockClear(); - await controller.mqtt.publish('fo', 'bar', { retain: true }) + await controller.mqtt.publish('fo', 'bar', {retain: true}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function)); }); it('Should disable legacy options on new network start', async () => { @@ -711,7 +939,11 @@ describe('Controller', () => { await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'deviceAnnounce'}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/remote', stringify({"brightness":255,"last_seen":1000}), { qos: 0, retain: true }, expect.any(Function)); + 'zigbee2mqtt/remote', + stringify({brightness: 255, last_seen: 1000}), + {qos: 0, retain: true}, + expect.any(Function), + ); }); it('Should not publish last seen changes when reason is messageEmitted', async () => { @@ -728,16 +960,25 @@ describe('Controller', () => { // https://github.com/Koenkk/zigbee2mqtt/issues/9218 await controller.start(); const device = zigbeeHerdsman.devices.coordinator; - const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}}; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); - expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`); + expect(logger.debug).toHaveBeenCalledWith( + `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`, + ); }); it('Should remove state of removed device when stopped', async () => { await controller.start(); const device = controller.zigbee.resolveEntity('bulb'); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"}); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); device.zh.isDeleted = true; await controller.stop(); expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined); @@ -745,7 +986,9 @@ describe('Controller', () => { it('EventBus should handle errors', async () => { const eventbus = controller.eventBus; - const callback = jest.fn().mockImplementation(async () => {throw new Error('Whoops!')}); + const callback = jest.fn().mockImplementation(async () => { + throw new Error('Whoops!'); + }); eventbus.onStateChange('test', callback); eventbus.emitStateChange({}); await flushPromises(); diff --git a/test/externalConverters.test.js b/test/externalConverters.test.js index 2ac98592..eec2fe4b 100644 --- a/test/externalConverters.test.js +++ b/test/externalConverters.test.js @@ -12,31 +12,48 @@ const fs = require('fs'); zigbeeHerdsmanConverters.addDefinition = jest.fn(); -const mocksClear = [zigbeeHerdsmanConverters.addDefinition, zigbeeHerdsman.permitJoin, - mockExit, MQTT.end, zigbeeHerdsman.stop, logger.debug, - MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error, +const mocksClear = [ + zigbeeHerdsmanConverters.addDefinition, + zigbeeHerdsman.permitJoin, + mockExit, + MQTT.end, + zigbeeHerdsman.stop, + logger.debug, + MQTT.publish, + MQTT.connect, + zigbeeHerdsman.devices.bulb_color.removeFromNetwork, + zigbeeHerdsman.devices.bulb.removeFromNetwork, + logger.error, ]; jest.mock( - 'mock-external-converter-module', () => { + 'mock-external-converter-module', + () => { return { - mock: true + mock: true, }; - }, { - virtual: true - }); + }, + { + virtual: true, + }, +); jest.mock( - 'mock-multiple-external-converter-module', () => { - return [{ - mock: 1 - }, { - mock: 2 - }]; - }, { - virtual: true - }); + 'mock-multiple-external-converter-module', + () => { + return [ + { + mock: 1, + }, + { + mock: 2, + }, + ]; + }, + { + virtual: true, + }, +); describe('Loads external converters', () => { let controller; @@ -44,7 +61,7 @@ describe('Loads external converters', () => { let resetExtension = async () => { await controller.enableDisableExtension(false, 'ExternalConverters'); await controller.enableDisableExtension(true, 'ExternalConverters'); - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -83,12 +100,15 @@ describe('Loads external converters', () => { description: 'external', fromZigbee: [], toZigbee: [], - exposes: [] + exposes: [], }); }); it('Loads multiple external converters', async () => { - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js')); + fs.copyFileSync( + path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), + path.join(data.mockDir, 'mock-external-converter-multiple.js'), + ); settings.set(['external_converters'], ['mock-external-converter-multiple.js']); await resetExtension(); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2); @@ -100,7 +120,7 @@ describe('Loads external converters', () => { description: 'external_1', fromZigbee: [], toZigbee: [], - exposes: [] + exposes: [], }); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { mock: 2, @@ -110,7 +130,7 @@ describe('Loads external converters', () => { description: 'external_2', fromZigbee: [], toZigbee: [], - exposes: [] + exposes: [], }); }); @@ -119,7 +139,7 @@ describe('Loads external converters', () => { await resetExtension(); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({ - mock: true + mock: true, }); }); @@ -128,18 +148,20 @@ describe('Loads external converters', () => { await resetExtension(); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, { - mock: 1 + mock: 1, }); expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { - mock: 2 + mock: 2, }); }); it('Loads external converters with error', async () => { fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); settings.set(['external_converters'], ['mock-external-converter.js']); - zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => {throw new Error('Invalid definition!')}); + zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => { + throw new Error('Invalid definition!'); + }); await resetExtension(); expect(logger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`); }); -}); \ No newline at end of file +}); diff --git a/test/externalExtension.test.js b/test/externalExtension.test.js index f63830fb..38c7fafd 100644 --- a/test/externalExtension.test.js +++ b/test/externalExtension.test.js @@ -9,9 +9,15 @@ const Controller = require('../lib/controller'); const stringify = require('json-stable-stringify-without-jsonify'); const flushPromises = require('./lib/flushPromises'); const mocksClear = [ - zigbeeHerdsman.permitJoin, MQTT.end, zigbeeHerdsman.stop, logger.debug, - MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error, + zigbeeHerdsman.permitJoin, + MQTT.end, + zigbeeHerdsman.stop, + logger.debug, + MQTT.publish, + MQTT.connect, + zigbeeHerdsman.devices.bulb_color.removeFromNetwork, + zigbeeHerdsman.devices.bulb.removeFromNetwork, + logger.error, ]; const fs = require('fs'); @@ -30,7 +36,7 @@ describe('User extensions', () => { settings.reRead(); mocksClear.forEach((m) => m.mockClear()); }); - + afterAll(async () => { jest.useRealTimers(); }); @@ -46,18 +52,23 @@ describe('User extensions', () => { afterEach(() => { const extensionPath = path.join(data.mockDir, 'extension'); rimrafSync(extensionPath); - }) + }); it('Load user extension', async () => { const extensionPath = path.join(data.mockDir, 'extension'); const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); fs.mkdirSync(extensionPath); - fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js')) + fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js')); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "exampleExtension.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/extensions', + stringify([{name: 'exampleExtension.js', code: extensionCode}]), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Load user extension from api call', async () => { @@ -67,53 +78,72 @@ describe('User extensions', () => { await controller.start(); await flushPromises(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({"name": "foo.js", "code": extensionCode})); + MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "foo.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/extensions', + stringify([{name: 'foo.js', code: extensionCode}]), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/example/extension', + 'call from constructor', + {retain: false, qos: 0}, + expect.any(Function), + ); expect(mkdirSyncSpy).toHaveBeenCalledWith(extensionPath); }); it('Do not load corrupted extensions', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = "definetly not a correct javascript code"; + const extensionCode = 'definetly not a correct javascript code'; controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({"name": "foo.js", "code": extensionCode})); + MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/extension/save', expect.any(String), { retain: false, qos: 0 }, expect.any(Function)); - const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); - expect(payload).toEqual( - expect.objectContaining({"data":{},"status":"error"}) + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/save', + expect.any(String), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(payload.error).toMatch("Unexpected identifier"); + const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); + expect(payload).toEqual(expect.objectContaining({data: {}, status: 'error'})); + expect(payload.error).toMatch('Unexpected identifier'); }); it('Removes user extension', async () => { const extensionPath = path.join(data.mockDir, 'extension'); const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); fs.mkdirSync(extensionPath); - const extensionFilePath = path.join(extensionPath, 'exampleExtension.js') - fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath) + const extensionFilePath = path.join(extensionPath, 'exampleExtension.js'); + fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "exampleExtension.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/extensions', + stringify([{name: 'exampleExtension.js', code: extensionCode}]), + {retain: true, qos: 0}, + expect.any(Function), + ); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "exampleExtension.js"})); + MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'exampleExtension.js'})); await flushPromises(); expect(unlinkSyncSpy).toHaveBeenCalledWith(extensionFilePath); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "non existing.js"})); + MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'non existing.js'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/extension/remove', - stringify({"data":{},"status":"error","error":"Extension non existing.js doesn't exists"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Extension non existing.js doesn't exists"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); }); diff --git a/test/frontend.test.js b/test/frontend.test.js index 129be422..c2f3cf47 100644 --- a/test/frontend.test.js +++ b/test/frontend.test.js @@ -7,13 +7,15 @@ const Controller = require('../lib/controller'); const stringify = require('json-stable-stringify-without-jsonify'); const flushPromises = require('./lib/flushPromises'); const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const path = require("path"); +const path = require('path'); jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockHTTP = { implementation: { listen: jest.fn(), - on: (event, handler) => {mockHTTP.events[event] = handler}, + on: (event, handler) => { + mockHTTP.events[event] = handler; + }, close: jest.fn().mockImplementation((cb) => cb()), }, variables: {}, @@ -23,7 +25,9 @@ const mockHTTP = { const mockHTTPS = { implementation: { listen: jest.fn(), - on: (event, handler) => {mockHTTPS.events[event] = handler}, + on: (event, handler) => { + mockHTTPS.events[event] = handler; + }, close: jest.fn().mockImplementation((cb) => cb()), }, variables: {}, @@ -37,9 +41,11 @@ const mockWSocket = { const mockWS = { implementation: { clients: [], - on: (event, handler) => {mockWS.events[event] = handler}, + on: (event, handler) => { + mockWS.events[event] = handler; + }, handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => { - cb(mockWSocket) + cb(mockWSocket); }), emit: jest.fn(), close: jest.fn(), @@ -70,11 +76,11 @@ jest.mock('https', () => ({ Agent: jest.fn(), })); -jest.mock("connect-gzip-static", () => +jest.mock('connect-gzip-static', () => jest.fn().mockImplementation((path) => { - mockNodeStatic.variables.path = path - return mockNodeStatic.implementation - }) + mockNodeStatic.variables.path = path; + return mockNodeStatic.implementation; + }), ); jest.mock('zigbee2mqtt-frontend', () => ({ @@ -100,7 +106,7 @@ describe('Frontend', () => { data.writeDefaultConfiguration(); data.writeDefaultState(); settings.reRead(); - settings.set(['frontend'], {port: 8081, host: "127.0.0.1"}); + settings.set(['frontend'], {port: 8081, host: '127.0.0.1'}); settings.set(['homeassistant'], true); zigbeeHerdsman.devices.bulb.linkquality = 10; }); @@ -109,15 +115,15 @@ describe('Frontend', () => { jest.useRealTimers(); }); - afterEach(async() => { + afterEach(async () => { delete zigbeeHerdsman.devices.bulb.linkquality; }); it('Start/stop', async () => { controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockNodeStatic.variables.path).toBe("my/dummy/path"); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1"); + expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); + expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); const mockWSClient = { implementation: { terminate: jest.fn(), @@ -140,7 +146,7 @@ describe('Frontend', () => { settings.set(['frontend'], {port: 8081}); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockNodeStatic.variables.path).toBe("my/dummy/path"); + expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081); const mockWSClient = { implementation: { @@ -161,11 +167,11 @@ describe('Frontend', () => { }); it('Start/stop unix socket', async () => { - settings.set(['frontend'], {host: "/tmp/zigbee2mqtt.sock"}); + settings.set(['frontend'], {host: '/tmp/zigbee2mqtt.sock'}); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockNodeStatic.variables.path).toBe("my/dummy/path"); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith("/tmp/zigbee2mqtt.sock"); + expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); + expect(mockHTTP.implementation.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock'); const mockWSClient = { implementation: { terminate: jest.fn(), @@ -184,42 +190,39 @@ describe('Frontend', () => { mockHTTPS.implementation.listen.mockClear(); }); - it('Start/stop HTTPS valid', async () => { - settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt')); - settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key')); + settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt')); + settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key')); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1"); - expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1"); + expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); await controller.stop(); mockHTTP.implementation.listen.mockClear(); mockHTTPS.implementation.listen.mockClear(); }); it('Start/stop HTTPS invalid : missing config', async () => { - settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt')); + settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt')); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1"); - expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1"); + expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); await controller.stop(); mockHTTP.implementation.listen.mockClear(); mockHTTPS.implementation.listen.mockClear(); - }); it('Start/stop HTTPS invalid : missing file', async () => { - settings.set(['frontend','ssl_cert'], 'filesNotExists.crt'); - settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key')); + settings.set(['frontend', 'ssl_cert'], 'filesNotExists.crt'); + settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key')); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1"); - expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1"); + expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); await controller.stop(); mockHTTP.implementation.listen.mockClear(); mockHTTPS.implementation.listen.mockClear(); - }); it('Websocket interaction', async () => { @@ -229,7 +232,9 @@ describe('Frontend', () => { // Connect const mockWSClient = { implementation: { - on: (event, handler) => {mockWSClient.events[event] = handler}, + on: (event, handler) => { + mockWSClient.events[event] = handler; + }, send: jest.fn(), readyState: 'open', }, @@ -239,22 +244,28 @@ describe('Frontend', () => { await mockWS.events.connection(mockWSClient.implementation); expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: 'online'})); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic:"remote", payload:{brightness:255}})); + expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}})); // Message MQTT.publish.mockClear(); mockWSClient.implementation.send.mockClear(); - mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false) + mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({ + state: 'ON', + power_on_behavior: null, + linkquality: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: false, qos: 0}, + expect.any(Function), ); mockWSClient.events.message(undefined, false); - mockWSClient.events.message("", false); + mockWSClient.events.message('', false); mockWSClient.events.message(null, false); await flushPromises(); @@ -264,12 +275,23 @@ describe('Frontend', () => { // Received message on socket expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}})); + expect(mockWSClient.implementation.send).toHaveBeenCalledWith( + stringify({ + topic: 'bulb_color', + payload: { + state: 'ON', + power_on_behavior: null, + linkquality: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }, + }), + ); // Shouldnt set when not ready mockWSClient.implementation.send.mockClear(); mockWSClient.implementation.readyState = 'close'; - mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false) + mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(0); // Send last seen on connect @@ -278,7 +300,9 @@ describe('Frontend', () => { settings.set(['advanced'], {last_seen: 'ISO_8601'}); mockWS.implementation.clients.push(mockWSClient.implementation); await mockWS.events.connection(mockWSClient.implementation); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic:"remote", payload:{brightness:255, last_seen: "1970-01-01T00:00:01.000Z"}})); + expect(mockWSClient.implementation.send).toHaveBeenCalledWith( + stringify({topic: 'remote', payload: {brightness: 255, last_seen: '1970-01-01T00:00:01.000Z'}}), + ); }); it('onReques/onUpgrade', async () => { @@ -290,9 +314,9 @@ describe('Frontend', () => { mockHTTP.events.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3); expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "http://localhost:8080/api"}, mockSocket, 3, expect.any(Function)); + expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: 'http://localhost:8080/api'}, mockSocket, 3, expect.any(Function)); mockWS.implementation.handleUpgrade.mock.calls[0][3](99); - expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {"url": "http://localhost:8080/api"}); + expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'}); mockHTTP.variables.onRequest(1, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); @@ -303,11 +327,11 @@ describe('Frontend', () => { controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1"); + expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); }); it('Authentification', async () => { - const authToken = 'sample-secure-token' + const authToken = 'sample-secure-token'; settings.set(['frontend'], {auth_token: authToken}); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); @@ -317,8 +341,8 @@ describe('Frontend', () => { mockHTTP.events.upgrade({url: '/api'}, mockSocket, mockWSocket); expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "/api"}, mockSocket, mockWSocket, expect.any(Function)); - expect(mockWSocket.close).toHaveBeenCalledWith(4401, "Unauthorized"); + expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: '/api'}, mockSocket, mockWSocket, expect.any(Function)); + expect(mockWSocket.close).toHaveBeenCalledWith(4401, 'Unauthorized'); mockWSocket.close.mockClear(); mockWS.implementation.emit.mockClear(); @@ -332,6 +356,5 @@ describe('Frontend', () => { expect(mockWSocket.close).toHaveBeenCalledTimes(0); mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket); expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url}); - }); }); diff --git a/test/group.test.js b/test/group.test.js index cda17407..71683083 100644 --- a/test/group.test.js +++ b/test/group.test.js @@ -22,7 +22,7 @@ describe('Groups', () => { let resetExtension = async () => { await controller.enableDisableExtension(false, 'Groups'); await controller.enableDisableExtension(true, 'Groups'); - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -36,22 +36,22 @@ describe('Groups', () => { }); beforeEach(() => { - Object.values(zigbeeHerdsman.groups).forEach((g) => g.members = []); + Object.values(zigbeeHerdsman.groups).forEach((g) => (g.members = [])); data.writeDefaultConfiguration(); settings.reRead(); MQTT.publish.mockClear(); zigbeeHerdsman.groups.gledopto_group.command.mockClear(); zigbeeHerdsmanConverters.toZigbee.__clearStore__(); controller.state.state = {}; - }) + }); it('Apply group updates add', async () => { - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}}); - zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1)) + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}}); + zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1)); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ zigbeeHerdsman.devices.bulb.getEndpoint(1), - zigbeeHerdsman.devices.bulb_color.getEndpoint(1) + zigbeeHerdsman.devices.bulb_color.getEndpoint(1), ]); }); @@ -59,17 +59,19 @@ describe('Groups', () => { const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false,}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); }); it('Apply group updates remove handle fail', async () => { const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - endpoint.removeFromGroup.mockImplementationOnce(() => {throw new Error("failed!")}); + endpoint.removeFromGroup.mockImplementationOnce(() => { + throw new Error('failed!'); + }); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false,}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); logger.error.mockClear(); await resetExtension(); expect(logger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); @@ -81,28 +83,28 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'3': {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {3: {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); }); it('Add non standard endpoint to group with name', async () => { const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); }); it('Add non standard endpoint to group with number', async () => { const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); }); it('Shouldnt crash on non-existing devices', async () => { logger.error.mockClear(); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); expect(logger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); @@ -110,11 +112,11 @@ describe('Groups', () => { it('Should resolve device friendly names', async () => { settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}}); await resetExtension(); expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ zigbeeHerdsman.devices.bulb.getEndpoint(1), - zigbeeHerdsman.devices.bulb_color.getEndpoint(1) + zigbeeHerdsman.devices.bulb_color.getEndpoint(1), ]); }); @@ -122,28 +124,38 @@ describe('Groups', () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: []}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color'); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_group_add","message":{"friendly_name":"bulb_color","group":"group_1"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group_1'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Legacy api: Add to group with slashes via MQTT', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups["group/with/slashes"]; - settings.set(['groups'], {'99': {friendly_name: 'group/with/slashes', retain: false, devices: []}}); + const group = zigbeeHerdsman.groups['group/with/slashes']; + settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); MQTT.events.message('zigbee2mqtt/bridge/group/group/with/slashes/add', 'bulb_color'); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_group_add","message":{"friendly_name":"bulb_color","group":"group/with/slashes"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group/with/slashes'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Legacy api: Add to group via MQTT with postfix', async () => { @@ -177,13 +189,18 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_group_remove","message":{"friendly_name":"bulb_color","group":"group_1"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_group_remove', message: {friendly_name: 'bulb_color', group: 'group_1'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Legacy api: Remove from group via MQTT when in zigbee but not in settings', async () => { @@ -191,7 +208,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); await flushPromises(); @@ -204,7 +221,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/3'); await flushPromises(); @@ -217,7 +234,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'wall_switch_double/3'); await flushPromises(); @@ -230,7 +247,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/right'); await flushPromises(); @@ -243,13 +260,18 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/remove_all', '0x0017880104e45542/right'); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_group_remove_all","message":{"friendly_name":"wall_switch_double"}}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + stringify({type: 'device_group_remove_all', message: {friendly_name: 'wall_switch_double'}}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Remove from group all deprecated', async () => { @@ -257,7 +279,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove_all', '0x0017880104e45542/right'); await flushPromises(); @@ -294,7 +316,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); @@ -303,8 +325,8 @@ describe('Groups', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not republish identical optimistic group states', async () => { @@ -314,16 +336,55 @@ describe('Groups', () => { await resetExtension(); MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device: device1, endpoint: device1.getEndpoint(1), type: 'attributeReport', linkquality: 10}); - await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device: device2, endpoint: device2.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {onOff: 1}, + cluster: 'genOnOff', + device: device1, + endpoint: device1.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); + await zigbeeHerdsman.events.message({ + data: {onOff: 1}, + cluster: 'genOnOff', + device: device2, + endpoint: device2.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(6); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_tradfri_remote", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_with_tradfri", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/ha_discovery_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_tradfri_remote', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color_2', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_with_tradfri', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/ha_discovery_group', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/switch_group', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should publish state change of all members when a group changes its state', async () => { @@ -331,15 +392,15 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not publish state change when group changes state and device is disabled', async () => { @@ -348,14 +409,14 @@ describe('Groups', () => { const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); settings.set(['devices', device.ieeeAddr, 'disabled'], true); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state change for group when members state change', async () => { @@ -364,29 +425,29 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state of device with endpoint name', async () => { @@ -397,10 +458,20 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({"state_cct":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/GLEDOPTO_2ID', + stringify({state_cct: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/gledopto_group', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should publish state of group when specific state of specific endpoint is changed', async () => { @@ -411,8 +482,18 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({"state_cct":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/GLEDOPTO_2ID', + stringify({state_cct: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/gledopto_group', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); expect(group.command).toHaveBeenCalledTimes(0); }); @@ -421,15 +502,20 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON","brightness":100}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 100}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Shouldnt publish group state change when a group is not optimistic', async () => { @@ -437,7 +523,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}}); await resetExtension(); MQTT.publish.mockClear(); @@ -446,7 +532,7 @@ describe('Groups', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state change of another group with shared device when a group changes its state', async () => { @@ -455,9 +541,9 @@ describe('Groups', () => { const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); settings.set(['groups'], { - '1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}, - '2': {friendly_name: 'group_2', retain: false, devices: [device.ieeeAddr]}, - '3': {friendly_name: 'group_3', retain: false, devices: []} + 1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}, + 2: {friendly_name: 'group_2', retain: false, devices: [device.ieeeAddr]}, + 3: {friendly_name: 'group_3', retain: false, devices: []}, }); await resetExtension(); @@ -465,9 +551,9 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not publish state change off if any lights within are still on when changed via device', async () => { @@ -479,7 +565,7 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false} + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, }); await resetExtension(); @@ -490,7 +576,7 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => { @@ -502,7 +588,7 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'} + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'}, }); await resetExtension(); @@ -513,8 +599,20 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, + 'zigbee2mqtt/group_1', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should not publish state change off if any lights within are still on when changed via shared group', async () => { @@ -526,8 +624,8 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, - '2': {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]}, + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, + 2: {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]}, }); await resetExtension(); @@ -538,8 +636,8 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state change off if all lights within turn off', async () => { @@ -551,7 +649,7 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false} + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, }); await resetExtension(); @@ -563,9 +661,9 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"OFF"}), {"retain": true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should only update group state with changed properties', async () => { @@ -577,7 +675,7 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false} + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, }); await resetExtension(); MQTT.publish.mockClear(); @@ -590,9 +688,24 @@ describe('Groups', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color_mode": "color_temp","color_temp":300,"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode": "color_temp","color_temp":300,"state":"ON"}), {"retain": true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color_mode": "color_temp","color_temp":300,"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({color_mode: 'color_temp', color_temp: 300, state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should publish state change off even when missing current state', async () => { @@ -604,7 +717,7 @@ describe('Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - '1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false} + 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, }); await resetExtension(); @@ -617,27 +730,28 @@ describe('Groups', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Add to group via MQTT', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: []}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({transaction: "123", group: 'group_1', device: 'bulb_color'})); + MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({transaction: '123', group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"bulb_color","group":"group_1"},"transaction": "123", "status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group_1'}, transaction: '123', status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -645,10 +759,12 @@ describe('Groups', () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: []}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); - endpoint.addToGroup.mockImplementationOnce(() => {throw new Error('timeout')}); + endpoint.addToGroup.mockImplementationOnce(() => { + throw new Error('timeout'); + }); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); @@ -658,16 +774,17 @@ describe('Groups', () => { expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"error","error":"Failed to add from group (timeout)"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'error', error: 'Failed to add from group (timeout)'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Add to group with slashes via MQTT', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups["group/with/slashes"]; - settings.set(['groups'], {'99': {friendly_name: 'group/with/slashes', retain: false, devices: []}}); + const group = zigbeeHerdsman.groups['group/with/slashes']; + settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); MQTT.publish.mockClear(); @@ -678,8 +795,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"bulb_color","group":"group/with/slashes"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group/with/slashes'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -697,8 +815,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"wall_switch_double/right","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -718,8 +837,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"wall_switch_double/right","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -728,7 +848,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); @@ -738,8 +858,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -748,18 +869,22 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true}), + ); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -768,7 +893,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); @@ -778,8 +903,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -788,7 +914,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); @@ -798,8 +924,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"0x0017880104e45542/3","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: '0x0017880104e45542/3', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -808,7 +935,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); @@ -818,8 +945,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"wall_switch_double/3","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: 'wall_switch_double/3', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -828,7 +956,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); @@ -838,8 +966,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"0x0017880104e45542/right","group":"group_1"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: '0x0017880104e45542/right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -848,7 +977,7 @@ describe('Groups', () => { const endpoint = device.getEndpoint(1); const group = zigbeeHerdsman.groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); @@ -858,8 +987,9 @@ describe('Groups', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove_all', - stringify({"data":{"device":"0x0017880104e45542/right"},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {device: '0x0017880104e45542/right'}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -872,8 +1002,13 @@ describe('Groups', () => { expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({"data":{"device":"bulb_color","group":"group_1_not_existing"},"status":"error","error":"Group 'group_1_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {device: 'bulb_color', group: 'group_1_not_existing'}, + status: 'error', + error: "Group 'group_1_not_existing' does not exist", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -886,8 +1021,13 @@ describe('Groups', () => { expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"bulb_color_not_existing","group":"group_1"},"status":"error","error":"Device 'bulb_color_not_existing' does not exist"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {device: 'bulb_color_not_existing', group: 'group_1'}, + status: 'error', + error: "Device 'bulb_color_not_existing' does not exist", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -895,13 +1035,21 @@ describe('Groups', () => { await resetExtension(); logger.error.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'}), + ); await flushPromises(); expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({"data":{"device":"bulb_color/not_existing_endpoint","group":"group_1"},"status":"error","error":"Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {device: 'bulb_color/not_existing_endpoint', group: 'group_1'}, + status: 'error', + error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'", + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -911,23 +1059,53 @@ describe('Groups', () => { const group = zigbeeHerdsman.groups.group_1; group.members.push(bulbColor.getEndpoint(1)); group.members.push(bulbColorTemp.getEndpoint(1)); - settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}}); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}}); await resetExtension(); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({color_mode: 'color_temp', color_temp: 50}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({color_mode: 'color_temp', color_temp: 50}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({color_mode: 'color_temp', color_temp: 50}), + {retain: true, qos: 0}, + expect.any(Function), + ); MQTT.publish.mockClear(); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color":{"x":0.5,"y":0.3},"color_mode":"xy","color_temp":548}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color":{"x":0.5,"y":0.3},"color_mode":"xy","color_temp":548}), {"retain": false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode":"color_temp","color_temp":548}), {"retain": true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({color_mode: 'color_temp', color_temp: 548}), + {retain: true, qos: 0}, + expect.any(Function), + ); }); }); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index df15b236..6a679aca 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -15,7 +15,7 @@ describe('HomeAssistant extension', () => { let extension; let origin; - let resetExtension = async (runTimers=true) => { + let resetExtension = async (runTimers = true) => { await controller.enableDisableExtension(false, 'HomeAssistant'); MQTT.publish.mockClear(); await controller.enableDisableExtension(true, 'HomeAssistant'); @@ -23,16 +23,16 @@ describe('HomeAssistant extension', () => { if (runTimers) { await jest.runOnlyPendingTimersAsync(); } - } + }; let resetDiscoveryPayloads = (id) => { // Change discovered payload, otherwise it's not re-published because it's the same. - Object.values(extension.discovered[id].messages).forEach((m) => m.payload = 'changed'); - } + Object.values(extension.discovered[id].messages).forEach((m) => (m.payload = 'changed')); + }; let clearDiscoveredTrigger = (id) => { extension.discovered[id].triggers = new Set(); - } + }; beforeEach(async () => { data.writeDefaultConfiguration(); @@ -92,378 +92,360 @@ describe('HomeAssistant extension', () => { let payload; payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "brightness":true, - "brightness_scale":254, - "command_topic":"zigbee2mqtt/ha_discovery_group/set", - "device":{ - "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], - "name":"ha_discovery_group", - "sw_version": version, - "model": "Group", - "manufacturer": "Zigbee2MQTT", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + brightness: true, + brightness_scale: 254, + command_topic: 'zigbee2mqtt/ha_discovery_group/set', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "max_mireds": 454, - "min_mireds": 250, - "json_attributes_topic":"zigbee2mqtt/ha_discovery_group", - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/ha_discovery_group", - "supported_color_modes":[ - "xy", - "color_temp" + max_mireds: 454, + min_mireds: 250, + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/ha_discovery_group', + supported_color_modes: ['xy', 'color_temp'], + effect: true, + effect_list: [ + 'blink', + 'breathe', + 'okay', + 'channel_change', + 'candle', + 'fireplace', + 'colorloop', + 'finish_effect', + 'stop_effect', + 'stop_hue_effect', ], - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "candle", - "fireplace", - "colorloop", - "finish_effect", - "stop_effect", - "stop_hue_effect", - ], - "object_id":"ha_discovery_group", - "unique_id":"9_light_zigbee2mqtt", - 'origin': origin, + object_id: 'ha_discovery_group', + unique_id: '9_light_zigbee2mqtt', + origin: origin, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "command_topic":"zigbee2mqtt/ha_discovery_group/set", - "device":{ - "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], - "name":"ha_discovery_group", - "sw_version": version, - "model": "Group", - "manufacturer": "Zigbee2MQTT", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/ha_discovery_group/set', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic":"zigbee2mqtt/ha_discovery_group", - "name":null, - "payload_off":"OFF", - "payload_on":"ON", - "state_topic":"zigbee2mqtt/ha_discovery_group", - "object_id":"ha_discovery_group", - "unique_id":"9_switch_zigbee2mqtt", - 'origin': origin, - "value_template":"{{ value_json.state }}" - }; + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', + name: null, + payload_off: 'OFF', + payload_on: 'ON', + state_topic: 'zigbee2mqtt/ha_discovery_group', + object_id: 'ha_discovery_group', + unique_id: '9_switch_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.state }}', + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_9/switch/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - 'enabled_by_default': true, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + enabled_by_default: true, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '%', - 'device_class': 'humidity', - 'state_class': 'measurement', - 'value_template': '{{ value_json.humidity }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_humidity', - 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'humidity', + state_class: 'measurement', + value_template: '{{ value_json.humidity }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_humidity', + unique_id: '0x0017880104e45522_humidity_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': 'hPa', - 'device_class': 'atmospheric_pressure', - 'state_class': 'measurement', - 'value_template': '{{ value_json.pressure }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_pressure', - 'unique_id': '0x0017880104e45522_pressure_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: 'hPa', + device_class: 'atmospheric_pressure', + state_class: 'measurement', + value_template: '{{ value_json.pressure }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_pressure', + unique_id: '0x0017880104e45522_pressure_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '%', - 'device_class': 'battery', - 'state_class': 'measurement', - 'value_template': '{{ value_json.battery }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_battery', - 'unique_id': '0x0017880104e45522_battery_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'entity_category': 'diagnostic', - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'battery', + state_class: 'measurement', + value_template: '{{ value_json.battery }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_battery', + unique_id: '0x0017880104e45522_battery_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + entity_category: 'diagnostic', + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'icon': 'mdi:signal', - 'enabled_by_default': false, - 'entity_category': 'diagnostic', - 'unit_of_measurement': 'lqi', - 'state_class': 'measurement', - 'value_template': '{{ value_json.linkquality }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'name': 'Linkquality', - 'object_id': 'weather_sensor_linkquality', - 'unique_id': '0x0017880104e45522_linkquality_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + icon: 'mdi:signal', + enabled_by_default: false, + entity_category: 'diagnostic', + unit_of_measurement: 'lqi', + state_class: 'measurement', + value_template: '{{ value_json.linkquality }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + name: 'Linkquality', + object_id: 'weather_sensor_linkquality', + unique_id: '0x0017880104e45522_linkquality_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "command_topic":"zigbee2mqtt/wall_switch_double/left/set", - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45542" - ], - "manufacturer":"Aqara", - "model":"Smart wall switch (no neutral, double rocker) (QBKG03LM)", - "name":"wall_switch_double", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/wall_switch_double/left/set', + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45542'], + manufacturer: 'Aqara', + model: 'Smart wall switch (no neutral, double rocker) (QBKG03LM)', + name: 'wall_switch_double', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic":"zigbee2mqtt/wall_switch_double", - "name":"Left", - "payload_off":"OFF", - "payload_on":"ON", - "state_topic":"zigbee2mqtt/wall_switch_double", - "object_id":"wall_switch_double_left", - "unique_id":"0x0017880104e45542_switch_left_zigbee2mqtt", - 'origin': origin, - "value_template":"{{ value_json.state_left }}" + json_attributes_topic: 'zigbee2mqtt/wall_switch_double', + name: 'Left', + payload_off: 'OFF', + payload_on: 'ON', + state_topic: 'zigbee2mqtt/wall_switch_double', + object_id: 'wall_switch_double_left', + unique_id: '0x0017880104e45542_switch_left_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.state_left }}', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_left/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "command_topic":"zigbee2mqtt/wall_switch_double/right/set", - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45542" - ], - "manufacturer":"Aqara", - "model":"Smart wall switch (no neutral, double rocker) (QBKG03LM)", - "name":"wall_switch_double", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/wall_switch_double/right/set', + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45542'], + manufacturer: 'Aqara', + model: 'Smart wall switch (no neutral, double rocker) (QBKG03LM)', + name: 'wall_switch_double', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic":"zigbee2mqtt/wall_switch_double", - "name":"Right", - "payload_off":"OFF", - "payload_on":"ON", - "state_topic":"zigbee2mqtt/wall_switch_double", - "object_id":"wall_switch_double_right", - "unique_id":"0x0017880104e45542_switch_right_zigbee2mqtt", - 'origin': origin, - "value_template":"{{ value_json.state_right }}" + json_attributes_topic: 'zigbee2mqtt/wall_switch_double', + name: 'Right', + payload_off: 'OFF', + payload_on: 'ON', + state_topic: 'zigbee2mqtt/wall_switch_double', + object_id: 'wall_switch_double_right', + unique_id: '0x0017880104e45542_switch_right_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.state_right }}', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_right/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "brightness":true, - "brightness_scale":254, - "supported_color_modes": ["color_temp"], - "min_mireds": 250, - "max_mireds": 454, - "command_topic":"zigbee2mqtt/bulb/set", - "device":{ - "identifiers":[ - "zigbee2mqtt_0x000b57fffec6a5b2" - ], - "manufacturer":"IKEA", - "model":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)", - "name":"bulb", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + brightness: true, + brightness_scale: 254, + supported_color_modes: ['color_temp'], + min_mireds: 250, + max_mireds: 454, + command_topic: 'zigbee2mqtt/bulb/set', + device: { + identifiers: ['zigbee2mqtt_0x000b57fffec6a5b2'], + manufacturer: 'IKEA', + model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', + name: 'bulb', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "finish_effect", - "stop_effect" - ], - "json_attributes_topic":"zigbee2mqtt/bulb", - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/bulb", - "object_id":"bulb", - "unique_id":"0x000b57fffec6a5b2_light_zigbee2mqtt", - 'origin': origin, + effect: true, + effect_list: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], + json_attributes_topic: 'zigbee2mqtt/bulb', + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/bulb', + object_id: 'bulb', + unique_id: '0x000b57fffec6a5b2_light_zigbee2mqtt', + origin: origin, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); - it('Should not discovery devices which are already discovered', async() => { + it('Should not discovery devices which are already discovered', async () => { await resetExtension(false); const topic1 = 'homeassistant/sensor/0x0017880104e45522/humidity/config'; const payload1 = stringify({ - 'unit_of_measurement': '%', - 'device_class': 'humidity', - 'state_class': 'measurement', - 'value_template': '{{ value_json.humidity }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_humidity', - 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'humidity', + state_class: 'measurement', + value_template: '{{ value_json.humidity }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_humidity', + unique_id: '0x0017880104e45522_humidity_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }); const topic2 = 'homeassistant/device_automation/0x0017880104e45522/action_double/config'; const payload2 = stringify({ - "automation_type":"trigger", - "type":"action", - "subtype":"double", - "payload":"double", - "topic":"zigbee2mqtt/weather_sensor_renamed/action", - 'origin': origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45522" - ], - "name":"weather_sensor_renamed", - "sw_version": null, - "model":"Temperature and humidity sensor (WSDCGQ11LM)", - "manufacturer":"Aqara", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", - } + automation_type: 'trigger', + type: 'action', + subtype: 'double', + payload: 'double', + topic: 'zigbee2mqtt/weather_sensor_renamed/action', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor_renamed', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', + }, }); // Should subscribe to `homeassistant/#` to find out what devices are already discovered. @@ -481,7 +463,7 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).not.toHaveBeenCalledWith(topic1, expect.anything(), expect.any(Object), expect.any(Function)); // Device automation should not be cleared expect(MQTT.publish).not.toHaveBeenCalledWith(topic2, null, expect.any(Object), expect.any(Function)); - expect(logger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`) + expect(logger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`); }); it('Should discover devices with precision', async () => { @@ -491,7 +473,7 @@ describe('HomeAssistant extension', () => { pressure_precision: 2, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); @@ -499,89 +481,89 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'enabled_by_default': true, - 'value_template': "{{ value_json.temperature }}", - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + enabled_by_default: true, + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '%', - 'device_class': 'humidity', - 'state_class': 'measurement', - 'value_template': '{{ value_json.humidity }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_humidity', - 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'humidity', + state_class: 'measurement', + value_template: '{{ value_json.humidity }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_humidity', + unique_id: '0x0017880104e45522_humidity_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': 'hPa', - 'device_class': 'atmospheric_pressure', - 'state_class': 'measurement', - 'value_template': '{{ value_json.pressure }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'enabled_by_default': true, - 'object_id': 'weather_sensor_pressure', - 'unique_id': '0x0017880104e45522_pressure_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: 'hPa', + device_class: 'atmospheric_pressure', + state_class: 'measurement', + value_template: '{{ value_json.pressure }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + enabled_by_default: true, + object_id: 'weather_sensor_pressure', + unique_id: '0x0017880104e45522_pressure_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -595,8 +577,8 @@ describe('HomeAssistant extension', () => { expire_after: 90, device: { manufacturer: 'From Aqara', - sw_version: 'test' - } + sw_version: 'test', + }, }, humidity: { unique_id: null, @@ -608,7 +590,7 @@ describe('HomeAssistant extension', () => { }, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); @@ -616,63 +598,63 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'enabled_by_default': true, - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': 'test', - 'model': 'custom model', - 'manufacturer': 'From Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + enabled_by_default: true, + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: 'test', + model: 'custom model', + manufacturer: 'From Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - 'expire_after': 90, - 'icon': 'mdi:test', + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + expire_after: 90, + icon: 'mdi:test', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '%', - 'device_class': 'humidity', - 'state_class': 'measurement', - 'value_template': '{{ value_json.humidity }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'custom model', - 'manufacturer': 'Not from Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'humidity', + state_class: 'measurement', + value_template: '{{ value_json.humidity }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'custom model', + manufacturer: 'Not from Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'origin': origin, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - 'expire_after': 30, - 'icon': 'mdi:test', - 'object_id': 'weather_sensor_humidity' + origin: origin, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + expire_after: 30, + icon: 'mdi:test', + object_id: 'weather_sensor_humidity', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -680,11 +662,11 @@ describe('HomeAssistant extension', () => { it('Should discover devices with overridden name', async () => { settings.set(['devices', '0x0017880104e45522'], { homeassistant: { - name: "Weather Sensor", + name: 'Weather Sensor', }, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); @@ -692,60 +674,60 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'Weather Sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'Weather Sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - 'enabled_by_default': true, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + enabled_by_default: true, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'unit_of_measurement': '%', - 'device_class': 'humidity', - 'state_class': 'measurement', - 'value_template': '{{ value_json.humidity }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_humidity', - 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'Weather Sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '%', + device_class: 'humidity', + state_class: 'measurement', + value_template: '{{ value_json.humidity }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_humidity', + unique_id: '0x0017880104e45522_humidity_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'Weather Sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -756,14 +738,14 @@ describe('HomeAssistant extension', () => { homeassistant: { switch: { type: 'light', - object_id: 'light' + object_id: 'light', }, light: { type: 'this should be ignored', - name: 'my_light_name_override' + name: 'my_light_name_override', }, }, - }) + }); await resetExtension(); @@ -771,36 +753,33 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - "command_topic": "zigbee2mqtt/my_switch/set", - "device": { - "identifiers": [ - "zigbee2mqtt_0x0017880104e45541" - ], - "manufacturer": "Aqara", - "model": "Smart wall switch (no neutral, single rocker) (QBKG04LM)", - "name": "my_switch", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/my_switch/set', + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45541'], + manufacturer: 'Aqara', + model: 'Smart wall switch (no neutral, single rocker) (QBKG04LM)', + name: 'my_switch', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic": "zigbee2mqtt/my_switch", - "name": "my_light_name_override", - "payload_off": "OFF", - "payload_on": "ON", - "state_topic": "zigbee2mqtt/my_switch", - "object_id": "my_switch", - "unique_id": "0x0017880104e45541_light_zigbee2mqtt", - 'origin': origin, - "value_template": "{{ value_json.state }}" - } + json_attributes_topic: 'zigbee2mqtt/my_switch', + name: 'my_light_name_override', + payload_off: 'OFF', + payload_on: 'ON', + state_topic: 'zigbee2mqtt/my_switch', + object_id: 'my_switch', + unique_id: '0x0017880104e45541_light_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.state }}', + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x0017880104e45541/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); - }); it('Shouldnt discover devices when homeassistant null is set in device options', async () => { @@ -808,14 +787,14 @@ describe('HomeAssistant extension', () => { homeassistant: null, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); await flushPromises(); const topics = MQTT.publish.mock.calls.map((c) => c[0]); - expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config') - expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/temperature/config') + expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config'); + expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/temperature/config'); }); it('Shouldnt discover sensor when set to null', async () => { @@ -824,56 +803,52 @@ describe('HomeAssistant extension', () => { homeassistant: {humidity: null}, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); const topics = MQTT.publish.mock.calls.map((c) => c[0]); - expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config') - expect(topics).toContain('homeassistant/sensor/0x0017880104e45522/temperature/config') + expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config'); + expect(topics).toContain('homeassistant/sensor/0x0017880104e45522/temperature/config'); }); it('Should discover devices with fan', async () => { let payload; payload = { - "state_topic":"zigbee2mqtt/fan", - "state_value_template":"{{ value_json.fan_state }}", - "command_topic":"zigbee2mqtt/fan/set/fan_state", - "percentage_state_topic":"zigbee2mqtt/fan", - "percentage_command_topic":"zigbee2mqtt/fan/set/fan_mode", - "percentage_value_template":"{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json.fan_mode] | default('None') }}", - "percentage_command_template":"{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}", - "preset_mode_state_topic":"zigbee2mqtt/fan", - "preset_mode_command_topic":"zigbee2mqtt/fan/set/fan_mode", - "preset_mode_value_template":"{{ value_json.fan_mode if value_json.fan_mode in ['smart'] else 'None' | default('None') }}", - "preset_modes":[ - "smart" - ], - "speed_range_min":1, - "speed_range_max":4, - "json_attributes_topic":"zigbee2mqtt/fan", - "name":null, - "object_id": "fan", - "unique_id":"0x0017880104e45548_fan_zigbee2mqtt", - 'origin': origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45548" - ], - "name":"fan", - "sw_version": null, - "model":"Universal wink enabled white ceiling fan premier remote control (99432)", - "manufacturer":"Hampton Bay", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + state_topic: 'zigbee2mqtt/fan', + state_value_template: '{{ value_json.fan_state }}', + command_topic: 'zigbee2mqtt/fan/set/fan_state', + percentage_state_topic: 'zigbee2mqtt/fan', + percentage_command_topic: 'zigbee2mqtt/fan/set/fan_mode', + percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json.fan_mode] | default('None') }}", + percentage_command_template: "{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}", + preset_mode_state_topic: 'zigbee2mqtt/fan', + preset_mode_command_topic: 'zigbee2mqtt/fan/set/fan_mode', + preset_mode_value_template: "{{ value_json.fan_mode if value_json.fan_mode in ['smart'] else 'None' | default('None') }}", + preset_modes: ['smart'], + speed_range_min: 1, + speed_range_max: 4, + json_attributes_topic: 'zigbee2mqtt/fan', + name: null, + object_id: 'fan', + unique_id: '0x0017880104e45548_fan_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45548'], + name: 'fan', + sw_version: null, + model: 'Universal wink enabled white ceiling fan premier remote control (99432)', + manufacturer: 'Hampton Bay', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - }; + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/fan/0x0017880104e45548/fan/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -882,63 +857,50 @@ describe('HomeAssistant extension', () => { let payload; payload = { - "action_template":"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}", - "action_topic":"zigbee2mqtt/TS0601_thermostat", - "availability":[ + action_template: + "{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}", + action_topic: 'zigbee2mqtt/TS0601_thermostat', + availability: [ { - "topic":"zigbee2mqtt/bridge/state" - } + topic: 'zigbee2mqtt/bridge/state', + }, ], - "current_temperature_template":"{{ value_json.local_temperature }}", - "current_temperature_topic":"zigbee2mqtt/TS0601_thermostat", - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017882104a44559" - ], - "manufacturer":"Tuya", - "model":"Radiator valve with thermostat (TS0601_thermostat)", - "name":"TS0601_thermostat", - "sw_version":null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + current_temperature_template: '{{ value_json.local_temperature }}', + current_temperature_topic: 'zigbee2mqtt/TS0601_thermostat', + device: { + identifiers: ['zigbee2mqtt_0x0017882104a44559'], + manufacturer: 'Tuya', + model: 'Radiator valve with thermostat (TS0601_thermostat)', + name: 'TS0601_thermostat', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "preset_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset", - "preset_modes":[ - "schedule", - "manual", - "boost", - "complex", - "comfort", - "eco", - "away" - ], - "preset_mode_value_template":"{{ value_json.preset }}", - "preset_mode_state_topic":"zigbee2mqtt/TS0601_thermostat", - "json_attributes_topic":"zigbee2mqtt/TS0601_thermostat", - "max_temp":"35", - "min_temp":"5", - "mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/system_mode", - "mode_state_template":"{{ value_json.system_mode }}", - "mode_state_topic":"zigbee2mqtt/TS0601_thermostat", - "modes":[ - "heat", - "auto", - "off" - ], - "name":null, - "temp_step":0.5, - "temperature_command_topic":"zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint", - "temperature_state_template":"{{ value_json.current_heating_setpoint }}", - "temperature_state_topic":"zigbee2mqtt/TS0601_thermostat", - "temperature_unit":"C", - "object_id": "ts0601_thermostat", - "unique_id":"0x0017882104a44559_climate_zigbee2mqtt", - 'origin': origin, + preset_mode_command_topic: 'zigbee2mqtt/TS0601_thermostat/set/preset', + preset_modes: ['schedule', 'manual', 'boost', 'complex', 'comfort', 'eco', 'away'], + preset_mode_value_template: '{{ value_json.preset }}', + preset_mode_state_topic: 'zigbee2mqtt/TS0601_thermostat', + json_attributes_topic: 'zigbee2mqtt/TS0601_thermostat', + max_temp: '35', + min_temp: '5', + mode_command_topic: 'zigbee2mqtt/TS0601_thermostat/set/system_mode', + mode_state_template: '{{ value_json.system_mode }}', + mode_state_topic: 'zigbee2mqtt/TS0601_thermostat', + modes: ['heat', 'auto', 'off'], + name: null, + temp_step: 0.5, + temperature_command_topic: 'zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint', + temperature_state_template: '{{ value_json.current_heating_setpoint }}', + temperature_state_topic: 'zigbee2mqtt/TS0601_thermostat', + temperature_unit: 'C', + object_id: 'ts0601_thermostat', + unique_id: '0x0017882104a44559_climate_zigbee2mqtt', + origin: origin, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/climate/0x0017882104a44559/climate/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -961,103 +923,102 @@ describe('HomeAssistant extension', () => { name: null, object_id: 'smart_vent', unique_id: '0x0017880104e45551_cover_zigbee2mqtt', - 'origin': origin, - device: - { - identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ], + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45551'], name: 'smart vent', sw_version: null, model: 'Smart vent (SV01)', manufacturer: 'Keen Home', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0x0017880104e45551/cover/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - "availability": [{ "topic": "zigbee2mqtt/bridge/state" }], - "command_topic": "zigbee2mqtt/zigfred_plus/l6/set", - "device": { - "identifiers": ["zigbee2mqtt_0xf4ce368a38be56a1"], - "manufacturer": "Siglis", - "model": "zigfred plus smart in-wall switch (ZFP-1A-CH)", - "name": "zigfred_plus", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/zigfred_plus/l6/set', + device: { + identifiers: ['zigbee2mqtt_0xf4ce368a38be56a1'], + manufacturer: 'Siglis', + model: 'zigfred plus smart in-wall switch (ZFP-1A-CH)', + name: 'zigfred_plus', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic": "zigbee2mqtt/zigfred_plus/l6", - "name": "L6", - "position_template": "{{ value_json.position }}", - "position_topic": "zigbee2mqtt/zigfred_plus/l6", - "set_position_template": "{ \"position_l6\": {{ position }} }", - "set_position_topic": "zigbee2mqtt/zigfred_plus/l6/set", - "state_stopped": 'STOP', - "state_closed": "CLOSE", - "state_open": "OPEN", - "state_topic": "zigbee2mqtt/zigfred_plus/l6", - "tilt_command_topic": "zigbee2mqtt/zigfred_plus/l6/set/tilt", - "tilt_status_template": "{{ value_json.tilt }}", - "tilt_status_topic": "zigbee2mqtt/zigfred_plus/l6", - "object_id": "zigfred_plus_l6", - "unique_id": "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt", - 'origin': origin, - "value_template": "{{ value_json.state }}" - } + json_attributes_topic: 'zigbee2mqtt/zigfred_plus/l6', + name: 'L6', + position_template: '{{ value_json.position }}', + position_topic: 'zigbee2mqtt/zigfred_plus/l6', + set_position_template: '{ "position_l6": {{ position }} }', + set_position_topic: 'zigbee2mqtt/zigfred_plus/l6/set', + state_stopped: 'STOP', + state_closed: 'CLOSE', + state_open: 'OPEN', + state_topic: 'zigbee2mqtt/zigfred_plus/l6', + tilt_command_topic: 'zigbee2mqtt/zigfred_plus/l6/set/tilt', + tilt_status_template: '{{ value_json.tilt }}', + tilt_status_topic: 'zigbee2mqtt/zigfred_plus/l6', + object_id: 'zigfred_plus_l6', + unique_id: '0xf4ce368a38be56a1_cover_l6_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.state }}', + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should discover devices with custom homeassistant_discovery_topic', async () => { - settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic') + settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic'); await resetExtension(); let payload; payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'enabled_by_default': true, - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + enabled_by_default: true, + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'my_custom_discovery_topic/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should throw error when starting with attributes output', async () => { - settings.set(['experimental', 'output'], 'attribute') - settings.set(['homeassistant'], true) + settings.set(['experimental', 'output'], 'attribute'); + settings.set(['homeassistant'], true); expect(() => { const controller = new Controller(false); }).toThrowError('Home Assistant integration is not possible with attribute output!'); @@ -1067,29 +1028,36 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'cache_state'], false); logger.warning.mockClear(); await resetExtension(); - expect(logger.warning).toHaveBeenCalledWith("In order for Home Assistant integration to work properly set `cache_state: true"); + expect(logger.warning).toHaveBeenCalledWith('In order for Home Assistant integration to work properly set `cache_state: true'); }); it('Should set missing values to null', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/6987 const device = zigbeeHerdsman.devices.WSDCGQ11LM; - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/weather_sensor', - stringify({"battery":null,"humidity":null,"linkquality":null,"pressure":null,"temperature":-0.85,"voltage":null}), - { retain: false, qos: 1 }, + stringify({battery: null, humidity: null, linkquality: null, pressure: null, temperature: -0.85, voltage: null}), + {retain: false, qos: 1}, expect.any(Function), ); }); it('Should copy hue/saturtion to h/s if present', async () => { const device = zigbeeHerdsman.devices.bulb_color; - const data = {currentHue: 0, currentSaturation: 254} + const data = {currentHue: 0, currentSaturation: 254}; const payload = {data, cluster: 'lightingColorCtrl', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload); @@ -1097,8 +1065,16 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: false, qos: 0 }, + stringify({ + color: {hue: 0, saturation: 100, h: 0, s: 100}, + color_mode: 'hs', + linkquality: null, + state: null, + update_available: null, + power_on_behavior: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: false, qos: 0}, expect.any(Function), ); }); @@ -1113,15 +1089,23 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: false, qos: 0 }, + stringify({ + color: {x: 0.4576, y: 0.41}, + color_mode: 'xy', + linkquality: null, + state: null, + update_available: null, + power_on_behavior: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: false, qos: 0}, expect.any(Function), ); }); it('Should not copy hue/saturtion if color is missing', async () => { const device = zigbeeHerdsman.devices.bulb_color; - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload); @@ -1129,16 +1113,29 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"linkquality": null,"state": "ON", "update_available": null, "power_on_behavior": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: false, qos: 0 }, + stringify({ + linkquality: null, + state: 'ON', + update_available: null, + power_on_behavior: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: false, qos: 0}, expect.any(Function), ); }); it('Shouldt discover when already discovered', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -1149,37 +1146,44 @@ describe('HomeAssistant extension', () => { it('Should discover when not discovered yet', async () => { extension.discovered = {}; const device = zigbeeHerdsman.devices.WSDCGQ11LM; - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); const payloadHA = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'enabled_by_default': true, - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + enabled_by_default: true, + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payloadHA), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -1197,7 +1201,7 @@ describe('HomeAssistant extension', () => { const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); resetDiscoveryPayloads(device.ieeeAddr); MQTT.publish.mockClear(); - controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {'test': 123}}); + controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {test: 123}}); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( `homeassistant/light/${device.ID}/light/config`, @@ -1220,22 +1224,34 @@ describe('HomeAssistant extension', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"state":"ON","color_options":null,"brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + state: 'ON', + color_options: null, + brightness: 50, + color_temp: 370, + linkquality: 99, + power_on_behavior: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', - stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: true, qos: 0 }, - expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/group_1', - stringify({"state":'ON'}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({ + action: null, + action_duration: null, + battery: null, + brightness: 255, + linkquality: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: true, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should send all status when home assistant comes online', async () => { @@ -1250,15 +1266,32 @@ describe('HomeAssistant extension', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"state":"ON","color_options":null,"brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + state: 'ON', + color_options: null, + brightness: 50, + color_temp: 370, + linkquality: 99, + power_on_behavior: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', - stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + action: null, + action_duration: null, + battery: null, + brightness: 255, + linkquality: null, + update_available: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -1288,38 +1321,38 @@ describe('HomeAssistant extension', () => { }); it('Should discover devices with availability', async () => { - settings.set(['availability'], true) + settings.set(['availability'], true); await resetExtension(); let payload; payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'enabled_by_default': true, - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + enabled_by_default: true, + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability_mode': 'all', - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}], + availability_mode: 'all', + availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -1332,241 +1365,246 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should refresh discovery when device is renamed', async () => { - await MQTT.events.message('homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({topic: 'zigbee2mqtt/weather_sensor/action'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x0017880104e45522/action_double/config', + stringify({topic: 'zigbee2mqtt/weather_sensor/action'}), + ); await flushPromises(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({"from": "weather_sensor", "to": "weather_sensor_renamed","homeassistant_rename":true})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/rename', + stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: true}), + ); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); const payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'enabled_by_default': true, - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor_renamed', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor_renamed', - 'object_id': 'weather_sensor_renamed_temperature', - 'origin': origin, - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor_renamed', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + enabled_by_default: true, + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor_renamed', + json_attributes_topic: 'zigbee2mqtt/weather_sensor_renamed', + object_id: 'weather_sensor_renamed_temperature', + origin: origin, + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor_renamed', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({ - "automation_type":"trigger", - "type":"action", - "subtype":"double", - "payload":"double", - "topic":"zigbee2mqtt/weather_sensor_renamed/action", - 'origin': origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45522" - ], - "name":"weather_sensor_renamed", - "sw_version": null, - "model":"Temperature and humidity sensor (WSDCGQ11LM)", - "manufacturer":"Aqara", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", - } + automation_type: 'trigger', + type: 'action', + subtype: 'double', + payload: 'double', + topic: 'zigbee2mqtt/weather_sensor_renamed/action', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor_renamed', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', + }, }), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should refresh discovery when group is renamed', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({"from": "ha_discovery_group", "to": "ha_discovery_group_new","homeassistant_rename":true})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/group/rename', + stringify({from: 'ha_discovery_group', to: 'ha_discovery_group_new', homeassistant_rename: true}), + ); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); const payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "brightness":true, - "brightness_scale":254, - "command_topic":"zigbee2mqtt/ha_discovery_group_new/set", - "device":{ - "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], - "name":"ha_discovery_group_new", - "sw_version": version, - "model": "Group", - "manufacturer": "Zigbee2MQTT", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + brightness: true, + brightness_scale: 254, + command_topic: 'zigbee2mqtt/ha_discovery_group_new/set', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group_new', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic":"zigbee2mqtt/ha_discovery_group_new", - "max_mireds": 454, - "min_mireds": 250, - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/ha_discovery_group_new", - "supported_color_modes":[ - "xy", - "color_temp" + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group_new', + max_mireds: 454, + min_mireds: 250, + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/ha_discovery_group_new', + supported_color_modes: ['xy', 'color_temp'], + effect: true, + effect_list: [ + 'blink', + 'breathe', + 'okay', + 'channel_change', + 'candle', + 'fireplace', + 'colorloop', + 'finish_effect', + 'stop_effect', + 'stop_hue_effect', ], - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "candle", - "fireplace", - "colorloop", - "finish_effect", - "stop_effect", - "stop_hue_effect", - ], - "object_id":"ha_discovery_group_new", - "unique_id":"9_light_zigbee2mqtt", - 'origin': origin, - }; + object_id: 'ha_discovery_group_new', + unique_id: '9_light_zigbee2mqtt', + origin: origin, + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Shouldnt refresh discovery when device is renamed and homeassistant_rename is false', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({"from": "weather_sensor", "to": "weather_sensor_renamed","homeassistant_rename":false})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/rename', + stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: false}), + ); await flushPromises(); expect(MQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); const payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'enabled_by_default': true, - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor_renamed', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor_renamed', - 'object_id': 'weather_sensor_renamed_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor_renamed', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + enabled_by_default: true, + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor_renamed', + json_attributes_topic: 'zigbee2mqtt/weather_sensor_renamed', + object_id: 'weather_sensor_renamed_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor_renamed', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should discover update_available sensor when device supports it', async () => { const payload = { - "payload_on":true, - "payload_off":false, - "value_template":`{{ value_json['update']['state'] == "available" }}`, - "enabled_by_default": false, - "state_topic":"zigbee2mqtt/bulb", - "json_attributes_topic":"zigbee2mqtt/bulb", - "name":null, - "object_id":"bulb_update_available", - "unique_id":"0x000b57fffec6a5b2_update_available_zigbee2mqtt", - 'origin': origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x000b57fffec6a5b2" - ], - "name":"bulb", - 'sw_version': null, - "model":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)", - "manufacturer":"IKEA", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + payload_on: true, + payload_off: false, + value_template: `{{ value_json['update']['state'] == "available" }}`, + enabled_by_default: false, + state_topic: 'zigbee2mqtt/bulb', + json_attributes_topic: 'zigbee2mqtt/bulb', + name: null, + object_id: 'bulb_update_available', + unique_id: '0x000b57fffec6a5b2_update_available_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x000b57fffec6a5b2'], + name: 'bulb', + sw_version: null, + model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', + manufacturer: 'IKEA', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], - 'device_class': 'update', - 'entity_category': 'diagnostic' + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + device_class: 'update', + entity_category: 'diagnostic', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -1585,89 +1623,83 @@ describe('HomeAssistant extension', () => { await flushPromises(); const discoverPayloadAction = { - "automation_type":"trigger", - "type":"action", - "subtype":"single", - "payload":"single", - "topic":"zigbee2mqtt/button/action", - "origin": origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45520" - ], - "name":"button", - "sw_version": null, - "model":"Wireless mini switch (WXKG11LM)", - "manufacturer":"Aqara", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", - } + automation_type: 'trigger', + type: 'action', + subtype: 'single', + payload: 'single', + topic: 'zigbee2mqtt/button/action', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45520'], + name: 'button', + sw_version: null, + model: 'Wireless mini switch (WXKG11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', + }, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); const discoverPayloadClick = { - "automation_type":"trigger", - "type":"click", - "subtype":"single", - "payload":"single", - "topic":"zigbee2mqtt/button/click", - "origin": origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45520" - ], - "name":"button", - "sw_version": null, - "model":"Wireless mini switch (WXKG11LM)", - "manufacturer":"Aqara", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", - } + automation_type: 'trigger', + type: 'click', + subtype: 'single', + payload: 'single', + topic: 'zigbee2mqtt/button/click', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45520'], + name: 'button', + sw_version: null, + model: 'Wireless mini switch (WXKG11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', + }, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button/action', - 'single', - { retain: false, qos: 0 }, - expect.any(Function), - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button/click', - 'single', - { retain: false, qos: 0 }, + 'zigbee2mqtt/button', + stringify({ + action: 'single', + click: 'single', + battery: null, + linkquality: null, + voltage: null, + power_outage_count: null, + device_temperature: null, + }), + {retain: false, qos: 0}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', - stringify({action: "single", click: "single", battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), - { retain: false, qos: 0 }, + stringify({action: '', battery: null, linkquality: null, voltage: null, click: null, power_outage_count: null, device_temperature: null}), + {retain: false, qos: 0}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', - stringify({action: "", battery: null, linkquality: null, voltage: null, click: null, power_outage_count: null, device_temperature: null}), - { retain: false, qos: 0 }, - expect.any(Function), - ); - - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button', - stringify({click: "", action: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), - { retain: false, qos: 0 }, + stringify({click: '', action: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), + {retain: false, qos: 0}, expect.any(Function), ); @@ -1678,56 +1710,65 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); expect(MQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button/action', - 'single', - { retain: false, qos: 0 }, - expect.any(Function), - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button/click', - 'single', - { retain: false, qos: 0 }, - expect.any(Function), - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); // Shouldn't rediscover when already discovered in previous session clearDiscoveredTrigger('0x0017880104e45520'); - await MQTT.events.message('homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'})); - await MQTT.events.message('homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x0017880104e45520/action_double/config', + stringify({topic: 'zigbee2mqtt/button/action'}), + ); + await MQTT.events.message( + 'homeassistant/device_automation/0x0017880104e45520/action_double/config', + stringify({topic: 'zigbee2mqtt/button/action'}), + ); await flushPromises(); MQTT.publish.mockClear(); - const payload2 = {data: {'32768': 2}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const payload2 = {data: {32768: 2}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload2); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).not.toHaveBeenCalledWith( + 'homeassistant/device_automation/0x0017880104e45520/action_double/config', + expect.any(String), + expect.any(Object), + expect.any(Function), + ); // Should rediscover when already discovered in previous session but with different name clearDiscoveredTrigger('0x0017880104e45520'); - await MQTT.events.message('homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button_other_name/action'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x0017880104e45520/action_double/config', + stringify({topic: 'zigbee2mqtt/button_other_name/action'}), + ); await flushPromises(); MQTT.publish.mockClear(); await zigbeeHerdsman.events.message(payload2); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/device_automation/0x0017880104e45520/action_double/config', + expect.any(String), + expect.any(Object), + expect.any(Function), + ); }); it('Should not discover device_automation when disabled', async () => { settings.set(['device_options'], { homeassistant: {device_automation: null}, - }) + }); await resetExtension(); MQTT.publish.mockClear(); @@ -1749,7 +1790,7 @@ describe('HomeAssistant extension', () => { legacy: false, friendly_name: 'weather_sensor', retain: false, - }) + }); await resetExtension(); const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); @@ -1777,42 +1818,35 @@ describe('HomeAssistant extension', () => { await flushPromises(); const discoverPayload = { - "automation_type":"trigger", - "type":"action", - "subtype":"single", - "payload":"single", - "topic":"zigbee2mqtt/button/action", - "origin": origin, - "device":{ - "identifiers":[ - "zigbee2mqtt_0x0017880104e45520" - ], - "name":"button", - "sw_version": null, - "model":"Wireless mini switch (WXKG11LM)", - "manufacturer":"Aqara", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", - } + automation_type: 'trigger', + type: 'action', + subtype: 'single', + payload: 'single', + topic: 'zigbee2mqtt/button/action', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45520'], + name: 'button', + sw_version: null, + model: 'Wireless mini switch (WXKG11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', + }, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/button/action', - 'single', - { retain: false, qos: 0 }, - expect.any(Function), - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', - stringify({action: "single", "battery":null,"linkquality":null,"voltage":null, "power_outage_count": null, "device_temperature": null}), - { retain: false, qos: 0 }, + stringify({action: 'single', battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), + {retain: false, qos: 0}, expect.any(Function), ); @@ -1824,9 +1858,24 @@ describe('HomeAssistant extension', () => { await MQTT.events.message('zigbee2mqtt/U202DST600ZB/l2/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB', stringify({state_l2:"ON", brightness_l2:20, linkquality: null, state_l1: null, power_on_behavior: null}), {"qos": 0, "retain": false}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l2', stringify({state:"ON", brightness:20}), {"qos": 0, "retain": false}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l1', stringify({state: null}), {"qos": 0, "retain": false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/U202DST600ZB', + stringify({state_l2: 'ON', brightness_l2: 20, linkquality: null, state_l1: null, power_on_behavior: null}), + {qos: 0, retain: false}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/U202DST600ZB/l2', + stringify({state: 'ON', brightness: 20}), + {qos: 0, retain: false}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/U202DST600ZB/l1', + stringify({state: null}), + {qos: 0, retain: false}, + expect.any(Function), + ); }); it('Shouldnt crash in onPublishEntityState on group publish', async () => { @@ -1842,17 +1891,33 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(4); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', click: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + action: 'single', + click: null, + battery: null, + linkquality: null, + voltage: null, + power_outage_count: null, + device_temperature: null, + }); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', click: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + action: '', + click: null, + battery: null, + linkquality: null, + voltage: null, + power_outage_count: null, + device_temperature: null, + }); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config'); expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); }); @@ -1860,14 +1925,25 @@ describe('HomeAssistant extension', () => { it('Should clear outdated configs', async () => { // Non-existing group -> clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/light/1221051039810110150109113116116_91231/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/light/1221051039810110150109113116116_91231/light/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/1221051039810110150109113116116_91231/light/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/light/1221051039810110150109113116116_91231/light/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); // Existing group -> dont clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/light/1221051039810110150109113116116_9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/light/1221051039810110150109113116116_9/light/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); @@ -1880,36 +1956,63 @@ describe('HomeAssistant extension', () => { // Existing group, non existing config -> clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/light/1221051039810110150109113116116_9/switch/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/light/1221051039810110150109113116116_9/switch/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/1221051039810110150109113116116_9/switch/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/light/1221051039810110150109113116116_9/switch/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); // Non-existing device -> clear MQTT.publish.mockClear(); await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x123/temperature/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); // Existing device -> don't clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); // Non-existing device of different instance -> don't clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/sensor/0x123/temperature/config', + stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); // Existing device but non-existing config -> don't clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); // Non-existing device but invalid payload -> clear MQTT.publish.mockClear(); @@ -1919,13 +2022,19 @@ describe('HomeAssistant extension', () => { // Existing device, device automation -> don't clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', + stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); // Device automation of different instance -> don't clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/device_automation/0x000b57fffec6a5b2_not_existing/action_button_3_single/config', stringify({topic: 'zigbee2mqtt_different/0x000b57fffec6a5b2_not_existing/availability'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x000b57fffec6a5b2_not_existing/action_button_3_single/config', + stringify({topic: 'zigbee2mqtt_different/0x000b57fffec6a5b2_not_existing/availability'}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); @@ -1934,13 +2043,29 @@ describe('HomeAssistant extension', () => { await resetExtension(); MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'})); + await MQTT.events.message( + 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', + stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), + ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', null, {qos: 1, retain: true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', + null, + {qos: 1, retain: true}, + expect.any(Function), + ); }); it('Should not have Home Assistant legacy entity attributes when disabled', async () => { @@ -1951,30 +2076,30 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'value_template': '{{ value_json.temperature }}', - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'enabled_by_default': true, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae' + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + enabled_by_default: true, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -1982,54 +2107,54 @@ describe('HomeAssistant extension', () => { it('Should rediscover group when device is added to it', async () => { resetDiscoveryPayloads(9); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'ha_discovery_group', device: 'wall_switch_double/left'})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'ha_discovery_group', device: 'wall_switch_double/left'}), + ); await flushPromises(); const payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state"}], - "brightness":true, - "brightness_scale":254, - "command_topic":"zigbee2mqtt/ha_discovery_group/set", - "device":{ - "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], - "name":"ha_discovery_group", - "sw_version": version, - "model": "Group", - "manufacturer": "Zigbee2MQTT", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + brightness: true, + brightness_scale: 254, + command_topic: 'zigbee2mqtt/ha_discovery_group/set', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "json_attributes_topic":"zigbee2mqtt/ha_discovery_group", - "max_mireds": 454, - "min_mireds": 250, - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/ha_discovery_group", - "supported_color_modes":[ - "xy", - "color_temp" + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', + max_mireds: 454, + min_mireds: 250, + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/ha_discovery_group', + supported_color_modes: ['xy', 'color_temp'], + effect: true, + effect_list: [ + 'blink', + 'breathe', + 'okay', + 'channel_change', + 'candle', + 'fireplace', + 'colorloop', + 'finish_effect', + 'stop_effect', + 'stop_hue_effect', ], - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "candle", - "fireplace", - "colorloop", - "finish_effect", - "stop_effect", - "stop_hue_effect", - ], - "object_id":"ha_discovery_group", - "unique_id":"9_light_zigbee2mqtt", - 'origin': origin, - }; + object_id: 'ha_discovery_group', + unique_id: '9_light_zigbee2mqtt', + origin: origin, + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2039,50 +2164,47 @@ describe('HomeAssistant extension', () => { await resetExtension(); const payload = { - "availability":[{"topic":"zigbee2mqtt/bridge/state","value_template":'{{ value_json.state }}'}], - "brightness":true, - "brightness_scale":254, - "command_topic":"zigbee2mqtt/ha_discovery_group/set", - "device":{ - "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], - "name":"ha_discovery_group", - "sw_version": version, - "model": "Group", - "manufacturer": "Zigbee2MQTT", - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], + brightness: true, + brightness_scale: 254, + command_topic: 'zigbee2mqtt/ha_discovery_group/set', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "max_mireds": 454, - "min_mireds": 250, - "json_attributes_topic":"zigbee2mqtt/ha_discovery_group", - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/ha_discovery_group", - "supported_color_modes":[ - "xy", - "color_temp" + max_mireds: 454, + min_mireds: 250, + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/ha_discovery_group', + supported_color_modes: ['xy', 'color_temp'], + effect: true, + effect_list: [ + 'blink', + 'breathe', + 'okay', + 'channel_change', + 'candle', + 'fireplace', + 'colorloop', + 'finish_effect', + 'stop_effect', + 'stop_hue_effect', ], - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "candle", - "fireplace", - "colorloop", - "finish_effect", - "stop_effect", - "stop_hue_effect", - ], - "object_id":"ha_discovery_group", - "unique_id":"9_light_zigbee2mqtt", - 'origin': origin, + object_id: 'ha_discovery_group', + unique_id: '9_light_zigbee2mqtt', + origin: origin, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2093,52 +2215,41 @@ describe('HomeAssistant extension', () => { await resetExtension(); const payload = { - "availability":[ - { - "topic":"zigbee2mqtt/bridge/state", - "value_template": `{{ "offline" }}`, - } + availability: [ + { + topic: 'zigbee2mqtt/bridge/state', + value_template: `{{ "offline" }}`, + }, ], - "brightness":true, - "brightness_scale":254, - "command_topic":"zigbee2mqtt/bulb/set", - "device":{ - "identifiers":[ - "zigbee2mqtt_0x000b57fffec6a5b2" - ], - "manufacturer":"IKEA", - "model":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)", - "name":"bulb", - "sw_version":null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + brightness: true, + brightness_scale: 254, + command_topic: 'zigbee2mqtt/bulb/set', + device: { + identifiers: ['zigbee2mqtt_0x000b57fffec6a5b2'], + manufacturer: 'IKEA', + model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', + name: 'bulb', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "effect":true, - "effect_list":[ - "blink", - "breathe", - "okay", - "channel_change", - "finish_effect", - "stop_effect" - ], - "json_attributes_topic":"zigbee2mqtt/bulb", - "max_mireds":454, - "min_mireds":250, - "name":null, - "schema":"json", - "state_topic":"zigbee2mqtt/bulb", - "supported_color_modes":[ - "color_temp" - ], - "object_id":"bulb", - "unique_id":"0x000b57fffec6a5b2_light_zigbee2mqtt", - 'origin': origin, + effect: true, + effect_list: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], + json_attributes_topic: 'zigbee2mqtt/bulb', + max_mireds: 454, + min_mireds: 250, + name: null, + schema: 'json', + state_topic: 'zigbee2mqtt/bulb', + supported_color_modes: ['color_temp'], + object_id: 'bulb', + unique_id: '0x000b57fffec6a5b2_light_zigbee2mqtt', + origin: origin, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2148,38 +2259,36 @@ describe('HomeAssistant extension', () => { await resetExtension(); const payload = { - "availability":[ - { - "topic":"zigbee2mqtt/bridge/state" - } + availability: [ + { + topic: 'zigbee2mqtt/bridge/state', + }, ], - "device":{ - "identifiers":[ - "zigbee2mqtt_0x000b57fffec6a5b2" - ], - "manufacturer":"IKEA", - "model":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)", - "name":"bulb", - "sw_version": null, - "via_device": "zigbee2mqtt_bridge_0x00124b00120144ae", + device: { + identifiers: ['zigbee2mqtt_0x000b57fffec6a5b2'], + manufacturer: 'IKEA', + model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', + name: 'bulb', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - "enabled_by_default":false, - "icon":"mdi:clock", - "json_attributes_topic":"zigbee2mqtt/bulb", - "name":"Last seen", - "state_topic":"zigbee2mqtt/bulb", - "object_id": "bulb_last_seen", - "unique_id":"0x000b57fffec6a5b2_last_seen_zigbee2mqtt", - 'origin': origin, - "value_template":"{{ value_json.last_seen }}", - "device_class": "timestamp", - "entity_category": "diagnostic" - }; + enabled_by_default: false, + icon: 'mdi:clock', + json_attributes_topic: 'zigbee2mqtt/bulb', + name: 'Last seen', + state_topic: 'zigbee2mqtt/bulb', + object_id: 'bulb_last_seen', + unique_id: '0x000b57fffec6a5b2_last_seen_zigbee2mqtt', + origin: origin, + value_template: '{{ value_json.last_seen }}', + device_class: 'timestamp', + entity_category: 'diagnostic', + }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/last_seen/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2193,32 +2302,32 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'unit_of_measurement': '°C', - 'device_class': 'temperature', - 'state_class': 'measurement', - 'enabled_by_default': true, - 'value_template': "{{ value_json.temperature }}", - 'state_topic': 'zigbee2mqtt/weather_sensor', - 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', - 'object_id': 'weather_sensor_temperature', - 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', - 'origin': origin, - 'device': { - 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], - 'name': 'weather_sensor', - 'sw_version': null, - 'model': 'Temperature and humidity sensor (WSDCGQ11LM)', - 'manufacturer': 'Aqara', - 'configuration_url': 'http://zigbee.mqtt/#/device/0x0017880104e45522/info', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + unit_of_measurement: '°C', + device_class: 'temperature', + state_class: 'measurement', + enabled_by_default: true, + value_template: '{{ value_json.temperature }}', + state_topic: 'zigbee2mqtt/weather_sensor', + json_attributes_topic: 'zigbee2mqtt/weather_sensor', + object_id: 'weather_sensor_temperature', + unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', + origin: origin, + device: { + identifiers: ['zigbee2mqtt_0x0017880104e45522'], + name: 'weather_sensor', + sw_version: null, + model: 'Temperature and humidity sensor (WSDCGQ11LM)', + manufacturer: 'Aqara', + configuration_url: 'http://zigbee.mqtt/#/device/0x0017880104e45522/info', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state'}], }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2243,30 +2352,30 @@ describe('HomeAssistant extension', () => { await flushPromises(); let payload = { - 'name': 'Chill scene', - 'command_topic': 'zigbee2mqtt/bulb_color_2/set', - 'payload_on': '{ "scene_recall": 1 }', - 'json_attributes_topic': 'zigbee2mqtt/bulb_color_2', - 'object_id': 'bulb_color_2_1_chill_scene', - 'unique_id': '0x000b57fffec6a5b4_scene_1_zigbee2mqtt', - 'device': { - 'identifiers': [ 'zigbee2mqtt_0x000b57fffec6a5b4' ], - 'name': 'bulb_color_2', - 'sw_version': '5.127.1.26581', - 'model': 'Hue Go (7146060PH)', - 'manufacturer': 'Philips', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae', + name: 'Chill scene', + command_topic: 'zigbee2mqtt/bulb_color_2/set', + payload_on: '{ "scene_recall": 1 }', + json_attributes_topic: 'zigbee2mqtt/bulb_color_2', + object_id: 'bulb_color_2_1_chill_scene', + unique_id: '0x000b57fffec6a5b4_scene_1_zigbee2mqtt', + device: { + identifiers: ['zigbee2mqtt_0x000b57fffec6a5b4'], + name: 'bulb_color_2', + sw_version: '5.127.1.26581', + model: 'Hue Go (7146060PH)', + manufacturer: 'Philips', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'origin': origin, - 'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ] - } + origin: origin, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + }; expect(MQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes( 12 ); + expect(MQTT.publish).toHaveBeenCalledTimes(12); // Group scenes. const group = controller.zigbee.resolveEntity('ha_discovery_group'); @@ -2287,30 +2396,30 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - 'name': 'Scene 4', - 'command_topic': 'zigbee2mqtt/ha_discovery_group/set', - 'payload_on': '{ "scene_recall": 4 }', - 'json_attributes_topic':'zigbee2mqtt/ha_discovery_group', - 'object_id': 'ha_discovery_group_4_scene_4', - 'unique_id': '9_scene_4_zigbee2mqtt', - 'device': { - 'identifiers': [ 'zigbee2mqtt_1221051039810110150109113116116_9' ], - 'name': 'ha_discovery_group', - 'sw_version': version, - 'model': 'Group', - 'manufacturer': 'Zigbee2MQTT', - 'via_device': 'zigbee2mqtt_bridge_0x00124b00120144ae' + name: 'Scene 4', + command_topic: 'zigbee2mqtt/ha_discovery_group/set', + payload_on: '{ "scene_recall": 4 }', + json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', + object_id: 'ha_discovery_group_4_scene_4', + unique_id: '9_scene_4_zigbee2mqtt', + device: { + identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], + name: 'ha_discovery_group', + sw_version: version, + model: 'Group', + manufacturer: 'Zigbee2MQTT', + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'origin': origin, - 'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ] - } + origin: origin, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + }; expect(MQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes( 6 ); + expect(MQTT.publish).toHaveBeenCalledTimes(6); }); it('Should not clear bridge entities unnecessarily', async () => { @@ -2318,23 +2427,23 @@ describe('HomeAssistant extension', () => { const topic = 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config'; const payload = { - 'name': 'Restart', - 'object_id': 'zigbee2mqtt_bridge_restart', - 'unique_id': 'bridge_0x00124b00120144ae_restart_zigbee2mqtt', - 'device_class': 'restart', - 'command_topic': 'zigbee2mqtt/bridge/request/restart', - 'payload_press': '', - 'origin': origin, - 'device': { - 'name': 'Zigbee2MQTT Bridge', - 'identifiers': ['zigbee2mqtt_bridge_0x00124b00120144ae'], - 'manufacturer': 'Zigbee2MQTT', - 'model': 'Bridge', - 'hw_version': 'z-Stack 20190425', - 'sw_version': z2m_version, + name: 'Restart', + object_id: 'zigbee2mqtt_bridge_restart', + unique_id: 'bridge_0x00124b00120144ae_restart_zigbee2mqtt', + device_class: 'restart', + command_topic: 'zigbee2mqtt/bridge/request/restart', + payload_press: '', + origin: origin, + device: { + name: 'Zigbee2MQTT Bridge', + identifiers: ['zigbee2mqtt_bridge_0x00124b00120144ae'], + manufacturer: 'Zigbee2MQTT', + model: 'Bridge', + hw_version: 'z-Stack 20190425', + sw_version: z2m_version, }, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; controller.eventBus.emitMQTTMessage({ @@ -2343,12 +2452,7 @@ describe('HomeAssistant extension', () => { }); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( - topic, - null, - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(MQTT.publish).not.toHaveBeenCalledWith(topic, null, {retain: true, qos: 1}, expect.any(Function)); }); it('Should discover bridge entities', async () => { @@ -2356,221 +2460,223 @@ describe('HomeAssistant extension', () => { await resetExtension(); const devicePayload = { - 'name': 'Zigbee2MQTT Bridge', - 'identifiers': ['zigbee2mqtt_bridge_0x00124b00120144ae'], - 'manufacturer': 'Zigbee2MQTT', - 'model': 'Bridge', - 'hw_version': 'z-Stack 20190425', - 'sw_version': z2m_version, + name: 'Zigbee2MQTT Bridge', + identifiers: ['zigbee2mqtt_bridge_0x00124b00120144ae'], + manufacturer: 'Zigbee2MQTT', + model: 'Bridge', + hw_version: 'z-Stack 20190425', + sw_version: z2m_version, }; // Binary sensors. let payload; payload = { - 'name': 'Connection state', - 'object_id': 'zigbee2mqtt_bridge_connection_state', - 'entity_category': 'diagnostic', - 'device_class': 'connectivity', - 'unique_id': 'bridge_0x00124b00120144ae_connection_state_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/state', - 'value_template': '{{ value }}', - 'payload_on': 'online', - 'payload_off': 'offline', - 'origin': origin, - 'device': devicePayload, + name: 'Connection state', + object_id: 'zigbee2mqtt_bridge_connection_state', + entity_category: 'diagnostic', + device_class: 'connectivity', + unique_id: 'bridge_0x00124b00120144ae_connection_state_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/state', + value_template: '{{ value }}', + payload_on: 'online', + payload_off: 'offline', + origin: origin, + device: devicePayload, }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/connection_state/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'name': 'Restart required', - 'object_id': 'zigbee2mqtt_bridge_restart_required', - 'entity_category': 'diagnostic', - 'device_class': 'problem', - 'enabled_by_default': false, - 'unique_id': 'bridge_0x00124b00120144ae_restart_required_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ value_json.restart_required }}', - 'payload_on': true, - 'payload_off': false, - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Restart required', + object_id: 'zigbee2mqtt_bridge_restart_required', + entity_category: 'diagnostic', + device_class: 'problem', + enabled_by_default: false, + unique_id: 'bridge_0x00124b00120144ae_restart_required_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ value_json.restart_required }}', + payload_on: true, + payload_off: false, + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/restart_required/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); // Buttons. payload = { - 'name': 'Restart', - 'object_id': 'zigbee2mqtt_bridge_restart', - 'unique_id': 'bridge_0x00124b00120144ae_restart_zigbee2mqtt', - 'device_class': 'restart', - 'command_topic': 'zigbee2mqtt/bridge/request/restart', - 'payload_press': '', - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Restart', + object_id: 'zigbee2mqtt_bridge_restart', + unique_id: 'bridge_0x00124b00120144ae_restart_zigbee2mqtt', + device_class: 'restart', + command_topic: 'zigbee2mqtt/bridge/request/restart', + payload_press: '', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); // Selects. payload = { - 'name': 'Log level', - 'object_id': 'zigbee2mqtt_bridge_log_level', - 'entity_category': 'config', - 'unique_id': 'bridge_0x00124b00120144ae_log_level_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ value_json.log_level | lower }}', - 'command_topic': 'zigbee2mqtt/bridge/request/options', - 'command_template': - '{"options": {"advanced": {"log_level": "{{ value }}" } } }', - 'options': settings.LOG_LEVELS, - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Log level', + object_id: 'zigbee2mqtt_bridge_log_level', + entity_category: 'config', + unique_id: 'bridge_0x00124b00120144ae_log_level_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ value_json.log_level | lower }}', + command_topic: 'zigbee2mqtt/bridge/request/options', + command_template: '{"options": {"advanced": {"log_level": "{{ value }}" } } }', + options: settings.LOG_LEVELS, + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/select/1221051039810110150109113116116_0x00124b00120144ae/log_level/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); // Sensors. payload = { - 'name': 'Version', - 'object_id': 'zigbee2mqtt_bridge_version', - 'entity_category': 'diagnostic', - 'icon': 'mdi:zigbee', - 'unique_id': 'bridge_0x00124b00120144ae_version_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ value_json.version }}', - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Version', + object_id: 'zigbee2mqtt_bridge_version', + entity_category: 'diagnostic', + icon: 'mdi:zigbee', + unique_id: 'bridge_0x00124b00120144ae_version_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ value_json.version }}', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/version/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'name': 'Coordinator version', - 'object_id': 'zigbee2mqtt_bridge_coordinator_version', - 'entity_category': 'diagnostic', - 'enabled_by_default': false, - 'icon': 'mdi:chip', - 'unique_id': 'bridge_0x00124b00120144ae_coordinator_version_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ value_json.coordinator.meta.revision }}', - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Coordinator version', + object_id: 'zigbee2mqtt_bridge_coordinator_version', + entity_category: 'diagnostic', + enabled_by_default: false, + icon: 'mdi:chip', + unique_id: 'bridge_0x00124b00120144ae_coordinator_version_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ value_json.coordinator.meta.revision }}', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/coordinator_version/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'name': 'Network map', - 'object_id': 'zigbee2mqtt_bridge_network_map', - 'entity_category': 'diagnostic', - 'enabled_by_default': false, - 'unique_id': 'bridge_0x00124b00120144ae_network_map_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/response/networkmap', - 'value_template': "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}", - 'json_attributes_topic': 'zigbee2mqtt/bridge/response/networkmap', - 'json_attributes_template': '{{ value_json.data.value | tojson }}', - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Network map', + object_id: 'zigbee2mqtt_bridge_network_map', + entity_category: 'diagnostic', + enabled_by_default: false, + unique_id: 'bridge_0x00124b00120144ae_network_map_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/response/networkmap', + value_template: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}", + json_attributes_topic: 'zigbee2mqtt/bridge/response/networkmap', + json_attributes_template: '{{ value_json.data.value | tojson }}', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/network_map/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); payload = { - 'name': 'Permit join timeout', - 'object_id': 'zigbee2mqtt_bridge_permit_join_timeout', - 'entity_category': 'diagnostic', - 'device_class': 'duration', - 'unit_of_measurement': 's', - 'unique_id': 'bridge_0x00124b00120144ae_permit_join_timeout_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', - 'origin': origin, - 'device': devicePayload, - 'availability': [{'topic': 'zigbee2mqtt/bridge/state'}], - 'availability_mode': 'all', + name: 'Permit join timeout', + object_id: 'zigbee2mqtt_bridge_permit_join_timeout', + entity_category: 'diagnostic', + device_class: 'duration', + unit_of_measurement: 's', + unique_id: 'bridge_0x00124b00120144ae_permit_join_timeout_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/permit_join_timeout/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); // Switches. payload = { - 'name': 'Permit join', - 'object_id': 'zigbee2mqtt_bridge_permit_join', - 'icon': 'mdi:human-greeting-proximity', - 'unique_id': 'bridge_0x00124b00120144ae_permit_join_zigbee2mqtt', - 'state_topic': 'zigbee2mqtt/bridge/info', - 'value_template': '{{ value_json.permit_join | lower }}', - 'command_topic': 'zigbee2mqtt/bridge/request/permit_join', - 'payload_on': 'true', - 'payload_off': 'false', - 'origin': origin, - 'device': devicePayload, - 'availability': [ { 'topic': 'zigbee2mqtt/bridge/state' } ], - 'availability_mode': 'all' + name: 'Permit join', + object_id: 'zigbee2mqtt_bridge_permit_join', + icon: 'mdi:human-greeting-proximity', + unique_id: 'bridge_0x00124b00120144ae_permit_join_zigbee2mqtt', + state_topic: 'zigbee2mqtt/bridge/info', + value_template: '{{ value_json.permit_join | lower }}', + command_topic: 'zigbee2mqtt/bridge/request/permit_join', + payload_on: 'true', + payload_off: 'false', + origin: origin, + device: devicePayload, + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_0x00124b00120144ae/permit_join/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); it('Should remove discovery entries for removed exposes when device options change', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({"id": "0xf4ce368a38be56a1", "options": {"dimmer_1_enabled": "false", "dimmer_1_dimming_enabled": "false"}})); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/device/options', + stringify({id: '0xf4ce368a38be56a1', options: {dimmer_1_enabled: 'false', dimmer_1_dimming_enabled: 'false'}}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0xf4ce368a38be56a1/light_l2/config', null, - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); @@ -2578,38 +2684,38 @@ describe('HomeAssistant extension', () => { it('Should publish discovery message when a converter announces changed exposes', async () => { MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices['BMCT-SLZ']; - const data = {deviceMode: 0} + const data = {deviceMode: 0}; const msg = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; resetDiscoveryPayloads('0x18fc26000000cafe'); await zigbeeHerdsman.events.message(msg); await flushPromises(); const payload = { - 'availability':[{'topic':'zigbee2mqtt/bridge/state'}], - 'command_topic':'zigbee2mqtt/0x18fc26000000cafe/set/device_mode', - 'device':{ - 'identifiers':['zigbee2mqtt_0x18fc26000000cafe'], - 'manufacturer':'Bosch', - 'model':'Light/shutter control unit II (BMCT-SLZ)', - 'name':'0x18fc26000000cafe', - 'sw_version':null, - 'via_device':'zigbee2mqtt_bridge_0x00124b00120144ae' + availability: [{topic: 'zigbee2mqtt/bridge/state'}], + command_topic: 'zigbee2mqtt/0x18fc26000000cafe/set/device_mode', + device: { + identifiers: ['zigbee2mqtt_0x18fc26000000cafe'], + manufacturer: 'Bosch', + model: 'Light/shutter control unit II (BMCT-SLZ)', + name: '0x18fc26000000cafe', + sw_version: null, + via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - 'entity_category':'config', - 'icon':'mdi:tune', - 'json_attributes_topic':'zigbee2mqtt/0x18fc26000000cafe', - 'name':'Device mode', - 'object_id':'0x18fc26000000cafe_device_mode', - 'options':['light','shutter','disabled'], - 'origin': origin, - 'state_topic':'zigbee2mqtt/0x18fc26000000cafe', - 'unique_id':'0x18fc26000000cafe_device_mode_zigbee2mqtt', - 'value_template':'{{ value_json.device_mode }}', - 'enabled_by_default': true, - } + entity_category: 'config', + icon: 'mdi:tune', + json_attributes_topic: 'zigbee2mqtt/0x18fc26000000cafe', + name: 'Device mode', + object_id: '0x18fc26000000cafe_device_mode', + options: ['light', 'shutter', 'disabled'], + origin: origin, + state_topic: 'zigbee2mqtt/0x18fc26000000cafe', + unique_id: '0x18fc26000000cafe_device_mode_zigbee2mqtt', + value_template: '{{ value_json.device_mode }}', + enabled_by_default: true, + }; expect(MQTT.publish).toHaveBeenCalledWith( - "homeassistant/select/0x18fc26000000cafe/device_mode/config", + 'homeassistant/select/0x18fc26000000cafe/device_mode/config', stringify(payload), - { retain: true, qos: 1 }, + {retain: true, qos: 1}, expect.any(Function), ); }); diff --git a/test/legacy/bridgeLegacy.test.js b/test/legacy/bridgeLegacy.test.js index 61c673f2..342894d0 100644 --- a/test/legacy/bridgeLegacy.test.js +++ b/test/legacy/bridgeLegacy.test.js @@ -9,7 +9,6 @@ const settings = require('../../lib/util/settings'); const Controller = require('../../lib/controller'); const flushPromises = require('../lib/flushPromises'); - describe('Bridge legacy', () => { let controller; let version; @@ -19,7 +18,7 @@ describe('Bridge legacy', () => { version = await require('../../lib/util/utils').default.getZigbee2MQTTVersion(); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - }) + }); beforeEach(() => { data.writeDefaultConfiguration(); @@ -35,9 +34,16 @@ describe('Bridge legacy', () => { it('Should publish bridge configuration on startup', async () => { expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/config', - stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1, "revision": 20190425}},"network":{"panID":5674,"extendedPanID":[0,11,22],"channel":15},"log_level":'info',"permit_join":false}), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + version: version.version, + commit: version.commitHash, + coordinator: {type: 'z-Stack', meta: {version: 1, revision: 20190425}}, + network: {panID: 5674, extendedPanID: [0, 11, 22], channel: 15}, + log_level: 'info', + permit_join: false, + }), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -62,23 +68,23 @@ describe('Bridge legacy', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', - stringify({type: "device_whitelisted", "message": {friendly_name: "bulb_color"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb_color'}}), + {retain: false, qos: 0}, + expect.any(Function), ); - MQTT.publish.mockClear() + MQTT.publish.mockClear(); expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr]); MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', - stringify({type: "device_whitelisted", "message": {friendly_name: "bulb"}}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + expect.any(Function), ); - MQTT.publish.mockClear() + MQTT.publish.mockClear(); expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); await flushPromises(); @@ -88,39 +94,48 @@ describe('Bridge legacy', () => { it('Should allow changing device options', async () => { const bulb_color = zigbeeHerdsman.devices.bulb_color; - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": false} - ); + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {retain: true}})); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": true} - ); + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', optionswrong: {retain: true}})); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": true} - ); + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); MQTT.events.message('zigbee2mqtt/bridge/config/device_options', "{friendly_name: 'bulb_color'malformed: {retain: true}}"); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": true} - ); + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {random_setting: true}})); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "random_setting": true, "retain": true} + expect(settings.getDevice('bulb_color')).toStrictEqual({ + ID: '0x000b57fffec6a5b3', + friendly_name: 'bulb_color', + random_setting: true, + retain: true, + }); + MQTT.events.message( + 'zigbee2mqtt/bridge/config/device_options', + stringify({friendly_name: 'bulb_color', options: {options: {random_1: true}}}), ); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {options: {random_1: true}}})); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "random_setting": true, "retain": true, options: {random_1: true}} + expect(settings.getDevice('bulb_color')).toStrictEqual({ + ID: '0x000b57fffec6a5b3', + friendly_name: 'bulb_color', + random_setting: true, + retain: true, + options: {random_1: true}, + }); + MQTT.events.message( + 'zigbee2mqtt/bridge/config/device_options', + stringify({friendly_name: 'bulb_color', options: {options: {random_2: false}}}), ); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {options: {random_2: false}}})); await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual( - {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "random_setting": true, "retain": true, options: {random_1: true, random_2: false}} - ); + expect(settings.getDevice('bulb_color')).toStrictEqual({ + ID: '0x000b57fffec6a5b3', + friendly_name: 'bulb_color', + random_setting: true, + retain: true, + options: {random_1: true, random_2: false}, + }); }); it('Should allow permit join', async () => { @@ -142,7 +157,9 @@ describe('Bridge legacy', () => { await flushPromises(); expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.reset).toHaveBeenCalledWith('soft'); - zigbeeHerdsman.reset.mockImplementationOnce(() => {throw new Error('')}); + zigbeeHerdsman.reset.mockImplementationOnce(() => { + throw new Error(''); + }); MQTT.events.message('zigbee2mqtt/bridge/config/reset', ''); await flushPromises(); expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(2); @@ -182,8 +199,30 @@ describe('Bridge legacy', () => { expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/config/devices'); const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length); - expect(payload[1]).toStrictEqual({"ieeeAddr": "0x00124b00120144ae", "type": "Coordinator", "dateCode": "20190425", "friendly_name": "Coordinator", networkAddress: 0, softwareBuildID: "z-Stack", lastSeen: 100}); - expect(payload[2]).toStrictEqual({"dateCode": null, "friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "lastSeen": 1000, "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "softwareBuildID": null, "type": "Router", "description": "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm", "vendor": "IKEA"}); + expect(payload[1]).toStrictEqual({ + ieeeAddr: '0x00124b00120144ae', + type: 'Coordinator', + dateCode: '20190425', + friendly_name: 'Coordinator', + networkAddress: 0, + softwareBuildID: 'z-Stack', + lastSeen: 100, + }); + expect(payload[2]).toStrictEqual({ + dateCode: null, + friendly_name: 'bulb', + ieeeAddr: '0x000b57fffec6a5b2', + lastSeen: 1000, + manufacturerID: 4476, + model: 'LED1545G12', + modelID: 'TRADFRI bulb E27 WS opal 980lm', + networkAddress: 40369, + powerSource: 'Mains (single phase)', + softwareBuildID: null, + type: 'Router', + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + vendor: 'IKEA', + }); Date.now = now; }); @@ -193,13 +232,25 @@ describe('Bridge legacy', () => { await flushPromises(); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); - expect(payload).toStrictEqual({"message":[{"ID":1,"devices":[],"friendly_name":"group_1","retain":false},{"ID":2,"devices":[],"friendly_name":"group_2","retain":false},{"ID":9,"devices":["bulb_color_2","bulb_2","wall_switch_double/right"],"friendly_name":"ha_discovery_group"},{"ID":11,"devices":["bulb_2"],"friendly_name":"group_with_tradfri","retain":false},{"ID":12,"devices":["TS0601_thermostat"],"friendly_name":"thermostat_group","retain":false},{"ID":14,"devices":["power_plug","bulb_2"],"friendly_name":"switch_group","retain":false},{"ID":21,"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"},{"ID":15071,"devices":["bulb_color_2","bulb_2"],"friendly_name":"group_tradfri_remote","retain":false}],"type":"groups"}); + expect(payload).toStrictEqual({ + message: [ + {ID: 1, devices: [], friendly_name: 'group_1', retain: false}, + {ID: 2, devices: [], friendly_name: 'group_2', retain: false}, + {ID: 9, devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'}, + {ID: 11, devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false}, + {ID: 12, devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false}, + {ID: 14, devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false}, + {ID: 21, devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'}, + {ID: 15071, devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false}, + ], + type: 'groups', + }); }); it('Should allow rename devices', async () => { - const bulb_color2 = {"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color2", "retain": false}; + const bulb_color2 = {ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color2', retain: false}; MQTT.publish.mockClear(); - expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": false}); + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color', new: 'bulb_color2'})); await flushPromises(); expect(settings.getDevice('bulb_color')).toStrictEqual(null); @@ -208,7 +259,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'device_renamed', message: {from: 'bulb_color', to: 'bulb_color2'}}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color2', newmalformed: 'bulb_color3'})); @@ -226,15 +277,15 @@ describe('Bridge legacy', () => { it('Should allow rename groups', async () => { MQTT.publish.mockClear(); - expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], "friendly_name": "group_1", retain: false}); + expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'group_1', new: 'group_1_renamed'})); await flushPromises(); - expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], "friendly_name": "group_1_renamed", retain: false}); + expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1_renamed', retain: false}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', stringify({type: 'group_renamed', message: {from: 'group_1', to: 'group_1_renamed'}}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -251,7 +302,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -273,9 +324,9 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'group_added', message: 'new_group'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); - expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendly_name": "new_group", devices: []}); + expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []}); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3); }); @@ -284,14 +335,14 @@ describe('Bridge legacy', () => { zigbeeHerdsman.createGroup.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group"}'); await flushPromises(); - expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendly_name": "new_group", devices: []}); + expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []}); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', stringify({type: 'group_added', message: 'new_group'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -299,14 +350,14 @@ describe('Bridge legacy', () => { zigbeeHerdsman.createGroup.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group", "id": 42}'); await flushPromises(); - expect(settings.getGroup('new_group')).toStrictEqual({"ID": 42, "friendly_name": "new_group", devices: []}); + expect(settings.getGroup('new_group')).toStrictEqual({ID: 42, friendly_name: 'new_group', devices: []}); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', stringify({type: 'group_added', message: 'new_group'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -314,9 +365,9 @@ describe('Bridge legacy', () => { zigbeeHerdsman.createGroup.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"id": 42}'); await flushPromises(); - expect(settings.getGroup('group_42')).toStrictEqual({"ID": 42, "friendly_name": "group_42", devices: []}); + expect(settings.getGroup('group_42')).toStrictEqual({ID: 42, friendly_name: 'group_42', devices: []}); expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42) + expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42); }); it('Should allow to remove groups', async () => { @@ -329,7 +380,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'group_removed', message: 'group_1'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -343,7 +394,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'group_removed', message: 'group_1'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); }); @@ -378,7 +429,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'device_removed', message: 'bulb_color'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); expect(controller.state.state).toStrictEqual({}); expect(settings.get().blocklist.length).toBe(0); @@ -400,7 +451,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'device_force_removed', message: 'bulb_color'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); expect(controller.state.state).toStrictEqual({}); expect(settings.get().blocklist.length).toBe(0); @@ -421,7 +472,7 @@ describe('Bridge legacy', () => { 'zigbee2mqtt/bridge/log', stringify({type: 'device_banned', message: 'bulb_color'}), {qos: 0, retain: false}, - expect.any(Function) + expect.any(Function), ); expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b3']); }); @@ -436,28 +487,32 @@ describe('Bridge legacy', () => { it('Should handle when remove fails', async () => { const device = zigbeeHerdsman.devices.bulb_color; device.removeFromNetwork.mockClear(); - device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('')}) + device.removeFromNetwork.mockImplementationOnce(() => { + throw new Error(''); + }); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); await flushPromises(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": false}) + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); expect(MQTT.publish).toHaveBeenCalledTimes(1); }); it('Should handle when ban fails', async () => { const device = zigbeeHerdsman.devices.bulb_color; device.removeFromNetwork.mockClear(); - device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('')}) + device.removeFromNetwork.mockImplementationOnce(() => { + throw new Error(''); + }); await flushPromises(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); await flushPromises(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendly_name": "bulb_color", "retain": false}) + expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); expect(MQTT.publish).toHaveBeenCalledTimes(1); }); diff --git a/test/legacy/report.test.js b/test/legacy/report.test.js index 1b79bbb5..425a40d9 100644 --- a/test/legacy/report.test.js +++ b/test/legacy/report.test.js @@ -32,12 +32,22 @@ describe('Report', () => { expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 300, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 300, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + ]); if (colorXY) { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + {attribute: 'currentX', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + {attribute: 'currentY', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + ]); } else { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + ]); } } @@ -51,12 +61,22 @@ describe('Report', () => { expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); expect(endpoint.unbind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, + ]); if (colorXY) { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, + {attribute: 'currentX', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, + {attribute: 'currentY', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, + ]); } else { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, + ]); } } @@ -68,7 +88,7 @@ describe('Report', () => { endpoint.bind.mockClear(); endpoint.unbind.mockClear(); } - } + }; beforeAll(async () => { jest.useFakeTimers(); @@ -130,7 +150,7 @@ describe('Report', () => { device.save.mockClear(); mockClear(device); delete device.meta.reporting; - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -155,7 +175,9 @@ describe('Report', () => { it('Should not mark as configured when reporting setup fails', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('failed')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('failed'); + }); delete device.meta.reporting; mockClear(device); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; @@ -183,7 +205,7 @@ describe('Report', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); mockClear(device); - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -233,7 +255,9 @@ describe('Report', () => { it('Should not configure reporting again when it already failed once', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); - endpoint.bind.mockImplementationOnce(async () => {throw new Error('failed')}); + endpoint.bind.mockImplementationOnce(async () => { + throw new Error('failed'); + }); delete device.meta.reporting; mockClear(device); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; @@ -281,8 +305,10 @@ describe('Report', () => { expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); - expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']) - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]); + expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, + ]); expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); endpoint.configuredReportings = configuredReportings; }); diff --git a/test/logger.test.js b/test/logger.test.js index f3c52bb7..95a74872 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -30,8 +30,7 @@ describe('Logger', () => { consoleWriteSpy.mockClear(); }); - afterEach(async () => { - }); + afterEach(async () => {}); it('Create log directory', () => { const dirs = fs.readdirSync(dir.name); @@ -50,7 +49,7 @@ describe('Logger', () => { expect(fs.readdirSync(dir.name).length).toBe(21); logger.cleanup(); expect(fs.readdirSync(dir.name).length).toBe(10); - }) + }); it('Should not cleanup when there is no timestamp set', () => { for (let i = 30; i < 40; i++) { @@ -61,7 +60,7 @@ describe('Logger', () => { expect(fs.readdirSync(dir.name).length).toBe(21); logger.cleanup(); expect(fs.readdirSync(dir.name).length).toBe(21); - }) + }); it('Set and get log level', () => { logger.setLevel('debug'); @@ -82,8 +81,7 @@ describe('Logger', () => { it('Add/remove transport', () => { class DummyTransport extends Transport { - log(info, callback) { - } + log(info, callback) {} } expect(logger.winston.transports.length).toBe(2); @@ -143,7 +141,7 @@ describe('Logger', () => { it('Should allow to symlink logs to current directory', () => { settings.set(['advanced', 'log_symlink_current'], true); logger.init(); - expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy() + expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy(); jest.resetModules(); }); @@ -192,51 +190,51 @@ describe('Logger', () => { it('Logs Error object', () => { const logSpy = jest.spyOn(logger.winston, 'log'); - logger.error(new Error('msg'));// test for stack=true + logger.error(new Error('msg')); // test for stack=true expect(logSpy).toHaveBeenLastCalledWith('error', `z2m: ${new Error('msg')}`); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); - }) + }); it.each([ [ '^zhc:legacy:fz:(tuya|moes)', new RegExp(/^zhc:legacy:fz:(tuya|moes)/), [ - { ns: 'zhc:legacy:fz:tuya_device12', match: true }, - { ns: 'zhc:legacy:fz:moes_dimmer', match: true }, - { ns: 'zhc:legacy:fz:not_moes', match: false }, - { ns: 'zhc:legacy:fz', match: false }, - { ns: 'zhc:legacy:fz:', match: false }, - { ns: '1zhc:legacy:fz:tuya_device12', match: false }, - ] + {ns: 'zhc:legacy:fz:tuya_device12', match: true}, + {ns: 'zhc:legacy:fz:moes_dimmer', match: true}, + {ns: 'zhc:legacy:fz:not_moes', match: false}, + {ns: 'zhc:legacy:fz', match: false}, + {ns: 'zhc:legacy:fz:', match: false}, + {ns: '1zhc:legacy:fz:tuya_device12', match: false}, + ], ], [ '^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller', new RegExp(/^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller/), [ - { ns: 'zh:ember:uart:ash', match: true }, - { ns: 'zh:ember:uart', match: false }, - { ns: 'zh:controller', match: true }, - { ns: 'zh:controller:', match: true }, - { ns: 'azh:controller:', match: false }, - ] + {ns: 'zh:ember:uart:ash', match: true}, + {ns: 'zh:ember:uart', match: false}, + {ns: 'zh:controller', match: true}, + {ns: 'zh:controller:', match: true}, + {ns: 'azh:controller:', match: false}, + ], ], [ '', undefined, [ - { ns: 'zhc:legacy:fz:tuya_device12', match: false }, - { ns: 'zhc:legacy:fz:moes_dimmer', match: false }, - { ns: 'zhc:legacy:fz:not_moes', match: false }, - { ns: 'zhc:legacy:fz', match: false }, - { ns: 'zhc:legacy:fz:', match: false }, - { ns: '1zhc:legacy:fz:tuya_device12', match: false }, - { ns: 'zh:ember:uart:ash', match: false }, - { ns: 'zh:ember:uart', match: false }, - { ns: 'zh:controller', match: false }, - { ns: 'zh:controller:', match: false }, - { ns: 'azh:controller:', match: false }, - ] + {ns: 'zhc:legacy:fz:tuya_device12', match: false}, + {ns: 'zhc:legacy:fz:moes_dimmer', match: false}, + {ns: 'zhc:legacy:fz:not_moes', match: false}, + {ns: 'zhc:legacy:fz', match: false}, + {ns: 'zhc:legacy:fz:', match: false}, + {ns: '1zhc:legacy:fz:tuya_device12', match: false}, + {ns: 'zh:ember:uart:ash', match: false}, + {ns: 'zh:ember:uart', match: false}, + {ns: 'zh:controller', match: false}, + {ns: 'zh:controller:', match: false}, + {ns: 'azh:controller:', match: false}, + ], ], ])('Sets namespace ignore for debug level %s', (ignore, expected, tests) => { logger.setLevel('debug'); @@ -264,7 +262,7 @@ describe('Logger', () => { }); logger.init(); logger.setLevel('debug'); - expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'warning'}); + expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'}); expect(logger.getLevel()).toStrictEqual('debug'); const logSpy = jest.spyOn(logger.winston, 'log'); @@ -283,9 +281,9 @@ describe('Logger', () => { it('Logs with namespaced levels or default - lower', () => { expect(logger.getNamespacedLevels()).toStrictEqual({}); - logger.setNamespacedLevels({'z2m:mqtt': 'info'}) + logger.setNamespacedLevels({'z2m:mqtt': 'info'}); logger.setLevel('warning'); - expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'info'}); + expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'info'}); expect(logger.getLevel()).toStrictEqual('warning'); const logSpy = jest.spyOn(logger.winston, 'log'); @@ -332,7 +330,7 @@ describe('Logger', () => { expect(consoleWriteSpy).toHaveBeenCalledTimes(3); logger.setLevel('info'); - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels = Object.assign({}, nsLevels)); + expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = Object.assign({}, nsLevels))); logger.info(`unconfigured namespace info should now pass after default level change and cache reset`, 'z2m:mqtt'); expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(4); @@ -340,8 +338,8 @@ describe('Logger', () => { expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(5); - logger.setNamespacedLevels({'zh:zstack': 'warning'}) - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels = {'zh:zstack': 'warning'}); + logger.setNamespacedLevels({'zh:zstack': 'warning'}); + expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = {'zh:zstack': 'warning'})); logger.error(`error logged`, 'zh:zstack:unpi:writer'); expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(6); diff --git a/test/networkMap.test.js b/test/networkMap.test.js index 33fdaa4d..912f0291 100644 --- a/test/networkMap.test.js +++ b/test/networkMap.test.js @@ -12,7 +12,7 @@ zigbeeHerdsman.returnDevices.push(bulb_color.ieeeAddr); zigbeeHerdsman.returnDevices.push(WXKG02LM_rev1.ieeeAddr); zigbeeHerdsman.returnDevices.push(CC2530_ROUTER.ieeeAddr); zigbeeHerdsman.returnDevices.push(unsupported_router.ieeeAddr); -zigbeeHerdsman.returnDevices.push(external_converter_device.ieeeAddr) +zigbeeHerdsman.returnDevices.push(external_converter_device.ieeeAddr); const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); @@ -25,7 +25,7 @@ describe('Networkmap', () => { beforeAll(async () => { jest.useFakeTimers(); - Date.now = jest.fn() + Date.now = jest.fn(); Date.now.mockReturnValue(10000); data.writeDefaultConfiguration(); settings.reRead(); @@ -66,32 +66,62 @@ describe('Networkmap', () => { * | -> CC2530_ROUTER -> WXKG02LM_rev1 * */ - coordinator.lqi = () => {return {neighbors: [ - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120}, - {ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, - {ieeeAddr: external_converter_device.ieeeAddr, networkAddress: external_converter_device.networkAddress, relationship: 2, depth: 1, linkquality: 92} - ]}}; - coordinator.routingTable = () => {return {table: [ - {destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}, - ]}}; + coordinator.lqi = () => { + return { + neighbors: [ + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120}, + {ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, + { + ieeeAddr: external_converter_device.ieeeAddr, + networkAddress: external_converter_device.networkAddress, + relationship: 2, + depth: 1, + linkquality: 92, + }, + ], + }; + }; + coordinator.routingTable = () => { + return {table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}]}; + }; - bulb.lqi = () => {return {neighbors: [ - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110}, - {ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100} - ]}}; - bulb.routingTable = () => {return {table: []}}; + bulb.lqi = () => { + return { + neighbors: [ + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110}, + {ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100}, + ], + }; + }; + bulb.routingTable = () => { + return {table: []}; + }; - bulb_color.lqi = () => {return {neighbors: []}} - bulb_color.routingTable = () => {return {table: []}}; + bulb_color.lqi = () => { + return {neighbors: []}; + }; + bulb_color.routingTable = () => { + return {table: []}; + }; - CC2530_ROUTER.lqi = () => {return {neighbors: [ - {ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130}, - ]}}; - CC2530_ROUTER.routingTable = () => {return {table: []}}; + CC2530_ROUTER.lqi = () => { + return { + neighbors: [ + {ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130}, + ], + }; + }; + CC2530_ROUTER.routingTable = () => { + return {table: []}; + }; - unsupported_router.lqi = () => {throw new Error('failed')}; - unsupported_router.routingTable = () => {throw new Error('failed')}; + unsupported_router.lqi = () => { + throw new Error('failed'); + }; + unsupported_router.routingTable = () => { + throw new Error('failed'); + }; } it('Output raw networkmap legacy api', async () => { @@ -102,7 +132,175 @@ describe('Networkmap', () => { let call = MQTT.publish.mock.calls[0]; expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); - const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, identify, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Wireless remote switch (double rocker), 2016 model","model":"WXKG02LM_rev1","supports":"battery, voltage, power_outage_count, action, linkquality","vendor":"Aqara"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"action, linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"CC2530 router","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}; + const expected = { + links: [ + { + depth: 1, + linkquality: 120, + lqi: 120, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, + sourceIeeeAddr: '0x000b57fffec6a5b3', + sourceNwkAddr: 40399, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}], + source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + sourceIeeeAddr: '0x000b57fffec6a5b2', + sourceNwkAddr: 40369, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114}, + sourceIeeeAddr: '0x0017880104e45511', + sourceNwkAddr: 1114, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 2, + linkquality: 110, + lqi: 110, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, + sourceIeeeAddr: '0x000b57fffec6a5b3', + sourceNwkAddr: 40399, + target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + targetIeeeAddr: '0x000b57fffec6a5b2', + }, + { + depth: 2, + linkquality: 100, + lqi: 100, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, + sourceIeeeAddr: '0x0017880104e45559', + sourceNwkAddr: 6540, + target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + targetIeeeAddr: '0x000b57fffec6a5b2', + }, + { + depth: 2, + linkquality: 130, + lqi: 130, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538}, + sourceIeeeAddr: '0x0017880104e45521', + sourceNwkAddr: 6538, + target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, + targetIeeeAddr: '0x0017880104e45559', + }, + ], + nodes: [ + { + definition: null, + failed: [], + friendlyName: 'Coordinator', + ieeeAddr: '0x00124b00120144ae', + lastSeen: 1000, + modelID: null, + networkAddress: 0, + type: 'Coordinator', + }, + { + definition: { + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + model: 'LED1545G12', + supports: + 'light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, identify, linkquality', + vendor: 'IKEA', + }, + failed: [], + friendlyName: 'bulb', + ieeeAddr: '0x000b57fffec6a5b2', + lastSeen: 1000, + modelID: 'TRADFRI bulb E27 WS opal 980lm', + networkAddress: 40369, + type: 'Router', + }, + { + definition: { + description: 'Hue Go', + model: '7146060PH', + supports: + 'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality', + vendor: 'Philips', + }, + failed: [], + friendlyName: 'bulb_color', + ieeeAddr: '0x000b57fffec6a5b3', + lastSeen: 1000, + modelID: 'LLC020', + networkAddress: 40399, + type: 'Router', + }, + { + definition: { + description: 'Wireless remote switch (double rocker), 2016 model', + model: 'WXKG02LM_rev1', + supports: 'battery, voltage, power_outage_count, action, linkquality', + vendor: 'Aqara', + }, + friendlyName: 'button_double_key', + ieeeAddr: '0x0017880104e45521', + lastSeen: 1000, + modelID: 'lumi.sensor_86sw2.es1', + networkAddress: 6538, + type: 'EndDevice', + }, + { + definition: { + description: 'Automatically generated definition', + model: 'notSupportedModelID', + supports: 'action, linkquality', + vendor: 'Boef', + }, + failed: ['lqi', 'routingTable'], + friendlyName: '0x0017880104e45525', + ieeeAddr: '0x0017880104e45525', + lastSeen: 1000, + manufacturerName: 'Boef', + modelID: 'notSupportedModelID', + networkAddress: 6536, + type: 'Router', + }, + { + definition: {description: 'CC2530 router', model: 'CC2530.ROUTER', supports: 'led, linkquality', vendor: 'Custom devices (DiY)'}, + failed: [], + friendlyName: 'cc2530_router', + ieeeAddr: '0x0017880104e45559', + lastSeen: 1000, + modelID: 'lumi.router', + networkAddress: 6540, + type: 'Router', + }, + { + definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'}, + friendlyName: '0x0017880104e45511', + ieeeAddr: '0x0017880104e45511', + lastSeen: 1000, + modelID: 'external_converter_device', + networkAddress: 1114, + type: 'EndDevice', + }, + ], + }; expect(JSON.parse(call[1])).toStrictEqual(expected); /** @@ -122,7 +320,7 @@ describe('Networkmap', () => { } }); - expected.links.forEach((l) => l.routes = []) + expected.links.forEach((l) => (l.routes = [])); expect(JSON.parse(call[1])).toStrictEqual(expected); }); @@ -131,7 +329,7 @@ describe('Networkmap', () => { const device = zigbeeHerdsman.devices.bulb_color; device.lastSeen = null; const endpoint = device.getEndpoint(1); - const data = {modelID: 'test'} + const data = {modelID: 'test'}; const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; await zigbeeHerdsman.events.message(payload); MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'graphviz'); @@ -170,7 +368,7 @@ describe('Networkmap', () => { const device = zigbeeHerdsman.devices.bulb_color; device.lastSeen = null; const endpoint = device.getEndpoint(1); - const data = {modelID: 'test'} + const data = {modelID: 'test'}; const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; await zigbeeHerdsman.events.message(payload); MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'plantuml'); @@ -276,7 +474,187 @@ describe('Networkmap', () => { let call = MQTT.publish.mock.calls[0]; expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); - const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, identify, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Wireless remote switch (double rocker), 2016 model","model":"WXKG02LM_rev1","supports":"battery, voltage, power_outage_count, action, linkquality","vendor":"Aqara"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"action, linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"CC2530 router","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"}; + const expected = { + data: { + routes: true, + type: 'raw', + value: { + links: [ + { + depth: 1, + linkquality: 120, + lqi: 120, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, + sourceIeeeAddr: '0x000b57fffec6a5b3', + sourceNwkAddr: 40399, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}], + source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + sourceIeeeAddr: '0x000b57fffec6a5b2', + sourceNwkAddr: 40369, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114}, + sourceIeeeAddr: '0x0017880104e45511', + sourceNwkAddr: 1114, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 2, + linkquality: 110, + lqi: 110, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, + sourceIeeeAddr: '0x000b57fffec6a5b3', + sourceNwkAddr: 40399, + target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + targetIeeeAddr: '0x000b57fffec6a5b2', + }, + { + depth: 2, + linkquality: 100, + lqi: 100, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, + sourceIeeeAddr: '0x0017880104e45559', + sourceNwkAddr: 6540, + target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + targetIeeeAddr: '0x000b57fffec6a5b2', + }, + { + depth: 2, + linkquality: 130, + lqi: 130, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538}, + sourceIeeeAddr: '0x0017880104e45521', + sourceNwkAddr: 6538, + target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, + targetIeeeAddr: '0x0017880104e45559', + }, + ], + nodes: [ + { + definition: null, + failed: [], + friendlyName: 'Coordinator', + ieeeAddr: '0x00124b00120144ae', + lastSeen: 1000, + modelID: null, + networkAddress: 0, + type: 'Coordinator', + }, + { + definition: { + description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', + model: 'LED1545G12', + supports: + 'light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, color_options, identify, linkquality', + vendor: 'IKEA', + }, + failed: [], + friendlyName: 'bulb', + ieeeAddr: '0x000b57fffec6a5b2', + lastSeen: 1000, + modelID: 'TRADFRI bulb E27 WS opal 980lm', + networkAddress: 40369, + type: 'Router', + }, + { + definition: { + description: 'Hue Go', + model: '7146060PH', + supports: + 'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality', + vendor: 'Philips', + }, + failed: [], + friendlyName: 'bulb_color', + ieeeAddr: '0x000b57fffec6a5b3', + lastSeen: 1000, + modelID: 'LLC020', + networkAddress: 40399, + type: 'Router', + }, + { + definition: { + description: 'Wireless remote switch (double rocker), 2016 model', + model: 'WXKG02LM_rev1', + supports: 'battery, voltage, power_outage_count, action, linkquality', + vendor: 'Aqara', + }, + friendlyName: 'button_double_key', + ieeeAddr: '0x0017880104e45521', + lastSeen: 1000, + modelID: 'lumi.sensor_86sw2.es1', + networkAddress: 6538, + type: 'EndDevice', + }, + { + definition: { + description: 'Automatically generated definition', + model: 'notSupportedModelID', + supports: 'action, linkquality', + vendor: 'Boef', + }, + failed: ['lqi', 'routingTable'], + friendlyName: '0x0017880104e45525', + ieeeAddr: '0x0017880104e45525', + lastSeen: 1000, + manufacturerName: 'Boef', + modelID: 'notSupportedModelID', + networkAddress: 6536, + type: 'Router', + }, + { + definition: { + description: 'CC2530 router', + model: 'CC2530.ROUTER', + supports: 'led, linkquality', + vendor: 'Custom devices (DiY)', + }, + failed: [], + friendlyName: 'cc2530_router', + ieeeAddr: '0x0017880104e45559', + lastSeen: 1000, + modelID: 'lumi.router', + networkAddress: 6540, + type: 'Router', + }, + { + definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'}, + friendlyName: '0x0017880104e45511', + ieeeAddr: '0x0017880104e45511', + lastSeen: 1000, + modelID: 'external_converter_device', + networkAddress: 1114, + type: 'EndDevice', + }, + ], + }, + }, + status: 'ok', + }; const actual = JSON.parse(call[1]); expect(actual).toStrictEqual(expected); }); @@ -288,8 +666,9 @@ describe('Networkmap', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/networkmap', - stringify({"data":{},"status":"error","error":"Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {}, status: 'error', error: "Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -303,7 +682,147 @@ describe('Networkmap', () => { let call = MQTT.publish.mock.calls[0]; expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); - const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Wireless remote switch (double rocker), 2016 model","model":"WXKG02LM_rev1","supports":"battery, voltage, power_outage_count, action, linkquality","vendor":"Aqara"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":{"description":"Automatically generated definition","model":"notSupportedModelID","supports":"action, linkquality","vendor":"Boef"},"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"CC2530 router","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"} + const expected = { + data: { + routes: true, + type: 'raw', + value: { + links: [ + { + depth: 1, + linkquality: 120, + lqi: 120, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, + sourceIeeeAddr: '0x000b57fffec6a5b3', + sourceNwkAddr: 40399, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}], + source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, + sourceIeeeAddr: '0x000b57fffec6a5b2', + sourceNwkAddr: 40369, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 1, + linkquality: 92, + lqi: 92, + relationship: 2, + routes: [], + source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114}, + sourceIeeeAddr: '0x0017880104e45511', + sourceNwkAddr: 1114, + target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, + targetIeeeAddr: '0x00124b00120144ae', + }, + { + depth: 2, + linkquality: 130, + lqi: 130, + relationship: 1, + routes: [], + source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538}, + sourceIeeeAddr: '0x0017880104e45521', + sourceNwkAddr: 6538, + target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, + targetIeeeAddr: '0x0017880104e45559', + }, + ], + nodes: [ + { + definition: null, + failed: [], + friendlyName: 'Coordinator', + ieeeAddr: '0x00124b00120144ae', + lastSeen: 1000, + modelID: null, + networkAddress: 0, + type: 'Coordinator', + }, + { + definition: { + description: 'Hue Go', + model: '7146060PH', + supports: + 'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality', + vendor: 'Philips', + }, + failed: [], + friendlyName: 'bulb_color', + ieeeAddr: '0x000b57fffec6a5b3', + lastSeen: 1000, + modelID: 'LLC020', + networkAddress: 40399, + type: 'Router', + }, + { + definition: { + description: 'Wireless remote switch (double rocker), 2016 model', + model: 'WXKG02LM_rev1', + supports: 'battery, voltage, power_outage_count, action, linkquality', + vendor: 'Aqara', + }, + friendlyName: 'button_double_key', + ieeeAddr: '0x0017880104e45521', + lastSeen: 1000, + modelID: 'lumi.sensor_86sw2.es1', + networkAddress: 6538, + type: 'EndDevice', + }, + { + definition: { + description: 'Automatically generated definition', + model: 'notSupportedModelID', + supports: 'action, linkquality', + vendor: 'Boef', + }, + failed: ['lqi', 'routingTable'], + friendlyName: '0x0017880104e45525', + ieeeAddr: '0x0017880104e45525', + lastSeen: 1000, + manufacturerName: 'Boef', + modelID: 'notSupportedModelID', + networkAddress: 6536, + type: 'Router', + }, + { + definition: { + description: 'CC2530 router', + model: 'CC2530.ROUTER', + supports: 'led, linkquality', + vendor: 'Custom devices (DiY)', + }, + failed: [], + friendlyName: 'cc2530_router', + ieeeAddr: '0x0017880104e45559', + lastSeen: 1000, + modelID: 'lumi.router', + networkAddress: 6540, + type: 'Router', + }, + { + definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'}, + friendlyName: '0x0017880104e45511', + ieeeAddr: '0x0017880104e45511', + lastSeen: 1000, + modelID: 'external_converter_device', + networkAddress: 1114, + type: 'EndDevice', + }, + ], + }, + }, + status: 'ok', + }; const actual = JSON.parse(call[1]); expect(actual).toStrictEqual(expected); }); diff --git a/test/onEvent.test.js b/test/onEvent.test.js index f55489ea..66385f97 100644 --- a/test/onEvent.test.js +++ b/test/onEvent.test.js @@ -44,8 +44,8 @@ describe('On event', () => { it('Should call with start event', async () => { expect(mockOnEvent).toHaveBeenCalledTimes(1); const call = mockOnEvent.mock.calls[0]; - expect(call[0]).toBe('start') - expect(call[1]).toStrictEqual({}) + expect(call[0]).toBe('start'); + expect(call[1]).toStrictEqual({}); expect(call[2]).toBe(device); expect(call[3]).toStrictEqual(settings.getDevice(device.ieeeAddr)); expect(call[4]).toStrictEqual({}); @@ -57,8 +57,8 @@ describe('On event', () => { await flushPromises(); expect(mockOnEvent).toHaveBeenCalledTimes(1); const call = mockOnEvent.mock.calls[0]; - expect(call[0]).toBe('stop') - expect(call[1]).toStrictEqual({}) + expect(call[0]).toBe('stop'); + expect(call[1]).toStrictEqual({}); expect(call[2]).toBe(device); }); diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js index 320dcd6d..58a3098d 100644 --- a/test/otaUpdate.test.js +++ b/test/otaUpdate.test.js @@ -14,19 +14,18 @@ const zigbeeOTA = require('zigbee-herdsman-converters/lib/ota/zigbeeOTA'); const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride'); - describe('OTA update', () => { let controller; let resetExtension = async () => { await controller.enableDisableExtension(false, 'OTAUpdate'); await controller.enableDisableExtension(true, 'OTAUpdate'); - } + }; const mockClear = (mapped) => { mapped.ota.updateToLatest = jest.fn(); mapped.ota.isUpdateAvailable = jest.fn(); - } + }; beforeAll(async () => { data.writeDefaultConfiguration(); @@ -66,9 +65,9 @@ describe('OTA update', () => { let count = 0; endpoint.read.mockImplementation(() => { count++; - return {swBuildId: count, dateCode: '2019010' + count} + return {swBuildId: count, dateCode: '2019010' + count}; }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.info.mockClear(); device.save.mockClear(); @@ -87,164 +86,189 @@ describe('OTA update', () => { expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`); + expect(logger.info).toHaveBeenCalledWith( + `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, + ); expect(device.save).toHaveBeenCalledTimes(2); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': 'immediate'}); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': undefined}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":false,"update":{"state":"updating","progress":0}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: false, update: {state: 'updating', progress: 0}}), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":false,"update":{"state":"updating","progress":10,"remaining":3600}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: false, update: {state: 'updating', progress: 10, remaining: 3600}}), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":false,"update":{"state":"idle","installed_version":90,"latest_version":90}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: false, update: {state: 'idle', installed_version: 90, latest_version: 90}}), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({"data":{"from":{"date_code":"20190101","software_build_id":1},"id":"bulb","to":{"date_code":"20190102","software_build_id":2}},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/devices', - expect.any(String), - { retain: true, qos: 0 }, - expect.any(Function) + stringify({ + data: {from: {date_code: '20190101', software_build_id: 1}, id: 'bulb', to: {date_code: '20190102', software_build_id: 2}}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should handle when OTA update fails', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}}); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + endpoint.read.mockImplementation(() => { + return {swBuildId: 1, dateCode: '2019010'}; + }); + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); device.save.mockClear(); mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { throw new Error('Update failed'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: "bulb"})); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":true,"update":{"state":"available"}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: true, update: {state: 'available'}}), + {retain: true, qos: 0}, + expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({"data":{"id": "bulb"},"status":"error","error":"Update of 'bulb' failed (Update failed)"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb'}, status: 'error', error: "Update of 'bulb' failed (Update failed)"}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should be able to check if OTA update is available', async () => { const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "bulb","updateAvailable":false},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', updateAvailable: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); MQTT.publish.mockClear(); mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "bulb","updateAvailable":true},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', updateAvailable: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should handle if OTA update check fails', async () => { const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF signals disturbed because of dogs barking')}); + mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { + throw new Error('RF signals disturbed because of dogs barking'); + }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "bulb"},"status":"error","error": `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({ + data: {id: 'bulb'}, + status: 'error', + error: `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`, + }), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should fail when device does not exist', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "not_existing_deviceooo"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "not_existing_deviceooo"},"status":"error","error": `Device 'not_existing_deviceooo' does not exist`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'not_existing_deviceooo'}, status: 'error', error: `Device 'not_existing_deviceooo' does not exist`}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should not check for OTA when device does not support it', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "dimmer_wall_switch"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "dimmer_wall_switch"},"status":"error","error": `Device 'dimmer_wall_switch' does not support OTA updates`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'dimmer_wall_switch'}, status: 'error', error: `Device 'dimmer_wall_switch' does not support OTA updates`}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Should refuse to check/update when already in progress', async () => { const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - return new Promise((resolve, reject) => {setTimeout(() => resolve(), 99999)}) + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), 99999); + }); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); await flushPromises(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); jest.runOnlyPendingTimers(); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({"data":{"id": "bulb"},"status":"error","error": `Update or check for update already in progress for 'bulb'`}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb'}, status: 'error', error: `Update or check for update already in progress for 'bulb'`}), + {retain: false, qos: 0}, + expect.any(Function), ); }); it('Shouldnt crash when read modelID before/after OTA update fails', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => {throw new Error('Failed!')}); + endpoint.read.mockImplementation(() => { + throw new Error('Failed!'); + }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', "bulb"); + MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({"data":{"id":"bulb","from":null,"to":null},"status":"ok"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({data: {id: 'bulb', from: null, to: null}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -252,18 +276,26 @@ describe('OTA update', () => { const device = zigbeeHerdsman.devices.bulb; device.endpoints[0].commandResponse.mockClear(); const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; logger.info.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382}); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); // Should not request again when device asks again after a short time await zigbeeHerdsman.events.message(payload); @@ -277,8 +309,9 @@ describe('OTA update', () => { expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":true,"update":{"state":"available","installed_version":10,"latest_version":12}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: true, update: {state: 'available', installed_version: 10, latest_version: 12}}), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -286,21 +319,32 @@ describe('OTA update', () => { const device = zigbeeHerdsman.devices.bulb; device.endpoints[0].commandResponse.mockClear(); const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('Nothing to find here')}) - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { + throw new Error('Nothing to find here'); + }); + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; logger.info.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382}); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":false,"update":{"state":"idle"}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: false, update: {state: 'idle'}}), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -308,21 +352,30 @@ describe('OTA update', () => { const device = zigbeeHerdsman.devices.bulb; device.endpoints[0].commandResponse.mockClear(); const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13}); - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; logger.info.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382}); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"update_available":false,"update":{"state":"idle","installed_version": 13, "latest_version": 13}}), - {retain: true, qos: 0}, expect.any(Function) + stringify({update_available: false, update: {state: 'idle', installed_version: 13, latest_version: 13}}), + {retain: true, qos: 0}, + expect.any(Function), ); }); @@ -330,10 +383,18 @@ describe('OTA update', () => { settings.set(['ota', 'disable_automatic_update_check'], true); const device = zigbeeHerdsman.devices.bulb; const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13}); - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; logger.info.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -343,22 +404,38 @@ describe('OTA update', () => { it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => { const device = zigbeeHerdsman.devices.HGZB04D; const data = {imageType: 12382}; - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152}, undefined, 10); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); }); it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint to standard endpoint', async () => { const device = zigbeeHerdsman.devices.SV01; const data = {imageType: 12382}; - const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1), + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; logger.error.mockClear(); await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152}, undefined, 10); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); }); it('Legacy api: Should OTA update a device', async () => { @@ -367,9 +444,9 @@ describe('OTA update', () => { let count = 0; endpoint.read.mockImplementation(() => { count++; - return {swBuildId: count, dateCode: '2019010' + count} + return {swBuildId: count, dateCode: '2019010' + count}; }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.info.mockClear(); logger.error.mockClear(); @@ -389,18 +466,22 @@ describe('OTA update', () => { expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith(`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`); + expect(logger.info).toHaveBeenCalledWith( + `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, + ); expect(logger.error).toHaveBeenCalledTimes(0); expect(device.save).toHaveBeenCalledTimes(2); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': 'immediate'}); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': undefined}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); + expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); }); it('Legacy api: Should handle when OTA update fails', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}}); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + endpoint.read.mockImplementation(() => { + return {swBuildId: 1, dateCode: '2019010'}; + }); + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.info.mockClear(); logger.error.mockClear(); @@ -417,7 +498,7 @@ describe('OTA update', () => { it('Legacy api: Should be able to check if OTA update is available', async () => { const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.info.mockClear(); @@ -439,10 +520,12 @@ describe('OTA update', () => { it('Legacy api: Should handle if OTA update check fails', async () => { const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.error.mockClear(); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF signals disturbed because of dogs barking')}); + mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { + throw new Error('RF signals disturbed because of dogs barking'); + }); MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); await flushPromises(); @@ -462,12 +545,12 @@ describe('OTA update', () => { const endpoint = device.endpoints[0]; let count = 0; endpoint.read.mockImplementation(() => { - if (count === 1) throw new Error('Failed!') + if (count === 1) throw new Error('Failed!'); count++; - return {swBuildId: 1, dateCode: '2019010'} + return {swBuildId: 1, dateCode: '2019010'}; }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device) + const mapped = await zigbeeHerdsmanConverters.findByDevice(device); mockClear(mapped); logger.info.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); @@ -480,7 +563,7 @@ describe('OTA update', () => { await resetExtension(); expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json')); spyUseIndexOverride.mockClear(); - + settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json'); await resetExtension(); expect(spyUseIndexOverride).toHaveBeenCalledWith('http://my.site/index.json'); @@ -489,7 +572,7 @@ describe('OTA update', () => { it('Clear update state on startup', async () => { const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}}) + controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}}); await resetExtension(); expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}}); }); diff --git a/test/publish.test.js b/test/publish.test.js index 352b8c83..c15ef2f7 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -19,11 +19,11 @@ const expectNothingPublished = () => { expect(e.read).toHaveBeenCalledTimes(0); expect(e.write).toHaveBeenCalledTimes(0); }); - }) + }); Object.values(zigbeeHerdsman.groups).forEach((g) => { expect(g.command).toHaveBeenCalledTimes(0); - }) -} + }); +}; describe('Publish', () => { let controller; @@ -48,8 +48,8 @@ describe('Publish', () => { e.command.mockClear(); e.read.mockClear(); e.write.mockClear(); - }) - }) + }); + }); Object.values(zigbeeHerdsman.groups).forEach((g) => { g.command.mockClear(); }); @@ -68,16 +68,16 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {"level": 200, "transtime": 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should corretly handle mallformed messages', async () => { - await MQTT.events.message('zigbee2mqtt/foo', ""); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', ""); + await MQTT.events.message('zigbee2mqtt/foo', ''); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', ''); await flushPromises(); expectNothingPublished(); }); @@ -96,16 +96,16 @@ describe('Publish', () => { it('Should publish messages to zigbee devices with complicated topic', async () => { const device = zigbeeHerdsman.devices.bulb_color; - settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'wohnzimmer.light.wall.right') + settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'wohnzimmer.light.wall.right'); const endpoint = device.getEndpoint(1); await MQTT.events.message('zigbee2mqtt/wohnzimmer.light.wall.right/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in %', async () => { @@ -114,11 +114,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_percent: '92'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 235, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 235, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in number', async () => { @@ -127,11 +127,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 230})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 230, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 230, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp', async () => { @@ -140,11 +140,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: '222'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 222, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 222, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp in %', async () => { @@ -153,11 +153,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: 100})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 500, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep', async () => { @@ -166,11 +166,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep and postfix', async () => { @@ -179,11 +179,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch_double/right/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(2); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'}); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with endpoint ID', async () => { @@ -192,9 +192,13 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch_double/3/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_right: 'OFF'}), - {"qos": 0, "retain": false}, expect.any(Function)); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/wall_switch_double', + stringify({state_right: 'OFF'}), + {qos: 0, retain: false}, + expect.any(Function), + ); }); it('Should publish messages to zigbee devices to non default-ep with state_[EP]', async () => { @@ -203,11 +207,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch_double/set', stringify({state_right: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices to non default-ep with brightness_[EP]', async () => { @@ -216,12 +220,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/0x0017882194e45543/set', stringify({state_l2: 'ON', brightness_l2: 50})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN', stringify({brightness_l2: 50, state_l2: 'ON'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -231,12 +235,17 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/TS0601_switch/set', stringify({state_l2: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_switch', stringify({state_l2: 'ON'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -248,14 +257,29 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/l2/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [0], datatype: 4, dp: 1}], seq: expect.any(Number)}, {disableDefaultResponse: true}); - expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [1], datatype: 1, dp: 102}], seq: expect.any(Number)}, {disableDefaultResponse: true}); - expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [0], datatype: 4, dp: 1}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); + expect(endpoint.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [1], datatype: 1, dp: 102}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); + expect(endpoint.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_cover_switch', stringify({state_l2: 'OFF', state_l1: 'ON', state: 'OPEN'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -265,11 +289,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color xy and state', async () => { @@ -278,13 +302,14 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON'}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -294,13 +319,14 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON', brightness: 20}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -310,14 +336,14 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color: {x: 100, y: 50}, state: 'ON', brightness: 20, color_mode: 'xy'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -327,14 +353,14 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command).toHaveBeenNthCalledWith(1, "lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenNthCalledWith(2, "genOnOff", "off", {}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(1, 'lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(2, 'genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'OFF'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -344,10 +370,10 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {r: 100, g: 200, b: 10}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should publish messages to zigbee devices with color rgb string', async () => { @@ -356,10 +382,10 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {rgb: '100,200,10'}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should publish messages to zigbee devices with brightness', async () => { @@ -368,7 +394,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: '50'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); @@ -379,7 +405,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); @@ -390,7 +416,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 128, transtime: 0}, {}); + expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 128, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 128}); @@ -401,7 +427,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [1], datatype: 1, dp: 7}], seq: expect.any(Number)}, {disableDefaultResponse: true}); + expect(group.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [1], datatype: 1, dp: 7}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); }); it('Should publish messages to groups with on and brightness', async () => { @@ -409,7 +440,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}); + expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); @@ -420,7 +451,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); @@ -431,7 +462,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}, {}); + expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 24248, colory: 18350, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); @@ -442,7 +473,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}); + expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); @@ -455,7 +486,7 @@ describe('Publish', () => { await flushPromises(); expect(Object.values(zigbeeHerdsman.groups).length).toBe(11); expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); delete zigbeeHerdsman.groups.group_12312; }); @@ -466,18 +497,18 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {"level": 200, "transtime": 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(0); }); it('Shouldnt publish new brightness state when filtered_optimistic is used', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - settings.set(['devices', device.ieeeAddr, 'filtered_optimistic'], ["brightness"]); + settings.set(['devices', device.ieeeAddr, 'filtered_optimistic'], ['brightness']); await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {"level": 200, "transtime": 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); }); @@ -633,7 +664,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set/state', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with color attribute topic', async () => { @@ -642,10 +673,10 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: "xy"}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should parse set with ieeeAddr topic', async () => { @@ -654,7 +685,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/0x000b57fffec6a5b3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with non-existing postfix', async () => { @@ -671,7 +702,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "goToLiftPercentage", {percentageliftvalue: 10}, {}); + expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 10}, {}); // // Inverted endpoint.command.mockClear(); @@ -679,7 +710,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "goToLiftPercentage", {percentageliftvalue: 90}, {}); + expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 90}, {}); }); it('Should send state update on toggle specific endpoint', async () => { @@ -690,11 +721,21 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'TOGGLE'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "toggle", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/wall_switch_double", stringify({state_left: 'ON'}), {"qos": 0, "retain": false}, expect.any(Function)]); - expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/wall_switch_double", stringify({state_left: 'OFF'}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/wall_switch_double', + stringify({state_left: 'ON'}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/wall_switch_double', + stringify({state_left: 'OFF'}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Should not use state converter on non-json message when value is not on/off/toggle', async () => { @@ -711,11 +752,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with and slashes in base and deviceID postfix topic', async () => { - settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home') + settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); const device = zigbeeHerdsman.devices.QBKG03LM; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'in/basement/wall_switch_double'); @@ -723,7 +764,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/at/my/home/in/basement/wall_switch_double/left/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with number at the end of friendly_name and postfix', async () => { @@ -733,7 +774,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should not publish messages to zigbee devices when payload is invalid', async () => { @@ -748,28 +789,28 @@ describe('Publish', () => { it('Should set state before color', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; + const payload = {state: 'ON', color: {x: 0.701, y: 0.299}}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]); }); it('Should read after write when enabled', async () => { const device = zigbeeHerdsman.devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'retrieve_state'], true); const endpoint = device.getEndpoint(1); - const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; + const payload = {state: 'ON', color: {x: 0.701, y: 0.299}}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); jest.runOnlyPendingTimers(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]); expect(endpoint.read).toHaveBeenCalledTimes(2); - expect(endpoint.read.mock.calls[0]).toEqual(["genOnOff", ["onOff"]]); - expect(endpoint.read.mock.calls[1]).toEqual(["lightingColorCtrl", ["colorMode", "currentX", "currentY", "colorTemperature"]]); + expect(endpoint.read.mock.calls[0]).toEqual(['genOnOff', ['onOff']]); + expect(endpoint.read.mock.calls[1]).toEqual(['lightingColorCtrl', ['colorMode', 'currentX', 'currentY', 'colorTemperature']]); }); it('Should also use on/off cluster when controlling group with switch', async () => { @@ -780,12 +821,13 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(2); - expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 0}, {}); - expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); + expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 0}, {}); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON', brightness: 100}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); MQTT.publish.mockClear(); @@ -793,11 +835,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genOnOff", "off", {}, {}); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'OFF', brightness: 100}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -807,7 +850,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 200}, {}); + expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 200}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 100}); @@ -821,7 +864,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 200}, {}]); }); it('Should use transition from device_options on brightness command', async () => { @@ -832,75 +875,116 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 200}, {}]); }); it('Should turn bulb on with correct brightness when device is turned off twice and brightness is reported', async () => { // Test case for: https://github.com/Koenkk/zigbee2mqtt/issues/5413 const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "brightness": 200})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 200})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "OFF", "transition": 0})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); await flushPromises(); - await zigbeeHerdsman.events.message({data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, endpoint, type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {currentLevel: 1}, + cluster: 'genLevelCtrl', + device, + endpoint, + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "OFF", "transition": 0})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 0})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenNthCalledWith(3, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenNthCalledWith(4, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenNthCalledWith(5, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 200}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 200}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 3, + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 1}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 4, + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 1}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenNthCalledWith( + 5, + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 200}), + {retain: false, qos: 0}, + expect.any(Function), + ); expect(endpoint.command).toHaveBeenCalledTimes(4); - expect(endpoint.command).toHaveBeenNthCalledWith(1, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenNthCalledWith(2, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenNthCalledWith(3, "genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}); - expect(endpoint.command).toHaveBeenNthCalledWith(4, "genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(1, 'genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(2, 'genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(3, 'genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 0}, {}); + expect(endpoint.command).toHaveBeenNthCalledWith(4, 'genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); }); it('Should turn bulb on with full brightness when transition is used and no brightness is known', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3799 const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "OFF", "transition": 0.5})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0.5})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 0.5})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0.5})); await flushPromises(); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 254}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 5}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 5}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 5}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 254, transtime: 5}, {}]); }); it('Transition parameter should not influence brightness on state ON', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3563 const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "brightness": 50})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON"})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 1})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["genOnOff", "on", {}, {}]); - expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 10}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 10}, {}]); }); it('Should use transition when color temp', async () => { @@ -911,7 +995,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 200, transtime: 200}, {}]); }); it('Should use transition only once when setting brightness and color temperature for TRADFRI', async () => { @@ -921,17 +1005,17 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 200, transtime: 200}, {}]); }); it('Should use transition only once when setting brightness and color temperature for group which contains TRADFRI', async () => { const group = zigbeeHerdsman.groups.group_with_tradfri; - await MQTT.events.message('zigbee2mqtt/group_with_tradfri/set', stringify({"state": "ON", "transition": 60, "brightness": 20, "color_temp": 400})); + await MQTT.events.message('zigbee2mqtt/group_with_tradfri/set', stringify({state: 'ON', transition: 60, brightness: 20, color_temp: 400})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(2); - expect(group.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]); - expect(group.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 400, transtime: 600}, {}]); + expect(group.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); + expect(group.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 400, transtime: 600}, {}]); }); it('Message transition should overrule options transition', async () => { @@ -942,18 +1026,18 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 100}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 100}, {}]); }); it('Should set state with brightness before color', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}, 'transition': 3, 'brightness': 100}; + const payload = {state: 'ON', color: {x: 0.701, y: 0.299}, transition: 3, brightness: 100}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 30}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 30}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 30}, {}]); }); it('Should turn device off when brightness 0 is send', async () => { @@ -966,13 +1050,28 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]); - expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]); - expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 50}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 0}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[2]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 50}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Should turn device off when brightness 0 is send with transition', async () => { @@ -985,66 +1084,86 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]); - expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]); - expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]); - expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 50}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 0}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[2]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 50}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Should allow to set color via hue and saturation', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const payload = {"color":{"hue":250, "saturation":50}}; + const payload = {color: {hue: 250, saturation: 50}}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 45510, "saturation": 127, "transtime": 0,}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual([ + 'lightingColorCtrl', + 'enhancedMoveToHueAndSaturation', + {direction: 0, enhancehue: 45510, saturation: 127, transtime: 0}, + {}, + ]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}, "color_mode": "hs"}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {hue: 250, saturation: 50}, color_mode: 'hs'}); }); it('ZNCLDJ11LM open', async () => { const device = zigbeeHerdsman.devices.ZNCLDJ11LM; const endpoint = device.getEndpoint(1); - const payload = {'state': 'OPEN'}; + const payload = {state: 'OPEN'}; await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); - expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {presentValue: 100}); + expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 100}); }); it('ZNCLDJ11LM position', async () => { const device = zigbeeHerdsman.devices.ZNCLDJ11LM; const endpoint = device.getEndpoint(1); - const payload = {'position': 10}; + const payload = {position: 10}; await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); - expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {presentValue: 10}); + expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 10}); }); it('ZNCLDJ11LM position', async () => { const device = zigbeeHerdsman.devices.ZNCLDJ11LM; const endpoint = device.getEndpoint(1); - const payload = {'state': 'CLOSE'}; + const payload = {state: 'CLOSE'}; await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); - expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {presentValue: 0}); + expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 0}); }); it('ZNCLDJ11LM position', async () => { const device = zigbeeHerdsman.devices.ZNCLDJ11LM; const endpoint = device.getEndpoint(1); - const payload = {'state': 'STOP'}; + const payload = {state: 'STOP'}; await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "stop", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'stop', {}, {}); }); it('Should turn device on with on/off when transition is provided', async () => { @@ -1054,7 +1173,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 254, transtime: 30}, {}]); }); it('Should turn device on with on/off with transition when transition 0 is provided', async () => { @@ -1064,7 +1183,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 254, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 254, transtime: 0}, {}]); }); it('Should turn device off with onOff on off with transition', async () => { @@ -1074,7 +1193,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 10}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); @@ -1093,23 +1212,45 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 200}), + {qos: 0, retain: false}, + expect.any(Function), + ]); // Bulb reports brightness while decreasing brightness - await zigbeeHerdsman.events.message({data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, endpoint, type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {currentLevel: 1}, + cluster: 'genLevelCtrl', + device, + endpoint, + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 1}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 1}), + {qos: 0, retain: false}, + expect.any(Function), + ]); // Turn on again await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 30}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[2]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 200}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('When device is turned off with transition and turned on WITHOUT transition it should restore the brightness', async () => { @@ -1125,17 +1266,27 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 200}), + {qos: 0, retain: false}, + expect.any(Function), + ]); // Turn on again await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 200}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Home Assistant: should set state', async () => { @@ -1146,7 +1297,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); @@ -1156,13 +1307,13 @@ describe('Publish', () => { settings.set(['homeassistant'], true); const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); controller.state.remove(device); - controller.state.set(device, {state: 'ON'}) + controller.state.set(device, {state: 'ON'}); const endpoint = device.zh.getEndpoint(1); const payload = {state: 'ON', color_temp: 100}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'}); @@ -1172,20 +1323,20 @@ describe('Publish', () => { settings.set(['homeassistant'], true); const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); controller.state.remove(device); - controller.state.set(device, {state: 'OFF'}) + controller.state.set(device, {state: 'OFF'}); const endpoint = device.zh.getEndpoint(1); const payload = {state: 'ON', color_temp: 100}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({state: 'ON', color_temp: 100, color_mode: 'color_temp'}), - { retain: false, qos: 0 }, - expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1193,13 +1344,13 @@ describe('Publish', () => { settings.set(['homeassistant'], true); const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); controller.state.remove(device); - controller.state.set(device, {state: 'ON'}) + controller.state.set(device, {state: 'ON'}); const endpoint = device.zh.getEndpoint(1); const payload = {state: 'ON', color: {x: 0.41, y: 0.25}}; await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 26869, colory: 16384, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 26869, colory: 16384, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'}); @@ -1212,17 +1363,19 @@ describe('Publish', () => { // At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to MQTT. expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "toggle", {}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(0); // Turn bulb off so that the bulb gets a state. await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); // Toggle again, now that we have state it should publish state ON @@ -1235,10 +1388,12 @@ describe('Publish', () => { await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), - {retain: false, qos: 0}, expect.any(Function) + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1248,22 +1403,22 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/led_controller_1/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {disableDefaultResponse: true}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices', async () => { - settings.set(['advanced', 'last_seen'], 'ISO_8601') + settings.set(['advanced', 'last_seen'], 'ISO_8601'); const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toStrictEqual('string'); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish brightness_move up to zigbee devices', async () => { @@ -1271,7 +1426,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: -40})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 1, "rate": 40}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'move', {movemode: 1, rate: 40}, {}); }); it('Should publish brightness_move down to zigbee devices', async () => { @@ -1279,7 +1434,7 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: 30})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 0, "rate": 30}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'move', {movemode: 0, rate: 30}, {}); }); it('HS2WD-E burglar warning', async () => { @@ -1288,7 +1443,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100, "strobedutycycle": 0, "strobelevel": 1}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'ssIasWd', + 'startWarning', + {startwarninginfo: 22, warningduration: 100, strobedutycycle: 0, strobelevel: 1}, + {disableDefaultResponse: true}, + ); }); it('HS2WD-E emergency warning', async () => { @@ -1297,7 +1457,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10, "strobedutycycle": 0, "strobelevel": 1}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'ssIasWd', + 'startWarning', + {startwarninginfo: 51, warningduration: 10, strobedutycycle: 0, strobelevel: 1}, + {disableDefaultResponse: true}, + ); }); it('HS2WD-E emergency without level', async () => { @@ -1306,7 +1471,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10, "strobedutycycle": 0, "strobelevel": 1}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'ssIasWd', + 'startWarning', + {startwarninginfo: 49, warningduration: 10, strobedutycycle: 0, strobelevel: 1}, + {disableDefaultResponse: true}, + ); }); it('HS2WD-E wrong payload (should use defaults)', async () => { @@ -1315,7 +1485,12 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10, "strobedutycycle": 0, "strobelevel": 1}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'ssIasWd', + 'startWarning', + {startwarninginfo: 53, warningduration: 10, strobedutycycle: 0, strobelevel: 1}, + {disableDefaultResponse: true}, + ); }); it('Shouldnt do anything when device is not supported', async () => { @@ -1330,11 +1505,11 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/roller_shutter/set', stringify({state: 'OPEN'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {"level": "255", "transtime": 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: '255', transtime: 0}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish to MKS-CM-W5', async () => { @@ -1343,11 +1518,16 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuya", "dataRequest", {dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)}, {disableDefaultResponse: true}); + expect(endpoint.command).toHaveBeenCalledWith( + 'manuSpecificTuya', + 'dataRequest', + {dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)}, + {disableDefaultResponse: true}, + ); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish separate genOnOff to GL-S-007ZS when setting state and brightness as bulb doesnt turn on with moveToLevelWithOnOff', async () => { @@ -1359,16 +1539,21 @@ describe('Publish', () => { jest.runOnlyPendingTimers(); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual([ 'genOnOff', 'on', {}, {} ]); - expect(endpoint.command.mock.calls[1]).toEqual([ 'genLevelCtrl', 'moveToLevelWithOnOff', { level: 20, transtime: 0 }, {} ]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/GL-S-007ZS', stringify({"state":"ON","brightness":20}), { qos: 0, retain: false }, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/GL-S-007ZS', + stringify({state: 'ON', brightness: 20}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Should log as error when setting property with no defined converter', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - const payload = {'brightness_move': 20}; + const payload = {brightness_move: 20}; logger.error.mockClear(); await MQTT.events.message('zigbee2mqtt/bulb_color/get', stringify(payload)); await flushPromises(); @@ -1387,7 +1572,7 @@ describe('Publish', () => { zigbeeHerdsmanConverters.toZigbee.__clearStore__(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 1.0})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); @@ -1404,22 +1589,22 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "brightness": 123})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 123})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 123, transtime: 0}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "OFF"})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]); + expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({"state": "ON", "transition": 1.0})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(4); - expect(endpoint.command.mock.calls[3]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 123, transtime: 10}, {}]); + expect(endpoint.command.mock.calls[3]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 123, transtime: 10}, {}]); }); it('Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case', async () => { @@ -1427,93 +1612,129 @@ describe('Publish', () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({'state': 'ON'})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({'brightness': 150})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 150})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({'state': 'OFF'})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({'state': 'ON'})); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(4); - expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 150, transtime: 0}, {}]); - expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "off", {}, {}]); - expect(endpoint.command.mock.calls[3]).toEqual(["genOnOff", "on", {}, {}]); + expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); + expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 150, transtime: 0}, {}]); + expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]); + expect(endpoint.command.mock.calls[3]).toEqual(['genOnOff', 'on', {}, {}]); expect(MQTT.publish).toHaveBeenCalledTimes(4); - expect(MQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({"state":"ON"}), { qos: 0, retain: false }, expect.any(Function)]); - expect(MQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({"state":"ON","brightness":150}), { qos: 0, retain: false }, expect.any(Function)]); - expect(MQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({"state":"OFF","brightness":150}), { qos: 0, retain: false }, expect.any(Function)]); - expect(MQTT.publish.mock.calls[3]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({"state":"ON","brightness":150}), { qos: 0, retain: false }, expect.any(Function)]); + expect(MQTT.publish.mock.calls[0]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[1]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 150}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[2]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF', brightness: 150}), + {qos: 0, retain: false}, + expect.any(Function), + ]); + expect(MQTT.publish.mock.calls[3]).toEqual([ + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON', brightness: 150}), + {qos: 0, retain: false}, + expect.any(Function), + ]); }); it('Scenes', async () => { const bulb_color_2 = zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1); const bulb_2 = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); const group = zigbeeHerdsman.groups.group_tradfri_remote; - await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({"state": "ON", "brightness": 50, "color_temp": 290})); - await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({"state": "ON", "brightness": 100})); + await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 50, color_temp: 290})); + await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({"scene_store": 1})); + await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_store: 1})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith('genScenes', 'store', { groupid: 15071, sceneid: 1 }, {}); + expect(group.command).toHaveBeenCalledWith('genScenes', 'store', {groupid: 15071, sceneid: 1}, {}); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({"state": "ON", "brightness": 250, "color_temp": 20})); - await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({"state": "ON", "brightness": 110})); + await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 250, color_temp: 20})); + await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 110})); await flushPromises(); MQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({"scene_recall": 1})); + await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_recall: 1})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); - expect(group.command).toHaveBeenCalledWith('genScenes', 'recall', { groupid: 15071, sceneid: 1 }, {}); + expect(group.command).toHaveBeenCalledWith('genScenes', 'recall', {groupid: 15071, sceneid: 1}, {}); expect(MQTT.publish).toHaveBeenCalledTimes(8); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, 'zigbee2mqtt/group_tradfri_remote', - stringify({"brightness":50,"color_temp":290,"state":"ON","color_mode": "color_temp"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 50, color_temp: 290, state: 'ON', color_mode: 'color_temp'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, 'zigbee2mqtt/bulb_color_2', - stringify({"color_mode": "color_temp", "brightness":50,"color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({color_mode: 'color_temp', brightness: 50, color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(3, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 3, 'zigbee2mqtt/ha_discovery_group', - stringify({"brightness":50,"color_mode":"color_temp","color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 50, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(4, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 4, 'zigbee2mqtt/group_tradfri_remote', - stringify({"brightness":100,"color_temp":290,"state":"ON","color_mode": "color_temp"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 100, color_temp: 290, state: 'ON', color_mode: 'color_temp'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(5, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 5, 'zigbee2mqtt/bulb_2', - stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(6, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 6, 'zigbee2mqtt/group_with_tradfri', - stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(7, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 7, 'zigbee2mqtt/switch_group', - stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(8, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 8, 'zigbee2mqtt/ha_discovery_group', - stringify({"brightness":100,"color_mode":"color_temp","color_temp":290,"state":"ON"}), - {retain: false, qos: 0}, expect.any(Function) + stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); }); @@ -1525,20 +1746,26 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenNthCalledWith(1, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 1, 'zigbee2mqtt/bulb_color', - stringify({"color_mode":"color_temp","color_temp":100}), - {retain: false, qos: 0}, expect.any(Function) + stringify({color_mode: 'color_temp', color_temp: 100}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(2, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 2, 'zigbee2mqtt/bulb_color', - stringify({"color":{"x":0.1,"y":0.5},"color_mode":"xy","color_temp":79}), - {retain: false, qos: 0}, expect.any(Function) + stringify({color: {x: 0.1, y: 0.5}, color_mode: 'xy', color_temp: 79}), + {retain: false, qos: 0}, + expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith(3, + expect(MQTT.publish).toHaveBeenNthCalledWith( + 3, 'zigbee2mqtt/bulb_color', - stringify({"color":{"x":0.4152,"y":0.3954},"color_mode":"color_temp","color_temp":300}), - {retain: false, qos: 0}, expect.any(Function) + stringify({color: {x: 0.4152, y: 0.3954}, color_mode: 'color_temp', color_temp: 300}), + {retain: false, qos: 0}, + expect.any(Function), ); }); diff --git a/test/receive.test.js b/test/receive.test.js index ae3d8f6f..283a4f2d 100755 --- a/test/receive.test.js +++ b/test/receive.test.js @@ -38,72 +38,108 @@ describe('Receive', () => { it('Should handle a zigbee message', async () => { const device = zigbeeHerdsman.devices.WXKG11LM; device.linkquality = 10; - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single', click: 'single', linkquality: 10}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/button', + stringify({action: 'single', click: 'single', linkquality: 10}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should handle a zigbee message which uses ep (left)', async () => { const device = zigbeeHerdsman.devices.WXKG02LM_rev1; - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle a zigbee message which uses ep (right)', async () => { const device = zigbeeHerdsman.devices.WXKG02LM_rev1; - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle a zigbee message with default precision', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should allow to invert cover', async () => { const device = zigbeeHerdsman.devices.J1; // Non-inverted (open = 100, close = 0) - await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, + cluster: 'closuresWindowCovering', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/J1_cover', + stringify({position: 10, tilt: 20, state: 'OPEN'}), + {retain: false, qos: 0}, + expect.any(Function), + ); // Inverted MQTT.publish.mockClear(); settings.set(['devices', device.ieeeAddr, 'invert_cover'], true); - await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, + cluster: 'closuresWindowCovering', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/J1_cover', + stringify({position: 90, tilt: 80, state: 'OPEN'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should allow to disable the legacy integration', async () => { const device = zigbeeHerdsman.devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); - const data = {onOff: 1} + const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -114,14 +150,35 @@ describe('Receive', () => { it('Should debounce messages', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - const data1 = {measuredValue: 8} - const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data1 = {measuredValue: 8}; + const payload1 = { + data: data1, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload1); - const data2 = {measuredValue: 1} - const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data2 = {measuredValue: 1}; + const payload2 = { + data: data2, + cluster: 'msRelativeHumidity', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload2); - const data3 = {measuredValue: 2} - const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data3 = {measuredValue: 2}; + const payload3 = { + data: data3, + cluster: 'msPressureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); @@ -131,7 +188,7 @@ describe('Receive', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should debounce and retain messages when set via device_options', async () => { @@ -139,14 +196,35 @@ describe('Receive', () => { settings.set(['device_options', 'debounce'], 0.1); settings.set(['device_options', 'retain'], true); delete settings.get().devices['0x0017880104e45522']['retain']; - const data1 = {measuredValue: 8} - const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data1 = {measuredValue: 8}; + const payload1 = { + data: data1, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload1); - const data2 = {measuredValue: 1} - const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data2 = {measuredValue: 1}; + const payload2 = { + data: data2, + cluster: 'msRelativeHumidity', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload2); - const data3 = {measuredValue: 2} - const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data3 = {measuredValue: 2}; + const payload3 = { + data: data3, + cluster: 'msPressureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); @@ -156,20 +234,48 @@ describe('Receive', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": true}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true}); }); it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']); - const tempMsg = {data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13}; + const tempMsg = { + data: {measuredValue: 8}, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 13, + }; await zigbeeHerdsman.events.message(tempMsg); - const pressureMsg = {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13}; + const pressureMsg = { + data: {measuredValue: 2}, + cluster: 'msPressureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 13, + }; await zigbeeHerdsman.events.message(pressureMsg); - const tempMsg2 = {data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13}; + const tempMsg2 = { + data: {measuredValue: 7}, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 13, + }; await zigbeeHerdsman.events.message(tempMsg2); - const humidityMsg = {data: {measuredValue: 3}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13}; + const humidityMsg = { + data: {measuredValue: 3}, + cluster: 'msRelativeHumidity', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 13, + }; await zigbeeHerdsman.events.message(humidityMsg); await flushPromises(); jest.advanceTimersByTime(50); @@ -182,16 +288,36 @@ describe('Receive', () => { }); it('Should NOT publish old messages from State cache during debouncing', async () => { - // Summary: // First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State". // Then send another measurement to that same device and trigger asynchronous event to push data from Cache. Newest value should be sent out. const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); - await zigbeeHerdsman.events.message( {data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} ); - await zigbeeHerdsman.events.message( {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} ); + await zigbeeHerdsman.events.message({ + data: {measuredValue: 8}, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); + await zigbeeHerdsman.events.message({ + data: {measuredValue: 1}, + cluster: 'msRelativeHumidity', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); + await zigbeeHerdsman.events.message({ + data: {measuredValue: 2}, + cluster: 'msPressureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await flushPromises(); jest.advanceTimersByTime(50); // Test that measurements are combined(=debounced) @@ -205,11 +331,18 @@ describe('Receive', () => { expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); // Send another Zigbee message... - await zigbeeHerdsman.events.message({data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {measuredValue: 9}, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); const realDevice = controller.zigbee.resolveEntity(device); // Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache. - await controller.publishEntityState( realDevice, {} ); + await controller.publishEntityState(realDevice, {}); jest.runOnlyPendingTimers(); await flushPromises(); @@ -220,13 +353,20 @@ describe('Receive', () => { expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); // Test that messages after debouncing contains NEW measurement and not old. expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); - }); + }); it('Shouldnt republish old state', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3572 const device = zigbeeHerdsman.devices.bulb; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({data: {onOff: 0}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}); + await zigbeeHerdsman.events.message({ + data: {onOff: 0}, + cluster: 'genOnOff', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'})); await flushPromises(); jest.runOnlyPendingTimers(); @@ -238,83 +378,118 @@ describe('Receive', () => { it('Should handle a zigbee message with 1 precision', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1); - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 0 precision', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 1 precision when set via device_options', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 2 precision when overrides device_options', async () => { const device = zigbeeHerdsman.devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); - const data = {measuredValue: -85} - const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + const data = {measuredValue: -85}; + const payload = { + data, + cluster: 'msTemperatureMeasurement', + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with voltage 2990', async () => { const device = zigbeeHerdsman.devices.WXKG02LM_rev1; - const data = {'65281': {'1': 2990}} + const data = {65281: {1: 2990}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish 1 message when converted twice', async () => { const device = zigbeeHerdsman.devices.RTCGQ11LM; - const data = {'65281': {'1': 3045, '3': 19, '5': 35, '6': [0, 3], '11': 381, '100': 0}} + const data = {65281: {1: 3045, 3: 19, 5: 35, 6: [0, 3], 11: 381, 100: 0}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'device_temperature': 19, 'power_outage_count': 34}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + battery: 100, + illuminance: 381, + illuminance_lux: 381, + voltage: 3045, + device_temperature: 19, + power_outage_count: 34, + }); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish 1 message when converted twice', async () => { const device = zigbeeHerdsman.devices.RTCGQ11LM; - const data = {'9999': {'1': 3045}}; + const data = {9999: {1: 3045}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); await flushPromises(); @@ -330,8 +505,8 @@ describe('Receive', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number') - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number'); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601', async () => { @@ -343,8 +518,8 @@ describe('Receive', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string') - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601_local', async () => { @@ -356,8 +531,8 @@ describe('Receive', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string') - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle messages from Xiaomi router devices', async () => { @@ -367,19 +542,13 @@ describe('Receive', () => { await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/power_plug', - stringify({state: 'ON'}), - { retain: false, qos: 0 }, - expect.any(Function) - ); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/power_plug', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', - stringify({'state': 'ON'}), - { retain: false, qos: 0 }, - expect.any(Function) + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), ); - }); it('Should not handle messages from coordinator', async () => { @@ -405,13 +574,21 @@ describe('Receive', () => { it('Should handle a command', async () => { const device = zigbeeHerdsman.devices.E1743; const data = {}; - const payload = {data, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}}; + const payload = { + data, + cluster: 'genLevelCtrl', + device, + endpoint: device.getEndpoint(1), + type: 'commandStopWithOnOff', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 1}, + }; await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should add elapsed', async () => { @@ -419,7 +596,7 @@ describe('Receive', () => { const device = zigbeeHerdsman.devices.E1743; const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'}; const oldNow = Date.now; - Date.now = jest.fn() + Date.now = jest.fn(); Date.now.mockReturnValue(new Date(150)); await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 2}}); await flushPromises(); @@ -428,12 +605,12 @@ describe('Receive', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(2); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({'click': 'brightness_stop', action: 'brightness_stop'}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'}); expect(JSON.parse(MQTT.publish.mock.calls[1][1]).elapsed).toBe(50); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); Date.now = oldNow; }); @@ -444,16 +621,26 @@ describe('Receive', () => { await zigbeeHerdsman.events.message(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.debug).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\''); + expect(logger.debug).toHaveBeenCalledWith( + "No converter available for 'ZNCZ02LM' with cluster 'genBinaryOutput' and type 'attributeReport' and data '{\"inactiveText\":\"hello\"}'", + ); }); it('Should report correct energy and power values for different versions of SP600', async () => { // https://github.com/Koenkk/zigbee-herdsman-converters/issues/915, OLD and NEW use different date code // divisor of OLD is not correct and therefore underreports by factor 10. - const data = {instantaneousDemand:496,currentSummDelivered:[0,6648]} + const data = {instantaneousDemand: 496, currentSummDelivered: [0, 6648]}; const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW; - await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}}); + await zigbeeHerdsman.events.message({ + data, + cluster: 'seMetering', + device: SP600_NEW, + endpoint: SP600_NEW.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 1}, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW'); @@ -461,7 +648,15 @@ describe('Receive', () => { MQTT.publish.mockClear(); const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD; - await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 2}}); + await zigbeeHerdsman.events.message({ + data, + cluster: 'seMetering', + device: SP600_OLD, + endpoint: SP600_OLD.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 2}, + }); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD'); @@ -470,9 +665,9 @@ describe('Receive', () => { it('Should emit DevicesChanged event when a converter announces changed exposes', async () => { const device = zigbeeHerdsman.devices['BMCT-SLZ']; - const data = {deviceMode: 0} + const data = {deviceMode: 0}; const payload = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; await zigbeeHerdsman.events.message(payload); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/devices"); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); }); }); diff --git a/test/settings.test.js b/test/settings.test.js index 5f164b37..7f4b2273 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -19,8 +19,8 @@ const minimalConfig = { }; describe('Settings', () => { - const write = (file, json, reread=true) => { - fs.writeFileSync(file, yaml.dump(json)) + const write = (file, json, reread = true) => { + fs.writeFileSync(file, yaml.dump(json)); if (reread) { settings.reRead(); } @@ -28,14 +28,14 @@ describe('Settings', () => { const read = (file) => yaml.load(fs.readFileSync(file, 'utf8')); const remove = (file) => { if (fs.existsSync(file)) fs.unlinkSync(file); - } + }; const clearEnvironmentVariables = () => { Object.keys(process.env).forEach((key) => { - if(key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) { + if (key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) { delete process.env[key]; } }); - } + }; beforeEach(() => { remove(configurationFile); @@ -69,7 +69,8 @@ describe('Settings', () => { process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'attribute_and_json'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT'] = '["console"]'; - process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}'; + process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = + '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}'; process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic'; process.env['ZIGBEE2MQTT_CONFIG_MQTT_SERVER'] = 'testserver'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY'] = 'GENERATE'; @@ -97,7 +98,7 @@ describe('Settings', () => { expected.groups = {}; expected.serial.disable_led = true; expected.advanced.soft_reset_timeout = 1; - expected.advanced.log_output = ["console"]; + expected.advanced.log_output = ['console']; expected.advanced.output = 'attribute_and_json'; expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'}; expected.mqtt.base_topic = 'testtopic'; @@ -153,7 +154,7 @@ describe('Settings', () => { const device = settings.getDevice('0x12345678'); const expected = { - ID: "0x12345678", + ID: '0x12345678', friendly_name: '0x12345678', retain: false, }; @@ -175,14 +176,14 @@ describe('Settings', () => { password: '!secret password', }, advanced: { - network_key: '!secret network_key' - } + network_key: '!secret network_key', + }, }; const contentSecret = { username: 'mysecretusername', password: 'mysecretpassword', - network_key: [1,2,3], + network_key: [1, 2, 3], }; write(secretFile, contentSecret, false); @@ -192,22 +193,22 @@ describe('Settings', () => { base_topic: 'zigbee2mqtt', include_device_information: false, force_disable_retain: false, - password: "mysecretpassword", - server: "my.mqtt.server", - user: "mysecretusername", + password: 'mysecretpassword', + server: 'my.mqtt.server', + user: 'mysecretusername', }; expect(settings.get().mqtt).toStrictEqual(expected); - expect(settings.get().advanced.network_key).toStrictEqual([1,2,3]); + expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]); settings.testing.write(); expect(read(configurationFile)).toStrictEqual(contentConfiguration); expect(read(secretFile)).toStrictEqual(contentSecret); settings.set(['mqtt', 'user'], 'test123'); - settings.set(['advanced', 'network_key'], [1,2,3, 4]); + settings.set(['advanced', 'network_key'], [1, 2, 3, 4]); expect(read(configurationFile)).toStrictEqual(contentConfiguration); - expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1,2,3,4]}); + expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1, 2, 3, 4]}); }); it('Should read ALL secrets form a separate file', () => { @@ -219,14 +220,14 @@ describe('Settings', () => { }, advanced: { network_key: '!secret network_key', - } + }, }; const contentSecret = { server: 'my.mqtt.server', username: 'mysecretusername', password: 'mysecretpassword', - network_key: [1,2,3], + network_key: [1, 2, 3], }; write(secretFile, contentSecret, false); @@ -236,13 +237,13 @@ describe('Settings', () => { base_topic: 'zigbee2mqtt', include_device_information: false, force_disable_retain: false, - password: "mysecretpassword", - server: "my.mqtt.server", - user: "mysecretusername", + password: 'mysecretpassword', + server: 'my.mqtt.server', + user: 'mysecretusername', }; expect(settings.get().mqtt).toStrictEqual(expected); - expect(settings.get().advanced.network_key).toStrictEqual([1,2,3]); + expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]); settings.testing.write(); expect(read(configurationFile)).toStrictEqual(contentConfiguration); @@ -269,7 +270,7 @@ describe('Settings', () => { write(devicesFile, contentDevices); const device = settings.getDevice('0x12345678'); const expected = { - ID: "0x12345678", + ID: '0x12345678', friendly_name: '0x12345678', retain: false, }; @@ -279,7 +280,7 @@ describe('Settings', () => { it('Should read devices form 2 separate files', () => { const contentConfiguration = { - devices: ['devices.yaml', 'devices2.yaml'] + devices: ['devices.yaml', 'devices2.yaml'], }; const contentDevices = { @@ -343,7 +344,7 @@ describe('Settings', () => { const contentDevices = { '0x12345678': { friendly_name: '0x12345678', - retain: false, + retain: false, }, }; @@ -358,9 +359,9 @@ describe('Settings', () => { const expected = { '0x12345678': { friendly_name: '0x12345678', - retain: false, + retain: false, }, - '0x1234': { + '0x1234': { friendly_name: '0x1234', }, }; @@ -373,13 +374,13 @@ describe('Settings', () => { extractFromMultipleDeviceConfigs({ '0x87654321': { friendly_name: '0x87654321', - retain: false, + retain: false, }, }); }); it('Should add devices for first file when using 2 separates file and the second file is empty', () => { - extractFromMultipleDeviceConfigs(null) + extractFromMultipleDeviceConfigs(null); }); it('Should add devices to a separate file if devices.yaml doesnt exist', () => { @@ -400,8 +401,7 @@ describe('Settings', () => { }; expect(read(devicesFile)).toStrictEqual(expected); - } - ); + }); it('Should add and remove devices to a separate file if devices.yaml doesnt exist', () => { const contentConfiguration = { @@ -417,13 +417,12 @@ describe('Settings', () => { expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); expect(read(devicesFile)).toStrictEqual({}); - } - ); + }); it('Should read groups', () => { const content = { groups: { - '1': { + 1: { friendly_name: '123', }, }, @@ -447,7 +446,7 @@ describe('Settings', () => { }; const contentGroups = { - '1': { + 1: { friendly_name: '123', }, }; @@ -472,7 +471,7 @@ describe('Settings', () => { }; const contentGroups = { - '1': { + 1: { friendly_name: '123', devices: [], }, @@ -523,7 +522,7 @@ describe('Settings', () => { const added = settings.addGroup('test123'); const expected = { - '1': { + 1: { friendly_name: 'test123', }, }; @@ -536,7 +535,7 @@ describe('Settings', () => { const added = settings.addGroup('test123', 123); const expected = { - '123': { + 123: { friendly_name: 'test123', }, }; @@ -560,7 +559,7 @@ describe('Settings', () => { settings.addGroup('test123'); }).toThrow(new Error("friendly_name 'test123' is already in use")); const expected = { - '1': { + 1: { friendly_name: 'test123', }, }; @@ -576,7 +575,7 @@ describe('Settings', () => { settings.addGroup('test_id_123', 123); }).toThrow(new Error("Group ID '123' is already in use")); const expected = { - '123': { + 123: { friendly_name: 'test123', }, }; @@ -590,14 +589,14 @@ describe('Settings', () => { '0x123': { friendly_name: 'bulb', retain: true, - } - } + }, + }, }); settings.addGroup('test123'); settings.addDeviceToGroup('test123', ['0x123']); const expected = { - '1': { + 1: { friendly_name: 'test123', devices: ['0x123'], }, @@ -612,19 +611,19 @@ describe('Settings', () => { '0x123': { friendly_name: 'bulb', retain: true, - } + }, }, groups: { - '1': { + 1: { friendly_name: 'test123', devices: ['0x123'], - } - } + }, + }, }); settings.removeDeviceFromGroup('test123', ['0x123']); const expected = { - '1': { + 1: { friendly_name: 'test123', devices: [], }, @@ -639,18 +638,18 @@ describe('Settings', () => { '0x123': { friendly_name: 'bulb', retain: true, - } + }, }, groups: { - '1': { + 1: { friendly_name: 'test123', - } - } + }, + }, }); settings.removeDeviceFromGroup('test123', ['0x123']); const expected = { - '1': { + 1: { friendly_name: 'test123', }, }; @@ -664,12 +663,12 @@ describe('Settings', () => { '0x123': { friendly_name: 'bulb', retain: true, - } + }, }, }); expect(() => { - settings.removeDeviceFromGroup('test123', 'bulb') + settings.removeDeviceFromGroup('test123', 'bulb'); }).toThrow(new Error("Group 'test123' does not exist")); }); @@ -679,12 +678,12 @@ describe('Settings', () => { '0x123': { friendly_name: 'bulb', retain: true, - } + }, }, }); expect(() => { - settings.addDevice('0x123') + settings.addDevice('0x123'); }).toThrow(new Error("Device '0x123' already exists")); }); @@ -735,7 +734,6 @@ describe('Settings', () => { expect(settings.validate()).toEqual([]); }); - it('Should not allow retention configuration without MQTT v5', () => { write(configurationFile, { ...minimalConfig, @@ -782,10 +780,13 @@ describe('Settings', () => { }); it('Should throw error when yaml file is invalid', () => { - fs.writeFileSync(configurationFile, ` + fs.writeFileSync( + configurationFile, + ` good: 9 \t wrong - `) + `, + ); settings.testing.clear(); const error = `Your YAML file: '${configurationFile}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`; @@ -813,7 +814,7 @@ describe('Settings', () => { write(configurationFile, { ...minimalConfig, devices: {'0x0017880104e45519': {friendly_name: 'myname', retain: false}}, - groups: {'1': {friendly_name: 'myname', retain: false}}, + groups: {1: {friendly_name: 'myname', retain: false}}, }); settings.reRead(); @@ -886,7 +887,7 @@ describe('Settings', () => { write(configurationFile, { devices: { '0x0017880104e45519': {friendly_name: 'myname', retain: false}, - '0x0017880104e45511': {friendly_name: 'myname1', retain: false} + '0x0017880104e45511': {friendly_name: 'myname1', retain: false}, }, }); @@ -901,7 +902,7 @@ describe('Settings', () => { write(configurationFile, { devices: { '0x0017880104e45519': {friendly_name: 'myname', retain: false}, - '0x0017880104e45511': {friendly_name: 'myname1', retain: false} + '0x0017880104e45511': {friendly_name: 'myname1', retain: false}, }, }); @@ -925,48 +926,48 @@ describe('Settings', () => { it('Should keep homeassistant null property on device setting change', () => { write(configurationFile, { - devices: { - '0x12345678': { - friendly_name: 'custom discovery', - homeassistant: { - entityXYZ: { - entity_category: null, - } - } - } - } - }); - settings.changeEntityOptions('0x12345678',{disabled: true}); + devices: { + '0x12345678': { + friendly_name: 'custom discovery', + homeassistant: { + entityXYZ: { + entity_category: null, + }, + }, + }, + }, + }); + settings.changeEntityOptions('0x12345678', {disabled: true}); const actual = read(configurationFile); const expected = { - devices: { - '0x12345678': { - friendly_name: 'custom discovery', - disabled: true, - homeassistant: { - entityXYZ: { - entity_category: null, - } - } + devices: { + '0x12345678': { + friendly_name: 'custom discovery', + disabled: true, + homeassistant: { + entityXYZ: { + entity_category: null, + }, + }, + }, }, - } }; expect(actual).toStrictEqual(expected); }); it('Should keep homeassistant null properties on apply', async () => { write(configurationFile, { - device_options: { - homeassistant: {temperature: null}, - }, - devices: { - '0x1234567812345678': { - friendly_name: 'custom discovery', - homeassistant: {humidity: null}, - } - } - }); + device_options: { + homeassistant: {temperature: null}, + }, + devices: { + '0x1234567812345678': { + friendly_name: 'custom discovery', + homeassistant: {humidity: null}, + }, + }, + }); settings.reRead(); settings.apply({permit_join: false}); expect(settings.get().device_options.homeassistant).toStrictEqual({temperature: null}); @@ -974,86 +975,76 @@ describe('Settings', () => { }); it('Frontend config', () => { - write(configurationFile, {...minimalConfig, - frontend: true, - }); + write(configurationFile, {...minimalConfig, frontend: true}); settings.reRead(); - expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false}) + expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false}); }); it('Baudrate config', () => { - write(configurationFile, {...minimalConfig, - advanced: {baudrate: 20}, - }); + write(configurationFile, {...minimalConfig, advanced: {baudrate: 20}}); settings.reRead(); - expect(settings.get().serial.baudrate).toStrictEqual(20) + expect(settings.get().serial.baudrate).toStrictEqual(20); }); it('ikea_ota_use_test_url config', () => { - write(configurationFile, {...minimalConfig, - advanced: {ikea_ota_use_test_url: true}, - }); + write(configurationFile, {...minimalConfig, advanced: {ikea_ota_use_test_url: true}}); settings.reRead(); - expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true) + expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true); }); it('transmit_power config', () => { - write(configurationFile, {...minimalConfig, - experimental: {transmit_power: 1337}, - }); + write(configurationFile, {...minimalConfig, experimental: {transmit_power: 1337}}); settings.reRead(); - expect(settings.get().advanced.transmit_power).toStrictEqual(1337) + expect(settings.get().advanced.transmit_power).toStrictEqual(1337); }); it('output config', () => { - write(configurationFile, {...minimalConfig, - experimental: {output: 'json'}, - }); + write(configurationFile, {...minimalConfig, experimental: {output: 'json'}}); settings.reRead(); - expect(settings.get().advanced.output).toStrictEqual('json') + expect(settings.get().advanced.output).toStrictEqual('json'); }); it('Baudrartsctste config', () => { - write(configurationFile, {...minimalConfig, - advanced: {rtscts: true}, - }); + write(configurationFile, {...minimalConfig, advanced: {rtscts: true}}); settings.reRead(); - expect(settings.get().serial.rtscts).toStrictEqual(true) + expect(settings.get().serial.rtscts).toStrictEqual(true); }); it('Deprecated: Home Assistant config', () => { - write(configurationFile, {...minimalConfig, + write(configurationFile, { + ...minimalConfig, homeassistant: {discovery_topic: 'new'}, advanced: {homeassistant_discovery_topic: 'old', homeassistant_status_topic: 'olds'}, }); settings.reRead(); - expect(settings.get().homeassistant).toStrictEqual({discovery_topic: 'new', legacy_entity_attributes: true, legacy_triggers: true, status_topic: 'olds'}) + expect(settings.get().homeassistant).toStrictEqual({ + discovery_topic: 'new', + legacy_entity_attributes: true, + legacy_triggers: true, + status_topic: 'olds', + }); }); it('Deprecated: ban/whitelist config', () => { - write(configurationFile, {...minimalConfig, - ban: ['ban'], whitelist: ['whitelist'], passlist: ['passlist'], blocklist: ['blocklist'] - }); + write(configurationFile, {...minimalConfig, ban: ['ban'], whitelist: ['whitelist'], passlist: ['passlist'], blocklist: ['blocklist']}); settings.reRead(); - expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban']) - expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist']) + expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban']); + expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist']); }); it('Deprecated: warn log level', () => { - write(configurationFile, {...minimalConfig, - advanced: {log_level: 'warn'} - }); + write(configurationFile, {...minimalConfig, advanced: {log_level: 'warn'}}); settings.reRead(); - expect(settings.get().advanced.log_level).toStrictEqual('warning') - }) + expect(settings.get().advanced.log_level).toStrictEqual('warning'); + }); }); diff --git a/test/stub/data.js b/test/stub/data.js index cb8a2f36..a2435532 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -12,121 +12,121 @@ function writeDefaultConfiguration() { homeassistant: false, permit_join: true, mqtt: { - base_topic: "zigbee2mqtt", - server: "mqtt://localhost", + base_topic: 'zigbee2mqtt', + server: 'mqtt://localhost', }, serial: { - "port": "/dev/dummy", + port: '/dev/dummy', }, devices: { - "0x000b57fffec6a5b2": { + '0x000b57fffec6a5b2': { retain: true, - friendly_name: "bulb", - description: "this is my bulb", + friendly_name: 'bulb', + description: 'this is my bulb', }, - "0x0017880104e45517": { + '0x0017880104e45517': { retain: true, - friendly_name: "remote" + friendly_name: 'remote', }, - "0x0017880104e45520": { + '0x0017880104e45520': { retain: false, - friendly_name: "button" + friendly_name: 'button', }, - "0x0017880104e45521": { + '0x0017880104e45521': { retain: false, - friendly_name: "button_double_key" + friendly_name: 'button_double_key', }, - "0x0017880104e45522": { + '0x0017880104e45522': { qos: 1, retain: false, - friendly_name: "weather_sensor" + friendly_name: 'weather_sensor', }, - "0x0017880104e45523": { + '0x0017880104e45523': { retain: false, - friendly_name: "occupancy_sensor" + friendly_name: 'occupancy_sensor', }, - "0x0017880104e45524": { + '0x0017880104e45524': { retain: false, - friendly_name: "power_plug" + friendly_name: 'power_plug', }, - "0x0017880104e45530": { + '0x0017880104e45530': { retain: false, - friendly_name: "button_double_key_interviewing" + friendly_name: 'button_double_key_interviewing', }, - "0x0017880104e45540": { - friendly_name: "ikea_onoff" + '0x0017880104e45540': { + friendly_name: 'ikea_onoff', }, '0x000b57fffec6a5b7': { retain: false, - friendly_name: "bulb_2" + friendly_name: 'bulb_2', }, - "0x000b57fffec6a5b3": { + '0x000b57fffec6a5b3': { retain: false, - friendly_name: "bulb_color" + friendly_name: 'bulb_color', }, '0x000b57fffec6a5b4': { retain: false, - friendly_name: "bulb_color_2" + friendly_name: 'bulb_color_2', }, - "0x0017880104e45541": { + '0x0017880104e45541': { retain: false, - friendly_name: "wall_switch" + friendly_name: 'wall_switch', }, - "0x0017880104e45542": { + '0x0017880104e45542': { retain: false, - friendly_name: "wall_switch_double" + friendly_name: 'wall_switch_double', }, - "0x0017880104e45543": { + '0x0017880104e45543': { retain: false, - friendly_name: "led_controller_1" + friendly_name: 'led_controller_1', }, - "0x0017880104e45544": { + '0x0017880104e45544': { retain: false, - friendly_name: "led_controller_2" + friendly_name: 'led_controller_2', }, '0x0017880104e45545': { retain: false, - friendly_name: "dimmer_wall_switch" + friendly_name: 'dimmer_wall_switch', }, '0x0017880104e45547': { retain: false, - friendly_name: "curtain" + friendly_name: 'curtain', }, '0x0017880104e45548': { retain: false, - friendly_name: 'fan' + friendly_name: 'fan', }, '0x0017880104e45549': { retain: false, - friendly_name: 'siren' + friendly_name: 'siren', }, '0x0017880104e45529': { retain: false, - friendly_name: 'unsupported2' + friendly_name: 'unsupported2', }, '0x0017880104e45550': { retain: false, - friendly_name: 'thermostat' + friendly_name: 'thermostat', }, '0x0017880104e45551': { retain: false, - friendly_name: 'smart vent' + friendly_name: 'smart vent', }, '0x0017880104e45552': { retain: false, - friendly_name: 'j1' + friendly_name: 'j1', }, '0x0017880104e45553': { retain: false, - friendly_name: 'bulb_enddevice' + friendly_name: 'bulb_enddevice', }, '0x0017880104e45559': { retain: false, - friendly_name: 'cc2530_router' + friendly_name: 'cc2530_router', }, '0x0017880104e45560': { retain: false, - friendly_name: 'livolo' + friendly_name: 'livolo', }, '0x90fd9ffffe4b64ae': { retain: false, @@ -151,7 +151,7 @@ function writeDefaultConfiguration() { friendly_name: 'GL-S-007ZS', }, '0x0017880104e43559': { - friendly_name: 'U202DST600ZB' + friendly_name: 'U202DST600ZB', }, '0xf4ce368a38be56a1': { retain: false, @@ -196,44 +196,44 @@ function writeDefaultConfiguration() { }, '0x0017880104e45562': { friendly_name: 'heating_actuator', - } + }, }, groups: { - '1': { + 1: { friendly_name: 'group_1', retain: false, }, - '2': { + 2: { friendly_name: 'group_2', retain: false, }, - '15071': { + 15071: { friendly_name: 'group_tradfri_remote', retain: false, - devices: ['bulb_color_2', 'bulb_2'] + devices: ['bulb_color_2', 'bulb_2'], }, - '11': { + 11: { friendly_name: 'group_with_tradfri', retain: false, - devices: ['bulb_2'] + devices: ['bulb_2'], }, - '12': { + 12: { friendly_name: 'thermostat_group', retain: false, devices: ['TS0601_thermostat'], }, - '14': { + 14: { friendly_name: 'switch_group', retain: false, devices: ['power_plug', 'bulb_2'], }, - '21': { + 21: { friendly_name: 'gledopto_group', devices: ['GLEDOPTO_2ID/cct'], }, - '9': { + 9: { friendly_name: 'ha_discovery_group', - devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'] + devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], }, }, external_converters: [], @@ -257,19 +257,19 @@ function stateExists() { } const defaultState = { - "0x000b57fffec6a5b2": { - "state": "ON", - "brightness": 50, - "color_temp": 370, - "linkquality": 99, + '0x000b57fffec6a5b2': { + state: 'ON', + brightness: 50, + color_temp: 370, + linkquality: 99, }, - "0x0017880104e45517": { - "brightness": 255 + '0x0017880104e45517': { + brightness: 255, }, - "1": { - 'state': 'ON', - } -} + 1: { + state: 'ON', + }, +}; function getDefaultState() { return defaultState; diff --git a/test/stub/logger.js b/test/stub/logger.js index c48be383..b9b9346c 100644 --- a/test/stub/logger.js +++ b/test/stub/logger.js @@ -11,7 +11,7 @@ const callTransports = (level, message, namespace) => { transport.log({level, message, namespace}, () => {}); } } -} +}; const mock = { init: jest.fn(), @@ -26,16 +26,24 @@ const mock = { removeTransport: (transport) => { transports = transports.filter((t) => t !== transport); }, - setLevel: (newLevel) => {level = newLevel}, + setLevel: (newLevel) => { + level = newLevel; + }, getLevel: () => level, - setNamespacedLevels: (nsLevels) => {namespacedLevels = nsLevels}, + setNamespacedLevels: (nsLevels) => { + namespacedLevels = nsLevels; + }, getNamespacedLevels: () => namespacedLevels, - setDebugNamespaceIgnore: (newIgnore) => {debugNamespaceIgnore = newIgnore}, + setDebugNamespaceIgnore: (newIgnore) => { + debugNamespaceIgnore = newIgnore; + }, getDebugNamespaceIgnore: () => debugNamespaceIgnore, - setTransportsEnabled: (value) => {transportsEnabled = value}, + setTransportsEnabled: (value) => { + transportsEnabled = value; + }, end: jest.fn(), }; -jest.mock('../../lib/util/logger', () => (mock)); +jest.mock('../../lib/util/logger', () => mock); module.exports = {...mock}; diff --git a/test/stub/mqtt.js b/test/stub/mqtt.js index 2e2fe682..c3287b89 100644 --- a/test/stub/mqtt.js +++ b/test/stub/mqtt.js @@ -7,13 +7,13 @@ const mock = { unsubscribe: jest.fn(), reconnecting: false, on: jest.fn(), - stream: {setMaxListeners: jest.fn()} + stream: {setMaxListeners: jest.fn()}, }; const mockConnect = jest.fn().mockReturnValue(mock); jest.mock('mqtt', () => { - return {connect: mockConnect}; + return {connect: mockConnect}; }); const restoreOnMock = () => { @@ -22,12 +22,16 @@ const restoreOnMock = () => { handler(); } - events[type] = handler + events[type] = handler; }); -} +}; restoreOnMock(); module.exports = { - events, ...mock, connect: mockConnect, mock, restoreOnMock -}; \ No newline at end of file + events, + ...mock, + connect: mockConnect, + mock, + restoreOnMock, +}; diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 7f779b2f..3f7ccba1 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -19,20 +19,20 @@ class Group { } const clusters = { - 'genBasic': 0, - 'genOta': 25, - 'genScenes': 5, - 'genOnOff': 6, - 'genLevelCtrl': 8, - 'lightingColorCtrl': 768, - 'closuresWindowCovering': 258, - 'hvacThermostat': 513, - 'msIlluminanceMeasurement': 1024, - 'msTemperatureMeasurement': 1026, - 'msRelativeHumidity': 1029, - 'msSoilMoisture': 1032, - 'msCO2': 1037 -} + genBasic: 0, + genOta: 25, + genScenes: 5, + genOnOff: 6, + genLevelCtrl: 8, + lightingColorCtrl: 768, + closuresWindowCovering: 258, + hvacThermostat: 513, + msIlluminanceMeasurement: 1024, + msTemperatureMeasurement: 1026, + msRelativeHumidity: 1029, + msSoilMoisture: 1032, + msCO2: 1037, +}; const custom_clusters = { custom_1: { @@ -42,14 +42,25 @@ const custom_clusters = { attribute_0: {ID: 0, type: 49}, }, commands: { - command_0: { ID: 0, response: 0, parameters: [{name: 'reset', type: 40}], }, + command_0: {ID: 0, response: 0, parameters: [{name: 'reset', type: 40}]}, }, commandsResponse: {}, }, -} +}; class Endpoint { - constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}, configuredReportings=[], profileID=null, deviceID=null, meta={}) { + constructor( + ID, + inputClusters, + outputClusters, + deviceIeeeAddress, + binds = [], + clusterValues = {}, + configuredReportings = [], + profileID = null, + deviceID = null, + meta = {}, + ) { this.deviceIeeeAddress = deviceIeeeAddress; this.clusterValues = clusterValues; this.ID = ID; @@ -68,32 +79,38 @@ class Endpoint { this.profileID = profileID; this.deviceID = deviceID; this.configuredReportings = configuredReportings; - this.getInputClusters = () => inputClusters.map((c) => { - return {ID: c, name: getKeyByValue(clusters, c)}; - }).filter((c) => c.name); + this.getInputClusters = () => + inputClusters + .map((c) => { + return {ID: c, name: getKeyByValue(clusters, c)}; + }) + .filter((c) => c.name); - this.getOutputClusters = () => outputClusters.map((c) => { - return {ID: c, name: getKeyByValue(clusters, c)}; - }).filter((c) => c.name); + this.getOutputClusters = () => + outputClusters + .map((c) => { + return {ID: c, name: getKeyByValue(clusters, c)}; + }) + .filter((c) => c.name); this.supportsInputCluster = (cluster) => { assert(clusters[cluster] !== undefined, `Undefined '${cluster}'`); return this.inputClusters.includes(clusters[cluster]); - } + }; this.supportsOutputCluster = (cluster) => { assert(clusters[cluster], `Undefined '${cluster}'`); return this.outputClusters.includes(clusters[cluster]); - } + }; this.addToGroup = jest.fn(); this.addToGroup.mockImplementation((group) => { if (!group.members.includes(this)) group.members.push(this); - }) + }); this.getDevice = () => { - return Object.values(devices).find(d => d.ieeeAddr === deviceIeeeAddress); - } + return Object.values(devices).find((d) => d.ieeeAddr === deviceIeeeAddress); + }; this.removeFromGroup = jest.fn(); this.removeFromGroup.mockImplementation((group) => { @@ -104,8 +121,8 @@ class Endpoint { }); this.removeFromAllGroups = () => { - Object.values(groups).forEach((g) => this.removeFromGroup(g)) - } + Object.values(groups).forEach((g) => this.removeFromGroup(g)); + }; this.getClusterAttributeValue = jest.fn(); this.getClusterAttributeValue.mockImplementation((cluster, value) => { @@ -116,7 +133,21 @@ class Endpoint { } class Device { - constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null, softwareBuildID=null, customClusters = {}) { + constructor( + type, + ieeeAddr, + networkAddress, + manufacturerID, + endpoints, + interviewCompleted, + powerSource = null, + modelID = null, + interviewing = false, + manufacturerName, + dateCode = null, + softwareBuildID = null, + customClusters = {}, + ) { this.type = type; this.ieeeAddr = ieeeAddr; this.dateCode = dateCode; @@ -147,88 +178,624 @@ class Device { const returnDevices = []; -const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "LLC020"); -const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4', [], {lightingColorCtrl: {colorCapabilities: 254}}, [], null, null, {'scenes': {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}})], true, "Mains (single phase)", "LLC020", false, 'Philips', '2019.09', '5.127.1.26581'); -const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"); -const TS0601_thermostat = new Device('EndDevice', '0x0017882104a44559', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44559')], true, "Mains (single phase)", 'kud7u2l'); -const TS0601_switch = new Device('EndDevice', '0x0017882104a44560', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44560')], true, "Mains (single phase)", 'kjintbl'); -const TS0601_cover_switch = new Device('EndDevice', '0x0017882104a44562', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44562')], true, "Mains (single phase)", 'TS0601', false, '_TZE200_5nldle7w'); -const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0, 6], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug"); -const GLEDOPTO_2ID = new Device('Router', '0x0017880104e45724', 6540,4151, [new Endpoint(11, [0,3,4,5,6,8,768], [], '0x0017880104e45724', [], {}, [], 49246, 528), new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258), new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694), new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256)], true, "Mains (single phase)", 'GL-C-007', false, 'GLEDOPTO'); -const QBKG03LM = new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2'); -const zigfred_plus = new Device('Router', '0xf4ce368a38be56a1', 6589, 0x129C, [new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xFC42], [0xFC42], '0xf4ce368a38be56a1'), new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1')], true, "Mains (single phase)", 'zigfred plus', false, 'Siglis'); +const bulb_color = new Device( + 'Router', + '0x000b57fffec6a5b3', + 40399, + 4107, + [ + new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5b3', [], { + lightingColorCtrl: {colorCapabilities: 254}, + }), + ], + true, + 'Mains (single phase)', + 'LLC020', +); +const bulb_color_2 = new Device( + 'Router', + '0x000b57fffec6a5b4', + 401292, + 4107, + [ + new Endpoint( + 1, + [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + [5, 25, 32, 4096], + '0x000b57fffec6a5b4', + [], + {lightingColorCtrl: {colorCapabilities: 254}}, + [], + null, + null, + {scenes: {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}}, + ), + ], + true, + 'Mains (single phase)', + 'LLC020', + false, + 'Philips', + '2019.09', + '5.127.1.26581', +); +const bulb_2 = new Device( + 'Router', + '0x000b57fffec6a5b7', + 40369, + 4476, + [new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})], + true, + 'Mains (single phase)', + 'TRADFRI bulb E27 WS opal 980lm', +); +const TS0601_thermostat = new Device( + 'EndDevice', + '0x0017882104a44559', + 6544, + 4151, + [new Endpoint(1, [], [], '0x0017882104a44559')], + true, + 'Mains (single phase)', + 'kud7u2l', +); +const TS0601_switch = new Device( + 'EndDevice', + '0x0017882104a44560', + 6544, + 4151, + [new Endpoint(1, [], [], '0x0017882104a44560')], + true, + 'Mains (single phase)', + 'kjintbl', +); +const TS0601_cover_switch = new Device( + 'EndDevice', + '0x0017882104a44562', + 6544, + 4151, + [new Endpoint(1, [], [], '0x0017882104a44562')], + true, + 'Mains (single phase)', + 'TS0601', + false, + '_TZE200_5nldle7w', +); +const ZNCZ02LM = new Device( + 'Router', + '0x0017880104e45524', + 6540, + 4151, + [new Endpoint(1, [0, 6], [], '0x0017880104e45524')], + true, + 'Mains (single phase)', + 'lumi.plug', +); +const GLEDOPTO_2ID = new Device( + 'Router', + '0x0017880104e45724', + 6540, + 4151, + [ + new Endpoint(11, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 528), + new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258), + new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694), + new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256), + ], + true, + 'Mains (single phase)', + 'GL-C-007', + false, + 'GLEDOPTO', +); +const QBKG03LM = new Device( + 'Router', + '0x0017880104e45542', + 6540, + 4151, + [ + new Endpoint(1, [0], [], '0x0017880104e45542'), + new Endpoint(2, [0, 6], [], '0x0017880104e45542'), + new Endpoint(3, [0, 6], [], '0x0017880104e45542'), + ], + true, + 'Mains (single phase)', + 'lumi.ctrl_neutral2', +); +const zigfred_plus = new Device( + 'Router', + '0xf4ce368a38be56a1', + 6589, + 0x129c, + [ + new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xfc42], [0xfc42], '0xf4ce368a38be56a1'), + new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), + new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), + new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), + new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), + new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), + new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), + ], + true, + 'Mains (single phase)', + 'zigfred plus', + false, + 'Siglis', +); const groups = { - 'group_1': new Group(1, []), - 'group_tradfri_remote': new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]), + group_1: new Group(1, []), + group_tradfri_remote: new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]), 'group/with/slashes': new Group(99, []), - 'group_with_tradfri': new Group(11, [bulb_2.endpoints[0]]), - 'thermostat_group': new Group(12, [TS0601_thermostat.endpoints[0]]), - 'group_with_switch': new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]), - 'gledopto_group': new Group(21, [GLEDOPTO_2ID.endpoints[3]]), - 'default_bind_group': new Group(901, []), - 'ha_discovery_group': new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]), -} + group_with_tradfri: new Group(11, [bulb_2.endpoints[0]]), + thermostat_group: new Group(12, [TS0601_thermostat.endpoints[0]]), + group_with_switch: new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]), + gledopto_group: new Group(21, [GLEDOPTO_2ID.endpoints[3]]), + default_bind_group: new Group(901, []), + ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]), +}; const devices = { - 'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false), - 'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2', [], {lightingColorCtrl: {colorCapabilities: 17}}, [{cluster: {name: 'genOnOff'}, attribute: {name: 'onOff'}, minimumReportInterval: 1, maximumReportInterval: 10, reportableChange: 20}])], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"), - 'bulb_color': bulb_color, - 'bulb_2': bulb_2, - 'bulb_color_2': bulb_color_2, - 'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45517', [{target: bulb_color.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}}, {target: groups.group_1, cluster: {ID: 6, name: 'genOnOff'}}, {target: groups.group_1, cluster: {ID: 6, name: 'genLevelCtrl'}}]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"), - 'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID", false, "notSupportedMfg"), - 'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"), - 'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true), - 'notInSettings': new Device('EndDevice', '0x0017880104e45519', 6537, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "lumi.sensor_switch.aq2"), - 'WXKG11LM': new Device('EndDevice', '0x0017880104e45520', 6537,4151, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45520', [], {}, [{cluster: {name: 'genOnOff'}, attribute: {name: undefined, ID: 1337}, minimumReportInterval: 1, maximumReportInterval: 10, reportableChange: 20}])], true, "Battery", "lumi.sensor_switch.aq2"), - 'WXKG02LM_rev1': new Device('EndDevice', '0x0017880104e45521', 6538,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Battery", "lumi.sensor_86sw2.es1"), - 'WSDCGQ11LM': new Device('EndDevice', '0x0017880104e45522', 6539,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.weather"), - 'RTCGQ11LM': new Device('EndDevice', '0x0017880104e45523', 6540,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.sensor_motion.aq2"), - 'ZNCZ02LM': ZNCZ02LM, - 'E1743': new Device('Router', '0x0017880104e45540', 6540,4476, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'TRADFRI on/off switch'), - 'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], [25]), new Endpoint(2, [0, 6], [])], true, "Mains (single phase)", 'lumi.ctrl_neutral1'), - 'QBKG03LM':QBKG03LM, - 'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540, 4151, [new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')], true, "Mains (single phase)", 'GL-C-008'), - 'GLEDOPTO111213': new Device('Router', '0x0017880104e45544', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], true, "Mains (single phase)", 'GL-C-008'), - 'GLEDOPTO_2ID': GLEDOPTO_2ID, - 'HGZB04D': new Device('Router', '0x0017880104e45545', 6540,4151, [new Endpoint(1, [0], [25], '0x0017880104e45545')], true, "Mains (single phase)", 'FB56+ZSC05HG1.0'), - 'ZNCLDJ11LM': new Device('Router', '0x0017880104e45547', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'lumi.curtain'), - 'HAMPTON99432': new Device('Router', '0x0017880104e45548', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'HDC52EastwindFan'), - 'HS2WD': new Device('Router', '0x0017880104e45549', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'WarningDevice'), - '1TST_EU': new Device('Router', '0x0017880104e45550', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'Thermostat'), - 'SV01': new Device('Router', '0x0017880104e45551', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'SV01-410-MP-1.0'), - 'J1': new Device('Router', '0x0017880104e45552', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'J1 (5502)'), - 'E11_G13': new Device('EndDevice', '0x0017880104e45553', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'E11-G13'), - 'nomodel': new Device('Router', '0x0017880104e45535', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", undefined, true), - 'unsupported_router': new Device('Router', '0x0017880104e45525', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", "notSupportedModelID", false, "Boef"), - 'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'), - 'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '), - 'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"), - 'roller_shutter': new Device('EndDevice', '0x90fd9ffffe4b64af', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64af')], true, "Battery", "SCM-R_00.00.03.15TC"), - 'ZNLDP12LM': new Device('Router', '0x90fd9ffffe4b64ax', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64ax', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "lumi.light.aqcn02"), - 'SP600_OLD': new Device('Router', '0x90fd9ffffe4b64aa', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20160120'), - 'SP600_NEW': new Device('Router', '0x90fd9ffffe4b64ab', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20170220'), - 'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false), - 'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'), - 'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'), - 'zigfred_plus': zigfred_plus, - '3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100', false, 'Centralite'), - 'J1': new Device('Router', '0x0017880104a44559', 6543,4151, [new Endpoint(1, [], [], '0x0017880104a44559')], true, "Mains (single phase)", 'J1 (5502)'), - 'TS0601_thermostat': TS0601_thermostat, - 'TS0601_switch': TS0601_switch, - 'TS0601_cover_switch': TS0601_cover_switch, - 'external_converter_device': new Device('EndDevice', '0x0017880104e45511', 1114, 'external', [new Endpoint(1, [], [], '0x0017880104e45511')], false, null, 'external_converter_device' ), - 'QS_Zigbee_D02_TRIAC_2C_LN':new Device('Router', '0x0017882194e45543', 6549,4151, [new Endpoint(1, [0], [], '0x0017882194e45543'), new Endpoint(2, [0, 6], [], '0x0017882194e45543')], true, "Mains (single phase)", 'TS110F', false, '_TYZB01_v8gtiaed'), - 'unknown': new Device('Router', '0x0017980134e45545', 6540,4151, [], true, "Mains (single phase)"), - 'temperature_sensor': new Device('EndDevice', '0x0017880104e45561', 6544,4151, [new Endpoint(1, [0,3,4,1026], [])], true, "Battery", "temperature.sensor"), - 'heating_actuator': new Device('Router', '0x0017880104e45562', 6545,4151, [new Endpoint(1, [0,3,4,513], [1026])], true, "Mains (single phase)", "heating.actuator"), - 'bj_scene_switch': new Device('EndDevice', '0xd85def11a1002caa', 50117, 4398, [new Endpoint(10, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa', [{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},]), new Endpoint(11, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa')], true, 'Battery', 'RB01', false, 'Busch-Jaeger', '20161222', '1.2.0'), - 'GW003-AS-IN-TE-FC': new Device('Router', '0x0017548104a44669', 6545,4699, [new Endpoint(1, [3], [0,3,513,514], '0x0017548104a44669')], true, "Mains (single phase)", 'Adapter Zigbee FUJITSU'), - 'BMCT-SLZ': new Device('Router', '0x18fc26000000cafe', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x18fc26000000cafe')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU'), - 'BMCT_SLZ': new Device('Router', '0x0026decafe000473', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x0026decafe000473')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU', false, null, null, null, custom_clusters), - 'bulb_custom_cluster': new Device('Router', '0x000b57fffec6a5c2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5c2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm", false, null, null, null, custom_clusters), -} + coordinator: new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false), + bulb: new Device( + 'Router', + '0x000b57fffec6a5b2', + 40369, + 4476, + [ + new Endpoint( + 1, + [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + [5, 25, 32, 4096], + '0x000b57fffec6a5b2', + [], + {lightingColorCtrl: {colorCapabilities: 17}}, + [ + { + cluster: {name: 'genOnOff'}, + attribute: {name: 'onOff'}, + minimumReportInterval: 1, + maximumReportInterval: 10, + reportableChange: 20, + }, + ], + ), + ], + true, + 'Mains (single phase)', + 'TRADFRI bulb E27 WS opal 980lm', + ), + bulb_color: bulb_color, + bulb_2: bulb_2, + bulb_color_2: bulb_color_2, + remote: new Device( + 'EndDevice', + '0x0017880104e45517', + 6535, + 4107, + [ + new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45517', [ + {target: bulb_color.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, + {target: bulb_color.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, + {target: bulb_color.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}}, + {target: groups.group_1, cluster: {ID: 6, name: 'genOnOff'}}, + {target: groups.group_1, cluster: {ID: 6, name: 'genLevelCtrl'}}, + ]), + new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6]), + ], + true, + 'Battery', + 'RWL021', + ), + unsupported: new Device( + 'EndDevice', + '0x0017880104e45518', + 6536, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Battery', + 'notSupportedModelID', + false, + 'notSupportedMfg', + ), + unsupported2: new Device( + 'EndDevice', + '0x0017880104e45529', + 6536, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Battery', + 'notSupportedModelID', + ), + interviewing: new Device( + 'EndDevice', + '0x0017880104e45530', + 6536, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Battery', + undefined, + true, + ), + notInSettings: new Device( + 'EndDevice', + '0x0017880104e45519', + 6537, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Battery', + 'lumi.sensor_switch.aq2', + ), + WXKG11LM: new Device( + 'EndDevice', + '0x0017880104e45520', + 6537, + 4151, + [ + new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45520', [], {}, [ + { + cluster: {name: 'genOnOff'}, + attribute: {name: undefined, ID: 1337}, + minimumReportInterval: 1, + maximumReportInterval: 10, + reportableChange: 20, + }, + ]), + ], + true, + 'Battery', + 'lumi.sensor_switch.aq2', + ), + WXKG02LM_rev1: new Device( + 'EndDevice', + '0x0017880104e45521', + 6538, + 4151, + [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + true, + 'Battery', + 'lumi.sensor_86sw2.es1', + ), + WSDCGQ11LM: new Device('EndDevice', '0x0017880104e45522', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), + RTCGQ11LM: new Device('EndDevice', '0x0017880104e45523', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.sensor_motion.aq2'), + ZNCZ02LM: ZNCZ02LM, + E1743: new Device('Router', '0x0017880104e45540', 6540, 4476, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'TRADFRI on/off switch'), + QBKG04LM: new Device( + 'Router', + '0x0017880104e45541', + 6549, + 4151, + [new Endpoint(1, [0], [25]), new Endpoint(2, [0, 6], [])], + true, + 'Mains (single phase)', + 'lumi.ctrl_neutral1', + ), + QBKG03LM: QBKG03LM, + GLEDOPTO1112: new Device( + 'Router', + '0x0017880104e45543', + 6540, + 4151, + [new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')], + true, + 'Mains (single phase)', + 'GL-C-008', + ), + GLEDOPTO111213: new Device( + 'Router', + '0x0017880104e45544', + 6540, + 4151, + [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], + true, + 'Mains (single phase)', + 'GL-C-008', + ), + GLEDOPTO_2ID: GLEDOPTO_2ID, + HGZB04D: new Device( + 'Router', + '0x0017880104e45545', + 6540, + 4151, + [new Endpoint(1, [0], [25], '0x0017880104e45545')], + true, + 'Mains (single phase)', + 'FB56+ZSC05HG1.0', + ), + ZNCLDJ11LM: new Device( + 'Router', + '0x0017880104e45547', + 6540, + 4151, + [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + true, + 'Mains (single phase)', + 'lumi.curtain', + ), + HAMPTON99432: new Device( + 'Router', + '0x0017880104e45548', + 6540, + 4151, + [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + true, + 'Mains (single phase)', + 'HDC52EastwindFan', + ), + HS2WD: new Device('Router', '0x0017880104e45549', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'WarningDevice'), + '1TST_EU': new Device('Router', '0x0017880104e45550', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'Thermostat'), + SV01: new Device('Router', '0x0017880104e45551', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'SV01-410-MP-1.0'), + J1: new Device('Router', '0x0017880104e45552', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'J1 (5502)'), + E11_G13: new Device('EndDevice', '0x0017880104e45553', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'E11-G13'), + nomodel: new Device( + 'Router', + '0x0017880104e45535', + 6536, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Mains (single phase)', + undefined, + true, + ), + unsupported_router: new Device( + 'Router', + '0x0017880104e45525', + 6536, + 0, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + true, + 'Mains (single phase)', + 'notSupportedModelID', + false, + 'Boef', + ), + CC2530_ROUTER: new Device('Router', '0x0017880104e45559', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'lumi.router'), + LIVOLO: new Device('Router', '0x0017880104e45560', 6541, 4152, [new Endpoint(6, [0, 6], [])], true, 'Mains (single phase)', 'TI0001 '), + tradfri_remote: new Device( + 'EndDevice', + '0x90fd9ffffe4b64ae', + 33906, + 4476, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64ae')], + true, + 'Battery', + 'TRADFRI remote control', + ), + roller_shutter: new Device( + 'EndDevice', + '0x90fd9ffffe4b64af', + 33906, + 4476, + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64af')], + true, + 'Battery', + 'SCM-R_00.00.03.15TC', + ), + ZNLDP12LM: new Device( + 'Router', + '0x90fd9ffffe4b64ax', + 33901, + 4476, + [ + new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64ax', [], { + lightingColorCtrl: {colorCapabilities: 254}, + }), + ], + true, + 'Mains (single phase)', + 'lumi.light.aqcn02', + ), + SP600_OLD: new Device( + 'Router', + '0x90fd9ffffe4b64aa', + 33901, + 4476, + [ + new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], { + seMetering: {multiplier: 1, divisor: 10000}, + }), + ], + true, + 'Mains (single phase)', + 'SP600', + false, + 'Salus', + '20160120', + ), + SP600_NEW: new Device( + 'Router', + '0x90fd9ffffe4b64ab', + 33901, + 4476, + [ + new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], { + seMetering: {multiplier: 1, divisor: 10000}, + }), + ], + true, + 'Mains (single phase)', + 'SP600', + false, + 'Salus', + '20170220', + ), + 'MKS-CM-W5': new Device( + 'Router', + '0x90fd9ffffe4b64ac', + 33901, + 4476, + [new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], {})], + true, + 'Mains (single phase)', + 'qnazj70', + false, + ), + 'GL-S-007ZS': new Device( + 'Router', + '0x0017880104e45526', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45526')], + true, + 'Mains (single phase)', + 'GL-S-007ZS', + ), + U202DST600ZB: new Device( + 'Router', + '0x0017880104e43559', + 6540, + 4151, + [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], + true, + 'Mains (single phase)', + 'U202DST600ZB', + ), + zigfred_plus: zigfred_plus, + 3157100: new Device( + 'Router', + '0x0017880104e44559', + 6542, + 4151, + [new Endpoint(1, [], [], '0x0017880104e44559')], + true, + 'Mains (single phase)', + '3157100', + false, + 'Centralite', + ), + J1: new Device( + 'Router', + '0x0017880104a44559', + 6543, + 4151, + [new Endpoint(1, [], [], '0x0017880104a44559')], + true, + 'Mains (single phase)', + 'J1 (5502)', + ), + TS0601_thermostat: TS0601_thermostat, + TS0601_switch: TS0601_switch, + TS0601_cover_switch: TS0601_cover_switch, + external_converter_device: new Device( + 'EndDevice', + '0x0017880104e45511', + 1114, + 'external', + [new Endpoint(1, [], [], '0x0017880104e45511')], + false, + null, + 'external_converter_device', + ), + QS_Zigbee_D02_TRIAC_2C_LN: new Device( + 'Router', + '0x0017882194e45543', + 6549, + 4151, + [new Endpoint(1, [0], [], '0x0017882194e45543'), new Endpoint(2, [0, 6], [], '0x0017882194e45543')], + true, + 'Mains (single phase)', + 'TS110F', + false, + '_TYZB01_v8gtiaed', + ), + unknown: new Device('Router', '0x0017980134e45545', 6540, 4151, [], true, 'Mains (single phase)'), + temperature_sensor: new Device( + 'EndDevice', + '0x0017880104e45561', + 6544, + 4151, + [new Endpoint(1, [0, 3, 4, 1026], [])], + true, + 'Battery', + 'temperature.sensor', + ), + heating_actuator: new Device( + 'Router', + '0x0017880104e45562', + 6545, + 4151, + [new Endpoint(1, [0, 3, 4, 513], [1026])], + true, + 'Mains (single phase)', + 'heating.actuator', + ), + bj_scene_switch: new Device( + 'EndDevice', + '0xd85def11a1002caa', + 50117, + 4398, + [ + new Endpoint(10, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], '0xd85def11a1002caa', [ + {target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, + {target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, + {target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}}, + ]), + new Endpoint(11, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], '0xd85def11a1002caa'), + ], + true, + 'Battery', + 'RB01', + false, + 'Busch-Jaeger', + '20161222', + '1.2.0', + ), + 'GW003-AS-IN-TE-FC': new Device( + 'Router', + '0x0017548104a44669', + 6545, + 4699, + [new Endpoint(1, [3], [0, 3, 513, 514], '0x0017548104a44669')], + true, + 'Mains (single phase)', + 'Adapter Zigbee FUJITSU', + ), + 'BMCT-SLZ': new Device( + 'Router', + '0x18fc26000000cafe', + 6546, + 4617, + [new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], '0x18fc26000000cafe')], + true, + 'Mains (single phase)', + 'RBSH-MMS-ZB-EU', + ), + BMCT_SLZ: new Device( + 'Router', + '0x0026decafe000473', + 6546, + 4617, + [new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], '0x0026decafe000473')], + true, + 'Mains (single phase)', + 'RBSH-MMS-ZB-EU', + false, + null, + null, + null, + custom_clusters, + ), + bulb_custom_cluster: new Device( + 'Router', + '0x000b57fffec6a5c2', + 40369, + 4476, + [new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5c2')], + true, + 'Mains (single phase)', + 'TRADFRI bulb E27 WS opal 980lm', + false, + null, + null, + null, + custom_clusters, + ), +}; const mock = { setTransmitPower: jest.fn(), @@ -252,13 +819,19 @@ const mock = { return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)); }), getDevicesByType: jest.fn().mockImplementation((type) => { - return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).filter((d) => d.type === type); + return Object.values(devices) + .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) + .filter((d) => d.type === type); }), getDeviceByIeeeAddr: jest.fn().mockImplementation((ieeeAddr) => { - return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).find((d) => d.ieeeAddr === ieeeAddr); + return Object.values(devices) + .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) + .find((d) => d.ieeeAddr === ieeeAddr); }), getDeviceByNetworkAddress: jest.fn().mockImplementation((networkAddress) => { - return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).find((d) => d.networkAddress === networkAddress); + return Object.values(devices) + .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) + .find((d) => d.networkAddress === networkAddress); }), getGroups: jest.fn().mockImplementation((query) => { return Object.values(groups); @@ -271,9 +844,9 @@ const mock = { reset: jest.fn(), createGroup: jest.fn().mockImplementation((groupID) => { const group = new Group(groupID, []); - groups[`group_${groupID}`] = group + groups[`group_${groupID}`] = group; return group; - }) + }), }; const mockConstructor = jest.fn().mockImplementation(() => mock); @@ -284,5 +857,11 @@ jest.mock('zigbee-herdsman', () => ({ })); module.exports = { - events, ...mock, constructor: mockConstructor, devices, groups, returnDevices, custom_clusters + events, + ...mock, + constructor: mockConstructor, + devices, + groups, + returnDevices, + custom_clusters, }; diff --git a/test/utils.test.js b/test/utils.test.js index e4fc9646..053e004a 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -7,25 +7,25 @@ describe('Utils', () => { it('Object has properties', () => { expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy(); expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy(); - }) + }); it('git last commit', async () => { let mockReturnValue = []; jest.mock('git-last-commit', () => ({ - getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]) + getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]), })); - mockReturnValue = [false, {shortHash: '123'}] - expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({"commitHash": "123", "version": version}); + mockReturnValue = [false, {shortHash: '123'}]; + expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: '123', version: version}); - mockReturnValue = [true, null] - expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({"commitHash": expect.any(String), "version": version}); - }) + mockReturnValue = [true, null]; + expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: expect.any(String), version: version}); + }); it('Check dependency version', async () => { - expect(await utils.getDependencyVersion('zigbee-herdsman')).toStrictEqual({"version": versionHerdsman}); - expect(await utils.getDependencyVersion('zigbee-herdsman-converters')).toStrictEqual({"version": versionHerdsmanConverters}); - }) + expect(await utils.getDependencyVersion('zigbee-herdsman')).toStrictEqual({version: versionHerdsman}); + expect(await utils.getDependencyVersion('zigbee-herdsman-converters')).toStrictEqual({version: versionHerdsmanConverters}); + }); it('To local iso string', async () => { var date = new Date('August 19, 1975 23:15:30 UTC+00:00'); @@ -35,7 +35,7 @@ describe('Utils', () => { Date.prototype.getTimezoneOffset = () => -60; expect(utils.formatDate(date, 'ISO_8601_local').endsWith('+01:00')).toBeTruthy(); Date.prototype.getTimezoneOffset = getTimezoneOffset; - }) + }); it('Removes null properties from object', () => { const obj1 = { ab: 0,