// @ts-check const { BaseKonnector, log, hydrateAndFilter, addData, errors, } = 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') const { parseSgeXmlData, formateDataForDoctype, parseTags, parseValue, } = require('./helpers/parsing') const { consultationMesuresDetailleesMaxPower, consultationMesuresDetaillees, } = require('./requests/sge') const { updateBoConsent, createBoConsent, getBoConsent, deleteBoConsent, } = 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 /*** Connector Constants ***/ const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false let startDailyDate = manualExecution ? moment().subtract(12, 'month') : moment().subtract(6, 'month') 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' 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 */ async function start(fields, cozyParameters) { log('info', 'Konnector configuration ...') const pointId = parseInt(fields.pointId) 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))) { 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, true ) 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 ) } } 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 {string} sgeLogin * @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, sgeLogin, pointId ) await getMaxPowerData( `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, apiAuthKey, sgeLogin, pointId ) await getDataHalfHour( `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, apiAuthKey, sgeLogin, pointId ) log('info', 'Querying data: done') } /** * 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, } setStartDate() const { response } = await soapRequest({ url: url, headers: sgeHeaders, xml: consultationMesuresDetaillees( pointId, userLogin, startDailyDateString, endDateString, 'ENERGIE', 'EA' ), }).catch(err => { log('error', 'consultationMesuresDetaillees') log('error', err) return err }) xml2js.parseString( response.body, { tagNameProcessors: [parseTags], valueProcessors: [parseValue], explicitArray: false, }, processData() ) } /** * 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 = { 'Content-Type': 'text/xml;charset=UTF-8', apikey: apiAuthKey, } setStartDate() const { response } = await soapRequest({ url: url, headers: sgeHeaders, xml: consultationMesuresDetailleesMaxPower( pointId, userLogin, startDailyDateString, endDateString ), }).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') ) } /** * 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, xml: consultationMesuresDetaillees( pointId, userLogin, increamentedStartDateString, incrementedEndDateString, 'COURBE', 'PA' ), }).catch(err => { 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 log('info', doctype) 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}`) } } } } /** * Save data in the right doctype db and prevent duplicated keys * @param {EnedisKonnectorData[]} data * @param {string} doctype * @param {string[]} filterKeys * @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(account) { if (account && account.data && account.data.consentId) { log('info', 'Konnector not first start') return false } log('info', 'Konnector first start') return true }