@daemonitor/plugins 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,8 +39,13 @@ const QNAP = {
39
39
  hdNumber: "1.3.6.1.4.1.24681.1.2.10.0", // installed disk count
40
40
  hdDescr: "1.3.6.1.4.1.24681.1.2.11.1.2.", // + index
41
41
  hdTemp: "1.3.6.1.4.1.24681.1.2.11.1.3.",
42
- hdStatus: "1.3.6.1.4.1.24681.1.2.11.1.4.", // 0 = ready/ok
42
+ hdStatus: "1.3.6.1.4.1.24681.1.2.11.1.4.", // 0 = ready/ok, negative = fault/empty
43
+ hdModel: "1.3.6.1.4.1.24681.1.2.11.1.5.", // "--" only when the bay is empty
44
+ hdCapacity: "1.3.6.1.4.1.24681.1.2.11.1.6.", // "7.28 TB" / "--"
43
45
  hdSmart: "1.3.6.1.4.1.24681.1.2.11.1.7.", // "GOOD" / "Warning"
46
+ memTotal: "1.3.6.1.4.1.24681.1.2.2.0", // "16013.4 MB"
47
+ memFree: "1.3.6.1.4.1.24681.1.2.3.0", // "11984.9 MB"
48
+ fanSpeed: "1.3.6.1.4.1.24681.1.2.15.1.3.", // + index, "1835 RPM"
44
49
  };
