@open3cl/engine 1.8.50 → 1.8.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -370,7 +370,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
370
370
 
371
371
  <!-- CORPUS:START -->
372
372
 
373
- > **Version `1.8.49`** · branche `main` · généré le 2026-10-12
373
+ > **Version `1.8.50`** · branche `main` · généré le 2026-10-12
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
@@ -403,24 +403,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>5,56 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,89 ms</strong><br/><sub>médiane</sub></td>
408
- <td align="center"><strong>0,13 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>581,4 ms</strong><br/><sub>max</sub></td>
406
+ <td align="center"><strong>5,86 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>5,18 ms</strong><br/><sub>médiane</sub></td>
408
+ <td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
409
+ <td align="center"><strong>549,4 ms</strong><br/><sub>max</sub></td>
410
410
  </tr>
411
411
  </table>
412
412
 
413
413
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
414
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 4,50 ms | 4,07 ms | 0,21 ms | 36,7 ms | 8,24 ms | 11,2 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,06 ms | 3,74 ms | 1,54 ms | 29,8 ms | 6,88 ms | 8,57 ms |
417
- | **Logement individuel (2025)** | 5,52 ms | 4,95 ms | 1,71 ms | 34,2 ms | 10,1 ms | 13,7 ms |
418
- | **Maison individuelle (2025)** | 6,52 ms | 5,95 ms | 2,14 ms | 49,4 ms | 11,6 ms | 14,7 ms |
419
- | **Immeuble · chauffage individuel** | 6,74 ms | 5,70 ms | 0,13 ms | 159,0 ms | 13,9 ms | 21,9 ms |
420
- | **Appartement · chauffage collectif (2025)** | 4,52 ms | 4,20 ms | 1,58 ms | 39,5 ms | 7,25 ms | 9,05 ms |
421
- | **Immeuble · chauffage collectif** | 5,77 ms | 4,91 ms | 0,13 ms | 131,8 ms | 10,7 ms | 18,8 ms |
422
- | **Immeuble · chauffage mixte** | 5,79 ms | 4,72 ms | 0,21 ms | 581,4 ms | 12,2 ms | 19,9 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,62 ms | 5,72 ms | 1,79 ms | 58,3 ms | 12,5 ms | 20,3 ms |
415
+ | **Généraliste** | 4,94 ms | 4,45 ms | 0,23 ms | 35,1 ms | 9,08 ms | 13,0 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 4,79 ms | 4,42 ms | 1,59 ms | 39,2 ms | 8,31 ms | 11,2 ms |
417
+ | **Logement individuel (2025)** | 5,40 ms | 4,84 ms | 1,68 ms | 42,3 ms | 10,0 ms | 13,4 ms |
418
+ | **Maison individuelle (2025)** | 6,47 ms | 5,87 ms | 1,94 ms | 55,6 ms | 11,4 ms | 14,6 ms |
419
+ | **Immeuble · chauffage individuel** | 7,31 ms | 6,23 ms | 0,12 ms | 153,7 ms | 14,9 ms | 23,5 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,94 ms | 4,79 ms | 1,61 ms | 34,7 ms | 7,75 ms | 9,99 ms |
421
+ | **Immeuble · chauffage collectif** | 6,38 ms | 5,54 ms | 0,13 ms | 119,4 ms | 11,9 ms | 21,2 ms |
422
+ | **Immeuble · chauffage mixte** | 5,90 ms | 4,82 ms | 0,22 ms | 549,4 ms | 12,7 ms | 20,4 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,63 ms | 5,69 ms | 1,75 ms | 67,7 ms | 12,3 ms | 20,8 ms |
424
424
 
425
425
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
426
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
package/conso.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import enums from './enums.js';
2
2
  import calc_conso_eclairage from './16_conso_eclairage.js';
3
3
  import tvs from './tv.js';
4
- import { tv } from './utils.js';
4
+ import { bug_for_bug_compat, tv } from './utils.js';
5
5
 
6
6
  export const COEFF_EP_2_3 = 2.3;
