feat(mqtt): Collect runtime metrics and provide them via REST

This commit is contained in:
Sören Beye 2022-02-12 18:13:42 +01:00
parent 3c24eba779
commit dcc1db4f86
9 changed files with 257 additions and 79 deletions

View File

@ -68,20 +68,21 @@ class Valetudo {
robot: this.robot
});
this.mqttController = new MqttController({
config: this.config,
robot: this.robot
});
this.webserver = new Webserver({
config: this.config,
robot: this.robot,
mqttController: this.mqttController,
ntpClient: this.ntpClient,
updater: this.updater,
valetudoEventStore: this.valetudoEventStore
});
this.mqttClient = new MqttController({
config: this.config,
robot: this.robot
});
this.scheduler = new Scheduler({
config: this.config,
robot: this.robot,
@ -216,8 +217,8 @@ class Valetudo {
await this.networkAdvertisementManager.shutdown();
await this.scheduler.shutdown();
if (this.mqttClient) {
await this.mqttClient.shutdown();
if (this.mqttController) {
await this.mqttController.shutdown();
}
await this.webserver.shutdown();

View File

@ -43,6 +43,35 @@ class MqttController {
this.subscriptions = {};
this.state = HomieCommonAttributes.STATE.INIT;
this.stats = {
messages: {
count: {
received: 0,
sent: 0
},
bytes: {
received: 0,
sent: 0
}
},
connection: {
connects: 0,
disconnects: 0,
reconnects: 0,
errors: 0
}
};
this.configDefaults = {
identity: {
friendlyName: this.robot.getModelName() + " " + Tools.GET_HUMAN_READABLE_SYSTEM_ID(),
identifier: Tools.GET_HUMAN_READABLE_SYSTEM_ID()
},
customizations: {
topicPrefix: "valetudo"
}
};
/** @public */
this.homieAddICBINVMapProperty = false;
@ -116,6 +145,25 @@ class MqttController {
});
}
/**
* @public
* @return {{stats: ({messages: {bytes: {received: number, sent: number}, count: {received: number, sent: number}}, connection: {reconnects: number, connects: number, disconnects: number, errors: number}}), state: string}}
*/
getStatus() {
return {
state: this.state,
stats: this.stats
};
}
/**
* @public
* @return {{identity: {identifier: string, friendlyName: string}, customizations: {topicPrefix: string}}}
*/
getConfigDefaults() {
return this.configDefaults;
}
/**
* @private
*/
@ -134,15 +182,15 @@ class MqttController {
});
if (!this.currentConfig.identity.identifier) {
this.currentConfig.identity.identifier = Tools.GET_HUMAN_READABLE_SYSTEM_ID();
this.currentConfig.identity.identifier = this.configDefaults.identity.identifier;
}
if (!this.currentConfig.identity.friendlyName) {
this.currentConfig.identity.friendlyName = this.robot.getModelName() + " " + Tools.GET_HUMAN_READABLE_SYSTEM_ID();
this.currentConfig.identity.friendlyName = this.configDefaults.identity.friendlyName;
}
if (!this.currentConfig.customizations.topicPrefix) {
this.currentConfig.customizations.topicPrefix = "valetudo";
this.currentConfig.customizations.topicPrefix = this.configDefaults.customizations.topicPrefix;
}
this.currentConfig.stateTopic = this.currentConfig.customizations.topicPrefix + "/" + this.currentConfig.identity.identifier + "/$state";
@ -231,6 +279,7 @@ class MqttController {
this.client.on("connect", () => {
Logger.info("Connected successfully to MQTT broker");
this.stats.connection.connects++;
this.messageDeduplicationCache.clear();
this.reconfigure(async () => {
@ -259,6 +308,9 @@ class MqttController {
});
this.client.on("message", (topic, message, packet) => {
this.stats.messages.count.received++;
this.stats.messages.bytes.received += packet.length;
if (!Object.prototype.hasOwnProperty.call(this.subscriptions, topic)) {
return;
}
@ -266,10 +318,10 @@ class MqttController {
const msg = message.toString();
//@ts-ignore
if (packet?.retain === true) {
if (packet.retain === true) {
Logger.warn(
"Received a retained MQTT message. Most certainly you or the home automation software integration " +
"you are using are sending the MQTT command incorrectly. Please remove the \"retained\" flag to fix this issue. Discarding.",
"you are using is sending the MQTT command incorrectly. Please remove the \"retained\" flag to fix this issue. Discarding message.",
{
topic: topic,
message: msg
@ -283,8 +335,10 @@ class MqttController {
});
this.client.on("error", (e) => {
this.stats.connection.errors++;
if (e && e.message === "Not supported") {
Logger.info("Connected to non standard compliant MQTT Broker.");
Logger.info("Connected to non-standard-compliant MQTT Broker.");
} else {
Logger.error("MQTT error:", e.toString());
@ -303,6 +357,7 @@ class MqttController {
});
this.client.on("reconnect", () => {
this.stats.connection.reconnects++;
Logger.info("Attempting to reconnect to MQTT broker");
});
@ -383,6 +438,7 @@ class MqttController {
this.messageDeduplicationCache.clear();
Logger.info("Successfully disconnected from the MQTT Broker");
this.stats.connection.disconnects++;
}
/**
@ -755,6 +811,8 @@ class MqttController {
const hasChanged = this.messageDeduplicationCache.update(topic, message);
if (hasChanged) {
this.stats.messages.count.sent++;
this.stats.messages.bytes.sent += message.length;
return this.asyncClient.publish(topic, message, options);
} else {
return new Promise(resolve => {

View File

@ -0,0 +1,40 @@
const express = require("express");
class MQTTRouter {
/**
*
* @param {object} options
* @param {import("../mqtt/MqttController")} options.mqttController
* @param {import("../Configuration")} options.config
* @param {*} options.validator
*/
constructor(options) {
this.router = express.Router({mergeParams: true});
this.config = options.config;
this.mqttController = options.mqttController;
this.validator = options.validator;
this.initRoutes();
}
initRoutes() {
this.router.get("/status", (req, res) => {
res.json(this.mqttController.getStatus());
});
this.router.get("/properties", (req, res) => {
res.json({
defaults: this.mqttController.getConfigDefaults()
});
});
}
getRouter() {
return this.router;
}
}
module.exports = MQTTRouter;

View File

@ -81,21 +81,6 @@ class ValetudoRouter {
res.json(mqttConfig);
});
this.router.get("/config/interfaces/mqtt/properties", (req, res) => {
//It might make sense to pull this from the mqttController but that would introduce a dependency between the webserver and the mqttController :/
res.json({
defaults: {
identity: {
friendlyName: this.robot.getModelName() + " " + Tools.GET_HUMAN_READABLE_SYSTEM_ID(),
identifier: Tools.GET_HUMAN_READABLE_SYSTEM_ID()
},
customizations: {
topicPrefix: "valetudo"
}
}
});
});
this.router.put("/config/interfaces/mqtt", this.validator, (req, res) => {
let mqttConfig = req.body;
let oldConfig = this.config.get("mqtt");

View File

@ -21,6 +21,7 @@ const ValetudoRouter = require("./ValetudoRouter");
const fs = require("fs");
const MiioValetudoRobot = require("../robots/MiioValetudoRobot");
const MQTTRouter = require("./MQTTRouter");
const NTPClientRouter = require("./NTPClientRouter");
const SSDPRouter = require("./SSDPRouter");
const SystemRouter = require("./SystemRouter");
@ -33,6 +34,7 @@ class WebServer {
/**
* @param {object} options
* @param {import("../core/ValetudoRobot")} options.robot
* @param {import("../mqtt/MqttController")} options.mqttController
* @param {import("../NTPClient")} options.ntpClient
* @param {import("../updater/Updater")} options.updater
* @param {import("../ValetudoEventStore")} options.valetudoEventStore
@ -112,6 +114,8 @@ class WebServer {
this.app.use("/api/v2/valetudo/", this.valetudoRouter.getRouter());
this.app.use("/api/v2/mqtt/", new MQTTRouter({config: this.config, mqttController: options.mqttController, validator: this.validator}).getRouter());
this.app.use("/api/v2/ntpclient/", new NTPClientRouter({config: this.config, ntpClient: options.ntpClient, validator: this.validator}).getRouter());
this.app.use("/api/v2/timers/", new TimerRouter({config: this.config, robot: this.robot, validator: this.validator}).getRouter());

View File

@ -0,0 +1,139 @@
{
"/api/v2/mqtt/status": {
"get": {
"tags": [
"MQTT"
],
"summary": "Get MQTTController status",
"responses": {
"200": {
"description": "The MQTTControllers current status.",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"state": {
"type": "string",
"enum": [
"init",
"ready",
"disconnected",
"lost",
"alert"
]
},
"stats": {
"type": "object",
"additionalProperties": false,
"properties": {
"messages": {
"type": "object",
"additionalProperties": false,
"properties": {
"count": {
"type": "object",
"additionalProperties": false,
"properties": {
"received": {
"type": "number"
},
"sent": {
"type": "number"
}
}
},
"bytes": {
"type": "object",
"additionalProperties": false,
"properties": {
"received": {
"type": "number"
},
"sent": {
"type": "number"
}
}
}
}
},
"connection": {
"type": "object",
"additionalProperties": false,
"properties": {
"connects": {
"type": "number"
},
"disconnects": {
"type": "number"
},
"reconnects": {
"type": "number"
},
"errors": {
"type": "number"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/api/v2/mqtt/properties": {
"get": {
"tags": [
"MQTT"
],
"summary": "Get MQTT properties such as the default config values",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"defaults": {
"type": "object",
"additionalProperties": false,
"properties": {
"identity": {
"type": "object",
"additionalProperties": false,
"properties": {
"friendlyName": {
"type": "string"
},
"identifier": {
"type": "string"
}
}
},
"customizations": {
"type": "object",
"additionalProperties": false,
"properties": {
"topicPrefix": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@ -198,56 +198,6 @@
}
}
},
"/api/v2/valetudo/config/interfaces/mqtt/properties": {
"get": {
"tags": [
"Valetudo"
],
"summary": "Get MQTT config properties such as the default values",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"defaults": {
"type": "object",
"additionalProperties": false,
"properties": {
"identity": {
"type": "object",
"additionalProperties": false,
"properties": {
"friendlyName": {
"type": "string"
},
"identifier": {
"type": "string"
}
}
},
"customizations": {
"type": "object",
"additionalProperties": false,
"properties": {
"topicPrefix": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/api/v2/valetudo/config/interfaces/http/auth/basic": {
"get": {
"tags": [

View File

@ -488,7 +488,7 @@ export const sendMQTTConfiguration = async (mqttConfiguration: MQTTConfiguration
export const fetchMQTTProperties = async (): Promise<MQTTProperties> => {
return valetudoAPI
.get<MQTTProperties>("/valetudo/config/interfaces/mqtt/properties")
.get<MQTTProperties>("/mqtt/properties")
.then(({data}) => {
return data;
});

View File

@ -24,6 +24,7 @@ const options = {
{name: "ValetudoEvents", description: "Valetudo Events"},
{name: "Robot", description: "Robot API"},
{name: "System", description: "System API"},
{name: "MQTT", description: "MQTT Controller API"},
{name: "NTP", description: "NTP Client API"},
{name: "Timers", description: "Timers API"},
{name: "Updater", description: "Update Valetudo using Valetudo"},