Skip to content
Snippets Groups Projects
index.js 10.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Romain CREY's avatar
    Romain CREY committed
    const {
      BaseKonnector,
      log,
      errors,
      addData,
      hydrateAndFilter,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      cozyClient,
    } = require('cozy-konnector-libs')
    
    Hugo's avatar
    Hugo committed
    
    
    const axios = require('axios').default
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    const moment = require('moment')
    require('moment-timezone')
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    moment.locale('fr') // set the language
    moment.tz.setDefault('Europe/Paris') // set the timezone
    
    const Sentry = require('@sentry/node')
    // eslint-disable-next-line
    const Tracing = require('@sentry/tracing') // Needed for tracking performance in Sentry
    const { version } = require('../package.json')
    const { isDev } = require('./helpers/env')
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Hugo's avatar
    Hugo committed
    const manualExecution =
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false
    
    Hugo's avatar
    Hugo committed
    
    const startDate = manualExecution
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          .subtract(1, 'year')
          .format('MM/DD/YYYY')
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          .subtract(3, 'year')
          .format('MM/DD/YYYY')
    
    Hugo's avatar
    Hugo committed
    
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    const endDate = moment().format('MM/DD/YYYY')
    
    const rangeDate = {
      day: {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        doctype: 'com.grandlyon.egl.day',
        keys: ['year', 'month', 'day'],
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        doctype: 'com.grandlyon.egl.month',
        keys: ['year', 'month'],
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        doctype: 'com.grandlyon.egl.year',
        keys: ['year'],
      },
    }
    
    module.exports = new BaseKonnector(start)
    /**
     * Sentry
     */
    Sentry.init({
      dsn:
        'https://3f97baf46c2b44c2bd9e0c371abe3e05@grandlyon.errors.cozycloud.cc/2',
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      // Set tracesSampleRate to 1.0 to capture 100%
      // of transactions for performance monitoring.
      // We recommend adjusting this value in production
      tracesSampleRate: 1.0,
      release: version,
      environment: isDev() ? 'development' : 'production',
      debug: isDev(),
      integrations: [
        // enable HTTP calls tracing
        new Sentry.Integrations.Http({ tracing: true }),
      ],
    })
    
    Romain CREY's avatar
    Romain CREY committed
    
    // The start function is run by the BaseKonnector instance only when it got all the account
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    // information (fields). When you run this connector yourself in 'standalone' mode or 'dev' mode,
    
    Romain CREY's avatar
    Romain CREY committed
    // the account information come from ./konnector-dev-config.json file
    async function start(fields, cozyParameters) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      const transaction = Sentry.startTransaction({
        op: 'konnector',
        name: 'EGL Konnector',
      })
      transaction.startChild({ op: 'Konnector starting' })
    
    
    Romain CREY's avatar
    Romain CREY committed
      try {
    
    Hugo's avatar
    Hugo committed
        // const baseUrl = fields.eglBaseURL
        // const apiAuthKey = fields.eglAPIAuthKey
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        const baseUrl = cozyParameters.secret.eglBaseURL
        const apiAuthKey = cozyParameters.secret.eglAPIAuthKey
        log('info', 'Authenticating ...')
    
    Romain CREY's avatar
    Romain CREY committed
        const response = await authenticate(
          fields.login,
          fields.password,
          baseUrl,
          apiAuthKey
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        )
        log('info', 'Successfully logged in')
    
    Hugo's avatar
    Hugo committed
    
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        const eglData = await getData(response, baseUrl, apiAuthKey)
    
    Hugo's avatar
    Hugo committed
        if (eglData) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          log('debug', 'Process egl daily data')
    
    Hugo's avatar
    Hugo committed
          const processedLoadData = await processData(
            eglData,
            rangeDate.day.doctype,
            rangeDate.day.keys
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          )
          log('debug', 'Aggregate egl load data for month and year')
          await aggregateMonthAndYearData(processedLoadData)
    
    Hugo's avatar
    Hugo committed
        } else {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          log('debug', 'No data found')
    
    Hugo's avatar
    Hugo committed
        }
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        transaction.setStatus(Tracing.SpanStatus.Ok)
        transaction.finish()
    
    Romain CREY's avatar
    Romain CREY committed
      } catch (error) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        log('error', error)
        Sentry.captureException(error)
        transaction.setStatus(Tracing.SpanStatus.Aborted)
        transaction.finish()
        await Sentry.flush()
        throw error
    
    Romain CREY's avatar
    Romain CREY committed
      }
    }
    
    Hugo's avatar
    Hugo 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 PAILHAREY's avatar
    Rémi PAILHAREY committed
      log('debug', 'processData - data formatted')
    
    Hugo's avatar
    Hugo committed
      // Remove data for existing days into the DB
      const filteredData = await hydrateAndFilter(data, doctype, {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        keys: filterKeys,
      })
      log('debug', 'processData - data filtered')
    
    Hugo's avatar
    Hugo committed
      // Store new day data
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      await storeData(filteredData, doctype, filterKeys)
      return filteredData
    
    Hugo's avatar
    Hugo committed
    }
    
    /**
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
     * Aggregate data from daily data to monthly and yearly data
    
    Hugo's avatar
    Hugo committed
     */
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    async function aggregateMonthAndYearData(data) {
    
    Hugo's avatar
    Hugo committed
      // Sum year and month values into object with year or year-month as keys
      if (data && data.length !== 0) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        let monthData = {}
        let yearData = {}
    
    Hugo's avatar
    Hugo committed
    
        data.forEach(element => {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          const monthDataKey = element.year + '-' + element.month
          if (monthDataKey in monthData) {
            monthData[monthDataKey] += element.load
          } else {
            monthData[monthDataKey] = element.load
          }
    
          const yearDataKey = element.year
          if (yearDataKey in yearData) {
            yearData[yearDataKey] += element.load
          } else {
            yearData[yearDataKey] = element.load
          }
        })
        // Aggregation for Month data
        const aggregatedMonthData = await buildAggregatedData(
    
    Hugo's avatar
    Hugo committed
          monthData,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          'com.grandlyon.egl.month'
        )
        await storeData(aggregatedMonthData, 'com.grandlyon.egl.month', [
          'year',
          'month',
        ])
        // Aggregation for Year data
        const aggregatedYearData = await buildAggregatedData(
    
    Hugo's avatar
    Hugo committed
          yearData,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          'com.grandlyon.egl.year'
        )
        await storeData(aggregatedYearData, 'com.grandlyon.egl.year', ['year'])
    
    Romain CREY's avatar
    Romain CREY committed
    
    
    Hugo's avatar
    Hugo committed
    /**
     * Retrieve and remove old data for a specific doctype
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
     * Return an Array of aggregated data
    
    Hugo's avatar
    Hugo committed
     */
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
    async function buildAggregatedData(data, doctype) {
      log('info', 'entering buildAggregatedData')
      let aggregatedData = []
    
    Hugo's avatar
    Hugo committed
      for (let [key, value] of Object.entries(data)) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        const data = await buildDataFromKey(doctype, key, value)
        const oldValue = await resetInProgressAggregatedData(data, doctype)
        log('info', 'Data load + old value is ' + data.load + ' + ' + oldValue)
        data.load += oldValue
        aggregatedData.push(data)
    
    Hugo's avatar
    Hugo committed
      }
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      return aggregatedData
    
    Hugo's avatar
    Hugo committed
    }
    
    
    Romain CREY's avatar
    Romain CREY committed
    async function authenticate(login, password, baseUrl, apiAuthKey) {
      const authRequest = {
    
        method: 'post',
        url: baseUrl + '/connect.aspx',
    
    Romain CREY's avatar
    Romain CREY committed
        headers: {
          AuthKey: apiAuthKey,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          'Content-Type': 'application/x-www-form-urlencoded',
    
    Romain CREY's avatar
    Romain CREY committed
        },
    
    Romain CREY's avatar
    Romain CREY committed
          login: login,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          pass: password,
    
    unknown's avatar
    unknown committed
        },
    
        const resp = await axios(authRequest)
        if (resp.data.codeRetour === 100) {
          return resp.data
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        } else {
    
          Sentry.captureException(JSON.stringify(resp.data))
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          throw new Error()
        }
      } catch (error) {
        throw new Error(errors.LOGIN_FAILED)
    
    Romain CREY's avatar
    Romain CREY committed
      }
    }
    
    async function getData(response, baseUrl, apiAuthKey) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      log('debug', 'Start date : ' + startDate)
      log('debug', 'End date : ' + endDate)
    
    Romain CREY's avatar
    Romain CREY committed
      const dataRequest = {
    
        method: 'post',
        url: baseUrl + '/getAllAgregatsByAbonnement.aspx',
    
    Romain CREY's avatar
    Romain CREY committed
        headers: {
          AuthKey: apiAuthKey,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          'Content-Type': 'application/x-www-form-urlencoded',
    
    Romain CREY's avatar
    Romain CREY committed
        },
    
    Romain CREY's avatar
    Romain CREY committed
          token: response.resultatRetour.token,
          num_abt: response.resultatRetour.num_abt,
          date_debut: startDate,
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          date_fin: endDate,
    
    unknown's avatar
    unknown committed
        },
    
    Romain CREY's avatar
    Romain CREY committed
      try {
    
        const resp = await axios(dataRequest)
        resp.data.resultatRetour.sort(function(a, b) {
          return new Date(a.DateReleve) - new Date(b.DateReleve)
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        })
    
        switch (resp.data.codeRetour) {
    
    Romain CREY's avatar
    Romain CREY committed
          case 100:
    
            return format(resp.data)
    
    Romain CREY's avatar
    Romain CREY committed
          case -2:
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            throw errors.LOGIN_FAILED
    
    Romain CREY's avatar
    Romain CREY committed
          case -1:
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            throw errors.VENDOR_DOWN
    
    Romain CREY's avatar
    Romain CREY committed
          default:
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            throw errors.UNKNOWN_ERROR
    
    Romain CREY's avatar
    Romain CREY committed
        }
      } catch (error) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        log('debug', 'Error from getAllAgregatsByAbonnement')
        throw new Error(errors.VENDOR_DOWN)
    
    Romain CREY's avatar
    Romain CREY committed
      }
    }
    
    function format(response) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      log('info', 'origin response size is : ' + response.resultatRetour.length)
    
      // Store first value as reference for index processing
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      let refValue = response.resultatRetour[0]
    
      // Create copy of data without first value
    
    Hugo's avatar
    Hugo committed
      const data = response.resultatRetour
        .slice(1)
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        .filter(value => value.ValeurIndex)
      log('info', 'filtered size is : ' + data.length)
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        const time = moment(value.DateReleve, moment.ISO_8601)
        const processedLoad = value.ValeurIndex - refValue.ValeurIndex
        if (processedLoad < 0) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            'error',
            `processing load for day ${parseInt(time.format('D'))}/${parseInt(
              time.format('M')
            )}/${parseInt(time.format('YYYY'))}, value is : ${processedLoad}`
          )
          throw errors.VENDOR_DOWN
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        refValue = value
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          load: processedLoad,
          year: parseInt(time.format('YYYY')),
          month: parseInt(time.format('M')),
          day: parseInt(time.format('D')),
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          type: value.TypeAgregat,
        }
      })
    
    Hugo's avatar
    Hugo committed
    /**
     * Save data in the right doctype db and prevent duplicated keys
     */
    async function storeData(data, doctype, filterKeys) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      log('debug', 'Store into ' + doctype)
      log('debug', 'Store into keys : ' + filterKeys)
    
    Hugo's avatar
    Hugo committed
      // data.map(v => {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      //   log('info', 'Saving data ' + v.load + ' for ' + v.day + '/' + v.month + '/' + v.year)
      // })
    
    Hugo's avatar
    Hugo committed
      const filteredDocuments = await hydrateAndFilter(data, doctype, {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        keys: filterKeys,
      })
      return await addData(filteredDocuments, doctype)
    
    Hugo's avatar
    Hugo committed
    /**
     * Format an entry for DB storage
     * using key and value
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
     * For year doctype: key = 'YYYY'
     * For month doctype: key = 'YYYY-MM'
    
    Hugo's avatar
    Hugo committed
     */
    async function buildDataFromKey(doctype, key, value) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      let year, month, day, hour
      if (doctype === 'com.grandlyon.egl.year') {
        year = key
        month = 1
        day = 0
        hour = 0
      } else if (doctype === 'com.grandlyon.egl.month') {
        const split = key.split('-')
        year = split[0]
        month = split[1]
        day = 0
        hour = 0
    
    Hugo's avatar
    Hugo committed
      } else {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        const split = key.split('-')
        year = split[0]
        month = split[1]
        day = split[2]
        hour = split[3]
    
    Hugo's avatar
    Hugo committed
      }
      return {
        load: Math.round(value * 10000) / 10000,
        year: parseInt(year),
        month: parseInt(month),
        day: parseInt(day),
        hour: parseInt(hour),
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        minute: 0,
      }
    
    Hugo's avatar
    Hugo committed
    }
    
    
    /**
     * 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.egl.month :
    
     * { load: 76.712, month: 2020, ... } need to be replace by
    
    Hugo's avatar
    Hugo committed
     * { load: 82.212, month: 2020, ... } after egl data reprocess
    
    Hugo's avatar
    Hugo committed
    async function resetInProgressAggregatedData(data, doctype) {
    
      // /!\ Warning: cannot use mongo queries because not supported for dev by cozy-konnectors-libs
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      log('debug', 'Remove aggregated data for ' + doctype)
      const result = await cozyClient.data.findAll(doctype)
    
    Hugo's avatar
    Hugo committed
      if (result && result.length > 0) {
    
        // Filter data to remove
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        let filtered = []
        if (doctype === 'com.grandlyon.egl.year') {
    
          // Yearly case
          filtered = result.filter(function(el) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            return el.year == data.year
          })
        } else if (doctype === 'com.grandlyon.egl.month') {
    
          // Monthly case
    
    Hugo's avatar
    Hugo committed
          filtered = result.filter(function(el) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
            return el.year == data.year && el.month == data.month
          })
    
    Hugo's avatar
    Hugo committed
        } else {
          // Hourly case
    
          filtered = result.filter(function(el) {
            return (
    
    Hugo's avatar
    Hugo committed
              el.year == data.year &&
              el.month == data.month &&
              el.day == data.day &&
              el.hour == data.hour
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        let sum = 0.0
    
        for (const doc of filtered) {
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
          sum += doc.load
          log('debug', 'Removing this entry for ' + doc.load)
          await cozyClient.data.delete(doctype, doc)
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
        return sum
    
    Romain CREY's avatar
    Romain CREY committed
      }
    
    Rémi PAILHAREY's avatar
    Rémi PAILHAREY committed
      return 0.0
    
    Romain CREY's avatar
    Romain CREY committed
    }