Skip to content
Snippets Groups Projects
index.js 12.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • rpapin's avatar
    rpapin committed
    const {
    
    remip's avatar
    remip committed
      cozyClient,
    
    rpapin's avatar
    rpapin committed
      BaseKonnector,
      addData,
      hydrateAndFilter,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      errors,
    
    remip's avatar
    remip committed
      log
    
    rpapin's avatar
    rpapin committed
    } = require('cozy-konnector-libs')
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
    // const https = require('https') //optional for ssl issue
    const getAccountId = require('./helpers/getAccountId')
    
    const jwt = require('jsonwebtoken')
    
    rpapin's avatar
    rpapin committed
    const moment = require('moment')
    require('moment-timezone')
    
    moment.locale('fr') // set the language
    moment.tz.setDefault('Europe/Paris') // set the timezone
    
    const startDate = moment()
    
    remip's avatar
    remip committed
      .startOf('day')
      .subtract(4, 'day')
      .format('YYYY-MM-DD')
    const endDate = moment().format('YYYY-MM-DD')
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    // const baseUrl = 'https://api.grdf.fr/adict/v1'
    
    remip's avatar
    remip committed
    //console.log(startDate)
    //console.log(endDate)
    
    remip's avatar
    remip committed
    /*const timeRange = ['day', 'month', 'year']
    
    rpapin's avatar
    rpapin committed
    const rangeDate = {
      day: {
        doctype: 'com.grandlyon.grdf.day',
        keys: ['year', 'month', 'day']
      },
      month: {
        doctype: 'com.grandlyon.grdf.month',
        keys: ['year', 'month']
      },
      year: {
        doctype: 'com.grandlyon.grdf.year',
        keys: ['year']
      }
    }
    
    
    remip's avatar
    remip committed
    const client_id = 'metropole_de_lyon_grdf'
    const redirectUrl = 'https://grdf.pchugo.wf.alpha.grandlyon.com'*/
    
    rpapin's avatar
    rpapin committed
    
    module.exports = new BaseKonnector(start)
    
    // The start function is run by the BaseKonnector instance only when it got all the account
    // information (fields). When you run this connector yourself in "standalone" mode or "dev" mode,
    // the account information come from ./konnector-dev-config.json file
    // cozyParameters are static parameters, independents from the account. Most often, it can be a
    // secret api key.
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    async function start(fields, cozyParameters, doRetry = true) {
      // var grdfPCE = ''
      log('debug', 'Starting grdf adict konnector')
      // if (cozyParameters) log('info', 'CozyParameters found')
      // const grdfToken = await authenticate(fields.login, fields.password)
    
      const accountId = getAccountId()
      try {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        // const low_scope_access_token = fields.access_token // To use for /donnees-techniques -> fréquence du compteur
        const high_scope_access_token = await authenticate(
          fields.client_id,
          fields.client_secret
        )
    
        // is account necessary ? maybe id_token is already in fields from the begining
        log('debug', 'KONNECTOR FIELDS : ' + high_scope_access_token)
        log(
          'debug',
          'KONNECTOR ACCOUNT : ' + this._account.oauth_callback_results.id_token
        )
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        if (
          this._account &&
          this._account.oauth_callback_results &&
          this._account.oauth_callback_results.id_token
        ) {
          log('debug', 'time to decode jwt')
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
          // getPCEid(fields.id_token) ?
    
          const id_pce = await getPCEid(
            this._account.oauth_callback_results.id_token
          ) //19108248691849 PCEid FOR TESTING PURPOSE
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          log('debug', 'Fetching grdf adict daily data')
    
    git-directory-deploy's avatar
    git-directory-deploy committed
          log('debug', 'THE ID_PCE SENDING TO GETDATA : ' + id_pce)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          const grdfData = await getData(high_scope_access_token, id_pce)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
          if (grdfData.consommation) {
            log('debug', 'Process grdf daily data')
            const processedLoadData = await processData(
              grdfData,
              'com.grandlyon.grdf.day',
              ['year', 'month', 'day']
            )
            log('debug', 'Agregate grdf load data for month and year')
            await agregateMonthAndYearData(processedLoadData)
          } else {
            log('debug', 'No consent or data for load curve')
          }
          // await addData(data, "io.cozy.accounts", accountId);
          // return start(fields, cozyParameters, true);
        } else {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          log('debug', 'no id_token found in oauth_callback_results')
          log(
            'debug',
            'callback_result contains: ',
            this._account.oauth_callback_results
          )
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
        }
      } catch (err) {
        if (
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          err.statusCode === 403 ||
          err.code === 403 ||
          err.statusCode === 401 ||
          err.code === 401
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        ) {
          if (doRetry) {
            log('debug', 'asking refresh from the stack')
            let body
            try {
              body = await cozyClient.fetchJSON(
                'POST',
                `/accounts/grdf-konnector/${accountId}/refresh`
              )
            } catch (err) {
              log('debug', `Error during refresh ${err.message}`)
              throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
            }
    
            log('debug', 'refresh response')
            log('debug', JSON.stringify(body))
            fields.access_token = body.attributes.oauth.access_token
            return start(fields, cozyParameters, false)
          }
          log('error', `Error during authentication: ${err.message}`)
          throw errors.VENDOR_DOWN
        } else {
          log('error', 'caught an unexpected error')
          log('error', err.message)
          throw errors.VENDOR_DOWN
        }
    
    rpapin's avatar
    rpapin committed
      }
    }
    
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    async function authenticate(client_id, client_secret) {
      let myHeaders = new Headers()
      myHeaders.append('Content-Type', 'application/x-www-form-urlencoded')
      myHeaders.append('Authorization', 'Basic cG9jX2FwaTpwb2NfYXBp')
    
      let urlencoded = new URLSearchParams()
      urlencoded.append('grant_type', 'client_credentials')
      urlencoded.append('client_id', client_id)
      urlencoded.append('client_secret', client_secret)
      urlencoded.append('scope', '/adict/v1')
    
      let requestOptions = {
        method: 'POST',
        headers: myHeaders,
        body: urlencoded,
        redirect: 'follow'
      }
    
      const rep = await fetch(
        'https://sofit-sso-oidc.grdf.fr/openam/oauth2/realms/externeGrdf/access_token',
        requestOptions
      )
        .then(response => response.text())
        .then(result => {
          result = JSON.parse(result)
          log('info', result)
          return result.access_token
        })
        .catch(error => log('error', error))
      return rep
    }
    
    
    remip's avatar
    remip committed
    async function getPCEid(tokenToDecrypt) {
      // verify a token symmetric - synchronous
    
    git-directory-deploy's avatar
    git-directory-deploy committed
      log('debug', 'GETPCEID : ' + tokenToDecrypt)
    
    remip's avatar
    remip committed
      var decoded = jwt.decode(tokenToDecrypt)
    
    remip's avatar
    remip committed
      const decodedConsentements = JSON.parse(decoded.consentements)
      return decodedConsentements[0].pce
    }
    
    rpapin's avatar
    rpapin committed
    //Retrieve data from grdf API
    
    remip's avatar
    remip committed
    async function getData(token, idPCE) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      log('debug', 'ENTERING GETDATA')
    
    remip's avatar
    remip committed
      var myHeaders = new Headers()
      myHeaders.append('Content-Type', 'application/x-ndjson')
      myHeaders.append('Authorization', 'Bearer ' + token)
    
    rpapin's avatar
    rpapin committed
    
      var requestOptions = {
        method: 'GET',
        headers: myHeaders,
        redirect: 'follow'
    
    remip's avatar
    remip committed
      }
      var url =
        'https://api.grdf.fr/adict/v1/pce/' +
        idPCE +
    
    remip's avatar
    remip committed
        '/donnees_consos_informatives?date_debut=' +
        startDate +
        '&date_fin=' +
        endDate
    
    rpapin's avatar
    rpapin committed
      const rep = await fetch(url, requestOptions)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        .then(response => response.text())
    
    rpapin's avatar
    rpapin committed
        .then(result => {
    
    remip's avatar
    remip committed
          result = JSON.parse(result)
    
    rpapin's avatar
    rpapin committed
          return result
        })
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        .catch(error => log('error', error))
    
    remip's avatar
    remip committed
      return rep
    
    rpapin's avatar
    rpapin committed
    }
    
    /**
     * Parse data
     * Remove existing data from DB using hydrateAndFilter
     * Store filtered data
     * Return the list of filtered data
     */
    async function processData(data, doctype, filterKeys) {
    
    remip's avatar
    remip committed
      const intervalData = data.consommation
      const formatedData = await formateData(intervalData)
      const dataArray = [formatedData]
    
    rpapin's avatar
    rpapin committed
      // Remove data for existing days into the DB
    
    remip's avatar
    remip committed
      const filteredData = await hydrateAndFilter(dataArray, doctype, {
    
    rpapin's avatar
    rpapin committed
        keys: filterKeys
      })
      // Store new day data
      await storeData(filteredData, doctype, filterKeys)
      return filteredData
    }
    
    /**
     * Save data in the right doctype db and prevent duplicated keys
     */
    async function storeData(data, doctype, filterKeys) {
      log('debug', doctype, 'Store into')
      const filteredDocuments = await hydrateAndFilter(data, doctype, {
        keys: filterKeys
      })
      return await addData(filteredDocuments, doctype)
    }
    
    /**
     * Format data for DB storage
     * Remove bad data
     */
    
    remip's avatar
    remip committed
    async function formateData(data) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      log('debug', 'Formating data')
    
    remip's avatar
    remip committed
    
    
    remip's avatar
    remip committed
      let date = moment(data.date_debut_consommation, 'YYYY/MM/DD h:mm:ss')
      let load =
        data.energie !== 0
          ? data.energie
          : data.volume_brut * data.coeff_calcul.coeff_conversion
    
      //TRAITEMENT DES MINUTES
      // const load =
      //   doctype === 'com.grandlyon.grdf.minute'
      //     ? record.value / 2
      //     : record.value
      // if (doctype === 'com.grandlyon.grdf.minute') {
      //   date = date.subtract(30, 'minute')
      // }
      return {
        load: parseFloat(load),
        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'))
      }
    
    rpapin's avatar
    rpapin committed
    }
    
    /**
     * Agregate data from load data (every 30 min) to Hourly data
     */
    
    remip's avatar
    remip committed
    /*async function agregateHourlyData(data) {
    
    rpapin's avatar
    rpapin committed
      // Sum year and month values into object with year or year-month as keys
      if (data && data.length > 0) {
        let hourData = {}
        data.forEach(element => {
          let key =
            element.year +
            '-' +
            element.month +
            '-' +
            element.day +
            '-' +
            element.hour
          key in hourData
            ? (hourData[key] += element.load)
            : (hourData[key] = element.load)
        })
        // Agregation for Month data
        const agregatedMonthData = await buildAgregatedData(
          hourData,
          'com.grandlyon.grdf.hour'
        )
        await storeData(agregatedMonthData, 'com.grandlyon.grdf.hour', [
          'year',
          'month',
          'day',
          'hour'
        ])
      }
    
    remip's avatar
    remip committed
    }*/
    
    rpapin's avatar
    rpapin committed
    /**
     * Agregate data from daily data to monthly and yearly data
     */
    async function agregateMonthAndYearData(data) {
    
    remip's avatar
    remip committed
      //console.log('agregateMonthAndYearData')
      //console.log(data)
    
    rpapin's avatar
    rpapin committed
      // Sum year and month values into object with year or year-month as keys
    
    remip's avatar
    remip committed
      if (data && data.length !== 0) {
    
    rpapin's avatar
    rpapin committed
        let monthData = {}
        let yearData = {}
    
    remip's avatar
    remip committed
    
    
    rpapin's avatar
    rpapin committed
        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)
        })
    
    remip's avatar
    remip committed
    
        //console.log(monthData)
        //console.log(yearData)
    
    rpapin's avatar
    rpapin committed
        // Agregation for Month data
        const agregatedMonthData = await buildAgregatedData(
          monthData,
    
    remip's avatar
    remip committed
          'com.grandlyon.grdf.month'
    
    rpapin's avatar
    rpapin committed
        )
    
    remip's avatar
    remip committed
        //console.log('agregatedMonthData')
        //console.log(agregatedMonthData)
        await storeData(agregatedMonthData, 'com.grandlyon.grdf.month', [
    
    rpapin's avatar
    rpapin committed
          'year',
          'month'
        ])
        // Agregation for Year data
        const agregatedYearData = await buildAgregatedData(
          yearData,
    
    remip's avatar
    remip committed
          'com.grandlyon.grdf.year'
    
    rpapin's avatar
    rpapin committed
        )
    
    remip's avatar
    remip committed
        await storeData(agregatedYearData, 'com.grandlyon.grdf.year', ['year'])
    
    rpapin's avatar
    rpapin committed
      }
    }
    
    /**
     * Retrieve and remove old data for a specific doctype
     * Return an Array of agregated data
     */
    async function buildAgregatedData(data, doctype) {
      let agregatedData = []
      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.grdf.year') {
        year = key
        month = 1
        day = 0
        hour = 0
      } else if (doctype === 'com.grandlyon.grdf.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.grdf.year :
     * { load: 76.712, year: 2020, ... } need to be replace by
     * { load: 82.212, year: 2020, ... } after grdf 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.grdf.year') {
          // Yearly case
          filtered = result.filter(function(el) {
            return el.year == data.year
          })
        } else if (doctype === 'com.grandlyon.grdf.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
        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
    }