45
50
  function slug(s) {
46
51
  return s.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -88,32 +93,38 @@ async function snmpWalk(t, oid, timeout = 10000) {
88
93
  }
89
94
  async function collectPrinter(t) {
90
95
  const warnPct = t.tonerWarnPct ?? 10;
91
- const [model, serial, pagesRaw, statusRaw, descs, maxes, levels] = await Promise.all([
96
+ // Walk LEVELS (clean numeric values, one per row) to get the supply indices,
97
+ // then fetch each supply's description/max by that index — a single get whose
98
+ // (possibly multi-line hex) value decodeSnmp handles, so a long description
99
+ // can't be split into phantom supplies and misalign the levels.
100
+ const [model, serial, pagesRaw, statusRaw, levelRows] = await Promise.all([
92
101
  snmpGet(t, OID.model),
93
102
  snmpGet(t, OID.serial),
94
103
  snmpGet(t, OID.pages),
95
104
  snmpGet(t, OID.status),
96
- snmpWalk(t, OID.supplyDesc),
97
- snmpWalk(t, OID.supplyMax),
98
- snmpWalk(t, OID.supplyLevel),
105
+ snmpWalkIndexed(t, OID.supplyLevel),
99
106
  ]);
100
107
  // No response at all → the printer is unreachable / SNMP off.
101
- if (model == null && serial == null && pagesRaw == null && !descs.length) {
108
+ if (model == null && serial == null && pagesRaw == null && !levelRows.length) {
102
109
  return { name: t.name, ip: t.ip, kind: "printer", reachable: false };
103
110
  }
104
- // Pair descriptions with levels. prtMarkerSuppliesLevel semantics: -2 =
105
- // unknown, -3 = "some remaining" (a level the device won't quantify); else a
106
- // count out of MaxCapacity (which can itself be -2 = unknown).
107
- const supplies = descs.map((desc, i) => {
108
- const max = Number(maxes[i]);
109
- const lvl = Number(levels[i]);
111
+ // prtMarkerSuppliesLevel semantics: -2 = unknown, -3 = "some remaining" (a
112
+ // level the device won't quantify); else a count out of MaxCapacity (which can
113
+ // itself be -2 = unknown).
114
+ const supplies = await Promise.all(levelRows.map(async (row) => {
115
+ const [descRaw, maxRaw] = await Promise.all([
116
+ snmpGet(t, `${OID.supplyDesc}.${row.idx}`),
117
+ snmpGet(t, `${OID.supplyMax}.${row.idx}`),
118
+ ]);
119
+ const lvl = Number(row.val);
120
+ const max = Number(maxRaw);
110
121
  let pct = null;
111
122
  if (lvl === -3)
112
- pct = 100; // "ok / some remaining"
123
+ pct = 100;
113
124
  else if (lvl >= 0 && max > 0)
114
125
  pct = Math.round((lvl / max) * 100);
115
- return { name: desc, level: lvl, max, pct };
116
- });
126
+ return { name: descRaw || "supply", level: lvl, max, pct };
127
+ }));
117
128
  const low = supplies.filter((s) => s.pct != null && s.pct <= warnPct);
118
129
  return {
119
130
  name: t.name,
@@ -193,13 +204,16 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
193
204
  async function collectQnap(t) {
194
205
  const tempWarn = t.tempWarnC ?? 70;
195
206
  const volWarn = t.volumeWarnPct ?? 90;
196
- const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs] = await Promise.all([
207
+ const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs, memTotal, memFree, fanSpeeds] = await Promise.all([
197
208
  snmpGet(t, QNAP.model),
198
209
  snmpGet(t, QNAP.cpuTemp),
199
210
  snmpGet(t, QNAP.sysTemp),
200
211
  snmpGet(t, QNAP.hdNumber),
201
212
  snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
202
213
  snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"), // hrStorageDescr
214
+ snmpGet(t, QNAP.memTotal),
215
+ snmpGet(t, QNAP.memFree),
216
+ snmpWalk(t, "1.3.6.1.4.1.24681.1.2.15.1.3"), // SystemFanSpeed table
203
217
  ]);
204
218
  if (model == null && cpuTemp == null && !cpuLoads.length) {
205
219
  return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
@@ -207,20 +221,32 @@ async function collectQnap(t) {
207
221
  // CPU: average per-core load.
208
222
  const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
209
223
  const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
210
- // Disks — iterate 1..hdNumber (bounded).
224
+ // RAM used %. Values look like "16013.4 MB".
225
+ const memTotalMB = parseNum(memTotal), memFreeMB = parseNum(memFree);
226
+ const memPct = memTotalMB && memTotalMB > 0 && memFreeMB != null
227
+ ? Math.round(((memTotalMB - memFreeMB) / memTotalMB) * 100) : undefined;
228
+ // Fans — each entry looks like "1835 RPM".
229
+ const fans = fanSpeeds.map((f) => parseNum(f)).filter((n) => n != null && n > 0);
230
+ // Disks — iterate 1..hdNumber (bounded). Presence is judged by MODEL/CAPACITY:
231
+ // an empty bay reports model "--", but a FAILING disk can report an unreadable
232
+ // temp ("-- C/-- F") + negative status yet still advertise a real model. The old
233
+ // temp+status heuristic silently skipped those bad disks (e.g. an HDD in SMART
234
+ // "Warning" with status -9) — so the NAS looked healthy while QTS flagged it.
211
235
  const hdN = Math.min(Number(hdNumRaw) || 0, 24);
212
236
  const disks = [];
213
237
  for (let i = 1; i <= hdN; i++) {
214
- const [st, tp, sm, ds] = await Promise.all([
215
- snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i), snmpGet(t, QNAP.hdDescr + i),
238
+ const [st, tp, sm, ds, md, cap] = await Promise.all([
239
+ snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i),
240
+ snmpGet(t, QNAP.hdDescr + i), snmpGet(t, QNAP.hdModel + i), snmpGet(t, QNAP.hdCapacity + i),
216
241
  ]);
242
+ const model = md && md !== "--" ? md : undefined;
243
+ const capacity = cap && cap !== "--" ? cap : undefined;
244
+ // Only a truly empty bay (no model AND no capacity) is skipped.
245
+ if (!model && !capacity)
246
+ continue;
217
247
  const status = st != null ? Number(st) : undefined;
218
248
  const tempC = parseC(tp);
219
- // Empty bay: no readable temp and a non-ready (negative/absent, e.g. -5)
220
- // status. Skip it — an empty slot isn't a failed disk.
221
- if (tempC == null && (status == null || status < 0))
222
- continue;
223
- disks.push({ slot: i, descr: ds || undefined, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
249
+ disks.push({ slot: i, descr: ds || undefined, model, capacity, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
224
250
  }
225
251
  // A present disk is bad only on a real error status, an explicitly-bad SMART
226
252
  // verdict, or overheating — not a blank "--" or an empty bay.
@@ -242,7 +268,10 @@ async function collectQnap(t) {
242
268
  name: t.name, ip: t.ip, kind: "qnap", reachable: true,
243
269
  model: model || undefined,
244
270
  cpuPct, cpuTempC: parseC(cpuTemp), sysTempC: parseC(sysTemp),
245
- disks, badDisks: badDisks.map((d) => ({ slot: d.slot, status: d.status, smart: d.smart, tempC: d.tempC })),
271
+ memPct, memTotalMB: memTotalMB || undefined,
272
+ fans, fanRpm: fans.length ? Math.min(...fans) : undefined,
273
+ disks, diskCount: disks.length,
274
+ badDisks: badDisks.map((d) => ({ slot: d.slot, descr: d.descr, model: d.model, status: d.status, smart: d.smart, tempC: d.tempC })),
246
275
  volumes, fullVolumes: fullVols,
247
276
  tempWarnC: tempWarn, volumeWarnPct: volWarn,
248
277
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",