@open3cl/engine 1.8.9 → 1.8.10
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/9_besoin_ch.js +26 -1
- package/9_besoin_ch.spec.js +151 -2
- package/9_generateur_ch.js +46 -0
- package/9_generateur_ch.spec.js +67 -0
- package/README.md +19 -19
- package/engine.js +1 -1
- package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.js +54 -8
- package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +185 -0
- package/package.json +1 -1
package/9_besoin_ch.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import enums from './enums.js';
|
|
2
2
|
import tvs from './tv.js';
|
|
3
3
|
import { calc_besoin_ecs_j } from './11_besoin_ecs.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
calc_Qrec_gen_ecs_j,
|
|
6
|
+
calc_Qrec_gen_j,
|
|
7
|
+
isGenerateurEcsSeulRecuperable
|
|
8
|
+
} from './9_generateur_ch.js';
|
|
5
9
|
import { calc_ai_j, calc_as_j } from './6.1_apport_gratuit.js';
|
|
6
10
|
import { calc_sse_j } from './6.2_surface_sud_equivalente.js';
|
|
7
11
|
import { bug_for_bug_compat, mois_liste } from './utils.js';
|
|
@@ -105,6 +109,20 @@ export default function calc_besoin_ch(
|
|
|
105
109
|
)
|
|
106
110
|
);
|
|
107
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 9.1.1 - Générateurs de la collection ECS assurant l'ECS uniquement, avec Qp0 > 0 et situés en
|
|
114
|
+
* volume chauffé : leurs pertes de génération sont aussi récupérées pour le chauffage.
|
|
115
|
+
* Les générateurs 'chauffage + ecs' sont exclus car déjà présents dans la collection chauffage.
|
|
116
|
+
* @see issue #153
|
|
117
|
+
*
|
|
118
|
+
* @type {GenerateurEcsItem[]}
|
|
119
|
+
*/
|
|
120
|
+
const gen_ecs_recup = instal_ecs.flatMap((inst_ecs) =>
|
|
121
|
+
(inst_ecs.generateur_ecs_collection?.generateur_ecs || []).filter(
|
|
122
|
+
isGenerateurEcsSeulRecuperable
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
|
|
108
126
|
const besoin_ch_mois = {};
|
|
109
127
|
const besoin_ch_mois_dep = {};
|
|
110
128
|
for (const mois of mois_liste) {
|
|
@@ -145,6 +163,13 @@ export default function calc_besoin_ch(
|
|
|
145
163
|
gen_recup_dep += calc_Qrec_gen_j(gen_ch, nref21, Bch_hp_j_dep) / ratio;
|
|
146
164
|
});
|
|
147
165
|
|
|
166
|
+
gen_ecs_recup.forEach((gen_ecs) => {
|
|
167
|
+
// Même facteur 1000 que pour les générateurs de chauffage (compatibilité « bug for bug »)
|
|
168
|
+
const ratio = bug_for_bug_compat ? 1000 : 1;
|
|
169
|
+
gen_recup += calc_Qrec_gen_ecs_j(gen_ecs, nref19) / ratio;
|
|
170
|
+
gen_recup_dep += calc_Qrec_gen_ecs_j(gen_ecs, nref21) / ratio;
|
|
171
|
+
});
|
|
172
|
+
|
|
148
173
|
pertes_generateur_ch_recup += gen_recup;
|
|
149
174
|
pertes_generateur_ch_recup_depensier += gen_recup_dep;
|
|
150
175
|
|
package/9_besoin_ch.spec.js
CHANGED
|
@@ -42,7 +42,9 @@ vi.mock('./11_besoin_ecs.js', () => ({
|
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
44
|
vi.mock('./9_generateur_ch.js', () => ({
|
|
45
|
-
calc_Qrec_gen_j: vi.fn()
|
|
45
|
+
calc_Qrec_gen_j: vi.fn(),
|
|
46
|
+
calc_Qrec_gen_ecs_j: vi.fn(),
|
|
47
|
+
isGenerateurEcsSeulRecuperable: vi.fn()
|
|
46
48
|
}));
|
|
47
49
|
|
|
48
50
|
const utilState = vi.hoisted(() => ({ bug: false }));
|
|
@@ -59,7 +61,8 @@ const { default: tvsBch } = await import('./tv.js');
|
|
|
59
61
|
const { calc_ai_j, calc_as_j } = await import('./6.1_apport_gratuit.js');
|
|
60
62
|
const { calc_sse_j } = await import('./6.2_surface_sud_equivalente.js');
|
|
61
63
|
const { calc_besoin_ecs_j } = await import('./11_besoin_ecs.js');
|
|
62
|
-
const { calc_Qrec_gen_j } =
|
|
64
|
+
const { calc_Qrec_gen_j, calc_Qrec_gen_ecs_j, isGenerateurEcsSeulRecuperable } =
|
|
65
|
+
await import('./9_generateur_ch.js');
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* 9. Besoins de chauffage (Bch)
|
|
@@ -401,3 +404,149 @@ describe('calc_besoin_ch - branches complémentaires', () => {
|
|
|
401
404
|
expect(ret.pertes_generateur_ch_recup).toBeCloseTo(1, 9);
|
|
402
405
|
});
|
|
403
406
|
});
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 9.1.1 - Pertes récupérées de génération : générateurs de la collection ECS assurant l'ECS
|
|
410
|
+
* uniquement, avec Qp0 > 0 et en volume chauffé (issue #153).
|
|
411
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
|
|
412
|
+
*/
|
|
413
|
+
describe('calc_besoin_ch - récupération des pertes des générateurs ECS seuls (#153)', () => {
|
|
414
|
+
const genEcsSeul = {
|
|
415
|
+
donnee_entree: { enum_usage_generateur_id: '2', position_volume_chauffe: 1 },
|
|
416
|
+
donnee_intermediaire: { qp0: 100 }
|
|
417
|
+
};
|
|
418
|
+
const genEcsMixte = {
|
|
419
|
+
donnee_entree: { enum_usage_generateur_id: '3', position_volume_chauffe: 1 },
|
|
420
|
+
donnee_intermediaire: { qp0: 100 }
|
|
421
|
+
};
|
|
422
|
+
const instal_ecs = [
|
|
423
|
+
{
|
|
424
|
+
donnee_entree: { enum_type_installation_id: '2', rdim: 1 },
|
|
425
|
+
generateur_ecs_collection: { generateur_ecs: [genEcsSeul, genEcsMixte] }
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
beforeEach(() => {
|
|
430
|
+
utilState.bug = false;
|
|
431
|
+
vi.mocked(calc_ai_j).mockReset().mockReturnValue(5000);
|
|
432
|
+
vi.mocked(calc_as_j).mockReset().mockReturnValue(2000);
|
|
433
|
+
vi.mocked(calc_sse_j).mockReset().mockReturnValue(10);
|
|
434
|
+
vi.mocked(calc_besoin_ecs_j).mockReset().mockReturnValue(10);
|
|
435
|
+
vi.mocked(calc_Qrec_gen_j).mockReset().mockReturnValue(0);
|
|
436
|
+
vi.mocked(calc_Qrec_gen_ecs_j).mockReset();
|
|
437
|
+
vi.mocked(isGenerateurEcsSeulRecuperable)
|
|
438
|
+
.mockReset()
|
|
439
|
+
.mockImplementation((gen) => gen === genEcsSeul);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('seuls les générateurs retenus par isGenerateurEcsSeulRecuperable sont calculés', () => {
|
|
443
|
+
vi.mocked(calc_Qrec_gen_ecs_j).mockReturnValue(500);
|
|
444
|
+
calc_besoin_ch(0, 0, 0, 0, 100, 100, 3, instal_ecs, [], [], null, 'maison', 1);
|
|
445
|
+
|
|
446
|
+
// un appel conventionnel (nref19 = 100) et un appel dépensier (nref21 = 120) pour le seul mois
|
|
447
|
+
expect(calc_Qrec_gen_ecs_j).toHaveBeenCalledTimes(2);
|
|
448
|
+
expect(calc_Qrec_gen_ecs_j).toHaveBeenCalledWith(genEcsSeul, 100);
|
|
449
|
+
expect(calc_Qrec_gen_ecs_j).toHaveBeenCalledWith(genEcsSeul, 120);
|
|
450
|
+
expect(calc_Qrec_gen_ecs_j).not.toHaveBeenCalledWith(genEcsMixte, expect.anything());
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('la récupération du générateur ECS seul s’ajoute aux pertes récupérées et réduit le besoin', () => {
|
|
454
|
+
vi.mocked(isGenerateurEcsSeulRecuperable).mockReturnValue(false);
|
|
455
|
+
const sansRecup = calc_besoin_ch(
|
|
456
|
+
0,
|
|
457
|
+
0,
|
|
458
|
+
0,
|
|
459
|
+
0,
|
|
460
|
+
100,
|
|
461
|
+
100,
|
|
462
|
+
3,
|
|
463
|
+
instal_ecs,
|
|
464
|
+
[],
|
|
465
|
+
[],
|
|
466
|
+
null,
|
|
467
|
+
'maison',
|
|
468
|
+
1
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
vi.mocked(isGenerateurEcsSeulRecuperable).mockImplementation((gen) => gen === genEcsSeul);
|
|
472
|
+
vi.mocked(calc_Qrec_gen_ecs_j).mockImplementation((gen, nref) => (nref === 100 ? 500 : 600));
|
|
473
|
+
const avecRecup = calc_besoin_ch(
|
|
474
|
+
0,
|
|
475
|
+
0,
|
|
476
|
+
0,
|
|
477
|
+
0,
|
|
478
|
+
100,
|
|
479
|
+
100,
|
|
480
|
+
3,
|
|
481
|
+
instal_ecs,
|
|
482
|
+
[],
|
|
483
|
+
[],
|
|
484
|
+
null,
|
|
485
|
+
'maison',
|
|
486
|
+
1
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
expect(avecRecup.pertes_generateur_ch_recup).toBeCloseTo(500, 9);
|
|
490
|
+
expect(avecRecup.pertes_generateur_ch_recup_depensier).toBeCloseTo(600, 9);
|
|
491
|
+
// 500 Wh récupérés => besoin réduit de 0,5 kWh
|
|
492
|
+
expect(sansRecup.besoin_ch - avecRecup.besoin_ch).toBeCloseTo(0.5, 9);
|
|
493
|
+
expect(sansRecup.besoin_ch_depensier - avecRecup.besoin_ch_depensier).toBeCloseTo(0.6, 9);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('cumul avec la récupération des générateurs de chauffage', () => {
|
|
497
|
+
vi.mocked(calc_Qrec_gen_j).mockReturnValue(1000);
|
|
498
|
+
vi.mocked(calc_Qrec_gen_ecs_j).mockReturnValue(500);
|
|
499
|
+
const instal_ch = [
|
|
500
|
+
{
|
|
501
|
+
generateur_chauffage_collection: {
|
|
502
|
+
generateur_chauffage: [
|
|
503
|
+
{ donnee_intermediaire: { qp0: 1 }, donnee_entree: { position_volume_chauffe: 1 } }
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
const ret = calc_besoin_ch(
|
|
509
|
+
0,
|
|
510
|
+
0,
|
|
511
|
+
0,
|
|
512
|
+
0,
|
|
513
|
+
100,
|
|
514
|
+
100,
|
|
515
|
+
3,
|
|
516
|
+
instal_ecs,
|
|
517
|
+
instal_ch,
|
|
518
|
+
[],
|
|
519
|
+
null,
|
|
520
|
+
'maison',
|
|
521
|
+
1
|
|
522
|
+
);
|
|
523
|
+
expect(ret.pertes_generateur_ch_recup).toBeCloseTo(1500, 9);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('compatibilité "bug for bug" : même facteur 1000 que pour les générateurs de chauffage', () => {
|
|
527
|
+
utilState.bug = true;
|
|
528
|
+
vi.mocked(calc_Qrec_gen_ecs_j).mockReturnValue(500);
|
|
529
|
+
const ret = calc_besoin_ch(0, 0, 0, 0, 100, 100, 3, instal_ecs, [], [], null, 'maison', 1);
|
|
530
|
+
expect(ret.pertes_generateur_ch_recup).toBeCloseTo(0.5, 9);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('installation ECS sans générateur : pas de récupération', () => {
|
|
534
|
+
const ret = calc_besoin_ch(
|
|
535
|
+
0,
|
|
536
|
+
0,
|
|
537
|
+
0,
|
|
538
|
+
0,
|
|
539
|
+
100,
|
|
540
|
+
100,
|
|
541
|
+
3,
|
|
542
|
+
[{ donnee_entree: { enum_type_installation_id: '2' }, generateur_ecs_collection: {} }],
|
|
543
|
+
[],
|
|
544
|
+
[],
|
|
545
|
+
null,
|
|
546
|
+
'maison',
|
|
547
|
+
1
|
|
548
|
+
);
|
|
549
|
+
expect(calc_Qrec_gen_ecs_j).not.toHaveBeenCalled();
|
|
550
|
+
expect(ret.pertes_generateur_ch_recup).toBe(0);
|
|
551
|
+
});
|
|
552
|
+
});
|
package/9_generateur_ch.js
CHANGED
|
@@ -44,6 +44,52 @@ export function calc_Qrec_gen_j(gen_ch, nrefj, Bch_hp_j) {
|
|
|
44
44
|
return 0.48 * Cper * di.qp0 * Dperj || 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 9.1.1 - Pertes récupérées de génération pour le chauffage sur le mois j (Wh)
|
|
49
|
+
* pour un générateur de la collection ECS assurant l'ECS uniquement.
|
|
50
|
+
*
|
|
51
|
+
* Pour les générateurs assurant l'ECS uniquement :
|
|
52
|
+
* Dperj = Nrefj * 1790 / 8760
|
|
53
|
+
* Qgen_rec_j = 0,48 * Cper * Qp0 * Dperj
|
|
54
|
+
*
|
|
55
|
+
* Seuls les générateurs avec des pertes à l'arrêt (Qp0 > 0), situés en volume chauffé et dont
|
|
56
|
+
* l'usage est 'ecs' sont concernés. Les générateurs 'chauffage + ecs' sont déjà pris en compte via
|
|
57
|
+
* la collection des générateurs de chauffage (cf. issue #153).
|
|
58
|
+
*
|
|
59
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
|
|
60
|
+
*
|
|
61
|
+
* @param gen_ecs {GenerateurEcsItem}
|
|
62
|
+
* @param nrefj {number}
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function calc_Qrec_gen_ecs_j(gen_ecs, nrefj) {
|
|
66
|
+
const de = gen_ecs.donnee_entree;
|
|
67
|
+
const di = gen_ecs.donnee_intermediaire || {};
|
|
68
|
+
|
|
69
|
+
if (!isGenerateurEcsSeulRecuperable(gen_ecs)) return 0;
|
|
70
|
+
|
|
71
|
+
const Cper = Number(de.presence_ventouse) === 1 ? 0.75 : 0.5;
|
|
72
|
+
return 0.48 * Cper * di.qp0 * pertes_gen_ecs(nrefj);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Un générateur ECS est concerné par la récupération des pertes de génération pour le chauffage
|
|
77
|
+
* s'il a des pertes à l'arrêt (Qp0 > 0), s'il est en volume chauffé et si son usage est 'ecs' seul
|
|
78
|
+
* (enum_usage_generateur_id = 2).
|
|
79
|
+
*
|
|
80
|
+
* @param gen_ecs {GenerateurEcsItem}
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
export function isGenerateurEcsSeulRecuperable(gen_ecs) {
|
|
84
|
+
const de = gen_ecs.donnee_entree || {};
|
|
85
|
+
const di = gen_ecs.donnee_intermediaire || {};
|
|
86
|
+
return (
|
|
87
|
+
Number(di.qp0 || 0) > 0 &&
|
|
88
|
+
Number(de.position_volume_chauffe ?? 0) === 1 &&
|
|
89
|
+
Number(de.enum_usage_generateur_id) === 2
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
47
93
|
function tv_rendement_generation(di, de, du) {
|
|
48
94
|
const matcher = {
|
|
49
95
|
enum_type_generateur_ch_id: requestInputID(de, du, 'type_generateur_ch')
|
package/9_generateur_ch.spec.js
CHANGED
|
@@ -59,6 +59,8 @@ vi.mock('./13.2_generateur_combustion.js', () => ({
|
|
|
59
59
|
|
|
60
60
|
const {
|
|
61
61
|
calc_Qrec_gen_j,
|
|
62
|
+
calc_Qrec_gen_ecs_j,
|
|
63
|
+
isGenerateurEcsSeulRecuperable,
|
|
62
64
|
type_generateur_ch,
|
|
63
65
|
checkForGeneratorType,
|
|
64
66
|
calc_generateur_ch,
|
|
@@ -147,6 +149,71 @@ describe('calc_Qrec_gen_j - pertes de génération récupérées', () => {
|
|
|
147
149
|
});
|
|
148
150
|
});
|
|
149
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 9.1.1 Pertes récupérées de génération pour le chauffage : générateur de la collection ECS
|
|
154
|
+
* assurant l'ECS uniquement (issue #153).
|
|
155
|
+
* Dperj = Nrefj * 1790 / 8760 ; Qgen_rec_j = 0,48 * Cper * Qp0 * Dperj
|
|
156
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
|
|
157
|
+
*/
|
|
158
|
+
describe('calc_Qrec_gen_ecs_j / isGenerateurEcsSeulRecuperable - générateur ECS seul', () => {
|
|
159
|
+
/** Générateur ECS seul (usage 2), en volume chauffé, avec Qp0 = 100 W. */
|
|
160
|
+
function generateurEcs(overridesDe = {}, di = { qp0: 100 }) {
|
|
161
|
+
return {
|
|
162
|
+
donnee_entree: {
|
|
163
|
+
position_volume_chauffe: 1,
|
|
164
|
+
enum_usage_generateur_id: '2',
|
|
165
|
+
presence_ventouse: 0,
|
|
166
|
+
...overridesDe
|
|
167
|
+
},
|
|
168
|
+
donnee_intermediaire: di
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
test('ECS seul, sans ventouse (Cper = 0.5) : 0.48 * 0.5 * Qp0 * Nref * 1790 / 8760', () => {
|
|
173
|
+
// 0.48 * 0.5 * 100 * (50 * 1790 / 8760) = 245.205479452...
|
|
174
|
+
expect(calc_Qrec_gen_ecs_j(generateurEcs(), 50)).toBeCloseTo(245.20547945205482, 9);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('ECS seul, avec ventouse (Cper = 0.75)', () => {
|
|
178
|
+
// 0.48 * 0.75 * 100 * (50 * 1790 / 8760) = 367.808219178...
|
|
179
|
+
const gen = generateurEcs({ presence_ventouse: 1 });
|
|
180
|
+
expect(calc_Qrec_gen_ecs_j(gen, 50)).toBeCloseTo(367.8082191780822, 9);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('la récupération est indépendante du besoin de chauffage et proportionnelle à Nref', () => {
|
|
184
|
+
const gen = generateurEcs();
|
|
185
|
+
expect(calc_Qrec_gen_ecs_j(gen, 100)).toBeCloseTo(2 * calc_Qrec_gen_ecs_j(gen, 50), 9);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('générateur hors volume chauffé : aucune récupération', () => {
|
|
189
|
+
const gen = generateurEcs({ position_volume_chauffe: 0 });
|
|
190
|
+
expect(isGenerateurEcsSeulRecuperable(gen)).toBe(false);
|
|
191
|
+
expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('position en volume chauffé non renseignée : aucune récupération', () => {
|
|
195
|
+
const gen = generateurEcs({ position_volume_chauffe: undefined });
|
|
196
|
+
expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("usage 'chauffage + ecs' : exclu (déjà compté dans la collection chauffage)", () => {
|
|
200
|
+
const gen = generateurEcs({ enum_usage_generateur_id: '3' });
|
|
201
|
+
expect(isGenerateurEcsSeulRecuperable(gen)).toBe(false);
|
|
202
|
+
expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('Qp0 nul ou absent : aucune récupération', () => {
|
|
206
|
+
expect(calc_Qrec_gen_ecs_j(generateurEcs({}, { qp0: 0 }), 50)).toBe(0);
|
|
207
|
+
expect(calc_Qrec_gen_ecs_j(generateurEcs({}, null), 50)).toBe(0);
|
|
208
|
+
expect(isGenerateurEcsSeulRecuperable({})).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('identifiants numériques acceptés (usage 2, position 1)', () => {
|
|
212
|
+
const gen = generateurEcs({ enum_usage_generateur_id: 2, position_volume_chauffe: '1' });
|
|
213
|
+
expect(isGenerateurEcsSeulRecuperable(gen)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
150
217
|
/**
|
|
151
218
|
* Détermination du type de générateur exploitable pour un usage donné.
|
|
152
219
|
*/
|
package/README.md
CHANGED
|
@@ -377,19 +377,19 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
377
377
|
<tr>
|
|
378
378
|
<td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
|
|
379
379
|
<td align="center"><strong>89 990</strong><br/><sub>DPE analysés</sub></td>
|
|
380
|
-
<td align="center"><strong>59
|
|
381
|
-
<td align="center"><strong>66,
|
|
380
|
+
<td align="center"><strong>59 879</strong><br/><sub>DPE conformes</sub></td>
|
|
381
|
+
<td align="center"><strong>66,54 %</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,
|
|
388
|
-
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,
|
|
389
|
-
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,
|
|
387
|
+
| 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,28 %** | `█████████░░░░░░░░░░░` | 4 628 / 10 000 |
|
|
388
|
+
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,80 %** | `██████████████████░░` | 9 180 / 10 000 |
|
|
389
|
+
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,70 %** | `██████████████████░░` | 8 863 / 9 992 |
|
|
390
390
|
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 840 / 10 000 |
|
|
391
391
|
| 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,14 %** | `███████████████░░░░░` | 7 413 / 9 999 |
|
|
392
|
-
| 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **
|
|
392
|
+
| 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **70,61 %** | `██████████████░░░░░░` | 7 061 / 10 000 |
|
|
393
393
|
| 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,50 %** | `████████████░░░░░░░░` | 6 249 / 9 999 |
|
|
394
394
|
| 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,06 %** | `██████████░░░░░░░░░░` | 4 906 / 10 000 |
|
|
395
395
|
| 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,39 %** | `█████░░░░░░░░░░░░░░░` | 2 739 / 10 000 |
|
|
@@ -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>
|
|
407
|
-
<td align="center"><strong>4,
|
|
408
|
-
<td align="center"><strong>0,
|
|
409
|
-
<td align="center"><strong>
|
|
406
|
+
<td align="center"><strong>5,31 ms</strong><br/><sub>moyenne</sub></td>
|
|
407
|
+
<td align="center"><strong>4,65 ms</strong><br/><sub>médiane</sub></td>
|
|
408
|
+
<td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
|
|
409
|
+
<td align="center"><strong>893,4 ms</strong><br/><sub>max</sub></td>
|
|
410
410
|
</tr>
|
|
411
411
|
</table>
|
|
412
412
|
|
|
413
413
|
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
|
|
414
414
|
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
|
|
415
|
-
| **Généraliste** | 4,
|
|
416
|
-
| **Appartement · chauffage individuel (2025)** |
|
|
417
|
-
| **Logement individuel (2025)** | 4,
|
|
418
|
-
| **Maison individuelle (2025)** | 5,
|
|
419
|
-
| **Immeuble · chauffage individuel** |
|
|
420
|
-
| **Appartement · chauffage collectif (2025)** | 4,
|
|
421
|
-
| **Immeuble · chauffage collectif** |
|
|
422
|
-
| **Immeuble · chauffage mixte** |
|
|
423
|
-
| **Individuel généré depuis l'immeuble (2026)** |
|
|
415
|
+
| **Généraliste** | 4,56 ms | 4,09 ms | 0,21 ms | 37,2 ms | 8,41 ms | 12,0 ms |
|
|
416
|
+
| **Appartement · chauffage individuel (2025)** | 4,01 ms | 3,65 ms | 1,47 ms | 33,8 ms | 6,95 ms | 9,38 ms |
|
|
417
|
+
| **Logement individuel (2025)** | 4,98 ms | 4,43 ms | 1,62 ms | 38,0 ms | 9,30 ms | 12,8 ms |
|
|
418
|
+
| **Maison individuelle (2025)** | 5,92 ms | 5,33 ms | 1,85 ms | 40,3 ms | 10,6 ms | 13,7 ms |
|
|
419
|
+
| **Immeuble · chauffage individuel** | 6,70 ms | 5,72 ms | 0,13 ms | 136,8 ms | 13,4 ms | 22,2 ms |
|
|
420
|
+
| **Appartement · chauffage collectif (2025)** | 4,60 ms | 4,36 ms | 1,46 ms | 37,2 ms | 7,27 ms | 9,45 ms |
|
|
421
|
+
| **Immeuble · chauffage collectif** | 5,31 ms | 4,38 ms | 0,12 ms | 99,5 ms | 9,95 ms | 17,7 ms |
|
|
422
|
+
| **Immeuble · chauffage mixte** | 5,38 ms | 4,34 ms | 0,20 ms | 893,4 ms | 11,2 ms | 18,5 ms |
|
|
423
|
+
| **Individuel généré depuis l'immeuble (2026)** | 6,36 ms | 5,53 ms | 1,67 ms | 50,2 ms | 12,4 ms | 19,1 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/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.8.
|
|
36
|
+
const LIB_VERSION = '1.8.10';
|
|
37
37
|
|
|
38
38
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
39
39
|
|
|
@@ -52,27 +52,73 @@ export class PerteChRecupService {
|
|
|
52
52
|
*/
|
|
53
53
|
pertesGenerateurChRecup(ctx, logement, depensier) {
|
|
54
54
|
const generateursWithPertesGeneration = this.generateursWithPertesGeneration(logement);
|
|
55
|
+
const generateursEcsSeulsWithPertesGeneration =
|
|
56
|
+
this.generateursEcsSeulsWithPertesGeneration(logement);
|
|
55
57
|
|
|
56
58
|
return mois_liste.reduce((acc, mois) => {
|
|
59
|
+
const nref = this.#tvStore.getData(
|
|
60
|
+
depensier ? 'nref21' : 'nref19',
|
|
61
|
+
ctx.altitude.value,
|
|
62
|
+
ctx.zoneClimatique.value,
|
|
63
|
+
mois,
|
|
64
|
+
ctx.inertie.ilpa
|
|
65
|
+
);
|
|
57
66
|
return (
|
|
58
67
|
acc +
|
|
59
68
|
this.Qrec(
|
|
60
69
|
generateursWithPertesGeneration,
|
|
61
|
-
|
|
62
|
-
depensier ? 'nref21' : 'nref19',
|
|
63
|
-
ctx.altitude.value,
|
|
64
|
-
ctx.zoneClimatique.value,
|
|
65
|
-
mois,
|
|
66
|
-
ctx.inertie.ilpa
|
|
67
|
-
),
|
|
70
|
+
nref,
|
|
68
71
|
depensier
|
|
69
72
|
? logement.donnees_de_calcul.besoinChauffageDepensierHP[mois]
|
|
70
73
|
: logement.donnees_de_calcul.besoinChauffageHP[mois]
|
|
71
|
-
)
|
|
74
|
+
) +
|
|
75
|
+
this.QrecEcsSeul(generateursEcsSeulsWithPertesGeneration, nref)
|
|
72
76
|
);
|
|
73
77
|
}, 0);
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Générateurs de la collection ECS assurant l'ECS uniquement dont les pertes de génération sont
|
|
82
|
+
* récupérées pour le chauffage (issue #153) :
|
|
83
|
+
* — pertes à l'arrêt Qp0 > 0
|
|
84
|
+
* — situés en volume chauffé
|
|
85
|
+
* — usage ECS uniquement (enum_usage_generateur_id = 2). Les générateurs 'chauffage + ecs' sont
|
|
86
|
+
* déjà pris en compte dans la collection des générateurs de chauffage.
|
|
87
|
+
*
|
|
88
|
+
* @param logement {Logement}
|
|
89
|
+
* @return {GenerateurEcs[]}
|
|
90
|
+
*/
|
|
91
|
+
generateursEcsSeulsWithPertesGeneration(logement) {
|
|
92
|
+
const installationsEcs = logement.installation_ecs_collection?.installation_ecs || [];
|
|
93
|
+
|
|
94
|
+
return installationsEcs.flatMap((installation) =>
|
|
95
|
+
(installation.generateur_ecs_collection?.generateur_ecs || []).filter((generateurEcs) => {
|
|
96
|
+
const generateurEcsDE = generateurEcs.donnee_entree || {};
|
|
97
|
+
const generateurEcsDI = generateurEcs.donnee_intermediaire || {};
|
|
98
|
+
return (
|
|
99
|
+
Number(generateurEcsDI.qp0 || 0) > 0 &&
|
|
100
|
+
Number(generateurEcsDE.position_volume_chauffe ?? 0) === 1 &&
|
|
101
|
+
parseInt(generateurEcsDE.enum_usage_generateur_id) === 2
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pertes récupérées de génération pour le chauffage sur le mois j (Wh) pour les générateurs
|
|
109
|
+
* assurant l'ECS uniquement : Dperj = Nrefj * 1790 / 8760
|
|
110
|
+
*
|
|
111
|
+
* @param generateurs {GenerateurEcs[]}
|
|
112
|
+
* @param nref {number}
|
|
113
|
+
* @return {number}
|
|
114
|
+
*/
|
|
115
|
+
QrecEcsSeul(generateurs, nref) {
|
|
116
|
+
return generateurs.reduce((acc, generateur) => {
|
|
117
|
+
const Cper = Number(generateur.donnee_entree.presence_ventouse || 0) === 1 ? 0.75 : 0.5;
|
|
118
|
+
return acc + 0.48 * Cper * generateur.donnee_intermediaire.qp0 * this.#pertes_gen_ecs(nref);
|
|
119
|
+
}, 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
76
122
|
/**
|
|
77
123
|
* Pertes récupérées des générateurs de chauffage pour le chauffage
|
|
78
124
|
* 9.1.1 Consommation de chauffage
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js';
|
|
3
|
+
import { PerteChRecupService } from './perte-ch-recup.service.js';
|
|
4
|
+
import { mois_liste } from '../../../../../utils.js';
|
|
5
|
+
|
|
6
|
+
/** @type {PerteChRecupService} **/
|
|
7
|
+
let service;
|
|
8
|
+
|
|
9
|
+
/** @type {FrTvStore} **/
|
|
10
|
+
let tvStore;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 9.1.1 Pertes récupérées de génération pour le chauffage
|
|
14
|
+
* Qgen_rec_j = 0,48 * Cper * Qp0 * Dperj
|
|
15
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
|
|
16
|
+
*/
|
|
17
|
+
describe('Calcul des pertes récupérées de génération pour le chauffage', () => {
|
|
18
|
+
/** @type {Contexte} */
|
|
19
|
+
const ctx = {
|
|
20
|
+
altitude: { value: '400-800m' },
|
|
21
|
+
zoneClimatique: { value: 'h1a' },
|
|
22
|
+
inertie: { ilpa: 0 }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Besoins de chauffage hors pertes (kWh) : 10 kWh par mois */
|
|
26
|
+
const donneesDeCalcul = {
|
|
27
|
+
besoinChauffageHP: Object.fromEntries(mois_liste.map((m) => [m, 10])),
|
|
28
|
+
besoinChauffageDepensierHP: Object.fromEntries(mois_liste.map((m) => [m, 12]))
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Générateur ECS seul (usage 2), en volume chauffé, Qp0 = 100 W */
|
|
32
|
+
function generateurEcs(de = {}, di = { qp0: 100 }) {
|
|
33
|
+
return {
|
|
34
|
+
donnee_entree: {
|
|
35
|
+
enum_usage_generateur_id: 2,
|
|
36
|
+
position_volume_chauffe: 1,
|
|
37
|
+
presence_ventouse: 0,
|
|
38
|
+
...de
|
|
39
|
+
},
|
|
40
|
+
donnee_intermediaire: di
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Logement avec une installation ECS et, optionnellement, des générateurs de chauffage */
|
|
45
|
+
function logement(generateursEcs = [], generateursCh = []) {
|
|
46
|
+
return {
|
|
47
|
+
donnees_de_calcul: donneesDeCalcul,
|
|
48
|
+
installation_ecs_collection: {
|
|
49
|
+
installation_ecs: [{ generateur_ecs_collection: { generateur_ecs: generateursEcs } }]
|
|
50
|
+
},
|
|
51
|
+
installation_chauffage_collection: {
|
|
52
|
+
installation_chauffage: [
|
|
53
|
+
{ generateur_chauffage_collection: { generateur_chauffage: generateursCh } }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
tvStore = new FrTvStore();
|
|
61
|
+
service = new PerteChRecupService(tvStore);
|
|
62
|
+
// Nref constant : 100 h (conventionnel) / 120 h (dépensier) pour chacun des 12 mois
|
|
63
|
+
vi.spyOn(tvStore, 'getData').mockImplementation((type) => (type === 'nref21' ? 120 : 100));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Générateurs ECS assurant l’ECS uniquement (issue #153)', () => {
|
|
67
|
+
test('ECS seul sans ventouse : 0,48 * 0,5 * Qp0 * Nref * 1790 / 8760 par mois', () => {
|
|
68
|
+
const result = service.execute(ctx, logement([generateurEcs()]));
|
|
69
|
+
|
|
70
|
+
// 12 * 0.48 * 0.5 * 100 * (100 * 1790 / 8760) / 1000 = 5.884931506... kWh
|
|
71
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(5.884931506849315, 9);
|
|
72
|
+
// 12 * 0.48 * 0.5 * 100 * (120 * 1790 / 8760) / 1000 = 7.061917808... kWh
|
|
73
|
+
expect(result.pertes_generateur_ch_recup_depensier).toBeCloseTo(7.061917808219178, 9);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('ECS seul avec ventouse : Cper = 0,75', () => {
|
|
77
|
+
const result = service.execute(ctx, logement([generateurEcs({ presence_ventouse: 1 })]));
|
|
78
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(1.5 * 5.884931506849315, 9);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test.each([
|
|
82
|
+
{ cas: 'hors volume chauffé', de: { position_volume_chauffe: 0 }, di: { qp0: 100 } },
|
|
83
|
+
{
|
|
84
|
+
cas: 'position non renseignée',
|
|
85
|
+
de: { position_volume_chauffe: undefined },
|
|
86
|
+
di: { qp0: 100 }
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
cas: "usage 'chauffage + ecs' (déjà compté)",
|
|
90
|
+
de: { enum_usage_generateur_id: 3 },
|
|
91
|
+
di: { qp0: 100 }
|
|
92
|
+
},
|
|
93
|
+
{ cas: 'Qp0 nul', de: {}, di: { qp0: 0 } },
|
|
94
|
+
{ cas: 'Qp0 absent', de: {}, di: null }
|
|
95
|
+
])('pas de récupération pour un générateur ECS $cas', ({ de, di }) => {
|
|
96
|
+
const result = service.execute(ctx, logement([generateurEcs(de, di)]));
|
|
97
|
+
expect(result.pertes_generateur_ch_recup).toBe(0);
|
|
98
|
+
expect(result.pertes_generateur_ch_recup_depensier).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('logement sans installation ECS ni chauffage : aucune récupération', () => {
|
|
102
|
+
const result = service.execute(ctx, { donnees_de_calcul: donneesDeCalcul });
|
|
103
|
+
expect(result.pertes_generateur_ch_recup).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('installation ECS sans collection de générateurs : aucune récupération', () => {
|
|
107
|
+
const result = service.execute(ctx, {
|
|
108
|
+
donnees_de_calcul: donneesDeCalcul,
|
|
109
|
+
installation_ecs_collection: { installation_ecs: [{}] }
|
|
110
|
+
});
|
|
111
|
+
expect(result.pertes_generateur_ch_recup).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Générateurs de chauffage', () => {
|
|
116
|
+
/** Générateur de chauffage en volume chauffé, Pn = 10 000 W, Qp0 = 100 W */
|
|
117
|
+
function generateurCh(de = {}) {
|
|
118
|
+
return {
|
|
119
|
+
donnee_entree: {
|
|
120
|
+
position_volume_chauffe: 1,
|
|
121
|
+
enum_type_generateur_ch_id: 85,
|
|
122
|
+
enum_usage_generateur_id: 1,
|
|
123
|
+
...de
|
|
124
|
+
},
|
|
125
|
+
donnee_intermediaire: { pn: 10000, qp0: 100 }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test('usage chauffage : Dperj = min(Nref, 1,3 * Bch / (0,3 * Pn))', () => {
|
|
130
|
+
const result = service.execute(ctx, logement([], [generateurCh()]));
|
|
131
|
+
// Dperj = min(100, 1.3 * 10 * 1000 / 3000) = 4.333.. h
|
|
132
|
+
// 12 * 0.48 * 0.5 * 100 * 4.333.. / 1000 = 1.248 kWh
|
|
133
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(1.248, 9);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('usage ECS : Dperj = Nref * 1790 / 8760', () => {
|
|
137
|
+
const result = service.execute(
|
|
138
|
+
ctx,
|
|
139
|
+
logement([], [generateurCh({ enum_usage_generateur_id: 2 })])
|
|
140
|
+
);
|
|
141
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(5.884931506849315, 9);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('usage chauffage + ECS avec ventouse : cumul des deux durées, Cper = 0,75', () => {
|
|
145
|
+
const result = service.execute(
|
|
146
|
+
ctx,
|
|
147
|
+
logement([], [generateurCh({ enum_usage_generateur_id: 3, presence_ventouse: 1 })])
|
|
148
|
+
);
|
|
149
|
+
// Dperj = min(100, 4.333.. + 20.4337..) ; 12 * 0.48 * 0.75 * 100 * Dperj / 1000
|
|
150
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(10.699397260273972, 9);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test.each([
|
|
154
|
+
{ cas: 'hors volume chauffé', de: { position_volume_chauffe: 0 } },
|
|
155
|
+
{ cas: 'à air chaud (50)', de: { enum_type_generateur_ch_id: 50 } },
|
|
156
|
+
{ cas: 'à air chaud (52)', de: { enum_type_generateur_ch_id: 52 } }
|
|
157
|
+
])('pas de récupération pour un générateur de chauffage $cas', ({ de }) => {
|
|
158
|
+
const result = service.execute(ctx, logement([], [generateurCh(de)]));
|
|
159
|
+
expect(result.pertes_generateur_ch_recup).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('installation de chauffage sans collection de générateurs : aucune récupération', () => {
|
|
163
|
+
const result = service.execute(ctx, {
|
|
164
|
+
donnees_de_calcul: donneesDeCalcul,
|
|
165
|
+
installation_chauffage_collection: { installation_chauffage: [{}] }
|
|
166
|
+
});
|
|
167
|
+
expect(result.pertes_generateur_ch_recup).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('cumul générateur de chauffage + générateur ECS seul', () => {
|
|
171
|
+
const result = service.execute(ctx, logement([generateurEcs()], [generateurCh()]));
|
|
172
|
+
expect(result.pertes_generateur_ch_recup).toBeCloseTo(1.248 + 5.884931506849315, 9);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('un générateur ECS mixte (usage 3) n’est pas compté deux fois', () => {
|
|
176
|
+
const ch = generateurCh({ enum_usage_generateur_id: 3 });
|
|
177
|
+
const seul = service.execute(ctx, logement([], [ch]));
|
|
178
|
+
const avecEcsMixte = service.execute(
|
|
179
|
+
ctx,
|
|
180
|
+
logement([generateurEcs({ enum_usage_generateur_id: 3 })], [ch])
|
|
181
|
+
);
|
|
182
|
+
expect(avecEcsMixte.pertes_generateur_ch_recup).toBe(seul.pertes_generateur_ch_recup);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|