Agregar configuracion de sensores habilitados

main
EMOTIONS-HUNTER 1 month ago
parent 0771ed48c7
commit f94e8864b5

@ -35,6 +35,12 @@ ventana de procesamiento en vez de bloquear un segundo por sensor.
- Chart.js 4.5.1 y SheetJS 0.20.3 instalados localmente.
- Servicios systemd, configuración Nginx e instalador para Raspberry Pi.
## Configuracion parcial de sensores
- `config/runtime.json` guarda `enabledSensors`.
- Los sensores deshabilitados se muestran como `DESHABILITADO`.
- Un sensor deshabilitado no cuenta como `OFFLINE`, alarma ni riesgo global.
## Modos
- `EZO_MODE=demo`: adquisición y comandos simulados.

@ -77,15 +77,24 @@ La guía de preparación, detección I2C, calibración y prueba integral está e
## Sensores conectados parcialmente
Para pruebas con solo EZO-RTD conectado, limite la adquisicion al sensor de
temperatura:
temperatura desde el panel de configuracion o en `config/runtime.json`:
```json
{
"loggingRateSeconds": 1,
"enabledSensors": ["temperature"]
}
```
Tambien puede forzarlo temporalmente por entorno:
```bash
EZO_MODE=hardware EZO_ENABLED_SENSORS=temperature npm run acquire
```
En produccion puede dejar `EZO_ENABLED_SENSORS=temperature` en
`/etc/default/photobioreactor`. Cuando esten conectados los cuatro circuitos,
deje la variable vacia para consultar RTD, pH, DO y EC.
`/etc/default/photobioreactor` como override. Cuando esten conectados los cuatro
circuitos, deje la variable vacia y habilite RTD, pH, DO y EC desde el panel.
## Archivos de datos

