Skip to content

Commit 142d01d

Browse files
authored
Merge pull request #4536 from airqo-platform/worst-air-quality
Add Endpoints to Retrieve Worst PM2.5 Readings for Devices and Sites
2 parents ce94866 + 9f420e6 commit 142d01d

File tree

5 files changed

+409
-0
lines changed

5 files changed

+409
-0
lines changed

src/device-registry/controllers/event.controller.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@ const processCohortIds = async (cohort_ids, request) => {
449449
(result) => !(result.success === false)
450450
);
451451

452+
// join the array of arrays into a single array
453+
const flattened = [].concat(...validDeviceIdResults);
454+
452455
if (isEmpty(invalidDeviceIdResults) && validDeviceIdResults.length > 0) {
453456
request.query.device_id = validDeviceIdResults.join(",");
454457
}
@@ -2975,6 +2978,168 @@ const createEvent = {
29752978
return;
29762979
}
29772980
},
2981+
getWorstReadingForSites: async (req, res, next) => {
2982+
try {
2983+
const errors = extractErrorsFromRequest(req);
2984+
if (errors) {
2985+
next(
2986+
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
2987+
);
2988+
return;
2989+
}
2990+
2991+
const request = req;
2992+
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
2993+
request.query.tenant = isEmpty(req.query.tenant)
2994+
? defaultTenant
2995+
: req.query.tenant;
2996+
2997+
const { site_id, grid_id } = { ...req.query, ...req.params };
2998+
let locationErrors = 0;
2999+
3000+
let siteIds = [];
3001+
if (Array.isArray(site_id)) {
3002+
siteIds = site_id.map(String);
3003+
} else if (site_id) {
3004+
siteIds = [String(site_id)];
3005+
}
3006+
3007+
if (isEmpty(siteIds) && !isEmpty(grid_id)) {
3008+
await processGridIds(grid_id, request);
3009+
if (isEmpty(request.query.site_id)) {
3010+
locationErrors++;
3011+
} else {
3012+
siteIds = request.query.site_id.split(",");
3013+
}
3014+
}
3015+
3016+
if (locationErrors === 0) {
3017+
const result = await createEventUtil.getWorstReadingForSites({
3018+
siteIds,
3019+
next,
3020+
});
3021+
3022+
if (isEmpty(result) || res.headersSent) {
3023+
return;
3024+
}
3025+
3026+
if (result.success === true) {
3027+
const status = result.status || httpStatus.OK;
3028+
res.status(status).json({
3029+
success: true,
3030+
message: result.message,
3031+
data: result.data,
3032+
});
3033+
} else {
3034+
const errorStatus = result.status || httpStatus.INTERNAL_SERVER_ERROR;
3035+
res.status(errorStatus).json({
3036+
success: false,
3037+
errors: result.errors || { message: "" },
3038+
message: result.message,
3039+
});
3040+
}
3041+
} else {
3042+
res.status(httpStatus.BAD_REQUEST).json({
3043+
success: false,
3044+
errors: {
3045+
message: `Unable to process measurements for the provided site IDs`,
3046+
},
3047+
message: "Bad Request Error",
3048+
});
3049+
}
3050+
} catch (error) {
3051+
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
3052+
next(
3053+
new HttpError(
3054+
"Internal Server Error",
3055+
httpStatus.INTERNAL_SERVER_ERROR,
3056+
{ message: error.message }
3057+
)
3058+
);
3059+
return;
3060+
}
3061+
},
3062+
getWorstReadingForDevices: async (req, res, next) => {
3063+
try {
3064+
const errors = extractErrorsFromRequest(req);
3065+
if (errors) {
3066+
next(
3067+
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
3068+
);
3069+
return;
3070+
}
3071+
3072+
const request = req;
3073+
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
3074+
request.query.tenant = isEmpty(req.query.tenant)
3075+
? defaultTenant
3076+
: req.query.tenant;
3077+
3078+
const { device_id, cohort_id } = { ...req.query, ...req.params };
3079+
let locationErrors = 0;
3080+
3081+
let deviceIds = [];
3082+
if (Array.isArray(device_id)) {
3083+
deviceIds = device_id.map(String);
3084+
} else if (device_id) {
3085+
deviceIds = [String(device_id)];
3086+
}
3087+
3088+
if (isEmpty(deviceIds) && !isEmpty(cohort_id)) {
3089+
await processCohortIds(cohort_id, request);
3090+
if (isEmpty(request.query.device_id)) {
3091+
locationErrors++;
3092+
} else {
3093+
deviceIds = request.query.device_id.split(",");
3094+
}
3095+
}
3096+
logObject("deviceIds", deviceIds);
3097+
if (locationErrors === 0) {
3098+
const result = await createEventUtil.getWorstReadingForDevices({
3099+
deviceIds,
3100+
next,
3101+
});
3102+
3103+
if (isEmpty(result) || res.headersSent) {
3104+
return;
3105+
}
3106+
3107+
if (result.success === true) {
3108+
const status = result.status || httpStatus.OK;
3109+
res.status(status).json({
3110+
success: true,
3111+
message: result.message,
3112+
data: result.data,
3113+
});
3114+
} else {
3115+
const errorStatus = result.status || httpStatus.INTERNAL_SERVER_ERROR;
3116+
res.status(errorStatus).json({
3117+
success: false,
3118+
errors: result.errors || { message: "" },
3119+
message: result.message,
3120+
});
3121+
}
3122+
} else {
3123+
res.status(httpStatus.BAD_REQUEST).json({
3124+
success: false,
3125+
errors: {
3126+
message: `Unable to process measurements for the provided device IDs`,
3127+
},
3128+
message: "Bad Request Error",
3129+
});
3130+
}
3131+
} catch (error) {
3132+
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
3133+
next(
3134+
new HttpError(
3135+
"Internal Server Error",
3136+
httpStatus.INTERNAL_SERVER_ERROR,
3137+
{ message: error.message }
3138+
)
3139+
);
3140+
return;
3141+
}
3142+
},
29783143
};
29793144

