Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/llle_project/egl-konnector
1 result
Show changes
Commits on Source (2)
......@@ -28,21 +28,26 @@
"acces",
"Agregat",
"Agregats",
"annee",
"apikey",
"arret",
"Arret",
"backoffice",
"consommation",
"Corrigees",
"cozyclient",
"criteres",
"Derniere",
"Detaillees",
"Efluid",
"enedissgegrandlyon",
"EPGL",
"Estime",
"Etage",
"etat",
"faultstring",
"firstname",
"Fuite",
"Generales",
"grandlyon",
"HISTO",
......@@ -50,11 +55,14 @@
"konnector",
"konnectors",
"lastname",
"libelle",
"maxpower",
"mois",
"numero",
"Perimetre",
"periodicite",
"PMAX",
"postes",
"Recurrente",
"Releve",
"resultat",
......
{
"COZY_URL": "http://cozy.tools:8080/",
"fields": {
"login": 1234567,
"password": ""
"contractId": "",
"meteringId": ""
},
"COZY_PARAMETERS": {
"secret": {
"eglBaseURL": "",
"eglAPIAuthKey": "",
"boBaseUrl": "https://ecolyo-agent-rec.apps.grandlyon.com/"
"boBaseUrl": "https://ecolyo-agent-rec.apps.grandlyon.com/",
"epglBaseUrl": "https://prepro-epgl-ecolyo-efluidconnect.multield.net/",
"epglUser": "",
"epglPassword": ""
}
}
}
\ No newline at end of file
}
......@@ -11,11 +11,11 @@
"frequency": "daily",
"categories": ["energy"],
"fields": {
"login": {
"contractId": {
"type": "text"
},
"password": {
"type": "password"
"meteringId": {
"type": "text"
},
"advancedFields": {
"folderPath": {
......@@ -51,6 +51,14 @@
"accounts": {
"description": "Utilisé pour accéder à vos données de consommation."
}
},
"fields": {
"contractId": {
"label": "Numéro de contrat"
},
"meteringId": {
"label": "Numéro de compteur"
}
}
},
"en": {
......@@ -63,6 +71,14 @@
"accounts": {
"description": "Used to access your consumption data."
}
},
"fields": {
"contractId": {
"label": "Contract ID"
},
"meteringId": {
"label": "Water metering ID"
}
}
}
},
......
......@@ -14,6 +14,7 @@
"start": "node ./src/index.js",
"dev": "cozy-konnector-dev",
"standalone": "cozy-konnector-standalone",
"standalone-no-data": "NO_DATA=true cozy-konnector-standalone src/index.js",
"lint": "eslint src --fix",
"pretest": "npm run clean",
"test": "jest",
......@@ -28,9 +29,10 @@
"dependencies": {
"@sentry/node": "7.30.0",
"@sentry/tracing": "7.30.0",
"axios": "1.7.2",
"axios": "1.7.7",
"cozy-konnector-libs": "^5.12.1",
"luxon": "^3.4.3"
"moment": "^2.30.1",
"moment-timezone": "^0.5.45"
},
"devDependencies": {
"cozy-jobs-cli": "^2.4.4",
......
const { log } = require('cozy-konnector-libs')
const { DateTime } = require('luxon')
const Sentry = require('@sentry/node')
/**
* Formats and processes data retrieved from the getData response.
* Formats and processes data retrieved from the fetchData response.
*
* @param {Releve[]} data - The data response to be formatted.
* @param {FetchDataResponse} data - The data response to be formatted.
*
* @throws {Error} - Throws an error with an error code in case of data processing failure.
* @returns {FormattedData[]} - An array of formatted data objects.
*/
function format(data) {
log('info', 'origin response size is: ' + data.length)
/** Filter loads where value is 0 */
const filteredData = data.filter(value => value.ValeurIndex !== 0)
const formattedLoads = []
for (let i = 1; i < filteredData.length; i++) {
const previousValue = filteredData[i - 1]
const currentValue = filteredData[i]
const processedLoad = currentValue.ValeurIndex - previousValue.ValeurIndex
const loadDate = DateTime.fromISO(currentValue.DateReleve, {
setZone: true,
function formatData(data) {
try {
const loadData = data.postes[0].data
const isInMeterCube = data.unites.consommation.toLowerCase() === 'm3'
return loadData.map(load => ({
load: load.consommation * (isInMeterCube ? 1000 : 1),
year: load.annee,
month: load.mois + 1, // Months are from 0 to 11 in Efluid
day: load.jour,
hour: 0,
minute: 0,
}))
} catch {
Sentry.captureMessage('New water meter or invalid load value', {
tags: {
section: 'format',
},
})
// Check that load is positive and that water meter has not changed
if (processedLoad >= 0 && currentValue.TypeAgregat !== 'D') {
formattedLoads.push({
load: processedLoad,
year: loadDate.year,
month: loadDate.month,
day: loadDate.day,
hour: 0,
minute: 0,
type: currentValue.TypeAgregat,
})
} else {
Sentry.captureMessage('New water meter or invalid load value', {
extra: {
processedLoad,
currentValue: currentValue,
},
tags: {
section: 'format',
},
})
}
}
return formattedLoads
return []
}
module.exports = { format }
module.exports = { formatData }
const { format } = require('./format')
const { formatData } = require('./format')
const mockCaptureMessage = jest.fn()
jest.mock('@sentry/node', () => ({
......@@ -15,187 +15,81 @@ describe('format', () => {
jest.clearAllMocks()
})
it('should format empty array', () => {
const formatted = format([])
expect(formatted).toStrictEqual([])
const formattedData = formatData([])
expect(formattedData).toStrictEqual([])
})
it('should format array of length 1', () => {
const formatted = format([
it('should format empty array, no data', () => {
const formattedData = formatData([
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
postes: [],
unites: {
consommation: 'l',
index: 'l',
debitMin: 'L/h',
volumeEstimeFuite: 'l',
},
},
])
expect(formatted).toStrictEqual([])
expect(formattedData).toStrictEqual([])
})
it('should process loads between two days', () => {
const formatted = format([
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-01-15T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10001,
},
])
expect(formatted).toStrictEqual([
{
it('should format load properly', () => {
const formattedData = formatData({
postes: [
{
data: [
{
consommation: 100,
index: 123456,
jour: 2,
mois: 0,
annee: 2024,
},
],
},
],
unites: {
consommation: 'l',
},
})
expect(formattedData).toStrictEqual([
{
load: 100,
year: 2024,
month: 1,
day: 15,
hour: 0,
minute: 0,
load: 1,
type: 'R',
},
])
})
it('should handle summer time change', () => {
const formatted = format([
{
DateReleve: '2024-03-31T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-04-01T00:00:00+02:00',
TypeAgregat: 'R',
ValeurIndex: 10001,
},
{
DateReleve: '2024-04-02T00:00:00+02:00',
TypeAgregat: 'R',
ValeurIndex: 10003,
},
])
expect(formatted).toStrictEqual([
{
year: 2024,
month: 4,
day: 1,
hour: 0,
minute: 0,
load: 1,
type: 'R',
},
{
year: 2024,
month: 4,
day: 2,
hour: 0,
minute: 0,
load: 2,
type: 'R',
},
])
})
it('should handle winter time change', () => {
const formatted = format([
{
DateReleve: '2024-10-27T00:00:00+02:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-10-28T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10001,
},
{
DateReleve: '2024-10-29T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10003,
},
])
expect(formatted).toStrictEqual([
{
year: 2024,
month: 10,
day: 28,
hour: 0,
minute: 0,
load: 1,
type: 'R',
},
{
year: 2024,
month: 10,
day: 29,
hour: 0,
minute: 0,
load: 2,
type: 'R',
},
])
})
it('should filter loads where value is 0', () => {
const formatted = format([
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'T',
ValeurIndex: 0,
},
{
DateReleve: '2024-01-15T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10001,
},
{
DateReleve: '2024-01-15T00:00:00+01:00',
TypeAgregat: 'T',
ValeurIndex: 0,
},
])
expect(formatted).toStrictEqual([
{
it('should convert load to liters', () => {
const formattedData = formatData({
postes: [
{
data: [
{
consommation: 100,
index: 123456,
jour: 2,
mois: 0,
annee: 2024,
},
],
},
],
unites: {
consommation: 'm3',
},
})
expect(formattedData).toStrictEqual([
{
load: 100000,
year: 2024,
month: 1,
day: 15,
day: 2,
hour: 0,
minute: 0,
load: 1,
type: 'R',
},
])
})
it('should not process loads if water meter changed', () => {
const formatted = format([
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-01-15T00:00:00+01:00',
TypeAgregat: 'D',
ValeurIndex: 10001,
},
])
expect(mockCaptureMessage).toHaveBeenCalledTimes(1)
expect(formatted).toStrictEqual([])
})
it('should not process loads if load is negative', () => {
const formatted = format([
{
DateReleve: '2024-01-14T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 10000,
},
{
DateReleve: '2024-01-15T00:00:00+01:00',
TypeAgregat: 'R',
ValeurIndex: 1,
},
])
expect(mockCaptureMessage).toHaveBeenCalledTimes(1)
expect(formatted).toStrictEqual([])
})
})
const { log } = require('cozy-konnector-libs')
const axios = require('axios').default
const Sentry = require('@sentry/node')
const { DateTime } = require('luxon')
const moment = require('moment-timezone')
require('../types/types')
/**
......@@ -32,25 +32,19 @@ async function getPrices(boBaseUrl) {
*/
async function applyPrices(data, fluidPrices) {
// Sort prices by descending start date
fluidPrices.sort(
(a, b) =>
DateTime.fromISO(b.startDate, { zone: 'UTC' }).toMillis() -
DateTime.fromISO(a.startDate, { zone: 'UTC' }).toMillis()
fluidPrices.sort((a, b) =>
moment(b.startDate).tz('UTC').diff(moment(a.startDate).tz('UTC'))
)
return data.map(load => {
// Select the first price that is before the load date
const loadDate = DateTime.fromObject(
{
year: load.year,
month: load.month,
day: load.day,
},
{ zone: 'UTC' }
const loadDate = moment.tz(
{ year: load.year, month: load.month - 1, day: load.day },
'UTC'
)
const fluidPrice = fluidPrices.find(p => {
const startDate = DateTime.fromISO(p.startDate, { zone: 'UTC' })
return loadDate >= startDate
const startDate = moment.tz(p.startDate, 'UTC')
return loadDate.isSameOrAfter(startDate)
})
if (!fluidPrice) return load
return { ...load, price: fluidPrice.price * load.load }
......
const { cozyClient } = require('cozy-konnector-libs')
const { AggregateTypes } = require('./../types/enums')
const { rangeDate } = require('./../types/constants')
const { DateTime } = require('luxon')
const moment = require('moment')
/**
* @param {FormattedData[]} data - The daily data to aggregate.
......@@ -38,7 +37,6 @@ async function aggregateLoads(data, hasFoundPrices) {
day: 0,
hour: 0,
minute: 0,
type: AggregateTypes.REEL,
}
if (hasFoundPrices) monthDeltas[monthKey].price = 0
}
......@@ -52,7 +50,6 @@ async function aggregateLoads(data, hasFoundPrices) {
day: 0,
hour: 0,
minute: 0,
type: AggregateTypes.REEL,
}
if (hasFoundPrices) yearDeltas[yearKey].price = 0
}
......@@ -139,31 +136,28 @@ async function getLastDailyLoadDate() {
}, allDailyLoads[0])
if (lastDailyLoad)
return DateTime.fromObject({
return moment({
year: lastDailyLoad.year,
month: lastDailyLoad.month,
month: lastDailyLoad.month - 1,
day: lastDailyLoad.day,
}).setLocale('en')
}).locale('en')
}
async function getStartDateString() {
const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true'
const defaultStartDate = DateTime.now()
.setLocale('en')
.minus({ year: manualExecution ? 1 : 3 })
const defaultStartDateString = defaultStartDate.toLocaleString({
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
async function getStartDateString(isManualExecution) {
const defaultStartDate = moment()
.startOf('day')
.locale('en')
.subtract(isManualExecution ? 1 : 3, 'year')
const lastDailyLoadDate = await getLastDailyLoadDate()
if (!lastDailyLoadDate) {
return defaultStartDateString
return defaultStartDate.toISOString()
}
// Fetch data up to one month before last daily load
const oneMonthBeforeLastDailyLoad = lastDailyLoadDate.minus({ months: 1 })
const oneMonthBeforeLastDailyLoad = lastDailyLoadDate
.clone()
.subtract(1, 'month')
// Ensure this does not exceed defaultStartDate
const startDate =
......@@ -171,11 +165,7 @@ async function getStartDateString() {
? defaultStartDate
: oneMonthBeforeLastDailyLoad
return startDate.toLocaleString({
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
return startDate.toISOString()
}
module.exports = {
......
const { cozyClient } = require('cozy-konnector-libs')
const { DateTime } = require('luxon')
const moment = require('moment')
const { getLastDailyLoadDate, getStartDateString } = require('./utils')
jest.mock('cozy-konnector-libs', () => ({
......@@ -25,58 +25,45 @@ describe('utils', () => {
describe('getLastDailyLoadDate', () => {
it('should return the last daily load date as a DateTime object', async () => {
cozyClient.data.findAll.mockResolvedValue(mockDailyDataLoads)
const result = await getLastDailyLoadDate()
expect(result).toBeInstanceOf(DateTime)
expect(result.year).toBe(2024)
expect(result.month).toBe(10)
expect(result.day).toBe(15)
expect(result).toBeInstanceOf(moment)
expect(result.year()).toBe(2024)
expect(result.month()).toBe(10 - 1)
expect(result.date()).toBe(15)
})
it('should return undefined if there are no daily loads', async () => {
cozyClient.data.findAll.mockResolvedValue([])
const result = await getLastDailyLoadDate()
expect(result).toBeUndefined()
})
})
describe('getStartDateString', () => {
// Mock DateTime.now() to return a fixed date
const fixedDate = DateTime.fromObject({ year: 2024, month: 10, day: 1 })
jest.spyOn(DateTime, 'now').mockReturnValue(fixedDate)
const fixedDate = moment({ year: 2024, month: 10 - 1, day: 1 })
jest.spyOn(moment, 'now').mockReturnValue(fixedDate)
it('should return a date one month before last daily load date', async () => {
cozyClient.data.findAll.mockResolvedValue(mockDailyDataLoads)
const startDate = await getStartDateString()
const startDate = await getStartDateString(false)
const frenchStartDate = moment(startDate).local().format('YYYY-MM-DD')
expect(startDate).toBeDefined()
expect(startDate).toBe('09/15/2024')
expect(frenchStartDate).toBe('2024-09-15')
})
it('should return 1 year ago if there are no daily loads on manual execution', async () => {
cozyClient.data.findAll.mockResolvedValue([])
// Mock a manual execution
process.env.COZY_JOB_MANUAL_EXECUTION = 'true'
const startDate = await getStartDateString()
expect(startDate).toBe('10/01/2023')
const startDate = await getStartDateString(true)
const frenchStartDate = moment(startDate).local().format('YYYY-MM-DD')
expect(frenchStartDate).toBe('2023-10-01')
})
it('should return 3 years ago if there are no daily loads on automatic execution', async () => {
cozyClient.data.findAll.mockResolvedValue([])
// Mock an automatic execution
process.env.COZY_JOB_MANUAL_EXECUTION = 'false'
const startDate = await getStartDateString()
expect(startDate).toBe('10/01/2021')
const startDate = await getStartDateString(false)
const frenchStartDate = moment(startDate).local().format('YYYY-MM-DD')
expect(frenchStartDate).toBe('2021-10-01')
})
})
})
......@@ -5,25 +5,26 @@ const {
updateOrCreate,
hydrateAndFilter,
} = require('cozy-konnector-libs')
const axios = require('axios').default
const { DateTime } = require('luxon')
const moment = require('moment')
const Sentry = require('@sentry/node')
// eslint-disable-next-line no-unused-vars
const Tracing = require('@sentry/tracing') // Needed for tracking performance in Sentry
const { version } = require('../package.json')
const { isDev } = require('./helpers/env')
const { rangeDate } = require('./types/constants')
const { getAuthToken, isValidContract, fetchData } = require('./requests/epgl')
const {
getStartDateString,
aggregateLoads,
updateAggregates,
} = require('./helpers/utils')
const { format } = require('./helpers/format')
const { formatData } = require('./helpers/format')
const { getPrices, applyPrices } = require('./helpers/prices.js')
require('./types/types')
const NO_DATA = process.env.NO_DATA === 'true'
const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true'
module.exports = new BaseKonnector(start)
/**
......@@ -47,46 +48,74 @@ Sentry.init({
})
/**
* The start function is run by the BaseKonnector instance only when it receives all the account information (fields).
* When you run this connector in 'standalone' or 'dev' mode, the account information comes from the ./konnector-dev-config.json file.
* 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 {object} fields - The account information fields.
* @param {object} cozyParameters - Cozy platform parameters.
* @param {import('./types').fields} fields - The account information fields.
* @param {{secret: import('./types').parameters}} cozyParameters - Cozy platform parameters.
*/
async function start(fields, cozyParameters) {
const transaction = Sentry.startTransaction({
op: 'konnector',
name: 'EGL Konnector',
name: 'EPGL Konnector',
})
transaction.startChild({ op: 'Konnector starting' })
const startDateString = await getStartDateString()
const endDateString = DateTime.now()
.setLocale('en')
.toLocaleString({ day: '2-digit', month: '2-digit', year: 'numeric' })
log('info', `isManual execution: ${manualExecution}`)
if (NO_DATA) {
log(
'debug',
'NO_DATA is enabled, konnector will stop before fetching EPGL data'
)
}
const startDateString = await getStartDateString(manualExecution)
const endDateString = moment().startOf('day').locale('en')
try {
const eglBaseUrl = cozyParameters.secret.eglBaseURL
const boBaseUrl = cozyParameters.secret.boBaseUrl
const apiAuthKey = cozyParameters.secret.eglAPIAuthKey
const epglBaseUrl = cozyParameters.secret.epglBaseUrl
const epglUser = cozyParameters.secret.epglUser
const epglPassword = cozyParameters.secret.epglPassword
log('info', 'Authenticating ...')
const response = await authenticate(
fields.login,
fields.password,
eglBaseUrl,
apiAuthKey
)
log('info', 'Successfully logged in')
// Prevent missing configuration
if (!boBaseUrl || !epglBaseUrl || !epglUser || !epglPassword) {
throw new Error('Missing configuration secrets')
}
const accessToken = await getAuthToken(epglBaseUrl, epglUser, epglPassword)
if (
!(await isValidContract(
epglBaseUrl,
accessToken,
fields.contractId,
fields.meteringId
))
) {
throw errors.LOGIN_FAILED
}
let eglData = await getData(
if (NO_DATA) {
log('debug', `Stopping konnector before fetching data`)
process.exit()
}
const responseData = await fetchData(
epglBaseUrl,
accessToken,
fields.contractId,
fields.meteringId,
startDateString,
endDateString,
response,
eglBaseUrl,
apiAuthKey
endDateString
)
if (eglData.length === 0) {
let epglData = formatData(responseData)
if (epglData.length === 0) {
log('debug', 'No data found')
transaction.setStatus(Tracing.SpanStatus.Ok)
transaction.finish()
......@@ -98,32 +127,33 @@ async function start(fields, cozyParameters) {
const hasFoundPrices = prices && prices.length > 0
if (hasFoundPrices) {
log('info', 'Found BO prices, applying them to EGL data')
eglData = await applyPrices(eglData, prices)
log('info', 'Found BO prices, applying them to EPGL data')
epglData = await applyPrices(epglData, prices)
}
log('debug', 'Process EGL daily data')
log('debug', 'Process EPGL daily data')
const filterDayKeys = [...rangeDate.day.keys, 'load']
if (prices) filterDayKeys.push('price')
/** @type FormattedData[] */
const daysToUpdate = await hydrateAndFilter(
eglData,
epglData,
rangeDate.day.doctype,
{ keys: filterDayKeys }
)
const { monthDeltas, yearDeltas } = await aggregateLoads(
daysToUpdate,
hasFoundPrices
)
log('debug', 'Store EGL daily load data')
log('debug', 'Store EPGL daily load data')
await updateOrCreate(
daysToUpdate,
rangeDate.day.doctype,
rangeDate.day.keys
)
log('debug', 'Store EGL monthly load data')
log('debug', 'Store EPGL monthly load data')
await updateAggregates(
monthDeltas,
rangeDate.month.doctype,
......@@ -131,7 +161,7 @@ async function start(fields, cozyParameters) {
hasFoundPrices
)
log('debug', 'Store EGL yearly load data')
log('debug', 'Store EPGL yearly load data')
await updateAggregates(
yearDeltas,
rangeDate.year.doctype,
......@@ -142,13 +172,9 @@ async function start(fields, cozyParameters) {
transaction.setStatus(Tracing.SpanStatus.Ok)
transaction.finish()
} catch (error) {
const errorMessage = `EGL konnector encountered an error. Response data: ${JSON.stringify(
error.message
)}`
Sentry.captureMessage(errorMessage, {
Sentry.captureException(error, {
tags: {
section: 'start',
login: fields.login,
},
})
transaction.setStatus(Tracing.SpanStatus.Aborted)
......@@ -157,139 +183,3 @@ async function start(fields, cozyParameters) {
throw error
}
}
/**
* Authenticates a user with the provided credentials and returns an authentication response.
*
* @param {number} login - The user's login.
* @param {string} password - The user's password.
* @param {string} baseUrl - The base URL for the authentication request.
* @param {string} apiAuthKey - The API authentication key.
*
* @throws {Error} - Throws a Cozy error (VENDOR_DOWN or LOGIN_FAILED) in case of authentication failure.
* @returns {Promise<AuthResponse>} - The authentication response containing a token.
*/
async function authenticate(login, password, baseUrl, apiAuthKey) {
log('info', 'Authenticating ...')
const authRequest = {
method: 'post',
url: baseUrl + '/connect.aspx',
headers: {
AuthKey: apiAuthKey,
'Content-Type': 'application/x-www-form-urlencoded',
},
data: {
login: login,
pass: password,
},
}
try {
/** @type {AuthResponse} */
const respData = (await axios(authRequest)).data
if (respData.codeRetour === 100) {
return respData
}
const errorMessage = `Authentication failed. Response data: ${respData?.libelleRetour}`
log('error', errorMessage)
log('error', `Code retour: ${respData?.codeRetour}`)
throw new Error(errors.VENDOR_DOWN)
} catch (error) {
log('error', error.response?.data)
Sentry.captureException(error, {
tags: {
section: 'authenticate',
},
extra: {
compte: login,
},
})
if (error.response?.data.codeRetour === -4) {
throw new Error(errors.LOGIN_FAILED)
}
throw new Error(errors.VENDOR_DOWN)
}
}
/**
* Retrieves data from a specified API using the provided response data and API configuration.
*
* @param {AuthResponse} response - The authentication response containing a valid token.
* @param {string} baseUrl - The base URL for the data request.
* @param {string} apiAuthKey - The API authentication key.
*
* @throws {Error} - Throws a Cozy error (VENDOR_DOWN, LOGIN_FAILED or UNKNOWN_ERROR) in case of failure.
* @returns {Promise<FormattedData[]>} - A promise that resolves to the retrieved and formatted data.
*/
async function getData(
startDateString,
endDateString,
response,
baseUrl,
apiAuthKey
) {
log('debug', `Get EGL data from ${startDateString} to ${endDateString}`)
const dataRequest = {
method: 'post',
url: baseUrl + '/getAllAgregatsByAbonnement.aspx',
headers: {
AuthKey: apiAuthKey,
'Content-Type': 'application/x-www-form-urlencoded',
},
data: {
token: response.resultatRetour.token,
num_abt: response.resultatRetour.num_abt,
date_debut: startDateString,
date_fin: endDateString,
},
}
try {
/** @type {GetDataResponse} */
const respData = (await axios(dataRequest)).data
switch (respData.codeRetour) {
case 100:
// Sort data by date
respData.resultatRetour.sort(
(a, b) => new Date(a.DateReleve) - new Date(b.DateReleve)
)
return format(respData.resultatRetour)
case -2:
log(
'error',
`Get data failed. codeRetour -2. ${respData.libelleRetour}`
)
throw errors.LOGIN_FAILED
case -1:
log(
'error',
`Get data failed. codeRetour -1. ${respData.libelleRetour}`
)
throw errors.VENDOR_DOWN
default:
log(
'error',
`Get data failed. ${respData.codeRetour}. ${respData.libelleRetour}`
)
log('error', respData)
throw errors.UNKNOWN_ERROR
}
} catch (error) {
log('debug', error.message)
Sentry.captureException(error, {
tags: {
section: 'getData',
},
extra: {
start: startDateString,
end: endDateString,
},
})
if (axios.isAxiosError(error)) {
throw new Error(errors.VENDOR_DOWN)
}
throw error
}
}
// @ts-check
const { default: Axios } = require('axios')
const { errors, log } = require('cozy-konnector-libs')
const Sentry = require('@sentry/node')
/**
* Requests an authentication token
*
* @param {string} epglBaseUrl
* @param {string} epglUser
* @param {string} epglPassword
* @returns {Promise<string>}
*/
async function getAuthToken(epglBaseUrl, epglUser, epglPassword) {
log('info', 'getAuthToken')
const data = JSON.stringify({
user: epglUser,
password: epglPassword,
})
try {
const response = await Axios({
method: 'POST',
url: `${epglBaseUrl}/login`,
headers: {
'Content-Type': 'application/json',
},
data: data,
})
const epglToken = response.headers.efluidconnect_token
if (!epglToken) {
throw new Error('Unable to find EFLUIDCONNECT_TOKEN header')
}
return epglToken
} catch (error) {
Sentry.captureException(`Error while getting auth token: ${error}`, {
tags: {
section: 'getAuthToken',
},
})
throw errors.VENDOR_DOWN
}
}
/**
* Makes sure the pair contract ID / metering
*
* @param {string} epglBaseUrl
* @param {string} epglToken
* @param {string} contractId
* @param {string} meteringId
* @returns {Promise<boolean>}
*/
async function isValidContract(epglBaseUrl, epglToken, contractId, meteringId) {
log('info', 'isValidContract')
const data = JSON.stringify({
refContrat: contractId,
refCompteur: meteringId,
})
try {
await Axios({
method: 'POST',
url: `${epglBaseUrl}/INT38/acteurs/rechercherClientParCompteur`,
headers: {
'Content-Type': 'application/json',
EFLUIDCONNECT_TOKEN: epglToken,
},
data: data,
})
return true
} catch (error) {
// If error 500, everything worked fine, but the contract and metering don't match
if (error.response && error.response.status === 500) {
return false
}
// Else, network or API error
Sentry.captureException(`Couldn't check contract validity: ${error}`, {
tags: {
section: 'isValidContract',
},
})
throw errors.VENDOR_DOWN
}
}
/**
* Fetches data with EFluid API
*
* @param {string} epglBaseUrl
* @param {string} epglToken
* @param {string} contractId
* @param {string} meteringId
* @param {string} startingDate
* @param {string} endingDate
*
* @throws {Error} - Throws a Cozy error (VENDOR_DOWN) in case of failure.
* @returns {Promise<FetchDataResponse>} - A promise that resolves to the retrieved data.
*/
async function fetchData(
epglBaseUrl,
epglToken,
contractId,
meteringId,
startingDate,
endingDate
) {
log('info', 'fetchData')
const data = JSON.stringify({
refContrat: contractId,
refCompteur: meteringId,
dateDebut: startingDate,
dateFin: endingDate,
})
try {
const response = await Axios({
method: 'POST',
url: `${epglBaseUrl}/INT38/contrats/consommationsJournalieres`,
headers: {
'Content-Type': 'application/json',
EFLUIDCONNECT_TOKEN: epglToken,
},
data: data,
})
return response.data
} catch (error) {
log('info', error.response)
// If error 500, everything worked fine, but the contract and metering don't match
if (error.response && error.response.status === 500) {
log('info', `Unexpected error status: ${error.response}`)
}
// Else, network or API error
Sentry.captureException(`Error while fetching data: ${error}`, {
tags: {
section: 'fetchData',
},
})
throw errors.VENDOR_DOWN
}
}
module.exports = { getAuthToken, isValidContract, fetchData }
/**
* Enum for aggregate types.
* @readonly
* @enum {string}
*/
const AggregateTypes = {
ANOMALIE: 'A',
ATTENTE: 'T',
DEPART: 'D',
REEL: 'R',
SERVI_A_VENTILATION: 'X',
VENTILE: 'V',
}
module.exports = { AggregateTypes }
......@@ -9,34 +9,37 @@
* @property {number} hour - The hour of the data point (in this case, always 0).
* @property {number} minute - The minute of the data point (in this case, always 0).
* @property {number} price - The price of the data point.
* @property {string} type - The type of the data point.
*/
/**
* @typedef {Object} GetDataResponse
* @property {number} codeRetour
* @property {string} libelleRetour
* @property {Releve[]} resultatRetour
* @typedef {Object} FetchDataResponse
* @property {Postes[]} postes
* @property {Unites} unites
*/
/**
* @typedef {Object} Releve
* @property {string} DateReleve
* @property {string} TypeAgregat
* @property {number} ValeurIndex
* @typedef {Object} Postes
* @property {Releve[]} data
* @property {string} libelle
*/
/**
* @typedef {Object} AuthResponse
* @property {number} codeRetour
* @property {string} libelleRetour
* @property {AuthResult} resultatRetour
* @typedef {Object} Releve
* @property {number} consommation
* @property {number} index
* @property {number} jour
* @property {number} mois
* @property {number} annee
* @property {number} [debitMin]
* @property {number} [volumeEstimeFuite]
*/
/**
* @typedef {Object} AuthResult
* @property {number} num_abt
* @property {string} token
* @typedef {Object} Unites
* @property {string} consommation
* @property {string} index
* @property {string} debitMin
* @property {string} volumeEstimeFuite
*/
/**
......
......@@ -2326,10 +2326,10 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef"
integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==
axios@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621"
integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==
axios@1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
......@@ -6108,11 +6108,6 @@ lru_map@^0.3.3:
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==
luxon@^3.4.3:
version "3.5.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
......@@ -6320,6 +6315,18 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment-timezone@^0.5.45:
version "0.5.46"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.46.tgz#a21aa6392b3c6b3ed916cd5e95858a28d893704a"
integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==
dependencies:
moment "^2.29.4"
moment@^2.29.4, moment@^2.30.1:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
morgan@^1.9.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"
......