[{"id":"bd296ee92e1677c7","type":"tab","label":"Flux Global","disabled":false,"info":"","env":[]},{"id":"4f8f8af45fd31b50","type":"function","z":"bd296ee92e1677c7","name":"global.getCatalog","func":"/** \n * Obtenció de dades del catàleg Sentilo\n * \n * Paràmetres d'entrada:\n * \n * - reqParams.sentilo.api.host: obligatori, host de l'API de Sentilo\n * - reqParams.sentilo.api.token: obligatori, token de credencials de l'API de Sentilo\n * - reqParams.sentilo.api.filters.type: opcional, filtre per tipus de sensor\n * - reqParams.sentilo.api.filters.component: opcional, filtre per identificador de component\n * - reqParams.sentilo.api.filters.componentType: opcional, filtre per tipus de component\n * \n */\n \n/** Estructura base de reqParams\nmsg.reqParams = {\n sentilo: {\n host: ,\n token: ,\n filters: {\n type: ,\n component: ,\n componentType: \n }\n }\n}*/\n\nfunction validatePaths(base, rules) {\n for (const [path, expectedType] of Object.entries(rules)) {\n const isOptional = path.endsWith('?');\n const parts = path.replace('?', '').split('.');\n let current = base;\n\n for (const part of parts) {\n if (!(part in current)) {\n if (!isOptional) {\n node.error(`[global.getCatalog] Error: falta \"${path}\"`);\n return false;\n } else {\n current = undefined;\n break;\n }\n }\n current = current[part];\n }\n\n if (current !== undefined && current !== null && expectedType) {\n const isArrayExpected = expectedType === \"array\";\n const isTypeCorrect = isArrayExpected ? Array.isArray(current) : typeof current === expectedType;\n\n if (!isTypeCorrect) {\n node.error(`[global.getCatalog] Error: \"${path}\" no és tipus ${expectedType} (valor rebut: ${JSON.stringify(current)})`);\n return false;\n }\n } else if ((current === null || current === undefined) && !isOptional) {\n node.error(`[global.getCatalog] Error: \"${path}\" és null o undefined`);\n return false;\n }\n }\n return true;\n}\n\n\n// Validació compacta\nif (!validatePaths(msg, {\n \"reqParams.sentilo\": \"object\",\n \"reqParams.sentilo.host\": \"string\",\n \"reqParams.sentilo.token\": \"string\",\n \"reqParams.sentilo.filters?\": \"object\",\n \"reqParams.sentilo.filters.type?\": \"string\",\n \"reqParams.sentilo.filters.component?\": \"string\",\n \"reqParams.sentilo.filters.componentType?\": \"string\",\n})) return null;\n\nconst { reqParams } = msg;\nconst { sentilo } = reqParams;\nconst host = sentilo.host;\nconst token = sentilo.token;\n\nconst queryParams = [];\nif (sentilo.filters?.componentType) queryParams.push(`componentType=${encodeURIComponent(sentilo.filters.componentType)}`);\nif (sentilo.filters?.type) queryParams.push(`type=${encodeURIComponent(sentilo.filters.type)}`);\nif (sentilo.filters?.component) queryParams.push(`component=${encodeURIComponent(sentilo.filters.component)}`);\n\nconst queryString = queryParams.length > 0 ? \"?\" + queryParams.join(\"&\") : \"\";\nconst request = `${host}/catalog${queryString}`;\n\nmsg.url = request;\nmsg.method = 'GET';\nmsg.headers = {\n 'identity_key': token\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":80,"wires":[["5eaf6f549a953010"]]},{"id":"5eaf6f549a953010","type":"http request","z":"bd296ee92e1677c7","name":"getCatalog","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":390,"y":80,"wires":[["d17adc4cc90e9668","dd4d67a5701f440b"]]},{"id":"40fc23911cbbe87b","type":"comment","z":"bd296ee92e1677c7","name":"getCatalog: Obtenir dades del catàleg","info":"","x":170,"y":40,"wires":[]},{"id":"e68bde957f92aeb8","type":"link in","z":"bd296ee92e1677c7","name":"global.getCatalog","links":[],"x":35,"y":80,"wires":[["4f8f8af45fd31b50","37e43ffba40887f5"]]},{"id":"d17adc4cc90e9668","type":"link out","z":"bd296ee92e1677c7","name":"getCatalog","mode":"return","links":[],"x":505,"y":80,"wires":[]},{"id":"37e43ffba40887f5","type":"debug","z":"bd296ee92e1677c7","name":"global.getCatalog reqParams","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"reqParams.sentilo","targetType":"msg","statusVal":"","statusType":"auto","x":200,"y":120,"wires":[]},{"id":"dd4d67a5701f440b","type":"debug","z":"bd296ee92e1677c7","name":"getCatalog response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":80,"wires":[]},{"id":"06d56e2c5172de80","type":"function","z":"bd296ee92e1677c7","name":"global.addCatalog","func":"/** \n * Afegir components i sensors al catàleg Sentilo\n * \n * Paràmetres d'entrada:\n * \n * - reqParams.sentilo.api.host: obligatori, host de l'API de Sentilo\n * - reqParams.sentilo.api.token: obligatori, token de credencials de l'API de Sentilo\n * - reqParams.sentilo.sensorsToCreate: opcional, llistat de sensors a crear\n * \n */\n \n/** Estructura base de reqParams\nmsg.reqParams = {\n sentilo: {\n host: ,\n token: ,\n provider: ,\n sensorsToCreate: [SENSORS_TO_CREATE]\n }\n}*/\n\nfunction validatePaths(base, rules) {\n for (const [path, expectedType] of Object.entries(rules)) {\n const isOptional = path.endsWith('?');\n const parts = path.replace('?', '').split('.');\n let current = base;\n\n for (const part of parts) {\n if (!(part in current)) {\n if (!isOptional) {\n node.error(`[global.getCatalog] Error: falta \"${path}\"`);\n return false;\n } else {\n current = undefined;\n break;\n }\n }\n current = current[part];\n }\n\n if (current !== undefined && current !== null && expectedType) {\n const isArrayExpected = expectedType === \"array\";\n const isTypeCorrect = isArrayExpected ? Array.isArray(current) : typeof current === expectedType;\n\n if (!isTypeCorrect) {\n node.error(`[global.getCatalog] Error: \"${path}\" no és tipus ${expectedType} (valor rebut: ${JSON.stringify(current)})`);\n return false;\n }\n } else if ((current === null || current === undefined) && !isOptional) {\n node.error(`[global.getCatalog] Error: \"${path}\" és null o undefined`);\n return false;\n }\n }\n return true;\n}\n\n// Validació compacta\nif (!validatePaths(msg, {\n \"reqParams\": \"object\",\n \"reqParams.sentilo.provider\": \"string\",\n \"reqParams.sentilo.token\": \"string\", \n})) return null;\n\nconst { reqParams } = msg;\nconst { host, provider, token, sensorsToCreate: sensors } = reqParams.sentilo;\nconst request = `${host}/catalog/${provider}`;\n\nmsg.url = request;\nmsg.method = 'POST';\nmsg.headers = { 'identity_key': token };\nmsg.payload = sensors;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":200,"wires":[["0a2078ffee8d5084"]]},{"id":"0a2078ffee8d5084","type":"http request","z":"bd296ee92e1677c7","name":"addCatalog","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":390,"y":200,"wires":[["9a4211f3a0722805","d99288227c087b0a"]]},{"id":"b22d0fdfd342ad6d","type":"comment","z":"bd296ee92e1677c7","name":"addCatalog: Afegir sensors al catàleg","info":"","x":170,"y":160,"wires":[]},{"id":"baea99486ae4d629","type":"link in","z":"bd296ee92e1677c7","name":"global.addCatalog","links":[],"x":35,"y":200,"wires":[["06d56e2c5172de80","10e8bd84d43f2ed4"]]},{"id":"9a4211f3a0722805","type":"link out","z":"bd296ee92e1677c7","name":"addCatalog","mode":"return","links":[],"x":495,"y":200,"wires":[]},{"id":"10e8bd84d43f2ed4","type":"debug","z":"bd296ee92e1677c7","name":"global.addCatalog reqParams","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"reqParams.sentilo","targetType":"msg","statusVal":"","statusType":"auto","x":210,"y":240,"wires":[]},{"id":"d99288227c087b0a","type":"debug","z":"bd296ee92e1677c7","name":"addCatalog response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":640,"y":200,"wires":[]},{"id":"3026d954cc2402c4","type":"function","z":"bd296ee92e1677c7","name":"global.getLastObservations","func":"/** \n * Obtenció de les dades de catàleg de components i sensors\n * \n * Paràmetres d'entrada:\n * \n * - reqParams.sentilo.api.host: obligatori, host de l'API de Sentilo\n * - reqParams.sentilo.api.provider: obligatori, provider sobre el que es recuperaran les dades\n * - reqParams.sentilo.api.token: obligatori, token de credencials de l'API de Sentilo\n * - reqParams.sentilo.api.filters.from: opcional, filtre data d'inici de les dades\n * - reqParams.sentilo.api.filters.to: opcional, filtre data fi de les dades\n * - reqParams.sentilo.api.filters.limit: opcional, nombre màxim de dades a recuperar\n * \n */\n \n/** Estructura base de reqParams\nmsg.reqParams = {\n sentilo: {\n host: ,\n token: ,\n provider: ,\n filters: {\n from: ,\n to: ,\n limit: \n }\n }\n}*/\n\nfunction validatePaths(base, rules) {\n for (const [path, type] of Object.entries(rules)) {\n const isOpt = path.endsWith('?');\n const parts = path.replace('?', '').split('.');\n let current = base;\n\n for (const part of parts) if (!(current = current?.[part])) {\n if (!isOpt) { node.error(`[getLastObservations] Error: falta ${path}`); return false; }\n break; // Opcional i no trobat → OK\n }\n if (current && type && typeof current !== type && !(type === \"array\" && Array.isArray(current))) {\n node.error(`[getLastObservations] Error: ${path} no és tipus ${type}`); return false;\n }\n }\n return true;\n}\n\n// Validació compacta\nif (!validatePaths(msg, {\n \"reqParams\": \"object\",\n \"reqParams.sentilo\": \"object\",\n \"reqParams.sentilo.host\": \"string\",\n \"reqParams.sentilo.provider\": \"string\",\n \"reqParams.sentilo.token\": \"string\",\n \"reqParams.sentilo.filters?\": \"object\",\n \"reqParams.sentilo.filters.from?\": \"string\",\n \"reqParams.sentilo.filters.to?\": \"string\",\n \"reqParams.sentilo.filters.limit?\": \"number\",\n})) return null;\n\nconst { host, token, provider, filters } = msg.reqParams.sentilo;\n\n// Construcción de query string con filtros opcionales\nconst queryParams = [];\nif (filters?.from) queryParams.push(`from=${encodeURIComponent(filters.from)}`);\nif (filters?.to) queryParams.push(`to=${encodeURIComponent(filters.to)}`);\nif (filters?.limit) queryParams.push(`limit=${filters.limit}`);\n\nconst queryString = queryParams.length > 0 ? '?' + queryParams.join('&') : '';\nconst request = `${host}/data/${provider}${queryString}`;\n\n// Preparar msg para el nodo HTTP\nmsg.url = request;\nmsg.method = 'GET';\nmsg.headers = { 'identity_key': token };\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":200,"y":440,"wires":[["67116d8a245ab980"]]},{"id":"67116d8a245ab980","type":"http request","z":"bd296ee92e1677c7","name":"getLastObservations","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":440,"y":440,"wires":[["1ea80c1444f26a04","9c82da31b833c1d7"]]},{"id":"114e83ebdfa0b647","type":"comment","z":"bd296ee92e1677c7","name":"getLastObservations: Obtenir les darreres observacions","info":"","x":220,"y":400,"wires":[]},{"id":"4afae6626745132b","type":"link in","z":"bd296ee92e1677c7","name":"global.getLastObservations","links":[],"x":35,"y":440,"wires":[["3026d954cc2402c4","d3a8927d4f1b5489"]]},{"id":"1ea80c1444f26a04","type":"link out","z":"bd296ee92e1677c7","name":"getLastObservations","mode":"return","links":[],"x":585,"y":440,"wires":[]},{"id":"d3a8927d4f1b5489","type":"debug","z":"bd296ee92e1677c7","name":"global.getLastObservations reqParams","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"reqParams.sentilo","targetType":"msg","statusVal":"","statusType":"auto","x":230,"y":480,"wires":[]},{"id":"9c82da31b833c1d7","type":"debug","z":"bd296ee92e1677c7","name":"getLastObservations response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":760,"y":440,"wires":[]},{"id":"6ab4e463d49a13c8","type":"function","z":"bd296ee92e1677c7","name":"global.publishObservations","func":"/** \n * Publicació de d'observacions per a diversos sensors d'un proveïdor\n * \n * Paràmetres d'entrada:\n * \n * - reqParams.sentilo.api.host: obligatori, host de l'API de Sentilo\n * - reqParams.sentilo.api.provider: obligatori, provider sobre el que es recuperaran les dades\n * - reqParams.sentilo.api.token: obligatori, token de credencials de l'API de Sentilo\n * - payload: estructura d'observacions a publicar a Sentilo\n * \n */\n \n/** Estructura base de reqParams\nmsg.reqParams = {\n sentilo: {\n host: ,\n token: ,\n provider: ,\n }\n}*/\n\nfunction validatePaths(base, rules) {\n for (const [path, type] of Object.entries(rules)) {\n const isOpt = path.endsWith('?');\n const parts = path.replace('?', '').split('.');\n let current = base;\n\n for (const part of parts) if (!(current = current?.[part])) {\n if (!isOpt) { node.error(`[global.publishObservations] Error: falta ${path}`); return false; }\n break; // Opcional i no trobat → OK\n }\n if (current && type && typeof current !== type && !(type === \"array\" && Array.isArray(current))) {\n node.error(`[global.publishObservations] Error: ${path} no és tipus ${type}`); return false;\n }\n }\n return true;\n}\n\n// Validació compacta\nif (!validatePaths(msg, {\n \"reqParams\": \"object\",\n \"reqParams.sentilo.host\": \"string\",\n \"reqParams.sentilo.provider\": \"string\",\n \"reqParams.sentilo.token\": \"string\"\n})) return null;\n\nconst { reqParams, payload: observations } = msg;\nconst { host, provider, token } = reqParams.sentilo;\n\nconst request = `${host}/data/${provider}`;\n\nmsg.url = request;\nmsg.method = 'PUT';\nmsg.httpRequestTimeout = 300000; // 5 minuts\nmsg.headers = { 'identity_key': token };\nmsg.payload = observations;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":200,"y":560,"wires":[["faddc4880aa83605"]]},{"id":"faddc4880aa83605","type":"http request","z":"bd296ee92e1677c7","name":"publishObservations","method":"use","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":440,"y":560,"wires":[["0b52f84b3dd88fbe","fee5d246bbe61f9d"]]},{"id":"3646ef6b9f531147","type":"comment","z":"bd296ee92e1677c7","name":"publishObservations: Publicar observacions","info":"","x":190,"y":520,"wires":[]},{"id":"de6ceaad8a39e17a","type":"link in","z":"bd296ee92e1677c7","name":"global.publishObservations","links":[],"x":35,"y":560,"wires":[["6ab4e463d49a13c8","99b70739ab6fad25"]]},{"id":"fee5d246bbe61f9d","type":"link out","z":"bd296ee92e1677c7","name":"publishObservations","mode":"return","links":[],"x":585,"y":560,"wires":[]},{"id":"99b70739ab6fad25","type":"debug","z":"bd296ee92e1677c7","name":"publishObservations reqParams","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"reqParams.sentilo","targetType":"msg","statusVal":"","statusType":"auto","x":210,"y":600,"wires":[]},{"id":"0b52f84b3dd88fbe","type":"debug","z":"bd296ee92e1677c7","name":"publishObservations response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":760,"y":560,"wires":[]},{"id":"3b667db3f08c2920","type":"function","z":"bd296ee92e1677c7","name":"global.updateCatalog","func":"/** \n * Actualitzar components i sensors del catàleg Sentilo\n * \n * Paràmetres d'entrada:\n * \n * - reqParams.sentilo.api.host: obligatori, host de l'API de Sentilo\n * - reqParams.sentilo.api.token: obligatori, token de credencials de l'API de Sentilo\n * - reqParams.sentilo.componentsToUpdate: opcional, llistat de components a actualitzar\n * - reqParams.sentilo.sensorsToUpdate: opcional, llistat de sensors a actualitzar\n * \n */\n \n/** Estructura base de reqParams\nmsg.reqParams = {\n sentilo: {\n host: ,\n token: ,\n provider: ,\n componentsToUpdate: [COMPONENTS_TO_UPDATE],\n sensorsToUpdate: [SENSORS_TO_UPDATE],\n }\n}*/\n\nfunction validatePaths(base, rules) {\n for (const [path, expectedType] of Object.entries(rules)) {\n const isOptional = path.endsWith('?');\n const parts = path.replace('?', '').split('.');\n let current = base;\n\n for (const part of parts) {\n if (!(part in current)) {\n if (!isOptional) {\n node.error(`[global.updateCatalog] Error: falta \"${path}\"`);\n return false;\n } else {\n current = undefined;\n break;\n }\n }\n current = current[part];\n }\n\n if (current !== undefined && current !== null && expectedType) {\n const isArrayExpected = expectedType === \"array\";\n const isTypeCorrect = isArrayExpected ? Array.isArray(current) : typeof current === expectedType;\n\n if (!isTypeCorrect) {\n node.error(`[global.updateCatalog] Error: \"${path}\" no és tipus ${expectedType} (valor rebut: ${JSON.stringify(current)})`);\n return false;\n }\n } else if ((current === null || current === undefined) && !isOptional) {\n node.error(`[global.updateCatalog] Error: \"${path}\" és null o undefined`);\n return false;\n }\n }\n return true;\n}\n\n// Validació compacta\nif (!validatePaths(msg, {\n \"reqParams\": \"object\",\n \"reqParams.sentilo.provider\": \"string\",\n \"reqParams.sentilo.token\": \"string\", \n})) return null;\n\nconst { reqParams } = msg;\nconst { host, provider, token, componentsToUpdate: components, sensorsToUpdate: sensors } = reqParams.sentilo;\nconst request = `${host}/catalog/${provider}`;\n\nmsg.url = request;\nmsg.method = 'PUT';\nmsg.headers = { 'identity_key': token };\nmsg.payload = components || sensors;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":180,"y":320,"wires":[["ae0402ae63bc2cfe"]]},{"id":"ae0402ae63bc2cfe","type":"http request","z":"bd296ee92e1677c7","name":"updateCatalog","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":380,"y":320,"wires":[["a10370b5fb97f9df","942d6c8c1f4456a5"]]},{"id":"8bd8eb41f9e75d36","type":"comment","z":"bd296ee92e1677c7","name":"updateCatalog: Actualitzar dades del catàleg","info":"","x":190,"y":280,"wires":[]},{"id":"74687ee44af516b1","type":"link in","z":"bd296ee92e1677c7","name":"global.updateCatalog","links":[],"x":35,"y":320,"wires":[["3b667db3f08c2920","42c70e9127d8dc11"]]},{"id":"a10370b5fb97f9df","type":"link out","z":"bd296ee92e1677c7","name":"addCatalog","mode":"return","links":[],"x":495,"y":320,"wires":[]},{"id":"42c70e9127d8dc11","type":"debug","z":"bd296ee92e1677c7","name":"global.updateCatalog reqParams","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"reqParams.sentilo","targetType":"msg","statusVal":"","statusType":"auto","x":220,"y":360,"wires":[]},{"id":"942d6c8c1f4456a5","type":"debug","z":"bd296ee92e1677c7","name":"updateCatalog response","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":320,"wires":[]}]