29803145
module.exports = createEvent;

src/device-registry/models/Reading.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,97 @@ ReadingsSchema.statics.getAirQualityAnalytics = async function(siteId, next) {
766766
}
767767
};
768768

769+
ReadingsSchema.statics.getWorstPm2_5Reading = async function({
770+
siteIds = [],
771+
next,
772+
} = {}) {
773+
try {
774+
if (isEmpty(siteIds) || !Array.isArray(siteIds)) {
775+
next(
776+
new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, {
777+
message: "siteIds array is required",
778+
})
779+
);
780+
return;
781+
}
782+
if (siteIds.length === 0) {
783+
return {
784+
success: true,
785+
message: "No site_ids were provided",
786+
data: [],
787+
status: httpStatus.OK,
788+
};
789+
}
790+
791+
// Validate siteIds type
792+
if (!siteIds.every((id) => typeof id === "string")) {
793+
next(
794+
new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, {
795+
message: "siteIds must be an array of strings",
796+
})
797+
);
798+
return;
799+
}
800+
801+
const formattedSiteIds = siteIds.map((id) => id.toString());
802+
const threeDaysAgo = new Date();
803+
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
804+
const pipeline = this.aggregate([
805+
{
806+
$match: {
807+
site_id: { $in: formattedSiteIds },
808+
time: { $gte: threeDaysAgo },
809+
"pm2_5.value": { $exists: true }, // Ensure pm2_5.value exists
810+
},
811+
},
812+
{
813+
$sort: { "pm2_5.value": -1, time: -1 }, // Sort by pm2_5 descending, then by time
814+
},
815+
{
816+
$limit: 1, // Take only the worst reading
817+
},
818+
{
819+
$project: {
820+
_id: 0, // Exclude the MongoDB-generated _id
821+
site_id: 1,
822+
time: 1,
823+
pm2_5: 1,
824+
device: 1,
825+
device_id: 1,
826+
siteDetails: 1,
827+
},
828+
},
829+
]).allowDiskUse(true);
830+
831+
const worstReading = await pipeline.exec();
832+
833+
if (!isEmpty(worstReading)) {
834+
return {
835+
success: true,
836+
message: "Successfully retrieved the worst pm2_5 reading.",
837+
data: worstReading[0],
838+
status: httpStatus.OK,
839+
};
840+
} else {
841+
return {
842+
success: true,
843+
message:
844+
"No pm2_5 readings found for the specified site_ids in the last three days.",
845+
data: {},
846+
status: httpStatus.OK,
847+
};
848+
}
849+
} catch (error) {
850+
logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
851+
next(
852+
new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
853+
message: error.message,
854+
})
855+
);
856+
return;
857+
}
858+
};
859+
769860
const ReadingModel = (tenant) => {
770861
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
771862
const dbTenant = isEmpty(tenant) ? defaultTenant : tenant;

src/device-registry/routes/v2/readings.routes.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ router.get(
2323
eventController.recentReadings
2424
);
2525

26+
router.get(
27+
"/worst/devices",
28+
readingsValidations.worstReadingForDevices,
29+
eventController.getWorstReadingForDevices
30+
);
31+
32+
router.get(
33+
"/worst/sites",
34+
readingsValidations.worstReadingForSites,
35+
eventController.getWorstReadingForSites
36+
);
37+
2638
router.get(
2739
"/sites/:site_id/averages",
2840
readingsValidations.listAverages,

0 commit comments

Comments
 (0)