From 1b9e444b838297f50a7862c63301797b502936b8 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:18:23 +0200 Subject: [PATCH 1/2] fix!: Improve permit join (#24257) * fix: Improve permit join * Update Home Assistant permit join switch * Remove `permit_join` from `settings.schema.json` * Update zigbee-herdsman version to pre-release. * Fix pnpm overrides * Update test/homeassistant.test.js --------- Co-authored-by: Koen Kanters --- lib/controller.ts | 13 ---- lib/extension/bridge.ts | 37 +++--------- lib/extension/homeassistant.ts | 5 +- lib/types/types.d.ts | 1 - lib/util/settings.schema.json | 6 -- lib/util/settings.ts | 1 - lib/zigbee.ts | 16 ++--- package.json | 6 +- pnpm-lock.yaml | 36 ++++++----- test/bridge.test.js | 107 +++++++++------------------------ test/controller.test.js | 19 ------ test/homeassistant.test.js | 5 +- test/settings.test.js | 8 +-- test/stub/data.js | 1 - test/stub/zigbeeHerdsman.js | 3 +- 15 files changed, 78 insertions(+), 186 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index 60dcde1733..e36ed832a0 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -167,19 +167,6 @@ export class Controller { logger.info(`Currently ${deviceCount} devices are joined.`); - // Enable zigbee join - try { - if (settings.get().permit_join) { - logger.warning('`permit_join` set to `true` in configuration.yaml.'); - logger.warning('Allowing new devices to join.'); - logger.warning('Set `permit_join` to `false` once you joined all devices.'); - } - - await this.zigbee.permitJoin(settings.get().permit_join); - } catch (error) { - logger.error(`Failed to set permit join to ${settings.get().permit_join} (${(error as Error).message})`); - } - // MQTT try { await this.mqtt.connect(); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index d5d1549d30..69ff55963f 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -225,10 +225,6 @@ export default class Bridge extends Extension { if (restartRequired) this.restartRequired = true; // Apply some settings on-the-fly. - if (newSettings.permit_join != undefined) { - await this.zigbee.permitJoin(settings.get().permit_join); - } - if (newSettings.homeassistant != undefined) { await this.enableDisableExtension(!!settings.get().homeassistant, 'HomeAssistant'); } @@ -323,17 +319,15 @@ export default class Bridge extends Extension { } @bind async permitJoin(message: KeyValue | string): Promise { - if (typeof message === 'object' && message.value === undefined) { - throw new Error('Invalid payload'); - } - - let value: boolean | string; let time: number | undefined; let device: Device | undefined; if (typeof message === 'object') { - value = message.value; - time = message.time; + if (message.time === undefined) { + throw new Error('Invalid payload'); + } + + time = Number.parseInt(message.time, 10); if (message.device) { const resolved = this.zigbee.resolveEntity(message.device); @@ -345,25 +339,15 @@ export default class Bridge extends Extension { } } } else { - value = message; - } - - if (typeof value === 'string') { - value = value.toLowerCase() === 'true'; + time = Number.parseInt(message, 10); } - await this.zigbee.permitJoin(value, device, time); + await this.zigbee.permitJoin(time, device); - const response: {value: boolean; device?: string; time?: number} = {value}; + const response: {time: number; device?: string} = {time}; - if (typeof message === 'object') { - if (device) { - response.device = message.device; - } - - if (time != undefined) { - response.time = message.time; - } + if (device) { + response.device = device.name; } return utils.getResponse(message, response); @@ -679,7 +663,6 @@ export default class Bridge extends Extension { }, network: utils.toSnakeCaseObject(await this.zigbee.getNetworkParameters()), log_level: logger.getLevel(), - permit_join: this.zigbee.getPermitJoin(), permit_join_timeout: this.zigbee.getPermitJoinTimeout(), restart_required: this.restartRequired, config, diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index f0ea91e6df..fdc9bf94a8 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -2151,8 +2151,9 @@ export default class HomeAssistant extends Extension { value_template: '{{ value_json.permit_join | lower }}', command_topic: `${baseTopic}/request/permit_join`, state_on: 'true', - payload_on: '{"value": true, "time": 254}', - payload_off: 'false', + state_off: 'false', + payload_on: '{"time": 254}', + payload_off: '{"time": 0}', }, }, ); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 7f943ccf45..f468c8a282 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -119,7 +119,6 @@ declare global { legacy_entity_attributes: boolean; legacy_triggers: boolean; }; - permit_join: boolean; availability?: { active: {timeout: number}; passive: {timeout: number}; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index f52e6cbc4b..8b9a528c46 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -45,12 +45,6 @@ } ] }, - "permit_join": { - "type": "boolean", - "default": false, - "title": "Permit join", - "description": "Allow new devices to join (re-applied at restart)" - }, "availability": { "oneOf": [ { diff --git a/lib/util/settings.ts b/lib/util/settings.ts index d0d10e976b..69d0172316 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -43,7 +43,6 @@ const ajvRestartRequiredGroupOptions = new Ajv({allErrors: true}) .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}) .compile(schemaJson.definitions.group); const defaults: RecursivePartial = { - permit_join: false, external_converters: [], mqtt: { base_topic: 'zigbee2mqtt', diff --git a/lib/zigbee.ts b/lib/zigbee.ts index 5786cdc713..8a7e5e2b52 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -225,26 +225,18 @@ export default class Zigbee { logger.info('Stopped zigbee-herdsman'); } - getPermitJoin(): boolean { - return this.herdsman.getPermitJoin(); - } - - getPermitJoinTimeout(): number | undefined { + getPermitJoinTimeout(): number { return this.herdsman.getPermitJoinTimeout(); } - async permitJoin(permit: boolean, device?: Device, time?: number): Promise { - if (permit) { + async permitJoin(time: number, device?: Device): Promise { + if (time > 0) { logger.info(`Zigbee: allowing new devices to join${device ? ` via ${device.name}` : ''}.`); } else { logger.info('Zigbee: disabling joining new devices.'); } - if (device && permit) { - await this.herdsman.permitJoin(permit, device.zh, time); - } else { - await this.herdsman.permitJoin(permit, undefined, time); - } + await this.herdsman.permitJoin(time, device?.zh); } @bind private resolveDevice(ieeeAddr: string): Device | undefined { diff --git a/package.json b/package.json index 12170814e9..b0e7e5454f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "winston-syslog": "^2.7.1", "winston-transport": "^4.8.0", "ws": "^8.18.0", - "zigbee-herdsman": "2.1.3", + "zigbee-herdsman": "3.0.0-pre.0", "zigbee-herdsman-converters": "20.28.0", "zigbee2mqtt-frontend": "0.7.4" }, @@ -92,8 +92,8 @@ "typescript": "^5.6.3", "typescript-eslint": "^8.8.1" }, - "overrides": { - "zigbee-herdsman-converters": { + "pnpm": { + "overrides": { "zigbee-herdsman": "$zigbee-herdsman" } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d0bb94d2..a7378bcb44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + zigbee-herdsman: 3.0.0-pre.0 + importers: .: @@ -81,11 +84,11 @@ importers: specifier: ^8.18.0 version: 8.18.0 zigbee-herdsman: - specifier: 2.1.3 - version: 2.1.3 + specifier: 3.0.0-pre.0 + version: 3.0.0-pre.0 zigbee-herdsman-converters: - specifier: 20.25.0 - version: 20.25.0 + specifier: 20.28.0 + version: 20.28.0 zigbee2mqtt-frontend: specifier: 0.7.4 version: 0.7.4 @@ -95,13 +98,13 @@ importers: version: 2.8.0 devDependencies: '@babel/core': - specifier: ^7.25.7 + specifier: ^7.25.8 version: 7.25.8 '@babel/plugin-proposal-decorators': specifier: ^7.25.7 version: 7.25.7(@babel/core@7.25.8) '@babel/preset-env': - specifier: ^7.25.7 + specifier: ^7.25.8 version: 7.25.8(@babel/core@7.25.8) '@babel/preset-typescript': specifier: ^7.25.7 @@ -131,7 +134,7 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.7.4 + specifier: ^22.7.5 version: 22.7.5 '@types/object-assign-deep': specifier: ^0.4.3 @@ -164,10 +167,10 @@ importers: specifier: ^0.2.3 version: 0.2.3 typescript: - specifier: ^5.6.2 + specifier: ^5.6.3 version: 5.6.3 typescript-eslint: - specifier: ^8.8.0 + specifier: ^8.8.1 version: 8.8.1(eslint@9.12.0)(typescript@5.6.3) packages: @@ -2800,11 +2803,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zigbee-herdsman-converters@20.25.0: - resolution: {integrity: sha512-iqCQmcdwrUGz00IhvtB36l3YN9sCWPhbx8D+lf9owyjpE5To7u2pOn4GA4g6QWQCU7BndCif8fXed1FWt9/nrg==} + zigbee-herdsman-converters@20.28.0: + resolution: {integrity: sha512-AMCuG4mlIR21JgShIyFnZsCimfITH2HpjQLS0vaAqIKmbYrG7sBF0MU/iznYc/8JqNq+Jav3BdV+BrE1M3GpEg==} - zigbee-herdsman@2.1.3: - resolution: {integrity: sha512-1LiSb3L2ZFzNOuGJHMWBFPRgPs1WVMS4CagtvYxEVUdifhVivp1vMNrCxBsbwNhDiNBh/Blk0pTR0szJttLzrA==} + zigbee-herdsman@3.0.0-pre.0: + resolution: {integrity: sha512-3mCSmdwu5eJb0DEJrTDSQPYEQAz1mOGAbqvXyEOjo6UOllo++GyzpmawTxWBH4FLWrbuKc9SefiVqQdC/4uZbQ==} zigbee2mqtt-frontend@0.7.4: resolution: {integrity: sha512-skWNYxThSa6Ywn7aRB0ZvRKWifpqbku4+vUM5BbXiNaXYxCCbU0b3pN258Ahxt3NsLtYk2zBdYoQcXuBZxmJxw==} @@ -5939,7 +5942,7 @@ snapshots: yocto-queue@0.1.0: {} - zigbee-herdsman-converters@20.25.0: + zigbee-herdsman-converters@20.28.0: dependencies: axios: 1.7.7 buffer-crc32: 1.0.0 @@ -5947,12 +5950,13 @@ snapshots: iconv-lite: 0.6.3 semver: 7.6.3 tar-stream: 3.1.7 - zigbee-herdsman: 2.1.3 + uri-js: 4.4.1 + zigbee-herdsman: 3.0.0-pre.0 transitivePeerDependencies: - debug - supports-color - zigbee-herdsman@2.1.3: + zigbee-herdsman@3.0.0-pre.0: dependencies: '@serialport/bindings-cpp': 12.0.1 '@serialport/parser-delimiter': 12.0.0 diff --git a/test/bridge.test.js b/test/bridge.test.js index 40feccbd8c..2218a7a78b 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -64,6 +64,7 @@ describe('Bridge', () => { logger.warning.mockClear(); logger.setTransportsEnabled(false); MQTT.publish.mockClear(); + zigbeeHerdsman.permitJoin.mockClear(); const device = zigbeeHerdsman.devices.bulb; device.interview.mockClear(); device.removeFromDatabase.mockClear(); @@ -203,14 +204,13 @@ describe('Bridge', () => { mqtt: {base_topic: 'zigbee2mqtt', force_disable_retain: false, include_device_information: false, server: 'mqtt://localhost'}, ota: {disable_automatic_update_check: false, update_check_interval: 1440}, passlist: [], - permit_join: true, serial: {disable_led: false, port: '/dev/dummy'}, }, config_schema: settings.schema, 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, + permit_join_timeout: 0, restart_required: false, version: version.version, zigbee_herdsman: zhVersion, @@ -2584,50 +2584,47 @@ describe('Bridge', () => { ); }); - it('Should allow permit join', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', 'true'); + it('Should allow permit join on all', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: true}, status: 'ok'}), + stringify({data: {time: 1}, 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})); + it('Should disallow permit join on all', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(0, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), + stringify({data: {time: 0}, 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'})); + it('Should allow permit join with number string (automatically on all)', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', '1'); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), + stringify({data: {time: 1}, 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})); + it('Should not allow permit join with invalid payload', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( @@ -2638,21 +2635,6 @@ describe('Bridge', () => { ); }); - 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})); - 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), - ); - }); - it('Should republish bridge info when permit join changes', async () => { MQTT.publish.mockClear(); await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); @@ -2670,23 +2652,22 @@ describe('Bridge', () => { it('Should allow permit join via device', async () => { const device = zigbeeHerdsman.devices.bulb; - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: true, device: 'bulb'})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, device, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(123, device); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: true, device: 'bulb'}, status: 'ok'}), + stringify({data: {time: 123, device: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); + }); - // Device does not exist - zigbeeHerdsman.permitJoin.mockClear(); + it('Should not allow permit join via non-existing device', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: true, device: 'bulb_not_existing_woeeee'})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( @@ -2699,11 +2680,11 @@ describe('Bridge', () => { 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({time: 0, transaction: 22})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok', transaction: 22}), + stringify({data: {time: 0}, status: 'ok', transaction: 22}), {retain: false, qos: 0}, expect.any(Function), ); @@ -2714,7 +2695,7 @@ describe('Bridge', () => { 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({time: 0})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', @@ -3560,7 +3541,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3605,7 +3585,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3632,7 +3611,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3659,7 +3637,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3709,7 +3686,6 @@ describe('Bridge', () => { }); it('Should allow to restart', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/restart', ''); await flushPromises(); @@ -3723,24 +3699,6 @@ describe('Bridge', () => { ); }); - it('Change options', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - settings.apply({permit_join: false}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {permit_join: true}})); - await flushPromises(); - 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/response/options', - stringify({data: {restart_required: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - it('Change options and apply - homeassistant', async () => { expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).toBeUndefined(); MQTT.publish.mockClear(); @@ -3812,7 +3770,6 @@ describe('Bridge', () => { }); it('Change options restart required', async () => { - zigbeeHerdsman.permitJoin.mockClear(); settings.apply({serial: {port: '123'}}); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); @@ -3827,7 +3784,6 @@ describe('Bridge', () => { }); it('Change options array', async () => { - zigbeeHerdsman.permitJoin.mockClear(); expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]); MQTT.publish.mockClear(); MQTT.events.message( @@ -3845,7 +3801,6 @@ describe('Bridge', () => { }); it('Change options with null', async () => { - zigbeeHerdsman.permitJoin.mockClear(); 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}}})); @@ -3860,7 +3815,6 @@ describe('Bridge', () => { }); it('Change options invalid payload', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); await flushPromises(); @@ -3873,13 +3827,12 @@ describe('Bridge', () => { }); it('Change options not valid against schema', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {permit_join: 'true'}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({data: {}, error: 'permit_join must be boolean', status: 'error'}), + stringify({data: {}, error: 'external_converters must be array', status: 'error'}), {retain: false, qos: 0}, expect.any(Function), ); diff --git a/test/controller.test.js b/test/controller.test.js index 939daf82d3..82c243f044 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -10,7 +10,6 @@ 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, @@ -84,8 +83,6 @@ describe('Controller', () => { }); 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)', @@ -105,15 +102,6 @@ describe('Controller', () => { 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!'); - }); - await controller.start(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(MQTT.connect).toHaveBeenCalledTimes(1); - }); - it('Start controller with specific MQTT settings', async () => { const ca = tmp.fileSync().name; fs.writeFileSync(ca, 'ca'); @@ -283,13 +271,6 @@ describe('Controller', () => { expect(mockExit).toHaveBeenCalledWith(1, false); }); - it('Start controller with permit join true', async () => { - settings.set(['permit_join'], false); - await controller.start(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); - }); - it('Start controller and stop with restart', async () => { await controller.start(); await controller.stop(true); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 558524774e..23493d7f0a 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -2719,8 +2719,9 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.permit_join | lower }}', command_topic: 'zigbee2mqtt/bridge/request/permit_join', state_on: 'true', - payload_on: '{"value": true, "time": 254}', - payload_off: 'false', + state_off: 'false', + payload_on: '{"time": 254}', + payload_off: '{"time": 0}', origin: origin, device: devicePayload, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], diff --git a/test/settings.test.js b/test/settings.test.js index bdd50b3d9b..1cdf9e3e32 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -13,7 +13,7 @@ const yaml = require('js-yaml'); const objectAssignDeep = require(`object-assign-deep`); const minimalConfig = { - permit_join: true, + external_converters: [], homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt', server: 'localhost'}, }; @@ -55,12 +55,12 @@ describe('Settings', () => { }); it('Should return settings', () => { - write(configurationFile, {permit_join: true}); + write(configurationFile, {external_converters: ['abcd.js']}); const s = settings.get(); const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; - expected.permit_join = true; + expected.external_converters = ['abcd.js']; expect(s).toStrictEqual(expected); }); @@ -964,7 +964,7 @@ describe('Settings', () => { }, }); settings.reRead(); - settings.apply({permit_join: false}); + settings.apply({external_converters: []}); expect(settings.get().device_options.homeassistant).toStrictEqual({temperature: null}); expect(settings.get().devices['0x1234567812345678'].homeassistant).toStrictEqual({humidity: null}); }); diff --git a/test/stub/data.js b/test/stub/data.js index 66a9f80e49..8e00f0d2eb 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -10,7 +10,6 @@ const stateFile = path.join(mockDir, 'state.json'); function writeDefaultConfiguration() { const config = { homeassistant: false, - permit_join: true, mqtt: { base_topic: 'zigbee2mqtt', server: 'mqtt://localhost', diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 5cdfe3c563..75a12437ca 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -895,8 +895,7 @@ const mock = { getGroupByID: jest.fn().mockImplementation((groupID) => { return Object.values(groups).find((d) => d.groupID === groupID); }), - getPermitJoin: jest.fn().mockReturnValue(false), - getPermitJoinTimeout: jest.fn().mockReturnValue(undefined), + getPermitJoinTimeout: jest.fn().mockReturnValue(0), reset: jest.fn(), createGroup: jest.fn().mockImplementation((groupID) => { const group = new Group(groupID, []); From 9ea11fac14433446950633f30f4c3310ab22e693 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:33:54 +0200 Subject: [PATCH 2/2] fix: Refactor tests to TS (#24357) --- .prettierrc | 1 + index.js | 2 +- lib/controller.ts | 2 - lib/util/utils.ts | 9 +- lib/util/yaml.ts | 4 +- ...{controller.test.js => controller.test.ts} | 584 +++++---- test/{data.test.js => data.test.ts} | 10 +- .../availability.test.ts} | 157 +-- .../{bind.test.js => extensions/bind.test.ts} | 387 +++--- .../bridge.test.ts} | 1024 ++++++++------- .../configure.test.ts} | 163 +-- .../externalConverters.test.ts} | 92 +- .../externalExtension.test.ts} | 112 +- test/extensions/frontend.test.ts | 392 ++++++ .../groups.test.ts} | 738 ++++++----- .../homeassistant.test.ts} | 705 +++++----- .../networkMap.test.ts} | 243 ++-- test/extensions/onEvent.test.ts | 88 ++ test/extensions/otaUpdate.test.ts | 426 ++++++ .../publish.test.ts} | 1138 +++++++++-------- .../receive.test.ts} | 459 +++---- test/frontend.test.js | 414 ------ test/lib/flushPromises.js | 2 - test/{logger.test.js => logger.test.ts} | 119 +- test/{stub/data.js => mocks/data.ts} | 45 +- test/mocks/debounce.ts | 3 + test/mocks/jszip.ts | 11 + test/mocks/logger.ts | 53 + test/{stub/mqtt.js => mocks/mqtt.ts} | 35 +- test/mocks/sleep.ts | 11 + test/mocks/types.d.ts | 15 + test/mocks/utils.ts | 15 + .../zigbeeHerdsman.ts} | 572 ++++++--- test/onEvent.test.js | 80 -- test/otaUpdate.test.js | 459 ------- test/{settings.test.js => settings.test.ts} | 67 +- test/stub/logger.js | 48 - test/stub/sleep.js | 10 - test/tsconfig.json | 10 + test/{utils.test.js => utils.test.ts} | 34 +- tsconfig.json | 7 +- 41 files changed, 4576 insertions(+), 4170 deletions(-) rename test/{controller.test.js => controller.test.ts} (59%) rename test/{data.test.js => data.test.ts} (83%) rename test/{availability.test.js => extensions/availability.test.ts} (76%) rename test/{bind.test.js => extensions/bind.test.ts} (64%) rename test/{bridge.test.js => extensions/bridge.test.ts} (84%) rename test/{configure.test.js => extensions/configure.test.ts} (63%) rename test/{externalConverters.test.js => extensions/externalConverters.test.ts} (57%) rename test/{externalExtension.test.js => extensions/externalExtension.test.ts} (56%) create mode 100644 test/extensions/frontend.test.ts rename test/{group.test.js => extensions/groups.test.ts} (52%) rename test/{homeassistant.test.js => extensions/homeassistant.test.ts} (84%) rename test/{networkMap.test.js => extensions/networkMap.test.ts} (83%) create mode 100644 test/extensions/onEvent.test.ts create mode 100644 test/extensions/otaUpdate.test.ts rename test/{publish.test.js => extensions/publish.test.ts} (56%) rename test/{receive.test.js => extensions/receive.test.ts} (55%) mode change 100755 => 100644 delete mode 100644 test/frontend.test.js delete mode 100644 test/lib/flushPromises.js rename test/{logger.test.js => logger.test.ts} (79%) rename test/{stub/data.js => mocks/data.ts} (91%) create mode 100644 test/mocks/debounce.ts create mode 100644 test/mocks/jszip.ts create mode 100644 test/mocks/logger.ts rename test/{stub/mqtt.js => mocks/mqtt.ts} (50%) create mode 100644 test/mocks/sleep.ts create mode 100644 test/mocks/types.d.ts create mode 100644 test/mocks/utils.ts rename test/{stub/zigbeeHerdsman.js => mocks/zigbeeHerdsman.ts} (62%) delete mode 100644 test/onEvent.test.js delete mode 100644 test/otaUpdate.test.js rename test/{settings.test.js => settings.test.ts} (95%) delete mode 100644 test/stub/logger.js delete mode 100644 test/stub/sleep.js create mode 100644 test/tsconfig.json rename test/{utils.test.js => utils.test.ts} (68%) diff --git a/.prettierrc b/.prettierrc index 0c99705e7f..aa720a4e85 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,6 +7,7 @@ "endOfLine": "lf", "tabWidth": 4, "importOrder": [ + "^[./]*/mocks", "", "^(node:)", "", diff --git a/index.js b/index.js index bfc1eecedd..61789ea726 100644 --- a/index.js +++ b/index.js @@ -148,7 +148,7 @@ async function start() { return exit(1); } - const Controller = require('./dist/controller'); + const {Controller} = require('./dist/controller'); controller = new Controller(restart, exit); await controller.start(); diff --git a/lib/controller.ts b/lib/controller.ts index e36ed832a0..8d5ecde8ca 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -366,5 +366,3 @@ export class Controller { } } } - -module.exports = Controller; diff --git a/lib/util/utils.ts b/lib/util/utils.ts index bde76c8a69..7985049725 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -10,6 +10,11 @@ import humanizeDuration from 'humanize-duration'; import data from './data'; +function pad(num: number): string { + const norm = Math.floor(Math.abs(num)); + return (norm < 10 ? '0' : '') + norm; +} + // construct a local ISO8601 string (instead of UTC-based) // Example: // - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000 @@ -17,10 +22,6 @@ import data from './data'; function toLocalISOString(date: Date): string { const tzOffset = -date.getTimezoneOffset(); const plusOrMinus = tzOffset >= 0 ? '+' : '-'; - const pad = (num: number): string => { - const norm = Math.floor(Math.abs(num)); - return (norm < 10 ? '0' : '') + norm; - }; return ( date.getFullYear() + diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index 2c77db2c45..b3aed60168 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import fs from 'fs'; import equals from 'fast-deep-equal/es6'; @@ -20,7 +21,8 @@ export class YAMLFileException extends YAMLException { function read(file: string): KeyValue { try { const result = yaml.load(fs.readFileSync(file, 'utf8')); - return (result as KeyValue) ?? {}; + assert(result instanceof Object); + return result as KeyValue; } catch (error) { if (error instanceof YAMLException) { throw new YAMLFileException(error, file); diff --git a/test/controller.test.js b/test/controller.test.ts similarity index 59% rename from test/controller.test.js rename to test/controller.test.ts index 82c243f044..671a8dd21c 100644 --- a/test/controller.test.js +++ b/test/controller.test.ts @@ -1,56 +1,60 @@ -process.env.NOTIFY_SOCKET = 'mocked'; -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const path = require('path'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const stringify = require('json-stable-stringify-without-jsonify'); -const flushPromises = require('./lib/flushPromises'); -const tmp = require('tmp'); -const mocksClear = [ - MQTT.end, - zigbeeHerdsman.stop, - logger.debug, - MQTT.publish, - MQTT.connect, - zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, - logger.error, -]; +import * as data from './mocks/data'; +import {mockLogger} from './mocks/logger'; +import {mockMQTT, mockMQTTConnect, events as mockMQTTEvents} from './mocks/mqtt'; +import {EventHandler, flushPromises, JestMockAny} from './mocks/utils'; +import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from './mocks/zigbeeHerdsman'; + +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import tmp from 'tmp'; -const fs = require('fs'); +import {Controller as ZHController} from 'zigbee-herdsman'; +import {Controller} from '../lib/controller'; +import * as settings from '../lib/util/settings'; + +process.env.NOTIFY_SOCKET = 'mocked'; const LOG_MQTT_NS = 'z2m:mqtt'; jest.mock( 'sd-notify', () => { return { - watchdogInterval: () => { - return 3000; - }, - startWatchdogMode: (interval) => {}, - stopWatchdogMode: () => {}, - ready: () => {}, - stopping: () => {}, + watchdogInterval: jest.fn(() => 3000), + startWatchdogMode: jest.fn(), + stopWatchdogMode: jest.fn(), + ready: jest.fn(), + stopping: jest.fn(), }; }, {virtual: true}, ); +const mocksClear = [ + mockZHController.stop, + mockMQTT.end, + mockMQTT.publish, + mockMQTTConnect, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockLogger.log, + mockLogger.debug, + mockLogger.info, + mockLogger.error, +]; + describe('Controller', () => { - let controller; - let mockExit; + let controller: Controller; + let mockExit: JestMockAny; beforeAll(async () => { jest.useFakeTimers(); }); beforeEach(() => { - MQTT.restoreOnMock(); - zigbeeHerdsman.returnDevices.splice(0); + returnDevices.splice(0); mockExit = jest.fn(); data.writeDefaultConfiguration(); settings.reRead(); @@ -67,7 +71,7 @@ describe('Controller', () => { it('Start controller', async () => { settings.set(['advanced', 'transmit_power'], 14); await controller.start(); - expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({ + expect(ZHController).toHaveBeenCalledWith({ network: { panID: 6754, extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], @@ -81,25 +85,29 @@ describe('Controller', () => { adapter: {concurrent: undefined, delay: undefined, disableLED: false, transmitPower: 14}, serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'}, }); - expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`); - expect(logger.info).toHaveBeenCalledWith( + expect(mockZHController.start).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith(`Currently ${Object.values(devices).length - 1} devices are joined.`); + expect(mockLogger.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', { + expect(mockLogger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); + expect(mockLogger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', { will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote', + stringify({brightness: 255}), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Start controller with specific MQTT settings', async () => { @@ -126,7 +134,7 @@ describe('Controller', () => { settings.set(['mqtt'], configuration); await controller.start(); await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); const expected = { will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, keepalive: 30, @@ -139,7 +147,7 @@ describe('Controller', () => { rejectUnauthorized: false, protocolVersion: 5, }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => { @@ -148,9 +156,9 @@ describe('Controller', () => { settings.set(['advanced', 'ext_pan_id'], 'GENERATE'); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.networkKey.length).toStrictEqual(16); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number)); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.networkKey.length).toStrictEqual(16); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number)); expect(data.read().advanced.network_key.length).toStrictEqual(16); expect(data.read().advanced.ext_pan_id.length).toStrictEqual(8); expect(data.read().advanced.pan_id).toStrictEqual(expect.any(Number)); @@ -160,14 +168,19 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote', + stringify({brightness: 255}), + {qos: 0, retain: true}, + expect.any(Function), + ); + expect(mockMQTT.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 () => { @@ -175,7 +188,7 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - const publishedTopics = MQTT.publish.mock.calls.map((m) => m[0]); + const publishedTopics = mockMQTT.publish.mock.calls.map((m) => m[0]); expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote'])); }); @@ -184,30 +197,34 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.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(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}, expect.any(Function)); }); it('Log when MQTT client is unavailable', async () => { await controller.start(); await flushPromises(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.mqtt.client.reconnecting = true; jest.advanceTimersByTime(11 * 1000); - expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + // @ts-expect-error private controller.mqtt.client.reconnecting = false; }); it('Dont publish to mqtt when client is unavailable', async () => { await controller.start(); await flushPromises(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.mqtt.client.reconnecting = true; + // @ts-expect-error private const device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, { state: 'ON', @@ -217,11 +234,12 @@ describe('Controller', () => { 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( + expect(mockLogger.error).toHaveBeenCalledTimes(2); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + expect(mockLogger.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"}', ); + // @ts-expect-error private controller.mqtt.client.reconnecting = false; }); @@ -229,30 +247,31 @@ describe('Controller', () => { data.removeState(); await controller.start(); await flushPromises(); + // @ts-expect-error private expect(controller.state.state).toStrictEqual({}); }); it('Should remove device not on passlist on startup', async () => { - settings.set(['passlist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => { + settings.set(['passlist'], [devices.bulb_color.ieeeAddr]); + devices.bulb.removeFromNetwork.mockImplementationOnce(() => { throw new Error('dummy'); }); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); + expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); }); it('Should remove device on blocklist on startup', async () => { - settings.set(['blocklist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); + settings.set(['blocklist'], [devices.bulb_color.ieeeAddr]); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); + expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); }); it('Start controller fails', async () => { - zigbeeHerdsman.start.mockImplementationOnce(() => { + mockZHController.start.mockImplementationOnce(() => { throw new Error('failed'); }); await controller.start(); @@ -260,13 +279,16 @@ describe('Controller', () => { }); it('Start controller fails due to MQTT', async () => { - MQTT.on.mockImplementation((type, handler) => { - if (type === 'error') handler({message: 'addr not found'}); - }); + const cb = (type: string, handler: EventHandler): void => { + if (type === 'error') { + handler({message: 'addr not found'}); + } + }; + mockMQTT.on.mockImplementationOnce(cb).mockImplementationOnce(cb); await controller.start(); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found'); - expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)'); + expect(mockLogger.error).toHaveBeenCalledWith('MQTT error: addr not found'); + expect(mockLogger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)'); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); @@ -274,65 +296,62 @@ describe('Controller', () => { it('Start controller and stop with restart', async () => { await controller.start(); await controller.stop(true); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(0, true); }); it('Start controller and stop', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => { - throw new Error('failed'); - }); + mockZHController.stop.mockRejectedValueOnce('failed'); await controller.start(); await controller.stop(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); it('Start controller adapter disconnects', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => { - throw new Error('failed'); - }); + mockZHController.stop.mockRejectedValueOnce('failed'); await controller.start(); - await zigbeeHerdsman.events.adapterDisconnected(); + await mockZHEvents.adapterDisconnected(); await flushPromises(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); it('Handle mqtt message', async () => { - const eventbus = controller.eventBus; - let spyEventbusEmitMQTTMessage = jest.spyOn(eventbus, 'emitMQTTMessage').mockImplementation(); + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); await controller.start(); - logger.debug.mockClear(); - await MQTT.events.message('dummytopic', 'dummymessage'); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('dummytopic', 'dummymessage'); expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'dummytopic', message: 'dummymessage'}); - expect(logger.log).toHaveBeenCalledWith('debug', "Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS); + expect(mockLogger.log).toHaveBeenCalledWith('debug', "Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS); }); it('Skip MQTT messages on topic we published to', async () => { - const eventbus = controller.eventBus; - let spyEventbusEmitMQTTMessage = jest.spyOn(eventbus, 'emitMQTTMessage').mockImplementation(); + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); await controller.start(); - logger.debug.mockClear(); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'zigbee2mqtt/skip-this-topic', message: 'skipped'}); - logger.debug.mockClear(); + mockLogger.debug.mockClear(); + // @ts-expect-error private await controller.mqtt.publish('skip-this-topic', '', {}); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledTimes(0); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); + expect(mockLogger.debug).toHaveBeenCalledTimes(0); }); it('On zigbee event message', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = { device, endpoint: device.getEndpoint(1), @@ -341,9 +360,9 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`, 'z2m', @@ -352,7 +371,7 @@ describe('Controller', () => { it('On zigbee event message with group ID', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = { device, endpoint: device.getEndpoint(1), @@ -362,9 +381,9 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`, 'z2m', @@ -373,17 +392,17 @@ describe('Controller', () => { it('Should add entities which are missing from configuration but are in database to configuration', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.notInSettings; + const device = devices.notInSettings; expect(settings.getDevice(device.ieeeAddr)).not.toBeUndefined(); }); it('On zigbee deviceJoined', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - await zigbeeHerdsman.events.deviceJoined(payload); + await mockZHEvents.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -393,60 +412,60 @@ describe('Controller', () => { it('acceptJoiningDeviceHandler reject device on blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(false); }); it('acceptJoiningDeviceHandler accept device not on blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['blocklist'], ['123']); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler accept device on passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler reject device not in passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], ['123']); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(false); }); it('acceptJoiningDeviceHandler should prefer passlist above blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], [device.ieeeAddr]); settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler accept when not on blocklist and passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const device = devices.bulb; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('Shouldnt crash when two device join events are received', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - zigbeeHerdsman.events.deviceJoined(payload); - zigbeeHerdsman.events.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -456,11 +475,11 @@ describe('Controller', () => { it('On zigbee deviceInterview started', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device, status: 'started'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -470,11 +489,11 @@ describe('Controller', () => { it('On zigbee deviceInterview failed', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device, status: 'failed'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -484,13 +503,13 @@ describe('Controller', () => { it('On zigbee deviceInterview successful supported', async () => { await controller.start(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.bulb; + mockMQTT.publish.mockClear(); + const device = devices.bulb; const payload = {device, status: 'successful'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); - const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publish.mock.calls[1][1]); expect(parsedMessage.type).toStrictEqual('device_interview'); expect(parsedMessage.data.friendly_name).toStrictEqual('bulb'); expect(parsedMessage.data.status).toStrictEqual('successful'); @@ -501,18 +520,18 @@ describe('Controller', () => { expect(parsedMessage.data.definition.description).toStrictEqual('TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm'); expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee deviceInterview successful not supported', async () => { await controller.start(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.unsupported; + mockMQTT.publish.mockClear(); + const device = devices.unsupported; const payload = {device, status: 'successful'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); - const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publish.mock.calls[1][1]); expect(parsedMessage.type).toStrictEqual('device_interview'); expect(parsedMessage.data.friendly_name).toStrictEqual(device.ieeeAddr); expect(parsedMessage.data.status).toStrictEqual('successful'); @@ -523,17 +542,17 @@ describe('Controller', () => { expect(parsedMessage.data.definition.description).toStrictEqual('Automatically generated definition'); expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee event device announce', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - await zigbeeHerdsman.events.deviceAnnounce(payload); + await mockZHEvents.deviceAnnounce(payload); await flushPromises(); - expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -543,14 +562,14 @@ describe('Controller', () => { it('On zigbee event device leave (removed from database and settings)', async () => { await controller.start(); - zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); + returnDevices.push('0x00124b00120144ae'); settings.set(['devices'], {}); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.bulb; + mockMQTT.publish.mockClear(); + const device = devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; - await zigbeeHerdsman.events.deviceLeave(payload); + await mockZHEvents.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -560,13 +579,13 @@ describe('Controller', () => { it('On zigbee event device leave (removed from database and NOT settings)', async () => { await controller.start(); - zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); + returnDevices.push('0x00124b00120144ae'); + const device = devices.bulb; + mockMQTT.publish.mockClear(); const payload = {ieeeAddr: device.ieeeAddr}; - await zigbeeHerdsman.events.deviceLeave(payload); + await mockZHEvents.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}), {retain: false, qos: 0}, @@ -577,8 +596,9 @@ describe('Controller', () => { it('Publish entity state attribute output', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute'); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, { dummy: {1: 'yes', 2: 'no'}, color: {r: 100, g: 50, b: 10}, @@ -589,29 +609,30 @@ describe('Controller', () => { 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(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}, expect.any(Function)); }); it('Publish entity state attribute_json output', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}), {qos: 0, retain: true}, @@ -622,15 +643,16 @@ describe('Controller', () => { it('Publish entity state attribute_json output filtered', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -642,14 +664,15 @@ describe('Controller', () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -660,21 +683,24 @@ describe('Controller', () => { it('Publish entity state attribute_json output filtered cache', async () => { await controller.start(); settings.set(['advanced', 'output'], 'attribute_and_json'); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); - MQTT.publish.mockClear(); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); + mockMQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private 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(); + // @ts-expect-error private 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), {qos: 0, retain: true}, @@ -686,20 +712,23 @@ describe('Controller', () => { await controller.start(); settings.set(['advanced', 'output'], 'attribute_and_json'); settings.set(['device_options', 'filtered_cache'], ['linkquality']); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private 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(); + // @ts-expect-error private 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), {qos: 0, retain: true}, @@ -710,11 +739,12 @@ describe('Controller', () => { it('Publish entity state with device information', async () => { await controller.start(); settings.set(['mqtt', 'include_device_information'], true); - MQTT.publish.mockClear(); - let device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + let device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -729,8 +759,6 @@ describe('Controller', () => { type: 'Router', manufacturerID: 4476, powerSource: 'Mains (single phase)', - dateCode: null, - softwareBuildID: null, }, }), {qos: 0, retain: true}, @@ -738,10 +766,11 @@ describe('Controller', () => { ); // Unsupported device should have model "unknown" - device = controller.zigbee.resolveEntity('unsupported2'); + // @ts-expect-error private + device = controller.zigbee.resolveEntity('unsupported2')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/unsupported2', stringify({ state: 'ON', @@ -753,8 +782,6 @@ describe('Controller', () => { type: 'EndDevice', manufacturerID: 0, powerSource: 'Battery', - dateCode: null, - softwareBuildID: null, }, }), {qos: 0, retain: false}, @@ -764,12 +791,13 @@ describe('Controller', () => { it('Should publish entity state without retain', async () => { await controller.start(); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], false); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], false); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: false}, @@ -779,12 +807,13 @@ describe('Controller', () => { it('Should publish entity state with retain', async () => { await controller.start(); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], true); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: true}, @@ -795,13 +824,14 @@ describe('Controller', () => { it('Should publish entity state with expiring retention', async () => { await controller.start(); settings.set(['mqtt', 'version'], 5); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], true); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retention'], 37); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + settings.set(['devices', devices.bulb.ieeeAddr, 'retention'], 37); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: true, properties: {messageExpiryInterval: 37}}, @@ -812,25 +842,27 @@ describe('Controller', () => { it('Publish entity state no empty messages', async () => { data.writeEmptyState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should allow to disable state persistency', async () => { settings.set(['advanced', 'cache_state_persistent'], false); data.removeState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); 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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -843,30 +875,34 @@ describe('Controller', () => { it('Shouldnt crash when it cannot save state', async () => { data.removeState(); await controller.start(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.state.file = '/'; - await controller.state.save(); - expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to \'\/\'/)); + // @ts-expect-error private + controller.state.save(); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to '\/'/)); }); it('Publish should not cache when set', async () => { settings.set(['advanced', 'cache_state'], false); data.writeEmptyState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); 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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}, expect.any(Function)); }); it('Should start when state is corrupted', async () => { fs.writeFileSync(path.join(data.mockDir, 'state.json'), 'corrupted'); await controller.start(); await flushPromises(); + // @ts-expect-error private expect(controller.state.state).toStrictEqual({}); }); @@ -874,52 +910,54 @@ describe('Controller', () => { settings.set(['mqtt', 'force_disable_retain'], true); await controller.start(); await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); const expected = { will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should republish retained messages on MQTT reconnect', async () => { await controller.start(); - MQTT.publish.mockClear(); - MQTT.events['connect'](); + mockMQTT.publish.mockClear(); + mockMQTTEvents['connect'](); await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers - expect(MQTT.publish).toHaveBeenCalledTimes(13); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(13); + expect(mockMQTT.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 () => { await controller.start(); - MQTT.publish.mockClear(); - MQTT.events['connect'](); + mockMQTT.publish.mockClear(); + mockMQTTEvents['connect'](); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bridge/info', 'dummy'); + await mockMQTTEvents.message('zigbee2mqtt/bridge/info', 'dummy'); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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); + // @ts-expect-error private await controller.mqtt.connect(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish last seen changes', async () => { settings.set(['advanced', 'last_seen'], 'epoch'); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.remote; - await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'deviceAnnounce'}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + mockMQTT.publish.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'deviceAnnounce'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({brightness: 255, last_seen: 1000}), {qos: 0, retain: true}, @@ -931,16 +969,16 @@ describe('Controller', () => { settings.set(['advanced', 'last_seen'], 'epoch'); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.remote; - await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'messageEmitted'}); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + mockMQTT.publish.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'messageEmitted'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Ignore messages from coordinator', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/9218 await controller.start(); - const device = zigbeeHerdsman.devices.coordinator; + const device = devices.coordinator; const payload = { device, endpoint: device.getEndpoint(1), @@ -949,33 +987,37 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', - `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`, + `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{}' from endpoint 1, ignoring since it is from coordinator`, 'z2m', ); }); it('Should remove state of removed device when stopped', async () => { await controller.start(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); device.zh.isDeleted = true; await controller.stop(); + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined); }); it('EventBus should handle errors', async () => { + // @ts-expect-error private const eventbus = controller.eventBus; const callback = jest.fn().mockImplementation(async () => { throw new Error('Whoops!'); }); - eventbus.onStateChange('test', callback); + eventbus.onStateChange({constructor: {name: 'Test'}}, callback); eventbus.emitStateChange({}); await flushPromises(); expect(callback).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`EventBus error 'String/stateChange': Whoops!`); + expect(mockLogger.error).toHaveBeenCalledWith(`EventBus error 'Test/stateChange': Whoops!`); }); }); diff --git a/test/data.test.js b/test/data.test.ts similarity index 83% rename from test/data.test.js rename to test/data.test.ts index 34b80302f1..6b9b41b437 100644 --- a/test/data.test.js +++ b/test/data.test.ts @@ -1,8 +1,8 @@ -const logger = require('./stub/logger'); -const data = require('../lib/util/data').default; -const path = require('path'); -const tmp = require('tmp'); -const fs = require('fs'); +import path from 'path'; + +import tmp from 'tmp'; + +import data from '../lib/util/data'; describe('Data', () => { describe('Get path', () => { diff --git a/test/availability.test.js b/test/extensions/availability.test.ts similarity index 76% rename from test/availability.test.js rename to test/extensions/availability.test.ts index a48c6e3009..43a2395655 100644 --- a/test/availability.test.js +++ b/test/extensions/availability.test.ts @@ -1,45 +1,47 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const Availability = require('../lib/extension/availability').default; -const stringify = require('json-stable-stringify-without-jsonify'); -const utils = require('../lib/util/utils').default; - -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.TS0601_thermostat.ieeeAddr, - devices.bulb_2.ieeeAddr, - devices.ZNCZ02LM.ieeeAddr, - devices.GLEDOPTO_2ID.ieeeAddr, - devices.QBKG03LM.ieeeAddr, - ], +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import assert from 'assert'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import Availability from '../../lib/extension/availability'; +import * as settings from '../../lib/util/settings'; +import utils from '../../lib/util/utils'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.info]; + +returnDevices.push( + devices.bulb_color.ieeeAddr, + devices.bulb_color_2.ieeeAddr, + devices.coordinator.ieeeAddr, + devices.remote.ieeeAddr, + devices.TS0601_thermostat.ieeeAddr, + devices.bulb_2.ieeeAddr, + devices.ZNCZ02LM.ieeeAddr, + devices.GLEDOPTO_2ID.ieeeAddr, + devices.QBKG03LM.ieeeAddr, ); -describe('Availability', () => { - let controller; +describe('Extension: Availability', () => { + let controller: Controller; - let resetExtension = async () => { + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Availability'); await controller.enableDisableExtension(true, 'Availability'); }; - const setTimeAndAdvanceTimers = async (value) => { + const setTimeAndAdvanceTimers = async (value: number): Promise => { jest.setSystemTime(Date.now() + value); await jest.advanceTimersByTimeAsync(value); }; beforeAll(async () => { - jest.spyOn(utils, 'sleep').mockImplementation(async (seconds) => {}); + jest.spyOn(utils, 'sleep').mockImplementation(); jest.useFakeTimers(); settings.reRead(); settings.set(['availability'], true); @@ -55,7 +57,7 @@ describe('Availability', () => { settings.set(['availability'], true); settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], false); Object.values(devices).forEach((d) => (d.lastSeen = utils.minutes(1))); - mocks.forEach((m) => m.mockClear()); + mocksClear.forEach((m) => m.mockClear()); await resetExtension(); Object.values(devices).forEach((d) => d.ping.mockClear()); }); @@ -68,19 +70,19 @@ describe('Availability', () => { }); it('Should publish availability on startup for device where it is enabled for', async () => { - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color_2/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -112,14 +114,14 @@ describe('Availability', () => { }); it('Should publish offline for active device when not seen for 10 minutes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(5)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); await setTimeAndAdvanceTimers(utils.minutes(7)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -128,17 +130,17 @@ describe('Availability', () => { }); it('Shouldnt do anything for a device when availability: false is set for device', async () => { - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); // Coverage satisfaction + await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2}); // Coverage satisfaction await setTimeAndAdvanceTimers(utils.minutes(12)); expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); }); it('Should publish offline for passive device when not seen for 25 hours', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -147,13 +149,13 @@ describe('Availability', () => { }); it('Should reset ping timer when device last seen changes for active device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(5)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); - expect(MQTT.publish).toHaveBeenCalledWith( + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -169,9 +171,9 @@ describe('Availability', () => { }); it('Should ping again when first ping fails', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); devices.bulb_color.ping.mockImplementationOnce(() => { throw new Error('failed'); @@ -184,13 +186,13 @@ describe('Availability', () => { }); it('Should reset ping timer when device last seen changes for passive device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(24)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.remote}); - expect(MQTT.publish).toHaveBeenCalledWith( + await mockZHEvents.lastSeenChanged({device: devices.remote}); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -205,10 +207,10 @@ describe('Availability', () => { }); it('Should immediately mark device as online when it lastSeen changes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(15)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -216,9 +218,9 @@ describe('Availability', () => { ); devices.bulb_color.lastSeen = Date.now(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -263,7 +265,7 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(9)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr}); + await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr}); await flushPromises(); await setTimeAndAdvanceTimers(utils.minutes(3)); @@ -274,7 +276,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'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb_color'})); await flushPromises(); await setTimeAndAdvanceTimers(utils.minutes(3)); @@ -302,18 +304,19 @@ describe('Availability', () => { }); it('Should retrieve device state when it reconnects', async () => { - //@ts-expect-error private + // @ts-expect-error private const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); - //@ts-expect-error private + // @ts-expect-error private controller.state.set(device, {state: 'OFF'}); const endpoint = devices.bulb_color.getEndpoint(1); + assert(endpoint); endpoint.read.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); await setTimeAndAdvanceTimers(utils.seconds(1)); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(0); @@ -323,7 +326,7 @@ describe('Availability', () => { expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); endpoint.read.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); endpoint.read.mockImplementationOnce(() => { throw new Error(''); @@ -333,20 +336,20 @@ describe('Availability', () => { }); it('Should republish availability when device is renamed', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_color', to: 'bulb_new_name'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_color', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_new_name/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); await setTimeAndAdvanceTimers(utils.hours(12)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_new_name/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -357,10 +360,10 @@ describe('Availability', () => { it('Should publish availability payload in JSON format', async () => { await resetExtension(); devices.remote.ping.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -373,25 +376,25 @@ describe('Availability', () => { await resetExtension(); devices.bulb_color_2.ping.mockClear(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); devices.bulb_color_2.lastSeen = Date.now(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -400,17 +403,21 @@ describe('Availability', () => { }); it('Should clear the ping queue on stop', async () => { - //@ts-expect-error private - const availability = controller.extensions.find((extension) => extension instanceof Availability); + // @ts-expect-error private + const availability = controller.extensions.find((extension) => extension instanceof Availability)!; + // @ts-expect-error private const publishAvailabilitySpy = jest.spyOn(availability, 'publishAvailability'); devices.bulb_color.ping.mockImplementationOnce(() => new Promise((resolve) => setTimeout(resolve, 1000))); + // @ts-expect-error private availability.addToPingQueue(devices.bulb_color); + // @ts-expect-error private availability.addToPingQueue(devices.bulb_color_2); await availability.stop(); await setTimeAndAdvanceTimers(utils.minutes(1)); + // @ts-expect-error private expect(availability.pingQueue).toEqual([]); // Validate the stop-interrupt implicitly by checking that it prevents further function invocations expect(publishAvailabilitySpy).not.toHaveBeenCalled(); @@ -418,8 +425,8 @@ describe('Availability', () => { }); it('Should prevent instance restart', async () => { - //@ts-expect-error private - const availability = controller.extensions.find((extension) => extension instanceof Availability); + // @ts-expect-error private + const availability = controller.extensions.find((extension) => extension instanceof Availability)!; await availability.stop(); diff --git a/test/bind.test.js b/test/extensions/bind.test.ts similarity index 64% rename from test/bind.test.js rename to test/extensions/bind.test.ts index 9a65d41514..b29bb33096 100644 --- a/test/bind.test.js +++ b/test/extensions/bind.test.ts @@ -1,18 +1,32 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -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)); -const debounce = require('debounce'); - -describe('Bind', () => { - let controller; - - const mockClear = (device) => { +import * as data from '../mocks/data'; +import {mockDebounce} from '../mocks/debounce'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {Device, devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [ + mockDebounce, + mockMQTT.publish, + devices.bulb_color.getEndpoint(1)!.configureReporting, + devices.bulb_color.getEndpoint(1)!.bind, + devices.bulb_color_2.getEndpoint(1)!.read, +]; + +describe('Extension: Bind', () => { + let controller: Controller; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'Bind'); + await controller.enableDisableExtension(true, 'Bind'); + }; + + const mockClear = (device: Device): void => { for (const endpoint of device.endpoints) { endpoint.read.mockClear(); endpoint.write.mockClear(); @@ -23,11 +37,6 @@ describe('Bind', () => { } }; - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'Bind'); - await controller.enableDisableExtension(true, 'Bind'); - }; - beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); @@ -38,13 +47,9 @@ describe('Bind', () => { beforeEach(async () => { data.writeDefaultConfiguration(); settings.reRead(); - zigbeeHerdsman.groups.group_1.members = []; - zigbeeHerdsman.devices.bulb_color.getEndpoint(1).configureReporting.mockClear(); - zigbeeHerdsman.devices.bulb_color.getEndpoint(1).bind.mockClear(); - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockClear(); - debounce.mockClear(); + groups.group_1.members = []; await resetExtension(); - MQTT.publish.mockClear(); + mocksClear.forEach((m) => m.mockClear()); }); afterAll(async () => { @@ -52,22 +57,22 @@ describe('Bind', () => { }); it('Should bind to device and configure reporting', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + 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.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); @@ -87,7 +92,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -98,37 +103,37 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Filters out unsupported clusters for reporting setup', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceInputClusters = device.getEndpoint(1).inputClusters; - device.getEndpoint(1).inputClusters = [...device.getEndpoint(1).inputClusters, 8]; - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceInputClusters = device.getEndpoint(1)!.inputClusters; + device.getEndpoint(1)!.inputClusters = [...device.getEndpoint(1)!.inputClusters, 8]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + 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 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.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); @@ -147,7 +152,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -158,27 +163,27 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; target.inputClusters = originalTargetInputClusters; target.outputClusters = originalTargetOutputClusters; - device.getEndpoint(1).inputClusters = originalDeviceInputClusters; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.inputClusters = originalDeviceInputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Filters out reporting setup based on bind status', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + 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.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); @@ -194,7 +199,7 @@ describe('Bind', () => { }, ]; - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); @@ -212,7 +217,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -223,24 +228,24 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.configuredReportings = originalTargetCR; target.binds = originalTargetBinds; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Should bind only specified clusters', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = 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']})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -249,14 +254,14 @@ describe('Bind', () => { }); it('Should log error when there is nothing to bind', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; mockClear(device); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'})); + mockLogger.error.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -265,27 +270,27 @@ describe('Bind', () => { }); it('Should unbind', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; // setup target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - const originalRemoteBinds = device.getEndpoint(1).binds; - device.getEndpoint(1).binds = []; + 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: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'lightingColorCtrl'}, target: devices.coordinator.getEndpoint(1)!}, ]; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); - delete zigbeeHerdsman.devices.bulb_color.meta.configured; - expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(undefined); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'bulb_color'})); + delete devices.bulb_color.meta.configured; + expect(devices.bulb_color.meta.configured).toBe(undefined); + mockMQTTEvents.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); @@ -305,8 +310,8 @@ describe('Bind', () => { {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( + expect(devices.bulb_color.meta.configured).toBe(332242049); + expect(mockMQTT.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}, @@ -315,22 +320,22 @@ describe('Bind', () => { // Teardown target.binds = originalTargetBinds; - device.getEndpoint(1).binds = originalRemoteBinds; + device.getEndpoint(1)!.binds = originalRemoteBinds; }); it('Should unbind coordinator', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.coordinator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.unbind.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'Coordinator'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -339,14 +344,14 @@ describe('Bind', () => { }); it('Should bind to groups', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'group_1'})); + mockMQTTEvents.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); @@ -359,7 +364,7 @@ describe('Bind', () => { expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -368,7 +373,7 @@ describe('Bind', () => { // Should configure reporting for device added to group target1Member.configureReporting.mockClear(); - await MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'})); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'})); await flushPromises(); expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ @@ -380,20 +385,20 @@ describe('Bind', () => { }); it('Should unbind from group', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -402,10 +407,10 @@ describe('Bind', () => { }); it('Should unbind from group with skip_disable_reporting=true', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb_2.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. @@ -413,12 +418,12 @@ describe('Bind', () => { endpoint.binds = []; target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: 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})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: true})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); // with skip_disable_reporting set to false, we don't expect it to reconfigure reporting @@ -427,10 +432,10 @@ describe('Bind', () => { }); it('Should unbind from group with skip_disable_reporting=false', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb_2.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. @@ -438,12 +443,12 @@ describe('Bind', () => { endpoint.binds = []; target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: 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})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: false})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); // with skip_disable_reporting set, we expect it to reconfigure reporting @@ -458,17 +463,17 @@ describe('Bind', () => { }); it('Should bind to group by number', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '1'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -477,17 +482,17 @@ describe('Bind', () => { }); it('Should log when bind fails', async () => { - logger.error.mockClear(); - const device = zigbeeHerdsman.devices.remote; - const endpoint = device.getEndpoint(1); + mockLogger.error.mockClear(); + const device = devices.remote; + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.bind.mockImplementation(() => { throw new Error('failed'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color', clusters: [], failed: ['genScenes', 'genOnOff', 'genLevelCtrl']}, @@ -500,15 +505,15 @@ describe('Bind', () => { }); it('Should bind from non default endpoints', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG03LM.getEndpoint(3); - const endpoint = device.getEndpoint(2); + const device = devices.remote; + const target = devices.QBKG03LM.getEndpoint(3)!; + const endpoint = device.getEndpoint(2)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -517,15 +522,15 @@ describe('Bind', () => { }); it('Should bind server clusters to client clusters', async () => { - const device = zigbeeHerdsman.devices.temperature_sensor; - const target = zigbeeHerdsman.devices.heating_actuator.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.temperature_sensor; + const target = devices.heating_actuator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'temperature_sensor', to: 'heating_actuator'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -534,15 +539,15 @@ describe('Bind', () => { }); it('Should bind to default endpoint returned by endpoints()', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG04LM.getEndpoint(2); - const endpoint = device.getEndpoint(2); + const device = devices.remote; + const target = devices.QBKG04LM.getEndpoint(2)!; + const endpoint = device.getEndpoint(2)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -551,17 +556,17 @@ describe('Bind', () => { }); it('Should unbind from default_bind_group', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; const target = 'default_bind_group'; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: target})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({ data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, @@ -573,13 +578,11 @@ describe('Bind', () => { }); it('Error bind fails when source device does not exist', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote_not_existing', to: 'bulb_color'}, @@ -592,13 +595,11 @@ describe('Bind', () => { }); it("Error bind fails when source device's endpoint does not exist", async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote/not_existing_endpoint', to: 'bulb_color'}, @@ -611,13 +612,11 @@ describe('Bind', () => { }); it('Error bind fails when target device or group does not exist', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color_not_existing'}, @@ -630,13 +629,11 @@ describe('Bind', () => { }); it("Error bind fails when target device's endpoint does not exist", async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color/not_existing_endpoint'}, @@ -649,79 +646,75 @@ describe('Bind', () => { }); it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { - const remote = zigbeeHerdsman.devices.remote; + const remote = 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), + endpoint: remote.getEndpoint(2)!, type: 'commandHueNotification', linkquality: 10, groupID: 0, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(mockDebounce).toHaveBeenCalledTimes(1); + expect(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 remote = devices.bj_scene_switch; const data = {action: 'recall_2_row_1'}; - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => { + devices.bulb_color_2.getEndpoint(1)!.read.mockImplementationOnce(() => { throw new Error('failed'); }); const payload = { data, cluster: 'genScenes', device: remote, - endpoint: remote.getEndpoint(10), + endpoint: remote.getEndpoint(10)!, type: 'commandRecall', linkquality: 10, groupID: 0, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockDebounce).toHaveBeenCalledTimes(3); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(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; + devices.bulb_color_2.getEndpoint(1)!.read.mockClear(); + devices.bulb_2.getEndpoint(1)!.read.mockClear(); + const remote = devices.tradfri_remote; const data = {stepmode: 0, stepsize: 43, transtime: 5}; const payload = { data, cluster: 'genLevelCtrl', device: remote, - endpoint: remote.getEndpoint(1), + endpoint: remote.getEndpoint(1)!, type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockDebounce).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genOnOff', ['onOff']); // Should also only debounce once - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(4); + expect(mockDebounce).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(4); // Should only call Hue bulb, not e.g. tradfri - expect(zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read).toHaveBeenCalledTimes(0); + expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); }); }); diff --git a/test/bridge.test.js b/test/extensions/bridge.test.ts similarity index 84% rename from test/bridge.test.js rename to test/extensions/bridge.test.ts index 2218a7a78b..6123188fae 100644 --- a/test/bridge.test.js +++ b/test/extensions/bridge.test.ts @@ -1,48 +1,52 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const fs = require('fs'); -const path = require('path'); -const flushPromises = require('./lib/flushPromises'); -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'); - -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; -zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); -zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr); -zigbeeHerdsman.returnDevices.push(WXKG11LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(remote.ieeeAddr); -zigbeeHerdsman.returnDevices.push(ZNCZ02LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb_color_2.ieeeAddr); -zigbeeHerdsman.returnDevices.push(WSDCGQ11LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(zigfred_plus.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb_custom_cluster.ieeeAddr); - -describe('Bridge', () => { - let controller; - let mockRestart; - let extension; - - let resetExtension = async () => { +import * as data from '../mocks/data'; +import {mockJSZipFile, mockJSZipGenerateAsync} from '../mocks/jszip'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises, JestMockAny} from '../mocks/utils'; +import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import type Bridge from '../../lib/extension/bridge'; + +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; +import utils from '../../lib/util/utils'; + +returnDevices.push(devices.coordinator.ieeeAddr); +returnDevices.push(devices.bulb.ieeeAddr); +returnDevices.push(devices.unsupported.ieeeAddr); +returnDevices.push(devices.WXKG11LM.ieeeAddr); +returnDevices.push(devices.remote.ieeeAddr); +returnDevices.push(devices.ZNCZ02LM.ieeeAddr); +returnDevices.push(devices.bulb_color_2.ieeeAddr); +returnDevices.push(devices.WSDCGQ11LM.ieeeAddr); +returnDevices.push(devices.zigfred_plus.ieeeAddr); +returnDevices.push(devices.bulb_custom_cluster.ieeeAddr); + +const mocksClear = [ + mockLogger.info, + mockLogger.warning, + mockMQTT.publish, + mockZHController.permitJoin, + devices.bulb.interview, + devices.bulb.removeFromDatabase, + devices.bulb.removeFromNetwork, +]; + +describe('Extension: Bridge', () => { + let controller: Controller; + let mockRestart: JestMockAny; + let extension: Bridge; + + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Bridge'); await controller.enableDisableExtension(true, 'Bridge'); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'Bridge'); }; @@ -52,26 +56,23 @@ describe('Bridge', () => { controller = new Controller(mockRestart, jest.fn()); await controller.start(); await flushPromises(); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'Bridge'); }); beforeEach(async () => { - MQTT.mock.reconnecting = false; + mockMQTT.reconnecting = false; data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultState(); - logger.info.mockClear(); - logger.warning.mockClear(); - logger.setTransportsEnabled(false); - MQTT.publish.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); - const device = zigbeeHerdsman.devices.bulb; - device.interview.mockClear(); - device.removeFromDatabase.mockClear(); - device.removeFromNetwork.mockClear(); - extension.lastJoinedDeviceIeeeAddr = null; + mocksClear.forEach((m) => m.mockClear()); + mockLogger.setTransportsEnabled(false); + // @ts-expect-error private + extension.lastJoinedDeviceIeeeAddr = undefined; + // @ts-expect-error private extension.restartRequired = false; - controller.state.state = {[zigbeeHerdsman.devices.bulb.ieeeAddr]: {brightness: 50}}; + // @ts-expect-error private + controller.state.state = {[devices.bulb.ieeeAddr]: {brightness: 50}}; }); afterAll(async () => { @@ -84,8 +85,8 @@ describe('Bridge', () => { const zhVersion = await utils.getDependencyVersion('zigbee-herdsman'); const zhcVersion = await utils.getDependencyVersion('zigbee-herdsman-converters'); const directory = settings.get().advanced.log_directory; - // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', stringify({ commit: version.commitHash, @@ -209,7 +210,7 @@ describe('Bridge', () => { config_schema: settings.schema, 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}, + network: {channel: 15, extended_pan_id: 0x001122, pan_id: 5674}, permit_join_timeout: 0, restart_required: false, version: version.version, @@ -223,28 +224,28 @@ describe('Bridge', () => { it('Should publish devices on startup', async () => { await resetExtension(); - // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]); - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/devices', stringify([ { - date_code: null, - // definition: null, + date_code: undefined, + // definition: undefined, 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, + model_id: undefined, network_address: 0, - power_source: null, - software_build_id: null, + power_source: undefined, + software_build_id: undefined, supported: true, type: 'Coordinator', }, { - date_code: null, + date_code: undefined, definition: { description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', exposes: [ @@ -486,7 +487,7 @@ describe('Bridge', () => { model_id: 'TRADFRI bulb E27 WS opal 980lm', network_address: 40369, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, @@ -687,7 +688,7 @@ describe('Bridge', () => { type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'Hue dimmer switch', exposes: [ @@ -823,12 +824,12 @@ describe('Bridge', () => { model_id: 'RWL021', network_address: 6535, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Automatically generated definition', exposes: [ @@ -918,12 +919,12 @@ describe('Bridge', () => { model_id: 'notSupportedModelID', network_address: 6536, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: false, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Wireless mini switch', exposes: [ @@ -1040,12 +1041,12 @@ describe('Bridge', () => { model_id: 'lumi.sensor_switch.aq2', network_address: 6537, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Temperature and humidity sensor', exposes: [ @@ -1183,12 +1184,12 @@ describe('Bridge', () => { model_id: 'lumi.weather', network_address: 6539, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Mi smart plug', exposes: [ @@ -1332,12 +1333,12 @@ describe('Bridge', () => { model_id: 'lumi.plug', network_address: 6540, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'zigfred plus smart in-wall switch', exposes: [ @@ -1834,12 +1835,12 @@ describe('Bridge', () => { model_id: 'zigfred plus', network_address: 6589, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', exposes: [ @@ -2069,11 +2070,11 @@ describe('Bridge', () => { ieee_address: '0x000b57fffec6a5c2', interview_completed: true, interviewing: false, - manufacturer: null, + manufacturer: undefined, model_id: 'TRADFRI bulb E27 WS opal 980lm', network_address: 40369, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, @@ -2085,76 +2086,76 @@ describe('Bridge', () => { it('Should publish definitions on startup', async () => { await resetExtension(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); }); it('Should log to MQTT', async () => { - 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 - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); // Should not publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + mockMQTT.publish.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should log to MQTT including debug when enabled', async () => { settings.set(['advanced', 'log_debug_to_mqtt_frontend'], true); await resetExtension(); - 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 - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); // Should publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + mockMQTT.publish.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); settings.set(['advanced', 'log_debug_to_mqtt_frontend'], false); settings.reRead(); }); it('Shouldnt log to MQTT when not connected', async () => { - logger.setTransportsEnabled(true); - MQTT.mock.reconnecting = true; - MQTT.publish.mockClear(); - logger.info.mockClear(); - logger.error.mockClear(); - logger.info('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledTimes(0); + mockLogger.setTransportsEnabled(true); + mockMQTT.reconnecting = true; + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.error.mockClear(); + mockLogger.info('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.error).toHaveBeenCalledTimes(0); }); it('Should publish groups on startup', async () => { await resetExtension(); - logger.setTransportsEnabled(true); - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', stringify([ {friendly_name: 'group_1', id: 1, members: [], scenes: []}, @@ -2182,10 +2183,10 @@ describe('Bridge', () => { }); it('Should publish event when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2194,18 +2195,18 @@ describe('Bridge', () => { }); it('Should publish devices when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceNetworkAddressChanged({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event when device announces', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2214,11 +2215,11 @@ describe('Bridge', () => { }); it('Should publish event when device interview started', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'started'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'started'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2227,27 +2228,27 @@ describe('Bridge', () => { }); it('Should publish event and devices when device interview failed', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'failed'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'failed'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.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)); + expect(mockMQTT.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 () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'successful'}); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.unsupported, status: 'successful'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'successful'}); + await mockZHEvents.deviceInterview({device: devices.unsupported, status: 'successful'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(7); - // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(7); + // console.log(mockMQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2473,7 +2474,7 @@ describe('Bridge', () => { {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2559,23 +2560,28 @@ describe('Bridge', () => { {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)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb.ieeeAddr}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( // Defintitions should be updated on device event 'zigbee2mqtt/bridge/definitions', expect.any(String), @@ -2585,11 +2591,11 @@ describe('Bridge', () => { }); it('Should allow permit join on all', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, @@ -2598,11 +2604,11 @@ describe('Bridge', () => { }); it('Should disallow permit join on all', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(0, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(0, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 0}, status: 'ok'}), {retain: false, qos: 0}, @@ -2611,11 +2617,11 @@ describe('Bridge', () => { }); it('Should allow permit join with number string (automatically on all)', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', '1'); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', '1'); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, @@ -2624,10 +2630,10 @@ describe('Bridge', () => { }); it('Should not allow permit join with invalid payload', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2636,28 +2642,33 @@ describe('Bridge', () => { }); it('Should republish bridge info when permit join changes', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); + mockMQTT.publish.mockClear(); + await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.isStopping.mockImplementationOnce(() => true); - await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); + mockMQTT.publish.mockClear(); + mockZHController.isStopping.mockImplementationOnce(() => true); + await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10}); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/info', + expect.any(String), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Should allow permit join via device', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(123, device); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(123, device); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 123, device: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, @@ -2666,11 +2677,11 @@ describe('Bridge', () => { }); it('Should not allow permit join via non-existing device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing_woeeee' does not exist"}), {retain: false, qos: 0}, @@ -2679,10 +2690,10 @@ describe('Bridge', () => { }); 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({time: 0, transaction: 22})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0, transaction: 22})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 0}, status: 'ok', transaction: 22}), {retain: false, qos: 0}, @@ -2691,13 +2702,13 @@ describe('Bridge', () => { }); it('Should put error in response when request fails', async () => { - zigbeeHerdsman.permitJoin.mockImplementationOnce(() => { + mockZHController.permitJoin.mockImplementationOnce(() => { throw new Error('Failed to connect to adapter'); }); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: 'Failed to connect to adapter'}), {retain: false, qos: 0}, @@ -2706,10 +2717,10 @@ describe('Bridge', () => { }); it('Should put error in response when format is incorrect', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: false})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: false})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2718,10 +2729,10 @@ describe('Bridge', () => { }); it('Coverage satisfaction', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); - const device = zigbeeHerdsman.devices.bulb; - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); + const device = devices.bulb; + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device, @@ -2733,10 +2744,10 @@ describe('Bridge', () => { }); it('Should allow a healthcheck', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/health_check', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/health_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/health_check', stringify({data: {healthy: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2745,11 +2756,11 @@ describe('Bridge', () => { }); it('Should allow a coordinator check', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.coordinatorCheck.mockReturnValueOnce({missingRouters: [zigbeeHerdsman.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); - MQTT.events.message('zigbee2mqtt/bridge/request/coordinator_check', ''); + mockMQTT.publish.mockClear(); + mockZHController.coordinatorCheck.mockReturnValueOnce({missingRouters: [mockZHController.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/coordinator_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/coordinator_check', stringify({data: {missing_routers: [{friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}]}, status: 'ok'}), {retain: false, qos: 0}, @@ -2758,7 +2769,7 @@ describe('Bridge', () => { }); it('Should allow to remove device by string', async () => { - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['groups'], { 1: { friendly_name: 'group_1', @@ -2775,16 +2786,17 @@ describe('Bridge', () => { ], }, }); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); await flushPromises(); + // @ts-expect-error private expect(controller.state[device.ieeeAddr]).toBeUndefined(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(device.removeFromDatabase).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2792,20 +2804,20 @@ describe('Bridge', () => { ); expect(settings.get().blocklist).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual(['0x999b57fffec6a5b9/1', 'other_bulb', 'bulb_1', 'bulb/room/2']); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); }); 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'})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(device.removeFromDatabase).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2814,15 +2826,15 @@ describe('Bridge', () => { }); 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})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(device.removeFromNetwork).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2831,14 +2843,14 @@ describe('Bridge', () => { }); 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})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', block: true, force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: true, force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2848,14 +2860,14 @@ describe('Bridge', () => { }); it('Should allow to remove group', async () => { - const group = zigbeeHerdsman.groups.group_1; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', 'group_1'); + const group = groups.group_1; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', 'group_1'); await flushPromises(); expect(group.removeFromNetwork).toHaveBeenCalledTimes(1); expect(settings.getGroup('group_1')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2864,14 +2876,14 @@ describe('Bridge', () => { }); 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})); + const group = groups.group_1; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'group_1', force: true})); await flushPromises(); expect(group.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getGroup('group_1')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2881,21 +2893,20 @@ describe('Bridge', () => { 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']}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123', '0x1234']}})); await flushPromises(); expect(settings.get().blocklist).toStrictEqual(['0x123', '0x1234']); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123']}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123']}})); await flushPromises(); expect(settings.get().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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {}, status: 'error', error: "Device 'non-existing-device' does not exist"}), {retain: false, qos: 0}, @@ -2904,14 +2915,14 @@ describe('Bridge', () => { }); it('Should throw error when remove device fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); + const device = devices.bulb; + mockMQTT.publish.mockClear(); device.removeFromNetwork.mockImplementationOnce(() => { throw new Error('device timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -2920,8 +2931,8 @@ describe('Bridge', () => { }); it('Should allow rename device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeUndefined(); expect(settings.getDevice('bulb_new_name')).toStrictEqual({ @@ -2930,10 +2941,15 @@ describe('Bridge', () => { 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_new_name', + stringify({brightness: 50}), + expect.any(Object), + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2942,10 +2958,10 @@ describe('Bridge', () => { }); it('Shouldnt allow rename device with to not allowed name containing a wildcard', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'living_room/blinds#'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'living_room/blinds#'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -2954,13 +2970,13 @@ describe('Bridge', () => { }); it('Should allow rename group', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); await flushPromises(); expect(settings.getGroup('group_1')).toBeUndefined(); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.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}, @@ -2969,10 +2985,10 @@ describe('Bridge', () => { }); it('Should throw error on invalid device rename payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from_bla: 'bulb', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from_bla: 'bulb', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2981,10 +2997,10 @@ describe('Bridge', () => { }); it('Should throw error on non-existing device rename', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_not_existing', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_not_existing', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, @@ -2993,9 +3009,9 @@ describe('Bridge', () => { }); it('Should allow to rename last joined device', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeUndefined(); expect(settings.getDevice('bulb_new_name')).toStrictEqual({ @@ -3004,8 +3020,8 @@ describe('Bridge', () => { 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3014,10 +3030,10 @@ describe('Bridge', () => { }); it('Should throw error when renaming device through not allowed friendlyName', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name/1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3026,10 +3042,10 @@ describe('Bridge', () => { }); it('Should throw error when renaming last joined device but none has joined', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'No device has joined since start'}), {retain: false, qos: 0}, @@ -3038,12 +3054,12 @@ describe('Bridge', () => { }); it('Should allow interviewing a device by friendly name', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(devices.bulb.interview).toHaveBeenCalled(); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3051,20 +3067,22 @@ describe('Bridge', () => { ); // 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(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should allow interviewing a device by ieeeAddr', async () => { - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; + assert('resolveDefinition' in device); device.resolveDefinition = jest.fn(); - MQTT.publish.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockClear(); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); expect(device.resolveDefinition).toHaveBeenCalledTimes(0); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'})); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalledWith(true); + expect(devices.bulb.interview).toHaveBeenCalledWith(true); expect(device.resolveDefinition).toHaveBeenCalledWith(true); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: '0x000b57fffec6a5b2'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3072,14 +3090,14 @@ describe('Bridge', () => { ); // 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(mockMQTT.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 () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3088,10 +3106,10 @@ describe('Bridge', () => { }); it('Should throw error on non-existing device interview', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, @@ -3100,10 +3118,10 @@ describe('Bridge', () => { }); it('Should throw error on id is device endpoint', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb/1' does not exist"}), {retain: false, qos: 0}, @@ -3112,10 +3130,10 @@ describe('Bridge', () => { }); it('Should throw error on id is a group', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'group_1' does not exist"}), {retain: false, qos: 0}, @@ -3124,12 +3142,12 @@ describe('Bridge', () => { }); 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'))); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); + devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3138,10 +3156,10 @@ describe('Bridge', () => { }); it('Should error when generate_external_definition is invalid', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: ZNCZ02LM.ieeeAddr})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, @@ -3150,10 +3168,10 @@ describe('Bridge', () => { }); it('Should error when generate_external_definition requested for unknown device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: 'non_existing_device'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: 'non_existing_device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3162,10 +3180,10 @@ describe('Bridge', () => { }); it('Should allow to generate device definition', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: ZNCZ02LM.ieeeAddr})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({ data: { @@ -3192,14 +3210,14 @@ describe('Bridge', () => { }); it('Should allow change device options', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); 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'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {retain: false, transition: 1}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3208,7 +3226,7 @@ describe('Bridge', () => { transition: 1, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3225,7 +3243,7 @@ describe('Bridge', () => { }); it('Should allow to remove device option', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); settings.set(['devices', '0x000b57fffec6a5b2', 'qos'], 1); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3234,7 +3252,7 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {qos: null}, id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {qos: null}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3242,7 +3260,7 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3259,14 +3277,14 @@ describe('Bridge', () => { }); it('Should allow change device options with restart required', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); 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'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {disabled: true}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3275,7 +3293,7 @@ describe('Bridge', () => { disabled: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3292,12 +3310,12 @@ describe('Bridge', () => { }); it('Should allow change group options', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); 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'})); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3306,9 +3324,9 @@ describe('Bridge', () => { }); it('Should allow change group options with restart required', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); 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'})); + mockMQTTEvents.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, @@ -3317,7 +3335,7 @@ describe('Bridge', () => { retain: false, off_state: 'all_members_off', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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'}, @@ -3329,10 +3347,10 @@ describe('Bridge', () => { }); it('Should throw error on invalid device change options payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options_: {retain: true, transition: 1}, id: 'bulb'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options_: {retain: true, transition: 1}, id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3341,12 +3359,12 @@ describe('Bridge', () => { }); it('Should allow to add group by string', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); await flushPromises(); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 3}, status: 'ok'}), {retain: false, qos: 0}, @@ -3355,12 +3373,12 @@ describe('Bridge', () => { }); 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})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 92}, status: 'ok'}), {retain: false, qos: 0}, @@ -3369,10 +3387,10 @@ describe('Bridge', () => { }); 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})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: '', id: 9})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3381,10 +3399,10 @@ describe('Bridge', () => { }); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: 'group_193'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3393,13 +3411,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset (succeeds)', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(true); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'ok'}), {retain: false, qos: 0}, @@ -3408,14 +3426,14 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset specific device', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryReset.mockClear(); - zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', stringify({ieee_address: '0x1239', channel: 12})); - await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledWith('0x1239', 12); - expect(MQTT.publish).toHaveBeenCalledWith( + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryReset.mockClear(); + mockZHController.touchlinkFactoryReset.mockReturnValueOnce(true); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', stringify({ieee_address: '0x1239', channel: 12})); + await flushPromises(); + expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledWith('0x1239', 12); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, @@ -3424,15 +3442,15 @@ describe('Bridge', () => { }); it('Add install code', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // By object - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', stringify({value: 'my-code'})); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', stringify({value: 'my-code'})); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-code'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1); + expect(mockZHController.addInstallCode).toHaveBeenCalledWith('my-code'); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3440,12 +3458,12 @@ describe('Bridge', () => { ); // By string - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', 'my-string-code'); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', 'my-string-code'); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-string-code'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1); + expect(mockZHController.addInstallCode).toHaveBeenCalledWith('my-string-code'); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3454,12 +3472,12 @@ describe('Bridge', () => { }); it('Add install code error', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', stringify({wrong: 'my-code'})); + mockMQTT.publish.mockClear(); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', stringify({wrong: 'my-code'})); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3468,13 +3486,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink identify specific device', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkIdentify.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239', channel: 12})); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkIdentify.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239', channel: 12})); await flushPromises(); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledWith('0x1239', 12); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledWith('0x1239', 12); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, @@ -3483,12 +3501,12 @@ describe('Bridge', () => { }); it('Touchlink identify fails when payload is invalid', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkIdentify.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239'})); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkIdentify.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239'})); await flushPromises(); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3497,13 +3515,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset (fails)', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(false); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(false); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'error', error: 'Failed to factory reset device through Touchlink'}), {retain: false, qos: 0}, @@ -3512,16 +3530,16 @@ describe('Bridge', () => { }); it('Should allow to touchlink scan', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkScan.mockClear(); - zigbeeHerdsman.touchlinkScan.mockReturnValueOnce([ + mockMQTT.publish.mockClear(); + mockZHController.touchlinkScan.mockClear(); + mockZHController.touchlinkScan.mockReturnValueOnce([ {ieeeAddr: '0x123', channel: 12}, {ieeeAddr: '0x124', channel: 24}, ]); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/scan', ''); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/scan', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkScan).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkScan).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/scan', stringify({ data: { @@ -3538,11 +3556,11 @@ describe('Bridge', () => { }); it('Should allow to configure reporting', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: '0x000b57fffec6a5b2/1', @@ -3555,14 +3573,14 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinator.endpoints[0]); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', devices.coordinator.endpoints[0]); expect(endpoint.configureReporting).toHaveBeenCalledTimes(1); expect(endpoint.configureReporting).toHaveBeenCalledWith( 'genLevelCtrl', [{attribute: 'currentLevel', maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}], undefined, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({ data: { @@ -3578,15 +3596,15 @@ describe('Bridge', () => { {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(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'bulb', @@ -3599,7 +3617,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3608,11 +3626,11 @@ describe('Bridge', () => { }); it('Should throw error when configure reporting is called for non-existing device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'non_existing_device', @@ -3625,7 +3643,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: "Device 'non_existing_device' does not exist"}), {retain: false, qos: 0}, @@ -3634,11 +3652,11 @@ describe('Bridge', () => { }); it('Should throw error when configure reporting is called for non-existing endpoint', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: '0x000b57fffec6a5b2/non_existing_endpoint', @@ -3651,7 +3669,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -3666,10 +3684,10 @@ describe('Bridge', () => { 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'); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/backup', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/backup', ''); await flushPromises(); - expect(zigbeeHerdsman.backup).toHaveBeenCalledTimes(1); + expect(mockZHController.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)); @@ -3677,7 +3695,7 @@ describe('Bridge', () => { expect(mockJSZipFile).toHaveBeenNthCalledWith(4, 'state.json', expect.any(Object)); expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1); expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: 'base64'}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/backup', stringify({data: {zip: 'THISISBASE64'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3686,12 +3704,12 @@ describe('Bridge', () => { }); it('Should allow to restart', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/restart', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/restart', ''); await flushPromises(); jest.runOnlyPendingTimers(); expect(mockRestart).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/restart', stringify({data: {}, status: 'ok'}), {retain: false, qos: 0}, @@ -3700,13 +3718,15 @@ describe('Bridge', () => { }); it('Change options and apply - homeassistant', async () => { + // @ts-expect-error private expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).toBeUndefined(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: true}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: true}})); await flushPromises(); + // @ts-expect-error private 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3715,13 +3735,13 @@ describe('Bridge', () => { }); it('Change options and apply - log_level', async () => { - logger.setLevel('info'); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 'debug'}}})); + mockLogger.setLevel('info'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockLogger.getLevel()).toStrictEqual('debug'); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3730,13 +3750,13 @@ describe('Bridge', () => { }); it('Change options and apply - log_debug_namespace_ignore', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const nsIgnore = '^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller'; - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}})); + mockMQTTEvents.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( + expect(mockLogger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3745,37 +3765,37 @@ describe('Bridge', () => { }); it('Change options and apply - log_namespaced_levels', async () => { - logger.setLevel('info'); + mockLogger.setLevel('info'); settings.apply({advanced: {log_namespaced_levels: {'zh:zstack': 'warning', 'z2m:mqtt': 'debug'}}}); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'}); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', 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}}}})); + mockMQTTEvents.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({}); + expect(mockLogger.getNamespacedLevels()).toStrictEqual({}); }); it('Change options restart required', async () => { settings.apply({serial: {port: '123'}}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); await flushPromises(); expect(settings.get().serial.port).toBe('/dev/newport'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3785,14 +3805,14 @@ describe('Bridge', () => { it('Change options array', async () => { expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3802,11 +3822,11 @@ describe('Bridge', () => { it('Change options with null', async () => { 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}}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {disable_led: false, port: null}}})); await flushPromises(); expect(settings.get().serial).toStrictEqual({disable_led: false}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3815,10 +3835,10 @@ describe('Bridge', () => { }); it('Change options invalid payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, @@ -3827,10 +3847,10 @@ describe('Bridge', () => { }); it('Change options not valid against schema', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, error: 'external_converters must be array', status: 'error'}), {retain: false, qos: 0}, @@ -3839,68 +3859,84 @@ describe('Bridge', () => { }); it('Icon link handling', async () => { - const bridge = controller.extensions.find((e) => e.constructor.name === 'Bridge'); + // @ts-expect-error private + const bridge: Bridge = controller.extensions.find((e) => e.constructor.name === 'Bridge'); expect(bridge).not.toBeUndefined(); - const definition = {model: 'lumi.plug', fromZigbee: []}; - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const definition = { + fingerprint: [], + model: 'lumi.plug', + vendor: 'abcd', + description: 'abcd', + toZigbee: [], + fromZigbee: [], + exposes: [], + icon: '', + }; + const device = devices.ZNCZ02LM; const svg_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo'; const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg'; definition.icon = icon_link; + // @ts-expect-error bare minimum mock let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(icon_link); definition.icon = icon_link; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {icon: svg_icon}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); definition.icon = '_${model}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_'); definition.icon = '_${model}_${zigbeeModel}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_lumi.plug_'); definition.icon = svg_icon; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); device.modelID = '?._Z\\NC+Z02*LM'; definition.model = '&&&&*+'; definition.icon = '_${model}_${zigbeeModel}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_'); }); it('Should publish bridge info, devices and definitions when a device with custom_clusters joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster}); await flushPromises(); - // 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( + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({data: {friendly_name: '0x000b57fffec6a5c2', ieee_address: '0x000b57fffec6a5c2'}, type: 'device_joined'}), {retain: false, qos: 0}, @@ -3910,25 +3946,25 @@ describe('Bridge', () => { it('Should publish bridge info, devices and definitions when a device with custom_clusters is reconfigured', async () => { // Adding a device first - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster}); + await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster}); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // After cleaning, reconfigure it - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', zigbeeHerdsman.devices.bulb_custom_cluster.ieeeAddr); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', devices.bulb_custom_cluster.ieeeAddr); await flushPromises(); - // 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( + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', expect.any(String), {retain: false, qos: 0}, diff --git a/test/configure.test.js b/test/extensions/configure.test.ts similarity index 63% rename from test/configure.test.js rename to test/extensions/configure.test.ts index d5a700deb3..4ab7bba995 100644 --- a/test/configure.test.js +++ b/test/extensions/configure.test.ts @@ -1,81 +1,83 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -const stringify = require('json-stable-stringify-without-jsonify'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -describe('Configure', () => { - let controller; - let coordinatorEndpoint; - - const expectRemoteConfigured = () => { - const device = zigbeeHerdsman.devices.remote; - const endpoint1 = device.getEndpoint(1); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {Device, devices, Endpoint, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Configure', () => { + let controller: Controller; + let coordinatorEndpoint: Endpoint; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'Configure'); + await controller.enableDisableExtension(true, 'Configure'); + }; + + const mockClear = (device: Device): void => { + for (const endpoint of device.endpoints) { + endpoint.read.mockClear(); + endpoint.write.mockClear(); + endpoint.configureReporting.mockClear(); + endpoint.bind.mockClear(); + } + }; + + const expectRemoteConfigured = (): void => { + const device = devices.remote; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.bind).toHaveBeenCalledTimes(2); expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - const endpoint2 = device.getEndpoint(2); + 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(device.meta.configured).toBe(332242049); }; - const expectBulbConfigured = () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint1 = device.getEndpoint(1); + const expectBulbConfigured = (): void => { + const device = 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']); }; - const expectBulbNotConfigured = () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint1 = device.getEndpoint(1); + const expectBulbNotConfigured = (): void => { + const device = devices.bulb; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.read).toHaveBeenCalledTimes(0); }; - const expectRemoteNotConfigured = () => { - const device = zigbeeHerdsman.devices.remote; - const endpoint1 = device.getEndpoint(1); + const expectRemoteNotConfigured = (): void => { + const device = devices.remote; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.bind).toHaveBeenCalledTimes(0); }; - const mockClear = (device) => { - for (const endpoint of device.endpoints) { - endpoint.read.mockClear(); - endpoint.write.mockClear(); - endpoint.configureReporting.mockClear(); - endpoint.bind.mockClear(); - } - }; - - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'Configure'); - await controller.enableDisableExtension(true, 'Configure'); - }; + const wait = async (ms: number): Promise => await new Promise((resolve) => setTimeout(resolve, ms)); beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); + await jest.runOnlyPendingTimersAsync(); }); beforeEach(async () => { data.writeDefaultConfiguration(); settings.reRead(); mocksClear.forEach((m) => m.mockClear()); - coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + coordinatorEndpoint = devices.coordinator.getEndpoint(1)!; await resetExtension(); - await jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); }); afterAll(async () => { @@ -92,32 +94,33 @@ describe('Configure', () => { it('Should re-configure when device rejoins', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; await flushPromises(); mockClear(device); const payload = {device}; - zigbeeHerdsman.events.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); await flushPromises(); expectBulbConfigured(); }); it('Should not re-configure disabled devices', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; await flushPromises(); mockClear(device); settings.set(['devices', device.ieeeAddr, 'disabled'], true); - zigbeeHerdsman.events.deviceJoined({device}); + mockZHEvents.deviceJoined({device}); await flushPromises(); expectBulbNotConfigured(); }); it('Should reconfigure reporting on reconfigure event', async () => { expectBulbConfigured(); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; mockClear(device.zh); expectBulbNotConfigured(); + // @ts-expect-error private controller.eventBus.emitReconfigure({device}); await flushPromises(); expectBulbConfigured(); @@ -125,29 +128,29 @@ describe('Configure', () => { it('Should not configure twice', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; mockClear(device); - await zigbeeHerdsman.events.deviceInterview({device}); + await mockZHEvents.deviceInterview({device}); await flushPromises(); expectBulbNotConfigured(); }); it('Should configure on zigbee message when not configured yet', async () => { - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; delete device.meta.configured; mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expectBulbConfigured(); }); it('Should allow to configure via MQTT', async () => { - mockClear(zigbeeHerdsman.devices.remote); + mockClear(devices.remote); expectRemoteNotConfigured(); - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', 'remote'); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', 'remote'); await flushPromises(); expectRemoteConfigured(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: 'remote'}, status: 'ok'}), {retain: false, qos: 0}, @@ -156,9 +159,9 @@ describe('Configure', () => { }); 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 mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -167,12 +170,12 @@ describe('Configure', () => { }); it('Fail to configure via MQTT when configure fails', async () => { - zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => { + 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 mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -181,9 +184,9 @@ describe('Configure', () => { }); 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 mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}, @@ -192,61 +195,61 @@ describe('Configure', () => { }); it('Should not configure when interview not completed', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; device.interviewCompleted = false; - const endpoint = device.getEndpoint(1); mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expectRemoteNotConfigured(); device.interviewCompleted = true; }); it('Should not configure when already configuring', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; endpoint.bind.mockImplementationOnce(async () => await wait(500)); mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); }); it('Should configure max 3 times when fails', async () => { + // @ts-expect-error private controller.extensions.find((e) => e.constructor.name === 'Configure').attempts = {}; - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); }); diff --git a/test/externalConverters.test.js b/test/extensions/externalConverters.test.ts similarity index 57% rename from test/externalConverters.test.js rename to test/extensions/externalConverters.test.ts index eec2fe4bc4..e5ba4fb0b9 100644 --- a/test/externalConverters.test.js +++ b/test/extensions/externalConverters.test.ts @@ -1,30 +1,16 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const path = require('path'); -const fs = require('fs'); - -zigbeeHerdsmanConverters.addDefinition = jest.fn(); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, mockController as mockZHController} from '../mocks/zigbeeHerdsman'; -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, -]; +import fs from 'fs'; +import path from 'path'; + +import * as zhc from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; jest.mock( 'mock-external-converter-module', @@ -55,10 +41,26 @@ jest.mock( }, ); -describe('Loads external converters', () => { - let controller; +const mockZHCAddDefinition = jest.fn(); +// @ts-expect-error mock +zhc.addDefinition = mockZHCAddDefinition; + +const mocksClear = [ + mockZHCAddDefinition, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockZHController.permitJoin, + mockZHController.stop, + mockMQTT.end, + mockMQTT.publish, + mockLogger.debug, + mockLogger.error, +]; + +describe('Extension: ExternalConverters', () => { + let controller: Controller; - let resetExtension = async () => { + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'ExternalConverters'); await controller.enableDisableExtension(true, 'ExternalConverters'); }; @@ -84,15 +86,15 @@ describe('Loads external converters', () => { it('Does not load external converters', async () => { settings.set(['external_converters'], []); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(0); + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(0); }); it('Loads external converters', async () => { - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); + 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']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({ + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(1); + expect(mockZHCAddDefinition).toHaveBeenCalledWith({ mock: true, zigbeeModel: ['external_converter_device'], vendor: 'external', @@ -106,13 +108,13 @@ describe('Loads external converters', () => { it('Loads multiple external converters', async () => { fs.copyFileSync( - path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), + 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); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, { + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(2); + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(1, { mock: 1, model: 'external_converters_device_1', zigbeeModel: ['external_converter_device_1'], @@ -122,7 +124,7 @@ describe('Loads external converters', () => { toZigbee: [], exposes: [], }); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(2, { mock: 2, model: 'external_converters_device_2', zigbeeModel: ['external_converter_device_2'], @@ -137,8 +139,8 @@ describe('Loads external converters', () => { it('Loads external converters from package', async () => { settings.set(['external_converters'], ['mock-external-converter-module']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({ + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(1); + expect(mockZHCAddDefinition).toHaveBeenCalledWith({ mock: true, }); }); @@ -146,22 +148,22 @@ describe('Loads external converters', () => { it('Loads multiple external converters from package', async () => { settings.set(['external_converters'], ['mock-multiple-external-converter-module']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, { + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(2); + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(1, { mock: 1, }); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(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')); + 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(() => { + mockZHCAddDefinition.mockImplementationOnce(() => { throw new Error('Invalid definition!'); }); await resetExtension(); - expect(logger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`); + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`); }); }); diff --git a/test/externalExtension.test.js b/test/extensions/externalExtension.test.ts similarity index 56% rename from test/externalExtension.test.js rename to test/extensions/externalExtension.test.ts index 38c7fafd71..20c3bbc6d9 100644 --- a/test/externalExtension.test.js +++ b/test/extensions/externalExtension.test.ts @@ -1,54 +1,55 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const path = require('path'); -const {rimrafSync} = require('rimraf'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const stringify = require('json-stable-stringify-without-jsonify'); -const flushPromises = require('./lib/flushPromises'); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, mockController as mockZHController, returnDevices} from '../mocks/zigbeeHerdsman'; + +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import {rimrafSync} from 'rimraf'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + 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, + mockZHController.permitJoin, + mockZHController.stop, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockMQTT.end, + mockMQTT.publish, + mockLogger.debug, + mockLogger.error, ]; -const fs = require('fs'); -const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); -const unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); - -describe('User extensions', () => { - let controller; +describe('Extension: ExternalExtension', () => { + let controller: Controller; + let mkdirSyncSpy: jest.SpyInstance; + let unlinkSyncSpy: jest.SpyInstance; beforeAll(async () => { jest.useFakeTimers(); - }); - - beforeEach(async () => { - data.writeDefaultConfiguration(); - settings.reRead(); - mocksClear.forEach((m) => m.mockClear()); + mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); + unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); }); afterAll(async () => { jest.useRealTimers(); }); - beforeEach(() => { - zigbeeHerdsman.returnDevices.splice(0); - controller = new Controller(jest.fn(), jest.fn()); + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + mocksClear.forEach((m) => m.mockClear()); + returnDevices.splice(0); mocksClear.forEach((m) => m.mockClear()); data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultState(); }); + afterEach(() => { const extensionPath = path.join(data.mockDir, 'extension'); rimrafSync(extensionPath); @@ -56,14 +57,14 @@ describe('User extensions', () => { it('Load user extension', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', stringify([{name: 'exampleExtension.js', code: extensionCode}]), {retain: true, qos: 0}, @@ -73,20 +74,20 @@ describe('User extensions', () => { it('Load user extension from api call', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), 'utf-8'); 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})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', stringify([{name: 'foo.js', code: extensionCode}]), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0}, @@ -96,50 +97,49 @@ describe('User extensions', () => { }); it('Do not load corrupted extensions', async () => { - const extensionPath = path.join(data.mockDir, 'extension'); 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})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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]); + const payload = JSON.parse(mockMQTT.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'); + 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); + 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.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'})); + mockMQTTEvents.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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'non existing.js'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/extension/remove', stringify({data: {}, status: 'error', error: "Extension non existing.js doesn't exists"}), {retain: false, qos: 0}, diff --git a/test/extensions/frontend.test.ts b/test/extensions/frontend.test.ts new file mode 100644 index 0000000000..500589b95b --- /dev/null +++ b/test/extensions/frontend.test.ts @@ -0,0 +1,392 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {EventHandler, flushPromises} from '../mocks/utils'; +import {devices} from '../mocks/zigbeeHerdsman'; + +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import ws from 'ws'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +let mockHTTPOnRequest: (request: {url: string}, response: number) => void; +const mockHTTPEvents: Record = {}; +const mockHTTP = { + listen: jest.fn(), + on: (event: string, handler: EventHandler): void => { + mockHTTPEvents[event] = handler; + }, + close: jest.fn().mockImplementation((cb) => cb()), +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let mockHTTPSOnRequest: (request: {url: string}, response: number) => void; +const mockHTTPSEvents: Record = {}; +const mockHTTPS = { + listen: jest.fn(), + on: (event: string, handler: EventHandler): void => { + mockHTTPSEvents[event] = handler; + }, + close: jest.fn().mockImplementation((cb) => cb()), +}; + +const mockWSocket = { + close: jest.fn(), +}; + +const mockWSClientEvents: Record = {}; +const mockWSClient = { + on: (event: string, handler: EventHandler): void => { + mockWSClientEvents[event] = handler; + }, + send: jest.fn(), + terminate: jest.fn(), + readyState: 'close', +}; +const mockWSEvents: Record = {}; +const mockWSClients: (typeof mockWSClient)[] = []; +const mockWS = { + clients: mockWSClients, + on: (event: string, handler: EventHandler): void => { + mockWSEvents[event] = handler; + }, + handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => { + cb(mockWSocket); + }), + emit: jest.fn(), + close: jest.fn(), +}; + +let mockNodeStaticPath: string = ''; +const mockNodeStatic = jest.fn(); + +const mockFinalHandler = jest.fn(); + +jest.mock('http', () => ({ + createServer: jest.fn().mockImplementation((onRequest) => { + mockHTTPOnRequest = onRequest; + return mockHTTP; + }), + Agent: jest.fn(), +})); + +jest.mock('https', () => ({ + createServer: jest.fn().mockImplementation((onRequest) => { + mockHTTPSOnRequest = onRequest; + return mockHTTPS; + }), + Agent: jest.fn(), +})); + +jest.mock('connect-gzip-static', () => + jest.fn().mockImplementation((path) => { + mockNodeStaticPath = path; + return mockNodeStatic; + }), +); + +jest.mock('zigbee2mqtt-frontend', () => ({ + getPath: (): string => 'my/dummy/path', +})); + +jest.mock('ws', () => ({ + OPEN: 'open', + Server: jest.fn().mockImplementation(() => { + return mockWS; + }), +})); + +jest.mock('finalhandler', () => + jest.fn().mockImplementation(() => { + return mockFinalHandler; + }), +); + +const mocksClear = [ + mockHTTP.close, + mockHTTP.listen, + mockHTTPS.close, + mockHTTPS.listen, + mockWSocket.close, + mockWS.close, + mockWS.handleUpgrade, + mockWS.emit, + mockWSClient.send, + mockWSClient.terminate, + mockNodeStatic, + mockFinalHandler, + mockMQTT.publish, + mockLogger.error, +]; + +describe('Extension: Frontend', () => { + let controller: Controller; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + beforeEach(async () => { + mockWS.clients = []; + data.writeDefaultConfiguration(); + data.writeDefaultState(); + settings.reRead(); + settings.set(['frontend'], {port: 8081, host: '127.0.0.1'}); + settings.set(['homeassistant'], true); + devices.bulb.linkquality = 10; + mocksClear.forEach((m) => m.mockClear()); + mockWSClient.readyState = 'close'; + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + afterEach(async () => { + delete devices.bulb.linkquality; + }); + + it('Start/stop with defaults', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + it('Start/stop without host', async () => { + settings.set(['frontend'], {port: 8081}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + it('Start/stop unix socket', async () => { + settings.set(['frontend'], {host: '/tmp/zigbee2mqtt.sock'}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock'); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + 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')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + it('Start/stop HTTPS invalid : missing config', async () => { + settings.set(['frontend', 'ssl_cert'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.crt')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + 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')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + it('Websocket interaction', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + mockWSClient.readyState = 'open'; + mockWS.clients.push(mockWSClient); + await mockWSEvents.connection(mockWSClient); + + const allTopics = mockWSClient.send.mock.calls.map((m) => JSON.parse(m).topic); + expect(allTopics).toContain('bridge/devices'); + expect(allTopics).toContain('bridge/info'); + expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}})); + expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}})); + + // Message + mockMQTT.publish.mockClear(); + mockWSClient.send.mockClear(); + mockWSClientEvents.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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), + ); + mockWSClientEvents.message(undefined, false); + mockWSClientEvents.message('', false); + mockWSClientEvents.message(null, false); + await flushPromises(); + + // Error + mockWSClientEvents.error(new Error('This is an error')); + expect(mockLogger.error).toHaveBeenCalledWith('WebSocket error: This is an error'); + + // Received message on socket + expect(mockWSClient.send).toHaveBeenCalledTimes(1); + expect(mockWSClient.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.send.mockClear(); + mockWSClient.readyState = 'close'; + mockWSClientEvents.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); + expect(mockWSClient.send).toHaveBeenCalledTimes(0); + + // Send last seen on connect + mockWSClient.send.mockClear(); + mockWSClient.readyState = 'open'; + settings.set(['advanced'], {last_seen: 'ISO_8601'}); + mockWS.clients.push(mockWSClient); + await mockWSEvents.connection(mockWSClient); + expect(mockWSClient.send).toHaveBeenCalledWith( + stringify({topic: 'remote', payload: {brightness: 255, last_seen: '1970-01-01T00:00:01.000Z'}}), + ); + }); + + it('onRequest/onUpgrade', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + const mockSocket = {destroy: jest.fn()}; + mockHTTPEvents.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url: 'http://localhost:8080/api'}, mockSocket, 3, expect.any(Function)); + mockWS.handleUpgrade.mock.calls[0][3](99); + expect(mockWS.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'}); + + mockHTTPOnRequest({url: '/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + }); + + it('Static server', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + }); + + it('Authentification', async () => { + const authToken = 'sample-secure-token'; + settings.set(['frontend'], {auth_token: authToken}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + const mockSocket = {destroy: jest.fn()}; + mockHTTPEvents.upgrade({url: '/api'}, mockSocket, mockWSocket); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url: '/api'}, mockSocket, mockWSocket, expect.any(Function)); + expect(mockWSocket.close).toHaveBeenCalledWith(4401, 'Unauthorized'); + + mockWSocket.close.mockClear(); + mockWS.emit.mockClear(); + + const url = `/api?token=${authToken}`; + mockWS.handleUpgrade.mockClear(); + mockHTTPEvents.upgrade({url: url}, mockSocket, 3); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function)); + expect(mockWSocket.close).toHaveBeenCalledTimes(0); + mockWS.handleUpgrade.mock.calls[0][3](mockWSocket); + expect(mockWS.emit).toHaveBeenCalledWith('connection', mockWSocket, {url}); + }); + + it.each(['/z2m/', '/z2m'])('Works with non-default base url %s', async (baseUrl) => { + settings.set(['frontend'], {base_url: baseUrl}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m/api'}); + + mockHTTPOnRequest({url: '/z2m'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m', url: '/'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + mockHTTPOnRequest({url: '/z2m/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + mockHTTPOnRequest({url: '/z/file.txt'}, 2); + expect(mockNodeStatic).not.toHaveBeenCalled(); + expect(mockFinalHandler).toHaveBeenCalled(); + }); + + it('Works with non-default complex base url', async () => { + const baseUrl = '/z2m-more++/c0mplex.url/'; + settings.set(['frontend'], {base_url: baseUrl}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m-more++/c0mplex.url/api'}); + + mockHTTPOnRequest({url: '/z2m-more++/c0mplex.url'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m-more++/c0mplex.url', url: '/'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + mockHTTPOnRequest({url: '/z2m-more++/c0mplex.url/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m-more++/c0mplex.url/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + mockHTTPOnRequest({url: '/z/file.txt'}, 2); + expect(mockNodeStatic).not.toHaveBeenCalled(); + expect(mockFinalHandler).toHaveBeenCalled(); + }); +}); diff --git a/test/group.test.js b/test/extensions/groups.test.ts similarity index 52% rename from test/group.test.js rename to test/extensions/groups.test.ts index 4c160bb416..559f059b7a 100644 --- a/test/group.test.js +++ b/test/extensions/groups.test.ts @@ -1,25 +1,30 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const stringify = require('json-stable-stringify-without-jsonify'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45542'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45724'); - -const MQTT = require('./stub/mqtt'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const settings = require('../lib/util/settings'); - -describe('Groups', () => { - let controller; - - let resetExtension = async () => { +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {toZigbee as zhcToZigbee} from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +returnDevices.push( + devices.coordinator.ieeeAddr, + devices.bulb_color.ieeeAddr, + devices.bulb.ieeeAddr, + devices.QBKG03LM.ieeeAddr, + devices.bulb_color_2.ieeeAddr, + devices.bulb_2.ieeeAddr, + devices.GLEDOPTO_2ID.ieeeAddr, +); + +describe('Extension: Groups', () => { + let controller: Controller; + + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Groups'); await controller.enableDisableExtension(true, 'Groups'); }; @@ -36,116 +41,115 @@ describe('Groups', () => { }); beforeEach(() => { - Object.values(zigbeeHerdsman.groups).forEach((g) => (g.members = [])); + Object.values(groups).forEach((g) => (g.members = [])); data.writeDefaultConfiguration(); settings.reRead(); - MQTT.publish.mockClear(); - zigbeeHerdsman.groups.gledopto_group.command.mockClear(); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + mockMQTT.publish.mockClear(); + groups.gledopto_group.command.mockClear(); + zhcToZigbee.__clearStore__(); + // @ts-expect-error private 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)); + groups.group_1.members.push(devices.bulb.getEndpoint(1)!); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ - zigbeeHerdsman.devices.bulb.getEndpoint(1), - zigbeeHerdsman.devices.bulb_color.getEndpoint(1), - ]); + expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); }); it('Apply group updates remove', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const endpoint = devices.bulb_color.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); + expect(groups.group_1.members).toStrictEqual([]); }); it('Apply group updates remove handle fail', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const endpoint = devices.bulb_color.getEndpoint(1)!; endpoint.removeFromGroup.mockImplementationOnce(() => { throw new Error('failed!'); }); - const group = zigbeeHerdsman.groups.group_1; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); - logger.error.mockClear(); + mockLogger.error.mockClear(); await resetExtension(); - expect(logger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([endpoint]); + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); + expect(groups.group_1.members).toStrictEqual([endpoint]); }); it('Move to non existing group', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {3: {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); + expect(groups.group_1.members).toStrictEqual([]); }); it('Add non standard endpoint to group with name', async () => { - const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + const QBKG03LM = devices.QBKG03LM; 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)]); + expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); }); it('Add non standard endpoint to group with number', async () => { - const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + const QBKG03LM = devices.QBKG03LM; 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)]); + expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); }); it('Shouldnt crash on non-existing devices', async () => { - logger.error.mockClear(); + mockLogger.error.mockClear(); 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'"); + expect(groups.group_1.members).toStrictEqual([]); + expect(mockLogger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); }); it('Should resolve device friendly names', async () => { - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); + settings.set(['devices', devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); 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), - ]); + expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); }); it('Should publish group state change when a device in it changes state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); 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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not republish identical optimistic group states', async () => { - const device1 = zigbeeHerdsman.devices.bulb_2; - const device2 = zigbeeHerdsman.devices.bulb_color_2; - const group = zigbeeHerdsman.groups.group_tradfri_remote; + const device1 = devices.bulb_2; + const device2 = devices.bulb_color_2; await resetExtension(); - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device: device1, @@ -153,7 +157,7 @@ describe('Groups', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device: device2, @@ -162,33 +166,33 @@ describe('Groups', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(6); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(6); + expect(mockMQTT.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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_with_tradfri', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/ha_discovery_group', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -197,83 +201,108 @@ describe('Groups', () => { }); it('Should publish state change of all members when a group changes its state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = 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]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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 () => { // Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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 () => { - const group = zigbeeHerdsman.groups.gledopto_group; + const group = groups.gledopto_group; await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -284,20 +313,20 @@ describe('Groups', () => { }); it('Should publish state of group when specific state of specific endpoint is changed', async () => { - const group = zigbeeHerdsman.groups.gledopto_group; + const group = groups.gledopto_group; await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -307,47 +336,52 @@ describe('Groups', () => { }); it('Should publish state change of all members when a group changes its state, filtered', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.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(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}}); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], { 1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}, @@ -356,21 +390,26 @@ describe('Groups', () => { }); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -378,22 +417,27 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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 () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -401,21 +445,21 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), @@ -425,11 +469,11 @@ describe('Groups', () => { }); it('Should not publish state change off if any lights within are still on when changed via shared group', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -438,23 +482,33 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'})); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_2', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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 () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -462,54 +516,64 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; 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}, }); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200})); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.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( + expect(mockMQTT.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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), {retain: false, qos: 0}, @@ -518,11 +582,11 @@ describe('Groups', () => { }); it('Should publish state change off even when missing current state', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -530,33 +594,47 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.state.state = {}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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 device = devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const group = groups.group_1; 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'bulb_color', group: 'group_1'}, transaction: '123', status: 'ok'}), {retain: false, qos: 0}, @@ -565,9 +643,9 @@ describe('Groups', () => { }); it('Add to group via MQTT fails', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); @@ -575,13 +653,13 @@ describe('Groups', () => { 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.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}, @@ -590,19 +668,19 @@ describe('Groups', () => { }); it('Add to group with slashes via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups['group/with/slashes']; + const group = 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(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: '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/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'bulb_color', group: 'group/with/slashes'}, status: 'ok'}), {retain: false, qos: 0}, @@ -611,18 +689,18 @@ describe('Groups', () => { }); it('Add to group via MQTT with postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -631,20 +709,20 @@ describe('Groups', () => { }); it('Add to group via MQTT with postfix shouldnt add it twice', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -653,19 +731,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -674,22 +752,22 @@ describe('Groups', () => { }); it('Remove from group via MQTT keeping device reporting', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -698,19 +776,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT when in zigbee but not in settings', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -719,19 +797,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 1', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: '0x0017880104e45542/3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -740,19 +818,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 2', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'wall_switch_double/3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -761,19 +839,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 3', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: '0x0017880104e45542/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -782,19 +860,19 @@ describe('Groups', () => { }); it('Remove from group all', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); 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'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); 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( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove_all', stringify({data: {device: '0x0017880104e45542/right'}, status: 'ok'}), {retain: false, qos: 0}, @@ -804,12 +882,12 @@ describe('Groups', () => { it('Error when adding to non-existing group', async () => { await resetExtension(); - logger.error.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({ data: {device: 'bulb_color', group: 'group_1_not_existing'}, @@ -823,12 +901,12 @@ describe('Groups', () => { it('Error when adding a non-existing device', async () => { 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'})); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'})); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({ data: {device: 'bulb_color_not_existing', group: 'group_1'}, @@ -842,15 +920,15 @@ describe('Groups', () => { it('Error when adding a non-existing endpoint', async () => { await resetExtension(); - logger.error.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({ data: {device: 'bulb_color/not_existing_endpoint', group: 'group_1'}, @@ -863,54 +941,54 @@ describe('Groups', () => { }); it('Should only include relevant properties when publishing member states', async () => { - const bulbColor = zigbeeHerdsman.devices.bulb_color; - const bulbColorTemp = zigbeeHerdsman.devices.bulb; - const group = zigbeeHerdsman.groups.group_1; - group.members.push(bulbColor.getEndpoint(1)); - group.members.push(bulbColorTemp.getEndpoint(1)); + const bulbColor = devices.bulb_color; + const bulbColorTemp = devices.bulb; + const group = 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]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 50}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 50}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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}})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.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( + expect(mockMQTT.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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 548}), {retain: true, qos: 0}, diff --git a/test/homeassistant.test.js b/test/extensions/homeassistant.test.ts similarity index 84% rename from test/homeassistant.test.js rename to test/extensions/homeassistant.test.ts index 23493d7f0a..ecebcf3ab1 100644 --- a/test/homeassistant.test.js +++ b/test/extensions/homeassistant.test.ts @@ -1,86 +1,107 @@ -const data = require('./stub/data'); -const settings = require('../lib/util/settings'); -const stringify = require('json-stable-stringify-without-jsonify'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const flushPromises = require('./lib/flushPromises'); -const MQTT = require('./stub/mqtt'); -const sleep = require('./stub/sleep'); -const Controller = require('../lib/controller'); - -describe('HomeAssistant extension', () => { - let version; - let z2m_version; - let controller; - let extension; - let origin; - - let resetExtension = async (runTimers = true) => { +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import assert from 'assert'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import HomeAssistant from '../../lib/extension/homeassistant'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.debug, mockLogger.warning, mockLogger.error]; + +describe('Extension: HomeAssistant', () => { + let controller: Controller; + let version: string; + let z2m_version: string; + let extension: HomeAssistant; + const origin = {name: 'Zigbee2MQTT', sw: '', url: 'https://www.zigbee2mqtt.io'}; + + const resetExtension = async (runTimers = true): Promise => { await controller.enableDisableExtension(false, 'HomeAssistant'); - MQTT.publish.mockClear(); + mocksClear.forEach((m) => m.mockClear()); await controller.enableDisableExtension(true, 'HomeAssistant'); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'HomeAssistant'); + if (runTimers) { await jest.runOnlyPendingTimersAsync(); } }; - let resetDiscoveryPayloads = (id) => { + const resetDiscoveryPayloads = (id: string): void => { // Change discovered payload, otherwise it's not re-published because it's the same. + // @ts-expect-error private Object.values(extension.discovered[id].messages).forEach((m) => (m.payload = 'changed')); }; - let clearDiscoveredTrigger = (id) => { + const clearDiscoveredTrigger = (id: string): void => { + // @ts-expect-error private extension.discovered[id].triggers = new Set(); }; - beforeEach(async () => { - data.writeDefaultConfiguration(); - settings.reRead(); - settings.set(['homeassistant'], true); - data.writeEmptyState(); - controller.state.load(); - await resetExtension(); - await flushPromises(); - }); - beforeAll(async () => { - z2m_version = (await require('../lib/util/utils').default.getZigbee2MQTTVersion()).version; - origin = {name: 'Zigbee2MQTT', sw: z2m_version, url: 'https://www.zigbee2mqtt.io'}; + const {getZigbee2MQTTVersion} = (await import('../../lib/util/utils')).default; + z2m_version = (await getZigbee2MQTTVersion()).version; version = `Zigbee2MQTT ${z2m_version}`; + origin.sw = z2m_version; jest.useFakeTimers(); settings.set(['homeassistant'], true); data.writeDefaultConfiguration(); settings.reRead(); data.writeEmptyState(); - MQTT.publish.mockClear(); - sleep.mock(); - controller = new Controller(false); + mockMQTT.publish.mockClear(); + mockSleep.mock(); + controller = new Controller(jest.fn(), jest.fn()); await controller.start(); }); afterAll(async () => { jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); + }); + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + settings.set(['homeassistant'], true); + data.writeEmptyState(); + // @ts-expect-error private + controller.state.load(); + await resetExtension(); + await flushPromises(); }); - it('Should not have duplicate type/object_ids in a mapping', () => { - const duplicated = []; - require('zigbee-herdsman-converters').definitions.forEach((d) => { - const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes; - const device = {definition: d, isDevice: () => true, isGroup: () => false, options: {}, exposes: () => exposes, zh: {endpoints: []}}; + it('Should not have duplicate type/object_ids in a mapping', async () => { + const duplicated: string[] = []; + (await import('zigbee-herdsman-converters')).definitions.forEach((d) => { + const exposes = typeof d.exposes == 'function' ? d.exposes(undefined, undefined) : d.exposes; + const device = { + definition: d, + isDevice: (): boolean => true, + isGroup: (): boolean => false, + options: {}, + exposes: (): unknown[] => exposes, + zh: {endpoints: []}, + }; + // @ts-expect-error private const configs = extension.getConfigs(device); - const cfg_type_object_ids = []; + const cfgTypeObjectIds: string[] = []; configs.forEach((c) => { const id = c['type'] + '/' + c['object_id']; - if (cfg_type_object_ids.includes(id)) { + if (cfgTypeObjectIds.includes(id)) { // A dynamic function must exposes all possible attributes for the docs if (typeof d.exposes != 'function') { duplicated.push(d.model); } } else { - cfg_type_object_ids.push(id); + cfgTypeObjectIds.push(id); } }); }); @@ -129,7 +150,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -158,7 +179,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_9/switch/config', stringify(payload), {retain: true, qos: 1}, @@ -178,7 +199,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -187,7 +207,7 @@ describe('HomeAssistant extension', () => { enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -208,7 +228,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -216,7 +235,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -237,7 +256,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -245,7 +263,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), {retain: true, qos: 1}, @@ -267,7 +285,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -275,7 +292,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', stringify(payload), {retain: true, qos: 1}, @@ -298,7 +315,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -306,7 +322,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', stringify(payload), {retain: true, qos: 1}, @@ -321,7 +337,6 @@ describe('HomeAssistant extension', () => { 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', @@ -335,7 +350,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_left }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_left/config', stringify(payload), {retain: true, qos: 1}, @@ -350,7 +365,6 @@ describe('HomeAssistant extension', () => { 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', @@ -364,7 +378,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_right }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_right/config', stringify(payload), {retain: true, qos: 1}, @@ -384,7 +398,6 @@ describe('HomeAssistant extension', () => { 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, @@ -398,7 +411,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), {retain: true, qos: 1}, @@ -423,7 +436,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -441,7 +453,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -449,21 +460,21 @@ describe('HomeAssistant extension', () => { }); // Should subscribe to `homeassistant/#` to find out what devices are already discovered. - expect(MQTT.subscribe).toHaveBeenCalledWith(`homeassistant/#`); + expect(mockMQTT.subscribe).toHaveBeenCalledWith(`homeassistant/#`); // Retained Home Assistant discovery message arrives - await MQTT.events.message(topic1, payload1); - await MQTT.events.message(topic2, payload2); + await mockMQTTEvents.message(topic1, payload1); + await mockMQTTEvents.message(topic2, payload2); await jest.runOnlyPendingTimersAsync(); // Should unsubscribe to not receive all messages that are going to be published to `homeassistant/#` again. - expect(MQTT.unsubscribe).toHaveBeenCalledWith(`homeassistant/#`); + expect(mockMQTT.unsubscribe).toHaveBeenCalledWith(`homeassistant/#`); - expect(MQTT.publish).not.toHaveBeenCalledWith(topic1, expect.anything(), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic1, expect.anything(), expect.any(Object), expect.any(Function)); // Device automation should not be cleared - expect(MQTT.publish).not.toHaveBeenCalledWith(topic2, '', expect.any(Object), expect.any(Function)); - expect(logger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic2, '', expect.any(Object), expect.any(Function)); + expect(mockLogger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`); }); it('Should discover devices with precision', async () => { @@ -494,7 +505,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -502,7 +512,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -523,7 +533,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -531,7 +540,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -552,7 +561,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -560,7 +568,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), {retain: true, qos: 1}, @@ -621,7 +629,7 @@ describe('HomeAssistant extension', () => { icon: 'mdi:test', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -639,7 +647,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'custom model', manufacturer: 'Not from Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -651,7 +658,7 @@ describe('HomeAssistant extension', () => { object_id: 'weather_sensor_humidity', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -686,7 +693,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'Weather Sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -695,7 +701,7 @@ describe('HomeAssistant extension', () => { enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -716,7 +722,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'Weather Sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -724,7 +729,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -749,10 +754,9 @@ describe('HomeAssistant extension', () => { await resetExtension(); - let payload; await flushPromises(); - payload = { + const payload = { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/my_switch/set', device: { @@ -760,7 +764,6 @@ describe('HomeAssistant extension', () => { 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', @@ -774,7 +777,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x0017880104e45541/light/config', stringify(payload), {retain: true, qos: 1}, @@ -792,13 +795,13 @@ describe('HomeAssistant extension', () => { await resetExtension(); await flushPromises(); - const topics = MQTT.publish.mock.calls.map((c) => c[0]); + const topics = mockMQTT.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'); }); it('Shouldnt discover sensor when set to null', async () => { - logger.error.mockClear(); + mockLogger.error.mockClear(); settings.set(['devices', '0x0017880104e45522'], { homeassistant: {humidity: null}, friendly_name: 'weather_sensor', @@ -807,15 +810,13 @@ describe('HomeAssistant extension', () => { await resetExtension(); - const topics = MQTT.publish.mock.calls.map((c) => c[0]); + const topics = mockMQTT.publish.mock.calls.map((c) => c[0]); 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 = { + const payload = { state_topic: 'zigbee2mqtt/fan', state_value_template: '{{ value_json.fan_state }}', command_topic: 'zigbee2mqtt/fan/set/fan_state', @@ -837,7 +838,6 @@ describe('HomeAssistant extension', () => { 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', @@ -845,7 +845,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/fan/0x0017880104e45548/fan/config', stringify(payload), {retain: true, qos: 1}, @@ -871,7 +871,6 @@ describe('HomeAssistant extension', () => { 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', @@ -896,7 +895,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/climate/0x0017882104a44559/climate/config', stringify(payload), {retain: true, qos: 1}, @@ -940,7 +939,7 @@ describe('HomeAssistant extension', () => { unique_id: '0x18fc2600000d7ae2_climate_zigbee2mqtt', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/climate/0x18fc2600000d7ae2/climate/config', stringify(payload), {qos: 1, retain: true}, @@ -970,7 +969,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45551'], name: 'smart vent', - sw_version: null, model: 'Smart vent (SV01)', manufacturer: 'Keen Home', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -978,7 +976,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0x0017880104e45551/cover/config', stringify(payload), {retain: true, qos: 1}, @@ -993,7 +991,6 @@ describe('HomeAssistant extension', () => { 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', @@ -1015,7 +1012,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config', stringify(payload), {retain: true, qos: 1}, @@ -1027,9 +1024,7 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic'); await resetExtension(); - let payload; - - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -1043,7 +1038,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1051,7 +1045,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'my_custom_discovery_topic/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1063,27 +1057,27 @@ describe('HomeAssistant extension', () => { settings.set(['experimental', 'output'], 'attribute'); settings.set(['homeassistant'], true); expect(() => { - new Controller(false); + new Controller(jest.fn(), jest.fn()); }).toThrow('Home Assistant integration is not possible with attribute output!'); }); it('Should throw error when homeassistant.discovery_topic equals the mqtt.base_topic', async () => { settings.set(['mqtt', 'base_topic'], 'homeassistant'); expect(() => { - new Controller(false); + new Controller(jest.fn(), jest.fn()); }).toThrow("'homeassistant.discovery_topic' cannot not be equal to the 'mqtt.base_topic' (got 'homeassistant')"); }); it('Should warn when starting with cache_state false', async () => { settings.set(['advanced', 'cache_state'], false); - logger.warning.mockClear(); + mockLogger.warning.mockClear(); await resetExtension(); - expect(logger.warning).toHaveBeenCalledWith('In order for Home Assistant integration to work properly set `cache_state: true'); + expect(mockLogger.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 device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1093,11 +1087,11 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/weather_sensor', stringify({battery: null, humidity: null, linkquality: null, pressure: null, temperature: -0.85, voltage: null}), {retain: false, qos: 1}, @@ -1106,14 +1100,14 @@ describe('HomeAssistant extension', () => { }); it('Should copy hue/saturtion to h/s if present', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; 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); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ color: {hue: 0, saturation: 100, h: 0, s: 100}, @@ -1130,14 +1124,14 @@ describe('HomeAssistant extension', () => { }); it('Should not copy hue/saturtion if properties are missing', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const data = {currentX: 29991, currentY: 26872}; const payload = {data, cluster: 'lightingColorCtrl', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ color: {x: 0.4576, y: 0.41}, @@ -1154,14 +1148,14 @@ describe('HomeAssistant extension', () => { }); it('Should not copy hue/saturtion if color is missing', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; 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); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ linkquality: null, @@ -1176,7 +1170,7 @@ describe('HomeAssistant extension', () => { }); it('Shouldt discover when already discovered', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1186,16 +1180,17 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); // 1 publish is the publish from receive - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); }); it('Should discover when not discovered yet', async () => { + // @ts-expect-error private extension.discovered = {}; - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1205,8 +1200,8 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); const payloadHA = { unit_of_measurement: '°C', @@ -1222,7 +1217,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1230,7 +1224,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payloadHA), {retain: true, qos: 1}, @@ -1239,21 +1233,25 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt discover when device leaves', async () => { + // @ts-expect-error private extension.discovered = {}; - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceLeave(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceLeave(payload); await flushPromises(); }); it('Should discover when options change', async () => { - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; + assert('ieeeAddr' in device); resetDiscoveryPayloads(device.ieeeAddr); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {test: 123}}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/light/${device.ID}/light/config`, expect.any(String), expect.any(Object), @@ -1263,16 +1261,17 @@ describe('HomeAssistant extension', () => { it('Should send all status when home assistant comes online (default topic)', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - expect(MQTT.subscribe).toHaveBeenCalledWith('homeassistant/status'); + expect(mockMQTT.subscribe).toHaveBeenCalledWith('homeassistant/status'); await flushPromises(); - MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/status', 'online'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('homeassistant/status', 'online'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -1287,7 +1286,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({ action: null, @@ -1301,20 +1300,21 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - expect(MQTT.subscribe).toHaveBeenCalledWith('hass/status'); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status', 'online'); + expect(mockMQTT.subscribe).toHaveBeenCalledWith('hass/status'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status', 'online'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -1329,7 +1329,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({ action: null, @@ -1347,36 +1347,36 @@ describe('HomeAssistant extension', () => { it('Shouldnt send all status when home assistant comes offline', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); await flushPromises(); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status', 'offline'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status', 'offline'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Shouldnt send all status when home assistant comes online with different topic', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status_different', 'offline'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status_different', 'offline'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should discover devices with availability', async () => { settings.set(['availability'], true); await resetExtension(); - let payload; - - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', enabled_by_default: true, @@ -1390,7 +1390,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1402,7 +1401,7 @@ describe('HomeAssistant extension', () => { ], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1411,35 +1410,35 @@ describe('HomeAssistant extension', () => { }); it('Should clear discovery when device is removed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', '', {retain: true, qos: 1}, @@ -1448,11 +1447,11 @@ describe('HomeAssistant extension', () => { }); it('Should clear discovery when group is removed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', '', {retain: true, qos: 1}, @@ -1461,13 +1460,13 @@ describe('HomeAssistant extension', () => { }); it('Should refresh discovery when device is renamed', async () => { - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({topic: 'zigbee2mqtt/weather_sensor/action'}), ); await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/rename', stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: true}), ); @@ -1489,7 +1488,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1497,21 +1495,21 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({ automation_type: 'trigger', @@ -1523,7 +1521,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1535,8 +1532,8 @@ describe('HomeAssistant extension', () => { }); it('Should refresh discovery when group is renamed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/rename', stringify({from: 'ha_discovery_group', to: 'ha_discovery_group_new', homeassistant_rename: true}), ); @@ -1582,14 +1579,14 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', '', {retain: true, qos: 1}, @@ -1598,14 +1595,14 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt refresh discovery when device is renamed and homeassistant_rename is false', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/rename', stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: false}), ); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, @@ -1626,7 +1623,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1634,7 +1630,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1657,7 +1653,6 @@ describe('HomeAssistant extension', () => { 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', @@ -1667,7 +1662,7 @@ describe('HomeAssistant extension', () => { entity_category: 'diagnostic', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify(payload), {retain: true, qos: 1}, @@ -1676,16 +1671,16 @@ describe('HomeAssistant extension', () => { }); it('Should discover trigger when click is published', async () => { - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(7); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/click/config'); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; const payload1 = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); await flushPromises(); const discoverPayloadAction = { @@ -1698,14 +1693,13 @@ describe('HomeAssistant extension', () => { 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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), {retain: true, qos: 1}, @@ -1722,25 +1716,24 @@ describe('HomeAssistant extension', () => { 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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.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(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({ action: 'single', @@ -1755,14 +1748,14 @@ describe('HomeAssistant extension', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.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( + expect(mockMQTT.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}, @@ -1770,43 +1763,43 @@ describe('HomeAssistant extension', () => { ); // Should only discover it once - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload1); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload1); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.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(mockMQTT.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( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'}), ); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'}), ); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload2 = {data: {32768: 2}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), @@ -1815,15 +1808,15 @@ describe('HomeAssistant extension', () => { // Should rediscover when already discovered in previous session but with different name clearDiscoveredTrigger('0x0017880104e45520'); - await MQTT.events.message( + await mockMQTTEvents.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); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload2); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), @@ -1836,14 +1829,14 @@ describe('HomeAssistant extension', () => { homeassistant: {device_automation: null}, }); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; const payload1 = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', expect.any(String), expect.any(Object), @@ -1859,7 +1852,7 @@ describe('HomeAssistant extension', () => { }); await resetExtension(); - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(6); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config'); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/battery/config'); @@ -1870,17 +1863,17 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_legacy_triggers'], false); await resetExtension(); - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(5); expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/click/config'); expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/action/config'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); const discoverPayload = { @@ -1893,50 +1886,49 @@ describe('HomeAssistant extension', () => { 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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({action: 'single', battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); }); it('Should republish payload to postfix topic with lightWithPostfix config', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/U202DST600ZB/l2/set', stringify({state: 'ON', brightness: 20})); + await mockMQTTEvents.message('zigbee2mqtt/U202DST600ZB/l2/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB', stringify({state_l2: 'ON', brightness_l2: 20, linkquality: null, state_l1: null, power_on_behavior_l1: null, power_on_behavior_l2: null}), {qos: 0, retain: false}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB/l2', stringify({state: 'ON', brightness: 20, power_on_behavior: null}), {qos: 0, retain: false}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB/l1', stringify({state: null, power_on_behavior: null}), {qos: 0, retain: false}, @@ -1945,28 +1937,28 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt crash in onPublishEntityState on group publish', async () => { - logger.error.mockClear(); - MQTT.publish.mockClear(); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - expect(logger.error).toHaveBeenCalledTimes(0); + expect(mockLogger.error).toHaveBeenCalledTimes(0); group.members.pop(); }); it('Should counter an action payload with an empty payload', async () => { - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + mockMQTT.publish.mockClear(); + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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({ + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({ action: 'single', click: null, battery: null, @@ -1975,9 +1967,9 @@ describe('HomeAssistant extension', () => { 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({ + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({ action: '', click: null, battery: null, @@ -1986,21 +1978,21 @@ describe('HomeAssistant extension', () => { 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'); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config'); + expect(mockMQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); }); it('Should clear outdated configs', async () => { // Non-existing group -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_91231/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_91231/light/config', '', {qos: 1, retain: true}, @@ -2008,33 +2000,33 @@ describe('HomeAssistant extension', () => { ); // Existing group -> dont clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing group with old topic structure (1.20.0) -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function)); // Existing group, non existing config -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_9/switch/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/switch/config', '', {qos: 1, retain: true}, @@ -2042,42 +2034,47 @@ describe('HomeAssistant extension', () => { ); // Non-existing device -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', '', {qos: 1, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x123/temperature/config', + '', + {qos: 1, retain: true}, + expect.any(Function), + ); // Existing device -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Non-existing device of different instance -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing device but non-existing config -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', '', {qos: 1, retain: true}, @@ -2085,52 +2082,52 @@ describe('HomeAssistant extension', () => { ); // Non-existing device but invalid payload -> clear - MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', '1}3'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('homeassistant/sensor/0x123/temperature/config', '1}3'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing device, device automation -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Device automation of different instance -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.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); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Device was flagged to be excluded from homeassistant discovery settings.set(['devices', '0x000b57fffec6a5b2', 'homeassistant'], null); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', '', {qos: 1, retain: true}, expect.any(Function), ); - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', '', {qos: 1, retain: true}, @@ -2142,10 +2139,9 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false); await resetExtension(); - let payload; await flushPromises(); - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -2158,7 +2154,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -2166,7 +2161,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -2175,9 +2170,9 @@ describe('HomeAssistant extension', () => { }); it('Should rediscover group when device is added to it', async () => { - resetDiscoveryPayloads(9); - MQTT.publish.mockClear(); - MQTT.events.message( + resetDiscoveryPayloads('9'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'ha_discovery_group', device: 'wall_switch_double/left'}), ); @@ -2221,7 +2216,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2268,7 +2263,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2296,7 +2291,6 @@ describe('HomeAssistant extension', () => { 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, @@ -2313,7 +2307,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2337,7 +2331,6 @@ describe('HomeAssistant extension', () => { 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, @@ -2353,7 +2346,7 @@ describe('HomeAssistant extension', () => { entity_category: 'diagnostic', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/last_seen/config', stringify(payload), {retain: true, qos: 1}, @@ -2365,11 +2358,9 @@ describe('HomeAssistant extension', () => { settings.set(['frontend', 'url'], 'http://zigbee.mqtt'); await resetExtension(); - - let payload; await flushPromises(); - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -2383,7 +2374,6 @@ describe('HomeAssistant extension', () => { 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', @@ -2392,7 +2382,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -2402,15 +2392,18 @@ describe('HomeAssistant extension', () => { it('Should rediscover scenes when a scene is changed', async () => { // Device/endpoint scenes. - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color_2); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color_2)!; + assert('ieeeAddr' in device); resetDiscoveryPayloads(device.ieeeAddr); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitScenesChanged({entity: device}); await flushPromises(); // Discovery messages for scenes have been purged. - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, '', {retain: true, qos: 1}, @@ -2437,24 +2430,26 @@ describe('HomeAssistant extension', () => { origin: origin, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(12); + expect(mockMQTT.publish).toHaveBeenCalledTimes(12); // Group scenes. + // @ts-expect-error private const group = controller.zigbee.resolveEntity('ha_discovery_group'); - resetDiscoveryPayloads(9); + resetDiscoveryPayloads('9'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitScenesChanged({entity: group}); await flushPromises(); // Discovery messages for scenes have been purged. - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, '', {retain: true, qos: 1}, @@ -2481,17 +2476,17 @@ describe('HomeAssistant extension', () => { origin: origin, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(6); + expect(mockMQTT.publish).toHaveBeenCalledTimes(6); }); it('Should not clear bridge entities unnecessarily', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const topic = 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config'; const payload = { @@ -2514,13 +2509,14 @@ describe('HomeAssistant extension', () => { availability_mode: 'all', }; + // @ts-expect-error private controller.eventBus.emitMQTTMessage({ topic: topic, message: stringify(payload), }); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith(topic, '', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic, '', {retain: true, qos: 1}, expect.any(Function)); }); it('Should discover bridge entities', async () => { @@ -2551,7 +2547,7 @@ describe('HomeAssistant extension', () => { origin: origin, device: devicePayload, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/connection_state/config', stringify(payload), {retain: true, qos: 1}, @@ -2574,7 +2570,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/restart_required/config', stringify(payload), {retain: true, qos: 1}, @@ -2594,7 +2590,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config', stringify(payload), {retain: true, qos: 1}, @@ -2617,7 +2613,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/select/1221051039810110150109113116116_0x00124b00120144ae/log_level/config', stringify(payload), {retain: true, qos: 1}, @@ -2638,7 +2634,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/version/config', stringify(payload), {retain: true, qos: 1}, @@ -2659,7 +2655,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/coordinator_version/config', stringify(payload), {retain: true, qos: 1}, @@ -2681,7 +2677,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/network_map/config', stringify(payload), {retain: true, qos: 1}, @@ -2702,7 +2698,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/permit_join_timeout/config', stringify(payload), {retain: true, qos: 1}, @@ -2727,7 +2723,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_0x00124b00120144ae/permit_join/config', stringify(payload), {retain: true, qos: 1}, @@ -2736,14 +2732,14 @@ describe('HomeAssistant extension', () => { }); it('Should remove discovery entries for removed exposes when device options change', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0xf4ce368a38be56a1/light_l2/config', '', {retain: true, qos: 1}, @@ -2752,12 +2748,12 @@ describe('HomeAssistant extension', () => { }); it('Should publish discovery message when a converter announces changed exposes', async () => { - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices['BMCT-SLZ']; + mockMQTT.publish.mockClear(); + const device = devices['BMCT-SLZ']; 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 mockZHEvents.message(msg); await flushPromises(); const payload = { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], @@ -2767,7 +2763,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Bosch', model: 'Light/shutter control unit II (BMCT-SLZ)', name: '0x18fc26000000cafe', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, entity_category: 'config', @@ -2782,7 +2777,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.device_mode }}', enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/select/0x18fc26000000cafe/device_mode/config', stringify(payload), {retain: true, qos: 1}, diff --git a/test/networkMap.test.js b/test/extensions/networkMap.test.ts similarity index 83% rename from test/networkMap.test.js rename to test/extensions/networkMap.test.ts index 0f553d7589..f8f347de97 100644 --- a/test/networkMap.test.js +++ b/test/extensions/networkMap.test.ts @@ -1,61 +1,34 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const stringify = require('json-stable-stringify-without-jsonify'); -const fs = require('fs'); -const path = require('path'); -const {coordinator, bulb, bulb_color, WXKG02LM_rev1, CC2530_ROUTER, unsupported_router, external_converter_device} = zigbeeHerdsman.devices; - -zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); -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); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; -const setTimeoutNative = setTimeout; - -describe('Networkmap', () => { - let controller; +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; - beforeAll(async () => { - jest.useFakeTimers(); - Date.now = jest.fn(); - Date.now.mockReturnValue(10000); - data.writeDefaultConfiguration(); - settings.reRead(); - data.writeEmptyState(); - 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']); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - mocksClear.forEach((m) => m.mockClear()); - await flushPromises(); - }); +import fs from 'fs'; +import path from 'path'; - beforeEach(async () => { - mocksClear.forEach((m) => m.mockClear()); - await flushPromises(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = 1000; - external_converter_device.lastSeen = 1000; - global.setTimeout = (r) => r(); - }); +import stringify from 'json-stable-stringify-without-jsonify'; - afterEach(async () => { - global.setTimeout = setTimeoutNative; - }); +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; - afterAll(async () => { - jest.useRealTimers(); - }); +returnDevices.push( + devices.coordinator.ieeeAddr, + devices.bulb.ieeeAddr, + devices.bulb_color.ieeeAddr, + devices.WXKG02LM_rev1.ieeeAddr, + devices.CC2530_ROUTER.ieeeAddr, + devices.unsupported_router.ieeeAddr, + devices.external_converter_device.ieeeAddr, +); + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; - function mock() { +describe('Extension: NetworkMap', () => { + let controller: Controller; + + const mock = (): void => { /** * Topology * | -> external_device @@ -66,49 +39,96 @@ describe('Networkmap', () => { * | -> CC2530_ROUTER -> WXKG02LM_rev1 * */ - coordinator.lqi = jest.fn().mockResolvedValue({ + devices.coordinator.lqi.mockResolvedValueOnce({ 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, + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 2, + depth: 1, + linkquality: 120, + }, + {ieeeAddr: devices.bulb.ieeeAddr, networkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, + { + ieeeAddr: devices.external_converter_device.ieeeAddr, + networkAddress: devices.external_converter_device.networkAddress, relationship: 2, depth: 1, linkquality: 92, }, ], }); - coordinator.routingTable = jest.fn().mockResolvedValue({ - table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], + devices.coordinator.routingTable.mockResolvedValueOnce({ + table: [{destinationAddress: devices.CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: devices.bulb.networkAddress}], }); - bulb.lqi = jest.fn().mockResolvedValue({ + devices.bulb.lqi.mockResolvedValueOnce({ 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}, + { + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 1, + depth: 2, + linkquality: 110, + }, + { + ieeeAddr: devices.CC2530_ROUTER.ieeeAddr, + networkAddress: devices.CC2530_ROUTER.networkAddress, + relationship: 1, + depth: 2, + linkquality: 100, + }, ], }); - bulb.routingTable = jest.fn().mockResolvedValue({table: []}); - bulb_color.lqi = jest.fn().mockResolvedValue({neighbors: []}); - bulb_color.routingTable = jest.fn().mockResolvedValue({table: []}); - CC2530_ROUTER.lqi = jest.fn().mockResolvedValue({ + devices.CC2530_ROUTER.lqi.mockResolvedValueOnce({ 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}, + {ieeeAddr: '0x0000000000000000', networkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, + { + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 4, + depth: 2, + linkquality: 130, + }, ], }); - CC2530_ROUTER.routingTable = jest.fn().mockResolvedValue({table: []}); - unsupported_router.lqi = jest.fn().mockRejectedValue(new Error('failed')); - unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed')); - } + devices.unsupported_router.lqi.mockRejectedValueOnce('failed').mockRejectedValueOnce('failed'); + devices.unsupported_router.routingTable.mockRejectedValueOnce('failed').mockRejectedValueOnce('failed'); + }; + + beforeAll(async () => { + jest.useFakeTimers(); + mockSleep.mock(); + jest.spyOn(Date, 'now').mockReturnValue(10000); + data.writeDefaultConfiguration(); + settings.reRead(); + data.writeEmptyState(); + 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']); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + }); + + beforeEach(async () => { + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + const device = devices.bulb_color; + device.lastSeen = 1000; + devices.external_converter_device.lastSeen = 1000; + }); + + afterEach(async () => {}); + + afterAll(async () => { + mockSleep.restore(); + jest.useRealTimers(); + }); it('Output raw networkmap', async () => { mock(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -196,7 +216,7 @@ describe('Networkmap', () => { friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, + // modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -291,23 +311,22 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); it('Output graphviz networkmap', async () => { mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; + const device = devices.bulb_color; + device.lastSeen = undefined; const endpoint = device.getEndpoint(1); 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/request/networkmap', stringify({type: 'graphviz', routes: true})); + await mockZHEvents.message(payload); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = `digraph G { node[shape=record]; @@ -327,7 +346,7 @@ describe('Networkmap', () => { }`; const expectedLines = expected.split('\n'); - const actualLines = JSON.parse(call[1]).data.value.split('\n'); + const actualLines = JSON.parse(mockMQTT.publish.mock.calls[0][1]).data.value.split('\n'); for (let i = 0; i < expectedLines.length; i++) { expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); @@ -336,17 +355,16 @@ describe('Networkmap', () => { it('Output plantuml networkmap', async () => { mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; + const device = devices.bulb_color; + device.lastSeen = undefined; const endpoint = device.getEndpoint(1); 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/request/networkmap', stringify({type: 'plantuml', routes: true})); + await mockZHEvents.message(payload); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = `' paste into: https://www.planttext.com/ @@ -429,7 +447,7 @@ describe('Networkmap', () => { @enduml`; const expectedLines = expected.split('\n'); - const actualLines = JSON.parse(call[1]).data.value.split('\n'); + const actualLines = JSON.parse(mockMQTT.publish.mock.calls[0][1]).data.value.split('\n'); for (let i = 0; i < expectedLines.length; i++) { expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); @@ -438,10 +456,11 @@ describe('Networkmap', () => { it('Should throw error when requesting invalid type', async () => { mock(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.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}, @@ -452,12 +471,11 @@ describe('Networkmap', () => { it('Should exclude disabled devices from networkmap', async () => { settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true); mock(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -516,12 +534,10 @@ describe('Networkmap', () => { ], nodes: [ { - // definition: null, failed: [], friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -600,20 +616,19 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); it('Handles retrying request when first attempt fails', async () => { settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true); mock(); - bulb.lqi.mockRejectedValueOnce(new Error('failed')); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + devices.bulb.lqi.mockRejectedValueOnce('failed'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -672,12 +687,10 @@ describe('Networkmap', () => { ], nodes: [ { - // definition: null, failed: [], friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -756,7 +769,7 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); }); diff --git a/test/extensions/onEvent.test.ts b/test/extensions/onEvent.test.ts new file mode 100644 index 0000000000..ce6a49287a --- /dev/null +++ b/test/extensions/onEvent.test.ts @@ -0,0 +1,88 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import * as zhc from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mockOnEvent = jest.fn(); +const mockLivoloOnEvent = jest.fn(); +const mappedLivolo = zhc.findByModel('TI0001'); +mappedLivolo.onEvent = mockLivoloOnEvent; +// @ts-expect-error mock +zhc.onEvent = mockOnEvent; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: OnEvent', () => { + let controller: Controller; + + beforeEach(async () => { + jest.useFakeTimers(); + data.writeDefaultConfiguration(); + settings.reRead(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + await flushPromises(); + }); + + beforeEach(async () => { + // @ts-expect-error private + controller.state.state = {}; + data.writeDefaultConfiguration(); + settings.reRead(); + mocksClear.forEach((m) => m.mockClear()); + mockOnEvent.mockClear(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + it('Should call with start event', async () => { + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + const call = mockLivoloOnEvent.mock.calls[0]; + expect(call[0]).toBe('start'); + expect(call[1]).toStrictEqual({}); + expect(call[2]).toBe(devices.LIVOLO); + expect(call[3]).toStrictEqual(settings.getDevice(devices.LIVOLO.ieeeAddr)); + expect(call[4]).toStrictEqual({}); + }); + + it('Should call with stop event', async () => { + mockLivoloOnEvent.mockClear(); + await controller.stop(); + await flushPromises(); + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + const call = mockLivoloOnEvent.mock.calls[0]; + expect(call[0]).toBe('stop'); + expect(call[1]).toStrictEqual({}); + expect(call[2]).toBe(devices.LIVOLO); + }); + + it('Should call with zigbee event', async () => { + mockLivoloOnEvent.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.LIVOLO}); + await flushPromises(); + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + expect(mockLivoloOnEvent).toHaveBeenCalledWith( + 'deviceAnnounce', + {device: devices.LIVOLO}, + devices.LIVOLO, + settings.getDevice(devices.LIVOLO.ieeeAddr), + {}, + ); + }); + + it('Should call index onEvent with zigbee event', async () => { + mockOnEvent.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.LIVOLO}); + await flushPromises(); + expect(mockOnEvent).toHaveBeenCalledTimes(1); + expect(mockOnEvent).toHaveBeenCalledWith('deviceAnnounce', {device: devices.LIVOLO}, devices.LIVOLO); + }); +}); diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts new file mode 100644 index 0000000000..97e3a8aa14 --- /dev/null +++ b/test/extensions/otaUpdate.test.ts @@ -0,0 +1,426 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import OTAUpdate from 'lib/extension/otaUpdate'; + +import * as zhc from 'zigbee-herdsman-converters'; +import {zigbeeOTA} from 'zigbee-herdsman-converters/lib/ota'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, devices.bulb.save, mockLogger.info]; + +describe('Extension: OTAUpdate', () => { + let controller: Controller; + let mapped: zhc.Definition; + let updateToLatestSpy: jest.SpyInstance; + let isUpdateAvailableSpy: jest.SpyInstance; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'OTAUpdate'); + await controller.enableDisableExtension(true, 'OTAUpdate'); + }; + + beforeAll(async () => { + jest.useFakeTimers(); + mockSleep.mock(); + data.writeDefaultConfiguration(); + settings.reRead(); + settings.set(['ota', 'ikea_ota_use_test_url'], true); + settings.reRead(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + // @ts-expect-error minimal mock + mapped = await zhc.findByDevice(devices.bulb); + updateToLatestSpy = jest.spyOn(mapped.ota!, 'updateToLatest'); + isUpdateAvailableSpy = jest.spyOn(mapped.ota!, 'isUpdateAvailable'); + await flushPromises(); + }); + + afterAll(async () => { + mockSleep.restore(); + jest.useRealTimers(); + }); + + beforeEach(async () => { + // @ts-expect-error private + const extension: OTAUpdate = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate'); + // @ts-expect-error private + extension.lastChecked = {}; + // @ts-expect-error private + extension.inProgress = new Set(); + mocksClear.forEach((m) => m.mockClear()); + devices.bulb.save.mockClear(); + devices.bulb.endpoints[0].commandResponse.mockClear(); + updateToLatestSpy.mockClear(); + isUpdateAvailableSpy.mockClear(); + // @ts-expect-error private + controller.state.state = {}; + }); + + afterEach(async () => { + settings.set(['ota', 'disable_automatic_update_check'], false); + }); + + it('Should OTA update a device', async () => { + let count = 0; + devices.bulb.endpoints[0].read.mockImplementation(() => { + count++; + return {swBuildId: count, dateCode: '2019010' + count}; + }); + updateToLatestSpy.mockImplementationOnce((device, onProgress) => { + onProgress(0, null); + onProgress(10, 3600.2123); + return 90; + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); + await flushPromises(); + expect(mockLogger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(0); + expect(updateToLatestSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledWith(devices.bulb, expect.any(Function)); + expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); + expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); + expect(mockLogger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); + expect(mockLogger.info).toHaveBeenCalledWith( + `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, + ); + expect(devices.bulb.save).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); + expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'updating', progress: 0}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'updating', progress: 10, remaining: 3600}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + }); + + it('Should handle when OTA update fails', async () => { + devices.bulb.endpoints[0].read.mockImplementation(() => { + return {swBuildId: 1, dateCode: '2019010'}; + }); + devices.bulb.save.mockClear(); + updateToLatestSpy.mockImplementationOnce(() => { + throw new Error('Update failed'); + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'})); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'available'}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.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), + ); + }); + + it('Should be able to check if OTA update is available', async () => { + isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + + mockMQTT.publish.mockClear(); + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(2); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should handle if OTA update check fails', async () => { + isUpdateAvailableSpy.mockImplementationOnce(() => { + throw new Error('RF signals disturbed because of dogs barking'); + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.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), + ); + }); + + it('Should fail when device does not exist', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo'); + await flushPromises(); + expect(mockMQTT.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), + ); + }); + + it('Should not check for OTA when device does not support it', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch'); + await flushPromises(); + expect(mockMQTT.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), + ); + }); + + it('Should refuse to check/update when already in progress', async () => { + isUpdateAvailableSpy.mockImplementationOnce(() => { + return new Promise((resolve) => { + setTimeout(() => resolve(), 99999); + }); + }); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + jest.runOnlyPendingTimers(); + await flushPromises(); + expect(mockMQTT.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), + ); + }); + + it('Shouldnt crash when read modelID before/after OTA update fails', async () => { + devices.bulb.endpoints[0].read.mockRejectedValueOnce('Failed from').mockRejectedValueOnce('Failed to'); + updateToLatestSpy.mockImplementation(); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); + await flushPromises(); + expect(mockMQTT.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), + ); + }); + + it('Should check for update when device requests it', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + mockLogger.info.mockClear(); + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(mockLogger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + + // Should not request again when device asks again after a short time + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + + mockLogger.info.mockClear(); + await mockZHEvents.message(payload); + await flushPromises(); + expect(mockLogger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockRejectedValueOnce('Nothing to find here'); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle'}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should check for update when device requests it and it is not available', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should not check for update when device requests it and disable_automatic_update_check is set to true', async () => { + settings.set(['ota', 'disable_automatic_update_check'], true); + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(0); + }); + + it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => { + const device = devices.HGZB04D; + const data = {imageType: 12382}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + 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 = devices.SV01; + const data = {imageType: 12382}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); + }); + + it('Set zigbee_ota_override_index_location', async () => { + const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride'); + settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json'); + 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'); + spyUseIndexOverride.mockClear(); + }); + + it('Clear update state on startup', async () => { + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); + // @ts-expect-error private + controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}}); + await resetExtension(); + // @ts-expect-error private + expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}}); + }); +}); diff --git a/test/publish.test.js b/test/extensions/publish.test.ts similarity index 56% rename from test/publish.test.js rename to test/extensions/publish.test.ts index 36f5e060e8..e986597e55 100644 --- a/test/publish.test.js +++ b/test/extensions/publish.test.ts @@ -1,199 +1,203 @@ -const data = require('./stub/data'); -const sleep = require('./stub/sleep'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const {loadTopicGetSetRegex} = require('../lib/extension/publish'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const stringify = require('json-stable-stringify-without-jsonify'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -const expectNothingPublished = () => { - Object.values(zigbeeHerdsman.devices).forEach((d) => { - d.endpoints.forEach((e) => { - expect(e.command).toHaveBeenCalledTimes(0); - expect(e.read).toHaveBeenCalledTimes(0); - expect(e.write).toHaveBeenCalledTimes(0); - }); - }); - Object.values(zigbeeHerdsman.groups).forEach((g) => { - expect(g.command).toHaveBeenCalledTimes(0); - }); -}; +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {toZigbee} from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import {loadTopicGetSetRegex} from '../../lib/extension/publish'; +import * as settings from '../../lib/util/settings'; -describe('Publish', () => { - let controller; +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Publish', () => { + let controller: Controller; + + const expectNothingPublished = (): void => { + Object.values(devices).forEach((d) => { + d.endpoints.forEach((e) => { + expect(e.command).toHaveBeenCalledTimes(0); + expect(e.read).toHaveBeenCalledTimes(0); + expect(e.write).toHaveBeenCalledTimes(0); + }); + }); + Object.values(groups).forEach((g) => { + expect(g.command).toHaveBeenCalledTimes(0); + }); + }; beforeAll(async () => { jest.useFakeTimers(); data.writeEmptyState(); controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); + mockSleep.mock(); await controller.start(); await flushPromises(); }); beforeEach(async () => { data.writeDefaultConfiguration(); + // @ts-expect-error private controller.state.state = {}; settings.reRead(); loadTopicGetSetRegex(); mocksClear.forEach((m) => m.mockClear()); - Object.values(zigbeeHerdsman.devices).forEach((d) => { + Object.values(devices).forEach((d) => { d.endpoints.forEach((e) => { e.command.mockClear(); e.read.mockClear(); e.write.mockClear(); }); }); - Object.values(zigbeeHerdsman.groups).forEach((g) => { + Object.values(groups).forEach((g) => { g.command.mockClear(); }); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + toZigbee.__clearStore__(); }); afterAll(async () => { await jest.runOnlyPendingTimersAsync(); jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); }); it('Should publish messages to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'}); + expect(mockMQTT.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 mockMQTTEvents.message('zigbee2mqtt/foo', ''); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', ''); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices when there is no converters', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_no: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness_no: '200'})); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices when there is a get converter but no set', async () => { - await MQTT.events.message('zigbee2mqtt/thermostat/set', stringify({relay_status_log_rsp: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/thermostat/set', stringify({relay_status_log_rsp: '200'})); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices with complicated topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; 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'})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/wohnzimmer.light.wall.right/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in %', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_percent: '92'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in number', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 230})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: '222'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp in %', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: 100})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep', async () => { - const device = zigbeeHerdsman.devices.QBKG04LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch/set', stringify({state: 'OFF'})); + const device = devices.QBKG04LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep and postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/right/set', stringify({state: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/right/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); 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(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with endpoint ID', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/3/set', stringify({state: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/wall_switch_double', stringify({state_right: 'OFF'}), {qos: 0, retain: false}, @@ -202,26 +206,26 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices to non default-ep with state_[EP]', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/set', stringify({state_right: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_right: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'}); + expect(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.QS_Zigbee_D02_TRIAC_2C_LN; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/0x0017882194e45543/set', stringify({state_l2: 'ON', brightness_l2: 50})); + const device = devices.QS_Zigbee_D02_TRIAC_2C_LN; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN', stringify({brightness_l2: 50, state_l2: 'ON'}), {retain: false, qos: 0}, @@ -230,9 +234,9 @@ describe('Publish', () => { }); it('Should publish messages to Tuya switch with dummy endpoints', async () => { - const device = zigbeeHerdsman.devices.TS0601_switch; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/TS0601_switch/set', stringify({state_l2: 'ON'})); + const device = devices.TS0601_switch; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/TS0601_switch/set', stringify({state_l2: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -241,7 +245,7 @@ describe('Publish', () => { {dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_switch', stringify({state_l2: 'ON'}), {retain: false, qos: 0}, @@ -250,11 +254,11 @@ describe('Publish', () => { }); it('Should publish messages to Tuya cover switch with dummy endpoints', async () => { - const device = zigbeeHerdsman.devices.TS0601_cover_switch; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state: 'OPEN'})); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state_l1: 'ON'})); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/l2/set', stringify({state: 'OFF'})); + const device = devices.TS0601_cover_switch; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state: 'OPEN'})); + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state_l1: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/l2/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command).toHaveBeenCalledWith( @@ -275,7 +279,7 @@ describe('Publish', () => { {dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_cover_switch', stringify({state_l2: 'OFF', state_l1: 'ON', state: 'OPEN'}), {retain: false, qos: 0}, @@ -284,28 +288,28 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color xy and state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON'}), {retain: false, qos: 0}, @@ -314,15 +318,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy and brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON', brightness: 20}), {retain: false, qos: 0}, @@ -331,15 +335,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy, brightness and state on', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color: {x: 100, y: 50}, state: 'ON', brightness: 20, color_mode: 'xy'}), {retain: false, qos: 0}, @@ -348,15 +352,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy, brightness and state off', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'OFF'}), {retain: false, qos: 0}, @@ -365,70 +369,70 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color rgb object', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {r: 100, g: 200, b: 10}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {rgb: '100,200,10'}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.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 () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: '50'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); }); it('Should publish messages groups', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); group.members.pop(); }); it('Should publish messages to groups with brightness_percent', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128}); group.members.pop(); }); it('Should publish messages to groups when converter is not in the default list but device in it supports it', async () => { - const group = zigbeeHerdsman.groups.thermostat_group; - await MQTT.events.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'})); + const group = groups.thermostat_group; + await mockMQTTEvents.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith( @@ -440,132 +444,134 @@ describe('Publish', () => { }); it('Should publish messages to groups with on and brightness', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50}); group.members.pop(); }); it('Should publish messages to groups with off and brightness', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'}); group.members.pop(); }); it('Should publish messages to groups color', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); group.members.pop(); }); it('Should publish messages to groups color temperature', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); group.members.pop(); }); it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => { - settings.addGroup('group_12312', 12312); - expect(Object.values(zigbeeHerdsman.groups).length).toBe(10); - await MQTT.events.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'})); + settings.addGroup('group_12312', '12312'); + expect(Object.values(groups).length).toBe(10); + await mockMQTTEvents.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'})); await flushPromises(); - expect(Object.values(zigbeeHerdsman.groups).length).toBe(11); + expect(Object.values(groups).length).toBe(11); // group contains no device - expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(0); - delete zigbeeHerdsman.groups.group_12312; + // @ts-expect-error runtime mock + expect(groups.group_12312.command).toHaveBeenCalledTimes(0); + // @ts-expect-error runtime mock + delete groups.group_12312; }); it('Shouldnt publish new state when optimistic = false', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; settings.set(['devices', device.ieeeAddr, 'optimistic'], false); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.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); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; settings.set(['devices', device.ieeeAddr, 'filtered_optimistic'], ['brightness']); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); }); it('Shouldnt publish new state when optimistic = false for group', async () => { settings.set(['groups', '2', 'optimistic'], false); - await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({brightness: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({brightness: '200'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt1/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt1/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt1/bulb_color/sett', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt1/bulb_color/sett', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/write', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/write', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('set', stringify({state: 'ON'})); + await mockMQTTEvents.message('set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle get', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/get', stringify({state: '', brightness: ''})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/get', stringify({state: '', brightness: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(2); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -573,10 +579,10 @@ describe('Publish', () => { }); it('Should handle get with multiple endpoints', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - const endpoint3 = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_left: '', state_right: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + const endpoint3 = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_left: '', state_right: ''})); await flushPromises(); expect(endpoint2.read).toHaveBeenCalledTimes(1); expect(endpoint2.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -585,10 +591,10 @@ describe('Publish', () => { }); it('Should handle get with multiple cover endpoints', async () => { - const device = zigbeeHerdsman.devices.zigfred_plus; - const endpoint11 = device.getEndpoint(11); - const endpoint12 = device.getEndpoint(12); - await MQTT.events.message('zigbee2mqtt/zigfred_plus/get', stringify({state_l6: '', state_l7: ''})); + const device = devices.zigfred_plus; + const endpoint11 = device.getEndpoint(11)!; + const endpoint12 = device.getEndpoint(12)!; + await mockMQTTEvents.message('zigbee2mqtt/zigfred_plus/get', stringify({state_l6: '', state_l7: ''})); await flushPromises(); expect(endpoint11.read).toHaveBeenCalledTimes(1); expect(endpoint11.read).toHaveBeenCalledWith('closuresWindowCovering', ['currentPositionLiftPercentage']); @@ -597,31 +603,31 @@ describe('Publish', () => { }); it('Should log error when device has no such endpoint (via topic)', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/center/get', stringify({state: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/center/get', stringify({state: ''})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Device 'wall_switch_double' has no endpoint 'center'`); + expect(mockLogger.error).toHaveBeenCalledWith(`Device 'wall_switch_double' has no endpoint 'center'`); expect(endpoint2.read).toHaveBeenCalledTimes(0); }); it('Should log error when device has no definition', async () => { - const device = zigbeeHerdsman.devices.interviewing; - logger.error.mockClear(); - await MQTT.events.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'})); + const device = devices.interviewing; + mockLogger.error.mockClear(); + await mockMQTTEvents.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'})); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m'); + expect(mockLogger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m'); }); it('Should log error when device has no such endpoint (via property)', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - const endpoint3 = device.getEndpoint(3); - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_center: '', state_right: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + const endpoint3 = device.getEndpoint(3)!; + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_center: '', state_right: ''})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`No converter available for 'state_center' ("")`); + expect(mockLogger.error).toHaveBeenCalledWith(`No converter available for 'state_center' ("")`); expect(endpoint2.read).toHaveBeenCalledTimes(0); expect(endpoint3.read).toHaveBeenCalledTimes(1); expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -630,19 +636,19 @@ describe('Publish', () => { it('Should parse topic with when base topic has multiple slashes', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/at/my/home/bulb_color/get', stringify({state: ''})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/at/my/home/bulb_color/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); }); it('Should parse topic with when deviceID has multiple slashes', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/floor0/basement/my_device_id2/get', stringify({state: ''})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/floor0/basement/my_device_id2/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -651,57 +657,57 @@ describe('Publish', () => { it('Should parse topic with when base and deviceID have multiple slashes', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/basement'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/get', stringify({state: ''})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); }); it('Should parse set with attribute topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set/state', 'ON'); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set/state', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with color attribute topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A'); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should parse set with ieeeAddr topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/0x000b57fffec6a5b3/set', stringify({state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/0x000b57fffec6a5b3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with non-existing postfix', async () => { - await MQTT.events.message('zigbee2mqtt/wall_switch_double/invalid/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/invalid/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should allow to invert cover', async () => { - const device = zigbeeHerdsman.devices.J1; - const endpoint = device.getEndpoint(1); + const device = devices.J1_cover; + const endpoint = device.getEndpoint(1)!; // Non-inverted (open = 100, close = 0) - await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); + await mockMQTTEvents.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 10}, {}); @@ -709,30 +715,30 @@ describe('Publish', () => { // // Inverted endpoint.command.mockClear(); settings.set(['devices', device.ieeeAddr, 'invert_cover'], true); - await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); + await mockMQTTEvents.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 90}, {}); }); it('Should send state update on toggle specific endpoint', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'TOGGLE'); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.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([ + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF'}), {qos: 0, retain: false}, @@ -741,17 +747,17 @@ describe('Publish', () => { }); it('Should not use state converter on non-json message when value is not on/off/toggle', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON_RANDOM'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON_RANDOM'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(0); }); it('Should parse set with postfix topic and attribute', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); @@ -760,39 +766,37 @@ describe('Publish', () => { it('Should parse set with and slashes in base and deviceID postfix topic', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.QBKG03LM; + const device = devices.QBKG03LM; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'in/basement/wall_switch_double'); - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/at/my/home/in/basement/wall_switch_double/left/set', stringify({state: 'ON'})); + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.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', {}, {}); }); it('Should parse set with number at the end of friendly_name and postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; + const device = devices.QBKG03LM; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'ground_floor/kitchen/wall_switch/2'); - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', stringify({state: 'ON'})); + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.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', {}, {}); }); it('Should not publish messages to zigbee devices when payload is invalid', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: true})); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: 1})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: true})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: 1})); await flushPromises(); expectNothingPublished(); }); it('Should set state before color', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', color: {x: 0.701, y: 0.299}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); @@ -800,29 +804,29 @@ describe('Publish', () => { }); it('Should also use on/off cluster when controlling group with switch', async () => { - const group = zigbeeHerdsman.groups.group_with_switch; + const group = groups.group_with_switch; - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'ON', brightness: 100})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON', brightness: 100}), {retain: false, qos: 0}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'OFF', brightness: 100}), {retain: false, qos: 0}, @@ -831,36 +835,36 @@ describe('Publish', () => { }); it('Should use transition when brightness with group', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); settings.set(['groups', '1', 'transition'], 20); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness: 100})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100}); group.members.pop(); }); it('Should use transition on brightness command', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 20}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); }); it('Should use transition from device_options on brightness command', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['device_options'], {transition: 20}); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 20}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); @@ -868,13 +872,13 @@ describe('Publish', () => { 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})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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 mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); await flushPromises(); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, @@ -883,41 +887,41 @@ describe('Publish', () => { linkquality: 10, }); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); + await mockMQTTEvents.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 mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 3, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 4, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 5, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), @@ -934,20 +938,20 @@ describe('Publish', () => { 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})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.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 mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0.5})); await flushPromises(); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 254}), @@ -961,13 +965,13 @@ describe('Publish', () => { 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})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1})); + await mockMQTTEvents.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}, {}]); @@ -976,21 +980,21 @@ describe('Publish', () => { }); it('Should use transition when color temp', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {color_temp: 200}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); }); it('Should use transition only once when setting brightness and color temperature for TRADFRI', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', brightness: 20, color_temp: 200, transition: 20}; - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); @@ -998,8 +1002,8 @@ describe('Publish', () => { }); 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})); + const group = groups.group_with_tradfri; + await mockMQTTEvents.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}, {}]); @@ -1007,21 +1011,21 @@ describe('Publish', () => { }); it('Message transition should overrule options transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 200, transition: 10}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); }); it('Should set state with brightness before color', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; 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 mockMQTTEvents.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}, {}]); @@ -1029,32 +1033,32 @@ describe('Publish', () => { }); it('Should turn device off when brightness 0 is send', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.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([ + expect(mockMQTT.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([ + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, @@ -1063,32 +1067,32 @@ describe('Publish', () => { }); it('Should turn device off when brightness 0 is send with transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0, transition: 3})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0, transition: 3})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.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([ + expect(mockMQTT.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([ + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, @@ -1097,10 +1101,10 @@ describe('Publish', () => { }); it('Should allow to set color via hue and saturation', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {color: {hue: 250, saturation: 50}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual([ @@ -1109,100 +1113,100 @@ describe('Publish', () => { {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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.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 device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'OPEN'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 100}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {position: 10}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 10}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'CLOSE'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 0}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'STOP'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'stop', {}, {}); }); it('Should turn device on with on/off when transition is provided', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', transition: 3}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); }); it('Should turn device on with on/off with transition when transition 0 is provided', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', transition: 0}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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}, {}]); }); it('Should turn device off with onOff on off with transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'OFF', transition: 1}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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(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'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); }); it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; // Set initial brightness in state - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); await flushPromises(); endpoint.command.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // Turn off - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {qos: 0, retain: false}, @@ -1210,7 +1214,7 @@ describe('Publish', () => { ]); // Bulb reports brightness while decreasing brightness - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, @@ -1219,8 +1223,8 @@ describe('Publish', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {qos: 0, retain: false}, @@ -1228,12 +1232,12 @@ describe('Publish', () => { ]); // Turn on again - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 3})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[2]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: false}, @@ -1242,21 +1246,21 @@ describe('Publish', () => { }); it('When device is turned off with transition and turned on WITHOUT transition it should restore the brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; // Set initial brightness in state - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); await flushPromises(); endpoint.command.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // Turn off - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {qos: 0, retain: false}, @@ -1264,12 +1268,12 @@ describe('Publish', () => { ]); // Turn on again - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: false}, @@ -1279,48 +1283,54 @@ describe('Publish', () => { it('Home Assistant: should set state', async () => { settings.set(['homeassistant'], true); - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON'}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); 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'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); }); it('Home Assistant: should not set state when color temperature is also set and device is already on', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'ON'}); - const endpoint = device.zh.getEndpoint(1); + const endpoint = device.zh.getEndpoint(1)!; const payload = {state: 'ON', color_temp: 100}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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(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'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'}); }); it('Home Assistant: should set state when color temperature is also set and device is off', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'OFF'}); - const endpoint = device.zh.getEndpoint(1); + const endpoint = device.zh.getEndpoint(1)!; const payload = {state: 'ON', color_temp: 100}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({state: 'ON', color_temp: 100, color_mode: 'color_temp'}), {retain: false, qos: 0}, @@ -1330,35 +1340,38 @@ describe('Publish', () => { it('Home Assistant: should not set state when color is also set', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'ON'}); - const endpoint = device.zh.getEndpoint(1); + 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 mockMQTTEvents.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(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'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'}); }); it('Should publish correct state on toggle command to zigbee bulb', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); await flushPromises(); - // At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to MQTT. + // At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to mockMQTT. expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.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 mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), @@ -1372,11 +1385,11 @@ describe('Publish', () => { return {currentLevel: 100}; } }); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), @@ -1386,49 +1399,48 @@ describe('Publish', () => { }); it('Should publish messages with options disableDefaultResponse', async () => { - const device = zigbeeHerdsman.devices.GLEDOPTO1112; - const endpoint = device.getEndpoint(11); - await MQTT.events.message('zigbee2mqtt/led_controller_1/set', stringify({state: 'OFF'})); + const device = devices.GLEDOPTO1112; + const endpoint = device.getEndpoint(11)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.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'); - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toStrictEqual('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish brightness_move up to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: -40})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.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}, {}); }); it('Should publish brightness_move down to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: 30})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.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}, {}); }); it('HS2WD-E burglar warning', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1440,9 +1452,9 @@ describe('Publish', () => { }); it('HS2WD-E emergency warning', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 10, mode: 'emergency', strobe: false, level: 'very_high'}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1454,9 +1466,9 @@ describe('Publish', () => { }); it('HS2WD-E emergency without level', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 10, mode: 'emergency', strobe: false}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1468,9 +1480,9 @@ describe('Publish', () => { }); it('HS2WD-E wrong payload (should use defaults)', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: 'wrong'}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1483,27 +1495,27 @@ describe('Publish', () => { it('Shouldnt do anything when device is not supported', async () => { const payload = {state: 'ON'}; - await MQTT.events.message('zigbee2mqtt/unsupported2/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/unsupported2/set', stringify(payload)); await flushPromises(); expectNothingPublished(); }); it('Should publish state to roller shutter', async () => { - const endpoint = zigbeeHerdsman.devices.roller_shutter.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/roller_shutter/set', stringify({state: 'OPEN'})); + const endpoint = devices.roller_shutter.getEndpoint(1)!; + await mockMQTTEvents.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(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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish to MKS-CM-W5', async () => { - const device = zigbeeHerdsman.devices['MKS-CM-W5']; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'})); + const device = devices['MKS-CM-W5']; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1512,23 +1524,23 @@ describe('Publish', () => { {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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'}); + expect(mockMQTT.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 () => { // https://github.com/Koenkk/zigbee2mqtt/issues/2757 - const device = zigbeeHerdsman.devices['GL-S-007ZS']; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20})); + const device = devices['GL-S-007ZS']; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); await jest.runOnlyPendingTimersAsync(); 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(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GL-S-007ZS', stringify({state: 'ON', brightness: 20}), {qos: 0, retain: false}, @@ -1537,28 +1549,28 @@ describe('Publish', () => { }); 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 device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {brightness_move: 20}; - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/get', stringify(payload)); + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/get', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(0); - expect(logger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)"); + expect(mockLogger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)"); }); it('Should restore brightness when its turned on with transition, Z2M is restarted and turned on again', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/7106 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; endpoint.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 20, transition: 0.0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 20, transition: 0.0})); await flushPromises(); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + toZigbee.__clearStore__(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); @@ -1568,26 +1580,26 @@ describe('Publish', () => { it('Should restore brightness when its turned off without transition and is turned on with', async () => { // https://github.com/Koenkk/zigbee-herdsman-converters/issues/1097 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; endpoint.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 123})); + await mockMQTTEvents.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}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); + await mockMQTTEvents.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}, {}]); @@ -1595,16 +1607,16 @@ describe('Publish', () => { it('Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3332 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 150})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 150})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(4); @@ -1613,26 +1625,26 @@ describe('Publish', () => { 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([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.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([ + expect(mockMQTT.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([ + expect(mockMQTT.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([ + expect(mockMQTT.publish.mock.calls[3]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 150}), {qos: 0, retain: false}, @@ -1641,81 +1653,79 @@ describe('Publish', () => { }); 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})); + const group = groups.group_tradfri_remote; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 50, color_temp: 290})); + await mockMQTTEvents.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 mockMQTTEvents.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(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)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.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 mockMQTTEvents.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 250, color_temp: 20})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 110})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_recall: 1})); + await mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledTimes(8); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(8); + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 8, 'zigbee2mqtt/ha_discovery_group', stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), @@ -1725,28 +1735,28 @@ describe('Publish', () => { }); it('Should sync colors', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 100})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 100})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 0.1, y: 0.5}})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 0.1, y: 0.5}})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 100}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.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), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 3, 'zigbee2mqtt/bulb_color', stringify({color: {x: 0.4152, y: 0.3954}, color_mode: 'color_temp', color_temp: 300}), @@ -1756,8 +1766,8 @@ describe('Publish', () => { }); it('Log an error when entity is not found', async () => { - await MQTT.events.message('zigbee2mqtt/an_unknown_entity/set', stringify({})); + await mockMQTTEvents.message('zigbee2mqtt/an_unknown_entity/set', stringify({})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Entity 'an_unknown_entity' is unknown"); + expect(mockLogger.error).toHaveBeenCalledWith("Entity 'an_unknown_entity' is unknown"); }); }); diff --git a/test/receive.test.js b/test/extensions/receive.test.ts old mode 100755 new mode 100644 similarity index 55% rename from test/receive.test.js rename to test/extensions/receive.test.ts index 2064aaa10a..bc013b7a08 --- a/test/receive.test.js +++ b/test/extensions/receive.test.ts @@ -1,49 +1,51 @@ -const data = require('./stub/data'); -const sleep = require('./stub/sleep'); -const logger = require('./stub/logger'); -const stringify = require('json-stable-stringify-without-jsonify'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; +import stringify from 'json-stable-stringify-without-jsonify'; -describe('Receive', () => { - let controller; +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Receive', () => { + let controller: Controller; beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); + mockSleep.mock(); await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); + await jest.runOnlyPendingTimersAsync(); }); beforeEach(async () => { + // @ts-expect-error private controller.state.state = {}; data.writeDefaultConfiguration(); settings.reRead(); mocksClear.forEach((m) => m.mockClear()); - delete zigbeeHerdsman.devices.WXKG11LM.linkquality; + delete devices.WXKG11LM.linkquality; }); afterAll(async () => { jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); }); it('Should handle a zigbee message', async () => { - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; device.linkquality = 10; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({action: 'single', click: 'single', linkquality: 10}), {retain: false, qos: 0}, @@ -52,31 +54,31 @@ describe('Receive', () => { }); it('Should handle a zigbee message which uses ep (left)', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'}); + expect(mockMQTT.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 device = devices.WXKG02LM_rev1; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'}); + expect(mockMQTT.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 device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -86,19 +88,19 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should allow to invert cover', async () => { - const device = zigbeeHerdsman.devices.J1; + const device = devices.J1_cover; // Non-inverted (open = 100, close = 0) - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, @@ -107,8 +109,8 @@ describe('Receive', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, @@ -116,9 +118,9 @@ describe('Receive', () => { ); // Inverted - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); settings.set(['devices', device.ieeeAddr, 'invert_cover'], true); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, @@ -127,8 +129,8 @@ describe('Receive', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, @@ -137,18 +139,23 @@ describe('Receive', () => { }); it('Should allow to disable the legacy integration', async () => { - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/button', + stringify({action: 'single'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should debounce messages', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); const data1 = {measuredValue: 8}; const payload1 = { @@ -159,7 +166,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 1}; const payload2 = { data: data2, @@ -169,7 +176,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 2}; const payload3 = { data: data3, @@ -179,20 +186,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); 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.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should debounce and retain messages when set via device_options', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'debounce'], 0.1); settings.set(['device_options', 'retain'], true); delete settings.get().devices['0x0017880104e45522']['retain']; @@ -205,7 +212,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 1}; const payload2 = { data: data2, @@ -215,7 +222,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 2}; const payload3 = { data: data3, @@ -225,20 +232,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); 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.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + expect(mockMQTT.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; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']); const tempMsg = { @@ -249,7 +256,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(tempMsg); + await mockZHEvents.message(tempMsg); const pressureMsg = { data: {measuredValue: 2}, cluster: 'msPressureMeasurement', @@ -258,7 +265,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(pressureMsg); + await mockZHEvents.message(pressureMsg); const tempMsg2 = { data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', @@ -267,7 +274,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(tempMsg2); + await mockZHEvents.message(tempMsg2); const humidityMsg = { data: {measuredValue: 3}, cluster: 'msRelativeHumidity', @@ -276,25 +283,25 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(humidityMsg); + await mockZHEvents.message(humidityMsg); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2}); jest.runOnlyPendingTimers(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03}); }); 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". + // First send multiple measurements to device that is debouncing. Make sure only one message is sent out to mockMQTT. 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; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, @@ -302,7 +309,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, @@ -310,7 +317,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, @@ -321,17 +328,17 @@ describe('Receive', () => { await flushPromises(); jest.advanceTimersByTime(50); // Test that measurements are combined(=debounced) - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); await flushPromises(); // Test that only one MQTT is sent out and test its values. - 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); // Send another Zigbee message... - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, @@ -339,6 +346,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); + // @ts-expect-error private const realDevice = controller.zigbee.resolveEntity(device); // Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache. @@ -347,16 +355,16 @@ describe('Receive', () => { await flushPromises(); // Total of 3 messages should have triggered. - expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); // Test that message pushed by asynchronous message contains NEW measurement and not old. - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); + expect(JSON.parse(mockMQTT.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}); + expect(JSON.parse(mockMQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); }); it('Should throttle multiple messages from spamming devices', async () => { - const device = zigbeeHerdsman.devices.SPAMMER; + const device = devices.SPAMMER; const throttle_for_testing = 1; settings.set(['device_options', 'throttle'], throttle_for_testing); settings.set(['device_options', 'retain'], true); @@ -370,7 +378,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 2}; const payload2 = { data: data2, @@ -380,7 +388,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 3}; const payload3 = { data: data3, @@ -390,25 +398,25 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.01}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.01}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true}); // Now we try after elapsed time to see if it publishes next message const timeshift = throttle_for_testing * 2000; jest.advanceTimersByTime(timeshift); - expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.03}); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.03}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true}); const data4 = {measuredValue: 4}; const payload4 = { @@ -419,20 +427,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload4); + await mockZHEvents.message(payload4); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.04}); - expect(MQTT.publish.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.04}); + expect(mockMQTT.publish.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true}); }); it('Shouldnt republish old state', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3572 - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {onOff: 0}, cluster: 'genOnOff', device, @@ -440,16 +448,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'})); await flushPromises(); jest.runOnlyPendingTimers(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); }); it('Should handle a zigbee message with 1 precision', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1); const data = {measuredValue: -85}; const payload = { @@ -460,16 +468,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); + expect(mockMQTT.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; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); const data = {measuredValue: -85}; const payload = { @@ -480,16 +488,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); + expect(mockMQTT.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; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); const data = {measuredValue: -85}; const payload = { @@ -500,16 +508,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); + expect(mockMQTT.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; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); const data = {measuredValue: -85}; @@ -521,35 +529,35 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); + expect(mockMQTT.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 device = devices.WXKG02LM_rev1; 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 mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990}); + expect(mockMQTT.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 device = devices.RTCGQ11LM; 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 mockZHEvents.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({ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({ battery: 100, illuminance: 381, illuminance_lux: 381, @@ -557,66 +565,71 @@ describe('Receive', () => { device_temperature: 19, power_outage_count: 34, }); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.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 device = devices.RTCGQ11LM; 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 mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should publish last_seen epoch', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'epoch'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('number'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'ISO_8601'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601_local', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'ISO_8601_local'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); 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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle messages from Xiaomi router devices', async () => { - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const device = devices.ZNCZ02LM; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/power_plug', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -625,27 +638,27 @@ describe('Receive', () => { }); it('Should not handle messages from coordinator', async () => { - const device = zigbeeHerdsman.devices.coordinator; + const device = devices.coordinator; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should not handle messages from still interviewing devices with unknown definition', async () => { - const device = zigbeeHerdsman.devices.interviewing; + const device = devices.interviewing; const data = {onOff: 1}; - logger.debug.mockClear(); + mockLogger.debug.mockClear(); const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`); }); it('Should handle a command', async () => { - const device = zigbeeHerdsman.devices.E1743; + const device = devices.E1743; const data = {}; const payload = { data, @@ -656,45 +669,41 @@ describe('Receive', () => { linkquality: 10, meta: {zclTransactionSequenceNumber: 1}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.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(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should add elapsed', async () => { settings.set(['advanced', 'elapsed'], true); - const device = zigbeeHerdsman.devices.E1743; + const device = devices.E1743; const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'}; - const oldNow = Date.now; - Date.now = jest.fn(); - Date.now.mockReturnValue(new Date(150)); - await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 2}}); + jest.spyOn(Date, 'now').mockReturnValueOnce(150).mockReturnValueOnce(200); + await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 2}}); await flushPromises(); - Date.now.mockReturnValue(new Date(200)); - await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 3}}); + await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 3}}); 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(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]).elapsed).toBe(50); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); - Date.now = oldNow; + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'}); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1]).elapsed).toBe(50); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should log when message is from supported device but has no converters', async () => { - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const device = devices.ZNCZ02LM; const data = {inactiveText: 'hello'}; const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.debug).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.debug).toHaveBeenCalledWith( "No converter available for 'ZNCZ02LM' with cluster 'genBinaryOutput' and type 'attributeReport' and data '{\"inactiveText\":\"hello\"}'", ); }); @@ -704,8 +713,8 @@ describe('Receive', () => { // divisor of OLD is not correct and therefore underreports by factor 10. const data = {instantaneousDemand: 496, currentSummDelivered: 6648}; - const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW; - await zigbeeHerdsman.events.message({ + const SP600_NEW = devices.SP600_NEW; + await mockZHEvents.message({ data, cluster: 'seMetering', device: SP600_NEW, @@ -715,13 +724,13 @@ describe('Receive', () => { meta: {zclTransactionSequenceNumber: 1}, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6}); - MQTT.publish.mockClear(); - const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD; - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + const SP600_OLD = devices.SP600_OLD; + await mockZHEvents.message({ data, cluster: 'seMetering', device: SP600_OLD, @@ -731,16 +740,16 @@ describe('Receive', () => { meta: {zclTransactionSequenceNumber: 2}, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496}); }); it('Should emit DevicesChanged event when a converter announces changed exposes', async () => { - const device = zigbeeHerdsman.devices['BMCT-SLZ']; + const device = devices['BMCT-SLZ']; 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'); + await mockZHEvents.message(payload); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); }); }); diff --git a/test/frontend.test.js b/test/frontend.test.js deleted file mode 100644 index 4a8dea6a45..0000000000 --- a/test/frontend.test.js +++ /dev/null @@ -1,414 +0,0 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -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 finalhandler = require('finalhandler'); -const ws = require('ws'); -jest.spyOn(process, 'exit').mockImplementation(() => {}); - -afterEach(() => { - jest.clearAllMocks(); -}); - -const mockHTTP = { - implementation: { - listen: jest.fn(), - on: (event, handler) => { - mockHTTP.events[event] = handler; - }, - close: jest.fn().mockImplementation((cb) => cb()), - }, - variables: {}, - events: {}, -}; - -const mockHTTPS = { - implementation: { - listen: jest.fn(), - on: (event, handler) => { - mockHTTPS.events[event] = handler; - }, - close: jest.fn().mockImplementation((cb) => cb()), - }, - variables: {}, - events: {}, -}; - -const mockWSocket = { - close: jest.fn(), -}; - -const mockWS = { - implementation: { - clients: [], - on: (event, handler) => { - mockWS.events[event] = handler; - }, - handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => { - cb(mockWSocket); - }), - emit: jest.fn(), - close: jest.fn(), - }, - variables: {}, - events: {}, -}; - -const mockNodeStatic = { - implementation: jest.fn(), - variables: {}, - events: {}, -}; - -const mockFinalHandler = { - implementation: jest.fn(), -}; - -jest.mock('http', () => ({ - createServer: jest.fn().mockImplementation((onRequest) => { - mockHTTP.variables.onRequest = onRequest; - return mockHTTP.implementation; - }), - Agent: jest.fn(), -})); - -jest.mock('https', () => ({ - createServer: jest.fn().mockImplementation((onRequest) => { - mockHTTPS.variables.onRequest = onRequest; - return mockHTTPS.implementation; - }), - Agent: jest.fn(), -})); - -jest.mock('connect-gzip-static', () => - jest.fn().mockImplementation((path) => { - mockNodeStatic.variables.path = path; - return mockNodeStatic.implementation; - }), -); - -jest.mock('zigbee2mqtt-frontend', () => ({ - getPath: () => 'my/dummy/path', -})); - -jest.mock('ws', () => ({ - OPEN: 'open', - Server: jest.fn().mockImplementation(() => { - return mockWS.implementation; - }), -})); - -jest.mock('finalhandler', () => - jest.fn().mockImplementation(() => { - return mockFinalHandler.implementation; - }), -); - -describe('Frontend', () => { - let controller; - - beforeAll(async () => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - mockWS.implementation.clients = []; - data.writeDefaultConfiguration(); - data.writeDefaultState(); - settings.reRead(); - settings.set(['frontend'], {port: 8081, host: '127.0.0.1'}); - settings.set(['homeassistant'], true); - zigbeeHerdsman.devices.bulb.linkquality = 10; - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - 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'); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - it('Start/stop without host', async () => { - settings.set(['frontend'], {port: 8081}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - it('Start/stop unix socket', async () => { - 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'); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - 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')); - 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'); - await controller.stop(); - }); - - it('Start/stop HTTPS invalid : missing config', async () => { - 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'); - await controller.stop(); - }); - - 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')); - 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'); - await controller.stop(); - }); - - it('Websocket interaction', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - // Connect - const mockWSClient = { - implementation: { - on: (event, handler) => { - mockWSClient.events[event] = handler; - }, - send: jest.fn(), - readyState: 'open', - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await mockWS.events.connection(mockWSClient.implementation); - - const allTopics = mockWSClient.implementation.send.mock.calls.map((m) => JSON.parse(m).topic); - expect(allTopics).toContain('bridge/devices'); - expect(allTopics).toContain('bridge/info'); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}})); - 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); - 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), - ); - mockWSClient.events.message(undefined, false); - mockWSClient.events.message('', false); - mockWSClient.events.message(null, false); - await flushPromises(); - - // Error - mockWSClient.events.error(new Error('This is an error')); - expect(logger.error).toHaveBeenCalledWith('WebSocket error: This is an error'); - - // 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}, - }, - }), - ); - - // 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); - expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(0); - - // Send last seen on connect - mockWSClient.implementation.send.mockClear(); - mockWSClient.implementation.readyState = 'open'; - 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'}}), - ); - }); - - it('onReques/onUpgrade', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - const mockSocket = {destroy: jest.fn()}; - 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)); - mockWS.implementation.handleUpgrade.mock.calls[0][3](99); - expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'}); - - mockHTTP.variables.onRequest({url: '/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/file.txt', url: '/file.txt'}, 2, expect.any(Function)); - }); - - it('Static server', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - }); - - it('Authentification', async () => { - const authToken = 'sample-secure-token'; - settings.set(['frontend'], {auth_token: authToken}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - const mockSocket = {destroy: jest.fn()}; - 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'); - - mockWSocket.close.mockClear(); - mockWS.implementation.emit.mockClear(); - - const url = `/api?token=${authToken}`; - mockWS.implementation.handleUpgrade.mockClear(); - mockHTTP.events.upgrade({url: url}, mockSocket, 3); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); - expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function)); - expect(mockWSocket.close).toHaveBeenCalledTimes(0); - mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket); - expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url}); - }); - - it.each(['/z2m/', '/z2m'])('Works with non-default base url %s', async (baseUrl) => { - settings.set(['frontend'], {base_url: baseUrl}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m/api'}); - - mockHTTP.variables.onRequest({url: '/z2m'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m', url: '/'}, 2, expect.any(Function)); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - mockHTTP.variables.onRequest({url: '/z2m/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m/file.txt', url: '/file.txt'}, 2, expect.any(Function)); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - mockHTTP.variables.onRequest({url: '/z/file.txt'}, 2); - expect(mockNodeStatic.implementation).not.toHaveBeenCalled(); - expect(mockFinalHandler.implementation).toHaveBeenCalled(); - }); - - it('Works with non-default complex base url', async () => { - const baseUrl = '/z2m-more++/c0mplex.url/'; - settings.set(['frontend'], {base_url: baseUrl}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m-more++/c0mplex.url/api'}); - - mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m-more++/c0mplex.url', url: '/'}, 2, expect.any(Function)); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/z2m-more++/c0mplex.url/file.txt', url: '/file.txt'}, - 2, - expect.any(Function), - ); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - mockHTTP.variables.onRequest({url: '/z/file.txt'}, 2); - expect(mockNodeStatic.implementation).not.toHaveBeenCalled(); - expect(mockFinalHandler.implementation).toHaveBeenCalled(); - }); -}); diff --git a/test/lib/flushPromises.js b/test/lib/flushPromises.js deleted file mode 100644 index 4b040f5df8..0000000000 --- a/test/lib/flushPromises.js +++ /dev/null @@ -1,2 +0,0 @@ -const globalSetImmediate = setImmediate; -module.exports = () => new Promise(globalSetImmediate); diff --git a/test/logger.test.js b/test/logger.test.ts similarity index 79% rename from test/logger.test.js rename to test/logger.test.ts index 81e52d1b21..98973d1ab1 100644 --- a/test/logger.test.js +++ b/test/logger.test.ts @@ -1,17 +1,27 @@ -const tmp = require('tmp'); -const dir = tmp.dirSync(); -let settings; -const fs = require('fs'); -const path = require('path'); -const data = require('./stub/data'); -const {rimrafSync} = require('rimraf'); -const Transport = require('winston-transport'); +import * as data from './mocks/data'; + +import fs from 'fs'; +import {platform} from 'os'; +import path from 'path'; + +import {rimrafSync} from 'rimraf'; +import tmp from 'tmp'; +import Transport from 'winston-transport'; + +import logger from '../lib/util/logger'; +import * as settings from '../lib/util/settings'; describe('Logger', () => { - let logger; - let consoleWriteSpy; + let consoleWriteSpy: jest.SpyInstance; + const dir = tmp.dirSync(); + + const getCachedNamespacedLevels = (): Record => { + // @ts-expect-error private + return logger.cachedNamespacedLevels; + }; beforeAll(() => { + // @ts-expect-error private consoleWriteSpy = jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); }); @@ -21,11 +31,8 @@ describe('Logger', () => { beforeEach(async () => { data.writeDefaultConfiguration(); - jest.resetModules(); - settings = require('../lib/util/settings'); - settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); settings.reRead(); - logger = require('../lib/util/logger').default; + settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); logger.init(); consoleWriteSpy.mockClear(); }); @@ -81,7 +88,7 @@ describe('Logger', () => { it('Add/remove transport', () => { class DummyTransport extends Transport { - log(info, callback) {} + log(): void {} } expect(logger.winston.transports.length).toBe(2); @@ -93,6 +100,7 @@ describe('Logger', () => { }); it('Logger should be console and file by default', () => { + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.length).toBe(2); expect(pipes[0].constructor.name).toBe('Console'); @@ -104,6 +112,7 @@ describe('Logger', () => { it('Logger can be file only', () => { settings.set(['advanced', 'log_output'], ['file']); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.length).toBe(2); expect(pipes[0].constructor.name).toBe('Console'); @@ -115,6 +124,7 @@ describe('Logger', () => { it('Logger can be console only', () => { settings.set(['advanced', 'log_output'], ['console']); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.constructor.name).toBe('Console'); expect(pipes.silent).toBe(false); @@ -123,6 +133,7 @@ describe('Logger', () => { it('Logger can be nothing', () => { settings.set(['advanced', 'log_output'], []); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.constructor.name).toBe('Console'); expect(pipes.silent).toBe(true); @@ -131,6 +142,7 @@ describe('Logger', () => { it('Should allow to disable log rotation', () => { settings.set(['advanced', 'log_rotation'], false); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes[1].constructor.name).toBe('File'); expect(pipes[1].maxFiles).toBeNull(); @@ -139,11 +151,17 @@ 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(); + try { + settings.set(['advanced', 'log_symlink_current'], true); + logger.init(); + expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy(); + } catch (error) { + if (platform() !== 'win32' || !(error as Error).message.startsWith('EPERM')) { + throw error; + } - jest.resetModules(); + // ignore 'operation not permitted' failure on Windows + } }); it.each([ @@ -158,20 +176,25 @@ describe('Logger', () => { consoleWriteSpy.mockClear(); let i = 1; + // @ts-expect-error dynamic logger[level]('msg'); expect(logSpy).toHaveBeenLastCalledWith(level, 'z2m: msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[level]('msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(level, 'abcd: msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[level](() => 'func msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(level, 'abcd: func msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); for (const higherLevel of otherLevels.higher) { + // @ts-expect-error dynamic logger[higherLevel]('higher msg'); expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'z2m: higher msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[higherLevel]('higher msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'abcd: higher msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); @@ -181,23 +204,17 @@ describe('Logger', () => { consoleWriteSpy.mockClear(); for (const lowerLevel of otherLevels.lower) { + // @ts-expect-error dynamic logger[lowerLevel]('lower msg'); expect(logSpy).not.toHaveBeenCalled(); expect(consoleWriteSpy).not.toHaveBeenCalled(); + // @ts-expect-error dynamic logger[lowerLevel]('lower msg', 'abcd'); expect(logSpy).not.toHaveBeenCalled(); expect(consoleWriteSpy).not.toHaveBeenCalled(); } }); - it('Logs Error object', () => { - const logSpy = jest.spyOn(logger.winston, 'log'); - - 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)', @@ -243,6 +260,7 @@ describe('Logger', () => { logger.setLevel('debug'); const logSpy = jest.spyOn(logger.winston, 'log'); logger.setDebugNamespaceIgnore(ignore); + // @ts-expect-error private expect(logger.debugNamespaceIgnoreRegex).toStrictEqual(expected); expect(logger.getDebugNamespaceIgnore()).toStrictEqual(ignore); @@ -308,52 +326,53 @@ describe('Logger', () => { it('Logs with namespaced levels hierarchy', () => { const nsLevels = {'zh:zstack': 'debug', 'zh:zstack:unpi:writer': 'error'}; - let cachedNSLevels = Object.assign({}, nsLevels); + let cachedNSLevels; + cachedNSLevels = Object.assign({}, nsLevels); logger.setNamespacedLevels(nsLevels); logger.setLevel('warning'); consoleWriteSpy.mockClear(); logger.debug(`--- parseNext [] debug picked from hierarchy`, 'zh:zstack:unpi:parser'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:parser': 'debug'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:parser': 'debug'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.warning(`--> frame [36,15] warning explicitely supressed`, 'zh:zstack:unpi:writer'); - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels); + expect(getCachedNamespacedLevels()).toStrictEqual(cachedNSLevels); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.warning(`Another supressed warning message in a sub namespace`, 'zh:zstack:unpi:writer:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.error(`but error should go through`, 'zh:zstack:unpi:writer:another:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(2); logger.warning(`new unconfigured namespace warning`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(3); logger.info(`cached unconfigured namespace info should be supressed`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels); + expect(getCachedNamespacedLevels()).toStrictEqual(cachedNSLevels); expect(consoleWriteSpy).toHaveBeenCalledTimes(3); logger.setLevel('info'); - expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = Object.assign({}, nsLevels))); + expect(getCachedNamespacedLevels()).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(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(4); logger.error(`configured namespace hierachy should still work after the cache reset`, 'zh:zstack:unpi:writer:another:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).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'})); + expect(getCachedNamespacedLevels()).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(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(6); logger.debug(`debug suppressed`, 'zh:zstack:unpi'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(6); logger.warning(`warning logged`, 'zh:zstack'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(7); logger.info(`unconfigured namespace`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(8); }); @@ -363,13 +382,13 @@ describe('Logger', () => { const logSpy = jest.spyOn(logger.winston, 'log'); consoleWriteSpy.mockClear(); - let net_map = '%d'; - logger.debug(net_map, 'z2m:mqtt'); - expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:mqtt: ${net_map}`); - expect(consoleWriteSpy.mock.calls[0][0]).toMatch(new RegExp(`^.*\tz2m:mqtt: ${net_map}`)); - net_map = 'anything %s goes here'; - logger.debug(net_map, 'z2m:test'); - expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:test: ${net_map}`); - expect(consoleWriteSpy.mock.calls[1][0]).toMatch(new RegExp(`^.*\tz2m:test: ${net_map}`)); + let splatChars = '%d'; + logger.debug(splatChars, 'z2m:mqtt'); + expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:mqtt: ${splatChars}`); + expect(consoleWriteSpy.mock.calls[0][0]).toMatch(new RegExp(`^.*\tz2m:mqtt: ${splatChars}`)); + splatChars = 'anything %s goes here'; + logger.debug(splatChars, 'z2m:test'); + expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:test: ${splatChars}`); + expect(consoleWriteSpy.mock.calls[1][0]).toMatch(new RegExp(`^.*\tz2m:test: ${splatChars}`)); }); }); diff --git a/test/stub/data.js b/test/mocks/data.ts similarity index 91% rename from test/stub/data.js rename to test/mocks/data.ts index 8e00f0d2eb..2fc10cc4db 100644 --- a/test/stub/data.js +++ b/test/mocks/data.ts @@ -1,13 +1,15 @@ -const tmp = require('tmp'); -const yaml = require('../../lib/util/yaml').default; -const path = require('path'); -const fs = require('fs'); -const stringify = require('json-stable-stringify-without-jsonify'); +import fs from 'fs'; +import path from 'path'; -const mockDir = tmp.dirSync().name; +import stringify from 'json-stable-stringify-without-jsonify'; +import tmp from 'tmp'; + +import yaml from '../../lib/util/yaml'; + +export const mockDir: string = tmp.dirSync().name; const stateFile = path.join(mockDir, 'state.json'); -function writeDefaultConfiguration() { +export function writeDefaultConfiguration(): void { const config = { homeassistant: false, mqtt: { @@ -244,17 +246,17 @@ function writeDefaultConfiguration() { yaml.writeIfChanged(path.join(mockDir, 'configuration.yaml'), config); } -function writeEmptyState() { +export function writeEmptyState(): void { fs.writeFileSync(stateFile, stringify({})); } -function removeState() { +export function removeState(): void { if (stateExists()) { fs.unlinkSync(stateFile); } } -function stateExists() { +export function stateExists(): boolean { return fs.existsSync(stateFile); } @@ -273,29 +275,22 @@ const defaultState = { }, }; -function getDefaultState() { +export function getDefaultState(): typeof defaultState { return defaultState; } -function writeDefaultState() { +export function writeDefaultState(): void { fs.writeFileSync(path.join(mockDir, 'state.json'), stringify(defaultState)); } +export function read(): ReturnType { + return yaml.read(path.join(mockDir, 'configuration.yaml')); +} + jest.mock('../../lib/util/data', () => ({ - joinPath: (file) => require('path').join(mockDir, file), - getPath: () => mockDir, + joinPath: (file: string): string => jest.requireActual('path').join(mockDir, file), + getPath: (): string => mockDir, })); writeDefaultConfiguration(); writeDefaultState(); - -module.exports = { - mockDir, - read: () => yaml.read(path.join(mockDir, 'configuration.yaml')), - writeDefaultConfiguration, - writeDefaultState, - removeState, - writeEmptyState, - stateExists, - getDefaultState, -}; diff --git a/test/mocks/debounce.ts b/test/mocks/debounce.ts new file mode 100644 index 0000000000..25c19be50b --- /dev/null +++ b/test/mocks/debounce.ts @@ -0,0 +1,3 @@ +export const mockDebounce = jest.fn((fn) => fn); + +jest.mock('debounce', () => mockDebounce); diff --git a/test/mocks/jszip.ts b/test/mocks/jszip.ts new file mode 100644 index 0000000000..835418ec1c --- /dev/null +++ b/test/mocks/jszip.ts @@ -0,0 +1,11 @@ +export const mockJSZipFile = jest.fn(); +export const mockJSZipGenerateAsync = jest.fn().mockReturnValue('THISISBASE64'); + +jest.mock('jszip', () => + jest.fn().mockImplementation(() => { + return { + file: mockJSZipFile, + generateAsync: mockJSZipGenerateAsync, + }; + }), +); diff --git a/test/mocks/logger.ts b/test/mocks/logger.ts new file mode 100644 index 0000000000..4f6f21461a --- /dev/null +++ b/test/mocks/logger.ts @@ -0,0 +1,53 @@ +import type {LogLevel} from 'lib/util/settings'; +import type Transport from 'winston-transport'; + +let level = 'info'; +let debugNamespaceIgnore: string = ''; +let namespacedLevels: Record = {}; +let transports: Transport[] = []; +let transportsEnabled: boolean = false; +const getMessage = (messageOrLambda: string | (() => string)): string => (messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda); + +export const mockLogger = { + log: jest.fn().mockImplementation((level, message, namespace = 'z2m') => { + if (transportsEnabled) { + for (const transport of transports) { + transport.log!({level, message, namespace}, () => {}); + } + } + }), + init: jest.fn(), + info: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('info', getMessage(messageOrLambda), namespace)), + warning: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('warning', getMessage(messageOrLambda), namespace)), + error: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('error', getMessage(messageOrLambda), namespace)), + debug: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('debug', getMessage(messageOrLambda), namespace)), + cleanup: jest.fn(), + logOutput: jest.fn(), + add: (transport: Transport): void => { + transports.push(transport); + }, + addTransport: (transport: Transport): void => { + transports.push(transport); + }, + removeTransport: (transport: Transport): void => { + transports = transports.filter((t) => t !== transport); + }, + setLevel: (newLevel: LogLevel): void => { + level = newLevel; + }, + getLevel: (): LogLevel => level, + setNamespacedLevels: (nsLevels: Record): void => { + namespacedLevels = nsLevels; + }, + getNamespacedLevels: (): Record => namespacedLevels, + setDebugNamespaceIgnore: (newIgnore: string): void => { + debugNamespaceIgnore = newIgnore; + }, + getDebugNamespaceIgnore: (): string => debugNamespaceIgnore, + setTransportsEnabled: (value: boolean): void => { + transportsEnabled = value; + }, + end: jest.fn(), +}; + +jest.mock('../../lib/util/logger', () => mockLogger); diff --git a/test/stub/mqtt.js b/test/mocks/mqtt.ts similarity index 50% rename from test/stub/mqtt.js rename to test/mocks/mqtt.ts index c3287b89a3..617f942ee8 100644 --- a/test/stub/mqtt.js +++ b/test/mocks/mqtt.ts @@ -1,37 +1,24 @@ -const events = {}; +import {EventHandler} from './utils'; -const mock = { +export const events: Record = {}; + +export const mockMQTT = { publish: jest.fn().mockImplementation((topic, payload, options, cb) => cb()), end: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), reconnecting: false, - on: jest.fn(), - stream: {setMaxListeners: jest.fn()}, -}; - -const mockConnect = jest.fn().mockReturnValue(mock); - -jest.mock('mqtt', () => { - return {connect: mockConnect}; -}); - -const restoreOnMock = () => { - mock.on.mockImplementation((type, handler) => { + on: jest.fn((type, handler) => { if (type === 'connect') { handler(); } events[type] = handler; - }); + }), + stream: {setMaxListeners: jest.fn()}, }; +export const mockMQTTConnect = jest.fn().mockReturnValue(mockMQTT); -restoreOnMock(); - -module.exports = { - events, - ...mock, - connect: mockConnect, - mock, - restoreOnMock, -}; +jest.mock('mqtt', () => { + return {connect: mockMQTTConnect}; +}); diff --git a/test/mocks/sleep.ts b/test/mocks/sleep.ts new file mode 100644 index 0000000000..3484c2808f --- /dev/null +++ b/test/mocks/sleep.ts @@ -0,0 +1,11 @@ +import utils from '../../lib/util/utils'; + +const spy = jest.spyOn(utils, 'sleep'); + +export function mock(): void { + spy.mockImplementation(); +} + +export function restore(): void { + spy.mockRestore(); +} diff --git a/test/mocks/types.d.ts b/test/mocks/types.d.ts new file mode 100644 index 0000000000..50a8b627cd --- /dev/null +++ b/test/mocks/types.d.ts @@ -0,0 +1,15 @@ +declare module 'json-stable-stringify-without-jsonify' { + export default function (obj: unknown): string; +} + +declare module 'tmp' { + export function dirSync(): { + name: string; + removeCallback: (err: Error | undefined, name: string, fd: number, cleanupFn: () => void) => void; + }; + export function fileSync(): { + name: string; + fd: number; + removeCallback: (err: Error | undefined, name: string, fd: number, cleanupFn: () => void) => void; + }; +} diff --git a/test/mocks/utils.ts b/test/mocks/utils.ts new file mode 100644 index 0000000000..95bee98d65 --- /dev/null +++ b/test/mocks/utils.ts @@ -0,0 +1,15 @@ +export type EventHandler = (...args: unknown[]) => unknown; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type JestMockAny = jest.Mock; + +export function flushPromises(): Promise { + return new Promise(jest.requireActual('timers').setImmediate); +} + +// https://github.com/jestjs/jest/issues/6028#issuecomment-567669082 +export function defuseRejection(promise: Promise): Promise { + promise.catch(() => {}); + + return promise; +} diff --git a/test/stub/zigbeeHerdsman.js b/test/mocks/zigbeeHerdsman.ts similarity index 62% rename from test/stub/zigbeeHerdsman.js rename to test/mocks/zigbeeHerdsman.ts index 75a12437ca..377dcf920f 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/mocks/zigbeeHerdsman.ts @@ -1,40 +1,43 @@ -const events = {}; -const assert = require('assert'); +import assert from 'assert'; -function getKeyByValue(object, value, fallback) { - const key = Object.keys(object).find((k) => object[k] === value); - return key != null ? key : fallback; -} +import {Zcl} from 'zigbee-herdsman'; +import {CoordinatorVersion, DeviceType, NetworkParameters, StartResult} from 'zigbee-herdsman/dist/adapter/tstype'; -class Group { - constructor(groupID, members) { - this.groupID = groupID; - this.command = jest.fn(); - this.meta = {}; - this.members = members; - this.removeFromDatabase = jest.fn(); - this.removeFromNetwork = jest.fn(); - this.hasMember = (endpoint) => this.members.includes(endpoint); - } -} +import {EventHandler, JestMockAny} from './utils'; + +type ZHConfiguredReporting = { + cluster: {name: string}; + attribute: {name: string | undefined; ID?: number}; + minimumReportInterval: number; + maximumReportInterval: number; + reportableChange: number; +}; +type ZHEndpointCluster = { + ID?: number; + name: string; +}; +type ZHBind = { + target: Endpoint | Group; + cluster: ZHEndpointCluster; +}; -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, +const CLUSTERS = { + genBasic: Zcl.Clusters.genBasic.ID, + genOta: Zcl.Clusters.genOta.ID, + genScenes: Zcl.Clusters.genScenes.ID, + genOnOff: Zcl.Clusters.genOnOff.ID, + genLevelCtrl: Zcl.Clusters.genLevelCtrl.ID, + lightingColorCtrl: Zcl.Clusters.lightingColorCtrl.ID, + closuresWindowCovering: Zcl.Clusters.closuresWindowCovering.ID, + hvacThermostat: Zcl.Clusters.hvacThermostat.ID, + msIlluminanceMeasurement: Zcl.Clusters.msIlluminanceMeasurement.ID, + msTemperatureMeasurement: Zcl.Clusters.msTemperatureMeasurement.ID, + msRelativeHumidity: Zcl.Clusters.msRelativeHumidity.ID, + msSoilMoisture: Zcl.Clusters.msSoilMoisture.ID, + msCO2: Zcl.Clusters.msCO2.ID, }; -const custom_clusters = { +export const CUSTOM_CLUSTERS = { custom_1: { ID: 64672, manufacturerCode: 4617, @@ -48,7 +51,7 @@ const custom_clusters = { }, }; -const customClusterBTHRA = { +const CUSTOM_CLUSTER_BTHRA = { custom_1: { ID: 513, attributes: { @@ -75,18 +78,50 @@ const customClusterBTHRA = { }, }; -class Endpoint { +function getClusterKey(value: unknown): string | undefined { + for (const key in CLUSTERS) { + if (CLUSTERS[key as keyof typeof CLUSTERS] === value) { + return key; + } + } + + return undefined; +} + +export class Endpoint { + deviceIeeeAddress: string; + clusterValues: Record>; + ID: number; + inputClusters: number[]; + outputClusters: number[]; + command: JestMockAny; + commandResponse: JestMockAny; + read: JestMockAny; + write: JestMockAny; + bind: JestMockAny; + unbind: JestMockAny; + save: JestMockAny; + configureReporting: JestMockAny; + meta: Record; + binds: ZHBind[]; + profileID: number | undefined; + deviceID: number | undefined; + configuredReportings: ZHConfiguredReporting[]; + addToGroup: JestMockAny; + removeFromGroup: JestMockAny; + getClusterAttributeValue: JestMockAny; + constructor( - ID, - inputClusters, - outputClusters, - deviceIeeeAddress, - binds = [], - clusterValues = {}, - configuredReportings = [], - profileID = null, - deviceID = null, - meta = {}, + ID: number, + inputClusters: number[], + outputClusters: number[], + deviceIeeeAddress: string, + binds: ZHBind[] = [], + clusterValues: Record> = {}, + configuredReportings: ZHConfiguredReporting[] = [], + profileID: number | undefined = undefined, + deviceID: number | undefined = undefined, + meta: Record = {}, ) { this.deviceIeeeAddress = deviceIeeeAddress; this.clusterValues = clusterValues; @@ -106,73 +141,111 @@ 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.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); - }; - - this.removeFromGroup = jest.fn(); - this.removeFromGroup.mockImplementation((group) => { + this.addToGroup = jest.fn((group: Group) => { + if (!group.members.includes(this)) { + group.members.push(this); + } + }); + this.removeFromGroup = jest.fn((group: Group) => { const index = group.members.indexOf(this); if (index != -1) { group.members.splice(index, 1); } }); - this.removeFromAllGroups = () => { - Object.values(groups).forEach((g) => this.removeFromGroup(g)); - }; + this.getClusterAttributeValue = jest.fn((cluster: string, value: string) => + !(cluster in this.clusterValues) ? undefined : this.clusterValues[cluster][value], + ); + } - this.getClusterAttributeValue = jest.fn(); - this.getClusterAttributeValue.mockImplementation((cluster, value) => { - if (!(cluster in this.clusterValues)) return undefined; - return this.clusterValues[cluster][value]; - }); + getInputClusters(): ZHEndpointCluster[] { + const clusters: ZHEndpointCluster[] = []; + + for (const clusterId of this.inputClusters) { + const name = getClusterKey(clusterId); + + if (name) { + clusters.push({ID: clusterId, name}); + } + } + + return clusters; + } + + getOutputClusters(): ZHEndpointCluster[] { + const clusters: ZHEndpointCluster[] = []; + + for (const clusterId of this.outputClusters) { + const name = getClusterKey(clusterId); + + if (name) { + clusters.push({ID: clusterId, name}); + } + } + + return clusters; + } + + supportsInputCluster(cluster: keyof typeof CLUSTERS): boolean { + assert(CLUSTERS[cluster] !== undefined, `Undefined '${cluster}'`); + return this.inputClusters.includes(CLUSTERS[cluster]); + } + + supportsOutputCluster(cluster: keyof typeof CLUSTERS): boolean { + assert(CLUSTERS[cluster], `Undefined '${cluster}'`); + return this.outputClusters.includes(CLUSTERS[cluster]); + } + + getDevice(): Device | undefined { + return Object.values(devices).find((d) => d.ieeeAddr === this.deviceIeeeAddress); + } + + removeFromAllGroups(): void { + Object.values(groups).forEach((g) => this.removeFromGroup(g)); } } -class Device { +export class Device { + type: string; + ieeeAddr: string; + dateCode: string | undefined; + networkAddress: number; + manufacturerID: number; + endpoints: Endpoint[]; + powerSource: string | undefined; + softwareBuildID: string | undefined; + interviewCompleted: boolean; + modelID: string | undefined; + interview: JestMockAny; + interviewing: boolean; + meta: Record; + ping: JestMockAny; + removeFromNetwork: JestMockAny; + removeFromDatabase: JestMockAny; + customClusters: Record; + addCustomCluster: JestMockAny; + save: JestMockAny; + manufacturerName: string | undefined; + lastSeen: number | undefined; + isDeleted: boolean; + linkquality?: number; + lqi: JestMockAny; + routingTable: JestMockAny; + constructor( - type, - ieeeAddr, - networkAddress, - manufacturerID, - endpoints, - interviewCompleted, - powerSource = null, - modelID = null, - interviewing = false, - manufacturerName, - dateCode = null, - softwareBuildID = null, + type: string, + ieeeAddr: string, + networkAddress: number, + manufacturerID: number, + endpoints: Endpoint[], + interviewCompleted: boolean, + powerSource: string | undefined = undefined, + modelID: string | undefined = undefined, + interviewing: boolean = false, + manufacturerName: string | undefined = undefined, + dateCode: string | undefined = undefined, + softwareBuildID: string | undefined = undefined, customClusters = {}, ) { this.type = type; @@ -196,14 +269,40 @@ class Device { this.save = jest.fn(); this.manufacturerName = manufacturerName; this.lastSeen = 1000; + this.isDeleted = false; + this.lqi = jest.fn(() => ({neighbors: []})); + this.routingTable = jest.fn(() => ({table: []})); } - getEndpoint(ID) { + getEndpoint(ID: number): Endpoint | undefined { return this.endpoints.find((e) => e.ID === ID); } } -const returnDevices = []; +export class Group { + groupID: number; + command: JestMockAny; + meta: Record; + members: Endpoint[]; + removeFromDatabase: JestMockAny; + removeFromNetwork: JestMockAny; + + constructor(groupID: number, members: Endpoint[]) { + this.groupID = groupID; + this.command = jest.fn(); + this.meta = {}; + this.members = members; + this.removeFromDatabase = jest.fn(); + this.removeFromNetwork = jest.fn(); + } + + hasMember(endpoint: Endpoint): boolean { + return this.members.includes(endpoint); + } +} + +export const events: Record = {}; +export const returnDevices: string[] = []; const bulb_color = new Device( 'Router', @@ -233,8 +332,8 @@ const bulb_color_2 = new Device( [], {lightingColorCtrl: {colorCapabilities: 254}}, [], - null, - null, + undefined, + undefined, {scenes: {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}}, ), ], @@ -350,7 +449,7 @@ const zigfred_plus = new Device( 'Siglis', ); -const groups = { +export const groups = { 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, []), @@ -362,7 +461,7 @@ const groups = { ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]), }; -const devices = { +export const devices = { coordinator: new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false), bulb: new Device( 'Router', @@ -405,7 +504,7 @@ const devices = { 'BOSCH', '20231122', '3.05.09', - customClusterBTHRA, + CUSTOM_CLUSTER_BTHRA, ), bulb_color: bulb_color, bulb_2: bulb_2, @@ -423,7 +522,7 @@ const devices = { {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]), + new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6], '0x0017880104e45517'), ], true, 'Battery', @@ -434,7 +533,7 @@ const devices = { '0x0017880104e45518', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45518')], true, 'Battery', 'notSupportedModelID', @@ -446,7 +545,7 @@ const devices = { '0x0017880104e45529', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45529')], true, 'Battery', 'notSupportedModelID', @@ -456,7 +555,7 @@ const devices = { '0x0017880104e45530', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45530')], true, 'Battery', undefined, @@ -467,7 +566,7 @@ const devices = { '0x0017880104e45519', 6537, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45519')], true, 'Battery', 'lumi.sensor_switch.aq2', @@ -497,23 +596,59 @@ const devices = { '0x0017880104e45521', 6538, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45521'), new Endpoint(2, [0], [], '0x0017880104e45521')], true, 'Battery', 'lumi.sensor_86sw2.es1', ), - WSDCGQ11LM: new Device('EndDevice', '0x0017880104e45522', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), + WSDCGQ11LM: new Device( + 'EndDevice', + '0x0017880104e45522', + 6539, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45522')], + true, + 'Battery', + 'lumi.weather', + ), // This are not a real spammer device, just copy of previous to test the throttle filter - SPAMMER: new Device('EndDevice', '0x0017880104e455fe', 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'), + SPAMMER: new Device( + 'EndDevice', + '0x0017880104e455fe', + 6539, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e455fe')], + true, + 'Battery', + 'lumi.weather', + ), + RTCGQ11LM: new Device( + 'EndDevice', + '0x0017880104e45523', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45523')], + 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'), + E1743: new Device( + 'Router', + '0x0017880104e45540', + 6540, + 4476, + [new Endpoint(1, [0], [], '0x0017880104e45540')], + 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], [])], + [new Endpoint(1, [0], [25], '0x0017880104e45541'), new Endpoint(2, [0, 6], [], '0x0017880104e45541')], true, 'Mains (single phase)', 'lumi.ctrl_neutral1', @@ -534,7 +669,11 @@ const devices = { '0x0017880104e45544', 6540, 4151, - [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], + [ + new Endpoint(11, [0], [], '0x0017880104e45544'), + new Endpoint(13, [0], [], '0x0017880104e45544'), + new Endpoint(12, [0], [], '0x0017880104e45544'), + ], true, 'Mains (single phase)', 'GL-C-008', @@ -555,7 +694,7 @@ const devices = { '0x0017880104e45547', 6540, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45547'), new Endpoint(2, [0], [], '0x0017880104e45547')], true, 'Mains (single phase)', 'lumi.curtain', @@ -565,22 +704,67 @@ const devices = { '0x0017880104e45548', 6540, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45548'), new Endpoint(2, [0], [], '0x0017880104e45548')], 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'), + HS2WD: new Device( + 'Router', + '0x0017880104e45549', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45549')], + true, + 'Mains (single phase)', + 'WarningDevice', + ), + '1TST_EU': new Device( + 'Router', + '0x0017880104e45550', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45550')], + true, + 'Mains (single phase)', + 'Thermostat', + ), + SV01: new Device( + 'Router', + '0x0017880104e45551', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45551')], + true, + 'Mains (single phase)', + 'SV01-410-MP-1.0', + ), + J1: new Device( + 'Router', + '0x0017880104e45552', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45552')], + true, + 'Mains (single phase)', + 'J1 (5502)', + ), + E11_G13: new Device( + 'EndDevice', + '0x0017880104e45553', + 6540, + 4151, + [new Endpoint(1, [0, 6], [], '0x0017880104e45553')], + true, + 'Mains (single phase)', + 'E11-G13', + ), nomodel: new Device( 'Router', '0x0017880104e45535', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45535')], true, 'Mains (single phase)', undefined, @@ -591,15 +775,33 @@ const devices = { '0x0017880104e45525', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45525')], 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 '), + CC2530_ROUTER: new Device( + 'Router', + '0x0017880104e45559', + 6540, + 4151, + [new Endpoint(1, [0, 6], [], '0x0017880104e45559')], + true, + 'Mains (single phase)', + 'lumi.router', + ), + LIVOLO: new Device( + 'Router', + '0x0017880104e45560', + 6541, + 4152, + [new Endpoint(6, [0, 6], [], '0x0017880104e45560')], + true, + 'Mains (single phase)', + 'TI0001 ', + ), tradfri_remote: new Device( 'EndDevice', '0x90fd9ffffe4b64ae', @@ -712,7 +914,7 @@ const devices = { false, 'Centralite', ), - J1: new Device( + J1_cover: new Device( 'Router', '0x0017880104a44559', 6543, @@ -729,10 +931,10 @@ const devices = { 'EndDevice', '0x0017880104e45511', 1114, - 'external', + 0xffff, [new Endpoint(1, [], [], '0x0017880104e45511')], false, - null, + undefined, 'external_converter_device', ), QS_Zigbee_D02_TRIAC_2C_LN: new Device( @@ -753,7 +955,7 @@ const devices = { '0x0017880104e45561', 6544, 4151, - [new Endpoint(1, [0, 3, 4, 1026], [])], + [new Endpoint(1, [0, 3, 4, 1026], [], '0x0017880104e45561')], true, 'Battery', 'temperature.sensor', @@ -763,7 +965,7 @@ const devices = { '0x0017880104e45562', 6545, 4151, - [new Endpoint(1, [0, 3, 4, 513], [1026])], + [new Endpoint(1, [0, 3, 4, 513], [1026], '0x0017880104e45562')], true, 'Mains (single phase)', 'heating.actuator', @@ -819,10 +1021,10 @@ const devices = { 'Mains (single phase)', 'RBSH-MMS-ZB-EU', false, - null, - null, - null, - custom_clusters, + undefined, + undefined, + undefined, + CUSTOM_CLUSTERS, ), bulb_custom_cluster: new Device( 'Router', @@ -834,89 +1036,75 @@ const devices = { 'Mains (single phase)', 'TRADFRI bulb E27 WS opal 980lm', false, - null, - null, - null, - custom_clusters, + undefined, + undefined, + undefined, + CUSTOM_CLUSTERS, ), }; -const mock = { - setTransmitPower: jest.fn(), +export const mockController = { + on: (type: string, handler: EventHandler): void => { + events[type] = handler; + }, + start: jest.fn((): Promise => Promise.resolve('reset')), + stop: jest.fn(), + touchlinkIdentify: jest.fn(), + touchlinkScan: jest.fn(), touchlinkFactoryReset: jest.fn(), touchlinkFactoryResetFirst: jest.fn(), - touchlinkScan: jest.fn(), - touchlinkIdentify: jest.fn(), - start: jest.fn(), + addInstallCode: jest.fn(), + permitJoin: jest.fn(), + getPermitJoinTimeout: jest.fn((): number => 0), + isStopping: jest.fn((): boolean => false), backup: jest.fn(), coordinatorCheck: jest.fn(), - isStopping: jest.fn(), - permitJoin: jest.fn(), - addInstallCode: jest.fn(), - getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1, revision: 20190425}}), - getNetworkParameters: jest.fn().mockReturnValue({panID: 0x162a, extendedPanID: [0, 11, 22], channel: 15}), - on: (type, handler) => { - events[type] = handler; - }, - stop: jest.fn(), - getDevicesIterator: jest.fn().mockImplementation(function* (predicate) { + getCoordinatorVersion: jest.fn((): Promise => Promise.resolve({type: 'z-Stack', meta: {version: 1, revision: 20190425}})), + getNetworkParameters: jest.fn((): Promise => Promise.resolve({panID: 0x162a, extendedPanID: 0x001122, channel: 15})), + getDevices: jest.fn((): Device[] => []), + getDevicesIterator: jest.fn(function* (predicate?: (value: Device) => boolean): Generator { for (const key in devices) { - const device = devices[key]; + const device = devices[key as keyof typeof devices]; if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && !device.isDeleted && (!predicate || predicate(device))) { yield device; } } }), - getDevicesByType: jest.fn().mockImplementation((type) => { - return Object.values(devices) + getDevicesByType: jest.fn((type: DeviceType): Device[] => + 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) => d.type === type), + ), + getDeviceByIeeeAddr: jest.fn((ieeeAddr: string): Device | undefined => + 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) + .find((d) => d.ieeeAddr === ieeeAddr), + ), + getDeviceByNetworkAddress: jest.fn((networkAddress: number): Device | undefined => + Object.values(devices) .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) - .find((d) => d.networkAddress === networkAddress); - }), - getGroupsIterator: jest.fn().mockImplementation(function* (predicate) { + .find((d) => d.networkAddress === networkAddress), + ), + getGroupByID: jest.fn((groupID: number): Group | undefined => Object.values(groups).find((g) => g.groupID === groupID)), + getGroups: jest.fn((): Group[] => []), + getGroupsIterator: jest.fn(function* (predicate?: (value: Group) => boolean): Generator { for (const key in groups) { - const group = groups[key]; + const group = groups[key as keyof typeof groups]; if (!predicate || predicate(group)) { yield group; } } }), - getGroupByID: jest.fn().mockImplementation((groupID) => { - return Object.values(groups).find((d) => d.groupID === groupID); - }), - getPermitJoinTimeout: jest.fn().mockReturnValue(0), - reset: jest.fn(), - createGroup: jest.fn().mockImplementation((groupID) => { + createGroup: jest.fn((groupID: number): Group => { const group = new Group(groupID, []); - groups[`group_${groupID}`] = group; + groups[`group_${groupID}` as keyof typeof groups] = group; return group; }), }; -const mockConstructor = jest.fn().mockImplementation(() => mock); - jest.mock('zigbee-herdsman', () => ({ ...jest.requireActual('zigbee-herdsman'), - Controller: mockConstructor, + Controller: jest.fn().mockImplementation(() => mockController), })); - -module.exports = { - events, - ...mock, - constructor: mockConstructor, - devices, - groups, - returnDevices, - custom_clusters, -}; diff --git a/test/onEvent.test.js b/test/onEvent.test.js deleted file mode 100644 index 66385f9797..0000000000 --- a/test/onEvent.test.js +++ /dev/null @@ -1,80 +0,0 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45560'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const mockOnEvent = jest.fn(); -const mappedLivolo = zigbeeHerdsmanConverters.findByModel('TI0001'); -mappedLivolo.onEvent = mockOnEvent; -zigbeeHerdsmanConverters.onEvent = jest.fn(); - -describe('On event', () => { - let controller; - const device = zigbeeHerdsman.devices.LIVOLO; - - beforeEach(async () => { - jest.useFakeTimers(); - data.writeDefaultConfiguration(); - settings.reRead(); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - await flushPromises(); - }); - - beforeEach(async () => { - controller.state.state = {}; - data.writeDefaultConfiguration(); - settings.reRead(); - mocksClear.forEach((m) => m.mockClear()); - zigbeeHerdsmanConverters.onEvent.mockClear(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - 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[2]).toBe(device); - expect(call[3]).toStrictEqual(settings.getDevice(device.ieeeAddr)); - expect(call[4]).toStrictEqual({}); - }); - - it('Should call with stop event', async () => { - mockOnEvent.mockClear(); - await controller.stop(); - await flushPromises(); - expect(mockOnEvent).toHaveBeenCalledTimes(1); - const call = mockOnEvent.mock.calls[0]; - expect(call[0]).toBe('stop'); - expect(call[1]).toStrictEqual({}); - expect(call[2]).toBe(device); - }); - - it('Should call with zigbee event', async () => { - mockOnEvent.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device}); - await flushPromises(); - expect(mockOnEvent).toHaveBeenCalledTimes(1); - expect(mockOnEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, settings.getDevice(device.ieeeAddr), {}); - }); - - it('Should call index onEvent with zigbee event', async () => { - zigbeeHerdsmanConverters.onEvent.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device}); - await flushPromises(); - expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device); - }); -}); diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js deleted file mode 100644 index 812d555f24..0000000000 --- a/test/otaUpdate.test.js +++ /dev/null @@ -1,459 +0,0 @@ -const path = require('path'); - -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const sleep = require('./stub/sleep'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const stringify = require('json-stable-stringify-without-jsonify'); -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(); - settings.reRead(); - data.writeDefaultConfiguration(); - settings.set(['ota', 'ikea_ota_use_test_url'], true); - settings.reRead(); - jest.useFakeTimers(); - controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); - await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); - }); - - afterEach(async () => { - settings.set(['ota', 'disable_automatic_update_check'], false); - jest.runOnlyPendingTimers(); - }); - - afterAll(async () => { - jest.useRealTimers(); - sleep.restore(); - }); - - beforeEach(async () => { - const extension = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate'); - extension.lastChecked = {}; - extension.inProgress = new Set(); - controller.state.state = {}; - MQTT.publish.mockClear(); - }); - - it('Should OTA update a device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - let count = 0; - endpoint.read.mockImplementation(() => { - count++; - return {swBuildId: count, dateCode: '2019010' + count}; - }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.info.mockClear(); - device.save.mockClear(); - mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { - onUpdate(0, null); - onUpdate(10, 3600.2123); - return 90; - }); - - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); - await flushPromises(); - expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, expect.any(Function)); - 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(device.save).toHaveBeenCalledTimes(2); - 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: {state: 'updating', progress: 0}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'updating', progress: 10, remaining: 3600}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({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)); - }); - - 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); - 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'})); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({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), - ); - }); - - it('Should be able to check if OTA update is available', async () => { - const device = zigbeeHerdsman.devices.bulb; - 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'); - 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', update_available: 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'); - 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', update_available: 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); - mockClear(mapped); - 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'); - 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), - ); - }); - - it('Should fail when device does not exist', async () => { - 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), - ); - }); - - 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'); - 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), - ); - }); - - it('Should refuse to check/update when already in progress', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), 99999); - }); - }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - 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), - ); - }); - - 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!'); - }); - - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - 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), - ); - }); - - it('Should check for update when device requests it', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - 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}, - }; - 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(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); - - // Should not request again when device asks again after a short time - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - - logger.info.mockClear(); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - 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}, - }; - 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(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'idle'}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should check for update when device requests it and it is not available', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - 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}, - }; - 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(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should not check for update when device requests it and disable_automatic_update_check is set to true', async () => { - settings.set(['ota', 'disable_automatic_update_check'], true); - const device = zigbeeHerdsman.devices.bulb; - const data = {imageType: 12382}; - 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}, - }; - logger.info.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); - }); - - 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}, - }; - 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); - }); - - 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}, - }; - 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); - }); - - it('Set zigbee_ota_override_index_location', async () => { - settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json'); - 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'); - spyUseIndexOverride.mockClear(); - }); - - 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'}}); - await resetExtension(); - expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}}); - }); -}); diff --git a/test/settings.test.js b/test/settings.test.ts similarity index 95% rename from test/settings.test.js rename to test/settings.test.ts index 1cdf9e3e32..46c2f884dd 100644 --- a/test/settings.test.js +++ b/test/settings.test.ts @@ -1,17 +1,19 @@ -require('./stub/logger'); -require('./stub/data'); -const data = require('../lib/util/data'); -const utils = require('../lib/util/utils').default; -const settings = require('../lib/util/settings.ts'); -const fs = require('fs'); -const configurationFile = data.joinPath('configuration.yaml'); -const devicesFile = data.joinPath('devices.yaml'); -const devicesFile2 = data.joinPath('devices2.yaml'); -const groupsFile = data.joinPath('groups.yaml'); -const secretFile = data.joinPath('secret.yaml'); -const yaml = require('js-yaml'); -const objectAssignDeep = require(`object-assign-deep`); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as data from './mocks/data'; +import fs from 'fs'; + +import yaml from 'js-yaml'; +import objectAssignDeep from 'object-assign-deep'; + +import mockedData from '../lib/util/data'; +import * as settings from '../lib/util/settings'; + +const configurationFile = mockedData.joinPath('configuration.yaml'); +const devicesFile = mockedData.joinPath('devices.yaml'); +const devicesFile2 = mockedData.joinPath('devices2.yaml'); +const groupsFile = mockedData.joinPath('groups.yaml'); +const secretFile = mockedData.joinPath('secret.yaml'); const minimalConfig = { external_converters: [], homeassistant: true, @@ -19,22 +21,28 @@ const minimalConfig = { }; describe('Settings', () => { - const write = (file, json, reread = true) => { + const write = (file: string, json: Record, reread: boolean = true): void => { fs.writeFileSync(file, yaml.dump(json)); + if (reread) { settings.reRead(); } }; - const read = (file) => yaml.load(fs.readFileSync(file, 'utf8')); - const remove = (file) => { - if (fs.existsSync(file)) fs.unlinkSync(file); + + const read = (file: string): unknown => yaml.load(fs.readFileSync(file, 'utf8')); + + const remove = (file: string): void => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } }; - const clearEnvironmentVariables = () => { - Object.keys(process.env).forEach((key) => { + + const clearEnvironmentVariables = (): void => { + for (const key in process.env) { if (key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) { delete process.env[key]; } - }); + } }; beforeEach(() => { @@ -48,6 +56,7 @@ describe('Settings', () => { it('Should return default settings', () => { write(configurationFile, {}); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; @@ -57,6 +66,7 @@ describe('Settings', () => { it('Should return settings', () => { write(configurationFile, {external_converters: ['abcd.js']}); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; @@ -66,7 +76,7 @@ describe('Settings', () => { it('Should apply environment variables', () => { process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true'; - process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = 15; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = '15'; 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'] = @@ -88,6 +98,7 @@ describe('Settings', () => { expect(settings.validate()).toStrictEqual([]); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = { '0x00158d00018255df': { @@ -336,7 +347,7 @@ describe('Settings', () => { expect(read(devicesFile)).toStrictEqual(expected); }); - function extractFromMultipleDeviceConfigs(contentDevices2) { + function extractFromMultipleDeviceConfigs(contentDevices2: Record): void { const contentConfiguration = { devices: ['devices.yaml', 'devices2.yaml'], }; @@ -380,7 +391,7 @@ describe('Settings', () => { }); it('Should add devices for first file when using 2 separates file and the second file is empty', () => { - extractFromMultipleDeviceConfigs(null); + extractFromMultipleDeviceConfigs({}); }); it('Should add devices to a separate file if devices.yaml doesnt exist', () => { @@ -520,7 +531,7 @@ describe('Settings', () => { it('Should add groups', () => { write(configurationFile, {}); - const added = settings.addGroup('test123'); + settings.addGroup('test123'); const expected = { 1: { friendly_name: 'test123', @@ -533,7 +544,7 @@ describe('Settings', () => { it('Should add groups with specific ID', () => { write(configurationFile, {}); - const added = settings.addGroup('test123', 123); + settings.addGroup('test123', '123'); const expected = { 123: { friendly_name: 'test123', @@ -578,9 +589,9 @@ describe('Settings', () => { it('Should not add duplicate groups with specific ID', () => { write(configurationFile, {}); - settings.addGroup('test123', 123); + settings.addGroup('test123', '123'); expect(() => { - settings.addGroup('test_id_123', 123); + settings.addGroup('test_id_123', '123'); }).toThrow(new Error("Group ID '123' is already in use")); const expected = { 123: { @@ -676,7 +687,7 @@ describe('Settings', () => { }); expect(() => { - settings.removeDeviceFromGroup('test123', 'bulb'); + settings.removeDeviceFromGroup('test123', ['bulb']); }).toThrow(new Error("Group 'test123' does not exist")); }); diff --git a/test/stub/logger.js b/test/stub/logger.js deleted file mode 100644 index c03dd45c47..0000000000 --- a/test/stub/logger.js +++ /dev/null @@ -1,48 +0,0 @@ -let level = 'info'; -let debugNamespaceIgnore = ''; -let namespacedLevels = {}; -let transports = []; -let transportsEnabled = false; - -const getMessage = (messageOrLambda) => (messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda); -const mock = { - log: jest.fn().mockImplementation((level, message, namespace = 'z2m') => { - if (transportsEnabled) { - for (const transport of transports) { - transport.log({level, message, namespace}, () => {}); - } - } - }), - init: jest.fn(), - info: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('info', getMessage(messageOrLambda), namespace)), - warning: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('warning', getMessage(messageOrLambda), namespace)), - error: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('error', getMessage(messageOrLambda), namespace)), - debug: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('debug', getMessage(messageOrLambda), namespace)), - cleanup: jest.fn(), - logOutput: jest.fn(), - add: (transport) => transports.push(transport), - addTransport: (transport) => transports.push(transport), - removeTransport: (transport) => { - transports = transports.filter((t) => t !== transport); - }, - setLevel: (newLevel) => { - level = newLevel; - }, - getLevel: () => level, - setNamespacedLevels: (nsLevels) => { - namespacedLevels = nsLevels; - }, - getNamespacedLevels: () => namespacedLevels, - setDebugNamespaceIgnore: (newIgnore) => { - debugNamespaceIgnore = newIgnore; - }, - getDebugNamespaceIgnore: () => debugNamespaceIgnore, - setTransportsEnabled: (value) => { - transportsEnabled = value; - }, - end: jest.fn(), -}; - -jest.mock('../../lib/util/logger', () => mock); - -module.exports = {...mock}; diff --git a/test/stub/sleep.js b/test/stub/sleep.js deleted file mode 100644 index 0a74c3aae7..0000000000 --- a/test/stub/sleep.js +++ /dev/null @@ -1,10 +0,0 @@ -const utils = require('../../lib/util/utils'); -const spy = jest.spyOn(utils.default, 'sleep'); - -export function mock() { - spy.mockImplementation(() => {}); -} - -export function restore() { - spy.mockRestore(); -} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000000..df7373f526 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig", + "include": ["./**/*"], + "compilerOptions": { + "types": ["jest"], + "rootDir": "..", + "noEmit": true + }, + "references": [{"path": ".."}] +} diff --git a/test/utils.test.js b/test/utils.test.ts similarity index 68% rename from test/utils.test.js rename to test/utils.test.ts index 76e5bd5697..94cd8c33d3 100644 --- a/test/utils.test.js +++ b/test/utils.test.ts @@ -1,7 +1,7 @@ -const utils = require('../lib/util/utils').default; -const version = require('../package.json').version; -const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').version; -const versionHerdsmanConverters = require('../node_modules/zigbee-herdsman-converters/package.json').version; +import fs from 'fs'; +import path from 'path'; + +import utils from '../lib/util/utils'; describe('Utils', () => { it('Object is empty', () => { @@ -15,12 +15,12 @@ describe('Utils', () => { }); it('git last commit', async () => { - let mockReturnValue = []; + const version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; + let mockReturnValue: [identical: boolean, result: {shortHash: string} | null] = [false, {shortHash: '123'}]; jest.mock('git-last-commit', () => ({ - getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]), + getLastCommit: (cb: (identical: boolean, result: {shortHash: string} | null) => void): void => cb(mockReturnValue[0], mockReturnValue[1]), })); - mockReturnValue = [false, {shortHash: '123'}]; expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: '123', version: version}); mockReturnValue = [true, null]; @@ -28,19 +28,25 @@ describe('Utils', () => { }); it('Check dependency version', async () => { + const versionHerdsman = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'package.json'), 'utf8'), + ).version; + const versionHerdsmanConverters = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'package.json'), 'utf8'), + ).version; 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'); - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = () => 60; - expect(utils.formatDate(date, 'ISO_8601_local').endsWith('-01:00')).toBeTruthy(); - Date.prototype.getTimezoneOffset = () => -60; - expect(utils.formatDate(date, 'ISO_8601_local').endsWith('+01:00')).toBeTruthy(); - Date.prototype.getTimezoneOffset = getTimezoneOffset; + const date = new Date('August 19, 1975 23:15:30 UTC+00:00').getTime(); + const getTzOffsetSpy = jest.spyOn(Date.prototype, 'getTimezoneOffset'); + getTzOffsetSpy.mockReturnValueOnce(60); + expect(utils.formatDate(date, 'ISO_8601_local').toString().endsWith('-01:00')).toBeTruthy(); + getTzOffsetSpy.mockReturnValueOnce(-60); + expect(utils.formatDate(date, 'ISO_8601_local').toString().endsWith('+01:00')).toBeTruthy(); }); + it('Removes null properties from object', () => { const obj1 = { ab: 0, diff --git a/tsconfig.json b/tsconfig.json index f7dc5307fd..27f73c8f73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,12 +13,11 @@ "declarationMap": true, "outDir": "dist", "baseUrl": ".", - "allowJs": true, "rootDir": "lib", "inlineSourceMap": true, "resolveJsonModule": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "composite": true }, - "include": ["lib/**/*", "lib/util/settings.schema.json"], - "exclude": ["node_modules"] + "include": ["./lib/**/*", "./lib/util/settings.schema.json"] }