Newer
Older

Hugo NOUTS
committed
const getDataGenericErrors = require('./helpers/getDataGenericErrors')
const { isDev, isLocal } = require('./helpers/env')
const moment = require('moment')
require('moment-timezone')
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 { createBoConsent, deleteBoConsent } = require('./requests/bo')
const {
createGRDFConsent,
getAuthToken,
getConsents
} = require('./requests/grdf')
const { getAccount, saveAccountData } = require('./requests/cozy')
Sentry.init({
dsn:
'https://fa503fe00434433f805d1c715999b7f5@grandlyon.errors.cozycloud.cc/3',
// 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,

Hugo NOUTS
committed
environment: isDev() ? 'development' : 'production',
debug: isDev(),
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Http({ tracing: true })
]
})
Sentry.setTag('method', 'TIERS-DIRECT')
const NO_DATA = process.env.NO_DATA === 'true'
const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true'
const startDate = moment().subtract(manualExecution ? 1 : 3, 'year')
const endDate = moment().startOf('day')
const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'grdfgrandlyon'
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
module.exports = new BaseKonnector(start)
/**
* @param {GRDFConsent[]} consents
* @param {string} boBaseUrl
* @param {string} boToken
* @returns {Promise<boolean>} Returns true if consent needs to be created. Returns false if consent is valid
*/
async function handleConsents(consents, boBaseUrl, boToken) {
if (consents.some(consent => consent.etat_droit_acces === 'Active')) {
log('info', 'Found consent "Active"')
return false
}
if (consents.some(consent => consent.etat_droit_acces === 'A valider')) {
log('info', 'Found consent "A valider"')
throw errors.CHALLENGE_ASKED
}
if (consents.some(consent => consent.etat_droit_acces === 'Révoquée')) {
log('info', 'Found consent "Révoquée"')
throw errors.USER_ACTION_NEEDED_ACCOUNT_REMOVED
}
// Check if daily check & if obsolete consent is found, continue otherwise
if (
!manualExecution &&
consents.some(consent => consent.etat_droit_acces === 'Obsolète')
) {
log('info', 'Found consent "Obsolète"')
}
if (consents.some(consent => consent.etat_droit_acces === 'A revérifier')) {
log('info', 'Found consent "A revérifier"')
throw errors.VENDOR_DOWN
}
if (consents.some(consent => consent.etat_droit_acces === 'Refusée')) {
log('info', 'Found consent "Refusée"')
const accountData = await getAccount(ACCOUNT_ID)
await deleteBoConsent({
boBaseUrl,
boToken,
consentId: accountData.data.consentId
})
delete accountData.data.consentId
await saveAccountData(accountData)
}
if (!manualExecution) {
throw errors.USER_ACTION_NEEDED_SCA_REQUIRED

Hugo NOUTS
committed
}
}
// "message_retour_traitement": "Aucun droit d’accès trouvé." OU Obsolète -> create consent
return true

