@open3cl/engine 1.7.2 → 1.8.0

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_besoin_ecs.js CHANGED
@@ -24,11 +24,14 @@ export function calc_besoin_ecs_j(ca, mois, zc, nadeq, depensier) {
24
24
  export default function calc_besoin_ecs(ca, zc, nadeq) {
25
25
  const ret = {
26
26
  besoin_ecs: 0,
27
- besoin_ecs_depensier: 0
27
+ besoin_ecs_depensier: 0,
28
+ besoin_ecs_par_mois: {}
28
29
  };
29
30
  for (const mois of mois_liste) {
30
- ret.besoin_ecs += calc_besoin_ecs_j(ca, mois, zc, nadeq, false);
31
+ const becs_j = calc_besoin_ecs_j(ca, mois, zc, nadeq, false);
32
+ ret.besoin_ecs += becs_j;
31
33
  ret.besoin_ecs_depensier += calc_besoin_ecs_j(ca, mois, zc, nadeq, true);
34
+ ret.besoin_ecs_par_mois[mois] = becs_j;
32
35
  }
33
36
  return ret;
34
37
  }
package/11_ecs.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import enums from './enums.js';
2
2
  import calc_gen_ecs from './14_generateur_ecs.js';
3
- import { tv, requestInput } from './utils.js';
3
+ import { tv, requestInput, mois_liste, Njj } from './utils.js';
4
4
 
5
5
  function tv_rendement_distribution_ecs(di, de, du, pvc) {
6
6
  let matcher = {};
@@ -49,6 +49,89 @@ function tv_rendement_distribution_ecs(di, de, du, pvc) {
49
49
  }
50
50
  }
51
51
 
52
+ /**
53
+ * Calcul de la consommation des auxiliaires de distribution ECS - CAS IMMEUBLE
54
+ * Calcul installation par installation pour les réseaux collectifs bouclés ou avec traçage.
55
+ *
56
+ * @param {object} de - donnee_entree de l'installation ECS
57
+ * @param {number} surfaceImmeuble - surface habitable totale de l'immeuble (m²)
58
+ * @param {object} becsParMois - besoin ECS mensuel de l'immeuble {Janvier: X, Février: X, ...} [kWh]
59
+ * @returns {number} conso_auxiliaire_distribution_ecs_installation [Wh]
60
+ */
61
+ function calc_auxiliaire_distribution_ecs_immeuble(de, surfaceImmeuble, becsParMois) {
62
+ const type_installation = enums.type_installation[de.enum_type_installation_id];
63
+
64
+ // Seulement pour les installations collectives
65
+ if (!type_installation || !type_installation.includes('collective')) {
66
+ return 0;
67
+ }
68
+
69
+ const enum_bouclage_id = String(de.enum_bouclage_reseau_ecs_id);
70
+ const surface_installation = de.surface_habitable || 0;
71
+ const surface_immeuble_total = surfaceImmeuble || 1;
72
+ const ratio_surface = surface_installation / surface_immeuble_total;
73
+
74
+ // CAS 1 : réseau non bouclé → 0
75
+ if (enum_bouclage_id === '1') {
76
+ return 0;
77
+ }
78
+
79
+ // CAS 3 : traçage → 0.14 × BECS_annuel × ratio_surface [Wh]
80
+ if (enum_bouclage_id === '3') {
81
+ const becs_annuel_kwh = Object.values(becsParMois).reduce((acc, v) => acc + v, 0);
82
+ // becs_annuel est en kWh → × 1000 pour obtenir des Wh
83
+ return 0.14 * becs_annuel_kwh * 1000 * ratio_surface;
84
+ }
85
+
86
+ // CAS 2 : réseau bouclé → calcul complet 9 étapes [Wh]
87
+ if (enum_bouclage_id === '2') {
88
+ const Sh_inst = surface_installation;
89
+ const Niv_inst = Number(de.nombre_niveau_installation_ecs) || 1;
90
+
91
+ // Étape 3 : Calcul de Lb pour l'installation i
92
+ // Lb = 1.2 × (1.1 × Sh_inst/nombre_niveaux + 4 × nombre_niveaux)
93
+ const Lb = 1.2 * (1.1 * (Sh_inst / Niv_inst) + 4 * Niv_inst);
94
+
95
+ // Étape 4 : DeltaPb = 0.2 × Lb + 10
96
+ const DeltaPb = 0.2 * Lb + 10;
97
+
98
+ let Qcirc_annuel = 0;
99
+
100
+ for (const mois of mois_liste) {
101
+ const Njj_mois = Njj[mois];
102
+ const becs_immeuble_j_kwh = becsParMois[mois] || 0;
103
+
104
+ // Étape 1 : Qdwj_i [kWh]
105
+ const Qdwj_i = 0.24 * becs_immeuble_j_kwh * ratio_surface;
106
+
107
+ // Étape 2 : qdwj_i [m³/h]
108
+ // Note: la spec officielle utilise Qdwj en Wh et divise par 1000 ensuite.
109
+ // Le moteur fournit becs en kWh, donc Qdwj est en kWh → on ne divise pas par 1000.
110
+ const qdwj_i = Qdwj_i / (5.815 * 5 * Njj_mois);
111
+
112
+ // Étape 5 : Phyd,j [W]
113
+ // Phyd,j = DeltaPb [kPa] × 1000 × qdwj_i [m³/h] / 3600
114
+ const Phyd_j = (DeltaPb * 1000 * qdwj_i) / 3600;
115
+
116
+ // Étape 6 : Effcirb,j (rendement pompe)
117
+ const Effcirb_j = Phyd_j > 0 ? 0.035 * Math.pow(Phyd_j, 0.45) : 0;
118
+
119
+ // Étape 7 : Pcirb,j [W] = max(Phyd,j / Effcirb,j, 20)
120
+ const Pcirb_j = Effcirb_j > 0 ? Math.max(Phyd_j / Effcirb_j, 20) : 20;
121
+
122
+ // Étape 8 : Qcirb,j [Wh]
123
+ const Qcirb_j = (5 * Pcirb_j + 19 * 20) * Njj_mois;
124
+
125
+ Qcirc_annuel += Qcirb_j;
126
+ }
127
+
128
+ // Étape 9 : conso annuelle en Wh
129
+ return Qcirc_annuel;
130
+ }
131
+
132
+ return 0;
133
+ }
134
+
52
135
  export default function calc_ecs(
53
136
  dpe,
54
137
  ecs,
@@ -61,7 +144,8 @@ export default function calc_ecs(
61
144
  virtualisationECS,
62
145
  surfaceImmeuble,
63
146
  nombreAppartements,
64
- isImmeubleSystemEcsIndividuels
147
+ isImmeubleSystemEcsIndividuels,
148
+ becsParMois
65
149
  ) {
66
150
  const de = ecs.donnee_entree;
67
151
  const di = {};
@@ -101,6 +185,19 @@ export default function calc_ecs(
101
185
  0
102
186
  );
103
187
 
188
+ // Calcul de la consommation des auxiliaires de distribution ECS pour l'immeuble
189
+ // Résultat en Wh, converti en kWh pour cohérence avec le reste du moteur
190
+ if (th === 'immeuble' && becsParMois && Object.keys(becsParMois).length > 0) {
191
+ const conso_aux_wh = calc_auxiliaire_distribution_ecs_immeuble(
192
+ de,
193
+ surfaceImmeuble,
194
+ becsParMois
195
+ );
196
+ di.conso_auxiliaire_distribution_ecs = conso_aux_wh / 1000;
197
+ } else {
198
+ di.conso_auxiliaire_distribution_ecs = 0;
199
+ }
200
+
104
201
  ecs.donnee_intermediaire = di;
105
202
  ecs.donnee_utilisateur = du;
106
203
  }
package/11_ecs.spec.js CHANGED
@@ -21,7 +21,10 @@ vi.mock('./enums.js', () => ({
21
21
  vi.mock('./utils.js', () => ({
22
22
  set_bug_for_bug_compat: vi.fn(),
23
23
  tv: vi.fn(),
24
- requestInput: vi.fn()
24
+ requestInput: vi.fn(),
25
+ // Calendrier réduit à deux mois pour le calcul mensuel des auxiliaires de distribution
26
+ mois_liste: ['Janvier', 'Février'],
27
+ Njj: { Janvier: 31, Février: 28 }
25
28
  }));
26
29
 
27
30
  vi.mock('./14_generateur_ecs.js', () => ({
@@ -281,3 +284,138 @@ describe('calc_ecs - agrégation des consommations', () => {
281
284
  expect(ecs.donnee_intermediaire.conso_ecs_depensier).toBe(28);
282
285
  });
283
286
  });
287
+
288
+ /**
289
+ * Consommation des auxiliaires de distribution ECS - cas immeuble
290
+ * Seuls les réseaux collectifs bouclés ou avec traçage consomment de l'énergie.
291
+ */
292
+ describe('calc_ecs - auxiliaires de distribution ECS (immeuble)', () => {
293
+ /** Calcule l'ECS d'un immeuble et retourne la conso des auxiliaires de distribution [kWh]. */
294
+ function consoAuxDistribution(deExtra, becsParMois, surfaceImmeuble = 400, th = 'immeuble') {
295
+ const ecs = makeEcs({ tv_rendement_distribution_ecs_id: '5', ...deExtra });
296
+ calc_ecs(
297
+ {},
298
+ ecs,
299
+ 100,
300
+ 200,
301
+ 1,
302
+ 'ca1',
303
+ 'h1a',
304
+ th,
305
+ false,
306
+ surfaceImmeuble,
307
+ null,
308
+ false,
309
+ becsParMois
310
+ );
311
+ return ecs.donnee_intermediaire.conso_auxiliaire_distribution_ecs;
312
+ }
313
+
314
+ test.each([
315
+ ['logement autre qu’un immeuble', 'maison', { Janvier: 1000 }],
316
+ ['besoin mensuel absent', 'immeuble', undefined],
317
+ ['besoin mensuel vide', 'immeuble', {}]
318
+ ])('%s : pas de consommation', (_libelle, th, becsParMois) => {
319
+ expect(
320
+ consoAuxDistribution(
321
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '2' },
322
+ becsParMois,
323
+ 400,
324
+ th
325
+ )
326
+ ).toBe(0);
327
+ });
328
+
329
+ test.each([
330
+ ['type d’installation inconnu', { enum_type_installation_id: '9' }],
331
+ ['installation individuelle', { enum_type_installation_id: '1' }],
332
+ [
333
+ 'réseau collectif non bouclé',
334
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '1' }
335
+ ],
336
+ ['type de bouclage non renseigné', { enum_type_installation_id: '2' }]
337
+ ])('%s : pas de consommation', (_libelle, de) => {
338
+ expect(consoAuxDistribution(de, { Janvier: 1000 })).toBe(0);
339
+ });
340
+
341
+ test('réseau avec traçage : 0,14 × besoin annuel × ratio de surface', () => {
342
+ const conso = consoAuxDistribution(
343
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: 3, surface_habitable: 100 },
344
+ { Janvier: 100, Février: 50 }
345
+ );
346
+
347
+ // 0,14 × 150 kWh × 1000 × (100 / 400) = 5250 Wh => 5,25 kWh
348
+ expect(conso).toBeCloseTo(5.25, 9);
349
+ });
350
+
351
+ test('réseau avec traçage sans surfaces renseignées : ratio de surface nul', () => {
352
+ const conso = consoAuxDistribution(
353
+ { enum_type_installation_id: '3', enum_bouclage_reseau_ecs_id: '3' },
354
+ { Janvier: 100 },
355
+ null
356
+ );
357
+
358
+ // surface_habitable absente => 0 ; surface de l'immeuble absente => 1
359
+ expect(conso).toBe(0);
360
+ });
361
+
362
+ test('réseau bouclé sans besoin : puissance minimale du circulateur de 20 W', () => {
363
+ const conso = consoAuxDistribution(
364
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '2', surface_habitable: 100 },
365
+ { Janvier: 0 }
366
+ );
367
+
368
+ // Pcirb = 20 W chaque mois : (5 × 20 + 19 × 20) × (31 + 28) jours = 28320 Wh
369
+ expect(conso).toBeCloseTo(28.32, 9);
370
+ });
371
+
372
+ test('réseau bouclé à faible besoin : puissance du circulateur ramenée au minimum de 20 W', () => {
373
+ const conso = consoAuxDistribution(
374
+ {
375
+ enum_type_installation_id: '2',
376
+ enum_bouclage_reseau_ecs_id: '2',
377
+ surface_habitable: 100,
378
+ nombre_niveau_installation_ecs: 2
379
+ },
380
+ { Janvier: 1000, Février: 1000 }
381
+ );
382
+
383
+ // Phyd / Effcirb < 20 W => Pcirb = 20 W, identique au cas sans besoin
384
+ expect(conso).toBeCloseTo(28.32, 9);
385
+ });
386
+
387
+ test('réseau bouclé : calcul complet de la consommation du circulateur', () => {
388
+ const conso = consoAuxDistribution(
389
+ {
390
+ enum_type_installation_id: '2',
391
+ enum_bouclage_reseau_ecs_id: '2',
392
+ surface_habitable: 100,
393
+ nombre_niveau_installation_ecs: '2'
394
+ },
395
+ { Janvier: 10000 }
396
+ );
397
+
398
+ // Lb = 1,2 × (1,1 × 100 / 2 + 4 × 2) = 75,6 m ; ΔPb = 0,2 × Lb + 10 = 25,12 kPa
399
+ // Janvier : Phyd ≈ 4,645 W => Pcirb ≈ 66,49 W ; Février (besoin absent) : Pcirb = 20 W
400
+ // Référence de régression (calcul des 9 étapes de la méthode) : 35526,3879475995 Wh
401
+ expect(conso).toBeCloseTo(35.526387947599495, 9);
402
+ });
403
+
404
+ test('réseau bouclé sans nombre de niveaux : un seul niveau par défaut', () => {
405
+ const avecUnNiveau = consoAuxDistribution(
406
+ {
407
+ enum_type_installation_id: '2',
408
+ enum_bouclage_reseau_ecs_id: '2',
409
+ surface_habitable: 100,
410
+ nombre_niveau_installation_ecs: 1
411
+ },
412
+ { Janvier: 10000 }
413
+ );
414
+ const sansNiveau = consoAuxDistribution(
415
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '2', surface_habitable: 100 },
416
+ { Janvier: 10000 }
417
+ );
418
+
419
+ expect(sansNiveau).toBe(avecUnNiveau);
420
+ });
421
+ });
package/3.2.1_mur.js CHANGED
@@ -122,7 +122,7 @@ function tv_umur(di, de, du, pc_id, zc, effetJoule) {
122
122
  }
123
123
  }
124
124
 
125
- function calc_umur0(di, de, du) {
125
+ function calc_umur0(di, de, du, versionDpe) {
126
126
  const umur0_avant = du.umur0_avant;
127
127
  const umur_avant = du.umur_avant;
128
128
 
@@ -166,8 +166,17 @@ function calc_umur0(di, de, du) {
166
166
  }
167
167
  }
168
168
 
169
- // Si présence d'un doublage
170
- if (type_doublage > 2) {
169
+ /**
170
+ * Le doublage NE doit PAS être cumulé à une isolation ITE ou ITI.
171
+ * Il doit uniquement être pris en compte en l'absence d'isolation ITE/ITI (ex: ITR seule, non isolé, inconnu).
172
+ * Types avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr)
173
+ * @see https://github.com/Open3CL/engine/issues/146
174
+ */
175
+ const type_isolation = parseInt(de.enum_type_isolation_id) || 1;
176
+ const hasIteOrIti = versionDpe >= 2.4 && [3, 4, 6, 7, 8].includes(type_isolation);
177
+
178
+ // Si présence d'un doublage et pas d'isolation ITE/ITI
179
+ if (type_doublage > 2 && !hasIteOrIti) {
171
180
  let umur0Doublage;
172
181
 
173
182
  // 3 - doublage indéterminé ou lame d'air inf 15 mm
@@ -212,7 +221,7 @@ function calc_umur0(di, de, du) {
212
221
  di.umur0 = Math.min(2.5, di.umur0);
213
222
  }
214
223
 
215
- export default function calc_mur(mur, zc, pc_id, effetJoule) {
224
+ export default function calc_mur(mur, zc, pc_id, effetJoule, versionDpe) {
216
225
  const de = mur.donnee_entree;
217
226
  const du = {};
218
227
  const di = {};
@@ -228,12 +237,12 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
228
237
  const methode_saisie_u = requestInput(de, du, 'methode_saisie_u');
229
238
  switch (methode_saisie_u) {
230
239
  case 'non isolé':
231
- calc_umur0(di, de, du);
240
+ calc_umur0(di, de, du, versionDpe);
232
241
  di.umur = Math.min(di.umur0, 2.5);
233
242
  break;
234
243
  case 'epaisseur isolation saisie justifiée par mesure ou observation':
235
244
  case 'epaisseur isolation saisie justifiée à partir des documents justificatifs autorisés': {
236
- calc_umur0(di, de, du);
245
+ calc_umur0(di, de, du, versionDpe);
237
246
  const epaisseurIsolation = requestInput(de, du, 'epaisseur_isolation', 'int') * 0.01;
238
247
 
239
248
  if (epaisseurIsolation) {
@@ -251,7 +260,7 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
251
260
  }
252
261
  case "resistance isolation saisie justifiée observation de l'isolant installé et mesure de son épaisseur":
253
262
  case 'resistance isolation saisie justifiée à partir des documents justificatifs autorisés': {
254
- calc_umur0(di, de, du);
263
+ calc_umur0(di, de, du, versionDpe);
255
264
  const resistanceIsolation = requestInput(de, du, 'resistance_isolation', 'float');
256
265
 
257
266
  if (resistanceIsolation) {
@@ -268,19 +277,19 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
268
277
  break;
269
278
  }
270
279
  case 'isolation inconnue (table forfaitaire)':
271
- calc_umur0(di, de, du);
280
+ calc_umur0(di, de, du, versionDpe);
272
281
  tv_umur(di, de, du, pc_id, zc, effetJoule);
273
282
  di.umur = Math.min(di.umur, di.umur0);
274
283
  break;
275
284
  case "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)": {
276
- calc_umur0(di, de, du);
285
+ calc_umur0(di, de, du, versionDpe);
277
286
  const pi_id = requestInputID(de, du, 'periode_isolation') || pc_id;
278
287
  tv_umur(di, de, du, pi_id, zc, effetJoule);
279
288
  di.umur = Math.min(di.umur, di.umur0);
280
289
  break;
281
290
  }
282
291
  case 'année de construction saisie (table forfaitaire)': {
283
- calc_umur0(di, de, du);
292
+ calc_umur0(di, de, du, versionDpe);
284
293
  // Si l'année d'isolation est connue, il faut l'utiliser et pas l'année de construction
285
294
  let pi_id = de.enum_periode_isolation_id || pc_id;
286
295
  if (!de.enum_periode_isolation_id) {
package/3.2.1_mur.spec.js CHANGED
@@ -819,3 +819,108 @@ describe('calc_mur - méthode de saisie de U inconnue', () => {
819
819
  warn.mockRestore();
820
820
  });
821
821
  });
822
+
823
+ /**
824
+ * Le doublage n'est plus cumulé à une isolation ITE ou ITI à partir de la version 2.4 du DPE.
825
+ * Les DPE antérieurs (2 à 2.3) ont été établis avec le cumul : on conserve ce comportement.
826
+ * @see https://github.com/Open3CL/engine/issues/146
827
+ */
828
+ describe('calc_mur - doublage et isolation ITE/ITI (#146)', () => {
829
+ /** Umur0 issu de la table forfaitaire mockée. */
830
+ const UMUR0_TABLE = 2;
831
+ /** Umur0 avec doublage type 4 ou 5 cumulé : 1 / (1/2 + 0,21). */
832
+ const UMUR0_AVEC_DOUBLAGE = 1.4084507042253522;
833
+
834
+ /**
835
+ * @param enum_type_isolation_id {string}
836
+ * @param enum_type_doublage_id {string}
837
+ * @return {object}
838
+ */
839
+ const murIsoleAvecDoublage = (enum_type_isolation_id, enum_type_doublage_id = '5') => ({
840
+ donnee_entree: {
841
+ methode_saisie_u: 'epaisseur isolation saisie justifiée par mesure ou observation',
842
+ methode_saisie_u0:
843
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
844
+ enum_materiaux_structure_mur_id: '5',
845
+ epaisseur_structure: 30,
846
+ epaisseur_isolation: 10,
847
+ enum_type_doublage_id,
848
+ enum_type_isolation_id
849
+ }
850
+ });
851
+
852
+ describe('DPE en version 2.4 et supérieure', () => {
853
+ test.each([
854
+ ['iti', '3'],
855
+ ['ite', '4'],
856
+ ['iti+ite', '6'],
857
+ ['iti+itr', '7'],
858
+ ['ite+itr', '8']
859
+ ])("isolation %s : le doublage n'est pas cumulé", (_label, typeIsolation) => {
860
+ const mur = murIsoleAvecDoublage(typeIsolation);
861
+
862
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
863
+
864
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9);
865
+ });
866
+
867
+ test.each([
868
+ ['inconnu', '1'],
869
+ ['non isolé', '2'],
870
+ ['itr', '5'],
871
+ ["isolé mais type d'isolation inconnu", '9']
872
+ ])('isolation %s : le doublage est cumulé', (_label, typeIsolation) => {
873
+ const mur = murIsoleAvecDoublage(typeIsolation);
874
+
875
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
876
+
877
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
878
+ });
879
+
880
+ test('type_isolation absent : le doublage est cumulé', () => {
881
+ const mur = murIsoleAvecDoublage(undefined);
882
+
883
+ calc_mur(mur, 'h1a', '1', '0', 2.5);
884
+
885
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
886
+ });
887
+ });
888
+
889
+ describe('DPE antérieurs à la version 2.4', () => {
890
+ test.each([[2], [2.1], [2.2], [2.3]])(
891
+ 'version %s : le doublage reste cumulé à une isolation ITI',
892
+ (versionDpe) => {
893
+ const mur = murIsoleAvecDoublage('3');
894
+
895
+ calc_mur(mur, 'h1a', '1', '0', versionDpe);
896
+
897
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
898
+ }
899
+ );
900
+
901
+ test('version absente : le doublage reste cumulé à une isolation ITE', () => {
902
+ const mur = murIsoleAvecDoublage('4');
903
+
904
+ calc_mur(mur, 'h1a', '1', '0');
905
+
906
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
907
+ });
908
+ });
909
+
910
+ test.each([
911
+ ['isolation inconnue (table forfaitaire)'],
912
+ [
913
+ "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)"
914
+ ],
915
+ ['année de construction saisie (table forfaitaire)'],
916
+ ['resistance isolation saisie justifiée à partir des documents justificatifs autorisés']
917
+ ])('methode_saisie_u "%s" : la version du DPE est prise en compte', (methode_saisie_u) => {
918
+ const mur = murIsoleAvecDoublage('3');
919
+ mur.donnee_entree.methode_saisie_u = methode_saisie_u;
920
+ mur.donnee_entree.resistance_isolation = 2.5;
921
+
922
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
923
+
924
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9);
925
+ });
926
+ });
package/3_deperdition.js CHANGED
@@ -224,7 +224,12 @@ export default function calc_deperdition(cg, zc, th, effetJoule, dpe, Sh) {
224
224
  const declaredPontsThermiques = structuredClone(pt_list);
225
225
  const vt_list = logement.ventilation_collection.ventilation || [];
226
226
 
227
- mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule));
227
+ /**
228
+ * Version de la méthode appliquée au DPE. Absente des DPE partiels : les calculs qui en
229
+ * dépendent retombent alors sur le comportement historique.
230
+ */
231
+ const versionDpe = parseFloat(dpe.administratif?.enum_version_id);
232
+ mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule, versionDpe));
228
233
  pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule, pb_list));
229
234
  ph_list.forEach((ph) => calc_ph(ph, zc, pc, effetJoule));
230
235
  bv_list.forEach((bv) => calc_bv(bv, zc));
@@ -125,6 +125,7 @@ describe('Upt - déperdition linéique des ponts thermiques', () => {
125
125
 
126
126
  /** Fabrique un DPE minimal avec une enveloppe pré-calculée. */
127
127
  function makeDpe({
128
+ enum_version_id = '2.4',
128
129
  mur = [],
129
130
  pb = [],
130
131
  ph = [],
@@ -141,6 +142,7 @@ function makeDpe({
141
142
  }
142
143
  return {
143
144
  numero_dpe: 'TEST',
145
+ administratif: enum_version_id === null ? undefined : { enum_version_id },
144
146
  logement: {
145
147
  sortie: { deperdition },
146
148
  enveloppe: {
@@ -239,6 +241,51 @@ describe("calc_deperdition - agrégation des déperditions de l'enveloppe", () =
239
241
  expect(calc_ventilation).toHaveBeenCalledTimes(1);
240
242
  });
241
243
 
244
+ /**
245
+ * Certains calculs de Umur dépendent de la version de la méthode appliquée au DPE
246
+ * (cumul du doublage avec une isolation ITE/ITI avant la version 2.4).
247
+ * @see https://github.com/Open3CL/engine/issues/146
248
+ */
249
+ describe('version du DPE transmise au calcul des murs', () => {
250
+ /** @return {object[]} */
251
+ const murCollection = () => [
252
+ {
253
+ donnee_entree: { surface_paroi_opaque: 10, enum_type_adjacence_id: '1' },
254
+ donnee_intermediaire: { umur: 0.5, b: 1 }
255
+ }
256
+ ];
257
+
258
+ test.each([
259
+ ['2.3', 2.3],
260
+ ['2.4', 2.4],
261
+ ['2.5', 2.5]
262
+ ])('enum_version_id "%s" transmis à calc_mur', (enum_version_id, expected) => {
263
+ const dpe = makeDpe({
264
+ enum_version_id,
265
+ mur: murCollection(),
266
+ ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }],
267
+ deperdition_mur: 5
268
+ });
269
+
270
+ calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100);
271
+
272
+ expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', expected);
273
+ });
274
+
275
+ test('bloc administratif absent : calc_mur reçoit NaN et conserve le comportement historique', () => {
276
+ const dpe = makeDpe({
277
+ enum_version_id: null,
278
+ mur: murCollection(),
279
+ ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }],
280
+ deperdition_mur: 5
281
+ });
282
+
283
+ calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100);
284
+
285
+ expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', NaN);
286
+ });
287
+ });
288
+
242
289
  /**
243
290
  * calc_Sdep (surface déperditive transmise à la ventilation) exclut les parois à coefficient
244
291
  * b nul et les parois adjacentes à un local non déperditif. Les parois à déperdition nulle
package/README.md CHANGED
@@ -23,8 +23,6 @@ Implémentation open source du moteur Open3CL de l'ADEME.
23
23
  &middot;
24
24
  <a href="https://github.com/Open3CL/engine/issues/new?labels=enhancement&template=feature-request---.md">Créer une feature</a>
25
25
  &middot;
26
- <a href="https://open3cl.github.io/engine/build/">Démo en ligne</a>
27
- &middot;
28
26
  <a href="https://open3cl.github.io/engine/reports/corpus">Rapports de corpus</a>
29
27
  </p>
30
28
  </div>
@@ -57,7 +55,6 @@ Implémentation open source du moteur Open3CL de l'ADEME.
57
55
  <li><a href="#api-publique">API publique</a></li>
58
56
  <li><a href="#options-de-calcul">Options de calcul</a></li>
59
57
  <li><a href="#lire-le-résultat">Lire le résultat</a></li>
60
- <li><a href="#tester-un-dpe-sans-écrire-de-code">Tester un DPE sans écrire de code</a></li>
61
58
  </ul>
62
59
  </li>
63
60
  <li><a href="#-tests-de-corpus">Tests de corpus</a>
@@ -142,12 +139,11 @@ flowchart LR
142
139
 
143
140
  ### L'écosystème Open3CL
144
141
 
145
- | Ressource | Description |
146
- | :-------------------------------------------------------------------- | :------------------------------------------------------------- |
147
- | [open3cl.fr](https://open3cl.fr) | Le site du projet |
148
- | [Démonstrateur](https://open3cl.github.io/engine/build/) | Chargez un DPE XML et comparez les sorties du moteur, en ligne |
149
- | [Rapports de corpus](https://open3cl.github.io/engine/reports/corpus) | Le tableau de bord interactif des résultats sur DPE réels |
150
- | [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine) | Le paquet npm |
142
+ | Ressource | Description |
143
+ | :-------------------------------------------------------------------- | :-------------------------------------------------------- |
144
+ | [open3cl.fr](https://open3cl.fr) | Le site du projet |
145
+ | [Rapports de corpus](https://open3cl.github.io/engine/reports/corpus) | Le tableau de bord interactif des résultats sur DPE réels |
146
+ | [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine) | Le paquet npm |
151
147
 
152
148
  <p align="right">(<a href="#readme-top">Retour sommaire</a>)</p>
153
149
 
@@ -292,17 +288,6 @@ Le DPE renvoyé est l'objet d'entrée, enrichi de `logement.sortie` :
292
288
  | `sortie.confort_ete` / `qualite_isolation` | Indicateurs de confort d'été et de qualité d'isolation |
293
289
  | `sortie.production_electricite` | Production photovoltaïque |
294
290
 
295
- ### Tester un DPE sans écrire de code
296
-
297
- Le [démonstrateur en ligne](https://open3cl.github.io/engine/build/) accepte un DPE au format XML, l'envoie au moteur et
298
- affiche **côte à côte** les valeurs du DPE d'origine, celles calculées par Open3CL, et le différentiel.
299
-
300
- C'est le moyen le plus rapide de qualifier un écart : problème dans le DPE d'origine, ou bug dans la librairie ?
301
-
302
- <p align="right">(<a href="#readme-top">Retour sommaire</a>)</p>
303
-
304
- ---
305
-
306
291
  ## 🧪 Tests de corpus
307
292
 
308
293
  > Un corpus, c'est une liste de numéros de DPE réels. Le moteur les rejoue **tous**, compare ses sorties à celles du
package/conso.js CHANGED
@@ -249,6 +249,7 @@ export default function calc_conso(
249
249
  null,
250
250
  prorataECS,
251
251
  prorataChauffage,
252
+ ecs,
252
253
  conso_aux_distribution_ecs_total
253
254
  ),
254
255
  ep_conso: calc_conso_pond(
@@ -262,6 +263,7 @@ export default function calc_conso(
262
263
  coeffEp,
263
264
  prorataECS,
264
265
  prorataChauffage,
266
+ ecs,
265
267
  conso_aux_distribution_ecs_total
266
268
  ),
267
269
  emission_ges: calc_conso_pond(
@@ -275,6 +277,7 @@ export default function calc_conso(
275
277
  coef_ges,
276
278
  prorataECS,
277
279
  prorataChauffage,
280
+ ecs,
278
281
  conso_aux_distribution_ecs_total
279
282
  ),
280
283
  cout: calc_conso_pond(
@@ -288,6 +291,7 @@ export default function calc_conso(
288
291
  coef_cout,
289
292
  prorataECS,
290
293
  prorataChauffage,
294
+ ecs,
291
295
  conso_aux_distribution_ecs_total
292
296
  )
293
297
  };
@@ -343,7 +347,8 @@ export default function calc_conso(
343
347
  '',
344
348
  null,
345
349
  prorataECS,
346
- prorataChauffage
350
+ prorataChauffage,
351
+ type_energie === 'électricité' ? ecs : []
347
352
  );
348
353
  conso_en = {
349
354
  conso_ch: conso_en._ch,
@@ -474,7 +479,7 @@ function calc_conso_pond(
474
479
  coef,
475
480
  prorataECS,
476
481
  prorataChauffage,
477
- conso_aux_distribution_ecs_total
482
+ ecs_installations
478
483
  ) {
479
484
  const ret = {};
480
485
  ret.auxiliaire_ventilation = vt_list.reduce((acc, vt) => {
@@ -538,11 +543,10 @@ function calc_conso_pond(
538
543
  return acc + getConso(coef, 'électricité auxiliaire', conso);
539
544
  }, 0);
540
545
 
541
- ret.auxiliaire_distribution_ecs = getConso(
542
- coef,
543
- 'électricité auxiliaire',
544
- conso_aux_distribution_ecs_total || 0
545
- );
546
+ ret.auxiliaire_distribution_ecs = ecs_installations.reduce((acc, inst) => {
547
+ const conso = inst.donnee_intermediaire.conso_auxiliaire_distribution_ecs || 0;
548
+ return acc + getConso(coef, 'électricité auxiliaire', conso);
549
+ }, 0);
546
550
 
547
551
  ret.ecs = getEcsConso(gen_ecs, 'conso_ecs', coef, prorataECS, prefix);
548
552
 
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.7.2';
36
+ const LIB_VERSION = '1.8.0';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
@@ -437,7 +437,8 @@ export function calcul_3cl(inputDpe, options) {
437
437
  virtualisationECS,
438
438
  dpe.logement.caracteristique_generale.surface_habitable_immeuble,
439
439
  dpe.logement.caracteristique_generale.nombre_appartement,
440
- isImmeubleSystemEcsIndividuels
440
+ isImmeubleSystemEcsIndividuels,
441
+ apport_et_besoin.besoin_ecs_par_mois || {}
441
442
  );
442
443
 
443
444
  if (!dpeGenereImmeuble) {
@@ -129,20 +129,29 @@ export class DeperditionMurService extends DeperditionService {
129
129
  }
130
130
 
131
131
  /**
132
- * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire
132
+ * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire.
133
+ * Cependant, lorsqu'une isolation ITE ou ITI est présente, le doublage NE doit PAS être cumulé.
134
+ * Le doublage n'est pris en compte que si aucune isolation ITE/ITI n'est appliquée.
135
+ * Types d'isolation avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr)
136
+ * @see https://github.com/Open3CL/engine/issues/146
133
137
  */
134
- switch (murDE.enum_type_doublage_id) {
135
- case '3':
136
- // doublage indéterminé ou lame d'air inf 15 mm
137
- umur0 = 1 / (1 / umur0 + 0.1);
138
- break;
139
- case '4': // doublage indéterminé ou lame d'air sup 15 mm
140
- case '5': // doublage connu (plâtre brique bois)
141
- umur0 = 1 / (1 / umur0 + 0.21);
142
- break;
143
- default:
144
- // absence de doublage ou inconnu
145
- break;
138
+ const typeIsolation = parseInt(murDE.enum_type_isolation_id) || 1;
139
+ const hasIteOrIti = [3, 4, 6, 7, 8].includes(typeIsolation);
140
+
141
+ if (!hasIteOrIti) {
142
+ switch (murDE.enum_type_doublage_id) {
143
+ case '3':
144
+ // doublage indéterminé ou lame d'air inf 15 mm
145
+ umur0 = 1 / (1 / umur0 + 0.1);
146
+ break;
147
+ case '4': // doublage indéterminé ou lame d'air sup 15 mm
148
+ case '5': // doublage connu (plâtre brique bois)
149
+ umur0 = 1 / (1 / umur0 + 0.21);
150
+ break;
151
+ default:
152
+ // absence de doublage ou inconnu
153
+ break;
154
+ }
146
155
  }
147
156
 
148
157
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -28,8 +28,9 @@
28
28
  "prepare": "husky install",
29
29
  "assets:sync": "node scripts/synchronize_assets.js",
30
30
  "version:update": "node scripts/update_version.js",
31
+ "reports:charts": "node scripts/generate_corpus_history_chart.js",
31
32
  "reports:preview": "npm run http-server dist/reports/corpus -o",
32
- "reports:readme": "node scripts/generate_corpus_readme.js"
33
+ "reports:readme": "node scripts/generate_corpus_readme.js && npm run reports:charts"
33
34
  },
34
35
  "repository": {
35
36
  "type": "git",