Commit 6f07476d authored by Giannis Kepas's avatar Giannis Kepas
Browse files

initial support for DeviceMeasurement

parent 31389d27
Loading
Loading
Loading
Loading
+70 −82
Original line number Diff line number Diff line
@@ -3,44 +3,38 @@ const NGSITranslator = require("../translator/NGSITranslator");
const typeMaps = require("../typeMaps/typeMaps");

/**
 * Generates a modality_id parameter based on the type and attributes provided.
 * If attributes are given, it will filter the modalities to only include the ones
 * that correspond to the given attributes. Otherwise, it will return all modalities
 * available for the given type.
 * 
 * Based on Smart City Heraklion API
 *
 * @param {string} type The NGSI-LD type to generate the modality_id for
 * @param {string} type The NGSI-LD type to generate the measurement_type for
 * @param {string} attributes A comma-separated list of attributes to filter by
 * @throws {Error} If the type is invalid or no valid attributes are found
 * @returns {string} The modality_id parameter as a string
 * @returns {string[]} List of measurement types
 */
function generateModalityIdParam(type, attributes) {
    const modalityMap = typeMaps[type];
    if (!modalityMap) {
function generateMeasurementTypeParam(type, attributes) {
    const typeMap = typeMaps[type];
    if (!typeMap) {
        throw new Error(`Invalid type: ${type}`);
    }

    // Filter attributes if provided, otherwise return all
    const modalityIds = attributes
        ? attributes.split(",").map(attr => Object.entries(modalityMap)
    const measurementTypes = attributes
        ? attributes.split(",").map(attr => Object.entries(typeMap)
            .find(([_, value]) => value.slug === attr)?.[0])
            .filter(Boolean)
        : Object.keys(modalityMap);
        : Object.keys(typeMap);

    if (modalityIds.length === 0) {
        throw new Error(`No valid attributes found for type: ${type}`);
    if (measurementTypes.length === 0) {
        throw new Error(`No valid attributes found for type '${type}' and attributes '${attributes}'`);
    }

    return modalityIds;
    // console.log("Measurement types:", measurementTypes);
    return measurementTypes;
}

/**
 * Fetches data from the Smart City Heraklion API and translates it to NGSI-LD.
 * Fetches data from the provided API and translates it to NGSI-LD.
 * 
 * @param {string} type The NGSI-LD type to retrieve
 * @param {string} [attrs] A comma-separated list of attributes to filter by
 * @param {Function} fetcher The specific fetcher function to use (fetchWeatherObservedTemporalData or fetchWeatherObservedRealTimeData)
 * @param {Function} fetcher The specific fetcher function to use (fetchTemporalData or fetchRealTimeData)
 * @param {Object} [additionalParams] Additional parameters to pass to the fetcher function
 * @param {Response} res The Express response object
 * @throws {Error} If the type is invalid, no valid attributes are found, or the API request fails
@@ -55,19 +49,17 @@ const fetchEntities = async (type, attrs, fetcher, additionalParams, res) => {
            return res.status(400).json({ error: `Unsupported entity type: ${type}` });
        }

        const modalityIdParam = generateModalityIdParam(type, attrs);
        if (!modalityIdParam) {
            return res.status(400).json({ error: "Missing modality_id parameter." });
        const measurementTypes = generateMeasurementTypeParam(type, attrs);
        if (!measurementTypes) {
            return res.status(400).json({ error: "Missing measurement_type parameter." });
        }

        // Call the specific fetcher function
        const apiResponse = await fetcher(type, modalityIdParam, additionalParams);
        const apiResponse = await fetcher(type, measurementTypes, additionalParams);

        if (!apiResponse || !apiResponse.data) {
        if (!apiResponse || apiResponse.data.length === 0) {
            return res.status(500).json({ error: "Failed to retrieve data from API." });
        }

        // Translate response to NGSI-LD
        const ngsiData = NGSITranslator.toNGSI(apiResponse.data, type);
        res.status(200).json(ngsiData);
    } catch (error) {
@@ -80,67 +72,63 @@ const fetchEntities = async (type, attrs, fetcher, additionalParams, res) => {
 * Fetches the temporal data for a given NGSI-LD entity type.
 * 
 * @param {string} type The NGSI-LD type to retrieve
 * @param {string} modalityIdParam The modality_id parameter for filtering
 * @param {string} measurementTypes The measurement type parameter for filtering
 * @param {Object} [params] Additional parameters for the API request
 * @throws {Error} If the entity type is unsupported
 */
const fetchWeatherObservedTemporalData = async (type, modalityIds, params) => {
    if (!Array.isArray(modalityIds) || modalityIds.length === 0) {
        throw new Error(`Invalid modality IDs for type: ${type}`);
    }

    const results = { data: [] };

    // Loop through each modality ID and make separate requests
    for (const modalityId of modalityIds) {
const fetchTemporalData = async (type, measurementTypes, params) => {
    switch (type) {
        case "DeviceMeasurement":
            try {
            const apiResponse = await axios.post(`${process.env.API_URL}/rpc/measurements_averages`, {
                _devices: "{71,72,73,63,64,36,68,70,61,69,67,66,65,60,59,120}",
                _from: params.time,
                _to: params.endTime,
                _granularity: "1 hour",
                _modality: typeMaps[type][modalityId].key,
                _show_out_of_range: false
                const response = await axios.get(`${process.env.API_URL}/device_measurement`, {
                    params: {
                        measurement_type: `in.(${measurementTypes.join(",")})`,
                        ts: params.time, // TODO: Implement proper time handling according to postgres timestamp format
                        limit: params.limit,
                    }
                });

            if (apiResponse && apiResponse.data) {
                formattedData = apiResponse.data.map(entry => ({
                    device_id: modalityId,
                    modality_id: parseInt(modalityId, 10),
                    value: entry.avg, 
                    observed_at: entry.at
                }));

                results.data.push(...formattedData);
            }
                return response;
            } catch (error) {
            console.error(`Error fetching data for modalityId ${modalityId}:`, error.message);
        }
                console.error("Error fetching temporal data:", error.message);
                return { data: [] };
            }

    return results;
        default:
            return { data: [] };
    }
};

/**
 * Fetches the latest real-time data for a given NGSI-LD entity type.
 * 
 * @param {string} type The NGSI-LD type to retrieve
 * @param {string} modalityIdParam The modality_id parameter for filtering
 * @param {string} measurementTypes The measurement type parameter for filtering
 * @throws {Error} If the entity type is unsupported
 */

const fetchWeatherObservedRealTimeData = async (type, modalityIdParam) => {
const fetchRealTimeData = async (type, measurementTypes, params) => {
    switch (type) {
        case "WeatherObserved":
            return axios.get(`${process.env.API_URL}/latest_measurements_averages`, {
                params: { modality_id: `in.(${Array.isArray(modalityIdParam) ? modalityIdParam.join(",") : modalityIdParam})` },
        case "DeviceMeasurement":
            try {
                const response = await axios.get(`${process.env.API_URL}/device_measurement`, {
                    params: {
                        measurement_type: `in.(${measurementTypes.join(",")})`,
                        order: "ts.desc",
                        limit: params.limit || 100, // Default to 100 if limit is not provided. (MEETING: Remove this?)
                    }
                });

                return response;
            } catch (error) {
                console.error("Error fetching real-time data:", error.message);
                return { data: [] };
            }

        default:
            throw new Error(`Unsupported real-time entity type: ${type}`);
            return { data: [] };
    }
};


/**
 * Handles incoming requests for temporal NGSI-LD entities.
 * 
@@ -149,26 +137,24 @@ const fetchWeatherObservedRealTimeData = async (type, modalityIdParam) => {
 * @prop {string} time The timestamp to apply the temporal relationship to
 * @prop {string} endTime The end time for the time range query
 * @prop {string} attrs A comma-separated list of attributes to filter by
 * @prop {string} limit The maximum number of entities to return
 */
const getTemporalEntities = async (req, res) => {
    console.log("Incoming temporal request:", req.query);
    let { type, timerel, time, endTime, attrs } = req.query;
    const { type, timerel, time, endTime, attrs, limit } = req.query;

    // Decode URL-encoded time values (fixes %3A issue)
    time = decodeURIComponent(time);
    if (endTime) endTime = decodeURIComponent(endTime);

    // Ensure proper ISO 8601 formatting
    let adjustedEndTime = endTime;
    const now = new Date().toISOString();

    // TODO: Implement "between" timerel and fix temporal queries not working at all.
    if (timerel === "after") {
        endTime = now; // Set endTime to current time
        adjustedEndTime = now;
    } else if (timerel === "before") {
        endTime = time; // Swap values for "before"
        time = new Date(new Date(endTime).setDate(new Date(endTime).getDate() - 30)).toISOString(); // 30 days before
        adjustedEndTime = time;
        time = new Date(new Date(adjustedEndTime).setDate(new Date(adjustedEndTime).getDate() - 30)).toISOString();
    }

    await fetchEntities(type, attrs, fetchWeatherObservedTemporalData, { time, endTime }, res);
    await fetchEntities(type, attrs, fetchTemporalData, { time, endTime: adjustedEndTime, limit }, res);
};

/**
@@ -176,11 +162,13 @@ const getTemporalEntities = async (req, res) => {
 * 
 * @prop {string} type The NGSI-LD type to retrieve
 * @prop {string} attrs A comma-separated list of attributes to filter by
 * @prop {string} limit The maximum number of entities to return
 */
const getRealTimeEntities = async (req, res) => {
    console.log("Incoming real-time request:", req.query);
    const { type, attrs } = req.query;
    await fetchEntities(type, attrs, fetchWeatherObservedRealTimeData, {}, res);
    const { type, attrs, limit } = req.query;

    await fetchEntities(type, attrs, fetchRealTimeData, { limit }, res);
};

module.exports = { getTemporalEntities, getRealTimeEntities };
+15 −15
Original line number Diff line number Diff line
@@ -3,14 +3,14 @@ const typeMaps = require("../typeMaps/typeMaps");
class NGSITranslator {
    static toNGSI(apiData, entityType) {
        switch (entityType) {
            case "WeatherObserved":
                return NGSITranslator.processWeatherObserved(apiData, entityType);
            case "DeviceMeasurement":
                return NGSITranslator.processDeviceMeasurement(apiData, entityType);
            default:
                throw new Error(`Unsupported entity type: ${entityType}`);
        }
    }

    static processWeatherObserved(apiData, entityType) {
    static processDeviceMeasurement(apiData, entityType) {
        const typeMap = typeMaps[entityType];
        if (!typeMap) {
            throw new Error(`No mapping found for entity type: ${entityType}`);
@@ -19,7 +19,7 @@ class NGSITranslator {
        const groupedByDevice = {};

        apiData.forEach(item => {
            const { device_id, modality_id, value, observed_at } = item;
            const { item: device_id, measurement_type, value, ts } = item;

            // Ensure the device entry exists
            if (!groupedByDevice[device_id]) {
@@ -28,30 +28,30 @@ class NGSITranslator {
                    type: entityType,
                    dateObserved: { type: "Property", values: [] },
                    "@context": [
                        "https://raw.githubusercontent.com/smart-data-models/dataModel.Weather/master/context.jsonld",
                        "https://raw.githubusercontent.com/smart-data-models/dataModel.Device/master/context.jsonld",
                        "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"
                    ]
                };
            }

            const modalityInfo = typeMap[modality_id];
            if (modalityInfo) {
            const measurementInfo = typeMap[measurement_type];
            if (measurementInfo) {
                // Ensure property exists, else initialize
                if (!groupedByDevice[device_id][modalityInfo.key]) {
                    groupedByDevice[device_id][modalityInfo.key] = {
                if (!groupedByDevice[device_id][measurementInfo.key]) {
                    groupedByDevice[device_id][measurementInfo.key] = {
                        type: "Property",
                        values: []
                    };
                }

                // Append new observation instead of overwriting
                groupedByDevice[device_id][modalityInfo.key].values.push(
                    NGSITranslator.createProperty(value, modalityInfo.unit, modalityInfo.min, modalityInfo.max, modalityInfo.slug, observed_at)
                groupedByDevice[device_id][measurementInfo.key].values.push(
                    NGSITranslator.createProperty(value, measurementInfo.unit, measurementInfo.min, measurementInfo.max, measurementInfo.slug, ts)
                );

                // Append observed_at to dateObserved (avoid duplicates)
                if (!groupedByDevice[device_id].dateObserved.values.includes(observed_at)) {
                    groupedByDevice[device_id].dateObserved.values.push(observed_at);
                // Append ts to dateObserved (avoid duplicates)
                if (!groupedByDevice[device_id].dateObserved.values.includes(ts)) {
                    groupedByDevice[device_id].dateObserved.values.push(ts);
                }
            }
        });
+56 −0
Original line number Diff line number Diff line
const deviceMeasurementTypeMap = {
	/*
	Each entry in the map is a key-value pair where the key is the **type of measurement** and the value is an object with the following properties:
		- key: A string representing the name of the property in NGSI format.
		- unit: A character string representing the measurement unit for the type (e.g. "C" for temperature, "%" for humidity).
		- min: Min is the minimum possible value for the measurement type.
		- max: Max is the maximum possible value for the measurement type.
		- slug: The slug is used as "attrs" in the controller, allowing for the selection of specific attributes to be returned in the response.
	*/

	// TODO: Replace the numbers with the actual measurement types
	"temperature": { key: "temperature", unit: "CEL", min: 0, max: 60, slug: "temperature" }, // Θερμοκρασία 836 στο αρχείο
	"humidity": { key: "humidity", unit: "P1", min: 0, max: 100, slug: "humidity" },
	"energy": { key: "energy", unit: "KWH", min: 0, max: 10000, slug: "energyconsumption" }, // Κατανάλωση ενέργειας 581 line
	"power": { key: "power", unit: "WTT", min: 0, max: 10000, slug: "powerUsage" }, // Κατανάλωση ισχύος 1155
	"luminance": { key: "luminance", unit: "A24", min: 0, max: 1000, slug: "luminance" }, // Φωτεινότητα γραμμή 1231
	6: { key: "volt", unit: "V", min: 0, max: 1000, slug: "volt" }, // Voltage γραμμή 1035
	7: { key: "ampere", unit: "AMP", min: 0, max: 100, slug: "current" }, // Ampere γραμμή 980
	8: { key: "frequency", unit: "HTZ", min: 0, max: 1000, slug: "frequency" }, // γραμμή 1191
	9: { key: "hectopascal", unit: "A97", min: 0, max: 1200, slug: "pressure" },
	10: { key: "windSpeed", unit: "KMH", min: 0, max: 50, slug: "wind-speed" },
	11: { key: "windDirection", unit: "DD", min: 0, max: 359.9, slug: "wind-direction" },
	"CO2": { key: "carbonDioxide", unit: "P1", min: 0, max: 100, slug: "co2" },
	13: { key: "decibel", unit: "2N", min: 0, max: 100, slug: "noise_level" },
	14: { key: "motion", unit: "A99", min: 0, max: 1, slug: "motion_detection" },
	15: { key: "door", unit: "A99", min: 0, max: 1, slug: "magnetic_switch" },
	//14: { key: "state", unit: "", min: null, max: null, slug: "state" } // Κατάσταση συσκευής 2N γραμμή 1289
};

/* NOTES
* - Percentages are represented as "P1" in the unit field.
* - Luminance is represented as "A24" in the unit field not candela per square meter.
* - Decibels are represented as "2N" in the unit field.
*
* type / unit
* battery level %
* humidity %
* CO2 %
* temperature Celcius
* temperature apparent Celcius
* switch Off
* switch On
* switch On/Off
* motion detection switch On/Off
* magnetic switch On/Off
* UV UV
* power Watt
* luminance cd/m2
* noise level dB
* wind angle degrees
* barometric pressure hPa
* energy kWh
* wind speed km/h
*/

module.exports = deviceMeasurementTypeMap;
+3 −4
Original line number Diff line number Diff line
const weatherObservedTypeMap = require("./weatherObservedTypeMaps");
const deviceMeasurementTypeMap = require('./deviceMeasurementTypeMap');

// List of all available NGSI-LD model types
const typeMaps = {
    "WeatherObserved": weatherObservedTypeMap,
    // "WaterQualityObserved": waterQualityObservedTypeMap
    "DeviceMeasurement": deviceMeasurementTypeMap,
};

module.exports = typeMaps;