@open3cl/engine 1.8.50 → 1.8.52

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/11_ecs.js CHANGED
@@ -179,6 +179,33 @@ export function ratio_besoin_ecs_individuel(dpe, de, surfaceImmeuble, nombreAppa
179
179
  return ratio < 1 ? ratio : 1 / nombreAppartements;
180
180
  }
181
181
 
182
+ /**
183
+ * enum_type_installation_id des installations ECS collectives :
184
+ * 2 - installation collective ; 3 - installation collective multi-bâtiment (réseau de chaleur)
185
+ */
186
+ const TYPES_INSTALLATION_ECS_COLLECTIVE = ['2', '3'];
187
+
188
+ /**
189
+ * Rdim (nombre d'installations individuelles identiques) sans objet pour une installation ECS
190
+ * collective décrite dans un DPE immeuble : elle dessert tout l'immeuble, le besoin ECS n'est donc
191
+ * pas ramené à un logement type. Sinon, le besoin serait divisé par rdim alors que les pertes de
192
+ * stockage (Qgw) et à l'arrêt (qp0) du générateur collectif restent celles de l'immeuble,
193
+ * effondrant les rendements de génération et de stockage.
194
+ * @see Guide Tribu TE-211208 p. 24 (Rdim = nombre d'installations individuelles identiques)
195
+ * @see Moteur_DPE.dll Calcul_batiment.cs l.583-592 (Becs / Rdim seulement pour une installation
196
+ * individuelle en immeuble ; Becs × Sh_ecs / SH sans Rdim pour une installation collective)
197
+ * @param th {string} type de bâtiment (maison, appartement, immeuble)
198
+ * @param de {Donnee_entree} donnée d'entrée de l'installation ECS
199
+ * @returns {boolean}
200
+ */
201
+ export function isRdimEcsNeutralise(th, de) {
202
+ return (
203
+ th === 'immeuble' &&
204
+ TYPES_INSTALLATION_ECS_COLLECTIVE.includes(String(de.enum_type_installation_id)) &&
205
+ Number(de.rdim) > 1
206
+ );
207
+ }
208
+
182
209
  export default function calc_ecs(
183
210
  dpe,
184
211
  ecs,
@@ -227,6 +254,12 @@ export default function calc_ecs(
227
254
  ((de.surface_habitable / (surfaceImmeuble || 1)) * (de.rdim || 1)) /
228
255
  (de.nombre_logement * nombreAppartements);
229
256
  }
257
+ } else if (isRdimEcsNeutralise(th, de)) {
258
+ /**
259
+ * Installation ECS collective d'un DPE immeuble : le besoin est celui de l'immeuble (pas de
260
+ * division par rdim), la consommation n'étant pas remultipliée par rdim (conso.js).
261
+ */
262
+ di.rdim_neutralise = true;
230
263
  } else if (de.rdim) {
231
264
  di.ratio_besoin_ecs = 1 / de.rdim || 1;
232
265
  }
package/11_ecs.spec.js CHANGED
@@ -31,7 +31,7 @@ vi.mock('./14_generateur_ecs.js', () => ({
31
31
  default: vi.fn()
32
32
  }));
33
33
 
34
- const { default: calc_ecs } = await import('./11_ecs.js');
34
+ const { default: calc_ecs, isRdimEcsNeutralise } = await import('./11_ecs.js');
35
35
  const { tv, requestInput } = await import('./utils.js');
36
36
  const { default: calc_gen_ecs } = await import('./14_generateur_ecs.js');
37
37
 
@@ -379,6 +379,91 @@ describe('calc_ecs - immeuble à plusieurs installations ECS (prorata des surfac
379
379
  });
380
380
  });
381
381
 
