mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-10-23 06:12:23 +00:00
feat: New health extension & extras in bridge/info (#27164)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
1 parent
242815e139
commit
cd9b752ede
14 files changed
+596
-15
No files matched your search
@@ -16,6 +16,7 @@ import ExtensionConfigure from "./extension/configure";
|
||||
import ExtensionExternalConverters from "./extension/externalConverters";
|
||||
import ExtensionExternalExtensions from "./extension/externalExtensions";
|
||||
import ExtensionGroups from "./extension/groups";
|
||||
import ExtensionHealth from "./extension/health";
|
||||
import ExtensionNetworkMap from "./extension/networkMap";
|
||||
import ExtensionOnEvent from "./extension/onEvent";
|
||||
import ExtensionOTAUpdate from "./extension/otaUpdate";
|
||||
@@ -76,6 +77,7 @@ export class Controller {
|
||||
new ExtensionOTAUpdate(...this.extensionArgs),
|
||||
new ExtensionExternalExtensions(...this.extensionArgs),
|
||||
new ExtensionAvailability(...this.extensionArgs),
|
||||
new ExtensionHealth(...this.extensionArgs),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,25 @@ type EventBusListener<K> = K extends keyof EventBusMap
|
||||
: 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);
|
||||
@@ -72,6 +88,18 @@ export default class EventBus {
|
||||
|
||||
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);
|
||||
@@ -79,6 +107,14 @@ export default class EventBus {
|
||||
|
||||
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);
|
||||
@@ -121,6 +157,14 @@ export default class EventBus {
|
||||
|
||||
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);
|
||||
@@ -135,6 +179,8 @@ export default class EventBus {
|
||||
|
||||
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);
|
||||
@@ -142,6 +188,8 @@ export default class EventBus {
|
||||
|
||||
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);
|
||||
|
||||
@@ -25,6 +25,8 @@ import Extension from "./extension";
|
||||
const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
// set on `start`
|
||||
#osInfo!: Zigbee2MQTTAPI["bridge/info"]["os"];
|
||||
private zigbee2mqttVersion!: {commitHash?: string; version: string};
|
||||
private zigbeeHerdsmanVersion!: {version: string};
|
||||
private zigbeeHerdsmanConvertersVersion!: {version: string};
|
||||
@@ -92,6 +94,15 @@ export default class Bridge extends Extension {
|
||||
|
||||
logger.addTransport(this.logTransport);
|
||||
|
||||
const os = await import("node:os");
|
||||
const process = await import("node:process");
|
||||
const logicalCpuCores = os.cpus();
|
||||
this.#osInfo = {
|
||||
version: `${os.version()} - ${os.release()} - ${os.arch()}`,
|
||||
node_version: process.version,
|
||||
cpus: `${[...new Set(logicalCpuCores.map((cpu) => cpu.model))].join(" | ")} (x${logicalCpuCores.length})`,
|
||||
memory_mb: Math.round(os.totalmem() / 1024 / 1024),
|
||||
};
|
||||
this.zigbee2mqttVersion = await utils.getZigbee2MQTTVersion();
|
||||
this.zigbeeHerdsmanVersion = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
this.zigbeeHerdsmanConvertersVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
|
||||
@@ -691,6 +702,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
const networkParams = await this.zigbee.getNetworkParameters();
|
||||
const payload: Zigbee2MQTTAPI["bridge/info"] = {
|
||||
os: this.#osInfo,
|
||||
mqtt: this.mqtt.info,
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
zigbee_herdsman_converters: this.zigbeeHerdsmanConvertersVersion,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as os from "node:os";
|
||||
import * as process from "node:process";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
/** Round with 2 decimals */
|
||||
const round2 = (n: number): number => Math.round(n * 100.0) / 100.0;
|
||||
/** Round with 4 decimals */
|
||||
const round4 = (n: number): number => Math.round(n * 10000.0) / 10000.0;
|
||||
|
||||
export default class Health extends Extension {
|
||||
#checkTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
override async start(): Promise<void> {
|
||||
await super.start();
|
||||
|
||||
this.#checkTimer = setInterval(this.#checkHealth.bind(this), utils.minutes(settings.get().health.interval));
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
clearInterval(this.#checkTimer);
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
clearStats(): void {
|
||||
this.eventBus.stats.devices.clear();
|
||||
this.eventBus.stats.mqtt.published = 0;
|
||||
this.eventBus.stats.mqtt.received = 0;
|
||||
}
|
||||
|
||||
async #checkHealth(): Promise<void> {
|
||||
const sysMemTotalKb = os.totalmem() / 1024;
|
||||
const sysMemFreeKb = os.freemem() / 1024;
|
||||
const procMemUsedKb = process.memoryUsage().rss / 1024;
|
||||
const healthcheck: Zigbee2MQTTAPI["bridge/health"] = {
|
||||
response_time: Date.now(),
|
||||
os: {
|
||||
load_average: os.loadavg(), // will be [0,0,0] on Windows (not supported)
|
||||
memory_used_mb: round2((sysMemTotalKb - sysMemFreeKb) / 1024),
|
||||
memory_percent: round4((sysMemFreeKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
process: {
|
||||
uptime_sec: Math.floor(process.uptime()),
|
||||
memory_used_mb: round2(procMemUsedKb / 1024),
|
||||
memory_percent: round4((procMemUsedKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
mqtt: {...this.mqtt.stats, ...this.eventBus.stats.mqtt},
|
||||
devices: {},
|
||||
};
|
||||
|
||||
for (const [ieeeAddr, device] of this.eventBus.stats.devices) {
|
||||
let messages = 0;
|
||||
let mps = 0;
|
||||
|
||||
if (device.lastSeenChanges) {
|
||||
const timeDiff = Date.now() - device.lastSeenChanges.first;
|
||||
messages = device.lastSeenChanges.messages;
|
||||
mps = timeDiff > 0 ? round4(messages / (timeDiff / 1000.0)) : 0;
|
||||
}
|
||||
|
||||
healthcheck.devices[ieeeAddr] = {
|
||||
messages,
|
||||
messages_per_sec: mps,
|
||||
leave_count: device.leaveCounts,
|
||||
network_address_changes: device.networkAddressChanges,
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.get().health.reset_on_check) {
|
||||
this.clearStats();
|
||||
}
|
||||
|
||||
await this.mqtt.publish("bridge/health", JSON.stringify(healthcheck), {clientOptions: {retain: true, qos: 1}});
|
||||
}
|
||||
}
|
||||
+14
@@ -30,6 +30,20 @@ export default class Mqtt {
|
||||
private defaultPublishOptions: MqttPublishOptions;
|
||||
public retainedMessages: {[s: string]: {topic: string; payload: string; options: MqttPublishOptions}} = {};
|
||||
|
||||
get info() {
|
||||
return {
|
||||
version: this.client.options.protocolVersion,
|
||||
server: `${this.client.options.protocol}://${this.client.options.host}:${this.client.options.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
get stats() {
|
||||
return {
|
||||
connected: this.isConnected(),
|
||||
queued: this.client.queue.length,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
this.defaultPublishOptions = {
|
||||
|
||||
@@ -183,6 +183,11 @@ export interface Zigbee2MQTTSettings {
|
||||
output: "json" | "attribute" | "attribute_and_json";
|
||||
transmit_power?: number;
|
||||
};
|
||||
health: {
|
||||
/** in minutes */
|
||||
interval: number;
|
||||
reset_on_check: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Zigbee2MQTTScene {
|
||||
@@ -330,6 +335,16 @@ export interface Zigbee2MQTTAPI {
|
||||
};
|
||||
|
||||
"bridge/info": {
|
||||
os: {
|
||||
version: string;
|
||||
node_version: string;
|
||||
cpus: string;
|
||||
memory_mb: number;
|
||||
};
|
||||
mqtt: {
|
||||
version: number | undefined;
|
||||
server: string;
|
||||
};
|
||||
version: string;
|
||||
commit: string | undefined;
|
||||
zigbee_herdsman_converters: {version: string};
|
||||
@@ -355,6 +370,36 @@ export interface Zigbee2MQTTAPI {
|
||||
config_schema: typeof schemaJson;
|
||||
};
|
||||
|
||||
"bridge/health": {
|
||||
/** time of message, msec from epoch, UTC */
|
||||
response_time: number;
|
||||
os: {
|
||||
load_average: number[];
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
process: {
|
||||
uptime_sec: number;
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
mqtt: {
|
||||
connected: boolean;
|
||||
queued: number;
|
||||
received: number;
|
||||
published: number;
|
||||
};
|
||||
devices: Record<
|
||||
string /* ieee */,
|
||||
{
|
||||
messages: number;
|
||||
messages_per_sec: number;
|
||||
leave_count: number;
|
||||
network_address_changes: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
"bridge/devices": Zigbee2MQTTDevice[];
|
||||
|
||||
"bridge/groups": Zigbee2MQTTGroup[];
|
||||
|
||||
@@ -770,6 +770,27 @@
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
"title": "Health",
|
||||
"description": "Periodically check the health of Zigbee2MQTT",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interval": {
|
||||
"type": "number",
|
||||
"title": "Interval",
|
||||
"description": "Interval between checks in minutes",
|
||||
"default": 10,
|
||||
"requiresRestart": true
|
||||
},
|
||||
"reset_on_check": {
|
||||
"type": "boolean",
|
||||
"title": "Reset on check",
|
||||
"description": "If true, will reset stats every time the health check is executed (only applicable to stats that can be reset).",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"required": ["mqtt"],
|
||||
|
||||
@@ -113,6 +113,10 @@ export const defaults = {
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss",
|
||||
output: "json",
|
||||
},
|
||||
health: {
|
||||
interval: 10,
|
||||
reset_on_check: false,
|
||||
},
|
||||
} satisfies RecursivePartial<Settings>;
|
||||
|
||||
let _settings: Partial<Settings> | undefined;
|
||||
|
||||
+3
-3
@@ -279,9 +279,9 @@ function isZHGroup(obj: unknown): obj is zh.Group {
|
||||
return obj?.constructor.name.toLowerCase() === "group";
|
||||
}
|
||||
|
||||
const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
const seconds = (seconds: number): number => 1000 * seconds;
|
||||
export const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
export const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
export const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
async function publishLastSeen(
|
||||
data: eventdata.LastSeenChanged,
|
||||
|
||||
Reference in new issue
Block a user