Skip to content
Snippets Groups Projects
index.js 13.9 KiB
Newer Older
// @ts-check
Hugo SUBTIL's avatar
Hugo SUBTIL committed
const {
  BaseKonnector,
  log,
  hydrateAndFilter,
  addData,
  errors,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
} = require('cozy-konnector-libs')
const soapRequest = require('easy-soap-request')
const moment = require('moment')
require('moment-timezone')
const xml2js = require('xml2js')
const { buildAgregatedData } = require('./helpers/aggregate')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
const {
  parseSgeXmlData,
  formateDataForDoctype,
  parseTags,
  parseValue,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
const {
Bastien DUMONT's avatar
Bastien DUMONT committed
  consultationMesuresDetailleesMaxPower,
  consultationMesuresDetaillees,
} = require('./requests/sge')
const {
  updateBoConsent,
  createBoConsent,
  getBoConsent,
  deleteBoConsent,
Bastien DUMONT's avatar
Bastien DUMONT committed
} = require('./requests/bo')
const {
  verifyUserIdentity,
  activateContract,
  verifyContract,
  terminateContract,
  getContractStartDate,
} = require('./core')
const { getAccount, saveAccountData } = require('./requests/cozy')
const { isLocal } = require('./helpers/env')
moment.locale('fr') // set the language
moment.tz.setDefault('Europe/Paris') // set the timezone
Hugo SUBTIL's avatar
Hugo SUBTIL committed

/*** Connector Constants ***/
const manualExecution =
  process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false
Hugo SUBTIL's avatar
Hugo SUBTIL committed
let startDailyDate = manualExecution
  ? moment().subtract(12, 'month')
  : moment().subtract(6, 'month')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
let startDailyDateString = startDailyDate.format('YYYY-MM-DD')
const startLoadDate = moment().subtract(7, 'day')
const endDate = moment()
const endDateString = endDate.format('YYYY-MM-DD')
const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'enedis-sge-grandlyon'
Hugo SUBTIL's avatar
Hugo SUBTIL 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.
 * @param {fields} fields
 * @param {{secret: fields}} cozyParameters
 */
Hugo SUBTIL's avatar
Hugo SUBTIL committed
async function start(fields, cozyParameters) {
  log('info', 'Konnector configuration ...')
  log('info', `isManual exectuion: ${manualExecution}`)
  let baseUrl = fields.wso2BaseUrl
  let apiAuthKey = fields.apiToken
  let contractId = fields.contractId
  let sgeLogin = fields.sgeLogin
  let boToken = fields.boToken
  let boBaseUrl = fields.boBaseUrl
  if (cozyParameters && Object.keys(cozyParameters).length !== 0) {
    log('debug', 'Found COZY_PARAMETERS')
    baseUrl = cozyParameters.secret.wso2BaseUrl
    apiAuthKey = cozyParameters.secret.apiToken
    contractId = cozyParameters.secret.contractId
    sgeLogin = cozyParameters.secret.sgeLogin
    boBaseUrl = cozyParameters.secret.boBaseUrl
    boToken = cozyParameters.secret.boToken
  }

  // Prevent missing configuration
  if (
    !baseUrl ||
    !apiAuthKey ||
    !contractId ||
    !sgeLogin ||
    !boToken ||
    !boBaseUrl
  ) {
    log('error', `Missing configuration secrets`)
    throw errors.VENDOR_DOWN

  /**
   * 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 (isFirstStart(await getAccount(ACCOUNT_ID))) {
    log('info', 'First start...')
    const user = await verifyUserIdentity(fields, baseUrl, apiAuthKey, sgeLogin)

    let consent = await createBoConsent(
      boBaseUrl,
      boToken,
      pointId,
      user.lastname,
      user.firstname,
      user.address,
      user.postalCode,
      user.inseeCode
    )

    // handle user contract start date in order to preperly request data
    const userContractstartDate = await getContractStartDate(
      baseUrl,
      apiAuthKey,
      sgeLogin,
      pointId
    )
    startDailyDate = moment(userContractstartDate, 'YYYY-MM-DD')
    startDailyDateString = startDailyDate.format('YYYY-MM-DD')

    const contractStartDate = moment().format('YYYY-MM-DD')
    const contractEndDate = moment()
      .add(1, 'year') // SGE force 1 year duration
      .format('YYYY-MM-DD')

    let serviceId = await verifyContract(
      baseUrl,
      apiAuthKey,
      sgeLogin,
      contractId,
      user.pointId
    )
    if (!serviceId) {
      serviceId = await activateContract(
        baseUrl,
        apiAuthKey,
        sgeLogin,
        contractId,
        user.lastname,
        user.pointId,
        contractStartDate,
        contractEndDate
      )
    }
    consent = await updateBoConsent(
      boBaseUrl,
      boToken,
      consent,
      serviceId.toString()
    )
    // Save bo id into account
    const accountData = await getAccount(ACCOUNT_ID)

    await saveAccountData(this.accountId, {
      ...accountData.data,
      consentId: consent.ID,
  } else {
    log('info', 'Alternate start...')
    const accountData = await getAccount(ACCOUNT_ID)
    const userConsent = await getBoConsent(
      boBaseUrl,
      boToken,
      accountData.data.consentId
    )
    const user = await verifyUserIdentity(
      fields,
      baseUrl,
      apiAuthKey,
      sgeLogin,
    )

    if (!userConsent) {
      log('error', 'No user consent found')
      throw errors.VENDOR_DOWN
    }

    const consentEndDate = Date.parse(userConsent.endDate)
    const today = Date.now()
    if (
      user.lastname.toLocaleUpperCase() !==
        userConsent.lastname.toLocaleUpperCase() ||
      !user ||
      consentEndDate < today
    ) {
      await deleteConsent(
        userConsent,
        baseUrl,
        apiAuthKey,
        sgeLogin,
        contractId,
        pointId,
        boBaseUrl,
        boToken
      )
      throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  log('info', 'Successfully logged in')

  await gatherData(baseUrl, apiAuthKey, sgeLogin, pointId)
}

/**
 * Delete User Consent
 * @param {Consent} userConsent
 * @param {string} baseUrl
 * @param {string} apiAuthKey
 * @param {string} sgeLogin
 * @param {string} contractId
 * @param {number} pointId
 * @param {string} boBaseUrl
 * @param {string} boToken
 */
async function deleteConsent(
  userConsent,
  baseUrl,
  apiAuthKey,
  sgeLogin,
  contractId,
  pointId,
  boBaseUrl,
  boToken
) {
  log('error', `Invalid or not found consent for user`)
  if (userConsent.serviceID) {
    await terminateContract(
      baseUrl,
      apiAuthKey,
      sgeLogin,
      contractId,
      pointId,
      userConsent.serviceID
    )
    await deleteBoConsent(boBaseUrl, boToken, userConsent.ID || 0)
  } else {
    log('error', `No service id retrieved from BO`)
    throw errors.VENDOR_DOWN
  }
  throw errors.TERMS_VERSION_MISMATCH
}

/**
 * Main method for gathering data
 * @param {string} baseUrl
 * @param {string} apiAuthKey
 * @param {number} pointId
 */
async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) {
  log('info', 'Querying data...')
  await getContractStartDate(baseUrl, apiAuthKey, sgeLogin, pointId)
  await getData(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    pointId
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  await getMaxPowerData(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    pointId
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  )
  await getDataHalfHour(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    pointId
  )
  log('info', 'Querying data: done')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
}
 * 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 sgeHeaders = {
    'Content-Type': 'text/xml;charset=UTF-8',
    apikey: apiAuthKey,
  }
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  setStartDate()
  const { response } = await soapRequest({
    url: url,
    headers: sgeHeaders,
Bastien DUMONT's avatar
Bastien DUMONT committed
    xml: consultationMesuresDetaillees(
      pointId,
      userLogin,
      startDailyDateString,
Bastien DUMONT's avatar
Bastien DUMONT committed
      endDateString,
      'ENERGIE',
      'EA'
    ),
  }).catch(err => {
Bastien DUMONT's avatar
Bastien DUMONT committed
    log('error', 'consultationMesuresDetaillees')
    log('error', err)
    return err
  xml2js.parseString(
    response.body,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    {
      tagNameProcessors: [parseTags],
      valueProcessors: [parseValue],
      explicitArray: false,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    },
    processData()
Hugo SUBTIL's avatar
Hugo SUBTIL committed
/**
 * 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 sgeHeaders = {
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    'Content-Type': 'text/xml;charset=UTF-8',
    apikey: apiAuthKey,
  }

Hugo SUBTIL's avatar
Hugo SUBTIL committed
  setStartDate()
Hugo SUBTIL's avatar
Hugo SUBTIL committed

  const { response } = await soapRequest({
    url: url,
    headers: sgeHeaders,
Bastien DUMONT's avatar
Bastien DUMONT committed
    xml: consultationMesuresDetailleesMaxPower(
      pointId,
      userLogin,
      startDailyDateString,
      endDateString
    ),
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  }).catch(err => {
    log('error', 'getMaxPowerData')
    log('error', err)
    return err
  })

  xml2js.parseString(
    response.body,
    {
      tagNameProcessors: [parseTags],
      valueProcessors: [parseValue],
      explicitArray: false,
    },
    processData('com.grandlyon.enedis.maxpower')
  )
}

Hugo SUBTIL's avatar
Hugo SUBTIL committed
/**
 * If start date exceed the maximum amount of data we can get with one query
 * get only 36 month.
 * On manual execution, set the start date to one year ago.
Hugo SUBTIL's avatar
Hugo SUBTIL committed
 */
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')
  }
  if (manualExecution) {
    startDailyDate = moment(endDate).subtract(12, '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 sgeHeaders = {
    '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: sgeHeaders,
Bastien DUMONT's avatar
Bastien DUMONT committed
      xml: consultationMesuresDetaillees(
        pointId,
        userLogin,
        increamentedStartDateString,
        incrementedEndDateString,
        'COURBE',
        'PA'
      ),
    }).catch(err => {
Bastien DUMONT's avatar
Bastien DUMONT committed
      log('error', 'consultationMesuresDetaillees 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
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    try {
      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)
      }
    } catch (e) {
      if (doctype === 'com.grandlyon.enedis.minute') {
        log(
          'warn',
          `No half-hour activated. Issue: ${result.Envelope.Body.Fault.faultstring}`
        )
      } else {
        log('error', `Unkown error ${e}`)
      }
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    }
/**
 * Save data in the right doctype db and prevent duplicated keys
 * @param {EnedisKonnectorData[]} data
 * @param {string} doctype
 * @param {string[]} filterKeys
Hugo SUBTIL's avatar
Hugo SUBTIL committed
 * @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}
 */
Bastien DUMONT's avatar
Bastien DUMONT committed
  if (account && account.data && account.data.consentId) {
    log('info', 'Konnector not first start')
    return false
  }
  log('info', 'Konnector first start')
  return true