7
7
  export const COEFF_EP_1_9 = 1.9;
@@ -311,53 +311,130 @@ function getConso(coef, type_energie, conso) {
311
311
  return conso;
312
312
  }
313
313
 
314
+ /**
315
+ * Arrêtés « réseaux de chaleur » et table de valeurs associée (modèle de données de l'observatoire
316
+ * DPE, `modele_donnee/arrete_reseau_chaleur.json`). Le nom de la table ne correspond pas à l'année
317
+ * de l'arrêté (ex. l'arrêté du 05/07/2024 publie la table `reseau_chaleur_2023`).
318
+ * - date_arrete : valeur attendue dans `date_arrete_reseau_chaleur` ;
319
+ * - date_application : date à partir de laquelle l'arrêté s'applique (DPE sans date d'arrêté).
320
+ */
321
+ export const ARRETES_RESEAU_CHALEUR = [
322
+ { table: 'reseau_chaleur_2020', date_arrete: '2020-10-12', date_application: '2020-10-17' },
323
+ { table: 'reseau_chaleur_2021', date_arrete: '2021-10-21', date_application: '2022-01-18' },
324
+ { table: 'reseau_chaleur_2022', date_arrete: '2023-03-16', date_application: '2023-04-16' },
325
+ { table: 'reseau_chaleur_2023', date_arrete: '2024-07-05', date_application: '2024-08-09' },
326
+ { table: 'reseau_chaleur_2024', date_arrete: '2025-04-11', date_application: '2025-05-26' },
327
+ { table: 'reseau_chaleur_2025', date_arrete: '2026-04-25', date_application: '2026-05-25' }
328
+ ];
329
+
330
+ /** Facteur « AUTRES RESEAUX DE CHALEUR » (réseau non répertorié ou inconnu), kgCO2/kWh */
331
+ export const COEF_GES_AUTRES_RESEAUX_CHALEUR = 0.385;
332
+
333
+ /**
334
+ * Normalise une date (chaîne ISO ou Date) au format AAAA-MM-JJ, null si invalide.
335
+ * @param date {string|Date|undefined}
336
+ * @returns {string|null}
337
+ */
338
+ function isoDate(date) {
339
+ if (typeof date !== 'string' && !(date instanceof Date)) return null;
340
+ const d = new Date(date);
341
+ return isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10);
342
+ }
343
+
344
+ /**
345
+ * Index (dans ARRETES_RESEAU_CHALEUR) de l'arrêté réseau de chaleur utilisé par le générateur :
346
+ * - `date_arrete_reseau_chaleur` renseignée : dernier arrêté publié à cette date ou avant ;
347
+ * - sinon : dernier arrêté applicable à la date d'établissement du DPE ;
348
+ * - date antérieure au premier arrêté ou illisible : premier arrêté.
349
+ * @param dateArrete {string|undefined} date_arrete_reseau_chaleur du générateur
350
+ * @param dateDpe {string|undefined} date d'établissement du DPE
351
+ * @returns {number}
352
+ */
353
+ export function indexArreteReseauChaleur(dateArrete, dateDpe) {
354
+ const arrete = isoDate(dateArrete);
355
+ const ref = arrete ?? isoDate(dateDpe);
356
+ const champ = arrete ? 'date_arrete' : 'date_application';
357
+ let index = 0;
358
+ if (ref) {
359
+ ARRETES_RESEAU_CHALEUR.forEach((a, i) => {
360
+ if (a[champ] <= ref) index = i;
361
+ });
362
+ }
363
+ return index;
364
+ }
365
+
366
+ /**
367
+ * Contenu CO2 d'un réseau de chaleur répertorié (kgCO2/kWh).
368
+ *
369
+ * Le facteur réglementaire est le contenu CO2 en analyse de cycle de vie (`contenu_co2_acv`) de la
370
+ * table publiée par l'arrêté utilisé. Si l'identifiant n'y figure pas (réseau retiré d'un arrêté plus
371
+ * récent), on retient la table antérieure la plus récente qui le contient. Réseau introuvable :
372
+ * facteur « autres réseaux de chaleur » (0,385).
373
+ *
374
+ * Mode bug_for_bug_compat : avec l'arrêté du 05/07/2024 (table reseau_chaleur_2023), LICIEL
375
+ * transmet au moteur le contenu CO2 hors ACV (`contenu_co2`) : 1 226/1 234 générateurs LICIEL des
376
+ * corpus immeuble, alors que DPEWIN, Pleiades et 4.1.1 exportent la valeur ACV (ex. réseau 6403C
377
+ * de Pau : 0,047 au lieu de 0,061).
378
+ *
379
+ * @param identifiant_reseau {string}
380
+ * @param dateArrete {string|undefined}
381
+ * @param dateDpe {string|undefined}
382
+ * @param versionLogiciel {string|undefined} administratif.diagnostiqueur.version_logiciel
383
+ * @returns {number}
384
+ */
385
+ export function getContenuCo2ReseauChaleur(
386
+ identifiant_reseau,
387
+ dateArrete,
388
+ dateDpe,
389
+ versionLogiciel
390
+ ) {
391
+ const index = indexArreteReseauChaleur(dateArrete, dateDpe);
392
+
393
+ for (let i = index; i >= 0; i--) {
394
+ const { table } = ARRETES_RESEAU_CHALEUR[i];
395
+ const row = tv(table, { identifiant_reseau });
396
+ if (row) {
397
+ const contenuHorsAcv =
398
+ bug_for_bug_compat &&
399
+ table === 'reseau_chaleur_2023' &&
400
+ /liciel/i.test(versionLogiciel || '');
401
+ return Number(contenuHorsAcv ? row.contenu_co2 : row.contenu_co2_acv);
402
+ }
403
+ }
404
+
405
+ console.error(
406
+ `Aucun coefficient GES trouvé pour le réseau de chaleur ${identifiant_reseau} : valeur « autres réseaux de chaleur » ${COEF_GES_AUTRES_RESEAUX_CHALEUR}`
407
+ );
408
+ return COEF_GES_AUTRES_RESEAUX_CHALEUR;
409
+ }
410
+
314
411
  /**
315
412
  * Récupération du coefficient d'émission GES pour le générateur
316
413
  * @param gen_ch {GenerateurChauffageItem||GenerateurEcsItem}
317
414
  * @param suffix {'ch'|'ecs'}
318
415
  * @param dateDpe {string} date d'établissement du DPE
416
+ * @param versionLogiciel {string} version du logiciel de saisie
319
417
  * @returns {number}
320
418
  */