Hugo NOUTS
committed
}
/**
* Creates consents
* - first in our back-office
* - then at GRDF
* @returns {Promise<boolean>} isWaitingForConsentValidation Boolean that indicates that the consent was created and needs a user validation.
*/
async function createConsent({
bearerToken,
pce,
email,
firstname,
lastname,
postalCode,
startDate,
endDate,
boToken,
boBaseUrl
}) {
const startDateString = moment(startDate).format('YYYY-MM-DD')
const endDateConsent = moment(endDate).add(1, 'year')
const endDateString = endDateConsent.format('YYYY-MM-DD')
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
const { ID: consentId } = await createBoConsent({
boBaseUrl,
boToken,
pce,
firstname,
lastname,
postalCode,
endDate: endDateConsent
}).catch(err => {
log('error', `Failed to create BO consent, ${err}`)
throw errors.MAINTENANCE
})
// Save BO consentId into account
const accountData = await getAccount(ACCOUNT_ID)
await saveAccountData(ACCOUNT_ID, {
...accountData.data,
consentId: consentId
})
await createGRDFConsent({
bearerToken,
email,
lastname,
pce,
postalCode,
startDate: startDateString,
endDate: endDateString
}).catch(async err => {
await deleteBoConsent({
boBaseUrl,
boToken,
consentId: consentId
})
throw err
})
return true
}
/**
* 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: parameters}} cozyParameters
async function start(fields, cozyParameters) {
log('info', `isManual execution: ${manualExecution}`)
log('debug', `FIELDS ${JSON.stringify(fields)}`)
log('debug', `COZY_PARAMETERS ${JSON.stringify(cozyParameters)}`)
if (NO_DATA) {
log(
'debug',
'NO_DATA is enabled, konnector will stop before creating GRDF consent'

Hugo NOUTS
committed
}
const { pce, email, firstname, lastname, postalCode } = fields
let boToken = ''
let boBaseUrl = ''
let grdfId = ''
let grdfSecret = ''
if (cozyParameters && Object.keys(cozyParameters).length !== 0) {
log('debug', 'Found COZY_PARAMETERS')
boToken = cozyParameters.secret.boToken
boBaseUrl = cozyParameters.secret.boBaseUrl
grdfId = cozyParameters.secret.client_id
grdfSecret = cozyParameters.secret.client_secret
}
const boUrlGRDF = new URL('/api/grdf', boBaseUrl).href
const { access_token } = await getAuthToken(grdfId, grdfSecret)
const consents = await getConsents(access_token, pce)
const noValidConsent = await handleConsents(consents, boBaseUrl, boToken)
if (NO_DATA) {
log('debug', `Stopping konnector before creating consents`)
process.exit()
}
let isWaitingForConsentValidation = false
if (noValidConsent) {
isWaitingForConsentValidation = await createConsent({
bearerToken: access_token,
pce,
firstname,
lastname,
email,
postalCode,
startDate,
endDate,
boBaseUrl: boUrlGRDF,
boToken
})
}
if (isWaitingForConsentValidation) {
'info',
'Need to validate consent before getting data, stopping konnector here'
const grdfData = await getData(
access_token,
pce,
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
)
if (grdfData) {
const processedLoadData = await processData(
grdfData,
'com.grandlyon.grdf.day',
['year', 'month', 'day']
)
await aggregateMonthAndYearData(processedLoadData)
} else {
log('debug', 'No consent or data for load curve')
}
} catch (error) {
log('error', 'Start failed', error)
Sentry.captureException(error)
/**
* @param {string} idPCE
* @param {string} startDate 'YYYY-MM-DD'
* @param {string} endDate 'YYYY-MM-DD'
* @returns {string}
*/

Hugo NOUTS
committed
function buildGetDataUrl(idPCE, startDate, endDate) {
const baseUrl = 'https://api.grdf.fr/adict/v2/pce'
const queryParams = `date_debut=${startDate}&date_fin=${endDate}`
return `${baseUrl}/${idPCE}/donnees_consos_informatives?${queryParams}`
}
async function getData(token, idPCE, startDate, endDate) {
log('debug', `getData from ${startDate} to ${endDate}`)

Hugo NOUTS
committed
const url = buildGetDataUrl(idPCE, startDate, endDate)
log('debug', url)

Hugo NOUTS
committed
const response = await fetch(url, {

Hugo NOUTS
committed
headers: {
'Content-Type': 'application/x-ndjson',
Authorization: `Bearer ${token}`
},

Hugo NOUTS
committed
})
.then(async response => {
if (response.status !== 200) {
log('error', `Response failed with status ${response.status}`)
throw errors.VENDOR_DOWN
return response.text()
return result.match(/.+/g).map(s => {
result = JSON.parse(s)
* Handle no data issue when retrieving grdf data.
* 1000008 code stands for "Il n'y a pas de données correspondant à ce PCE sur la période demandée".

Hugo NOUTS
committed
* It is NOT an important issue deserving to throw an error
* If there is no data, return null data in order to be filtered before saving
*/
if (result.statut_restitution.code !== '1000008') {

Hugo NOUTS
committed
const genError = getDataGenericErrors(
result.statut_restitution.code
)
log(
'warn',
'donnees_consos_informatives responded with : ' +
result.statut_restitution.code +
' -> ' +
result.statut_restitution.message +
' Periode ' +
result.periode.date_debut +
'/' +
result.periode.date_fin
)
Sentry.captureMessage(
`Get data threw an error: ${result.statut_restitution.code} - ${result.statut_restitution.message}`
)
throw genError
} else {
return { energie: null }
return result.consommation
})
})
.catch(error => {
log('debug', 'Error from getData')
throw error

Hugo NOUTS
committed
const filteredRep = response.filter(function(el) {
}
/**
* Parse data
* Remove existing data from DB using hydrateAndFilter
* Store filtered data
* Return the list of filtered data
*/
async function processData(data, doctype, filterKeys) {
log('debug', 'Process grdf daily data')
const formattedData = await formatData(data)
const filteredData = await hydrateAndFilter(formattedData, doctype, {
await storeData(filteredData, doctype, filterKeys)
return filteredData
}
/**
* Save data in the right doctype db and prevent duplicated keys
*/
async function storeData(data, doctype, filterKeys) {
log('debug', doctype, 'Store into')
const filteredDocuments = await hydrateAndFilter(data, doctype, {
keys: filterKeys
})
return addData(filteredDocuments, doctype)
}
/**
* Format data for DB storage
* Remove bad data
*/
async function formatData(data) {
return data.map(record => {
let date = moment(record.date_debut_consommation, 'YYYY/MM/DD h:mm:ss')
let load =
: record.volume_brut * record.coeff_calcul.coeff_conversion
return {
load: parseFloat(load),
year: parseInt(date.format('YYYY')),
month: parseInt(date.format('M')),
day: parseInt(date.format('D')),
hour: parseInt(date.format('H')),
minute: parseInt(date.format('m'))
}
})
* Aggregate data from daily data to monthly and yearly data
async function aggregateMonthAndYearData(data) {
log('debug', 'Aggregate grdf load data for month and year')
// Sum year and month values into object with year or year-month as keys
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
// Aggregation for Month data
const aggregatedMonthData = await buildAggregatedData(
await storeData(aggregatedMonthData, 'com.grandlyon.grdf.month', [
// Aggregation for Year data
const aggregatedYearData = await buildAggregatedData(
await storeData(aggregatedYearData, 'com.grandlyon.grdf.year', ['year'])
}
}
/**
* Retrieve and remove old data for a specific doctype
async function buildAggregatedData(data, doctype) {
let aggregatedData = []
for (let [key, value] of Object.entries(data)) {
const data = await buildDataFromKey(doctype, key, value)
const oldValue = await resetInProgressAggregatedData(data, doctype)
data.load += oldValue
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
}
/**
* Format an entry for DB storage
* using key and value
* For year doctype: key = "YYYY"
* For month doctype: key = "YYYY-MM"
*/
async function buildDataFromKey(doctype, key, value) {
let year, month, day, hour
if (doctype === 'com.grandlyon.grdf.year') {
year = key
month = 1
day = 0
hour = 0
} else if (doctype === 'com.grandlyon.grdf.month') {
const split = key.split('-')
year = split[0]
month = split[1]
day = 0
hour = 0
} else {
const split = key.split('-')
year = split[0]
month = split[1]
day = split[2]
hour = split[3]
}
return {
load: Math.round(value * 10000) / 10000,
year: parseInt(year),
month: parseInt(month),
day: parseInt(day),
hour: parseInt(hour),
minute: 0
}
}
/**
* 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.grdf.year :
* { load: 76.712, year: 2020, ... } need to be replace by
* { load: 82.212, year: 2020, ... } after grdf data reprocess
*/
async function resetInProgressAggregatedData(data, doctype) {
// /!\ Warning: cannot use mongo queries because not supported for dev by cozy-konnectors-libs
log('debug', doctype, 'Remove aggregated data for')
const result = await cozyClient.data.findAll(doctype)
if (result && result.length > 0) {
// Filter data to remove
var filtered = []
if (doctype === 'com.grandlyon.grdf.year') {
// Yearly case
filtered = result.filter(function(el) {
return el.year == data.year
})
} else if (doctype === 'com.grandlyon.grdf.month') {
// Monthly case
filtered = result.filter(function(el) {
return el.year == data.year && el.month == data.month
})
} else {
// Hourly case
filtered = result.filter(function(el) {
return (
el.year == data.year &&
el.month == data.month &&
el.day == data.day &&
el.hour == data.hour
)
})
}
// Remove data
let sum = 0.0
for (const doc of filtered) {
sum += doc.load
log('debug', doc, 'Removing this entry for ' + doctype)
await cozyClient.data.delete(doctype, doc)
}
return sum
}
return 0.0
}