diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 73a4746..c8126cf 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -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. diff --git a/README.md b/README.md index cb35e0b..c60e265 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/acquisition-service.js b/api/acquisition-service.js index 5c5cdb9..1a9bd89 100644 --- a/api/acquisition-service.js +++ b/api/acquisition-service.js @@ -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, diff --git a/api/acquisition.js b/api/acquisition.js index 46389fe..112ee98 100644 --- a/api/acquisition.js +++ b/api/acquisition.js @@ -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}`); diff --git a/api/server.js b/api/server.js index dd27a31..6711b46 100644 --- a/api/server.js +++ b/api/server.js @@ -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 }); diff --git a/config/runtime.json b/config/runtime.json index e136fcb..d11b47b 100644 --- a/config/runtime.json +++ b/config/runtime.json @@ -1,3 +1,6 @@ { - "loggingRateSeconds": 1 + "loggingRateSeconds": 1, + "enabledSensors": [ + "temperature" + ] } diff --git a/frontend/dashboard.css b/frontend/dashboard.css index afd6244..6dd43e3 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -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; diff --git a/frontend/dashboard.js b/frontend/dashboard.js index e5f4a38..3689d3f 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -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 () { diff --git a/frontend/export-excel.js b/frontend/export-excel.js index 75ef6db..fd3010e 100644 --- a/frontend/export-excel.js +++ b/frontend/export-excel.js @@ -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) { diff --git a/frontend/export-service.js b/frontend/export-service.js index fb225f8..ef13f34 100644 --- a/frontend/export-service.js +++ b/frontend/export-service.js @@ -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); diff --git a/frontend/index.html b/frontend/index.html index 4787f98..6925c0f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -162,6 +162,17 @@ +
+ Sensores habilitados + + + + + +
+