Skip to content
Snippets Groups Projects
index.js 12.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Rémi PAPIN's avatar
    Rémi PAPIN committed
    const {
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      cozyClient,
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      BaseKonnector,
      addData,
      hydrateAndFilter,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      errors,
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      log
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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')
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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()
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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'
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    //console.log(startDate)
    //console.log(endDate)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    /*const timeRange = ['day', 'month', 'year']
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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']
      }
    }
    
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    const client_id = 'metropole_de_lyon_grdf'
    const redirectUrl = 'https://grdf.pchugo.wf.alpha.grandlyon.com'*/
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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
        }
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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
    }
    
    
    async function getPCEid(tokenToDecrypt) {
      // verify a token symmetric - synchronous
    
    git-directory-deploy's avatar
    git-directory-deploy committed
      log('debug', 'GETPCEID : ' + tokenToDecrypt)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      var decoded = jwt.decode(tokenToDecrypt)
    
      const decodedConsentements = JSON.parse(decoded.consentements)
      return decodedConsentements[0].pce
    }
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    //Retrieve data from grdf API
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    async function getData(token, idPCE) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      log('debug', 'ENTERING GETDATA')
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      var myHeaders = new Headers()
      myHeaders.append('Content-Type', 'application/x-ndjson')
      myHeaders.append('Authorization', 'Bearer ' + token)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    
      var requestOptions = {
        method: 'GET',
        headers: myHeaders,
        redirect: 'follow'
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      }
      var url =
        'https://api.grdf.fr/adict/v1/pce/' +
        idPCE +
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        '/donnees_consos_informatives?date_debut=' +
        startDate +
        '&date_fin=' +
        endDate
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      const rep = await fetch(url, requestOptions)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        .then(response => response.text())
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        .then(result => {
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
          result = JSON.parse(result)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
          return result
        })
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        .catch(error => log('error', error))
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      return rep
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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) {
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      const intervalData = data.consommation
      const formatedData = await formateData(intervalData)
      const dataArray = [formatedData]
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      // Remove data for existing days into the DB
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      const filteredData = await hydrateAndFilter(dataArray, doctype, {
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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
     */
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    async function formateData(data) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      log('debug', 'Formating data')
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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'))
      }
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    }
    
    /**
     * Agregate data from load data (every 30 min) to Hourly data
     */
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    /*async function agregateHourlyData(data) {
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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'
        ])
      }
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    }*/
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    /**
     * Agregate data from daily data to monthly and yearly data
     */
    async function agregateMonthAndYearData(data) {
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      //console.log('agregateMonthAndYearData')
      //console.log(data)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      // Sum year and month values into object with year or year-month as keys
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
      if (data && data.length !== 0) {
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        let monthData = {}
        let yearData = {}
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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)
        })
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
    
        //console.log(monthData)
        //console.log(yearData)
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        // Agregation for Month data
        const agregatedMonthData = await buildAgregatedData(
          monthData,
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
          'com.grandlyon.grdf.month'
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        )
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        //console.log('agregatedMonthData')
        //console.log(agregatedMonthData)
        await storeData(agregatedMonthData, 'com.grandlyon.grdf.month', [
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
          'year',
          'month'
        ])
        // Agregation for Year data
        const agregatedYearData = await buildAgregatedData(
          yearData,
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
          'com.grandlyon.grdf.year'
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        )
    
    Rémi PAPIN's avatar
    Rémi PAPIN committed
        await storeData(agregatedYearData, 'com.grandlyon.grdf.year', ['year'])
    
    Rémi PAPIN's avatar
    Rémi PAPIN 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
    }