Skip to content
Snippets Groups Projects
index.js 12 KiB
Newer Older
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) {
    const errorMessage = `EGL konnector encountered an error. Response data: ${JSON.stringify(
      error.message
    )}`
    Sentry.captureMessage(errorMessage, {
      tags: {
        section: 'start',
      },
    })
Rémi PAILHAREY's avatar
Rémi PAILHAREY committed
    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) {
Bastien Dumont's avatar
Bastien Dumont committed
  log('info', 'Authenticating ...')
Romain CREY's avatar
Romain CREY committed
  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
    },
Hugo NOUTS's avatar
Hugo NOUTS committed

    const resp = await axios(authRequest)
    if (resp.data.codeRetour === 100) {
      return resp.data
Bastien Dumont's avatar
Bastien Dumont committed
    } else if (resp.data.codeRetour === -4) {
      throw new Error(errors.LOGIN_FAILED)
Bastien Dumont's avatar
Bastien Dumont committed
    const errorMessage = `Authentication failed. Response data: ${resp?.data?.libelleRetour}`
    log('error', errorMessage)
    throw new Error(errors.VENDOR_DOWN)
Rémi PAILHAREY's avatar
Rémi PAILHAREY committed
  } catch (error) {
Bastien Dumont's avatar
Bastien Dumont committed
    log('error', error.response?.data)
    Sentry.captureException(error, {
Hugo NOUTS's avatar
Hugo NOUTS committed
      tags: {
        section: 'authenticate',
      },
Hugo NOUTS's avatar
Hugo NOUTS committed
    })
Bastien Dumont's avatar
Bastien Dumont committed
    throw error
Romain CREY's avatar
Romain CREY committed
  }
}

async function getData(response, baseUrl, apiAuthKey) {
  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:
Hugo NOUTS's avatar
Hugo NOUTS committed
        log(
          'error',
          `Get data failed. codeRetour -2. ${resp.data.libelleRetour}`
Hugo NOUTS's avatar
Hugo NOUTS committed
        )
Rémi PAILHAREY's avatar
Rémi PAILHAREY committed
        throw errors.LOGIN_FAILED
Romain CREY's avatar
Romain CREY committed
      case -1:
Hugo NOUTS's avatar
Hugo NOUTS committed
        log(
          'error',
          `Get data failed. codeRetour -1. ${resp.data.libelleRetour}`
Hugo NOUTS's avatar
Hugo NOUTS committed
        )
Rémi PAILHAREY's avatar
Rémi PAILHAREY committed
        throw errors.VENDOR_DOWN
Romain CREY's avatar
Romain CREY committed
      default:
Hugo NOUTS's avatar
Hugo NOUTS committed
        log(
          'error',
          `Get data failed. ${resp.data.codeRetour}. ${resp.data.libelleRetour}`
Hugo NOUTS's avatar
Hugo NOUTS committed
        )
Rémi PAILHAREY's avatar
Rémi PAILHAREY committed
        throw errors.UNKNOWN_ERROR
Romain CREY's avatar
Romain CREY committed
    }
  } catch (error) {
    log('debug', error.message)
    Sentry.captureException(error, {
Hugo NOUTS's avatar
Hugo NOUTS committed
      tags: {
        section: 'getData',
      },
      extra: {
        start: startDate,
        end: endDate,
      },
Hugo NOUTS's avatar
Hugo NOUTS committed
    })
Hugo NOUTS's avatar
Hugo NOUTS committed
    if (axios.isAxiosError(error)) {
      throw new Error(errors.VENDOR_DOWN)
    }
    throw error
Romain CREY's avatar
Romain CREY committed
  }
}

function format(response) {
Hugo NOUTS's avatar
Hugo NOUTS 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]
Hugo NOUTS's avatar
Hugo NOUTS committed

  // 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)
Hugo NOUTS's avatar
Hugo NOUTS committed

  log('info', 'filtered size is: ' + data.length)

  try {
    return data.map(value => {
      const time = moment(value.DateReleve, moment.ISO_8601)
      const processedLoad = value.ValeurIndex - refValue.ValeurIndex

      if (processedLoad < 0) {
        const errorMessage = `Processing load error for day ${parseInt(
          time.format('D')
        )}/${parseInt(time.format('M'))}/${parseInt(
          time.format('YYYY')
        )}, value is: ${processedLoad}`
        log('debug', errorMessage)
        throw errors.VENDOR_DOWN
      }

      // Change index ref value
      refValue = value

      return {
        load: processedLoad,
        year: parseInt(time.format('YYYY')),
        month: parseInt(time.format('M')),
        day: parseInt(time.format('D')),
        hour: 0,
        minute: 0,
        type: value.TypeAgregat,
      }
    })
  } catch (error) {
    log('debug', error.message)
    Sentry.captureException(error, {
Hugo NOUTS's avatar
Hugo NOUTS committed
      tags: {
        section: 'format',
      },
    })
    throw error
  }
Hugo's avatar
Hugo committed
/**
 * Save data in the right doctype db and prevent duplicated keys
 */
async function storeData(data, doctype, filterKeys) {
  log('debug', 'Store into ' + doctype)
  log('debug', 'Store into keys : ' + filterKeys)
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
}