382
+ /**
383
+ * Installation ECS collective unique d'un DPE immeuble : le rdim (nombre d'installations
384
+ * individuelles identiques) est sans objet, le besoin n'est pas divisé par rdim et la consommation
385
+ * n'est pas remultipliée par rdim à l'agrégation (conso.js getRdimEcs).
386
+ * @see Guide Tribu TE-211208 p. 24
387
+ * @see Moteur_DPE.dll Calcul_batiment.cs l.583-592 (Becs / Rdim seulement en installation individuelle)
388
+ */
389
+ describe('calc_ecs - installation ECS collective en immeuble : rdim neutralisé', () => {
390
+ test.each([
391
+ ['collective', '2'],
392
+ ['collective multi-bâtiment', '3']
393
+ ])('installation %s, rdim 22 : besoin de l’immeuble (ratio 1)', (_libelle, type) => {
394
+ const ecs = makeEcs({
395
+ tv_rendement_distribution_ecs_id: '5',
396
+ enum_type_installation_id: type,
397
+ rdim: 22
398
+ });
399
+ calc_ecs({}, ecs, 1000, 1500, 1, 'ca1', 'h1a', 'immeuble', false, 681, 22, false, {});
400
+
401
+ expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(1);
402
+ expect(ecs.donnee_intermediaire.besoin_ecs).toBe(1000);
403
+ expect(ecs.donnee_intermediaire.besoin_ecs_depensier).toBe(1500);
404
+ expect(ecs.donnee_intermediaire.rdim_neutralise).toBe(true);
405
+ // le générateur reçoit le besoin de l'immeuble (cohérent avec ses pertes Qgw / qp0)
406
+ expect(vi.mocked(calc_gen_ecs).mock.calls[0][2].besoin_ecs).toBe(1000);
407
+ });
408
+
409
+ test.each([
410
+ ['installation individuelle en immeuble', '1', 'immeuble', 0.25],
411
+ ['installation hybride collective-individuelle', '4', 'immeuble', 0.25],
412
+ ['DPE appartement (installation collective)', '2', 'appartement', 0.25],
413
+ ['DPE maison', '2', 'maison', 0.25]
414
+ ])('%s : division par rdim conservée', (_libelle, type, th, ratio) => {
415
+ const ecs = makeEcs({
416
+ tv_rendement_distribution_ecs_id: '5',
417
+ enum_type_installation_id: type,
418
+ rdim: 4
419
+ });
420
+ calc_ecs({}, ecs, 1000, 1500, 1, 'ca1', 'h1a', th, false, 200, null, false, {});
421
+
422
+ expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(ratio);
423
+ expect(ecs.donnee_intermediaire.rdim_neutralise).toBeUndefined();
424
+ });
425
+
426
+ test('installation collective en immeuble, rdim 1 : inchangé (ratio 1, pas d’indicateur)', () => {
427
+ const ecs = makeEcs({
428
+ tv_rendement_distribution_ecs_id: '5',
429
+ enum_type_installation_id: '2',
430
+ rdim: 1
431
+ });
432
+ calc_ecs({}, ecs, 1000, 1500, 1, 'ca1', 'h1a', 'immeuble', false, 200, null, false, {});
433
+
434
+ expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(1);
435
+ expect(ecs.donnee_intermediaire.rdim_neutralise).toBeUndefined();
436
+ });
437
+
438
+ test('immeuble multi-installations : prorata Sh / SH / rdim conservé (branche prioritaire)', () => {
439
+ const ecs = makeEcs({
440
+ tv_rendement_distribution_ecs_id: '5',
441
+ enum_type_installation_id: '2',
442
+ surface_habitable: 100,
443
+ rdim: 4
444
+ });
445
+ calc_ecs({}, ecs, 1000, 1500, 1, 'ca1', 'h1a', 'immeuble', false, 200, null, false, {}, true);
446
+
447
+ expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBeCloseTo(100 / 200 / 4, 9);
448
+ expect(ecs.donnee_intermediaire.rdim_neutralise).toBeUndefined();
449
+ });
450
+ });
451
+
452
+ describe('isRdimEcsNeutralise', () => {
453
+ test.each([
454
+ ['immeuble', '2', 22, true],
455
+ ['immeuble', 3, '2', true],
456
+ ['immeuble', '2', 1, false],
457
+ ['immeuble', '2', undefined, false],
458
+ ['immeuble', '1', 22, false],
459
+ ['immeuble', '4', 22, false],
460
+ ['appartement', '2', 22, false],
461
+ ['maison', '2', 22, false]
462
+ ])('th %s, type %s, rdim %s → %s', (th, type, rdim, attendu) => {
463
+ expect(isRdimEcsNeutralise(th, { enum_type_installation_id: type, rdim })).toBe(attendu);
464
+ });
465
+ });
466
+
382
467
  describe('calc_ecs - rendement de distribution', () => {
383
468
  test('la ligne forfaitaire alimente rendement_distribution et l’identifiant de table', () => {
384
469
  const de = { tv_rendement_distribution_ecs_id: '5' };
package/README.md CHANGED
@@ -370,29 +370,29 @@ 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.51`** · branche `main` · généré le 2026-10-12
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
377
377
  <tr>
378
378
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
379
379
  <td align="center"><strong>89 984</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>62 220</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>69,15 %</strong><br/><sub>réussite globale</sub></td>
380
+ <td align="center"><strong>65 391</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>72,67 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
385
385
  | | Corpus | Réussite | | DPE conformes |
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,45 %** | `█████████░░░░░░░░░░░` | 4 645 / 9 999 |
388
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,84 %** | `███████████████████░` | 9 284 / 10 000 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,65 %** | `█████████░░░░░░░░░░░` | 4 665 / 9 999 |
388
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,90 %** | `███████████████████░` | 9 290 / 10 000 |
389
+ | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **91,27 %** | `██████████████████░░` | 9 127 / 10 000 |
389
390
  | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,93 %** | `██████████████████░░` | 8 986 / 9 992 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,83 %** | `██████████████████░░` | 8 882 / 9 999 |
391
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,88 %** | `██████████████████░░` | 8 887 / 9 999 |
391
392
  | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,51 %** | `█████████████████░░░` | 8 449 / 9 998 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,71 %** | `██████████████░░░░░░` | 7 171 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **66,41 %** | `█████████████░░░░░░░` | 6 640 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **54,15 %** | `███████████░░░░░░░░░` | 5 413 / 9 997 |
395
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,50 %** | `██████░░░░░░░░░░░░░░` | 2 750 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **74,46 %** | `███████████████░░░░░` | 7 445 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **56,59 %** | `███████████░░░░░░░░░` | 5 657 / 9 997 |
395
+ | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **28,85 %** | `██████░░░░░░░░░░░░░░` | 2 885 / 10 000 |
396
396
 
397
397
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
398
398
 
@@ -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,45 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,78 ms</strong><br/><sub>médiane</sub></td>
408
+ <td align="center"><strong>0,15 ms</strong><br/><sub>min</sub></td>
409
+ <td align="center"><strong>841,3 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,50 ms | 4,05 ms | 0,20 ms | 31,7 ms | 8,21 ms | 11,1 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 4,06 ms | 3,72 ms | 1,41 ms | 25,8 ms | 6,98 ms | 8,99 ms |
417
+ | **Appartement · chauffage collectif (2025)** | 4,59 ms | 4,24 ms | 1,54 ms | 36,0 ms | 7,48 ms | 9,85 ms |
418
+ | **Logement individuel (2025)** | 5,25 ms | 4,72 ms | 1,67 ms | 35,7 ms | 9,73 ms | 13,1 ms |
419
+ | **Maison individuelle (2025)** | 6,28 ms | 5,68 ms | 1,90 ms | 54,8 ms | 11,2 ms | 14,4 ms |
420
+ | **Immeuble · chauffage individuel** | 6,81 ms | 5,72 ms | 0,15 ms | 173,2 ms | 13,9 ms | 22,8 ms |
421
+ | **Immeuble · chauffage collectif** | 5,66 ms | 4,82 ms | 0,19 ms | 103,6 ms | 10,4 ms | 17,8 ms |
422
+ | **Immeuble · chauffage mixte** | 5,50 ms | 4,52 ms | 0,20 ms | 841,3 ms | 11,4 ms | 18,4 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,44 ms | 5,54 ms | 1,68 ms | 71,5 ms | 12,0 ms | 19,9 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
  }, []);
@@ -406,9 +484,9 @@ export default function calc_conso(
406
484
  // S'il existe une clé de repartition ECS, utilisation de celle-ci pour répartir la conso ECS collective à l'appartement
407
485
  generateur_ecs.forEach((value) => {
408
486
  value.donnee_entree.cle_repartition_ecs =
409
- (ecs.donnee_entree.cle_repartition_ecs || 1) * (ecs.donnee_entree.rdim || 1);
487
+ (ecs.donnee_entree.cle_repartition_ecs || 1) * getRdimEcs(ecs);
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);
@@ -794,6 +872,19 @@ function getEcsConso(gen_ecs, field, coef, prorataECS, prefix, facteurAc = 1) {
794
872
  }, 0);
795
873
  }
796
874
 
875
+ /**
876
+ * Rdim appliqué à la consommation d'une installation ECS lors de l'agrégation. Il vaut 1 lorsque le
877
+ * besoin n'a pas été divisé par rdim (installation ECS collective en immeuble, cf. 11_ecs.js
878
+ * isRdimEcsNeutralise), afin de rester cohérent avec le besoin calculé.
879
+ * @see Moteur_DPE.dll Calcul_batiment.cs l.1067 (conso × Rdim, Rdim = 1 transmis en collectif)
880
+ * @param inst {InstallationEcs}
881
+ * @returns {number}
882
+ */
883
+ export function getRdimEcs(inst) {
884
+ if (inst.donnee_intermediaire?.rdim_neutralise) return 1;
885
+ return Number(inst.donnee_entree.rdim) || 1;
886
+ }
887
+
797
888
  /**
798
889
  * Consommation des auxiliaires de génération ECS (circulateurs des générateurs).
799
890
  * Le besoin d'une installation dimensionnée (rdim > 1) est exprimé par unité : comme la
@@ -809,7 +900,7 @@ function getEcsConso(gen_ecs, field, coef, prorataECS, prefix, facteurAc = 1) {
809
900
  */
810
901
  export function getAuxGenerationEcs(ecs, field, coef, prorataECS, facteurAc = 1) {
811
902
  return ecs.reduce((acc, inst) => {
812
- const rdim = prorataECS === 1 ? Number(inst.donnee_entree.rdim) || 1 : 1;
903
+ const rdim = prorataECS === 1 ? getRdimEcs(inst) : 1;
813
904
  return inst.generateur_ecs_collection.generateur_ecs.reduce((acc2, gen) => {
814
905
  const conso = (gen.donnee_intermediaire[field] || 0) * rdim * facteurAc;
815
906
  return acc2 + getConso(coef, 'électricité auxiliaire', conso);
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 {
@@ -56,10 +64,16 @@ const {
56
64
  coefCoutProrata,
57
65
  masqueEnergie,
58
66
  getAuxGenerationEcs,
67
+ getRdimEcs,
59
68
  DATE_BAREME_COUT_2024,
60
69
  appointBijonction,
61
- coutBijonction
70
+ coutBijonction,
71
+ ARRETES_RESEAU_CHALEUR,
72
+ COEF_GES_AUTRES_RESEAUX_CHALEUR,
73
+ indexArreteReseauChaleur,
74
+ getContenuCo2ReseauChaleur
62
75
  } = await import('./conso.js');
76
+ const { tv } = await import('./utils.js');
63
77
 
64
78
  /**
65
79
  * Classe énergétique (bilan DPE)
@@ -432,7 +446,7 @@ describe('calc_conso - agrégation des consommations', () => {
432
446
  expect(res.emission_ges.emission_ges_ch).toBeCloseTo(150, 9);
433
447
  });
434
448
 
435
- test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée', () => {
449
+ test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée, facteur « autres réseaux » 0,385', () => {
436
450
  const ch = [
437
451
  installCh({ cle_repartition_ch: 1 }, [
438
452
  genCh(
@@ -443,8 +457,10 @@ describe('calc_conso - agrégation des consommations', () => {
443
457
  ])
444
458
  ];
445
459
 
446
- calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
460
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
447
461
  expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('R2'));
462
+ // Plus de repli sur 1 kgCO2/kWh (GES = consommation) : 1000 * 0,385
463
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
448
464
  });
449
465
 
450
466
  test('réseau de chaleur sans identifiant : coefficient GES forfaitaire 0,385', () => {
@@ -459,8 +475,8 @@ describe('calc_conso - agrégation des consommations', () => {
459
475
  expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
460
476
  });
461
477
 
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
478
+ test("date d'arrêté réseau de chaleur invalide : aucune exception", () => {
479
+ // date_arrete_reseau_chaleur = Symbol : ignorée (repli sur la date du DPE)
464
480
  const ch = [
465
481
  installCh({ cle_repartition_ch: 1 }, [
466
482
  genCh(
@@ -536,6 +552,20 @@ describe('calc_conso - agrégation des consommations', () => {
536
552
  expect(res.ef_conso.conso_ecs).toBe(600);
537
553
  });
538
554
 
555
+ test('ECS collective en immeuble, rdim neutralisé (11_ecs.js) : conso non remultipliée par rdim', () => {
556
+ const inst = installEcs({ cle_repartition_ecs: 2, rdim: 22 }, [
557
+ genEcs('1', { conso_ecs: 100, conso_ecs_depensier: 120 })
558
+ ]);
559
+ inst.donnee_intermediaire.rdim_neutralise = true;
560
+
561
+ const res = calc_conso(100, 1, 1, [], [], [inst], [], 1, 1, DATE_DPE, coef_ep);
562
+ // clé = 2 × 1 (rdim neutralisé) => conso_ecs = 100 × 2
563
+ expect(inst.generateur_ecs_collection.generateur_ecs[0].donnee_entree.cle_repartition_ecs).toBe(
564
+ 2
565
+ );
566
+ expect(res.ef_conso.conso_ecs).toBe(200);
567
+ });
568
+
539
569
  test('ECS avec prorataECS différent de 1 : clé issue du prorata', () => {
540
570
  // Quand prorataECS !== 1, la boucle ECS ne renseigne pas donnee_utilisateur :
541
571
  // on la fournit en amont (coefficient GES déjà connu) pour refléter les données réelles.
@@ -976,6 +1006,14 @@ describe('getAuxGenerationEcs - auxiliaires de génération ECS et rdim', () =>
976
1006
  expect(getAuxGenerationEcs(ecs, 'conso_auxiliaire_generation_ecs', coefUnitaire, 1)).toBe(4);
977
1007
  });
978
1008
 
1009
+ test('installation ECS collective en immeuble (rdim neutralisé) : auxiliaires non multipliés', () => {
1010
+ const collective = inst(22, 4);
1011
+ collective.donnee_intermediaire.rdim_neutralise = true;
1012
+ const ecs = [collective, inst(3, 1)];
1013
+ // 4 × 1 (rdim neutralisé) + 1 × 3
1014
+ expect(getAuxGenerationEcs(ecs, 'conso_auxiliaire_generation_ecs', coefUnitaire, 1)).toBe(7);
1015
+ });
1016
+
979
1017
  test('calc_conso : auxiliaire_generation_ecs intègre le rdim', () => {
980
1018
  const ecs = [inst(2, 5)];
981
1019
  const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE, coef_ep);
@@ -1000,6 +1038,23 @@ describe('getAuxGenerationEcs - auxiliaires de génération ECS et rdim', () =>
1000
1038
  });
1001
1039
  });
1002
1040
 
1041
+ /**
1042
+ * Rdim appliqué à la conso ECS lors de l'agrégation : 1 si le besoin n'a pas été divisé par rdim.
1043
+ * @see Moteur_DPE.dll Calcul_batiment.cs l.1067
1044
+ */
1045
+ describe('getRdimEcs', () => {
1046
+ test.each([
1047
+ ['rdim renseigné', { rdim: 3 }, {}, 3],
1048
+ ['rdim chaîne', { rdim: '2.5' }, {}, 2.5],
1049
+ ['rdim absent', {}, {}, 1],
1050
+ ['rdim non numérique', { rdim: 'abc' }, {}, 1],
1051
+ ['rdim neutralisé', { rdim: 22 }, { rdim_neutralise: true }, 1],
1052
+ ['sans donnée intermédiaire', { rdim: 4 }, undefined, 4]
1053
+ ])('%s', (_libelle, de, di, attendu) => {
1054
+ expect(getRdimEcs({ donnee_entree: de, donnee_intermediaire: di })).toBe(attendu);
1055
+ });
1056
+ });
1057
+
1003
1058
  /**
1004
1059
  * Convecteurs bi-jonction (§9.1.6) : base (60 %) facturée en électricité collective, appoint
1005
1060
  * individuel (40 %) en électricité individuelle.
@@ -1169,3 +1224,166 @@ describe('appointBijonction / coutBijonction', () => {
1169
1224
  );
1170
1225
  });
1171
1226
  });
1227
+
1228
+ /**
1229
+ * Facteur d'émission des réseaux de chaleur urbains : table de l'arrêté utilisé
1230
+ * (modele_donnee/arrete_reseau_chaleur.json de l'observatoire DPE).
1231
+ */
1232
+ describe('réseaux de chaleur - sélection de l’arrêté et du contenu CO2', () => {
1233
+ let errorSpy;
1234
+
1235
+ beforeEach(() => {
1236
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1237
+ vi.mocked(tv).mockReset();
1238
+ utilsState.bugForBugCompat = false;
1239
+ });
1240
+
1241
+ afterEach(() => {
1242
+ errorSpy.mockRestore();
1243
+ vi.mocked(tv).mockImplementation(tvParDefaut);
1244
+ utilsState.bugForBugCompat = false;
1245
+ });
1246
+
1247
+ /** Tables mockées : identifiant -> { table: ligne } */
1248
+ const mockTables = (lignes) =>
1249
+ vi
1250
+ .mocked(tv)
1251
+ .mockImplementation((table, matcher) => lignes[matcher.identifiant_reseau]?.[table] ?? null);
1252
+
1253
+ test('six arrêtés, dans l’ordre chronologique, table ≠ année de l’arrêté', () => {
1254
+ expect(ARRETES_RESEAU_CHALEUR.map((a) => a.table)).toEqual([
1255
+ 'reseau_chaleur_2020',
1256
+ 'reseau_chaleur_2021',
1257
+ 'reseau_chaleur_2022',
1258
+ 'reseau_chaleur_2023',
1259
+ 'reseau_chaleur_2024',
1260
+ 'reseau_chaleur_2025'
1261
+ ]);
1262
+ // L'arrêté du 05/07/2024 publie la table 2023 (cause du bug : 2024 - 1 = 2023 forcé à 2022)
1263
+ expect(ARRETES_RESEAU_CHALEUR[3]).toMatchObject({
1264
+ table: 'reseau_chaleur_2023',
1265
+ date_arrete: '2024-07-05'
1266
+ });
1267
+ const dates = ARRETES_RESEAU_CHALEUR.map((a) => a.date_arrete);
1268
+ expect([...dates].sort()).toEqual(dates);
1269
+ ARRETES_RESEAU_CHALEUR.forEach((a) => expect(a.date_application >= a.date_arrete).toBe(true));
1270
+ });
1271
+
1272
+ test.each([
1273
+ ['2020-10-12', 0],
1274
+ ['2021-10-21', 1],
1275
+ ['2022-01-18', 1], // date hors liste : dernier arrêté publié avant
1276
+ ['2023-03-16', 2],
1277
+ ['2024-07-05', 3],
1278
+ ['2024-07-10', 3],
1279
+ ['2025-04-11', 4],
1280
+ ['2026-04-25', 5],
1281
+ ['2030-01-01', 5],
1282
+ ['2019-01-01', 0]
1283
+ ])('date_arrete_reseau_chaleur %s : arrêté n°%i', (dateArrete, index) => {
1284
+ // La date du DPE est ignorée quand la date d'arrêté est renseignée
1285
+ expect(indexArreteReseauChaleur(dateArrete, '2030-01-01')).toBe(index);
1286
+ });
1287
+
1288
+ test.each([
1289
+ ['2022-01-17', 0],
1290
+ ['2022-01-18', 1],
1291
+ ['2023-04-15', 1], // arrêté du 16/03/2023 applicable au 16/04/2023 seulement
1292
+ ['2023-04-16', 2],
1293
+ ['2024-08-09', 3],
1294
+ [undefined, 0],
1295
+ ['date invalide', 0]
1296
+ ])('sans date d’arrêté, DPE du %s : arrêté applicable n°%i', (dateDpe, index) => {
1297
+ expect(indexArreteReseauChaleur(undefined, dateDpe)).toBe(index);
1298
+ });
1299
+
1300
+ test('date d’arrêté illisible (non chaîne) : repli sur la date du DPE', () => {
1301
+ expect(indexArreteReseauChaleur(Symbol('invalide'), '2024-08-09')).toBe(3);
1302
+ expect(indexArreteReseauChaleur(new Date('2024-07-05'), undefined)).toBe(3);
1303
+ });
1304
+
1305
+ test('arrêté du 05/07/2024 : lecture de reseau_chaleur_2023 et contenu ACV (cas Pau 6403C)', () => {
1306
+ mockTables({
1307
+ '6403C': { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1308
+ });
1309
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', 'Pleiades')).toBe(0.061);
1310
+ expect(tv).toHaveBeenCalledTimes(1);
1311
+ expect(tv).toHaveBeenCalledWith('reseau_chaleur_2023', { identifiant_reseau: '6403C' });
1312
+ });
1313
+
1314
+ test('bug_for_bug_compat + LICIEL + table 2023 : contenu CO2 hors ACV transmis par le logiciel', () => {
1315
+ utilsState.bugForBugCompat = true;
1316
+ mockTables({
1317
+ '6403C': {
1318
+ reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' },
1319
+ reseau_chaleur_2022: { contenu_co2: '0.1', contenu_co2_acv: '0.2' }
1320
+ }
1321
+ });
1322
+ const liciel = 'LICIEL Diagnostics v4 [Version XML:305]';
1323
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', liciel)).toBe(0.047);
1324
+ // Autre logiciel : valeur ACV
1325
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', 'DPEWIN V5.2.5')).toBe(
1326
+ 0.061
1327
+ );
1328
+ expect(getContenuCo2ReseauChaleur('6403C', '2024-07-05', '2025-01-24', undefined)).toBe(0.061);
1329
+ // LICIEL avec un autre arrêté (16/03/2023 -> table 2022) : valeur ACV
1330
+ expect(getContenuCo2ReseauChaleur('6403C', '2023-03-16', '2023-06-01', liciel)).toBe(0.2);
1331
+ });
1332
+
1333
+ test('sans bug_for_bug_compat, LICIEL lit aussi le contenu ACV', () => {
1334
+ mockTables({
1335
+ R: { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1336
+ });
1337
+ expect(getContenuCo2ReseauChaleur('R', '2024-07-05', undefined, 'LICIEL')).toBe(0.061);
1338
+ });
1339
+
1340
+ test('réseau absent de la table de l’arrêté : table antérieure la plus récente qui le contient', () => {
1341
+ mockTables({
1342
+ R: {
1343
+ reseau_chaleur_2021: { contenu_co2: '0.3', contenu_co2_acv: '0.33' },
1344
+ reseau_chaleur_2020: { contenu_co2: '0.4', contenu_co2_acv: '0.44' }
1345
+ }
1346
+ });
1347
+ expect(getContenuCo2ReseauChaleur('R', '2024-07-05', undefined, undefined)).toBe(0.33);
1348
+ expect(vi.mocked(tv).mock.calls.map((c) => c[0])).toEqual([
1349
+ 'reseau_chaleur_2023',
1350
+ 'reseau_chaleur_2022',
1351
+ 'reseau_chaleur_2021'
1352
+ ]);
1353
+ });
1354
+
1355
+ test('réseau introuvable dans toutes les tables : facteur « autres réseaux de chaleur » 0,385 et erreur', () => {
1356
+ mockTables({});
1357
+ expect(COEF_GES_AUTRES_RESEAUX_CHALEUR).toBe(0.385);
1358
+ expect(getContenuCo2ReseauChaleur('XXXX', '2024-07-05', undefined, undefined)).toBe(0.385);
1359
+ expect(tv).toHaveBeenCalledTimes(4);
1360
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('XXXX'));
1361
+ });
1362
+
1363
+ test('calc_conso : facteur réseau appliqué au chauffage et à l’ECS, version logiciel transmise', () => {
1364
+ utilsState.bugForBugCompat = true;
1365
+ mockTables({
1366
+ '6403C': { reseau_chaleur_2023: { contenu_co2: '0.047', contenu_co2_acv: '0.061' } }
1367
+ });
1368
+ const de = { identifiant_reseau_chaleur: '6403C', date_arrete_reseau_chaleur: '2024-07-05' };
1369
+ const ch = [
1370
+ installCh({ cle_repartition_ch: 1 }, [
1371
+ genCh('3', { conso_ch: 1000, conso_ch_depensier: 1200 }, de)
1372
+ ])
1373
+ ];
1374
+ const ecs = [
1375
+ installEcs({ cle_repartition_ecs: 1 }, [
1376
+ genEcs('3', { conso_ecs: 500, conso_ecs_depensier: 600 }, de)
1377
+ ])
1378
+ ];
1379
+ const args = [100, 1, 1, [], ch, ecs, [], 1, 1, '2025-01-24', coef_ep, 1, 1, {}];
1380
+
1381
+ const licielRes = calc_conso(...args, 'LICIEL Diagnostics v4');
1382
+ // 1000 * 0,047 et 500 * 0,047 (valeur ADEME du DPE 2564E0255485Z) — avant : 1000 * 1
1383
+ expect(licielRes.emission_ges.emission_ges_ch).toBeCloseTo(47, 9);
1384
+ expect(licielRes.emission_ges.emission_ges_ecs).toBeCloseTo(23.5, 9);
1385
+
1386
+ const autreRes = calc_conso(...args);
1387
+ expect(autreRes.emission_ges.emission_ges_ch).toBeCloseTo(61, 9);
1388
+ });
1389
+ });
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.52';
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.52",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {