From ce44657af162768a46b4f28d1b60a67ddfbeb353 Mon Sep 17 00:00:00 2001 From: David Glover Date: Thu, 7 May 2026 15:08:48 -0700 Subject: [PATCH 1/7] created /cell/cellId/sensors endpoint,still debugging and testing --- backend/api/__init__.py | 2 ++ backend/api/resources/cell_sensors.py | 11 +++++++ backend/api/schemas/sensor_schema.py | 13 ++++++++ frontend/src/pages/dashboard/Dashboard.jsx | 33 ++++++++++++++++++- .../dashboard/components/UnifiedChart.jsx | 17 ++++++++-- frontend/src/services/cell.js | 10 ++++++ 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 backend/api/resources/cell_sensors.py create mode 100644 backend/api/schemas/sensor_schema.py diff --git a/backend/api/__init__.py b/backend/api/__init__.py index 1773b901..38c71dd6 100644 --- a/backend/api/__init__.py +++ b/backend/api/__init__.py @@ -171,6 +171,7 @@ 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 Cell_Sensors from .auth.routes import auth @@ -187,6 +188,7 @@ def handle_unsubscribe_cells(data): api.add_resource(Session_r, "/session") api.add_resource(User_Data, "/user") api.add_resource(Status, "/status/") + api.add_resource(Cell_Sensors, "/cell//sensors") # Tag management endpoints api.add_resource(Tag, "/tag/") diff --git a/backend/api/resources/cell_sensors.py b/backend/api/resources/cell_sensors.py new file mode 100644 index 00000000..e1b0d5e4 --- /dev/null +++ b/backend/api/resources/cell_sensors.py @@ -0,0 +1,11 @@ +from flask_restful import Resource +from ..models.sensor import Sensor +from ..schemas.sensor_schema import SensorSchema + +sensor_schema = SensorSchema(many=True) + +class Cell_Sensors(Resource): + def get(self, cell_id): + sensors = Sensor.query.filter_by(cell_id=cell_id).all() + return sensor_schema.dump(sensors) + diff --git a/backend/api/schemas/sensor_schema.py b/backend/api/schemas/sensor_schema.py new file mode 100644 index 00000000..1732e354 --- /dev/null +++ b/backend/api/schemas/sensor_schema.py @@ -0,0 +1,13 @@ +from .. import ma +from ..models.sensor import Sensor + +class SensorSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Sensor + + id = ma.Integer() + cell_id = ma.Integer() + measurement = ma.String() + data_type = ma.String() + unit = ma.String() + name = ma.String() \ No newline at end of file diff --git a/frontend/src/pages/dashboard/Dashboard.jsx b/frontend/src/pages/dashboard/Dashboard.jsx index 60537625..fd7d3149 100644 --- a/frontend/src/pages/dashboard/Dashboard.jsx +++ b/frontend/src/pages/dashboard/Dashboard.jsx @@ -6,7 +6,7 @@ import DateRangeNotification from '../../components/DateRangeNotification'; import { useSmartDateRange } from '../../hooks/useSmartDateRange'; import useAxiosPrivate from '../../auth/hooks/useAxiosPrivate'; import useAuth from '../../auth/hooks/useAuth'; -import { useCells } from '../../services/cell'; +import { useCells, getCellSensors } from '../../services/cell'; import ArchiveModal from './components/ArchiveModal'; import BackBtn from './components/BackBtn'; import CellSelect from './components/CellSelect'; @@ -35,6 +35,7 @@ function Dashboard() { const [powerHasData, setPowerHasData] = useState(false); const [terosHasData, setTerosHasData] = useState(false); const [liveData, setLiveData] = useState([]); + const [cellSensorsById, setCellSensorsById] = useState({}); // Background streaming data - always collecting in background const backgroundStreamDataRef = useRef([]); @@ -405,6 +406,21 @@ useEffect(() => { } }; + useEffect(() => { + const loadCellSensors = async () => { + const sensorsById = {}; + for (const cell of selectedCells) { + sensorsById[cell.id] = await getCellSensors(cell.id); + } + setCellSensorsById(sensorsById); + }; + if (selectedCells.length > 0){ + loadCellSensors(); + } else { + setCellSensorsById({}); + } + }, [selectedCells]); + useEffect(() => { if (selectedCells.length === 0) { @@ -667,6 +683,7 @@ useEffect(() => { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> { stream={stream} liveData={liveData} processedData={processedLiveData.sensors} + cellSensorsById={cellSensorsById} /> diff --git a/frontend/src/pages/dashboard/components/UnifiedChart.jsx b/frontend/src/pages/dashboard/components/UnifiedChart.jsx index 3f70c363..c19bafc7 100644 --- a/frontend/src/pages/dashboard/components/UnifiedChart.jsx +++ b/frontend/src/pages/dashboard/components/UnifiedChart.jsx @@ -6,6 +6,7 @@ import { getSensorData } from '../../../services/sensor'; import UniversalChart from '../../../charts/UniversalChart'; import { extractUnifiedStreamValue, matchesSensorStreamType, normalizeUnifiedStreamValue } from './unifiedChartUtils'; + const CHART_CONFIGS = { power_voltage: { sensor_name: 'POWER_VOLTAGE', @@ -135,7 +136,7 @@ const CHART_CONFIGS = { chartId: 'waterFlow', }, }; -function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, processedData, onDataStatusChange }) { +function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, processedData, onDataStatusChange, cellSensorsById ={}, }) { const [resample, setResample] = useState('hour'); const chartSettings = { label: [], @@ -169,7 +170,18 @@ function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, proce data[id] = { name: name, }; + // get list of sensors that are associated with selected cells + const cellSensors = cellSensorsById || []; for (const meas of measurements) { + // verify sensor mathches one of the sensors from CHART_CONFIGS + const hasSensor = cellSensors.some((sensor) => { + return sensor.name === sensor_name && sensor.measurement === meas; + }); + // if no matching sensor, skip this request + if (!hasSensor){ + continue; + } + data[id] = { ...data[id], [meas]: await getSensorData(sensor_name, id, meas, startDate.toHTTP(), endDate.toHTTP(), resample), @@ -411,7 +423,7 @@ function UnifiedChart({ type, cells, startDate, endDate, stream, liveData, proce }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cells, stream, resample, startDate, endDate]); + }, [cells, stream, resample, startDate, endDate, cellSensorsById]); const handleResampleChange = (newResample) => { setResample(newResample); @@ -461,6 +473,7 @@ UnifiedChart.propTypes = { liveData: PropTypes.array, processedData: PropTypes.object, onDataStatusChange: PropTypes.func, + cellSensorsById: PropTypes.object, }; export default UnifiedChart; diff --git a/frontend/src/services/cell.js b/frontend/src/services/cell.js index f840c2b5..7a5143ac 100644 --- a/frontend/src/services/cell.js +++ b/frontend/src/services/cell.js @@ -20,6 +20,16 @@ export const getCells = () => { }); }; +export const getCellSensors = (cellId) => { + return axios + .get(`${process.env.PUBLIC_URL}/api/cell/${cellId}/sensors`) + .then((res) => res.data) + .catch((error) => { + console.log('Error getting cell sensors:', error.response ? error.response.data : error.message); + throw(error); + }); +}; + // Get cells with their tags included export const getCellsWithTags = () => { return axios From 4c0792d87f7fd9fc8289f86ee00a86c3ff8615f0 Mon Sep 17 00:00:00 2001 From: David Glover Date: Thu, 7 May 2026 16:58:40 -0700 Subject: [PATCH 2/7] sotre avaialable sensors in cellSensorsById, which is a string not an array, allowing for value comparison --- frontend/src/pages/dashboard/Dashboard.jsx | 13 +++++++++---- frontend/src/services/cell.js | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/dashboard/Dashboard.jsx b/frontend/src/pages/dashboard/Dashboard.jsx index fd7d3149..abeccc65 100644 --- a/frontend/src/pages/dashboard/Dashboard.jsx +++ b/frontend/src/pages/dashboard/Dashboard.jsx @@ -406,20 +406,25 @@ useEffect(() => { } }; + const selectedCellIds = useMemo(() => { + return selectedCells.map((cell) => cell.id.toString()).sort().join(','); + }, [selectedCells]) + useEffect(() => { const loadCellSensors = async () => { const sensorsById = {}; - for (const cell of selectedCells) { - sensorsById[cell.id] = await getCellSensors(cell.id); + const cellIds = selectedCellIds.split(',').filter(Boolean); + for (const cellId of cellIds){ + sensorsById[cellId] = await getCellSensors(cellId); } setCellSensorsById(sensorsById); }; - if (selectedCells.length > 0){ + if (selectedCellIds){ loadCellSensors(); } else { setCellSensorsById({}); } - }, [selectedCells]); + }, [selectedCellIds]); useEffect(() => { diff --git a/frontend/src/services/cell.js b/frontend/src/services/cell.js index 7a5143ac..a9e9fe1a 100644 --- a/frontend/src/services/cell.js +++ b/frontend/src/services/cell.js @@ -26,7 +26,6 @@ export const getCellSensors = (cellId) => { .then((res) => res.data) .catch((error) => { console.log('Error getting cell sensors:', error.response ? error.response.data : error.message); - throw(error); }); }; From 3173fbb52caa6204f324ba1b0b34a7612abc25f1 Mon Sep 17 00:00:00 2001 From: David Glover Date: Fri, 8 May 2026 22:47:07 -0700 Subject: [PATCH 3/7] created unit test for sensor endpoint --- CHANGELOG.md | 2 +- backend/tests/test_sensor.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14bab7ac..b516e571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,5 +30,5 @@ When adding a new entry, please use the following format: - [2026-03-10] hotfix: Fixed import errors [#698](https://github.com/jlab-sensing/ENTS-backend/pull/698) - [2026-03-05] chore: remove deprecated import_example_data script and update docs to use ents CLI [#665](https://github.com/jlab-sensing/ENTS-backend/pull/665) - [2026-03-05] fix: Temp removed "Export to CSV option" hotfix [#671](https://github.com/jlab-sensing/ENTS-backend/pull/671) - - [2026-03-21] fix: Fixed non-deterministic two-week date range fallback bug [#713](https://github.com/jlab-sensing/ENTS-backend/pull/713) +- [2026-05-08] feature: Added /cell/id/sensors endpoint to query sensors associated with each cell \ No newline at end of file diff --git a/backend/tests/test_sensor.py b/backend/tests/test_sensor.py index 03420cf9..bd269f69 100644 --- a/backend/tests/test_sensor.py +++ b/backend/tests/test_sensor.py @@ -105,3 +105,33 @@ def test_get_sensor_obj(init_database): # data_type = db.Column(db.Text(), nullable=False) # unit = db.Column(db.Text()) # name = db.Column(db.Text(), nullable=False) + +def test_sensor_endpoint_returns_only_sensors(init_database): + ts = 1705176162 + cell = Cell("test_cell_1", "", 1, 1, False, None) + cell.save() + + meas_dict = { + "type": "measurement_type", + "cellId": cell.id, + "data": { + "measurement_name": 1, + }, + # types: float, int, text + "data_type": { + "measurement_name": int, + }, + "ts": ts, + } + data = Sensor.add_data("measurement_name", "measurement_unit", meas_dict) + sensor = Sensor.query.get(data.sensor_id) + + + response = init_database.get(f"/api/cell/{cell.id}/sensors") + + assert response.status_code == 200 + result = response.get_json() + + returned_ids = [sensor["id"] for sensor in result] + assert sensor.id in returned_ids + assert all(sensors["cell_id"] == cell.id for sensors in result) From 9c4c4e299fa8c4dc3b4eda4e3151c1b77cfa6a7c Mon Sep 17 00:00:00 2001 From: David Glover <150413486+dvdthr5@users.noreply.github.com> Date: Fri, 8 May 2026 22:50:50 -0700 Subject: [PATCH 4/7] Update CHANGELOG with new feature and fixes Updated CHANGELOG to include recent feature and fix entries. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b516e571..d6316fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,10 @@ When adding a new entry, please use the following format: ## Log +- [2026-05-08] feature: Added /cell/id/sensors endpoint to query sensors associated with each cell - [2026-04-16] feature: added a clear all cells button to cell selection - [2026-04-16] fix: removed two scrollbars from dashboard, made power datapoints display as V and A, made data not truncate as often -- [2026-03-30] fix: added test decorators to resolve lack of "TTN_API_KEY" on fork PR's. removed k6 from github actions. changed github action to utilize env-import.py as opposeed to directly accessing s3 bucket for env variables. [#736] (https://github.com/jlab-sensing/ENTS-backend/pull/736) +- [2026-03-30] fix: added test decorators to resolve lack of "TTN_API_KEY" on fork PR's. removed k6 from github actions. changed github action to utilize env-import.py as opposeed to directly accessing s3 bucket for env variables. [#736](https://github.com/jlab-sensing/ENTS-backend/pull/736) - [2026-03-23] fix: increase fallback SECRET_KEY length to resolve PyJWT InsecureKeyLengthWarning [#682](https://github.com/jlab-sensing/ENTS-backend/pull/682) - [2026-03-12] fix: safely catch PyJWT DecodeErrors and remove log spam for invalid tokens [#683](https://github.com/jlab-sensing/ENTS-backend/pull/683) - [2026-03-12] hotfix: linted AddCellModal, ensured functionality of D10 charts, and added flags to github action pytest for logging [#703](https://github.com/jlab-sensing/ENTS-backend/pull/703) @@ -31,4 +32,3 @@ When adding a new entry, please use the following format: - [2026-03-05] chore: remove deprecated import_example_data script and update docs to use ents CLI [#665](https://github.com/jlab-sensing/ENTS-backend/pull/665) - [2026-03-05] fix: Temp removed "Export to CSV option" hotfix [#671](https://github.com/jlab-sensing/ENTS-backend/pull/671) - [2026-03-21] fix: Fixed non-deterministic two-week date range fallback bug [#713](https://github.com/jlab-sensing/ENTS-backend/pull/713) -- [2026-05-08] feature: Added /cell/id/sensors endpoint to query sensors associated with each cell \ No newline at end of file From 098055c211abc4b0c8e732cc9806dd93f278af1a Mon Sep 17 00:00:00 2001 From: David Glover Date: Sat, 9 May 2026 08:46:56 -0700 Subject: [PATCH 5/7] used black to reformat --- backend/api/resources/cell_sensors.py | 2 +- backend/api/schemas/sensor_schema.py | 5 +++-- backend/tests/test_sensor.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/api/resources/cell_sensors.py b/backend/api/resources/cell_sensors.py index e1b0d5e4..f4d295ca 100644 --- a/backend/api/resources/cell_sensors.py +++ b/backend/api/resources/cell_sensors.py @@ -4,8 +4,8 @@ sensor_schema = SensorSchema(many=True) + class Cell_Sensors(Resource): def get(self, cell_id): sensors = Sensor.query.filter_by(cell_id=cell_id).all() return sensor_schema.dump(sensors) - diff --git a/backend/api/schemas/sensor_schema.py b/backend/api/schemas/sensor_schema.py index 1732e354..edcf8182 100644 --- a/backend/api/schemas/sensor_schema.py +++ b/backend/api/schemas/sensor_schema.py @@ -1,13 +1,14 @@ from .. import ma from ..models.sensor import Sensor + class SensorSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Sensor - + id = ma.Integer() cell_id = ma.Integer() measurement = ma.String() data_type = ma.String() unit = ma.String() - name = ma.String() \ No newline at end of file + name = ma.String() diff --git a/backend/tests/test_sensor.py b/backend/tests/test_sensor.py index bd269f69..00b232d0 100644 --- a/backend/tests/test_sensor.py +++ b/backend/tests/test_sensor.py @@ -106,6 +106,7 @@ def test_get_sensor_obj(init_database): # unit = db.Column(db.Text()) # name = db.Column(db.Text(), nullable=False) + def test_sensor_endpoint_returns_only_sensors(init_database): ts = 1705176162 cell = Cell("test_cell_1", "", 1, 1, False, None) @@ -126,7 +127,6 @@ def test_sensor_endpoint_returns_only_sensors(init_database): data = Sensor.add_data("measurement_name", "measurement_unit", meas_dict) sensor = Sensor.query.get(data.sensor_id) - response = init_database.get(f"/api/cell/{cell.id}/sensors") assert response.status_code == 200 From 8c6d2661003767c58ff6bcde5b24f5d99837c395 Mon Sep 17 00:00:00 2001 From: David Glover Date: Sat, 9 May 2026 09:19:11 -0700 Subject: [PATCH 6/7] added unit test for getCellSensors --- frontend/src/__tests__/Profile.test.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/__tests__/Profile.test.jsx b/frontend/src/__tests__/Profile.test.jsx index f187fc6f..608bf4ad 100644 --- a/frontend/src/__tests__/Profile.test.jsx +++ b/frontend/src/__tests__/Profile.test.jsx @@ -5,7 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // import AccountInfo from '../pages/profile/components/AccountInfo'; import DeleteCellModal from '../pages/profile/components/DeleteCellModal'; import { describe, it, expect, vi, afterEach } from 'vitest'; -import { updateCell, getCells, deleteCell, getUserCells } from '../services/cell'; +import { updateCell, getCells, deleteCell, getUserCells, getCellSensors } from '../services/cell'; import { useOutletContext } from 'react-router-dom'; import axios from 'axios'; vi.mock('axios'); @@ -333,6 +333,14 @@ describe('Cell Service API Functions', () => { expect(getUserCells).toHaveBeenCalledTimes(1); }); + it('get cell sensors', async () => { + const mockResponse = [{ id: 1, name: 'Mock Cell' }]; + axios.get.mockResolvedValue({ data:mockResponse}); + const data = await getCellSensors(1); + expect(data).toEqual(mockResponse) + expect(axios.get).toHaveBeenCalledWith(`${process.env.PUBLIC_URL}/api/cell/1/sensors`) + }) + // it('fetches cell data', async () => { // const mockData = { result: 'Mock Cell Data' }; // axios.get.mockResolvedValue({ data: mockData }); From efea37cb95e4f2873b333ec3e3b7a6c1bf29b16b Mon Sep 17 00:00:00 2001 From: David Glover Date: Mon, 11 May 2026 11:00:37 -0700 Subject: [PATCH 7/7] added test for error --- frontend/src/__tests__/Profile.test.jsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/__tests__/Profile.test.jsx b/frontend/src/__tests__/Profile.test.jsx index 608bf4ad..4fa2e7a4 100644 --- a/frontend/src/__tests__/Profile.test.jsx +++ b/frontend/src/__tests__/Profile.test.jsx @@ -335,10 +335,23 @@ describe('Cell Service API Functions', () => { it('get cell sensors', async () => { const mockResponse = [{ id: 1, name: 'Mock Cell' }]; - axios.get.mockResolvedValue({ data:mockResponse}); + axios.get.mockResolvedValue({ data: mockResponse}); const data = await getCellSensors(1); - expect(data).toEqual(mockResponse) - expect(axios.get).toHaveBeenCalledWith(`${process.env.PUBLIC_URL}/api/cell/1/sensors`) + expect(data).toEqual(mockResponse); + expect(axios.get).toHaveBeenCalledWith(`${process.env.PUBLIC_URL}/api/cell/1/sensors`); + }); + + it ('returns error when get cell sensors fails', async () =>{ + const mockError = { + response: { + data: { error: 'Error getting cell sensors'}, + }, + }; + + axios.get.mockRejectedValueOnce(mockError); + const data = await getCellSensors(1); + expect(data).toBeUndefined(); + expect(axios.get).toHaveBeenCalledWith(`${process.env.PUBLIC_URL}/api/cell/1/sensors`); }) // it('fetches cell data', async () => {