@ -39,6 +39,22 @@ const SENSOR_ALIASES = {
do: 'do',
ec: 'ec'
};
const DEFAULT_ENABLED_SENSORS = Object.keys(SENSOR_FILES);
function normalizeEnabledSensors(value, fallback = DEFAULT_ENABLED_SENSORS) {
const source = Array.isArray(value)
? value
: String(value || '').split(',');
const sensors = [
...new Set(
source
.map((sensor) => SENSOR_ALIASES[String(sensor).trim().toLowerCase()])
.filter(Boolean)
)
];
return sensors.length > 0 ? sensors : [...fallback];
}
async function atomicWriteFile(filePath, content) {
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
@ -52,26 +68,47 @@ async function readRuntimeConfig() {
const rawConfig = await fsPromises.readFile(RUNTIME_CONFIG_FILE, 'utf8');
const config = JSON.parse(rawConfig);
const rate = Number(config.loggingRateSeconds);
const environmentSensors = String(process.env.EZO_ENABLED_SENSORS || '').trim();
return {
loggingRateSeconds: ALLOWED_LOGGING_RATES.has(rate) ? rate : 1
loggingRateSeconds: ALLOWED_LOGGING_RATES.has(rate) ? rate : 1,
enabledSensors: environmentSensors
? normalizeEnabledSensors(environmentSensors)
: normalizeEnabledSensors(config.enabledSensors)
};
} catch {
return { loggingRateSeconds: 1 };
return {
loggingRateSeconds: 1,
enabledSensors: normalizeEnabledSensors(process.env.EZO_ENABLED_SENSORS)
};
}
}
async function writeRuntimeConfig(rate) {
const numericRate = Number(rate);
async function writeRuntimeConfig(updates) {
const currentConfig = await readRuntimeConfig();
const numericRate = updates.rate === undefined
? currentConfig.loggingRateSeconds
: Number(updates.rate);
if (!ALLOWED_LOGGING_RATES.has(numericRate)) {
const error = new Error('Frecuencia no válida.');
const error = new Error('Frecuencia no valida.');
error.status = 400;
throw error;
}
const enabledSensors = updates.enabledSensors === undefined
? currentConfig.enabledSensors
: normalizeEnabledSensors(updates.enabledSensors, []);
if (enabledSensors.length === 0) {
const error = new Error('Debe habilitar al menos un sensor.');
error.status = 400;
throw error;
}
const config = {
loggingRateSeconds: numericRate,
enabledSensors,
updatedAt: new Date().toISOString()
};
await atomicWriteFile(
@ -90,15 +127,15 @@ function detectAcquisitionMode() {
if (requestedMode === 'hardware' && !hardwareReady) {
return {
mode: 'unavailable',
message: 'Se solicitó hardware, pero /dev/i2c-1 o EZO_ACQUIRE no está disponible.'
message: 'Se solicito hardware, pero /dev/i2c-1 o EZO_ACQUIRE no esta disponible.'
};
}
if (requestedMode === 'hardware' || (requestedMode === 'auto' && hardwareReady)) {
return { mode: 'hardware', message: 'Adquisición I2C real activa.' };
return { mode: 'hardware', message: 'Adquisicion I2C real activa.' };
}
return { mode: 'demo', message: 'Adquisición de demostración activa.' };
return { mode: 'demo', message: 'Adquisicion de demostracion activa.' };
}
function buildDemoReadings() {
@ -110,12 +147,8 @@ function buildDemoReadings() {
};
}
async function readHardwareSensors() {
const enabledSensors = String(process.env.EZO_ENABLED_SENSORS || '')
.split(',')
.map((sensor) => SENSOR_ALIASES[sensor.trim().toLowerCase()])
.filter(Boolean);
const helperArguments = ['/dev/i2c-1', ...new Set(enabledSensors)];
async function readHardwareSensors(enabledSensors) {
const helperArguments = ['/dev/i2c-1', ...enabledSensors];
const { stdout } = await execFileAsync(
ACQUISITION_HELPER,
helperArguments,
@ -124,7 +157,7 @@ async function readHardwareSensors() {
const readings = JSON.parse(stdout.trim());
if (!readings || typeof readings !== 'object') {
throw new Error('EZO_ACQUIRE devolvió una respuesta inválida.');
throw new Error('EZO_ACQUIRE devolvio una respuesta invalida.');
}
return readings;
@ -179,30 +212,59 @@ async function publishReading(sensorId, rawValue, timestamp, mode) {
return true;
}
async function publishDisabled(sensorId, timestamp) {
const sensor = SENSOR_FILES[sensorId];
await atomicWriteFile(
path.join(DATA_DIRECTORY, sensor.json),
`${JSON.stringify({
online: false,
disabled: true,
timestamp,
error: 'Sensor deshabilitado por configuracion'
})}\n`
);
}
async function collectReadings() {
const modeInfo = detectAcquisitionMode();
const runtimeConfig = await readRuntimeConfig();
const enabledSensorSet = new Set(runtimeConfig.enabledSensors);
if (modeInfo.mode === 'unavailable') {
throw new Error(modeInfo.message);
}
const readings = modeInfo.mode === 'hardware'
? await readHardwareSensors()
? await readHardwareSensors(runtimeConfig.enabledSensors)
: buildDemoReadings();
const timestamp = new Date().toISOString();
const results = await Promise.all(
Object.keys(SENSOR_FILES).map(async (sensorId) => ({
sensorId,
online: await publishReading(
Object.keys(SENSOR_FILES).map(async (sensorId) => {
if (!enabledSensorSet.has(sensorId)) {
await publishDisabled(sensorId, timestamp);
return { sensorId, online: false, disabled: true };
}
return {
sensorId,
readings[sensorId],
timestamp,
modeInfo.mode
)
}))
online: await publishReading(
sensorId,
readings[sensorId],
timestamp,
modeInfo.mode
),
disabled: false
};
})
);
return { mode: modeInfo.mode, timestamp, results };
return {
mode: modeInfo.mode,
timestamp,
results,
enabledSensors: runtimeConfig.enabledSensors
};
}
async function publishAllOffline(error) {
@ -226,11 +288,13 @@ async function publishAllOffline(error) {
module.exports = {
ACQUISITION_HELPER,
ALLOWED_LOGGING_RATES,
DEFAULT_ENABLED_SENSORS,
RUNTIME_CONFIG_FILE,
SENSOR_FILES,
atomicWriteFile,
collectReadings,
detectAcquisitionMode,
normalizeEnabledSensors,
publishAllOffline,
publishReading,
readRuntimeConfig,

@ -19,8 +19,9 @@ async function run() {
try {
const result = await collectReadings();
const onlineCount = result.results.filter((item) => item.online).length;
const expectedCount = result.enabledSensors.length;
console.log(
`[ACQUISITION] ${result.timestamp} ${result.mode}: ${onlineCount}/4 sensores.`
`[ACQUISITION] ${result.timestamp} ${result.mode}: ${onlineCount}/${expectedCount} sensores habilitados.`
);
} catch (error) {
console.error(`[ACQUISITION] ${error.message}`);

@ -30,6 +30,12 @@ const HISTORY_SENSOR_MAP = {
do: { file: 'do.csv', name: 'DO' },
ec: { file: 'ec.csv', name: 'EC' }
};
const HISTORY_SENSOR_IDS = {
temperature: 'rtd',
ph: 'ph',
do: 'do',
ec: 'ec'
};
app.use(cors());
app.use(express.json());
@ -120,8 +126,12 @@ app.get('/api/sensors/:type', async (req, res) => {
try {
if (sensorType === 'all') {
const runtimeConfig = await readRuntimeConfig();
const enabledHistorySensors = runtimeConfig.enabledSensors
.map((sensorId) => HISTORY_SENSOR_IDS[sensorId])
.filter(Boolean);
const histories = await Promise.all(
Object.keys(HISTORY_SENSOR_MAP).map(readSensorHistory)
enabledHistorySensors.map(readSensorHistory)
);
return res.json(
histories.flat().sort((left, right) =>
@ -158,12 +168,16 @@ app.post('/api/sensors/:type/command', async (req, res) => {
app.get('/api/config/logging', async (req, res) => {
const config = await readRuntimeConfig();
res.json({ success: true, rate: config.loggingRateSeconds });
res.json({
success: true,
rate: config.loggingRateSeconds,
enabledSensors: config.enabledSensors
});
});
app.post('/api/config/logging', async (req, res) => {
try {
const config = await writeRuntimeConfig(req.body.rate);
const config = await writeRuntimeConfig({ rate: req.body.rate });
console.log(
`[CONFIG] Frecuencia de adquisición: ${config.loggingRateSeconds}s`
);
@ -180,6 +194,39 @@ app.post('/api/config/logging', async (req, res) => {
}
});
app.get('/api/config/runtime', async (req, res) => {
const config = await readRuntimeConfig();
res.json({
success: true,
rate: config.loggingRateSeconds,
enabledSensors: config.enabledSensors,
availableSensors: Object.keys(HISTORY_FILES)
});
});
app.post('/api/config/runtime', async (req, res) => {
try {
const config = await writeRuntimeConfig({
rate: req.body.rate,
enabledSensors: req.body.enabledSensors
});
console.log(
`[CONFIG] Sensores activos: ${config.enabledSensors.join(', ')}`
);
res.json({
success: true,
rate: config.loggingRateSeconds,
enabledSensors: config.enabledSensors,
message: 'La configuracion sera aplicada por el recolector en el siguiente ciclo.'
});
} catch (error) {
res.status(error.status || 500).json({
success: false,
error: error.message
});
}
});
app.post('/api/history/clear', async (req, res) => {
try {
await fs.mkdir(LOGS_DIRECTORY, { recursive: true });

@ -1,3 +1,6 @@
{
"loggingRateSeconds": 1
"loggingRateSeconds": 1,
"enabledSensors": [
"temperature"
]
}

@ -129,6 +129,11 @@ h1 {
border-color: rgba(125, 135, 144, 0.45);
}
.metric-card.disabled {
border-color: rgba(125, 135, 144, 0.28);
background: #f8faf9;
}
.metric-card.normal {
border-color: rgba(22, 138, 74, 0.45);
}
@ -185,6 +190,10 @@ h1 {
color: var(--offline-neutral);
}
.metric-state.disabled {
color: var(--offline-neutral);
}
.metric-reading {
display: flex;
align-items: baseline;
@ -307,6 +316,10 @@ h1 {
color: var(--offline-neutral);
}
.sensor-health .disabled {
color: var(--offline-neutral);
}
.sensor-health .warning {
color: var(--warning);
}
@ -544,6 +557,29 @@ h1 {
color: var(--muted) !important;
}
.sensor-config-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin: 0;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
}
.sensor-config-group legend {
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.sensor-config-group label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.terminal-card,
.calibration-card {
overflow: hidden;

@ -13,13 +13,17 @@ let dashboardUpdateInProgress = false;
const stateTranslations = {
"NORMAL": "NORMAL",
"OFFLINE": "DESCONECTADO",
"DISABLED": "DESHABILITADO",
"WARNING": "ADVERTENCIA",
"CRITICAL": "CRÍTICO"
};
let currentEnabledSensorIds = new Set(["temperature", "ph", "do", "ec"]);
const sensors = [
{
id: "temperature",
sensorId: "temperature",
name: "Temperatura",
file: "../data/EZORTD.json",
key: "temperature",
@ -73,6 +77,7 @@ const historicalCharts = [
},
{
id: "ph-history",
sensorId: "ph",
title: "pH vs Tiempo",
file: "../logs/ph.csv",
valueKey: "ph",
@ -83,6 +88,7 @@ const historicalCharts = [
},
{
id: "do-history",
sensorId: "do",
title: "Oxígeno Disuelto vs Tiempo",
file: "../logs/do.csv",
valueKey: "do",
@ -93,6 +99,7 @@ const historicalCharts = [
},
{
id: "ec-history",
sensorId: "ec",
title: "Conductividad vs Tiempo",
file: "../logs/ec.csv",
valueKey: "ec",
@ -106,6 +113,16 @@ const historicalCharts = [
const chartInstances = new Map();
async function readSensor(sensor) {
if (!currentEnabledSensorIds.has(sensor.id)) {
return {
...sensor,
disabled: true,
online: false,
numericValue: null,
value: "--"
};
}
try {
const response = await fetch(`${sensor.file}?t=${Date.now()}`, {
cache: "no-store"
@ -138,6 +155,7 @@ async function readSensor(sensor) {
return {
...sensor,
disabled: false,
online: true,
numericValue: rawValue,
timestamp: data.timestamp || null,
@ -148,6 +166,7 @@ async function readSensor(sensor) {
return {
...sensor,
disabled: false,
online: false,
numericValue: null,
value: "DESCONECTADO"
@ -166,25 +185,27 @@ function setSensorState(result) {
// Traducir el estado para mostrar en pantalla, manteniendo la clase en inglés
stateElement.textContent = stateTranslations[state] || state;
["online", "normal", "warning", "critical", "offline"].forEach((className) => {
["online", "normal", "warning", "critical", "offline", "disabled"].forEach((className) => {
stateElement.classList.toggle(className, className === stateClass);
cardElement.classList.toggle(className, className === stateClass);
});
}
function renderSystemStatus(results) {
const activeSensors = results.filter((result) => result.online).length;
const offlineSensors = results.length - activeSensors;
const enabledResults = results.filter((result) => !result.disabled);
const activeSensors = enabledResults.filter((result) => result.online).length;
const offlineSensors = enabledResults.length - activeSensors;
const criticalSensors = results.filter((result) => result.alarmState === "CRITICAL").length;
const warningSensors = results.filter((result) => result.alarmState === "WARNING").length;
const allNormal = results.every((result) => result.alarmState === "NORMAL");
const allNormal = enabledResults.length > 0 &&
enabledResults.every((result) => result.alarmState === "NORMAL");
const anyOnline = activeSensors > 0;
const overallDot = document.getElementById("overall-dot");
const overallStatus = document.getElementById("overall-status");
const healthList = document.getElementById("sensor-health");
document.getElementById("active-count").textContent =
`${activeSensors} / ${results.length}`;
`${activeSensors} / ${enabledResults.length}`;
document.getElementById("offline-count").textContent = String(offlineSensors);
document.getElementById("last-update").textContent =
new Date().toLocaleString();
@ -230,10 +251,12 @@ async function updateDashboard() {
dashboardUpdateInProgress = true;
try {
const [results, alarmConfig] = await Promise.all([
Promise.all(sensors.map(readSensor)),
const [runtimeConfig, alarmConfig] = await Promise.all([
readRuntimeConfig(),
readAlarmConfig()
]);
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
const results = await Promise.all(sensors.map(readSensor));
const evaluatedResults = results.map((result) =>
evaluateSensorAlarm(result, alarmConfig.thresholds)
);
@ -246,6 +269,25 @@ async function updateDashboard() {
}
}
async function readRuntimeConfig() {
try {
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
const result = await response.json();
if (!response.ok || !Array.isArray(result.enabledSensors)) {
throw new Error("Runtime configuration unavailable");
}
return result;
} catch (error) {
console.warn("No se pudo cargar la configuracion de sensores:", error);
return {
rate: currentLoggingRateSeconds,
enabledSensors: [...currentEnabledSensorIds]
};
}
}
async function readAlarmConfig() {
try {
const response = await fetch(`${ALARM_CONFIG_FILE}?t=${Date.now()}`, {
@ -273,6 +315,14 @@ async function readAlarmConfig() {
}
function evaluateSensorAlarm(sensorResult, thresholds) {
if (sensorResult.disabled) {
return {
...sensorResult,
alarmState: "DISABLED",
alarmMessage: "Sensor no habilitado en la configuracion actual"
};
}
if (!sensorResult.online) {
return {
...sensorResult,
@ -342,7 +392,7 @@ function buildAlarmMessage(sensorResult, limits, alarmType) {
function getAlarmEvents(results) {
return results
.filter((result) => result.alarmState !== "NORMAL")
.filter((result) => !["NORMAL", "DISABLED"].includes(result.alarmState))
.map((result) => ({
sensorId: result.id,
sensorName: result.name,
@ -509,13 +559,28 @@ function formatTimestamp(rawTimestamp) {
return rawTimestamp;
}
function setChartAvailability(chartConfig, hasData) {
function setChartAvailability(chartConfig, hasData, message = "No hay datos históricos disponibles") {
const emptyElement = document.getElementById(chartConfig.emptyElement);
const frameElement = emptyElement.closest(".chart-frame");
emptyElement.textContent = message;
frameElement.classList.toggle("empty", !hasData);
}
function clearHistoricalChart(chartConfig, message) {
const existingChart = chartInstances.get(chartConfig.id);
if (existingChart) {
existingChart.data.labels = [];
existingChart.data.datasets.forEach((dataset) => {
dataset.data = [];
});
existingChart.update("none");
}
setChartAvailability(chartConfig, false, message);
}
function buildChartDataset(chartConfig, points) {
return {
labels: points.map((point) => point.label),
@ -605,14 +670,23 @@ function renderHistoricalChart(chartConfig, points) {
}
async function updateHistoricalTrends() {
const runtimeConfig = await readRuntimeConfig();
currentEnabledSensorIds = new Set(runtimeConfig.enabledSensors);
const chartData = await Promise.all(
historicalCharts.map(async (chartConfig) => ({
chartConfig,
points: await readHistoricalData(chartConfig)
points: currentEnabledSensorIds.has(chartConfig.sensorId)
? await readHistoricalData(chartConfig)
: []
}))
);
chartData.forEach(({ chartConfig, points }) => {
if (!currentEnabledSensorIds.has(chartConfig.sensorId)) {
clearHistoricalChart(chartConfig, "Sensor deshabilitado");
return;
}
renderHistoricalChart(chartConfig, points);
});
}
@ -669,11 +743,15 @@ window.updateLoggingRate = async function () {
async function loadLoggingRate() {
try {
const response = await fetch('/api/config/logging', { cache: 'no-store' });
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
const result = await response.json();
if (!response.ok) return;
currentLoggingRateSeconds = result.rate;
if (Array.isArray(result.enabledSensors)) {
currentEnabledSensorIds = new Set(result.enabledSensors);
setEnabledSensorControls(result.enabledSensors);
}
document.getElementById("data-logging-rate").value = String(result.rate);
document.getElementById("sampling-interval").textContent =
`${result.rate} ${result.rate === 1 ? "segundo" : "segundos"}`;
@ -682,6 +760,51 @@ async function loadLoggingRate() {
}
}
function getSelectedEnabledSensors() {
return [...document.querySelectorAll('input[name="enabled-sensor"]:checked')]
.map((input) => input.value);
}
function setEnabledSensorControls(enabledSensors) {
const enabled = new Set(enabledSensors);
document.querySelectorAll('input[name="enabled-sensor"]').forEach((input) => {
input.checked = enabled.has(input.value);
});
}
window.updateEnabledSensors = async function () {
const enabledSensors = getSelectedEnabledSensors();
if (enabledSensors.length === 0) {
alert("Debe quedar al menos un sensor habilitado.");
setEnabledSensorControls([...currentEnabledSensorIds]);
return;
}
try {
const response = await fetch('/api/config/runtime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: currentLoggingRateSeconds,
enabledSensors
})
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || "No se pudo actualizar la configuracion");
currentEnabledSensorIds = new Set(result.enabledSensors);
setEnabledSensorControls(result.enabledSensors);
await updateDashboard();
alert(`Sensores habilitados: ${result.enabledSensors.join(", ")}`);
} catch (error) {
console.error("Error configurando sensores habilitados:", error);
alert("No se pudo actualizar la configuracion de sensores.");
}
};
loadLoggingRate();
window.clearHistoricalData = async function () {

@ -2,6 +2,7 @@
async function handleExportExcel() {
try {
const enabledSensors = await getEnabledSensorsForExport();
console.log("[MOCK] Iniciando generación de reporte Excel...");
// 1. Crear un nuevo libro de trabajo (Workbook) en blanco
@ -9,11 +10,15 @@ async function handleExportExcel() {
// Definimos los sensores y el nombre que tendrá cada pestaña en el Excel
const sensors = [
{ id: 'rtd', sheetName: 'Temperatura_RTD' },
{ id: 'ph', sheetName: 'Sensor_pH' },
{ id: 'do', sheetName: 'Oxigeno_DO' },
{ id: 'ec', sheetName: 'Conductividad_EC' }
];
{ id: 'rtd', sensorId: 'temperature', sheetName: 'Temperatura_RTD' },
{ id: 'ph', sensorId: 'ph', sheetName: 'Sensor_pH' },
{ id: 'do', sensorId: 'do', sheetName: 'Oxigeno_DO' },
{ id: 'ec', sensorId: 'ec', sheetName: 'Conductividad_EC' }
].filter((sensor) => enabledSensors.has(sensor.sensorId));
if (sensors.length === 0) {
throw new Error('No hay sensores habilitados para exportar.');
}
// 2. Iterar sobre cada sensor, obtener sus datos y crear su hoja
for (const sensor of sensors) {

@ -26,11 +26,39 @@ function escapeCsvValue(value) {
return `"${text.replace(/"/g, '""')}"`;
}
async function getEnabledSensorsForExport() {
const response = await fetch('/api/config/runtime', { cache: 'no-store' });
const config = await response.json();
if (!response.ok || !Array.isArray(config.enabledSensors)) {
throw new Error('No se pudo leer la configuracion de sensores.');
}
return new Set(config.enabledSensors);
}
function normalizeExportSensor(sensorType) {
if (sensorType === 'rtd') return 'temperature';
return sensorType;
}
async function assertSensorExportEnabled(sensorType) {
if (sensorType === 'all') return;
const enabledSensors = await getEnabledSensorsForExport();
const sensorId = normalizeExportSensor(sensorType);
if (!enabledSensors.has(sensorId)) {
throw new Error(`El sensor ${sensorType.toUpperCase()} esta deshabilitado.`);
}
}
/**
* Manejador principal del evento click de los botones
*/
async function handleExport(sensorType) {
try {
await assertSensorExportEnabled(sensorType);
// 1. Obtener datos (Mock actual, fetch real en el futuro)
const data = await fetchHistoricalData(sensorType);

@ -162,6 +162,17 @@
</select>
</div>
<fieldset class="sensor-config-group">
<legend>Sensores habilitados</legend>
<label><input type="checkbox" name="enabled-sensor" value="temperature"> RTD</label>
<label><input type="checkbox" name="enabled-sensor" value="ph"> pH</label>
<label><input type="checkbox" name="enabled-sensor" value="do"> DO</label>
<label><input type="checkbox" name="enabled-sensor" value="ec"> EC</label>
<button class="btn-command" type="button" onclick="updateEnabledSensors()">
Aplicar sensores
</button>
</fieldset>
<div style="margin-top: auto;">
<button class="btn-command" onclick="clearHistoricalData()"
style="padding: 8px 15px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">

@ -59,6 +59,35 @@ test('returns real CSV history for all sensors', async () => {
assert.equal(typeof data[0].Valor, 'number');
});
test('all history export respects enabled sensors', async () => {
fs.writeFileSync(
path.join(temporaryLogs, 'temperature.csv'),
'timestamp,value\n2026-06-22T12:00:00.000Z,25.1\n'
);
fs.writeFileSync(
path.join(temporaryLogs, 'ph.csv'),
'timestamp,value\n2026-06-22T12:00:00.000Z,7.2\n'
);
fs.writeFileSync(path.join(temporaryLogs, 'do.csv'), 'timestamp,value\n');
fs.writeFileSync(path.join(temporaryLogs, 'ec.csv'), 'timestamp,value\n');
await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: 1,
enabledSensors: ['temperature']
})
});
const response = await fetch(`${baseUrl}/api/sensors/all`);
const data = await response.json();
assert.equal(response.status, 200);
assert.equal(data.length, 1);
assert.equal(data[0].Sensor, 'RTD');
});
test('serves the dashboard in local development mode', async () => {
const response = await fetch(`${baseUrl}/`);
const html = await response.text();
@ -123,7 +152,47 @@ test('persists logging rates used by acquisition', async () => {
assert.equal(JSON.parse(fs.readFileSync(temporaryRuntime)).loggingRateSeconds, 5);
});
test('persists enabled sensors used by acquisition', async () => {
const response = await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: 1,
enabledSensors: ['temperature']
})
});
const data = await response.json();
assert.equal(response.status, 200);
assert.deepEqual(data.enabledSensors, ['temperature']);
const readResponse = await fetch(`${baseUrl}/api/config/runtime`);
const readData = await readResponse.json();
assert.deepEqual(readData.enabledSensors, ['temperature']);
});
test('rejects empty enabled sensor configuration', async () => {
const response = await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: 1,
enabledSensors: []
})
});
assert.equal(response.status, 400);
});
test('demo acquisition writes live JSON and CSV files', async () => {
await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: 1,
enabledSensors: ['temperature', 'ph', 'do', 'ec']
})
});
const result = await collectReadings();
assert.equal(result.mode, 'demo');
@ -140,6 +209,25 @@ test('demo acquisition writes live JSON and CSV files', async () => {
}
});
test('disabled sensors are published as disabled without CSV rows', async () => {
await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rate: 1,
enabledSensors: ['temperature']
})
});
fs.rmSync(path.join(temporaryLogs, 'ph.csv'), { force: true });
const result = await collectReadings();
const phData = JSON.parse(fs.readFileSync(path.join(temporaryData, 'EZOPH.json')));
assert.equal(result.results.find((item) => item.sensorId === 'ph').disabled, true);
assert.equal(phData.disabled, true);
assert.equal(fs.existsSync(path.join(temporaryLogs, 'ph.csv')), false);
});
test('hardware null readings are marked offline and not appended as zero', async () => {
const timestamp = new Date().toISOString();
const csvPath = path.join(temporaryLogs, 'ph.csv');

@ -14,7 +14,10 @@ test('dashboard starts live polling and exposes required controls', () => {
const dashboard = read('frontend/dashboard.js');
assert.match(html, /id="history-interval"/);
assert.match(html, /name="enabled-sensor"/);
assert.match(dashboard, /updateDashboard\(\);/);
assert.match(dashboard, /updateEnabledSensors/);
assert.match(dashboard, /\/api\/config\/runtime/);
assert.match(
dashboard,
/setInterval\(updateDashboard,\s*POLLING_INTERVAL_MS\)/

Loading…
Cancel
Save