From c9de8f72dafbef8f865cb1f36ec11744a977c2e3 Mon Sep 17 00:00:00 2001
From: Pierre Ecarlat <pecarlat@grandlyon.com>
Date: Wed, 15 Jan 2025 14:46:26 +0000
Subject: [PATCH] feat: Update konnector to efluid API

---
 .vscode/settings.json             |   8 +
 konnector-dev-config.example.json |  13 +-
 manifest.konnector                |  22 ++-
 package.json                      |   6 +-
 src/helpers/format.js             |  62 +++-----
 src/helpers/format.spec.js        | 222 +++++++--------------------
 src/helpers/prices.js             |  22 +--
 src/helpers/utils.js              |  38 ++---
 src/helpers/utils.spec.js         |  45 ++----
 src/index.js                      | 246 +++++++++---------------------
 src/requests/epgl.js              | 148 ++++++++++++++++++
 src/types/enums.js                |  15 --
 src/types/types.js                |  35 +++--
 yarn.lock                         |  25 +--
 14 files changed, 408 insertions(+), 499 deletions(-)
 create mode 100644 src/requests/epgl.js
 delete mode 100644 src/types/enums.js

diff --git a/.vscode/settings.json b/.vscode/settings.json
index b4d4ac0..cddd37d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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",
diff --git a/konnector-dev-config.example.json b/konnector-dev-config.example.json
index aefd96b..2993be6 100644
--- a/konnector-dev-config.example.json
+++ b/konnector-dev-config.example.json
@@ -1,14 +1,15 @@
 {
   "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
+}
diff --git a/manifest.konnector b/manifest.konnector
index 9c786c5..4812fce 100644
--- a/manifest.konnector
+++ b/manifest.konnector
@@ -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"
+        }
       }
     }
   },
diff --git a/package.json b/package.json
index 7620358..b83f8f8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/helpers/format.js b/src/helpers/format.js
index 1bc065e..b31f1e1 100644
--- a/src/helpers/format.js
+++ b/src/helpers/format.js
@@ -1,51 +1,35 @@
-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 }
diff --git a/src/helpers/format.spec.js b/src/helpers/format.spec.js
index 5f641e1..4577b24 100644
--- a/src/helpers/format.spec.js
+++ b/src/helpers/format.spec.js
@@ -1,4 +1,4 @@
-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([])
   })
 })
diff --git a/src/helpers/prices.js b/src/helpers/prices.js
index d56fdeb..c580323 100644
--- a/src/helpers/prices.js
+++ b/src/helpers/prices.js
@@ -1,7 +1,7 @@
 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 }
diff --git a/src/helpers/utils.js b/src/helpers/utils.js
index 8c656b4..81f98c1 100644
--- a/src/helpers/utils.js
+++ b/src/helpers/utils.js
@@ -1,7 +1,6 @@
 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 = {
diff --git a/src/helpers/utils.spec.js b/src/helpers/utils.spec.js
index a55823a..023773f 100644
--- a/src/helpers/utils.spec.js
+++ b/src/helpers/utils.spec.js
@@ -1,5 +1,5 @@
 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')
     })
   })
 })
diff --git a/src/index.js b/src/index.js
index 2ff11dd..4ad590f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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
-  }
-}
diff --git a/src/requests/epgl.js b/src/requests/epgl.js
new file mode 100644
index 0000000..b89b542
--- /dev/null
+++ b/src/requests/epgl.js
@@ -0,0 +1,148 @@
+// @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 }
diff --git a/src/types/enums.js b/src/types/enums.js
deleted file mode 100644
index 026cc26..0000000
--- a/src/types/enums.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * 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 }
diff --git a/src/types/types.js b/src/types/types.js
index bbd157b..b2f8c8a 100644
--- a/src/types/types.js
+++ b/src/types/types.js
@@ -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
  */
 
 /**
diff --git a/yarn.lock b/yarn.lock
index f1e32e7..91e9f6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
-- 
GitLab