mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(mqtt): Collect runtime metrics and provide them via REST
This commit is contained in:
parent
3c24eba779
commit
dcc1db4f86
@ -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();
|
||||
|
||||
@ -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 => {
|
||||
|
||||
40
backend/lib/webserver/MQTTRouter.js
Normal file
40
backend/lib/webserver/MQTTRouter.js
Normal 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;
|
||||
@ -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");
|
||||
|
||||
@ -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());
|
||||
|
||||
139
backend/lib/webserver/doc/MQTTRouter.openapi.json
Normal file
139
backend/lib/webserver/doc/MQTTRouter.openapi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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": [
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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"},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user