@daemonitor/plugins 1.1.8 → 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, "");
@@ -199,13 +204,16 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
199
204
  async function collectQnap(t) {
200
205
  const tempWarn = t.tempWarnC ?? 70;
201
206
  const volWarn = t.volumeWarnPct ?? 90;
202
- const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs] = await Promise.all([
207
+ const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs, memTotal, memFree, fanSpeeds] = await Promise.all([
203
208
  snmpGet(t, QNAP.model),
204
209
  snmpGet(t, QNAP.cpuTemp),
205
210
  snmpGet(t, QNAP.sysTemp),
206
211
  snmpGet(t, QNAP.hdNumber),
207
212
  snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
208
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
209
217
  ]);
210
218
  if (model == null && cpuTemp == null && !cpuLoads.length) {
211
219
  return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
@@ -213,20 +221,32 @@ async function collectQnap(t) {
213
221
  // CPU: average per-core load.
214
222
  const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
215
223
  const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
216
- // 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.
217
235
  const hdN = Math.min(Number(hdNumRaw) || 0, 24);
218
236
  const disks = [];
219
237
  for (let i = 1; i <= hdN; i++) {
220
- const [st, tp, sm, ds] = await Promise.all([
221
- 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),
222
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;
223
247
  const status = st != null ? Number(st) : undefined;
224
248
  const tempC = parseC(tp);
225
- // Empty bay: no readable temp and a non-ready (negative/absent, e.g. -5)
226
- // status. Skip it — an empty slot isn't a failed disk.
227
- if (tempC == null && (status == null || status < 0))
228
- continue;
229
- 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 });
230
250
  }
231
251
  // A present disk is bad only on a real error status, an explicitly-bad SMART
232
252
  // verdict, or overheating — not a blank "--" or an empty bay.
@@ -248,7 +268,10 @@ async function collectQnap(t) {
248
268
  name: t.name, ip: t.ip, kind: "qnap", reachable: true,
249
269
  model: model || undefined,
250
270
  cpuPct, cpuTempC: parseC(cpuTemp), sysTempC: parseC(sysTemp),
251
- 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 })),
252
275
  volumes, fullVolumes: fullVols,
253
276
  tempWarnC: tempWarn, volumeWarnPct: volWarn,
254
277
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",