diff --git a/db/knex_migrations/2023-10-23-add-nut-monitor.js b/db/knex_migrations/2023-10-23-add-nut-monitor.js new file mode 100644 index 0000000000..8f8bc3b494 --- /dev/null +++ b/db/knex_migrations/2023-10-23-add-nut-monitor.js @@ -0,0 +1,20 @@ +exports.up = function (knex) { + // Add new column + return knex.schema + .alterTable("monitor", function (table) { + table.text("ups_name"); + table.text("nut_username"); + table.text("nut_password"); + }); + +}; + +exports.down = function (knex) { + // Drop nut variable column + return knex.schema + .alterTable("monitor", function (table) { + table.dropColumn("ups_name"); + table.dropColumn("nut_username"); + table.dropColumn("nut_password"); + }); +}; diff --git a/package-lock.json b/package-lock.json index 1c62e19d87..2bff9a3e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,7 @@ "favico.js": "~0.3.10", "jest": "~29.6.1", "marked": "~4.2.5", + "node-nut": "^1.0.3", "node-ssh": "~13.1.0", "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", @@ -13644,6 +13645,16 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-nut": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/node-nut/-/node-nut-1.0.3.tgz", + "integrity": "sha512-vJtSD+FNeinCpgjSijrhRpj1vW41q7CJgs7gJgfST/B448l3Q69xOUo4veomgxLH1mDlrESSYsUVkd8u4yiO7w==", + "dev": true, + "engines": { + "node": ">0.8.x", + "npm": ">1.1.x" + } + }, "node_modules/node-radius-client": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz", diff --git a/package.json b/package.json index f11712658a..6413c65e27 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "favico.js": "~0.3.10", "jest": "~29.6.1", "marked": "~4.2.5", + "node-nut": "^1.0.3", "node-ssh": "~13.1.0", "postcss-html": "~1.5.0", "postcss-rtlcss": "~3.7.2", diff --git a/server/model/monitor.js b/server/model/monitor.js index 5dcb7171c4..da23980d9b 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -130,6 +130,7 @@ class Monitor extends BeanModel { maintenance: await Monitor.isUnderMaintenance(this.id), mqttTopic: this.mqttTopic, mqttSuccessMessage: this.mqttSuccessMessage, + upsName: this.upsName, databaseQuery: this.databaseQuery, authMethod: this.authMethod, grpcUrl: this.grpcUrl, @@ -173,6 +174,8 @@ class Monitor extends BeanModel { radiusSecret: this.radiusSecret, mqttUsername: this.mqttUsername, mqttPassword: this.mqttPassword, + nutUsername: this.nutUsername, + nutPassword: this.nutPassword, authWorkstation: this.authWorkstation, authDomain: this.authDomain, tlsCa: this.tlsCa, diff --git a/server/monitor-types/nut.js b/server/monitor-types/nut.js new file mode 100644 index 0000000000..f330b63b83 --- /dev/null +++ b/server/monitor-types/nut.js @@ -0,0 +1,64 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, DOWN } = require("../../src/util"); +const dayjs = require("dayjs"); +const jsonata = require("jsonata"); +const Nut = require("node-nut"); + +const { log } = require("../../src/util"); + +class NutMonitorType extends MonitorType { + + name = "nut"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + let startTime = dayjs().valueOf(); + let expression = jsonata(monitor.jsonPath); + + const nut = new Nut(monitor.port, monitor.hostname); + + nut.on("ready", () => { + nut.GetUPSList((upslist, err) => { + if (err) { + nut.close(); + log.error("NUT Error: " + err); + } + + let upsname = upslist[monitor.upsName] && monitor.upsName || Object.keys(upslist)[0]; + + nut.GetUPSVars(upsname, async (vars, err) => { + nut.close(); + if (err) { + throw new Error("Error getting UPS variables"); + } else { + // convert data to object + if (typeof vars === "string") { + vars = JSON.parse(vars); + } + const data = ({ ...vars }); // copy vars + + // Check device status + let result = await expression.evaluate(data); + + if (result.toString() === monitor.expectedValue) { + heartbeat.status = UP; + heartbeat.msg = ""; + heartbeat.ping = dayjs().valueOf() - startTime; + } else { + heartbeat.status = DOWN; + heartbeat.msg = `Value not expected, value was: [${result}]`; + } + } + }); + }); + }); + + nut.start(); + } +} + +module.exports = { + NutMonitorType, +}; diff --git a/server/server.js b/server/server.js index f726790c2d..311da9c67e 100644 --- a/server/server.js +++ b/server/server.js @@ -796,6 +796,8 @@ let needSetup = false; bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null; bean.mqttUsername = monitor.mqttUsername; bean.mqttPassword = monitor.mqttPassword; + bean.nutUsername = monitor.nutUsername; + bean.nutPassword = monitor.nutPassword; bean.mqttTopic = monitor.mqttTopic; bean.mqttSuccessMessage = monitor.mqttSuccessMessage; bean.databaseConnectionString = monitor.databaseConnectionString; @@ -824,6 +826,7 @@ let needSetup = false; bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions); bean.kafkaProducerMessage = monitor.kafkaProducerMessage; bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly; + bean.upsName = monitor.upsName; bean.validate(); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 0b9ab47e48..e68f4e8afb 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -115,6 +115,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); + UptimeKumaServer.monitorTypeList["nut"] = new NutMonitorType(); this.io = new Server(this.httpServer); } @@ -433,3 +434,4 @@ module.exports = { const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type"); const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { DnsMonitorType } = require("./monitor-types/dns"); +const { NutMonitorType } = require("./monitor-types/nut"); diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 20c8aa13a1..e8909d847b 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -42,6 +42,7 @@ {{ filterPassword(monitor.databaseConnectionString) }} SQL Server: {{ filterPassword(monitor.databaseConnectionString) }} Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }} + NUT: {{monitor.upsName}} on {{ monitor.hostname }}:{{ monitor.port }}

diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index aef9642333..6714875686 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -61,6 +61,9 @@ + @@ -144,8 +147,26 @@
- -
+ + + + + +
@@ -221,15 +242,15 @@ - -
+ +
- -
+ +
@@ -1156,12 +1177,14 @@ message HealthCheckResponse { } } - // Set default port for DNS if not already defined - if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") { + // Set default port for DNS, RADIUS, NUT if not already defined + if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812" || this.monitor.port === "3493") { if (this.monitor.type === "dns") { this.monitor.port = "53"; } else if (this.monitor.type === "radius") { this.monitor.port = "1812"; + } else if (this.monitor.type === "nut") { + this.monitor.port = "3493"; } else { this.monitor.port = undefined; }