mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
fix: OnEvent fixes (#27063)
This commit is contained in:
@@ -575,12 +575,14 @@ export default class Bind extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
// If message is published to a group, add members of the group
|
||||
const group = data.groupID && data.groupID !== 0 && this.zigbee.groupByID(data.groupID);
|
||||
if (data.groupID && data.groupID !== 0) {
|
||||
// If message is published to a group, add members of the group
|
||||
const group = this.zigbee.groupByID(data.groupID);
|
||||
|
||||
if (group) {
|
||||
for (const member of group.zh.members) {
|
||||
toPoll.add(member);
|
||||
if (group) {
|
||||
for (const member of group.zh.members) {
|
||||
toPoll.add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-20
@@ -1,4 +1,4 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import {onEvent} from 'zigbee-herdsman-converters';
|
||||
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
@@ -9,25 +9,42 @@ import Extension from './extension';
|
||||
export default class OnEvent extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
// don't await, in case of repeated failures this would hold startup
|
||||
this.callOnEvent(device, 'start', {}).catch(utils.noop);
|
||||
}
|
||||
|
||||
this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data)));
|
||||
this.eventBus.onDeviceJoined(this, (data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data)));
|
||||
this.eventBus.onDeviceInterview(this, (data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data)));
|
||||
this.eventBus.onDeviceAnnounce(this, (data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data)));
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this, (data) =>
|
||||
this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)),
|
||||
);
|
||||
this.eventBus.onEntityOptionsChanged(this, async (data) => {
|
||||
if (data.entity.isDevice()) {
|
||||
await this.callOnEvent(data.entity, 'deviceOptionsChanged', data).then(() => this.eventBus.emitDevicesChanged());
|
||||
this.eventBus.onDeviceMessage(this, async (data) => {
|
||||
await this.callOnEvent(data.device, 'message', {
|
||||
endpoint: data.endpoint,
|
||||
meta: data.meta,
|
||||
cluster: typeof data.cluster === 'string' ? data.cluster : /* v8 ignore next */ undefined, // XXX: ZH typing is wrong?
|
||||
type: data.type,
|
||||
data: data.data, // XXX: typing is a bit convoluted: ZHC has `KeyValueAny` here while Z2M has `KeyValue | Array<string | number>`
|
||||
});
|
||||
});
|
||||
this.eventBus.onDeviceJoined(this, async (data) => {
|
||||
await this.callOnEvent(data.device, 'deviceJoined', {});
|
||||
});
|
||||
this.eventBus.onDeviceLeave(this, async (data) => {
|
||||
if (data.device) {
|
||||
await this.callOnEvent(data.device, 'stop', {});
|
||||
}
|
||||
});
|
||||
this.eventBus.onDeviceInterview(this, async (data) => {
|
||||
await this.callOnEvent(data.device, 'deviceInterview', {});
|
||||
});
|
||||
this.eventBus.onDeviceAnnounce(this, async (data) => {
|
||||
await this.callOnEvent(data.device, 'deviceAnnounce', {});
|
||||
});
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this, async (data) => {
|
||||
await this.callOnEvent(data.device, 'deviceNetworkAddressChanged', {});
|
||||
});
|
||||
this.eventBus.onEntityOptionsChanged(this, async (data) => {
|
||||
if (data.entity.isDevice()) {
|
||||
await this.callOnEvent(data.entity, 'deviceOptionsChanged', {});
|
||||
this.eventBus.emitDevicesChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private convertData(data: KeyValue): KeyValue {
|
||||
return {...data, device: data.device.zh};
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
@@ -38,12 +55,15 @@ export default class OnEvent extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise<void> {
|
||||
if (device.options.disabled) return;
|
||||
const state = this.state.get(device);
|
||||
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(data.device);
|
||||
private async callOnEvent(device: Device, type: Parameters<typeof onEvent>[0], data: Parameters<typeof onEvent>[1]): Promise<void> {
|
||||
if (device.options.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await zhc.onEvent(type, data, device.zh, {deviceExposesChanged});
|
||||
const state = this.state.get(device);
|
||||
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
|
||||
|
||||
await onEvent(type, data, device.zh, {deviceExposesChanged});
|
||||
|
||||
if (device.definition?.onEvent) {
|
||||
const options: KeyValue = device.options;
|
||||
|
||||
Vendored
+2
-2
@@ -71,7 +71,7 @@ declare global {
|
||||
type EntityOptionsChanged = {entity: Device | Group; from: KeyValue; to: KeyValue};
|
||||
type ExposesChanged = {device: Device};
|
||||
type Reconfigure = {device: Device};
|
||||
type DeviceLeave = {ieeeAddr: string; name: string};
|
||||
type DeviceLeave = {ieeeAddr: string; name: string; device?: Device};
|
||||
type GroupMembersChanged = {group: Group; action: 'remove' | 'add' | 'remove_all'; endpoint: zh.Endpoint; skipDisableReporting: boolean};
|
||||
type PublishEntityState = {entity: Group | Device; message: KeyValue; stateChangeReason?: StateChangeReason; payload: KeyValue};
|
||||
type DeviceMessage = {
|
||||
@@ -79,7 +79,7 @@ declare global {
|
||||
device: Device;
|
||||
endpoint: zh.Endpoint;
|
||||
linkquality: number;
|
||||
groupID: number;
|
||||
groupID: number; // XXX: should this be `?`
|
||||
cluster: string | number;
|
||||
data: KeyValue | Array<string | number>;
|
||||
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl};
|
||||
|
||||
+51
-50
@@ -1,4 +1,5 @@
|
||||
import type {Events as ZHEvents} from 'zigbee-herdsman';
|
||||
import type {StartResult} from 'zigbee-herdsman/dist/adapter/tstype';
|
||||
|
||||
import {randomInt} from 'node:crypto';
|
||||
|
||||
@@ -14,20 +15,20 @@ import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
|
||||
const entityIDRegex = new RegExp(`^(.+?)(?:/([^/]+))?$`);
|
||||
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
|
||||
|
||||
export default class Zigbee {
|
||||
// @ts-expect-error initialized in start
|
||||
private herdsman: Controller;
|
||||
private eventBus: EventBus;
|
||||
private groupLookup: {[s: number]: Group} = {};
|
||||
private deviceLookup: {[s: string]: Device} = {};
|
||||
private groupLookup: Map<number /* group ID */, Group> = new Map();
|
||||
private deviceLookup: Map<string /* IEEE address */, Device> = new Map();
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
async start(): Promise<'reset' | 'resumed' | 'restored'> {
|
||||
async start(): Promise<StartResult> {
|
||||
const infoHerdsman = await utils.getDependencyVersion('zigbee-herdsman');
|
||||
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
|
||||
const panId = settings.get().advanced.pan_id;
|
||||
@@ -63,12 +64,12 @@ export default class Zigbee {
|
||||
`Using zigbee-herdsman with settings: '${stringify(JSON.stringify(herdsmanSettings).replaceAll(JSON.stringify(herdsmanSettings.network.networkKey), '"HIDDEN"'))}'`,
|
||||
);
|
||||
|
||||
let startResult;
|
||||
let startResult: StartResult;
|
||||
try {
|
||||
this.herdsman = new Controller(herdsmanSettings);
|
||||
startResult = await this.herdsman.start();
|
||||
} catch (error) {
|
||||
logger.error(`Error while starting zigbee-herdsman`);
|
||||
logger.error('Error while starting zigbee-herdsman');
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -109,18 +110,17 @@ export default class Zigbee {
|
||||
this.herdsman.on('deviceLeave', (data: ZHEvents.DeviceLeavePayload) => {
|
||||
const name = settings.getDevice(data.ieeeAddr)?.friendly_name || data.ieeeAddr;
|
||||
logger.warning(`Device '${name}' left the network`);
|
||||
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name});
|
||||
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name, device: this.deviceLookup.get(data.ieeeAddr)});
|
||||
});
|
||||
this.herdsman.on('message', async (data: ZHEvents.MessagePayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr)!;
|
||||
await device.resolveDefinition();
|
||||
logger.debug(
|
||||
() =>
|
||||
`Received Zigbee message from '${device.name}', type '${data.type}', ` +
|
||||
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
||||
(data['groupID'] !== undefined ? ` with groupID ${data.groupID}` : ``) +
|
||||
(device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``),
|
||||
);
|
||||
logger.debug(() => {
|
||||
const groupId = data.groupID !== undefined ? ` with groupID ${data.groupID}` : '';
|
||||
const fromCoord = device.zh.type === 'Coordinator' ? ', ignoring since it is from coordinator' : '';
|
||||
|
||||
return `Received Zigbee message from '${device.name}', type '${data.type}', cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}${groupId}${fromCoord}`;
|
||||
});
|
||||
if (device.zh.type === 'Coordinator') return;
|
||||
this.eventBus.emitDeviceMessage({...data, device});
|
||||
});
|
||||
@@ -165,9 +165,7 @@ export default class Zigbee {
|
||||
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
|
||||
} else {
|
||||
logger.warning(
|
||||
`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` +
|
||||
`'${data.device.zh.manufacturerName}' is NOT supported, ` +
|
||||
`please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`,
|
||||
`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name '${data.device.zh.manufacturerName}' is NOT supported, please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`,
|
||||
);
|
||||
}
|
||||
} else if (data.status === 'failed') {
|
||||
@@ -241,55 +239,57 @@ export default class Zigbee {
|
||||
await this.herdsman.permitJoin(time, device?.zh);
|
||||
}
|
||||
|
||||
async resolveDevicesDefinitions(ignoreCache: boolean = false): Promise<void> {
|
||||
async resolveDevicesDefinitions(ignoreCache = false): Promise<void> {
|
||||
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
await device.resolveDefinition(ignoreCache);
|
||||
}
|
||||
}
|
||||
|
||||
@bind private resolveDevice(ieeeAddr: string): Device | undefined {
|
||||
if (!this.deviceLookup[ieeeAddr]) {
|
||||
if (!this.deviceLookup.has(ieeeAddr)) {
|
||||
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
|
||||
if (device) {
|
||||
this.deviceLookup[ieeeAddr] = new Device(device);
|
||||
this.deviceLookup.set(ieeeAddr, new Device(device));
|
||||
}
|
||||
}
|
||||
|
||||
const device = this.deviceLookup[ieeeAddr];
|
||||
const device = this.deviceLookup.get(ieeeAddr);
|
||||
if (device && !device.zh.isDeleted) {
|
||||
device.ensureInSettings();
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveGroup(groupID: number): Group {
|
||||
private resolveGroup(groupID: number): Group | undefined {
|
||||
const group = this.herdsman.getGroupByID(Number(groupID));
|
||||
if (group && !this.groupLookup[groupID]) {
|
||||
this.groupLookup[groupID] = new Group(group, this.resolveDevice);
|
||||
if (group && !this.groupLookup.has(groupID)) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
}
|
||||
|
||||
return this.groupLookup[groupID];
|
||||
return this.groupLookup.get(groupID);
|
||||
}
|
||||
|
||||
resolveEntity(key: string | number | zh.Device): Device | Group | undefined {
|
||||
if (typeof key === 'object') {
|
||||
return this.resolveDevice(key.ieeeAddr);
|
||||
} else if (typeof key === 'string' && key.toLowerCase() === 'coordinator') {
|
||||
}
|
||||
|
||||
if (typeof key === 'string' && key.toLowerCase() === 'coordinator') {
|
||||
return this.resolveDevice(this.herdsman.getDevicesByType('Coordinator')[0].ieeeAddr);
|
||||
} else {
|
||||
const settingsDevice = settings.getDevice(key.toString());
|
||||
}
|
||||
|
||||
if (settingsDevice) {
|
||||
return this.resolveDevice(settingsDevice.ID);
|
||||
}
|
||||
const settingsDevice = settings.getDevice(key.toString());
|
||||
|
||||
const groupSettings = settings.getGroup(key);
|
||||
if (settingsDevice) {
|
||||
return this.resolveDevice(settingsDevice.ID);
|
||||
}
|
||||
|
||||
if (groupSettings) {
|
||||
const group = this.resolveGroup(groupSettings.ID);
|
||||
// If group does not exist, create it (since it's already in configuration.yaml)
|
||||
return group ? group : this.createGroup(groupSettings.ID);
|
||||
}
|
||||
const groupSettings = settings.getGroup(key);
|
||||
|
||||
if (groupSettings) {
|
||||
const group = this.resolveGroup(groupSettings.ID);
|
||||
// If group does not exist, create it (since it's already in configuration.yaml)
|
||||
return group ? group : this.createGroup(groupSettings.ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,13 +339,13 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
|
||||
yield this.resolveGroup(group.groupID);
|
||||
yield this.resolveGroup(group.groupID)!;
|
||||
}
|
||||
}
|
||||
|
||||
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
|
||||
for (const group of this.herdsman.getGroupsIterator(predicate)) {
|
||||
yield this.resolveGroup(group.groupID);
|
||||
yield this.resolveGroup(group.groupID)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,21 +363,22 @@ export default class Zigbee {
|
||||
if (passlist.includes(ieeeAddr)) {
|
||||
logger.info(`Accepting joining device which is on passlist '${ieeeAddr}'`);
|
||||
return true;
|
||||
} else {
|
||||
logger.info(`Rejecting joining not in passlist device '${ieeeAddr}'`);
|
||||
return false;
|
||||
}
|
||||
} else if (blocklist.length > 0) {
|
||||
|
||||
logger.info(`Rejecting joining not in passlist device '${ieeeAddr}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blocklist.length > 0) {
|
||||
if (blocklist.includes(ieeeAddr)) {
|
||||
logger.info(`Rejecting joining device which is on blocklist '${ieeeAddr}'`);
|
||||
return false;
|
||||
} else {
|
||||
logger.info(`Accepting joining not in blocklist device '${ieeeAddr}'`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
|
||||
logger.info(`Accepting joining not in blocklist device '${ieeeAddr}'`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async touchlinkFactoryResetFirst(): Promise<boolean> {
|
||||
@@ -402,7 +403,7 @@ export default class Zigbee {
|
||||
|
||||
createGroup(ID: number): Group {
|
||||
this.herdsman.createGroup(ID);
|
||||
return this.resolveGroup(ID);
|
||||
return this.resolveGroup(ID)!;
|
||||
}
|
||||
|
||||
deviceByNetworkAddress(networkAddress: number): Device | undefined {
|
||||
@@ -410,7 +411,7 @@ export default class Zigbee {
|
||||
return device && this.resolveDevice(device.ieeeAddr);
|
||||
}
|
||||
|
||||
groupByID(ID: number): Group {
|
||||
groupByID(ID: number): Group | undefined {
|
||||
return this.resolveGroup(ID);
|
||||
}
|
||||
}
|
||||
|
||||
+137
-58
@@ -1,101 +1,180 @@
|
||||
import * as data from '../mocks/data';
|
||||
import {mockLogger} from '../mocks/logger';
|
||||
import {mockMQTTPublishAsync} from '../mocks/mqtt';
|
||||
import {flushPromises, getZhcBaseDefinitions} from '../mocks/utils';
|
||||
import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
|
||||
import {flushPromises} from '../mocks/utils';
|
||||
import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman';
|
||||
|
||||
import {MockInstance} from 'vitest';
|
||||
import type {MockInstance} from 'vitest';
|
||||
import type {OnEvent as DefinitionOnEvent} from 'zigbee-herdsman-converters/lib/types';
|
||||
|
||||
import type Device from '../../lib/model/device';
|
||||
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import {Controller} from '../../lib/controller';
|
||||
import OnEvent from '../../lib/extension/onEvent';
|
||||
import * as settings from '../../lib/util/settings';
|
||||
|
||||
const mockOnEvent = vi.spyOn(zhc, 'onEvent');
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug, mockOnEvent];
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
|
||||
|
||||
returnDevices.push(devices.bulb.ieeeAddr, devices.LIVOLO.ieeeAddr);
|
||||
|
||||
describe('Extension: OnEvent', () => {
|
||||
let controller: Controller;
|
||||
let mockLivoloOnEvent: MockInstance;
|
||||
let onEventSpy: MockInstance<typeof zhc.onEvent>;
|
||||
let deviceOnEventSpy: MockInstance<DefinitionOnEvent>;
|
||||
|
||||
const getZ2MDevice = (zhDevice: unknown): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
const clearOnEventSpies = (): void => {
|
||||
onEventSpy.mockClear();
|
||||
deviceOnEventSpy.mockClear();
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const livoloDefinition = (await getZhcBaseDefinitions()).find((d) => d.zigbeeModel?.includes(devices.LIVOLO.modelID!))!;
|
||||
mockLivoloOnEvent = vi.spyOn(livoloDefinition, 'onEvent');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers();
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
onEventSpy = vi.spyOn(zhc, 'onEvent');
|
||||
deviceOnEventSpy = vi.spyOn(getZ2MDevice(devices.LIVOLO).definition!, 'onEvent');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-expect-error private
|
||||
controller.state.state = {};
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
mocksClear.forEach((m) => m.mockClear());
|
||||
for (const mock of mocksClear) {
|
||||
mock.mockClear();
|
||||
}
|
||||
|
||||
await controller.removeExtension(controller.getExtension('OnEvent')!);
|
||||
clearOnEventSpies();
|
||||
await controller.addExtension(new OnEvent(...controller.extensionArgs));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller?.stop();
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
vi.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},
|
||||
it('starts & stops', async () => {
|
||||
expect(onEventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'start',
|
||||
{},
|
||||
devices.LIVOLO,
|
||||
settings.getDevice(devices.LIVOLO.ieeeAddr),
|
||||
{},
|
||||
{
|
||||
deviceExposesChanged: expect.any(Function),
|
||||
},
|
||||
{deviceExposesChanged: expect.any(Function)},
|
||||
);
|
||||
|
||||
// Test deviceExposesChanged
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
console.log(mockLivoloOnEvent.mock.calls[0][5].deviceExposesChanged());
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
|
||||
await controller.stop();
|
||||
|
||||
expect(onEventSpy).toHaveBeenCalledTimes(4);
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'stop',
|
||||
{},
|
||||
devices.LIVOLO,
|
||||
settings.getDevice(devices.LIVOLO.ieeeAddr),
|
||||
{},
|
||||
{deviceExposesChanged: expect.any(Function)},
|
||||
);
|
||||
});
|
||||
|
||||
it('Should call index onEvent with zigbee event', async () => {
|
||||
mockOnEvent.mockClear();
|
||||
it('calls on device events', async () => {
|
||||
clearOnEventSpies();
|
||||
await mockZHEvents.deviceAnnounce({device: devices.LIVOLO});
|
||||
await flushPromises();
|
||||
expect(mockOnEvent).toHaveBeenCalledTimes(1);
|
||||
expect(zhc.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device: devices.LIVOLO}, devices.LIVOLO, {
|
||||
deviceExposesChanged: expect.any(Function),
|
||||
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'deviceAnnounce',
|
||||
{},
|
||||
devices.LIVOLO,
|
||||
settings.getDevice(devices.LIVOLO.ieeeAddr),
|
||||
{},
|
||||
{deviceExposesChanged: expect.any(Function)},
|
||||
);
|
||||
|
||||
const emitExposesAndDevicesChangedSpy = vi.spyOn(
|
||||
// @ts-expect-error protected
|
||||
controller.getExtension('OnEvent')!.eventBus,
|
||||
'emitExposesAndDevicesChanged',
|
||||
);
|
||||
|
||||
deviceOnEventSpy.mock.calls[0][5]!.deviceExposesChanged();
|
||||
|
||||
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledTimes(1);
|
||||
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledWith(getZ2MDevice(devices.LIVOLO));
|
||||
|
||||
await mockZHEvents.deviceLeave({ieeeAddr: devices.LIVOLO.ieeeAddr});
|
||||
await flushPromises();
|
||||
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'stop',
|
||||
{},
|
||||
devices.LIVOLO,
|
||||
settings.getDevice(devices.LIVOLO.ieeeAddr),
|
||||
{},
|
||||
{deviceExposesChanged: expect.any(Function)},
|
||||
);
|
||||
});
|
||||
|
||||
it('calls on device message', async () => {
|
||||
clearOnEventSpies();
|
||||
|
||||
await mockZHEvents.message({
|
||||
type: 'attributeReport',
|
||||
device: devices.LIVOLO,
|
||||
endpoint: devices.LIVOLO.endpoints[0],
|
||||
linkquality: 213,
|
||||
groupID: 0,
|
||||
cluster: 'genBasic',
|
||||
data: {zclVersion: 8},
|
||||
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledWith(
|
||||
'message',
|
||||
{
|
||||
type: 'attributeReport',
|
||||
endpoint: devices.LIVOLO.endpoints[0],
|
||||
cluster: 'genBasic',
|
||||
data: {zclVersion: 8},
|
||||
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
|
||||
},
|
||||
devices.LIVOLO,
|
||||
settings.getDevice(devices.LIVOLO.ieeeAddr),
|
||||
{},
|
||||
{deviceExposesChanged: expect.any(Function)},
|
||||
);
|
||||
});
|
||||
|
||||
it('does not block startup on failure', async () => {
|
||||
await controller.removeExtension(controller.getExtension('OnEvent')!);
|
||||
clearOnEventSpies();
|
||||
deviceOnEventSpy.mockImplementationOnce(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
throw new Error('Failed');
|
||||
});
|
||||
await controller.addExtension(new OnEvent(...controller.extensionArgs));
|
||||
|
||||
expect(onEventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user