321
- function getGesCoeffForGenerateur(gen_ch, suffix, dateDpe) {
419
+ function getGesCoeffForGenerateur(gen_ch, suffix, dateDpe, versionLogiciel) {
322
420
  const typeEnergie = getTypeEnergie(gen_ch.donnee_entree, suffix);
323
421
 
324
- let anneeDpe;
325
- try {
326
- if (gen_ch.donnee_entree.date_arrete_reseau_chaleur) {
327
- anneeDpe = new Date(gen_ch.donnee_entree.date_arrete_reseau_chaleur).getFullYear() - 1;
328
- } else {
329
- anneeDpe = new Date(dateDpe).getFullYear() - 1;
330
- }
331
-
332
- if (anneeDpe < 2024) {
333
- anneeDpe = 2022;
334
- }
335
- } catch (e) {
336
- anneeDpe = 2022;
337
- }
338
-
339
- const key_reseau_chaleur = `reseau_chaleur_${anneeDpe}`;
340
-
341
422
  /**
342
423
  * Cas spécifique des réseaux de chaleur urbain qui ont des coefficients différents en fonction de réseau de distribution
343
424
  */
344
425
  if (typeEnergie === 'réseau de chauffage urbain') {
345
- const identifiant_reseau = gen_ch.donnee_entree.identifiant_reseau_chaleur;
346
-
347
- if (identifiant_reseau) {
348
- const row = tv(key_reseau_chaleur, { identifiant_reseau });
349
-
350
- if (row) {
351
- return row.contenu_co2_acv;
352
- } else {
353
- console.error(`
354
- Aucun coefficient GES trouvé pour le réseau de chaleur ${identifiant_reseau}
355
- `);
356
- }
357
- } else {
358
- // Prendre la valeur pour "AUTRES RESEAUX DE CHALEUR"
359
- return 0.385;
426
+ const { identifiant_reseau_chaleur, date_arrete_reseau_chaleur } = gen_ch.donnee_entree;
427
+
428
+ if (identifiant_reseau_chaleur) {
429
+ return getContenuCo2ReseauChaleur(
430
+ identifiant_reseau_chaleur,
431
+ date_arrete_reseau_chaleur,
432
+ dateDpe,
433
+ versionLogiciel
434
+ );
360
435
  }
436
+ // Prendre la valeur pour "AUTRES RESEAUX DE CHALEUR"
437
+ return COEF_GES_AUTRES_RESEAUX_CHALEUR;
361
438
  }
362
439
 
363
440
  return coef_ges[typeEnergie] ?? 1;
@@ -377,7 +454,8 @@ export default function calc_conso(
377
454
  coeffEp,
378
455
  nbLogements = 1,
379
456
  ratioSurfaceCollectif = 1,
380
- facteursAc = {}
457
+ facteursAc = {},
458
+ versionLogiciel = undefined
381
459
  ) {
382
460
  const gen_ch = ch.reduce((acc, ch) => {
383
461
  const generateur_chauffage = ch.generateur_chauffage_collection.generateur_chauffage;
@@ -393,7 +471,7 @@ export default function calc_conso(
393
471
  ch.donnee_entree.enum_methode_calcul_conso_id;
394
472
  value.donnee_entree.enum_type_installation_id = ch.donnee_entree.enum_type_installation_id;
395
473
  (value.donnee_utilisateur = value.donnee_utilisateur || {}).coeffEmissionGes =
396
- getGesCoeffForGenerateur(value, 'ch', dateDpe);
474
+ getGesCoeffForGenerateur(value, 'ch', dateDpe, versionLogiciel);
397
475
  });
398
476
  return acc.concat(generateur_chauffage);
399
477
  }, []);
@@ -408,7 +486,7 @@ export default function calc_conso(
408
486
  value.donnee_entree.cle_repartition_ecs =
409
487
  (ecs.donnee_entree.cle_repartition_ecs || 1) * (ecs.donnee_entree.rdim || 1);
410
488
  (value.donnee_utilisateur = value.donnee_utilisateur || {}).coeffEmissionGes =
411
- getGesCoeffForGenerateur(value, 'ecs', dateDpe);
489
+ getGesCoeffForGenerateur(value, 'ecs', dateDpe, versionLogiciel);
412
490
  });
413
491
  }
414
492
  return acc.concat(generateur_ecs);
package/conso.spec.js CHANGED
@@ -37,13 +37,21 @@ vi.mock('./tv.js', () => ({
37
37
  }
38
38
  }));
39
39
 
40
+ /** Mode bug_for_bug_compat piloté par les tests (export « live » du module utils mocké). */
41
+ const utilsState = vi.hoisted(() => ({ bugForBugCompat: false }));
42
+
43
+ /** Lecture des tables par défaut : réseau de chaleur "R1" connu, "R2" inconnu. */
44
+ const tvParDefaut = (table, matcher) => {
45
+ if (matcher?.identifiant_reseau === 'R1') return { contenu_co2_acv: 0.15 };
46
+ return null;
47
+ };
48
+
40
49
  vi.mock('./utils.js', () => ({
41
50
  set_bug_for_bug_compat: vi.fn(),
42
- tv: vi.fn((table, matcher) => {
43
- // Réseau de chaleur "R1" connu, "R2" inconnu
44
- if (matcher?.identifiant_reseau === 'R1') return { contenu_co2_acv: 0.15 };
45
- return null;
46
- })
51
+ get bug_for_bug_compat() {
52
+ return utilsState.bugForBugCompat;
53
+ },
54
+ tv: vi.fn((table, matcher) => tvParDefaut(table, matcher))
47
55
  }));
48
56
 
49
57
  const {
@@ -58,8 +66,13 @@ const {
58
66
  getAuxGenerationEcs,
59
67
  DATE_BAREME_COUT_2024,
60
68
  appointBijonction,
61
- coutBijonction
69
+ coutBijonction,
70
+ ARRETES_RESEAU_CHALEUR,
71
+ COEF_GES_AUTRES_RESEAUX_CHALEUR,
72
+ indexArreteReseauChaleur,
73
+ getContenuCo2ReseauChaleur
62
74
  } = await import('./conso.js');
75
+ const { tv } = await import('./utils.js');
63
76
 
64
77
  /**
65
78
  * Classe énergétique (bilan DPE)
@@ -432,7 +445,7 @@ describe('calc_conso - agrégation des consommations', () => {
432
445
  expect(res.emission_ges.emission_ges_ch).toBeCloseTo(150, 9);
433
446
  });
434
447
 
435
- test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée', () => {
448
+ test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée, facteur « autres réseaux » 0,385', () => {
436
449
  const ch = [
437
450
  installCh({ cle_repartition_ch: 1 }, [
438
451
  genCh(
@@ -443,8 +456,10 @@ describe('calc_conso - agrégation des consommations', () => {
443
456
  ])
444
457
  ];
445
458
 
446
- calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
459
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
447
460
  expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('R2'));
461
+ // Plus de repli sur 1 kgCO2/kWh (GES = consommation) : 1000 * 0,385
462
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
448
463
  });
449
464
 
450
465
  test('réseau de chaleur sans identifiant : coefficient GES forfaitaire 0,385', () => {
@@ -459,8 +474,8 @@ describe('calc_conso - agrégation des consommations', () => {
459
474
  expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
460
475
  });
461
476
 
462
- test("date de DPE invalide (levée d'exception) : année de repli 2022", () => {
463
- // date_arrete_reseau_chaleur = Symbol => new Date(...) lève => catch => 2022
477
+ test("date d'arrêté réseau de chaleur invalide : aucune exception", () => {
478
+ // date_arrete_reseau_chaleur = Symbol : ignorée (repli sur la date du DPE)
464
479
  const ch = [
465
480
  installCh({ cle_repartition_ch: 1 }, [
466
481
  genCh(
@@ -1169,3 +1184,166 @@ describe('appointBijonction / coutBijonction', () => {
1169
1184
  );
1170
1185
  });
1171
1186
  });
1187
+
1188
+ /**
1189
+ * Facteur d'émission des réseaux de chaleur urbains : table de l'arrêté utilisé
1190
+ * (modele_donnee/arrete_reseau_chaleur.json de l'observatoire DPE).
1191
+ */
1192
+ describe('réseaux de chaleur - sélection de l’arrêté et du contenu CO2', () => {
1193
+ let errorSpy;
1194
+
1195
+ beforeEach(() => {
1196
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1197
+ vi.mocked(tv).mockReset();
1198
+ utilsState.bugForBugCompat = false;
1199
+ });
1200
+
1201
+ afterEach(() => {
1202
+ errorSpy.mockRestore();
1203
+ vi.mocked(tv).mockImplementation(tvParDefaut);
1204
+ utilsState.bugForBugCompat = false;
1205
+ });
1206
+
1207
+ /** Tables mockées : identifiant -> { table: ligne } */
1208
+ const mockTables = (lignes) =>
1209
+ vi
1210
+ .mocked(tv)
1211
+ .mockImplementation((table, matcher) => lignes[matcher.identifiant_reseau]?.[table] ?? null);
1212
+
1213
+ test('six arrêtés, dans l’ordre chronologique, table ≠ année de l’arrêté', () => {
1214
+ expect(ARRETES_RESEAU_CHALEUR.map((a) => a.table)).toEqual([
1215
+ 'reseau_chaleur_2020',
1216
+ 'reseau_chaleur_2021',
1217
+ 'reseau_chaleur_2022',
1218
+ 'reseau_chaleur_2023',
1219
+ 'reseau_chaleur_2024',
1220
+ 'reseau_chaleur_2025'
1221
+ ]);
1222
+ // L'arrêté du 05/07/2024 publie la table 2023 (cause du bug : 2024 - 1 = 2023 forcé à 2022)
1223
+ expect(ARRETES_RESEAU_CHALEUR[3]).toMatchObject({
1224
+ table: 'reseau_chaleur_2023',
1225
+ date_arrete: '2024-07-05'
1226
+ });
1227
+ const dates = ARRETES_RESEAU_CHALEUR.map((a) => a.date_arrete);
1228
+ expect([...dates].sort()).toEqual(dates);
1229
+ ARRETES_RESEAU_CHALEUR.forEach((a) => expect(a.date_application >= a.date_arrete).toBe(true));
1230
+ });
1231
+
1232
+ test.each([
1233
+ ['2020-10-12', 0],
1234
+ ['2021-10-21', 1],
1235
+ ['2022-01-18', 1], // date hors liste : dernier arrêté publié avant
1236
+ ['2023-03-16', 2],
1237
+ ['2024-07-05', 3],
1238
+ ['2024-07-10', 3],
1239
+ ['2025-04-11', 4],
1240
+ ['2026-04-25', 5],
1241
+ ['2030-01-01', 5],
1242
+ ['2019-01-01', 0]
1243
+ ])('date_arrete_reseau_chaleur %s : arrêté n°%i', (dateArrete, index) => {
1244
+ // La date du DPE est ignorée quand la date d'arrêté est renseignée
1245
+ expect(indexArreteReseauChaleur(dateArrete, '2030-01-01')).toBe(index);
1246
+ });
1247
+
1248
+ test.each([
1249
+ ['2022-01-17', 0],
1250
+ ['2022-01-18', 1],
1251
+ ['2023-04-15', 1], // arrêté du 16/03/2023 applicable au 16/04/2023 seulement
1252
+ ['2023-04-16', 2],
1253
+ ['2024-08-09', 3],
1254
+ [undefined, 0],
1255
+ ['date invalide', 0]
1256
+ ])('sans date d’arrêté, DPE du %s : arrêté applicable n°%i', (dateDpe, index) => {
1257
+ expect(indexArreteReseauChaleur(undefined, dateDpe)).toBe(index);
1258
+ });
1259
+
1260
+ test('date d’arrêté illisible (non chaîne) : repli sur la date du DPE', () => {
1261
+ expect(indexArreteReseauChaleur(Symbol('invalide'), '2024-08-09')).toBe(3);
1262
+ expect(indexArreteReseauChaleur(new Date('2024-07-05'), undefined)).toBe(3);
1263
+ });
1264
+
1265
+ test('arrêté du 05/07/2024 : lecture de reseau_chaleur_2023 et contenu ACV (cas Pau 6403C)', () => {
1266
+ mockTables({
1267
+ '6403C': { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1268
+ });
1269
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', 'Pleiades')).toBe(0.061);
1270
+ expect(tv).toHaveBeenCalledTimes(1);
1271
+ expect(tv).toHaveBeenCalledWith('reseau_chaleur_2023', { identifiant_reseau: '6403C' });
1272
+ });
1273
+
1274
+ test('bug_for_bug_compat + LICIEL + table 2023 : contenu CO2 hors ACV transmis par le logiciel', () => {
1275
+ utilsState.bugForBugCompat = true;
1276
+ mockTables({
1277
+ '6403C': {
1278
+ reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' },
1279
+ reseau_chaleur_2022: { contenu_co2: '0.1', contenu_co2_acv: '0.2' }
1280
+ }
1281
+ });
1282
+ const liciel = 'LICIEL Diagnostics v4 [Version XML:305]';
1283
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', liciel)).toBe(0.047);
1284
+ // Autre logiciel : valeur ACV
1285
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', 'DPEWIN V5.2.5')).toBe(
1286
+ 0.061
1287
+ );
1288
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', undefined)).toBe(0.061);
1289
+ // LICIEL avec un autre arrêté (16/03/2023 -> table 2022) : valeur ACV
1290
+ expect(getContenuCo2ReseauChaleur('6403C', '2023-03-16', '2023-06-01', liciel)).toBe(0.2);
1291
+ });
1292
+
1293
+ test('sans bug_for_bug_compat, LICIEL lit aussi le contenu ACV', () => {
1294
+ mockTables({
1295
+ R: { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1296
+ });
1297
+ expect(getContenuCo2ReseauChaleur('R', '2024-07-05', undefined, 'LICIEL')).toBe(0.061);
1298
+ });
1299
+
1300
+ test('réseau absent de la table de l’arrêté : table antérieure la plus récente qui le contient', () => {
1301
+ mockTables({
1302
+ R: {
1303
+ reseau_chaleur_2021: { contenu_co2: '0.3', contenu_co2_acv: '0.33' },
1304
+ reseau_chaleur_2020: { contenu_co2: '0.4', contenu_co2_acv: '0.44' }
1305
+ }
1306
+ });
1307
+ expect(getContenuCo2ReseauChaleur('R', '2024-07-05', undefined, undefined)).toBe(0.33);
1308
+ expect(vi.mocked(tv).mock.calls.map((c) => c[0])).toEqual([
1309
+ 'reseau_chaleur_2023',
1310
+ 'reseau_chaleur_2022',
1311
+ 'reseau_chaleur_2021'
1312
+ ]);
1313
+ });
1314
+
1315
+ test('réseau introuvable dans toutes les tables : facteur « autres réseaux de chaleur » 0,385 et erreur', () => {
1316
+ mockTables({});
1317
+ expect(COEF_GES_AUTRES_RESEAUX_CHALEUR).toBe(0.385);
1318
+ expect(getContenuCo2ReseauChaleur('XXXX', '2024-07-05', undefined, undefined)).toBe(0.385);
1319
+ expect(tv).toHaveBeenCalledTimes(4);
1320
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('XXXX'));
1321
+ });
1322
+
1323
+ test('calc_conso : facteur réseau appliqué au chauffage et à l’ECS, version logiciel transmise', () => {
1324
+ utilsState.bugForBugCompat = true;
1325
+ mockTables({
1326
+ '6403C': { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1327
+ });
1328
+ const de = { identifiant_reseau_chaleur: '6403C', date_arrete_reseau_chaleur: '2024-07-05' };
1329
+ const ch = [
1330
+ installCh({ cle_repartition_ch: 1 }, [
1331
+ genCh('3', { conso_ch: 1000, conso_ch_depensier: 1200 }, de)
1332
+ ])
1333
+ ];
1334
+ const ecs = [
1335
+ installEcs({ cle_repartition_ecs: 1 }, [
1336
+ genEcs('3', { conso_ecs: 500, conso_ecs_depensier: 600 }, de)
1337
+ ])
1338
+ ];
1339
+ const args = [100, 1, 1, [], ch, ecs, [], 1, 1, '2025-01-24', coef_ep, 1, 1, {}];
1340
+
1341
+ const licielRes = calc_conso(...args, 'LICIEL Diagnostics v4');
1342
+ // 1000 * 0,047 et 500 * 0,047 (valeur ADEME du DPE 2564E0255485Z) — avant : 1000 * 1
1343
+ expect(licielRes.emission_ges.emission_ges_ch).toBeCloseTo(47, 9);
1344
+ expect(licielRes.emission_ges.emission_ges_ecs).toBeCloseTo(23.5, 9);
1345
+
1346
+ const autreRes = calc_conso(...args);
1347
+ expect(autreRes.emission_ges.emission_ges_ch).toBeCloseTo(61, 9);
1348
+ });
1349
+ });
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.50';
36
+ const LIB_VERSION = '1.8.51';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
@@ -85,6 +85,7 @@ export function calcul_3cl(inputDpe, options) {
85
85
  }
86
86
  const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id];
87
87
  const dateDpe = dpe.administratif.date_etablissement_dpe;
88
+ const versionLogiciel = dpe.administratif.diagnostiqueur?.version_logiciel;
88
89
  if (modele !== 'dpe 3cl 2021 méthode logement') {
89
90
  console.error('Moteur dpe non implémenté pour le modèle: ' + modele);
90
91
  return null;
@@ -606,7 +607,9 @@ export function calcul_3cl(inputDpe, options) {
606
607
  dateDpe,
607
608
  coef_ep,
608
609
  th === 'immeuble' ? Nb_lgt : 1,
609
- ratioSurfaceCoutCollectif
610
+ ratioSurfaceCoutCollectif,
611
+ {},
612
+ versionLogiciel
610
613
  );
611
614
 
612
615
  // Proratisation au logement d'une installation PV d'immeuble (appartement)
@@ -648,7 +651,8 @@ export function calcul_3cl(inputDpe, options) {
648
651
  coef_ep,
649
652
  th === 'immeuble' ? Nb_lgt : 1,
650
653
  ratioSurfaceCoutCollectif,
651
- facteursAc
654
+ facteursAc,
655
+ versionLogiciel
652
656
  );
653
657
  conso.emission_ges = consoNette.emission_ges;
654
658
  // Ligne électricité de sortie_par_energie nette de l'autoconsommation (issue #34)
@@ -669,7 +673,9 @@ export function calcul_3cl(inputDpe, options) {
669
673
  dateDpe,
670
674
  coef_ep_1_7,
671
675
  th === 'immeuble' ? Nb_lgt : 1,
672
- ratioSurfaceCoutCollectif
676
+ ratioSurfaceCoutCollectif,
677
+ {},
678
+ versionLogiciel
673
679
  );
674
680
 
675
681
  productionENR.calculateEnr(
@@ -695,7 +701,9 @@ export function calcul_3cl(inputDpe, options) {
695
701
  dateDpe,
696
702
  coef_ep_2_3,
697
703
  th === 'immeuble' ? Nb_lgt : 1,
698
- ratioSurfaceCoutCollectif
704
+ ratioSurfaceCoutCollectif,
705
+ {},
706
+ versionLogiciel
699
707
  );
700
708
 
701
709
  productionENR.calculateEnr(
package/engine.spec.js CHANGED
@@ -518,6 +518,24 @@ describe('calcul_3cl - run intégral (maison)', () => {
518
518
  expect(dpe.logement.sortie.cout).toEqual({ cout_5_usages: 2 });
519
519
  });
520
520
 
521
+ test('version du logiciel de saisie transmise à tous les calculs de conso (facteur GES réseau de chaleur)', () => {
522
+ const dpe = makeDpe();
523
+ dpe.administratif.diagnostiqueur = { version_logiciel: 'LICIEL Diagnostics v4' };
524
+ const facteurs = { ch: 0.8 };
525
+ vi.mocked(calculateEnr).mockImplementationOnce((enr, conso, Sh, th, zc, coef, ratio, f) => {
526
+ Object.assign(f, facteurs);
527
+ return { pv: 1 };
528
+ });
529
+
530
+ calcul_3cl(dpe, { sanitize: false });
531
+
532
+ // Lue avant l'écrasement de `diagnostiqueur` par la version du moteur
533
+ expect(calc_conso).toHaveBeenCalledTimes(4);
534
+ calc_conso.mock.calls.forEach((call) => expect(call[14]).toBe('LICIEL Diagnostics v4'));
535
+ expect(calc_conso.mock.calls[0][13]).toEqual({});
536
+ expect(calc_conso.mock.calls[1][13]).toEqual(facteurs);
537
+ });
538
+
521
539
  test('bâtiment matériaux anciens => ilpa = 1 ; incohérence inertie => erreur loggée', () => {
522
540
  const dpe = makeDpe();
523
541
  dpe.logement.meteo.batiment_materiaux_anciens = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.50",
3
+ "version": "1.8.51",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {