mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-06-20 12:11:43 +00:00
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import events from "node:events";
|
|
|
|
import logger from "./util/logger";
|
|
|
|
type ListenerKey = object;
|
|
|
|
interface EventBusMap {
|
|
adapterDisconnected: [];
|
|
permitJoinChanged: [data: eventdata.PermitJoinChanged];
|
|
publishAvailability: [];
|
|
entityRenamed: [data: eventdata.EntityRenamed];
|
|
entityRemoved: [data: eventdata.EntityRemoved];
|
|
lastSeenChanged: [data: eventdata.LastSeenChanged];
|
|
deviceNetworkAddressChanged: [data: eventdata.DeviceNetworkAddressChanged];
|
|
deviceAnnounce: [data: eventdata.DeviceAnnounce];
|
|
deviceInterview: [data: eventdata.DeviceInterview];
|
|
deviceJoined: [data: eventdata.DeviceJoined];
|
|
entityOptionsChanged: [data: eventdata.EntityOptionsChanged];
|
|
exposesChanged: [data: eventdata.ExposesChanged];
|
|
deviceLeave: [data: eventdata.DeviceLeave];
|
|
deviceMessage: [data: eventdata.DeviceMessage];
|
|
mqttMessage: [data: eventdata.MQTTMessage];
|
|
mqttMessagePublished: [data: eventdata.MQTTMessagePublished];
|
|
publishEntityState: [data: eventdata.PublishEntityState];
|
|
groupMembersChanged: [data: eventdata.GroupMembersChanged];
|
|
devicesChanged: [];
|
|
scenesChanged: [data: eventdata.ScenesChanged];
|
|
reconfigure: [data: eventdata.Reconfigure];
|
|
stateChange: [data: eventdata.StateChange];
|
|
}
|
|
type EventBusListener<K> = K extends keyof EventBusMap
|
|
? EventBusMap[K] extends unknown[]
|
|
? (...args: EventBusMap[K]) => Promise<void> | void
|
|
: never
|
|
: never;
|
|
|
|
type Stats = {
|
|
devices: Map<
|
|
string, // IEEE address
|
|
{
|
|
lastSeenChanges?: {messages: number; first: number};
|
|
leaveCounts: number;
|
|
networkAddressChanges: number;
|
|
}
|
|
>;
|
|
mqtt: {
|
|
published: number;
|
|
received: number;
|
|
};
|
|
};
|
|
|
|
export default class EventBus {
|
|
private callbacksByExtension = new Map<string, {event: keyof EventBusMap; callback: EventBusListener<keyof EventBusMap>}[]>();
|
|
private emitter = new events.EventEmitter<EventBusMap>();
|
|
readonly stats: Stats = {devices: new Map(), mqtt: {published: 0, received: 0}};
|
|
|
|
constructor() {
|
|
this.emitter.setMaxListeners(100);
|
|
}
|
|
|
|
public emitAdapterDisconnected(): void {
|
|
this.emitter.emit("adapterDisconnected");
|
|
}
|
|
public onAdapterDisconnected(key: ListenerKey, callback: () => Promise<void>): void {
|
|
this.on("adapterDisconnected", callback, key);
|
|
}
|
|
|
|
public emitPermitJoinChanged(data: eventdata.PermitJoinChanged): void {
|
|
this.emitter.emit("permitJoinChanged", data);
|
|
}
|
|
public onPermitJoinChanged(key: ListenerKey, callback: (data: eventdata.PermitJoinChanged) => void): void {
|
|
this.on("permitJoinChanged", callback, key);
|
|
}
|
|
|
|
public emitEntityRenamed(data: eventdata.EntityRenamed): void {
|
|
this.emitter.emit("entityRenamed", data);
|
|
}
|
|
public onEntityRenamed(key: ListenerKey, callback: (data: eventdata.EntityRenamed) => void): void {
|
|
this.on("entityRenamed", callback, key);
|
|
}
|
|
|
|
public emitEntityRemoved(data: eventdata.EntityRemoved): void {
|
|
this.emitter.emit("entityRemoved", data);
|
|
}
|
|
public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void {
|
|
this.on("entityRemoved", callback, key);
|
|
}
|
|
|
|
public emitLastSeenChanged(data: eventdata.LastSeenChanged): void {
|
|
this.emitter.emit("lastSeenChanged", data);
|
|
|
|
const device = this.stats.devices.get(data.device.ieeeAddr);
|
|
|
|
if (device?.lastSeenChanges) {
|
|
device.lastSeenChanges.messages += 1;
|
|
} else {
|
|
this.stats.devices.set(data.device.ieeeAddr, {
|
|
lastSeenChanges: {messages: 1, first: Date.now()},
|
|
leaveCounts: 0,
|
|
networkAddressChanges: 0,
|
|
});
|
|
}
|
|
}
|
|
public onLastSeenChanged(key: ListenerKey, callback: (data: eventdata.LastSeenChanged) => void): void {
|
|
this.on("lastSeenChanged", callback, key);
|
|
}
|
|
|
|
public emitDeviceNetworkAddressChanged(data: eventdata.DeviceNetworkAddressChanged): void {
|
|
this.emitter.emit("deviceNetworkAddressChanged", data);
|
|
|
|
const device = this.stats.devices.get(data.device.ieeeAddr);
|
|
|
|
if (device) {
|
|
device.networkAddressChanges += 1;
|
|
} else {
|
|
this.stats.devices.set(data.device.ieeeAddr, {leaveCounts: 0, networkAddressChanges: 1});
|
|
}
|
|
}
|
|
public onDeviceNetworkAddressChanged(key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void {
|
|
this.on("deviceNetworkAddressChanged", callback, key);
|
|
}
|
|
|
|
public emitDeviceAnnounce(data: eventdata.DeviceAnnounce): void {
|
|
this.emitter.emit("deviceAnnounce", data);
|
|
}
|
|
public onDeviceAnnounce(key: ListenerKey, callback: (data: eventdata.DeviceAnnounce) => void): void {
|
|
this.on("deviceAnnounce", callback, key);
|
|
}
|
|
|
|
public emitDeviceInterview(data: eventdata.DeviceInterview): void {
|
|
this.emitter.emit("deviceInterview", data);
|
|
}
|
|
public onDeviceInterview(key: ListenerKey, callback: (data: eventdata.DeviceInterview) => void): void {
|
|
this.on("deviceInterview", callback, key);
|
|
}
|
|
|
|
public emitDeviceJoined(data: eventdata.DeviceJoined): void {
|
|
this.emitter.emit("deviceJoined", data);
|
|
}
|
|
public onDeviceJoined(key: ListenerKey, callback: (data: eventdata.DeviceJoined) => void): void {
|
|
this.on("deviceJoined", callback, key);
|
|
}
|
|
|
|
public emitEntityOptionsChanged(data: eventdata.EntityOptionsChanged): void {
|
|
this.emitter.emit("entityOptionsChanged", data);
|
|
}
|
|
public onEntityOptionsChanged(key: ListenerKey, callback: (data: eventdata.EntityOptionsChanged) => void): void {
|
|
this.on("entityOptionsChanged", callback, key);
|
|
}
|
|
|
|
public emitExposesChanged(data: eventdata.ExposesChanged): void {
|
|
this.emitter.emit("exposesChanged", data);
|
|
}
|
|
public onExposesChanged(key: ListenerKey, callback: (data: eventdata.ExposesChanged) => void): void {
|
|
this.on("exposesChanged", callback, key);
|
|
}
|
|
|
|
public emitDeviceLeave(data: eventdata.DeviceLeave): void {
|
|
this.emitter.emit("deviceLeave", data);
|
|
|
|
const device = this.stats.devices.get(data.ieeeAddr);
|
|
|
|
if (device) {
|
|
device.leaveCounts += 1;
|
|
} else {
|
|
this.stats.devices.set(data.ieeeAddr, {leaveCounts: 1, networkAddressChanges: 0});
|
|
}
|
|
}
|
|
public onDeviceLeave(key: ListenerKey, callback: (data: eventdata.DeviceLeave) => void): void {
|
|
this.on("deviceLeave", callback, key);
|
|
}
|
|
|
|
public emitDeviceMessage(data: eventdata.DeviceMessage): void {
|
|
this.emitter.emit("deviceMessage", data);
|
|
}
|
|
public onDeviceMessage(key: ListenerKey, callback: (data: eventdata.DeviceMessage) => void): void {
|
|
this.on("deviceMessage", callback, key);
|
|
}
|
|
|
|
public emitMQTTMessage(data: eventdata.MQTTMessage): void {
|
|
this.emitter.emit("mqttMessage", data);
|
|
|
|
this.stats.mqtt.received += 1;
|
|
}
|
|
public onMQTTMessage(key: ListenerKey, callback: (data: eventdata.MQTTMessage) => void): void {
|
|
this.on("mqttMessage", callback, key);
|
|
}
|
|
|
|
public emitMQTTMessagePublished(data: eventdata.MQTTMessagePublished): void {
|
|
this.emitter.emit("mqttMessagePublished", data);
|
|
|
|
this.stats.mqtt.published += 1;
|
|
}
|
|
public onMQTTMessagePublished(key: ListenerKey, callback: (data: eventdata.MQTTMessagePublished) => void): void {
|
|
this.on("mqttMessagePublished", callback, key);
|
|
}
|
|
|
|
public emitPublishEntityState(data: eventdata.PublishEntityState): void {
|
|
this.emitter.emit("publishEntityState", data);
|
|
}
|
|
public onPublishEntityState(key: ListenerKey, callback: (data: eventdata.PublishEntityState) => void): void {
|
|
this.on("publishEntityState", callback, key);
|
|
}
|
|
|
|
public emitGroupMembersChanged(data: eventdata.GroupMembersChanged): void {
|
|
this.emitter.emit("groupMembersChanged", data);
|
|
}
|
|
public onGroupMembersChanged(key: ListenerKey, callback: (data: eventdata.GroupMembersChanged) => void): void {
|
|
this.on("groupMembersChanged", callback, key);
|
|
}
|
|
|
|
public emitDevicesChanged(): void {
|
|
this.emitter.emit("devicesChanged");
|
|
}
|
|
public onDevicesChanged(key: ListenerKey, callback: () => void): void {
|
|
this.on("devicesChanged", callback, key);
|
|
}
|
|
|
|
public emitScenesChanged(data: eventdata.ScenesChanged): void {
|
|
this.emitter.emit("scenesChanged", data);
|
|
}
|
|
public onScenesChanged(key: ListenerKey, callback: (data: eventdata.ScenesChanged) => void): void {
|
|
this.on("scenesChanged", callback, key);
|
|
}
|
|
|
|
public emitReconfigure(data: eventdata.Reconfigure): void {
|
|
this.emitter.emit("reconfigure", data);
|
|
}
|
|
public onReconfigure(key: ListenerKey, callback: (data: eventdata.Reconfigure) => void): void {
|
|
this.on("reconfigure", callback, key);
|
|
}
|
|
|
|
public emitStateChange(data: eventdata.StateChange): void {
|
|
this.emitter.emit("stateChange", data);
|
|
}
|
|
public onStateChange(key: ListenerKey, callback: (data: eventdata.StateChange) => void): void {
|
|
this.on("stateChange", callback, key);
|
|
}
|
|
|
|
public emitExposesAndDevicesChanged(device: Device): void {
|
|
this.emitDevicesChanged();
|
|
this.emitExposesChanged({device});
|
|
}
|
|
|
|
private on<K extends keyof EventBusMap>(event: K, callback: EventBusListener<K>, key: ListenerKey): void {
|
|
if (!this.callbacksByExtension.has(key.constructor.name)) {
|
|
this.callbacksByExtension.set(key.constructor.name, []);
|
|
}
|
|
|
|
const wrappedCallback = async (...args: never[]): Promise<void> => {
|
|
try {
|
|
await callback(...args);
|
|
} catch (error) {
|
|
logger.error(`EventBus error '${key.constructor.name}/${event}': ${(error as Error).message}`);
|
|
// biome-ignore lint/style/noNonNullAssertion: always Error
|
|
logger.debug((error as Error).stack!);
|
|
}
|
|
};
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: just created if wasn't valid
|
|
this.callbacksByExtension.get(key.constructor.name)!.push({event, callback: wrappedCallback});
|
|
this.emitter.on(event, wrappedCallback as EventBusListener<K>);
|
|
}
|
|
|
|
public removeListeners(key: ListenerKey): void {
|
|
const callbacks = this.callbacksByExtension.get(key.constructor.name);
|
|
|
|
if (callbacks) {
|
|
for (const cb of callbacks) {
|
|
this.emitter.removeListener(cb.event, cb.callback);
|
|
}
|
|
}
|
|
}
|
|
}
|