Skip to content
Snippets Groups Projects
index.js 12.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('./aggregate')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
const {
  parseSgeXmlData,
  parseSgeXmlTechnicalData,
  formateDataForDoctype,
  parseTags,
  parseValue,
  parseUserPdl,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
} = require('./parsing')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
const {
Bastien DUMONT's avatar
Bastien DUMONT committed
  consulterDonneesTechniquesContractuelles,
  consultationMesuresDetailleesMaxPower,
  consultationMesuresDetaillees,
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  rechercherPoint,
Bastien DUMONT's avatar
Bastien DUMONT committed
  commanderCollectePublicationMesures,
  commanderArretServiceSouscritMesures,
} = require('./requests/sge')
const {
  updateBoConsent,
  createBoConsent,
  getBoConsent,
  deleteBoConsent,
Bastien DUMONT's avatar
Bastien DUMONT committed
} = require('./requests/bo')
const { getInseeCode } = require('./requests/insee')

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')
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.
async function start(fields, cozyParameters) {
  log('info', 'Gathering data ...')
  let baseUrl = fields.wso2BaseUrl
  let apiAuthKey = fields.apiToken
  let loginUtilisateur = fields.loginUtilisateur
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  log('info', 'Authenticating ...')
  //TODO: Verify if condition is working in local and on build version
  if (cozyParameters && Object.keys(cozyParameters).length !== 0) {
    log('debug', 'Found COZY_PARAMETERS')
    baseUrl = cozyParameters.secret.wso2BaseUrl
    apiAuthKey = cozyParameters.secret.apiToken
    loginUtilisateur = cozyParameters.secret.loginUtilisateur
  }

  /**
   * 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 (await isFirstStart()) {
    if (
      !(await verifyUserIdentity(fields, baseUrl, apiAuthKey, loginUtilisateur))
    ) {
      throw errors.LOGIN_FAILED
    }
    await createBoConsent()
    //TODO: remove because useless ? Done later in code
    // const startDate = await getDataStartDate(
    //   baseUrl,
    //   apiAuthKey,
    //   loginUtilisateur,
    //   fields.pointId
    // )
Bastien DUMONT's avatar
Bastien DUMONT committed
    await commanderCollectePublicationMesures()
    await updateBoConsent()
  } else {
    await getBoConsent()
    if (!(await verifyUserIdentity(fields))) {
      await deleteBoConsent()
Bastien DUMONT's avatar
Bastien DUMONT committed
      await commanderArretServiceSouscritMesures()
      throw errors.TERMS_VERSION_MISMATCH
    }
  }
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  log('info', 'Successfully logged in')

  await gatherData(baseUrl, apiAuthKey, loginUtilisateur, fields.pointId)
}

/**
 * Verify user identity
 * @param {object} fields
 * @param {string} baseUrl
 * @param {string} apiAuthKey
 * @param {string} loginUtilisateur
async function verifyUserIdentity(
  fields,
  baseUrl,
  apiAuthKey,
  loginUtilisateur
) {
  const inseeCode = await getInseeCode(fields.postalCode)
  if (!inseeCode) throw errors.USER_ACTION_NEEDED

  const pdl = await findUserPdl(
    `${baseUrl}/enedis_SDE_recherche-point/1.0`,
    apiAuthKey,
    loginUtilisateur,
    fields.name,
    fields.addresse,
    fields.postalCode,
    inseeCode
  )

  if (fields.pointId != pdl) {
    log('error', 'PointId does not match')
    return false
  }
  return true
}

/**
 * Main method for gathering data
 * @param {string} baseUrl
 * @param {string} apiAuthKey
 * @param {string} loginUtilisateur
 * @param {number} pointId
 */
async function gatherData(baseUrl, apiAuthKey, loginUtilisateur, pointId) {
  log('info', 'Querying data...')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  await getDataStartDate(
    `${baseUrl}/enedis_SGE_ConsultationDonneesTechniquesContractuelles/1.0`,
    apiAuthKey,
    loginUtilisateur,
    pointId
  await getData(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    loginUtilisateur,
    pointId
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  await getMaxPowerData(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    loginUtilisateur,
    pointId
Hugo SUBTIL's avatar
Hugo SUBTIL committed
  )
  await getDataHalfHour(
    `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
    apiAuthKey,
    loginUtilisateur,
    pointId
  )
  log('info', 'Querying data: done')
Hugo SUBTIL's avatar
Hugo SUBTIL committed
}
Hugo SUBTIL's avatar
Hugo SUBTIL committed
/**
 *
 * @param {string} url
 * @param {string} apiAuthKey
 * @param {string} userLogin
 * @param {number} pointId
 */
async function getDataStartDate(url, apiAuthKey, userLogin, pointId) {
  log('info', 'Fetching data start date')
  const sgeHeaders = {
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    'Content-Type': 'text/xml;charset=UTF-8',
    apikey: apiAuthKey,
  }

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

  xml2js.parseString(
    response.body,
    {
      tagNameProcessors: [parseTags],
      valueProcessors: [parseValue],
      explicitArray: false,
    },
    processStartDate()
  )
}
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
 */
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')
  }
}

/**
 * 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
    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') {
Hugo SUBTIL's avatar
Hugo SUBTIL committed
      log('info', 'Agregating...')
      await agregateMonthAndYearData(processedDailyData)
    }
Hugo SUBTIL's avatar
Hugo SUBTIL committed
/**
Hugo SUBTIL's avatar
Hugo SUBTIL committed
 * Store an accurate start date based on contrat start
Hugo SUBTIL's avatar
Hugo SUBTIL committed
 */
function processStartDate() {
  return async (err, result) => {
    if (err) {
      log('error', err)
      throw err
    }
    // update start Date with contract openning date
Hugo SUBTIL's avatar
Hugo SUBTIL committed
    try {
      startDailyDate = moment(parseSgeXmlTechnicalData(result), 'YYYY-MM-DD')
      startDailyDateString = startDailyDate.format('YYYY-MM-DD')
    } catch (err) {
      log('error', err)
      //TODO: custom error ?
      throw err
    }
/**
 * 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}
 */
function isFirstStart() {
  console.log('isFirstStart')
  //TODO: Implement
  return true
 * @return {Promise<string>} User Pdl
async function findUserPdl(
  url,
  apiAuthKey,
  appLogin,
  name,
  addresse,
  postalCode,
  inseeCode
) {
  log('info', 'Fetching user data')
  const sgeHeaders = {
    'Content-Type': 'text/xml;charset=UTF-8',
    apikey: apiAuthKey,
  }
  const { response } = await soapRequest({
    url: url,
    headers: sgeHeaders,
    xml: rechercherPoint(appLogin, name, postalCode, inseeCode, addresse),
  }).catch(err => {
    log('error', 'rechercherPointResponse')
    log('error', err)
    throw errors.LOGIN_FAILED
    //TODO: handling code error SGT4F6 and SGT432 into USER_ACTIon_NEEDED
  })

  const parsedReply = await xml2js.parseStringPromise(response.body, {
    tagNameProcessors: [parseTags],
    valueProcessors: [parseValue],
    explicitArray: false,
  })

  //TODO: handle errors
  return parseUserPdl(parsedReply)