@open3cl/engine 1.6.2 → 1.7.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/15_conso_aux.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import enums from './enums.js';
2
2
  import tvs from './tv.js';
3
- import { mois_liste, Tbase } from './utils.js';
3
+ import { mois_liste, Njj, Tbase } from './utils.js';
4
4
 
5
5
  const G_CHAUDIERE = 20;
6
6
  const G_RADIATEURS_GAZ = 40;
@@ -259,3 +259,123 @@ function getPuissanceCirculateur(em_ch, de, di, du, surfaceHabitable, GV, Tbase)
259
259
  Math.max(1, surfaceHabitable / 400)
260
260
  );
261
261
  }
262
+
263
+ /**
264
+ * 15.2.3 Consommation des auxiliaires de distribution d'ECS
265
+ * Calcul installation par installation pour le cas appartement
266
+ *
267
+ * SI installation individuelle : conso = 0
268
+ * SI installation collective :
269
+ * CAS 1 - enum_bouclage_reseau_ecs_id = 1 (non bouclé) : conso = 0
270
+ * CAS 2 - enum_bouclage_reseau_ecs_id = 2 (bouclé) : calcul selon étapes 1-9
271
+ * CAS 3 - enum_bouclage_reseau_ecs_id = 3 (traçage) : conso = 0.14 * BECS_annuel * Sh_install / Sh_logement
272
+ *
273
+ * @param ecs {object} installation ECS
274
+ * @param de {Donnee_entree} donnée d'entrée de l'installation ECS
275
+ * @param di {Donnee_intermediaire} donnée intermédiaire de l'installation ECS
276
+ * @param Sh_logement {number} surface habitable du logement
277
+ * @param Sh_immeuble {number} surface habitable de l'immeuble
278
+ * @param caId {number} id de la classe d'altitude
279
+ * @param zcId {number} id de la zone climatique
280
+ * @param nadeq {number} nombre d'unités d'équivalence
281
+ * @param nombre_niveau_immeuble {number} nombre de niveaux de l'immeuble
282
+ */
283
+ export function conso_aux_distribution_ecs(
284
+ ecs,
285
+ de,
286
+ di,
287
+ Sh_logement,
288
+ Sh_immeuble,
289
+ caId,
290
+ zcId,
291
+ nadeq,
292
+ nombre_niveau_immeuble
293
+ ) {
294
+ const typeInstallation = parseInt(de.enum_type_installation_id);
295
+
296
+ if (typeInstallation === 1) {
297
+ di.conso_auxiliaire_distribution_ecs = 0;
298
+ return;
299
+ }
300
+
301
+ const enumBouclage = parseInt(de.enum_bouclage_reseau_ecs_id);
302
+
303
+ // CAS 1 - enum_bouclage_reseau_ecs_id =1 (réseau d'ECS non bouclé)
304
+ if (enumBouclage === 1 || isNaN(enumBouclage)) {
305
+ di.conso_auxiliaire_distribution_ecs = 0;
306
+ return;
307
+ }
308
+
309
+ // CAS 3 - enum_bouclage_reseau_ecs_id = 3 (traçage)
310
+ if (enumBouclage === 3) {
311
+ const BECS_annuel = di.besoin_ecs;
312
+ const Sh_install = de.surface_habitable || Sh_logement;
313
+ di.conso_auxiliaire_distribution_ecs = (0.14 * BECS_annuel * Sh_install) / Sh_logement;
314
+ return;
315
+ }
316
+
317
+ // CAS 2 - enum_bouclage_reseau_ecs_id = 2 (réseau bouclé)
318
+ const ca = enums.classe_altitude[caId];
319
+ const zc = enums.zone_climatique[zcId];
320
+
321
+ const ratioVirtu = parseFloat(de.ratio_virtualisation);
322
+ let Sh_immeuble_ratio = Sh_immeuble;
323
+ let Sh_install = de.surface_habitable || Sh_logement;
324
+ let Niv_inst_ecs = de.nombre_niveau_installation_ecs || 1;
325
+
326
+ if (!(Sh_immeuble > Sh_logement) && ratioVirtu > 0 && ratioVirtu < 1) {
327
+ Sh_immeuble_ratio = Sh_logement / ratioVirtu;
328
+ Sh_install = Sh_logement;
329
+ Niv_inst_ecs = nombre_niveau_immeuble || Niv_inst_ecs;
330
+ }
331
+
332
+ // Etape 3: Lb - longueur par défaut du bouclage ECS (m)
333
+ // Sh est la surface habitable des logements desservis par l'installation d'ECS
334
+ // Pour l'appartement, on utilise la surface à l'échelle de l'immeuble
335
+ const Sh = (Sh_install / Sh_logement) * Sh_immeuble_ratio;
336
+ const Lb = 4 * Math.pow(Sh / Niv_inst_ecs, 0.5) + 6 * (Niv_inst_ecs - 0.5);
337
+
338
+ // Etape 4: DeltaPb (kPa) - perte de charge dans le bouclage
339
+ const DeltaPb = 0.2 * Lb + 10;
340
+
341
+ let conso = 0;
342
+
343
+ for (const mois of mois_liste) {
344
+ // Besoin ECS mensuel pour le logement (kWh)
345
+ // spec : https://rt-re-batiment.developpement-durable.gouv.fr/IMG/pdf/consolide_annexe_1_arrete_du_31_03_2021_relatif_aux_methodes_et_procedures_applicables.pdf
346
+ // page 71-72
347
+ const tefsj = tvs.tefs[ca][mois][zc];
348
+ const njj = Njj[mois];
349
+ const BECS_j = (1.163 * nadeq * 56 * (40 - tefsj) * njj) / 1000;
350
+
351
+ // Etape 1: Qdwj_i (Wh) - pertes de distribution à l'échelle de l'immeuble
352
+ // Qd,w,j = (0.5 * Lvc / Sh + 0.112 + 0.028) * BECS_j
353
+ // Pour Ratecs=1 (bouclé): Lvc = 0.2 * Sh * Ratecs = 0.2 * Sh
354
+ // So Qd,w,j = (0.1 + 0.112 + 0.028) * BECS_j = 0.24 * BECS_j (kWh)
355
+ // Pour l'appartement: Qd,w,j est multiplié par Sh_immeuble_ratio / Sh_logement
356
+ const Qdwj_i =
357
+ (0.24 * BECS_j * 1000 * Sh_install * Sh_immeuble_ratio) / (Sh_logement * Sh_logement);
358
+
359
+ // Etape 2: qdwj_i (m³/h) - débit de distribution ECS
360
+ const Nh_puisage_j = njj * 5;
361
+ const qdwj_i = Qdwj_i / (5.815 * Nh_puisage_j) / 1000;
362
+
363
+ // Etape 5: Phyd,j (W) - puissance hydraulique du bouclage
364
+ const Phyd_j = (qdwj_i * DeltaPb) / 3.6;
365
+
366
+ // Etape 6: Effcirb,j - efficacité du circulateur
367
+ const Effcirb_j = Math.pow(Phyd_j, 0.324) / 15.3;
368
+
369
+ // Etape 7: Pcirb,j (W) - puissance électrique du circulateur
370
+ const Pcirb_j = Math.max(20, Phyd_j / Effcirb_j);
371
+
372
+ // Etape 8: Qcirb,j (Wh) - consommation mensuelle du circulateur
373
+ const Nh_mois_j = njj * 24;
374
+ const Qcirb_j = Nh_puisage_j * Pcirb_j + (Nh_mois_j - Nh_puisage_j) * 20;
375
+
376
+ conso += Qcirb_j;
377
+ }
378
+
379
+ // Etape 9: conso annuelle ramenée à l'appartement (kWh)
380
+ di.conso_auxiliaire_distribution_ecs = (conso * Sh_logement) / Sh_immeuble_ratio / 1000;
381
+ }
@@ -1,10 +1,13 @@
1
1
  import { beforeEach, describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  /**
4
- * Dépendances mockées uniquement pour `conso_aux_distribution_ch` :
4
+ * Dépendances mockées pour `conso_aux_distribution_ch` et `conso_aux_distribution_ecs` :
5
5
  * - `enums` : libellés de classe d'altitude / zone climatique ;
6
6
  * - `tvs.nref19` : nombre d'heures de fonctionnement mensuel ;
7
- * - `mois_liste` / `Tbase` : réduits à des valeurs contrôlées.
7
+ * - `tvs.tefs` : température d'eau froide sanitaire mensuelle ;
8
+ * - `mois_liste` / `Njj` / `Tbase` : réduits à des valeurs contrôlées.
9
+ * L'année est réduite à un seul mois (Janvier, 31 jours) pour rendre les
10
+ * consommations attendues calculables à la main.
8
11
  * `conso_aux_gen` n'utilise aucune de ces dépendances (calcul autonome).
9
12
  */
10
13
  vi.mock('./enums.js', () => ({
@@ -16,16 +19,20 @@ vi.mock('./enums.js', () => ({
16
19
 
17
20
  vi.mock('./tv.js', () => ({
18
21
  default: {
19
- nref19: { 0: { ca1: { Janvier: { h1a: 100 } } } }
22
+ nref19: { 0: { ca1: { Janvier: { h1a: 100 } } } },
23
+ tefs: { ca1: { Janvier: { h1a: 10 } } }
20
24
  }
21
25
  }));
22
26
 
23
27
  vi.mock('./utils.js', () => ({
24
28
  mois_liste: ['Janvier'],
29
+ Njj: { Janvier: 31 },
25
30
  Tbase: { ca1: { h1: -9 } }
26
31
  }));
27
32
 
28
- const { conso_aux_gen, conso_aux_distribution_ch } = await import('./15_conso_aux.js');
33
+ const { conso_aux_gen, conso_aux_distribution_ch, conso_aux_distribution_ecs } = await import(
34
+ './15_conso_aux.js'
35
+ );
29
36
 
30
37
  /**
31
38
  * 15.1 Consommation des auxiliaires de génération
@@ -223,3 +230,321 @@ describe('conso_aux_distribution_ch - auxiliaires de distribution', () => {
223
230
  expect(di.conso_auxiliaire_distribution_ch).toBeCloseTo(136.51553416032854, 9);
224
231
  });
225
232
  });
233
+
234
+ /**
235
+ * 15.2.3 Consommation des auxiliaires de distribution d'ECS
236
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §15.2.3
237
+ *
238
+ * Les scénarios ci-dessous s'appuient sur l'année réduite à Janvier (31 jours,
239
+ * tefs = 10 °C) définie par les mocks en tête de fichier :
240
+ * - Nh_puisage = 31 × 5 = 155 h
241
+ * - Nh_mois = 31 × 24 = 744 h
242
+ */
243
+ describe("conso_aux_distribution_ecs - auxiliaires de distribution d'ECS", () => {
244
+ // Consommation du circulateur maintenu à 20 W sur toutes les heures du mois (kWh)
245
+ const CONSO_PLANCHER_20W = (31 * 24 * 20) / 1000; // 14,88 kWh
246
+
247
+ test('installation individuelle : consommation nulle', () => {
248
+ const di = {};
249
+ conso_aux_distribution_ecs(
250
+ {},
251
+ { enum_type_installation_id: '1', enum_bouclage_reseau_ecs_id: '2' },
252
+ di,
253
+ 50,
254
+ 2000,
255
+ 1,
256
+ 1,
257
+ 30
258
+ );
259
+ expect(di.conso_auxiliaire_distribution_ecs).toBe(0);
260
+ });
261
+
262
+ test("réseau d'ECS non bouclé (id 1) : consommation nulle", () => {
263
+ const di = {};
264
+ conso_aux_distribution_ecs(
265
+ {},
266
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '1' },
267
+ di,
268
+ 50,
269
+ 2000,
270
+ 1,
271
+ 1,
272
+ 30
273
+ );
274
+ expect(di.conso_auxiliaire_distribution_ecs).toBe(0);
275
+ });
276
+
277
+ test('type de bouclage non renseigné : consommation nulle', () => {
278
+ const di = {};
279
+ conso_aux_distribution_ecs({}, { enum_type_installation_id: '2' }, di, 50, 2000, 1, 1, 30);
280
+ expect(di.conso_auxiliaire_distribution_ecs).toBe(0);
281
+ });
282
+
283
+ test('réseau avec traçage (id 3) : 0,14 × BECS × Sh_installation / Sh_logement', () => {
284
+ const di = { besoin_ecs: 1000 };
285
+ conso_aux_distribution_ecs(
286
+ {},
287
+ {
288
+ enum_type_installation_id: '2',
289
+ enum_bouclage_reseau_ecs_id: '3',
290
+ surface_habitable: 60
291
+ },
292
+ di,
293
+ 50,
294
+ 2000,
295
+ 1,
296
+ 1,
297
+ 30
298
+ );
299
+ // 0,14 × 1000 × 60 / 50
300
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo(168, 10);
301
+ });
302
+
303
+ test("traçage sans surface d'installation : repli sur la surface du logement", () => {
304
+ const di = { besoin_ecs: 1000 };
305
+ conso_aux_distribution_ecs(
306
+ {},
307
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '3' },
308
+ di,
309
+ 50,
310
+ 2000,
311
+ 1,
312
+ 1,
313
+ 30
314
+ );
315
+ // Sh_install = Sh_logement => 0,14 × 1000
316
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo(140, 10);
317
+ });
318
+
319
+ test('réseau bouclé, faible puissance hydraulique : plancher de 20 W', () => {
320
+ const di = {};
321
+ // Immeuble réduit au logement (18 m²) et nadeq faible : Phyd ≈ 0,07 W, donc
322
+ // Pcirb calculée (≈ 2,6 W) passe sous le plancher de 20 W tous les mois.
323
+ conso_aux_distribution_ecs(
324
+ {},
325
+ {
326
+ enum_type_installation_id: '2',
327
+ enum_bouclage_reseau_ecs_id: '2',
328
+ surface_habitable: 18,
329
+ nombre_niveau_installation_ecs: 1
330
+ },
331
+ di,
332
+ 18,
333
+ 18,
334
+ 1,
335
+ 1,
336
+ 1.15
337
+ );
338
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo(CONSO_PLANCHER_20W, 10);
339
+ });
340
+
341
+ test('réseau bouclé : consommation ramenée au prorata Sh_logement / Sh_immeuble', () => {
342
+ const di = {};
343
+ // Même régime de plancher (Pcirb ≈ 11 W) : la consommation immeuble (14,88 kWh)
344
+ // est ramenée à l'appartement, soit un dixième ici.
345
+ conso_aux_distribution_ecs(
346
+ {},
347
+ {
348
+ enum_type_installation_id: '2',
349
+ enum_bouclage_reseau_ecs_id: '2',
350
+ surface_habitable: 50,
351
+ nombre_niveau_installation_ecs: 1
352
+ },
353
+ di,
354
+ 50,
355
+ 500,
356
+ 1,
357
+ 1,
358
+ 0.5
359
+ );
360
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo((CONSO_PLANCHER_20W * 50) / 500, 10);
361
+ });
362
+
363
+ test('réseau bouclé, circulateur au-dessus du plancher de 20 W', () => {
364
+ const di = {};
365
+ conso_aux_distribution_ecs(
366
+ {},
367
+ {
368
+ enum_type_installation_id: '2',
369
+ enum_bouclage_reseau_ecs_id: '2',
370
+ surface_habitable: 50,
371
+ nombre_niveau_installation_ecs: 2
372
+ },
373
+ di,
374
+ 50,
375
+ 2000,
376
+ 1,
377
+ 1,
378
+ 30
379
+ );
380
+ // valeur de référence de régression (Pcirb ≈ 549 W pendant les heures de puisage)
381
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo(2.4208575056559525, 9);
382
+ });
383
+
384
+ test('plus de niveaux desservis : bouclage plus court, donc consommation plus faible', () => {
385
+ const scenario = (Niv) => {
386
+ const di = {};
387
+ conso_aux_distribution_ecs(
388
+ {},
389
+ {
390
+ enum_type_installation_id: '2',
391
+ enum_bouclage_reseau_ecs_id: '2',
392
+ surface_habitable: 50,
393
+ nombre_niveau_installation_ecs: Niv
394
+ },
395
+ di,
396
+ 50,
397
+ 2000,
398
+ 1,
399
+ 1,
400
+ 30
401
+ );
402
+ return di.conso_auxiliaire_distribution_ecs;
403
+ };
404
+ // Lb = 4 × (Sh / Niv)^0,5 + 6 × (Niv − 0,5) : sur cet immeuble, Lb décroît avec Niv
405
+ expect(scenario(1)).toBeCloseTo(2.767218880573735, 9);
406
+ expect(scenario(4)).toBeCloseTo(2.2222299894932647, 9);
407
+ expect(scenario(4)).toBeLessThan(scenario(1));
408
+ });
409
+
410
+ test("réseau bouclé sans surface ni nombre de niveaux d'installation : valeurs par défaut", () => {
411
+ const diDefaut = {};
412
+ conso_aux_distribution_ecs(
413
+ {},
414
+ { enum_type_installation_id: '2', enum_bouclage_reseau_ecs_id: '2' },
415
+ diDefaut,
416
+ 50,
417
+ 2000,
418
+ 1,
419
+ 1,
420
+ 30
421
+ );
422
+ const diExplicite = {};
423
+ // Sh_install = Sh_logement et Niv = 1
424
+ conso_aux_distribution_ecs(
425
+ {},
426
+ {
427
+ enum_type_installation_id: '2',
428
+ enum_bouclage_reseau_ecs_id: '2',
429
+ surface_habitable: 50,
430
+ nombre_niveau_installation_ecs: 1
431
+ },
432
+ diExplicite,
433
+ 50,
434
+ 2000,
435
+ 1,
436
+ 1,
437
+ 30
438
+ );
439
+ expect(diDefaut.conso_auxiliaire_distribution_ecs).toBeCloseTo(
440
+ diExplicite.conso_auxiliaire_distribution_ecs,
441
+ 10
442
+ );
443
+ });
444
+
445
+ test("installation desservant tout l'immeuble : Sh_installation étendue à l'immeuble", () => {
446
+ const di = {};
447
+ conso_aux_distribution_ecs(
448
+ {},
449
+ {
450
+ enum_type_installation_id: '2',
451
+ enum_bouclage_reseau_ecs_id: '2',
452
+ surface_habitable: 2000,
453
+ nombre_niveau_installation_ecs: 2
454
+ },
455
+ di,
456
+ 50,
457
+ 2000,
458
+ 1,
459
+ 1,
460
+ 30
461
+ );
462
+ // valeur de référence de régression
463
+ expect(di.conso_auxiliaire_distribution_ecs).toBeCloseTo(72.84189246242042, 9);
464
+ });
465
+ });
466
+
467
+ /**
468
+ * 15.2.3 - Cas des DPE d'appartement dont `surface_habitable_immeuble` est
469
+ * renseignée avec la surface du logement : l'échelle de l'immeuble est alors
470
+ * reconstruite à partir du ratio de virtualisation de l'installation.
471
+ */
472
+ describe("conso_aux_distribution_ecs - surface d'immeuble incohérente", () => {
473
+ const RATIO_VIRTUALISATION = 0.0211;
474
+ const SH_LOGEMENT = 18;
475
+ // 18 / 0,0211 = 853,08 m²
476
+ const SH_IMMEUBLE_RECONSTRUITE = SH_LOGEMENT / RATIO_VIRTUALISATION;
477
+
478
+ /** @param de {object} @param Sh_immeuble {number} @param nbNiveauImmeuble {number} */
479
+ const consoEcs = (de, Sh_immeuble, nbNiveauImmeuble) => {
480
+ const di = {};
481
+ conso_aux_distribution_ecs(
482
+ {},
483
+ {
484
+ enum_type_installation_id: '2',
485
+ enum_bouclage_reseau_ecs_id: '2',
486
+ surface_habitable: SH_LOGEMENT,
487
+ nombre_niveau_installation_ecs: 1,
488
+ ...de
489
+ },
490
+ di,
491
+ SH_LOGEMENT,
492
+ Sh_immeuble,
493
+ 1,
494
+ 1,
495
+ 1.15,
496
+ nbNiveauImmeuble
497
+ );
498
+ return di.conso_auxiliaire_distribution_ecs;
499
+ };
500
+
501
+ test("surface d'immeuble égale à celle du logement : reconstruite via le ratio de virtualisation", () => {
502
+ const reconstruite = consoEcs(
503
+ { ratio_virtualisation: String(RATIO_VIRTUALISATION) },
504
+ SH_LOGEMENT,
505
+ 1
506
+ );
507
+
508
+ // Sans reconstruction, le prorata vaut 1 et le circulateur reste au plancher de 20 W
509
+ expect(reconstruite).not.toBeCloseTo((31 * 24 * 20) / 1000, 6);
510
+ expect(reconstruite).toBeCloseTo(0.45766695775765603, 9);
511
+
512
+ // Équivalent à une surface d'immeuble correctement renseignée
513
+ const renseignee = consoEcs({}, SH_IMMEUBLE_RECONSTRUITE, 1);
514
+ expect(reconstruite).toBeCloseTo(renseignee, 10);
515
+ });
516
+
517
+ test("reconstruction : le nombre de niveaux de l'immeuble prime sur celui de l'installation", () => {
518
+ // nombre_niveau_installation_ecs = 1 est la valeur du logement, incohérente
519
+ // avec la surface d'immeuble reconstruite
520
+ expect(
521
+ consoEcs({ ratio_virtualisation: String(RATIO_VIRTUALISATION) }, SH_LOGEMENT, 6)
522
+ ).toBeCloseTo(0.4237342712345093, 9);
523
+ });
524
+
525
+ test("reconstruction sans nombre de niveaux d'immeuble : repli sur celui de l'installation", () => {
526
+ expect(
527
+ consoEcs(
528
+ {
529
+ ratio_virtualisation: String(RATIO_VIRTUALISATION),
530
+ nombre_niveau_installation_ecs: 3
531
+ },
532
+ SH_LOGEMENT,
533
+ undefined
534
+ )
535
+ ).toBeCloseTo(0.4253220756819142, 9);
536
+ });
537
+
538
+ test('ratio de virtualisation égal à 1 : aucune reconstruction', () => {
539
+ expect(consoEcs({ ratio_virtualisation: '1' }, SH_LOGEMENT, 6)).toBeCloseTo(
540
+ (31 * 24 * 20) / 1000,
541
+ 10
542
+ );
543
+ });
544
+
545
+ test("surface d'immeuble cohérente : le ratio de virtualisation est ignoré", () => {
546
+ expect(
547
+ consoEcs({ ratio_virtualisation: String(RATIO_VIRTUALISATION) }, SH_IMMEUBLE_RECONSTRUITE, 6)
548
+ ).toBeCloseTo(consoEcs({}, SH_IMMEUBLE_RECONSTRUITE, 6), 10);
549
+ });
550
+ });
@@ -6,7 +6,8 @@ export function calc_sse_j(bv_list, ets, ca, zc, mois) {
6
6
  const baiesAdjVeranda = bv_list.filter((bv) => bv.donnee_entree.enum_type_adjacence_id === '10');
7
7
  const baiesAdjExt = bv_list.filter((bv) => bv.donnee_entree.enum_type_adjacence_id === '1');
8
8
 
9
- if (!ets) {
9
+ //ets peut etre un tableau vide ou undefined selon les DPE, d'où la vérification ajoutée sur le tableau et sa longueur
10
+ if (!ets || (Array.isArray(ets) && ets.length === 0)) {
10
11
  return baiesAdjExt.reduce((acc, bv) => {
11
12
  return acc + getSsd(bv, zc, mois, bv.donnee_intermediaire.sw);
12
13
  }, 0);
package/conso.js CHANGED
@@ -218,6 +218,11 @@ export default function calc_conso(
218
218
  return acc.concat(generateur_chauffage);
219
219
  }, []);
220
220
 
221
+ ecs = Array.isArray(ecs) ? ecs : [];
222
+
223
+ const conso_aux_distribution_ecs_total = ecs.reduce((acc, ecs) => {
224
+ return acc + (ecs.donnee_intermediaire.conso_auxiliaire_distribution_ecs || 0);
225
+ }, 0);
221
226
  const gen_ecs = ecs.reduce((acc, ecs) => {
222
227
  const generateur_ecs = ecs.generateur_ecs_collection.generateur_ecs;
223
228
  if (prorataECS === 1) {
@@ -243,7 +248,8 @@ export default function calc_conso(
243
248
  'conso',
244
249
  null,
245
250
  prorataECS,
246
- prorataChauffage
251
+ prorataChauffage,
252
+ conso_aux_distribution_ecs_total
247
253
  ),
248
254
  ep_conso: calc_conso_pond(
249
255
  Sh,
@@ -255,7 +261,8 @@ export default function calc_conso(
255
261
  'ep_conso',
256
262
  coeffEp,
257
263
  prorataECS,
258
- prorataChauffage
264
+ prorataChauffage,
265
+ conso_aux_distribution_ecs_total
259
266
  ),
260
267
  emission_ges: calc_conso_pond(
261
268
  Sh,
@@ -267,7 +274,8 @@ export default function calc_conso(
267
274
  'emission_ges',
268
275
  coef_ges,
269
276
  prorataECS,
270
- prorataChauffage
277
+ prorataChauffage,
278
+ conso_aux_distribution_ecs_total
271
279
  ),
272
280
  cout: calc_conso_pond(
273
281
  Sh,
@@ -279,7 +287,8 @@ export default function calc_conso(
279
287
  'cout',
280
288
  coef_cout,
281
289
  prorataECS,
282
- prorataChauffage
290
+ prorataChauffage,
291
+ conso_aux_distribution_ecs_total
283
292
  )
284
293
  };
285
294
  ret.ep_conso.classe_bilan_dpe = classe_bilan_dpe(
@@ -464,7 +473,8 @@ function calc_conso_pond(
464
473
  prefix,
465
474
  coef,
466
475
  prorataECS,
467
- prorataChauffage
476
+ prorataChauffage,
477
+ conso_aux_distribution_ecs_total
468
478
  ) {
469
479
  const ret = {};
470
480
  ret.auxiliaire_ventilation = vt_list.reduce((acc, vt) => {
@@ -528,7 +538,11 @@ function calc_conso_pond(
528
538
  return acc + getConso(coef, 'électricité auxiliaire', conso);
529
539
  }, 0);
530
540
 
531
- ret.auxiliaire_distribution_ecs = 0;
541
+ ret.auxiliaire_distribution_ecs = getConso(
542
+ coef,
543
+ 'électricité auxiliaire',
544
+ conso_aux_distribution_ecs_total || 0
545
+ );
532
546
 
533
547
  ret.ecs = getEcsConso(gen_ecs, 'conso_ecs', coef, prorataECS, prefix);
534
548
 
package/conso.spec.js CHANGED
@@ -188,6 +188,7 @@ function genEcs(energieId, di, deExtra = {}) {
188
188
  function installEcs(deExtra, gens) {
189
189
  return {
190
190
  donnee_entree: { ...deExtra },
191
+ donnee_intermediaire: {},
191
192
  generateur_ecs_collection: { generateur_ecs: gens }
192
193
  };
193
194
  }
@@ -434,6 +435,25 @@ describe('calc_conso - agrégation des consommations', () => {
434
435
  expect(res.ep_conso.ep_conso_ecs).toBeCloseTo(190, 9);
435
436
  });
436
437
 
438
+ test.each([
439
+ ['absente', undefined],
440
+ ['nulle', null],
441
+ ['non tabulaire', { installation_ecs: [] }]
442
+ ])('collection ECS %s : traitée comme une liste vide', (_libelle, ecs) => {
443
+ const chFixture = () => [
444
+ installCh({ cle_repartition_ch: 1 }, [
445
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
446
+ ])
447
+ ];
448
+
449
+ const res = calc_conso(100, 1, 1, [], chFixture(), ecs, [], 1, 1, DATE_DPE, coef_ep);
450
+
451
+ // Identique à un appel avec une collection ECS vide : aucune conso ni auxiliaire d'ECS
452
+ expect(res).toEqual(calc_conso(100, 1, 1, [], chFixture(), [], [], 1, 1, DATE_DPE, coef_ep));
453
+ expect(res.ef_conso.conso_ecs).toBe(0);
454
+ expect(res.ef_conso.conso_auxiliaire_distribution_ecs).toBe(0);
455
+ });
456
+
437
457
  test('ventilation et froid : auxiliaires et froid agrégés par énergie', () => {
438
458
  const vt = [
439
459
  {
package/engine.js CHANGED
@@ -3,6 +3,7 @@ import calc_deperdition from './3_deperdition.js';
3
3
  import calc_apport_et_besoin from './apport_et_besoin.js';
4
4
  import calc_clim from './10_clim.js';
5
5
  import calc_ecs from './11_ecs.js';
6
+ import { conso_aux_distribution_ecs } from './15_conso_aux.js';
6
7
  import calc_besoin_ch from './9_besoin_ch.js';
7
8
  import calc_chauffage, { tauxChargeForGenerator } from './9_chauffage.js';
8
9
  import calc_confort_ete from './2021_04_13_confort_ete.js';
@@ -31,7 +32,7 @@ import getFicheTechnique from './ficheTechnique.js';
31
32
  import { ProductionENR } from './16.2_production_enr.js';
32
33
  import DpeSanitizerService from './dpe-sanitizer.service.js';
33
34
 
34
- const LIB_VERSION = '1.6.2';
35
+ const LIB_VERSION = '1.7.0';
35
36
 
36
37
  const dpeSanitizerService = new DpeSanitizerService();
37
38
 
@@ -90,6 +91,7 @@ export function calcul_3cl(inputDpe, options) {
90
91
 
91
92
  const cg = logement.caracteristique_generale;
92
93
  const map_id = cg.enum_methode_application_dpe_log_id;
94
+ const dpeGenereImmeuble = ['10', '11', '12', '13', '33', '34', '38', '39', '40'].includes(map_id);
93
95
  const th = calc_th(map_id);
94
96
 
95
97
  if (logement.enveloppe === undefined) {
@@ -435,6 +437,20 @@ export function calcul_3cl(inputDpe, options) {
435
437
  dpe.logement.caracteristique_generale.nombre_appartement,
436
438
  isImmeubleSystemEcsIndividuels
437
439
  );
440
+
441
+ if (!dpeGenereImmeuble) {
442
+ conso_aux_distribution_ecs(
443
+ ecs,
444
+ ecs.donnee_entree,
445
+ ecs.donnee_intermediaire,
446
+ Sh,
447
+ cg.surface_habitable_immeuble,
448
+ ca_id,
449
+ zc_id,
450
+ apport_et_besoin.nadeq,
451
+ dpe.logement.caracteristique_generale.nombre_niveau_immeuble
452
+ );
453
+ }
438
454
  });
439
455
 
440
456
  /**
package/engine.spec.js CHANGED
@@ -65,6 +65,7 @@ vi.mock('./10_clim.js', () => ({ default: vi.fn() }));
65
65
  vi.mock('./11_ecs.js', () => ({ default: vi.fn() }));
66
66
  vi.mock('./9_besoin_ch.js', () => ({ default: vi.fn() }));
67
67
  vi.mock('./9_chauffage.js', () => ({ default: vi.fn(), tauxChargeForGenerator: vi.fn() }));
68
+ vi.mock('./15_conso_aux.js', () => ({ conso_aux_distribution_ecs: vi.fn() }));
68
69
  vi.mock('./2021_04_13_confort_ete.js', () => ({ default: vi.fn() }));
69
70
  vi.mock('./2021_04_13_qualite_isolation.js', () => ({ default: vi.fn() }));
70
71
  vi.mock('./ficheTechnique.js', () => ({ default: vi.fn() }));
@@ -102,6 +103,7 @@ const calc_clim = (await import('./10_clim.js')).default;
102
103
  const calc_ecs = (await import('./11_ecs.js')).default;
103
104
  const calc_besoin_ch = (await import('./9_besoin_ch.js')).default;
104
105
  const { default: calc_chauffage, tauxChargeForGenerator } = await import('./9_chauffage.js');
106
+ const { conso_aux_distribution_ecs } = await import('./15_conso_aux.js');
105
107
  const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
106
108
  const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
107
109
  const getFicheTechnique = (await import('./ficheTechnique.js')).default;
@@ -172,6 +174,7 @@ function makeDpe() {
172
174
  surface_habitable_logement: 80,
173
175
  surface_habitable_immeuble: 200,
174
176
  nombre_appartement: 4,
177
+ nombre_niveau_immeuble: 3,
175
178
  hsp: 2.5,
176
179
  annee_construction: 1990
177
180
  },
@@ -192,6 +195,7 @@ function makeDpe() {
192
195
  installation_ecs: [
193
196
  {
194
197
  donnee_entree: { enum_type_installation_id: '1' },
198
+ donnee_intermediaire: {},
195
199
  generateur_ecs_collection: { generateur_ecs: [] }
196
200
  }
197
201
  ]
@@ -455,6 +459,22 @@ describe('calcul_3cl - run intégral (maison)', () => {
455
459
  expect(calc_ecs.mock.calls[0][2]).toBe(100);
456
460
  expect(calc_ecs.mock.calls[0][11]).toBe(false);
457
461
 
462
+ // Auxiliaires de distribution ECS calculés pour l'installation (maison => map hors
463
+ // liste des DPE générés à partir des données d'immeuble).
464
+ const ecs = dpe.logement.installation_ecs_collection.installation_ecs[0];
465
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(1);
466
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledWith(
467
+ ecs,
468
+ ecs.donnee_entree,
469
+ ecs.donnee_intermediaire,
470
+ 80, // Sh (maison)
471
+ 200, // surface_habitable_immeuble
472
+ 'ca1',
473
+ 'zc1',
474
+ 2, // nadeq
475
+ 3 // nombre_niveau_immeuble
476
+ );
477
+
458
478
  // Climatisation présente : calc_clim appelé.
459
479
  expect(calc_clim).toHaveBeenCalledTimes(1);
460
480
  });
@@ -484,6 +504,7 @@ describe('calcul_3cl - run intégral (maison)', () => {
484
504
  expect(calc_apport_et_besoin.mock.calls[0][5]).toBe(1);
485
505
  // Pas d'ECS ni de clim à traiter.
486
506
  expect(calc_ecs).not.toHaveBeenCalled();
507
+ expect(conso_aux_distribution_ecs).not.toHaveBeenCalled();
487
508
  expect(calc_clim).not.toHaveBeenCalled();
488
509
  });
489
510
  });
@@ -538,6 +559,9 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
538
559
  // isImmeubleSystemEcsIndividuels = true, besoin non divisé.
539
560
  expect(calc_ecs.mock.calls[0][11]).toBe(true);
540
561
  expect(calc_ecs.mock.calls[0][2]).toBe(100);
562
+ // Auxiliaires ECS calculés par installation, avec Sh = surface immeuble.
563
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(2);
564
+ expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(200);
541
565
  });
542
566
 
543
567
  test('immeuble : systèmes ECS non tous individuels => besoin divisé par deux', () => {
@@ -593,6 +617,77 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
593
617
  expect(calc_conso.mock.calls[0][0]).toBe(80);
594
618
  expect(calc_conso.mock.calls[0][7]).toBeCloseTo(0.4, 9); // prorataECS
595
619
  expect(calc_conso.mock.calls[0][8]).toBeCloseTo(0.4, 9); // prorataChauffage
620
+ // Auxiliaires ECS : Sh = surface logement, ramenée à l'immeuble dans le calcul.
621
+ expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(80);
622
+ expect(conso_aux_distribution_ecs.mock.calls[0][4]).toBe(200);
623
+ });
624
+ });
625
+
626
+ describe("calcul_3cl - auxiliaires de distribution d'ECS", () => {
627
+ // Méthodes d'application « appartement généré à partir des données DPE immeuble » :
628
+ // les auxiliaires de distribution d'ECS ne sont pas calculés pour ces DPE.
629
+ const MAP_IDS_GENERES_IMMEUBLE = ['10', '11', '12', '13', '33', '34', '38', '39', '40'];
630
+
631
+ test.each(MAP_IDS_GENERES_IMMEUBLE)(
632
+ "DPE appartement généré à partir des données d'immeuble (map %s) : auxiliaires non calculés",
633
+ (mapId) => {
634
+ enums.methode_application_dpe_log = {
635
+ [mapId]: 'dpe appartement généré à partir des données dpe immeuble chauffage collectif'
636
+ };
637
+ const dpe = makeDpe();
638
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = mapId;
639
+
640
+ calcul_3cl(dpe, { sanitize: false });
641
+
642
+ // L'installation est bien traitée par ailleurs, seul le calcul des auxiliaires est ignoré.
643
+ expect(calc_ecs).toHaveBeenCalledTimes(1);
644
+ expect(conso_aux_distribution_ecs).not.toHaveBeenCalled();
645
+ }
646
+ );
647
+
648
+ test('appartement non généré depuis un immeuble (map 15) : auxiliaires calculés', () => {
649
+ enums.methode_application_dpe_log = { 15: 'dpe appartement collectif' };
650
+ const dpe = makeDpe();
651
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = '15';
652
+
653
+ calcul_3cl(dpe, { sanitize: false });
654
+
655
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(1);
656
+ });
657
+
658
+ test('une installation ECS par appel : chaque installation est traitée', () => {
659
+ const dpe = makeDpe();
660
+ const installations = [
661
+ {
662
+ donnee_entree: { enum_type_installation_id: '1' },
663
+ donnee_intermediaire: { tag: 'ecs1' },
664
+ generateur_ecs_collection: { generateur_ecs: [] }
665
+ },
666
+ {
667
+ donnee_entree: { enum_type_installation_id: '2' },
668
+ donnee_intermediaire: { tag: 'ecs2' },
669
+ generateur_ecs_collection: { generateur_ecs: [] }
670
+ }
671
+ ];
672
+ dpe.logement.installation_ecs_collection.installation_ecs = installations;
673
+
674
+ calcul_3cl(dpe, { sanitize: false });
675
+
676
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(2);
677
+ installations.forEach((ecs, i) => {
678
+ expect(conso_aux_distribution_ecs.mock.calls[i][0]).toBe(ecs);
679
+ expect(conso_aux_distribution_ecs.mock.calls[i][1]).toBe(ecs.donnee_entree);
680
+ expect(conso_aux_distribution_ecs.mock.calls[i][2]).toBe(ecs.donnee_intermediaire);
681
+ });
682
+ });
683
+
684
+ test("nombre de niveaux de l'immeuble absent : transmis tel quel", () => {
685
+ const dpe = makeDpe();
686
+ dpe.logement.caracteristique_generale.nombre_niveau_immeuble = undefined;
687
+
688
+ calcul_3cl(dpe, { sanitize: false });
689
+
690
+ expect(conso_aux_distribution_ecs.mock.calls[0][8]).toBeUndefined();
596
691
  });
597
692
  });
598
693
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {