Configurar Top-Rack como ajuste global
This commit is contained in:
+10
-2
@@ -275,10 +275,18 @@ Si una RPi tiene un sensor externo de temperatura del rack, Node-RED puede escri
|
||||
}
|
||||
```
|
||||
|
||||
En el configurador, coloca la ruta en el dispositivo que tiene el sensor:
|
||||
En el configurador, usa la seccion general `Temperatura Top-Rack`:
|
||||
|
||||
```text
|
||||
/home/yamaray/monitor-rpi/rack-temp.json
|
||||
RPi origen: RPi - MeshCore · 192.168.0.60
|
||||
Path JSON temperatura: /home/yamaray/monitor-rpi/rack-temp.json
|
||||
```
|
||||
|
||||
La configuracion se guarda como:
|
||||
|
||||
```text
|
||||
rackTemperatureSourceDeviceId: rpi3-meshcore
|
||||
rackTemperatureFile: /home/yamaray/monitor-rpi/rack-temp.json
|
||||
```
|
||||
|
||||
El backend lee ese archivo durante el mismo scan SSH y muestra el valor como `Temperatura Top-Rack` en la primera linea del dashboard.
|
||||
|
||||
+2
-8
@@ -5,6 +5,8 @@
|
||||
"sshTimeoutSeconds": 8,
|
||||
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
|
||||
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub",
|
||||
"rackTemperatureSourceDeviceId": "rpi3-meshcore",
|
||||
"rackTemperatureFile": "/home/yamaray/monitor-rpi/rack-temp.json",
|
||||
"temperatureThresholdsC": {
|
||||
"warning": 60,
|
||||
"hot": 70,
|
||||
@@ -41,7 +43,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "Home Assistant Local",
|
||||
"port": 22,
|
||||
"id": "rpi5-ha-main",
|
||||
@@ -56,7 +57,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "Home Assistant Cabaña",
|
||||
"port": 22,
|
||||
"id": "rpi4-ha-second",
|
||||
@@ -71,7 +71,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "Docker",
|
||||
"port": 22,
|
||||
"id": "rpi5-jose-docker",
|
||||
@@ -86,7 +85,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "Docker - Daniel",
|
||||
"port": 22,
|
||||
"id": "rpi5-dani-docker",
|
||||
@@ -101,7 +99,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "nginx - wireguard ",
|
||||
"port": 22,
|
||||
"id": "rpi3-nginx",
|
||||
@@ -116,7 +113,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "MQTT - varios",
|
||||
"port": 22,
|
||||
"id": "rpi3-pi3home",
|
||||
@@ -131,7 +127,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "/home/yamaray/monitor-rpi/rack-temp.json",
|
||||
"role": "Meshcore-Interface",
|
||||
"port": 22,
|
||||
"id": "rpi3-meshcore",
|
||||
@@ -146,7 +141,6 @@
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"rackTemperatureFile": "",
|
||||
"role": "Reserva",
|
||||
"port": 22,
|
||||
"id": "rpi3-node-3",
|
||||
|
||||
+25
-7
@@ -233,7 +233,6 @@
|
||||
.field-role { grid-column: span 3; }
|
||||
.field-model { grid-column: span 2; }
|
||||
.field-location { grid-column: span 2; }
|
||||
.field-rack-temp { grid-column: span 4; }
|
||||
.field-host { grid-column: span 3; }
|
||||
.field-port { grid-column: span 1; }
|
||||
.field-username { grid-column: span 2; }
|
||||
@@ -446,7 +445,7 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
|
||||
.field-name, .field-role, .field-host, .password-credential, .field-rack-temp { grid-column: span 3; }
|
||||
.field-name, .field-role, .field-host, .password-credential { grid-column: span 3; }
|
||||
.field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; }
|
||||
.field-port { grid-column: span 1; }
|
||||
.credential-note { grid-column: span 4; }
|
||||
@@ -461,7 +460,7 @@
|
||||
.top-grid, .device { grid-template-columns: 1fr; }
|
||||
.field-active, .field-name, .field-id, .field-role, .field-model, .field-location,
|
||||
.field-host, .field-port, .field-username, .field-auth, .password-credential,
|
||||
.credential-note, .field-rack-temp, .field-delete { grid-column: span 1; }
|
||||
.credential-note, .field-delete { grid-column: span 1; }
|
||||
.threshold-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -503,6 +502,17 @@
|
||||
<input id="sshTimeoutSeconds" type="number" min="2" step="1">
|
||||
</label>
|
||||
</div>
|
||||
<div class="ssh-key-panel">
|
||||
<h2>Temperatura Top-Rack</h2>
|
||||
<div class="ssh-key-fields">
|
||||
<label title="RPi desde el que el monitor leera el archivo JSON de temperatura. Se muestra nombre e IP para identificarlo rapido.">RPi origen
|
||||
<select id="rackTemperatureSourceDeviceId"></select>
|
||||
</label>
|
||||
<label title="Path del archivo JSON en el RPi origen. Node-RED debe escribir ahi la ultima temperatura.">Path JSON temperatura
|
||||
<input id="rackTemperatureFile" placeholder="/home/yamaray/monitor-rpi/rack-temp.json">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ssh-key-panel">
|
||||
<h2>Clave SSH del monitor</h2>
|
||||
<div class="ssh-key-fields">
|
||||
@@ -703,9 +713,6 @@
|
||||
<div class="credential-note">
|
||||
Metodo activo: <strong>${authMethod === "key" ? "Clave asimetrica: copiar clave publica del monitor en la RPi" : "Password"}</strong>
|
||||
</div>
|
||||
<label class="field-rack-temp" title="Archivo opcional en este RPi con la temperatura del rack escrita por Node-RED. Si esta vacio, no se recoge temperatura Top-Rack desde este dispositivo.">Archivo temp rack
|
||||
<input data-field="rackTemperatureFile" value="${device.rackTemperatureFile || ""}" placeholder="/home/yamaray/monitor-rpi/rack-temp.json">
|
||||
</label>
|
||||
<label class="field-active" title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo
|
||||
<input data-field="active" type="checkbox" ${device.active ? "checked" : ""}>
|
||||
</label>
|
||||
@@ -720,6 +727,16 @@
|
||||
document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30;
|
||||
document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300;
|
||||
document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8;
|
||||
const rackSource = document.querySelector("#rackTemperatureSourceDeviceId");
|
||||
rackSource.innerHTML = [
|
||||
`<option value="">Sin sensor Top-Rack</option>`,
|
||||
...state.config.devices.map((device) => {
|
||||
const value = device.id || device.host || device.name || "";
|
||||
const label = `${device.name || value} · ${device.host || "sin IP"}`;
|
||||
return `<option value="${value}" ${state.config.rackTemperatureSourceDeviceId === value ? "selected" : ""}>${label}</option>`;
|
||||
})
|
||||
].join("");
|
||||
document.querySelector("#rackTemperatureFile").value = state.config.rackTemperatureFile || "";
|
||||
document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519";
|
||||
document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`;
|
||||
document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60;
|
||||
@@ -745,6 +762,8 @@
|
||||
state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30);
|
||||
state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300);
|
||||
state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8);
|
||||
state.config.rackTemperatureSourceDeviceId = document.querySelector("#rackTemperatureSourceDeviceId").value || "";
|
||||
state.config.rackTemperatureFile = document.querySelector("#rackTemperatureFile").value || "";
|
||||
state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519";
|
||||
state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`;
|
||||
state.config.temperatureThresholdsC = {
|
||||
@@ -852,7 +871,6 @@
|
||||
password: "",
|
||||
privateKeyPath: "",
|
||||
authMethod: "password",
|
||||
rackTemperatureFile: "",
|
||||
model: "RPi 4",
|
||||
role: "",
|
||||
location: "Rack",
|
||||
|
||||
@@ -156,8 +156,9 @@ chmod 600 ~/.ssh/authorized_keys</code></pre>
|
||||
"sensor": "rack-top",
|
||||
"updatedAt": "2026-06-05T21:12:18.997Z"
|
||||
}</code></pre>
|
||||
<p>En el configurador, en ese dispositivo, usa <code>Archivo temp rack</code> con una ruta como:</p>
|
||||
<pre><code>/home/yamaray/monitor-rpi/rack-temp.json</code></pre>
|
||||
<p>En el configurador, usa la seccion general <code>Temperatura Top-Rack</code>:</p>
|
||||
<pre><code>RPi origen: RPi - MeshCore · 192.168.0.60
|
||||
Path JSON temperatura: /home/yamaray/monitor-rpi/rack-temp.json</code></pre>
|
||||
|
||||
<h2>Docker</h2>
|
||||
<pre><code>cd /home/yamaray/docker/monitorRPi
|
||||
|
||||
@@ -49,6 +49,7 @@ async function readConfig() {
|
||||
parsed.temperatureThresholdsC = normalizeTemperatureThresholds(parsed.temperatureThresholdsC);
|
||||
parsed.metricThresholdsPercent = normalizeMetricThresholds(parsed.metricThresholdsPercent);
|
||||
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices.map(normalizeDeviceConfig) : [];
|
||||
normalizeRackTemperatureConfig(parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ async function writeConfig(nextConfig) {
|
||||
metricThresholdsPercent: normalizeMetricThresholds(nextConfig.metricThresholdsPercent),
|
||||
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices.map(normalizeDeviceConfig) : []
|
||||
};
|
||||
normalizeRackTemperatureConfig(normalized);
|
||||
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
||||
config = normalized;
|
||||
@@ -80,6 +82,19 @@ function normalizeDeviceConfig(device = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRackTemperatureConfig(nextConfig) {
|
||||
nextConfig.rackTemperatureSourceDeviceId = String(nextConfig.rackTemperatureSourceDeviceId || "");
|
||||
nextConfig.rackTemperatureFile = String(nextConfig.rackTemperatureFile || "");
|
||||
|
||||
if (!nextConfig.rackTemperatureSourceDeviceId || !nextConfig.rackTemperatureFile) {
|
||||
const legacyDevice = nextConfig.devices.find((device) => device.rackTemperatureFile);
|
||||
if (legacyDevice) {
|
||||
nextConfig.rackTemperatureSourceDeviceId = legacyDevice.id || "";
|
||||
nextConfig.rackTemperatureFile = legacyDevice.rackTemperatureFile || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function classifyTemp(tempC) {
|
||||
const thresholds = config.temperatureThresholdsC || {};
|
||||
if (tempC >= (thresholds.critical || 80)) return "critical";
|
||||
@@ -130,6 +145,7 @@ function mockMetrics(device) {
|
||||
const temp = seededNumber(`${device.id}:temp`, baseTemp - 4, baseTemp + 18);
|
||||
const dockerRole = /docker|gitea|nginx/i.test(device.role || "");
|
||||
const haRole = /home assistant/i.test(device.role || "");
|
||||
const rackTemperatureFile = rackTemperatureFileForDevice(device);
|
||||
|
||||
return {
|
||||
cpuTempC: Number(temp.toFixed(1)),
|
||||
@@ -166,13 +182,13 @@ function mockMetrics(device) {
|
||||
rxBytesPerSecond: Math.round(seededNumber(`${device.id}:rx`, 1200, 95000)),
|
||||
txBytesPerSecond: Math.round(seededNumber(`${device.id}:tx`, 700, 55000))
|
||||
},
|
||||
rackTemperature: device.rackTemperatureFile
|
||||
rackTemperature: rackTemperatureFile
|
||||
? {
|
||||
temperatureC: Number(seededNumber(`${device.id}:rack-temp`, 25, 37).toFixed(1)),
|
||||
sensor: "rack-top",
|
||||
updatedAt: new Date().toISOString(),
|
||||
ageSeconds: 0,
|
||||
sourceFile: device.rackTemperatureFile
|
||||
sourceFile: rackTemperatureFile
|
||||
}
|
||||
: null
|
||||
};
|
||||
@@ -340,6 +356,13 @@ function shellQuote(value) {
|
||||
return `'${String(value || "").replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function rackTemperatureFileForDevice(device) {
|
||||
const sourceId = config.rackTemperatureSourceDeviceId || "";
|
||||
const sourceMatches = sourceId && [device.id, device.host, device.name].includes(sourceId);
|
||||
if (sourceMatches) return config.rackTemperatureFile || "";
|
||||
return device.rackTemperatureFile || "";
|
||||
}
|
||||
|
||||
function networkRates(device, values) {
|
||||
const sampleKey = device.id || `${device.host}:${device.port || 22}`;
|
||||
const now = Date.now();
|
||||
@@ -470,7 +493,8 @@ async function sshMetrics(device) {
|
||||
];
|
||||
}
|
||||
|
||||
const remoteScript = `RACK_TEMPERATURE_FILE=${shellQuote(device.rackTemperatureFile || "")}\n${REMOTE_METRICS_SCRIPT}`;
|
||||
const rackTemperatureFile = rackTemperatureFileForDevice(device);
|
||||
const remoteScript = `RACK_TEMPERATURE_FILE=${shellQuote(rackTemperatureFile)}\n${REMOTE_METRICS_SCRIPT}`;
|
||||
const { stdout } = await runCommandWithInput(command, args, remoteScript, timeoutMs);
|
||||
const values = parseKeyValueOutput(stdout);
|
||||
const temp = toNumber(values.cpuTempC, 0);
|
||||
@@ -511,7 +535,7 @@ async function sshMetrics(device) {
|
||||
sensor: values.rackTemperatureSensor || "",
|
||||
updatedAt: values.rackTemperatureUpdatedAt || "",
|
||||
ageSeconds: toNumber(values.rackTemperatureAgeSeconds, null),
|
||||
sourceFile: device.rackTemperatureFile || ""
|
||||
sourceFile: rackTemperatureFile
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user