diff --git a/.gitignore b/.gitignore index 35d328e6d61fa10cf76e954c16d272bb6dd0b0f5..b6c5e5d6cae8c12ec44d39ef7b9bc8cd18b98eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ desktop.ini .floo .flooignore .idea/ - +.vscode/ # Default # /!\ KEEP THIS SECTION THE LAST ONE !.gitkeep diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fef13ef2d2249f6cb2e84092c31acb1d14f8da6a..1ecbeffe212bf59e5699fd0fd98116492ffda2fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,8 +6,57 @@ # Note that environment variables can be set in several places # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence stages: -- test + - test + - build + - publish + sast: stage: test include: -- template: Security/SAST.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml + +build-dev: + image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:16.14.2-alpine3.14 + stage: build + before_script: + - apk add git + - apk add bash + script: + - yarn + - yarn build + - git config --global user.name build-token + - git config --global user.email "$GIT_USER" + - git config --global user.password "$GIT_PWD" + - git config user.email "$GIT_USER" + - git remote set-url origin https://"$GIT_USER":"$GIT_PWD"@forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector.git + - git config --global credential.helper store + - yarn deploy-dev + +build: + image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:16.14.2-alpine3.14 + stage: build + before_script: + - apk add git + - apk add bash + script: + - yarn + - yarn build + - git config --global user.name build-token + - git config --global user.email "$GIT_USER" + - git config --global user.password "$GIT_PWD" + - git config user.email "$GIT_USER" + - git remote set-url origin https://"$GIT_USER":"$GIT_PWD"@forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector.git + - git config --global credential.helper store + - yarn deploy + only: + - master + +publish: + stage: publish + before_script: + - apk add git + script: + - yarn cozyPublish + only: + - tags + when: manual diff --git a/manifest.konnector b/manifest.konnector index 4196b41f2d5a992ab20dd690dd8be1d5af580213..e26d1d5f5e99005e71c97dea70c3171a24e71b9e 100644 --- a/manifest.konnector +++ b/manifest.konnector @@ -1,24 +1,18 @@ { - "version": "1.0.0", + "version": "1.0.1", "name": "Enedis SGE", "type": "konnector", "language": "node", "icon": "icon.png", - "slug": "enedissgegrandlyon", + "slug": "enedis-sge-grandlyon", "source": "https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector.git", "editor": "Cozy", "vendor_link": "Link to the target website", "categories": ["energy"], "frequency": "daily", "fields": { - "pdl": { + "pointId": { "type": "text" - }, - "nom": { - "type": "password" - }, - "prenom": { - "type": "password" } }, "data_types": [ @@ -26,7 +20,11 @@ "screenshots": [], "permissions": { "accounts": { - "type": "io.cozy.accounts" + "type": "io.cozy.accounts", + "verbs": ["GET"] + }, + "files": { + "type": "io.cozy.files" }, "enedis data": { "type": "com.grandlyon.enedis.*" @@ -45,6 +43,9 @@ "enedis data": { "description": "Requises pour accéder et stocker les données collectées par le compteur Linky et exposées par les API Enedis (consommations d’électricité à la demi-heure, au jour, mois et année). " }, + "files": { + "description": "Cozy files" + }, "accounts": { "description": "Utilisé pour accéder à vos données de consommation." } @@ -57,6 +58,9 @@ "enedis data": { "description": "Required to access and store the data collected by the Linky meter and exposed by Enedis APIs (half-an-hour, daily, monthly and yearly consumption)." }, + "files": { + "description": "Cozy files" + }, "accounts": { "description": "Used to access your consumption data." } diff --git a/package.json b/package.json index 66f38956166c45037afe3a2470eec6c2098506ae..4fe35f1d23ffddd902dc14581a41098e311d9689 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,17 @@ "clean": "rm -rf ./data", "build": "webpack", "lint": "eslint --fix .", - "deploy": "git-directory-deploy --directory build/ --branch ${DEPLOY_BRANCH:-build} --repo=${DEPLOY_REPOSITORY:-$npm_package_repository_url}", + "deploy": "git-directory-deploy --directory build/ --branch ${DEPLOY_BRANCH:-build}", + "deploy-dev": "git-directory-deploy --directory build/ --branch ${DEPLOY_BRANCH:-build-dev}", "cozyPublish": "cozy-app-publish --token $REGISTRY_TOKEN --build-commit $(git rev-parse ${DEPLOY_BRANCH:-build})", "travisDeployKey": "./bin/generate_travis_deploy_key" }, "dependencies": { - "cozy-konnector-libs": "4.52.1" + "cozy-konnector-libs": "4.52.1", + "easy-soap-request": "^4.7.0", + "moment": "^2.29.3", + "moment-timezone": "^0.5.34", + "xml2js": "^0.4.23" }, "devDependencies": { "cozy-jobs-cli": "1.17.6", diff --git a/src/aggregate.js b/src/aggregate.js new file mode 100644 index 0000000000000000000000000000000000000000..808d34f0c36877aa7ab15856851e60a8a9168965 --- /dev/null +++ b/src/aggregate.js @@ -0,0 +1,106 @@ +// @ts-check +const { log, cozyClient } = require('cozy-konnector-libs') + +/** + * Retrieve and remove old data for a specific doctype + * Return an Array of agregated data + */ +async function buildAgregatedData(data, doctype) { + let agregatedData = [] + // eslint-disable-next-line no-unused-vars + for (let [key, value] of Object.entries(data)) { + const data = await buildDataFromKey(doctype, key, value) + const oldValue = await resetInProgressAggregatedData(data, doctype) + data.load += oldValue + agregatedData.push(data) + } + return agregatedData +} + +/** + * Format an entry for DB storage + * using key and value + * For year doctype: key = "YYYY" + * For month doctype: key = "YYYY-MM" + */ +async function buildDataFromKey(doctype, key, value) { + let year, month, day, hour + if (doctype === 'com.grandlyon.enedis.year') { + year = key + month = 1 + day = 0 + hour = 0 + } else if (doctype === 'com.grandlyon.enedis.month') { + const split = key.split('-') + year = split[0] + month = split[1] + day = 0 + hour = 0 + } else { + const split = key.split('-') + year = split[0] + month = split[1] + day = split[2] + hour = split[3] + } + return { + load: Math.round(value * 10000) / 10000, + year: parseInt(year), + month: parseInt(month), + day: parseInt(day), + hour: parseInt(hour), + minute: 0, + } +} + +/** + * Function handling special case. + * The temporary aggregated data need to be remove in order for the most recent one te be saved. + * ex for com.grandlyon.enedis.year : + * { load: 76.712, year: 2020, ... } need to be replace by + * { load: 82.212, year: 2020, ... } after enedis data reprocess + */ +async function resetInProgressAggregatedData(data, doctype) { + // /!\ Warning: cannot use mongo queries because not supported for dev by cozy-konnectors-libs + log('debug', doctype, 'Remove aggregated data for') + const result = await cozyClient.data.findAll(doctype) + if (result && result.length > 0) { + // Filter data to remove + var filtered = [] + if (doctype === 'com.grandlyon.enedis.year') { + // Yearly case + filtered = result.filter(function(el) { + return el.year == data.year + }) + } else if (doctype === 'com.grandlyon.enedis.month') { + // Monthly case + filtered = result.filter(function(el) { + return el.year == data.year && el.month == data.month + }) + } else { + // Hourly case + filtered = result.filter(function(el) { + return ( + el.year == data.year && + el.month == data.month && + el.day == data.day && + el.hour == data.hour + ) + }) + } + // Remove data + let sum = 0.0 + // eslint-disable-next-line no-unused-vars + for (const doc of filtered) { + sum += doc.load + log('debug', doc, 'Removing this entry for ' + doctype) + await cozyClient.data.delete(doctype, doc) + } + return sum + } + return 0.0 +} + +module.exports = { + buildAgregatedData, +} diff --git a/src/index.js b/src/index.js index be21704979cb340475979e699619d819bb0afd2a..197ad2aa3ce82ed988f99fe9b76d8768d9cfa0a9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,26 +1,49 @@ // @ts-check const { BaseKonnector, - requestFactory, - scrape, log, - utils, + hydrateAndFilter, + addData, + errors, } = require('cozy-konnector-libs') -const request = requestFactory({ - // The debug mode shows all the details about HTTP requests and responses. Very useful for - // debugging but very verbose. This is why it is commented out by default - // debug: true, - // Activates [cheerio](https://cheerio.js.org/) parsing on each page - cheerio: true, - // If cheerio is activated do not forget to deactivate json parsing (which is activated by - // default in cozy-konnector-libs - json: false, - // This allows request-promise to keep cookies between requests - jar: true, -}) - -const VENDOR = 'template' -const baseUrl = 'http://books.toscrape.com' +const soapRequest = require('easy-soap-request') +const moment = require('moment') +require('moment-timezone') +const xml2js = require('xml2js') +const { buildAgregatedData } = require('./aggregate') +const { + parseSgeXmlData, + parseSgeXmlTechnicalData, + formateDataForDoctype, + parseTags, + parseValue, +} = require('./parsing') +const { + userTechnicalData, + userMaxPower, + userMesureDetailles, + rechercherPoint, + activateDataCollect, + stopDataCollect, + getInseeCode, + updateBoConsent, + createBoConsent, + getBoConsent, + deleteBoConsent, +} = require('./request') +moment.locale('fr') // set the language +moment.tz.setDefault('Europe/Paris') // set the timezone + +/*** Connector Constants ***/ +const manualExecution = + process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false +let startDailyDate = manualExecution + ? moment().subtract(12, 'month') + : moment().subtract(6, 'month') +let startDailyDateString = startDailyDate.format('YYYY-MM-DD') +const startLoadDate = moment().subtract(7, 'day') +const endDate = moment() +const endDateString = endDate.format('YYYY-MM-DD') module.exports = new BaseKonnector(start) @@ -30,96 +53,439 @@ module.exports = new BaseKonnector(start) // cozyParameters are static parameters, independents from the account. Most often, it can be a // secret api key. async function start(fields, cozyParameters) { + log('info', 'Gathering data ...') + let baseUrl = fields.wso2BaseUrl + let apiAuthKey = fields.apiToken + let loginUtilisateur = fields.loginUtilisateur log('info', 'Authenticating ...') - if (cozyParameters) log('debug', 'Found COZY_PARAMETERS') - await authenticate.bind(this)(fields.login, fields.password) + //TODO: Verify if condition is working in local and on build version + if (cozyParameters && Object.keys(cozyParameters).length !== 0) { + log('debug', 'Found COZY_PARAMETERS') + baseUrl = cozyParameters.secret.wso2BaseUrl + apiAuthKey = cozyParameters.secret.apiToken + loginUtilisateur = cozyParameters.secret.loginUtilisateur + } + + /** + * If it's first start we have to do the following operations: + * - verify pdl are matching + * - BO: create backoffice consent + * - get contract start date and store it + * - activate half-hour + * - BO: update consent with service ID + */ + log('info', 'User Logging...') + + if (await isFirstStart()) { + if (!(await verifyUserIdentity(fields))) { + throw errors.LOGIN_FAILED + } + await createBoConsent() + //TODO: remove because useless ? Done later in code + // const startDate = await getDataStartDate( + // baseUrl, + // apiAuthKey, + // loginUtilisateur, + // fields.pointId + // ) + await activateDataCollect() + await updateBoConsent() + } else { + await getBoConsent() + if (!(await verifyUserIdentity(fields))) { + await deleteBoConsent() + await stopDataCollect() + throw errors.TERMS_VERSION_MISMATCH + } + } log('info', 'Successfully logged in') - // The BaseKonnector instance expects a Promise as return of the function - log('info', 'Fetching the list of documents') - const $ = await request(`${baseUrl}/index.html`) - // cheerio (https://cheerio.js.org/) uses the same api as jQuery (http://jquery.com/) - log('info', 'Parsing list of documents') - const documents = await parseDocuments($) - - // Here we use the saveBills function even if what we fetch are not bills, - // but this is the most common case in connectors - log('info', 'Saving data to Cozy') - await this.saveBills(documents, fields, { - // This is a bank identifier which will be used to link bills to bank operations. These - // identifiers should be at least a word found in the title of a bank operation related to this - // bill. It is not case sensitive. - identifiers: ['books'], - }) + + await gatherData(baseUrl, apiAuthKey, loginUtilisateur, fields.pointId) +} + +/** + * Verify user identity + * @param {object} fields + */ +async function verifyUserIdentity(fields) { + const inseeCode = getInseeCode(fields.postalCode) + const user = await findUser( + fields.name, + fields.addresse, + fields.postalCode, + inseeCode + ) + if (fields.pointId !== user.pointId) { + log('error', 'PointId does not match') + return false + } + return true +} + +/** + * Main method for gathering data + * @param {string} baseUrl + * @param {string} apiAuthKey + * @param {string} loginUtilisateur + * @param {number} pointId + */ +async function gatherData(baseUrl, apiAuthKey, loginUtilisateur, pointId) { + log('info', 'Querying data...') + await getDataStartDate( + `${baseUrl}/enedis_SGE_ConsultationDonneesTechniquesContractuelles/1.0`, + apiAuthKey, + loginUtilisateur, + pointId + ) + await getData( + `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, + apiAuthKey, + loginUtilisateur, + pointId + ) + await getMaxPowerData( + `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, + apiAuthKey, + loginUtilisateur, + pointId + ) + await getDataHalfHour( + `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, + apiAuthKey, + loginUtilisateur, + pointId + ) + log('info', 'Querying data: done') } -// This shows authentication using the [signin function](https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#module_signin) -// even if this in another domain here, but it works as an example -function authenticate(username, password) { - return this.signin({ - url: `http://quotes.toscrape.com/login`, - formSelector: 'form', - formData: { username, password }, - // The validate function will check if the login request was a success. Every website has a - // different way to respond: HTTP status code, error message in HTML ($), HTTP redirection - // (fullResponse.request.uri.href)... - validate: (statusCode, $, fullResponse) => { - log( - 'debug', - fullResponse.request.uri.href, - 'not used here but should be useful for other connectors' - ) - // The login in toscrape.com always works except when no password is set - if ($(`a[href='/logout']`).length === 1) { - return true - } else { - // cozy-konnector-libs has its own logging function which format these logs with colors in - // standalone and dev mode and as JSON in production mode - log('error', $('.error').text()) - return false - } +/** + * + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {number} pointId + */ +async function getDataStartDate(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching data start date') + const sampleHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: url, + headers: sampleHeaders, + xml: userTechnicalData(pointId, userLogin), + }).catch(err => { + log('error', 'technicalDataResponse') + log('error', err) + return err + }) + + xml2js.parseString( + response.body, + { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, }, + processStartDate() + ) +} + +/** + * Get hour data + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {number} pointId + */ +async function getData(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching data') + const sampleHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + setStartDate() + + const { response } = await soapRequest({ + url: url, + headers: sampleHeaders, + xml: userMesureDetailles( + pointId, + userLogin, + startDailyDateString, + endDateString + ), + }).catch(err => { + log('error', 'userMesureDetailles') + log('error', err) + return err }) + + xml2js.parseString( + response.body, + { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }, + processData() + ) } -// The goal of this function is to parse a HTML page wrapped by a cheerio instance -// and return an array of JS objects which will be saved to the cozy by saveBills -// (https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#savebills) -function parseDocuments($) { - // You can find documentation about the scrape function here: - // https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#scrape - const docs = scrape( - $, +/** + * Get Max power data + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {number} pointId + */ +async function getMaxPowerData(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching Max Power data') + const sampleHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + setStartDate() + + const { response } = await soapRequest({ + url: url, + headers: sampleHeaders, + xml: userMaxPower(pointId, userLogin, startDailyDateString, endDateString), + }).catch(err => { + log('error', 'getMaxPowerData') + log('error', err) + return err + }) + + xml2js.parseString( + response.body, { - title: { - sel: 'h3 a', - attr: 'title', - }, - amount: { - sel: '.price_color', - parse: normalizePrice, - }, - fileurl: { - sel: 'img', - attr: 'src', - parse: src => `${baseUrl}/${src}`, - }, + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, }, - 'article' + processData('com.grandlyon.enedis.maxpower') ) - return docs.map(doc => ({ - ...doc, - // The saveBills function needs a date field - // even if it is a little artificial here (these are not real bills) - date: new Date(), - currency: 'EUR', - filename: `${utils.formatDate(new Date())}_${VENDOR}_${doc.amount.toFixed( - 2 - )}EUR${doc.vendorRef ? '_' + doc.vendorRef : ''}.jpg`, - vendor: VENDOR, - })) } -// Convert a price string to a float -function normalizePrice(price) { - return parseFloat(price.replace('£', '').trim()) +/** + * If start date exceed the maximum amount of data we can get with one query + * get only 36 month + */ +function setStartDate() { + if (moment(endDate).diff(startDailyDate, 'months', true) > 36) { + log( + 'info', + 'Start date exceed 36 month, setting start date to current date minus 36 month' + ) + startDailyDate = moment(endDate).subtract(36, 'month') + startDailyDateString = startDailyDate.format('YYYY-MM-DD') + } +} + +/** + * Get half-hour data + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {number} pointId + */ +async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching data') + const sampleHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + let MAX_HISTO = 4 + // If manual execution, retrieve only 1 week + if (manualExecution) { + MAX_HISTO = 1 + } + for (var i = 0; i < MAX_HISTO; i++) { + log('info', 'launch process with history') + const increamentedStartDateString = moment(startLoadDate) + .subtract(7 * i, 'day') + .format('YYYY-MM-DD') + const incrementedEndDateString = moment(endDate) + .subtract(7 * i, 'day') + .format('YYYY-MM-DD') + const { response } = await soapRequest({ + url: url, + headers: sampleHeaders, + xml: userMesureDetailles( + pointId, + userLogin, + increamentedStartDateString, + incrementedEndDateString, + 'COURBE', + 'PA' + ), + }).catch(err => { + log('error', 'userMesureDetailles half-hour') + log('error', err) + return err + }) + + xml2js.parseString( + response.body, + { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }, + processData('com.grandlyon.enedis.minute') + ) + } +} + +/** + * Parse data + * @param {string} doctype + * @returns + */ +function processData(doctype = 'com.grandlyon.enedis.day') { + return async (err, result) => { + if (err) { + log('error', err) + throw err + } + // Return only needed part of info + const data = parseSgeXmlData(result) + const processedDailyData = await storeData( + await formateDataForDoctype(data), + doctype, + ['year', 'month', 'day', 'hour', 'minute'] + ) + + log('info', 'Agregate enedis daily data for month and year') + if (doctype === 'com.grandlyon.enedis.day') { + log('info', 'Agregating...') + await agregateMonthAndYearData(processedDailyData) + } + } +} + +/** + * Store an accurate start date based on contrat start + */ +function processStartDate() { + return async (err, result) => { + if (err) { + log('error', err) + throw err + } + // update start Date with contract openning date + try { + startDailyDate = moment(parseSgeXmlTechnicalData(result), 'YYYY-MM-DD') + startDailyDateString = startDailyDate.format('YYYY-MM-DD') + } catch (err) { + log('error', err) + //TODO: custom error ? + throw err + } + } +} + +/** + * Save data in the right doctype db and prevent duplicated keys + * @param {EnedisKonnectorData[]} data + * @param {string} doctype + * @param {string[]} filterKeys + * @returns {Promise<*>} + */ +async function storeData(data, doctype, filterKeys) { + log('debug', doctype, 'Store into') + const filteredDocuments = await hydrateAndFilter(data, doctype, { + keys: filterKeys, + }) + await addData(filteredDocuments, doctype) + return filteredDocuments +} + +/** + * Agregate data from daily data to monthly and yearly data + */ +async function agregateMonthAndYearData(data) { + // Sum year and month values into object with year or year-month as keys + if (data && data.length > 0) { + let monthData = {} + let yearData = {} + data.forEach(element => { + element.year + '-' + element.month in monthData + ? (monthData[element.year + '-' + element.month] += element.load) + : (monthData[element.year + '-' + element.month] = element.load) + element.year in yearData + ? (yearData[element.year] += element.load) + : (yearData[element.year] = element.load) + }) + // Agregation for Month data + const agregatedMonthData = await buildAgregatedData( + monthData, + 'com.grandlyon.enedis.month' + ) + await storeData(agregatedMonthData, 'com.grandlyon.enedis.month', [ + 'year', + 'month', + ]) + // Agregation for Year data + const agregatedYearData = await buildAgregatedData( + yearData, + 'com.grandlyon.enedis.year' + ) + await storeData(agregatedYearData, 'com.grandlyon.enedis.year', ['year']) + } +} + +/** + * @returns {boolean} + */ +function isFirstStart() { + //TODO: Implement + return false +} + +/** + * @return {User} + */ +async function findUser( + url, + apiAuthKey, + appLogin, + name, + addresse, + postalCode, + inseeCode +) { + log('info', 'Fetching user data') + const sampleHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: url, + headers: sampleHeaders, + xml: rechercherPoint(appLogin, name, addresse, postalCode, inseeCode), + }).catch(err => { + log('error', 'rechercherPointResponse') + log('error', err) + throw errors.LOGIN_FAILED + //TODO: handling code error SGT4F6 and SGT432 into USER_ACTIon_NEEDED + }) + + //TODO: handle reply + xml2js.parseString( + response.body, + { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }, + processStartDate() + ) } diff --git a/src/parsing.js b/src/parsing.js new file mode 100644 index 0000000000000000000000000000000000000000..64e6e7e57bd7e034d79bc4aceac97bce4ec11081 --- /dev/null +++ b/src/parsing.js @@ -0,0 +1,85 @@ +// @ts-check +const { log } = require('cozy-konnector-libs') +const moment = require('moment') + +/** + * Return start date + * @param {string} result + * @returns {string} + */ +function parseSgeXmlTechnicalData(result) { + log('info', 'Parsing technical data') + let json = JSON.stringify(result) + return JSON.parse(json)['Envelope']['Body'][ + 'consulterDonneesTechniquesContractuellesResponse' + ]['point']['donneesGenerales'][ + 'dateDerniereModificationFormuleTarifaireAcheminement' + ] +} + +/** + * Parsing SGE xml reply to get only mesure data + * @param {string} result + * @returns {SGEData[]} + */ +function parseSgeXmlData(result) { + log('info', 'Parsing list of documents') + let json = JSON.stringify(result) + return JSON.parse(json)['Envelope']['Body'][ + 'consulterMesuresDetailleesResponse' + ]['grandeur']['mesure'] +} + +/** + * Format data for DB storage + * @param {SGEData[]} data + * @returns {Promise<EnedisKonnectorData[]>} Parsed timestamp array + */ +async function formateDataForDoctype(data) { + log('info', 'Formating data') + return data.map(record => { + let date = moment(record.d, 'YYYY/MM/DD h:mm:ss') + return { + load: record.v, + year: parseInt(date.format('YYYY')), + month: parseInt(date.format('M')), + day: parseInt(date.format('D')), + hour: parseInt(date.format('H')), + minute: parseInt(date.format('m')), + } + }) +} + +/** + * Format tag in order to be manipulated easly + * @param {string} name + * @returns {string} name + */ +function parseTags(name) { + if (name.split(':')[1] !== undefined) { + return name.split(':')[1] + } + return name +} + +/** + * + * @param {string} value + * @param {string} name + * @returns {string|number} value + */ +function parseValue(value, name) { + // Wh => KWh + if (name === 'v') { + return parseFloat((parseInt(value) / 1000).toFixed(2)) + } + return value +} + +module.exports = { + parseSgeXmlData, + parseSgeXmlTechnicalData, + formateDataForDoctype, + parseTags, + parseValue, +} diff --git a/src/request.js b/src/request.js new file mode 100644 index 0000000000000000000000000000000000000000..c8bcc1bdac5876b79a5af2744bc0df94e2338aa3 --- /dev/null +++ b/src/request.js @@ -0,0 +1,314 @@ +// @ts-check +const { log } = require('cozy-konnector-libs') + +/** + * Query SGE in order to get info + * @param {number} pointId + * @param {string} appLogin + * @param {string} startDt + * @param {string} endDt + * @returns {string} + */ +function userMesureDetailles( + pointId, + appLogin, + startDt, + endDt, + mesureType = 'ENERGIE', + unit = 'EA' +) { + log( + 'info', + `Query data ${mesureType}/${unit} between ${startDt} and ${endDt}` + ) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/services/consultationmesuresdetaillees/v2.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:consulterMesuresDetaillees> + <demande> + <initiateurLogin>${appLogin}</initiateurLogin> + <pointId>${pointId}</pointId> + <mesuresTypeCode>${mesureType}</mesuresTypeCode> + <grandeurPhysique>${unit}</grandeurPhysique> + <soutirage>true</soutirage> + <injection>false</injection> + <dateDebut>${startDt}</dateDebut> + <dateFin>${endDt}</dateFin> + <mesuresCorrigees>false</mesuresCorrigees> + <accordClient>true</accordClient> + </demande> + </v2:consulterMesuresDetaillees> + </soapenv:Body> + </soapenv:Envelope> + ` +} + +/** + * Get user technical data + * @param {number} pointId + * @param {string} appLogin + * @returns {string} + */ +function userTechnicalData(pointId, appLogin) { + log('info', `Query userMesureDetailles`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/services/consulterdonneestechniquescontractuelles/v1.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:consulterDonneesTechniquesContractuelles> + <pointId>${pointId}</pointId> + <loginUtilisateur>${appLogin}</loginUtilisateur> + <autorisationClient>true</autorisationClient> + </v2:consulterDonneesTechniquesContractuelles> + </soapenv:Body> + </soapenv:Envelope> + ` +} + +/** + * Get user max power + * @param {number} pointId + * @param {string} appLogin + * @param {string} startDt + * @param {string} endDt + * @returns {string} + */ +function userMaxPower(pointId, appLogin, startDt, endDt) { + log('info', `Query userMesureDetailles`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/services/consultationmesuresdetaillees/v2.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:consulterMesuresDetaillees> + <demande> + <initiateurLogin>${appLogin}</initiateurLogin> + <pointId>${pointId}</pointId> + <mesuresTypeCode>PMAX</mesuresTypeCode> + <grandeurPhysique>PMA</grandeurPhysique> + <soutirage>true</soutirage> + <injection>false</injection> + <dateDebut>${startDt}</dateDebut> + <dateFin>${endDt}</dateFin> + <mesuresPas>P1D</mesuresPas> + <mesuresCorrigees>false</mesuresCorrigees> + <accordClient>true</accordClient> + </demande> + </v2:consulterMesuresDetaillees> + </soapenv:Body> + </soapenv:Envelope> + ` +} + +/** + * Use rechercherPoint to find user PDL if exist + * @param {string} name + * @param {string} postalCode + * @param {string} address + * @returns {string} PDL + */ +function rechercherPoint(appLogin, name, postalCode, inseeCode, address) { + log('info', `Query rechercherPoint`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/services/rechercherpoint/v2.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:rechercherPoint> + <criteres> + <adresseInstallation> + <numeroEtNomVoie>${address}</numeroEtNomVoie> + <codePostal>${postalCode}</codePostal> + <codeInseeCommune>${inseeCode}</codeInseeCommune> + </adresseInstallation> + <nomClientFinalOuDenominationSociale>${name}</nomClientFinalOuDenominationSociale> + <rechercheHorsPerimetre>true</rechercheHorsPerimetre> + </criteres> + <loginUtilisateur>${appLogin}</loginUtilisateur> + </v2:rechercherPoint> + </soapenv:Body> + </soapenv:Envelope>` +} + +/** + * Search if user as a service + * @param {string} appLogin + * @param {string} contractId + * @param {string} pointId + * @returns {*} + */ +function searchServiceSouscrit(appLogin, contractId, pointId) { + log('info', `Query activateDataCollect`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/rechercherservicessouscritsmesures/v1.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:rechercherServicesSouscritsMesures> + <criteres> + <pointId>${pointId}</pointId> + <contratId>${contractId}</contratId> + </criteres> + <loginUtilisateur>${appLogin}</loginUtilisateur> + </v2:rechercherServicesSouscritsMesures> + </soapenv:Body> + </soapenv:Envelope>` +} + +/** + * Activate half hour data collect for user + * @param {string} appLogin + * @param {string} contractId + * @param {string} pointId + * @param {string} name + * @param {string} startDate + * @param {string} endDate + * @returns {*} + */ +function activateDataCollect( + appLogin, + contractId, + pointId, + name, + startDate, + endDate +) { + log('info', `Query activateDataCollect`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/commandercollectepublicationmesures/v3.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:commanderCollectePublicationMesures> + <demande> + <donneesGenerales> + <objetCode>AME</objetCode> + <pointId>${pointId}</pointId> + <initiateurLogin>${appLogin}</initiateurLogin> + <contratId>${contractId}</contratId> + </donneesGenerales> + <accesMesures> + <dateDebut>${startDate}</dateDebut> + <dateFin>${endDate}</dateFin> + <declarationAccordClient> + <accord>true</accord> + <personnePhysique> + <nom>${name}</nom> + </personnePhysique> + </declarationAccordClient> + <mesuresTypeCode>CDC</mesuresTypeCode> + <soutirage>true</soutirage> + <injection>false</injection> + <mesuresPas>PT30M</mesuresPas> + <mesuresCorrigees>false</mesuresCorrigees> + <transmissionRecurrente>true</transmissionRecurrente> + <periodiciteTransmission>P1D</periodiciteTransmission> + </accesMesures> + </demande> + </v2:commanderCollectePublicationMesures> + </soapenv:Body> + </soapenv:Envelope>` +} + +/** + * + * @param {string} appLogin + * @param {string} contractId + * @param {string} pointId + * @param {string} serviceSouscritId + * @returns {*} + */ +function stopDataCollect(appLogin, contractId, pointId, serviceSouscritId) { + log('info', `Query stopDataCollect`) + return `<?xml version='1.0' encoding='utf-8'?> + <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:v2="http://www.enedis.fr/sge/b2b/commanderarretservicesouscritmesures/v1.0" + xmlns:v1="http://www.enedis.fr/sge/b2b/technique/v1.0"> + <soapenv:Header/> + <soapenv:Body> + <v2:commanderArretServiceSouscritMesures> + <demande> + <donneesGenerales> + <objetCode>ASS</objetCode> + <pointId>${pointId}</pointId> + <initiateurLogin>${appLogin}</initiateurLogin> + <contratId>${contractId}</contratId> + </donneesGenerales> + <arretServiceSouscrit> + <serviceSouscritId>${serviceSouscritId}</serviceSouscritId> + </arretServiceSouscrit> + </demande> + </v2:commanderArretServiceSouscritMesures> + </soapenv:Body> + </soapenv:Envelope>` +} + +/** + * Return inseeCode given a postalCode + * @param {string} postalCode + * @return {string} inseeCode + */ +function getInseeCode(postalCode) { + //TODO: Implement + log('info', `Query getInseeCode for postalCode ${postalCode}`) + throw new Error('Function not implemented.') +} + +/** + * + */ +function createBoConsent() { + //TODO: Implement + log('info', `Query createBoConsent`) + throw new Error('Function not implemented.') +} + +/** + * + */ +function updateBoConsent() { + //TODO: Implement + log('info', `Query updateBoConsent`) + throw new Error('Function not implemented.') +} + +/** + * + */ +function getBoConsent() { + //TODO: Implement + log('info', `Query getBoConsent`) + throw new Error('Function not implemented.') +} +/** + * + */ +function deleteBoConsent() { + //TODO: deleteBoConsent + log('info', `Query createBoConsent`) + throw new Error('Function not implemented.') +} + +module.exports = { + userTechnicalData, + userMaxPower, + userMesureDetailles, + rechercherPoint, + searchServiceSouscrit, + activateDataCollect, + stopDataCollect, + getInseeCode, + createBoConsent, + updateBoConsent, + getBoConsent, + deleteBoConsent, +} diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000000000000000000000000000000000000..b2d1f4016528c80d44ce27bc9b6c461fd1c170c6 --- /dev/null +++ b/src/types.js @@ -0,0 +1,26 @@ +/** + * EnedisKonnectorData definition + * @typedef {object} EnedisKonnectorData + * @property {number} year + * @property {number} month + * @property {number} day + * @property {number} hour + * @property {number} minute + */ + +/** + * SGEData definition + * @typedef {object} SGEData + * @property {number} v + * @property {string} d + */ + +/** + * User definition + * @typedef {object} User + * @property {string} name + * @property {string} address + * @property {string} postalCode + * @property {string} pointId + * @property {string} [inseeCode] + */ diff --git a/yarn.lock b/yarn.lock index 2985990f679a5ca343b58634ee8056f5132b5f2a..b132129a871c880fbf6d6d2b97ea53ce06183817 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1797,6 +1797,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-eslint@10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed" @@ -2809,6 +2816,13 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: domelementtype "^2.2.0" domhandler "^4.2.0" +easy-soap-request@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/easy-soap-request/-/easy-soap-request-4.7.0.tgz#d57af87fc91a3c8ff606e3ca1102a2c37a095ff8" + integrity sha512-rFvXxk65ROqUkSSgVbxS4X9skRkdBPU4eoETCzgWaKh5seVibLD8feY5F0YV6UTnY7ErcM9DWMG3RNaYm02xHA== + dependencies: + axios "^0.26.1" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3568,6 +3582,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -4612,6 +4631,18 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +moment-timezone@^0.5.34: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== + morgan@^1.9.1: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" @@ -5686,7 +5717,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -6622,6 +6653,19 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"