From 684417b35c471c518b6889a4c0d637b6a2712a24 Mon Sep 17 00:00:00 2001 From: aleclevy <87339235+aleclevy@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:09:12 -0800 Subject: [PATCH] Revert "feat: query sensors only if they exist, batch sensor data requests" --- backend/api/__init__.py | 2 - backend/api/resources/cell_sensors.py | 42 --- backend/api/resources/sensor_data.py | 32 +- backend/api/schemas/get_sensor_data_schema.py | 3 +- frontend/src/pages/dashboard/Dashboard.jsx | 303 ++++++++---------- .../dashboard/components/UnifiedChart.jsx | 157 +++++++-- .../dashboard/components/chartConfigs.js | 122 ------- frontend/src/services/sensor.js | 14 - 8 files changed, 263 insertions(+), 412 deletions(-) delete mode 100644 backend/api/resources/cell_sensors.py delete mode 100644 frontend/src/pages/dashboard/components/chartConfigs.js diff --git a/backend/api/__init__.py b/backend/api/__init__.py index ecbeba25..57588aa7 100644 --- a/backend/api/__init__.py +++ b/backend/api/__init__.py @@ -170,7 +170,6 @@ def handle_unsubscribe_cells(data): from .resources.cell_tags import CellTags, CellTagDetail, CellsByTag from .resources.cell_users import CellUsers, CellUserDetail, CellByUser, CellShare from .resources.logger import Logger - from .resources.cell_sensors import CellSensors from .auth.routes import auth @@ -184,7 +183,6 @@ def handle_unsubscribe_cells(data): api.add_resource(SensorData, "/sensor/") api.add_resource(SensorData_Json, "/sensor_json/") api.add_resource(DataAvailability, "/data-availability/") - api.add_resource(CellSensors, "/cell-sensors/") api.add_resource(Session_r, "/session") api.add_resource(User_Data, "/user") api.add_resource(Status, "/status/") diff --git a/backend/api/resources/cell_sensors.py b/backend/api/resources/cell_sensors.py deleted file mode 100644 index ae888507..00000000 --- a/backend/api/resources/cell_sensors.py +++ /dev/null @@ -1,42 +0,0 @@ -from flask import request -from flask_restful import Resource -from ..models.sensor import Sensor - - -class CellSensors(Resource): - def get(self): - """Get distinct sensor names that exist for specified cells - - Query Parameters: - - cell_ids: Comma-separated list of cell IDs - - Returns: - - Dict mapping cell_id (string) to list of sensor names that have data - """ - cell_ids_param = request.args.get("cell_ids") - - if cell_ids_param is None: - return {"error": "cell_ids parameter is required"}, 400 - - try: - cell_ids = [ - int(id.strip()) for id in cell_ids_param.split(",") if id.strip() - ] - except ValueError: - return {"error": "Invalid cell_ids format"}, 400 - - if not cell_ids: - return {"error": "At least one valid cell_id is required"}, 400 - - rows = ( - Sensor.query.filter(Sensor.cell_id.in_(cell_ids)) - .with_entities(Sensor.cell_id, Sensor.name) - .distinct() - .all() - ) - - result = {} - for cell_id, name in rows: - result.setdefault(str(cell_id), []).append(name) - - return result, 200 diff --git a/backend/api/resources/sensor_data.py b/backend/api/resources/sensor_data.py index 85575e63..8b524a5c 100644 --- a/backend/api/resources/sensor_data.py +++ b/backend/api/resources/sensor_data.py @@ -41,42 +41,14 @@ class SensorData(Resource): get_sensor_data_schema = GetSensorDataSchema() def get(self): - """Gets specified sensor data - - Supports two modes: - - Single cell: pass cellId (int) — returns sensor data object directly - - Batch cells: pass cellIds (comma-separated ints) — returns {cell_id: data_obj} - """ + """Gets specified sensor data""" # get args v_args = self.get_sensor_data_schema.load(request.args) stream = v_args.get("stream", False) resample = v_args.get("resample", "hour") - cell_ids_raw = v_args.get("cellIds") - if cell_ids_raw: - # Batch mode: return data for all requested cells in one response - try: - cell_ids = [ - int(x.strip()) for x in cell_ids_raw.split(",") if x.strip() - ] - except ValueError: - return {"error": "Invalid cellIds format"}, 400 - - result = {} - for cid in cell_ids: - result[str(cid)] = Sensor.get_sensor_data_obj( - name=v_args["name"], - cell_id=cid, - measurement=v_args["measurement"], - resample=resample, - start_time=v_args.get("startTime"), - end_time=v_args.get("endTime"), - stream=stream, - ) - return jsonify(result) - - # Single cell mode (existing behaviour) + # get data sensor_data_obj = Sensor.get_sensor_data_obj( name=v_args["name"], cell_id=v_args["cellId"], diff --git a/backend/api/schemas/get_sensor_data_schema.py b/backend/api/schemas/get_sensor_data_schema.py index 1fed7557..2c9590da 100644 --- a/backend/api/schemas/get_sensor_data_schema.py +++ b/backend/api/schemas/get_sensor_data_schema.py @@ -4,8 +4,7 @@ class GetSensorDataSchema(ma.SQLAlchemySchema): """validates get request for sensor data""" - cellId = ma.Int(required=False) - cellIds = ma.String(required=False, load_default=None) + cellId = ma.Int() name = ma.String() measurement = ma.String() resample = ma.String(required=False) diff --git a/frontend/src/pages/dashboard/Dashboard.jsx b/frontend/src/pages/dashboard/Dashboard.jsx index 4e62cf87..af57dd68 100644 --- a/frontend/src/pages/dashboard/Dashboard.jsx +++ b/frontend/src/pages/dashboard/Dashboard.jsx @@ -7,7 +7,6 @@ import { useSmartDateRange } from '../../hooks/useSmartDateRange'; import useAxiosPrivate from '../../auth/hooks/useAxiosPrivate'; import useAuth from '../../auth/hooks/useAuth'; import { useCells } from '../../services/cell'; -import { getCellSensors } from '../../services/sensor'; import ArchiveModal from './components/ArchiveModal'; import BackBtn from './components/BackBtn'; import CellSelect from './components/CellSelect'; @@ -16,7 +15,6 @@ import DownloadBtn from './components/DownloadBtn'; import PowerCharts from './components/PowerCharts'; import StreamToggle from './components/StreamToggle'; import TerosCharts from './components/TerosCharts'; -import { CHART_CONFIGS } from './components/chartConfigs'; import UnifiedChart from './components/UnifiedChart'; import { io } from 'socket.io-client'; import TopNav from '../../components/TopNav'; @@ -36,7 +34,6 @@ function Dashboard() { const [powerHasData, setPowerHasData] = useState(false); const [terosHasData, setTerosHasData] = useState(false); const [liveData, setLiveData] = useState([]); - const [availableSensors, setAvailableSensors] = useState(null); // Background streaming data - always collecting in background const backgroundStreamDataRef = useRef([]); @@ -426,26 +423,6 @@ function Dashboard() { } }, [loggedIn, stream]); - // Fetch which sensor names exist for the selected cells - useEffect(() => { - if (selectedCells.length === 0) { - setAvailableSensors(null); - return; - } - const cellIds = selectedCells.map((c) => c.id); - getCellSensors(cellIds) - .then((data) => setAvailableSensors(data)) - .catch(() => setAvailableSensors(null)); - }, [selectedCells]); - - // Helper: returns true if the sensor_name for the given chart type exists - // for at least one selected cell (or while the availability check is still loading) - const sensorVisible = (type) => { - if (!availableSensors) return true; - const sensorName = CHART_CONFIGS[type]?.sensor_name; - return selectedCells.some((c) => (availableSensors[String(c.id)] ?? []).includes(sensorName)); - }; - // Check if top section should be hidden const topSectionHasData = powerHasData || terosHasData; @@ -661,160 +638,132 @@ function Dashboard() { justifyContent='spaced-evently' sx={{ width: '95%', boxSizing: 'border-box' }} > - {sensorVisible('power_voltage') && ( - - )} - {sensorVisible('power_current') && ( - - )} - {sensorVisible('teros12_vwc') && ( - - )} - {sensorVisible('teros12_vwc_adj') && ( - - )} - {sensorVisible('teros12_temp') && ( - - )} - {sensorVisible('teros12_ec') && ( - - )} - {sensorVisible('soilPot') && ( - - )} - {sensorVisible('presHum') && ( - - )} - {sensorVisible('sensor') && ( - - )} - {sensorVisible('co2') && ( - - )} - {sensorVisible('temperature') && ( - - )} - {sensorVisible('soilHum') && ( - - )} - {sensorVisible('waterPress') && ( - - )} - {sensorVisible('waterFlow') && ( - - )} + + + + + + + + + + + + + + )} diff --git a/frontend/src/pages/dashboard/components/UnifiedChart.jsx b/frontend/src/pages/dashboard/components/UnifiedChart.jsx index 64d2fcd2..4be93755 100644 --- a/frontend/src/pages/dashboard/components/UnifiedChart.jsx +++ b/frontend/src/pages/dashboard/components/UnifiedChart.jsx @@ -2,15 +2,137 @@ import { Grid } from '@mui/material'; import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; import { React, useEffect, useState, useRef } from 'react'; -import { getSensorDataBatch } from '../../../services/sensor'; +import { getSensorData } from '../../../services/sensor'; import UniversalChart from '../../../charts/UniversalChart'; -import { CHART_CONFIGS } from './chartConfigs'; import { extractUnifiedStreamValue, matchesSensorStreamType, normalizeUnifiedStreamValue, } from './unifiedChartUtils'; +const CHART_CONFIGS = { + power_voltage: { + sensor_name: 'POWER_VOLTAGE', + measurements: ['Voltage'], + units: ['mV'], + axisIds: ['y'], + chartId: 'powerVoltage', + }, + power_current: { + sensor_name: 'POWER_CURRENT', + measurements: ['Current'], + units: ['uA'], + axisIds: ['y'], + chartId: 'powerCurrent', + }, + teros12_vwc: { + sensor_name: 'TEROS12_VWC', + measurements: ['Volumetric Water Content (Raw)'], + units: ['raw'], + axisIds: ['y'], + chartId: 'teros12VWC', + }, + teros12_vwc_adj: { + sensor_name: 'TEROS12_VWC_ADJ', + measurements: ['Volumetric Water Content'], + units: ['%'], + axisIds: ['y'], + axisPolicy: 'vwcPercent', + chartId: 'teros12VWCADJ', + }, + teros12_temp: { + sensor_name: 'TEROS12_TEMP', + measurements: ['Temperature'], + units: ['°C'], + axisIds: ['y'], + chartId: 'teros12Temp', + }, + teros12_ec: { + sensor_name: 'TEROS12_EC', + measurements: ['Electrical Conductivity'], + units: ['µS/cm'], + axisIds: ['y'], + chartId: 'teros12EC', + }, + temperature: { + sensor_name: 'bme280', + measurements: ['temperature'], + units: ['°C'], + axisIds: ['y'], + chartId: 'bme280', + }, + bme280Temperature: { + sensor_name: 'BME280_TEMP', + measurements: ['Temperature'], + units: ['°C'], + axisIds: ['y'], + chartId: 'bme280temp', + }, + co2: { + sensor_name: 'co2', + measurements: ['co2'], + units: ['ppm'], + axisIds: ['y'], + chartId: 'co2', + }, + presHum: { + sensor_name: 'bme280', + measurements: ['pressure', 'humidity'], + units: ['kPa', '%'], + axisIds: ['pressureAxis', 'humidityAxis'], + chartId: 'presHum', + }, + bme280Pressure: { + sensor_name: 'BME280_PRESSURE', + measurements: ['Pressure'], + units: ['kPa'], + axisIds: ['pressureAxis'], + chartId: 'bme280pressure', + }, + bme280Humidity: { + sensor_name: 'BME280_HUMIDITY', + measurements: ['Humidity'], + units: ['%'], + axisIds: ['humidityAxis'], + chartId: 'bme280humidity', + }, + sensor: { + sensor_name: 'phytos31', + measurements: ['dielectric_permittivity'], + units: ['1 (unitless)'], + axisIds: ['y'], + chartId: 'sensor', + }, + soilPot: { + sensor_name: 'teros21', + measurements: ['soil_water_potential'], + units: ['kPa'], + axisIds: ['y'], + chartId: 'soilPot', + }, + soilHum: { + sensor_name: 'sen0308', + measurements: ['humidity'], + units: ['%'], + axisIds: ['y'], + chartId: 'soilHum', + }, + waterPress: { + sensor_name: 'sen0257', + measurements: ['pressure'], + units: ['kPa'], + axisIds: ['y'], + chartId: 'waterPress', + }, + waterFlow: { + sensor_name: 'yfs210c', + measurements: ['flow'], + units: ['L/Min'], + axisIds: ['y'], + chartId: 'waterFlow', + }, +}; + function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, processedData, onDataStatusChange }) { const [resample, setResample] = useState('hour'); const chartSettings = { @@ -39,27 +161,16 @@ function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, proce async function getCellChartData() { const data = {}; - const cellIds = cells.map((c) => c.id); - cells.forEach(({ id, name }) => { - data[id] = { name }; - }); - - for (const meas of measurements) { - const batchResult = await getSensorDataBatch( - sensor_name, - cellIds, - meas, - startDate.toHTTP(), - endDate.toHTTP(), - resample, - ); - for (const { id } of cells) { - data[id][meas] = batchResult[String(id)] ?? { - timestamp: [], - data: [], - measurement: '', - unit: '', - type: '', + // Always fetch data for all selected cells when cells change + let loadCells = cells; + for (const { id, name } of loadCells) { + data[id] = { + name: name, + }; + for (const meas of measurements) { + data[id] = { + ...data[id], + [meas]: await getSensorData(sensor_name, id, meas, startDate.toHTTP(), endDate.toHTTP(), resample), }; } } diff --git a/frontend/src/pages/dashboard/components/chartConfigs.js b/frontend/src/pages/dashboard/components/chartConfigs.js deleted file mode 100644 index 121247ad..00000000 --- a/frontend/src/pages/dashboard/components/chartConfigs.js +++ /dev/null @@ -1,122 +0,0 @@ -export const CHART_CONFIGS = { - power_voltage: { - sensor_name: 'POWER_VOLTAGE', - measurements: ['Voltage'], - units: ['mV'], - axisIds: ['y'], - chartId: 'powerVoltage', - }, - power_current: { - sensor_name: 'POWER_CURRENT', - measurements: ['Current'], - units: ['uA'], - axisIds: ['y'], - chartId: 'powerCurrent', - }, - teros12_vwc: { - sensor_name: 'TEROS12_VWC', - measurements: ['Volumetric Water Content (Raw)'], - units: ['raw'], - axisIds: ['y'], - chartId: 'teros12VWC', - }, - teros12_vwc_adj: { - sensor_name: 'TEROS12_VWC_ADJ', - measurements: ['Volumetric Water Content'], - units: ['%'], - axisIds: ['y'], - axisPolicy: 'vwcPercent', - chartId: 'teros12VWCADJ', - }, - teros12_temp: { - sensor_name: 'TEROS12_TEMP', - measurements: ['Temperature'], - units: ['°C'], - axisIds: ['y'], - chartId: 'teros12Temp', - }, - teros12_ec: { - sensor_name: 'TEROS12_EC', - measurements: ['Electrical Conductivity'], - units: ['µS/cm'], - axisIds: ['y'], - chartId: 'teros12EC', - }, - temperature: { - sensor_name: 'bme280', - measurements: ['temperature'], - units: ['°C'], - axisIds: ['y'], - chartId: 'bme280', - }, - bme280Temperature: { - sensor_name: 'BME280_TEMP', - measurements: ['Temperature'], - units: ['°C'], - axisIds: ['y'], - chartId: 'bme280temp', - }, - co2: { - sensor_name: 'co2', - measurements: ['co2'], - units: ['ppm'], - axisIds: ['y'], - chartId: 'co2', - }, - presHum: { - sensor_name: 'bme280', - measurements: ['pressure', 'humidity'], - units: ['kPa', '%'], - axisIds: ['pressureAxis', 'humidityAxis'], - chartId: 'presHum', - }, - bme280Pressure: { - sensor_name: 'BME280_PRESSURE', - measurements: ['Pressure'], - units: ['kPa'], - axisIds: ['pressureAxis'], - chartId: 'bme280pressure', - }, - bme280Humidity: { - sensor_name: 'BME280_HUMIDITY', - measurements: ['Humidity'], - units: ['%'], - axisIds: ['humidityAxis'], - chartId: 'bme280humidity', - }, - sensor: { - sensor_name: 'phytos31', - measurements: ['dielectric_permittivity'], - units: ['1 (unitless)'], - axisIds: ['y'], - chartId: 'sensor', - }, - soilPot: { - sensor_name: 'teros21', - measurements: ['soil_water_potential'], - units: ['kPa'], - axisIds: ['y'], - chartId: 'soilPot', - }, - soilHum: { - sensor_name: 'sen0308', - measurements: ['humidity'], - units: ['%'], - axisIds: ['y'], - chartId: 'soilHum', - }, - waterPress: { - sensor_name: 'sen0257', - measurements: ['pressure'], - units: ['kPa'], - axisIds: ['y'], - chartId: 'waterPress', - }, - waterFlow: { - sensor_name: 'yfs210c', - measurements: ['flow'], - units: ['L/Min'], - axisIds: ['y'], - chartId: 'waterFlow', - }, -}; diff --git a/frontend/src/services/sensor.js b/frontend/src/services/sensor.js index 99693beb..109792d6 100644 --- a/frontend/src/services/sensor.js +++ b/frontend/src/services/sensor.js @@ -8,20 +8,6 @@ export const getSensorData = (name, cellId, meas, startTime, endTime, resample = .then((res) => res.data); }; -export const getSensorDataBatch = (name, cellIds, meas, startTime, endTime, resample = 'hour') => { - return axios - .get( - `${process.env.PUBLIC_URL}/api/sensor/?name=${name}&cellIds=${cellIds.join(',')}&measurement=${meas}&startTime=${startTime}&endTime=${endTime}&resample=${resample}`, - ) - .then((res) => res.data); -}; - -export const getCellSensors = (cellIds) => { - return axios - .get(`${process.env.PUBLIC_URL}/api/cell-sensors/?cell_ids=${cellIds.join(',')}`) - .then((res) => res.data); -}; - export const streamSensorData = ( name, cellId,