@open3cl/engine 1.8.7 → 1.8.9

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.
@@ -0,0 +1,476 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import {
3
+ conventionPuissanceNominale,
4
+ decalageDecimal,
5
+ inverserRendement,
6
+ nombreDecimales,
7
+ puissanceDepuisRpn,
8
+ puissanceRecalculeeDepuisRpn,
9
+ qp0Coherent,
10
+ valeurCoherente
11
+ } from './13.2_generateur_combustion_pn.js';
12
+
13
+ /**
14
+ * Module pur (aucune dépendance importée) : les caractéristiques du générateur sont fournies par
15
+ * une fonction injectée, remplacée ici par un double de test déterministe.
16
+ *
17
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2 et §17.2.1 (virtualisation)
18
+ */
19
+
20
+ /**
21
+ * Double de test des caractéristiques d'une chaudière : rpn = (84 + 2 log10(Pn collectif kW)) / 100
22
+ * et qp0 = 1 % de Pn collectif, ramené au logement (× ratio).
23
+ */
24
+ const caracteristiquesChaudiere = vi.fn((pn, ratio) => {
25
+ const pnCollectifKw = pn / ratio / 1000;
26
+ return {
27
+ rpn: (84 + 2 * Math.log10(pnCollectifKw)) / 100,
28
+ qp0: 0.01 * pnCollectifKw * 1000 * ratio
29
+ };
30
+ });
31
+
32
+ describe('nombreDecimales', () => {
33
+ test.each([
34
+ [12, 0],
35
+ [0.87, 2],
36
+ [0.868943, 6],
37
+ [1e-7, 7],
38
+ [1.5e-7, 8],
39
+ [1.5e3, 0]
40
+ ])('%s possède %s décimale(s)', (valeur, attendu) => {
41
+ expect(nombreDecimales(valeur)).toBe(attendu);
42
+ });
43
+ });
44
+
45
+ describe('valeurCoherente', () => {
46
+ test('valeur arrondie à 2 décimales : tolérance d’une demi-unité (0,005)', () => {
47
+ expect(valeurCoherente(0.87, 0.8749)).toBe(true);
48
+ expect(valeurCoherente(0.87, 0.8651)).toBe(true);
49
+ expect(valeurCoherente(0.87, 0.876)).toBe(false);
50
+ });
51
+
52
+ test('valeur arrondie à 1 décimale : la tolérance reste bornée à 0,005', () => {
53
+ expect(valeurCoherente(0.9, 0.904)).toBe(true);
54
+ expect(valeurCoherente(0.9, 0.908)).toBe(false);
55
+ });
56
+
57
+ test('valeur non arrondie : tolérance relative de 0,1 % (troncature de la puissance)', () => {
58
+ expect(valeurCoherente(0.868943160626844, 0.8696)).toBe(true);
59
+ expect(valeurCoherente(0.868943160626844, 0.8705)).toBe(false);
60
+ });
61
+
62
+ test('grande valeur entière : tolérance relative de 0,1 %', () => {
63
+ expect(valeurCoherente(2400, 2402)).toBe(true);
64
+ expect(valeurCoherente(2400, 2403)).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('qp0Coherent', () => {
69
+ test('qp0 stocké en W', () => {
70
+ expect(qp0Coherent(240, 240)).toBe(true);
71
+ });
72
+
73
+ test('qp0 stocké en kW (certains logiciels)', () => {
74
+ expect(qp0Coherent(0.24, 240)).toBe(true);
75
+ });
76
+
77
+ test('qp0 incohérent dans les deux unités', () => {
78
+ expect(qp0Coherent(0.5, 240)).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('conventionPuissanceNominale - cas sans détection', () => {
83
+ test.each([
84
+ ['installation individuelle (ratio = 1)', 23000, 1, { rpn: 0.87 }],
85
+ ['ratio absent', 23000, undefined, { rpn: 0.87 }],
86
+ ['ratio nul', 23000, 0, { rpn: 0.87 }],
87
+ ['puissance absente', 0, 0.1, { rpn: 0.87 }],
88
+ ['rpn du DPE absent', 23000, 0.1, {}],
89
+ ['rpn du DPE non numérique', 23000, 0.1, { rpn: '0.87' }],
90
+ ['valeurs du DPE absentes', 23000, 0.1, undefined]
91
+ ])('%s : convention individualisée conservée', (_, pn, ratio, valeursDpe) => {
92
+ const caracteristiques = vi.fn();
93
+ expect(conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiques, true)).toEqual({
94
+ pn,
95
+ ratio,
96
+ convention: 'individualisee'
97
+ });
98
+ expect(caracteristiques).not.toHaveBeenCalled();
99
+ });
100
+ });
101
+
102
+ describe('conventionPuissanceNominale - détection de la convention du logiciel', () => {
103
+ const ratio = 0.1;
104
+ const pn = 23000;
105
+
106
+ test('rpn cohérent avec pn / ratio : pn est bien la puissance virtualisée (convention 3CL)', () => {
107
+ // rpn sur Pn collectif = 230 kW
108
+ const rpn = caracteristiquesChaudiere(pn, ratio).rpn;
109
+ expect(
110
+ conventionPuissanceNominale(pn, ratio, { rpn }, caracteristiquesChaudiere, false)
111
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
112
+ });
113
+
114
+ test('rpn non recalculé (caractéristique saisie) : convention individualisée conservée', () => {
115
+ const caracteristiques = vi.fn(() => ({ qp0: 100 }));
116
+ expect(conventionPuissanceNominale(pn, ratio, { rpn: 0.87 }, caracteristiques, false)).toEqual({
117
+ pn,
118
+ ratio,
119
+ convention: 'individualisee'
120
+ });
121
+ expect(caracteristiques).toHaveBeenCalledTimes(1);
122
+ });
123
+
124
+ test('rpn incohérent avec les deux hypothèses : convention individualisée conservée', () => {
125
+ expect(
126
+ conventionPuissanceNominale(pn, ratio, { rpn: 0.5 }, caracteristiquesChaudiere, true)
127
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
128
+ });
129
+
130
+ test('rpn et qp0 calculés par le logiciel sur pn collectif, qp0 virtualisé : pn ramené à Pe = pn × ratio', () => {
131
+ // rpn sur 23 kW, qp0 = 1 % × 23 kW × 0,1 = 23 W
132
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 23 };
133
+ const resultat = conventionPuissanceNominale(
134
+ pn,
135
+ ratio,
136
+ valeursDpe,
137
+ caracteristiquesChaudiere,
138
+ true
139
+ );
140
+ expect(resultat.convention).toBe('collective_virtualisee');
141
+ expect(resultat.pn).toBeCloseTo(2300, 9);
142
+ expect(resultat.ratio).toBe(ratio);
143
+ // Hypothèse virtualisée évaluée sur Pe = pn × ratio
144
+ expect(caracteristiquesChaudiere).toHaveBeenCalledWith(pn * ratio, ratio);
145
+ });
146
+
147
+ test('qp0 du DPE absent : rpn suffit à établir que pn est la puissance collective', () => {
148
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn };
149
+ expect(
150
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
151
+ ).toMatchObject({ convention: 'collective_virtualisee', ratio });
152
+ });
153
+
154
+ test('qp0 non recalculé (méthode de saisie 4/5) : rpn suffit', () => {
155
+ const rpnCollectif = caracteristiquesChaudiere(pn, 1).rpn;
156
+ const caracteristiques = (p, r) => ({ rpn: caracteristiquesChaudiere(p, r).rpn });
157
+ expect(
158
+ conventionPuissanceNominale(
159
+ pn,
160
+ ratio,
161
+ { rpn: rpnCollectif, qp0: 999 },
162
+ caracteristiques,
163
+ true
164
+ )
165
+ ).toMatchObject({ convention: 'collective_virtualisee' });
166
+ });
167
+
168
+ test('logiciel n’ayant pas virtualisé qp0 (qp0 en kW sur pn) + bug_for_bug_compat : calcul non virtualisé reproduit', () => {
169
+ // qp0 = 1 % × 23 kW = 0,23 kW, non multiplié par le ratio
170
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 0.23 };
171
+ expect(
172
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
173
+ ).toEqual({ pn, ratio: 1, convention: 'collective_non_virtualisee' });
174
+ });
175
+
176
+ test('logiciel n’ayant pas virtualisé qp0 sans bug_for_bug_compat : application de la méthode (pn × ratio)', () => {
177
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 230 };
178
+ const resultat = conventionPuissanceNominale(
179
+ pn,
180
+ ratio,
181
+ valeursDpe,
182
+ caracteristiquesChaudiere,
183
+ false
184
+ );
185
+ expect(resultat.convention).toBe('collective_virtualisee');
186
+ expect(resultat.pn).toBeCloseTo(2300, 9);
187
+ expect(resultat.ratio).toBe(ratio);
188
+ });
189
+
190
+ test('qp0 incohérent avec les deux hypothèses : convention individualisée conservée', () => {
191
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 5000 };
192
+ expect(
193
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
194
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
195
+ });
196
+
197
+ test('qp0 non recalculable dans l’hypothèse non virtualisée : convention individualisée conservée', () => {
198
+ const caracteristiques = (p, r) =>
199
+ r === 1 ? { rpn: 0.8 } : { ...caracteristiquesChaudiere(p, r) };
200
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 5000 };
201
+ expect(conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiques, true)).toEqual({
202
+ pn,
203
+ ratio,
204
+ convention: 'individualisee'
205
+ });
206
+ });
207
+
208
+ test('cas réel DPEWin 2415E2886950G (rpn = 0,87 ; qp0 = 0,276 kW) avec une table réaliste', () => {
209
+ // Chaudière gaz standard : rpn = 84 + 2 log(Pn), qp0 = 1,2 % de Pn
210
+ const caracteristiques = (p, r) => ({
211
+ rpn: (84 + 2 * Math.log10(p / r / 1000)) / 100,
212
+ qp0: 0.012 * p
213
+ });
214
+ const resultat = conventionPuissanceNominale(
215
+ 23000,
216
+ 0.1150571,
217
+ { rpn: 0.87, qp0: 0.276 },
218
+ caracteristiques,
219
+ true
220
+ );
221
+ expect(resultat).toEqual({ pn: 23000, ratio: 1, convention: 'collective_non_virtualisee' });
222
+ });
223
+ });
224
+
225
+ /**
226
+ * 4e hypothèse : pn recalculé à partir du rpn stocké dans le DPE (issue #124, DPE 2592E2935275X).
227
+ * Double de test d'une chaudière gaz à condensation 2001-2015 : rpn = 91 + log10(Pn),
228
+ * rpint = 97 + log10(Pn) (Pn collectif en kW), qp0 = 1 % de la puissance saisie.
229
+ */
230
+ const condensation = {
231
+ rpn: (pnCollectif) => (91 + Math.log10(pnCollectif / 1000)) / 100,
232
+ caracteristiques: (pn, ratio) => ({
233
+ rpn: (91 + Math.log10(pn / ratio / 1000)) / 100,
234
+ rpint: (97 + Math.log10(pn / ratio / 1000)) / 100,
235
+ qp0: 0.01 * pn
236
+ })
237
+ };
238
+ const formuleRpnCondensation = () => condensation.rpn;
239
+
240
+ describe('inverserRendement', () => {
241
+ test('formule « A + B × log10(Pn) » : Pn retrouvé (W)', () => {
242
+ // rpn = 91 + log10(696) % -> Pn = 696 kW
243
+ expect(inverserRendement((91 + Math.log10(696)) / 100, condensation.rpn)).toBeCloseTo(
244
+ 696000,
245
+ 6
246
+ );
247
+ const formule = (p) => (84 + 2 * Math.log10(p / 1000)) / 100;
248
+ expect(inverserRendement(formule(23000), formule)).toBeCloseTo(23000, 6);
249
+ });
250
+
251
+ test('rendement indépendant de Pn (B = 0) : non inversible', () => {
252
+ expect(inverserRendement(0.9, () => 0.9)).toBeUndefined();
253
+ });
254
+
255
+ test('formule non affine en log10(Pn) : non inversible', () => {
256
+ expect(inverserRendement(0.9, (p) => (80 + Math.log10(p / 1000) ** 2) / 100)).toBeUndefined();
257
+ });
258
+
259
+ test('formule non évaluable : non inversible', () => {
260
+ expect(inverserRendement(0.9, () => NaN)).toBeUndefined();
261
+ });
262
+ });
263
+
264
+ describe('puissanceDepuisRpn', () => {
265
+ /**
266
+ * Table à deux lignes selon le critère de puissance : Pn ≤ 70 kW -> 84 + 2 log10(Pn),
267
+ * Pn > 70 kW -> 91 + log10(Pn).
268
+ */
269
+ const petite = (p) => (84 + 2 * Math.log10(p / 1000)) / 100;
270
+ const formuleSelonPn = (pnCollectif) => (pnCollectif <= 70000 ? petite : condensation.rpn);
271
+
272
+ test('ligne de table stable : Pn obtenu en une inversion', () => {
273
+ const formuleRpn = vi.fn(formuleSelonPn);
274
+ expect(puissanceDepuisRpn(500000, condensation.rpn(696000), formuleRpn)).toBeCloseTo(696000, 6);
275
+ expect(formuleRpn).toHaveBeenCalledTimes(2);
276
+ });
277
+
278
+ test('première ligne inadaptée : ligne re-sélectionnée sur le Pn obtenu', () => {
279
+ // Départ sur la ligne « Pn ≤ 70 kW », Pn réel 696 kW
280
+ const formuleRpn = vi.fn(formuleSelonPn);
281
+ expect(puissanceDepuisRpn(24000, condensation.rpn(696000), formuleRpn)).toBeCloseTo(696000, 6);
282
+ expect(formuleRpn).toHaveBeenCalledTimes(4);
283
+ });
284
+
285
+ test('aucune ligne de table : abandon', () => {
286
+ expect(puissanceDepuisRpn(24000, 0.93, () => undefined)).toBeUndefined();
287
+ });
288
+
289
+ test('ligne sans formule rpn après re-sélection : abandon après 3 itérations', () => {
290
+ const formuleRpn = vi.fn((p) => (p === 24000 ? condensation.rpn : undefined));
291
+ expect(puissanceDepuisRpn(24000, 0.93, formuleRpn)).toBeUndefined();
292
+ });
293
+
294
+ test('rpn indépendant de Pn : abandon', () => {
295
+ expect(puissanceDepuisRpn(24000, 0.9, () => () => 0.9)).toBeUndefined();
296
+ });
297
+
298
+ test('lignes incompatibles (Pn hors de la plage de chaque ligne) : abandon', () => {
299
+ // 0,93 -> 100 kW sur la ligne « Pn ≤ 70 kW » et 14 kW sur la ligne « Pn > 70 kW »
300
+ const grande = (p) => (70 + 20 * Math.log10(p / 1000)) / 100;
301
+ const formuleRpn = (p) => (p <= 70000 ? condensation.rpn : grande);
302
+ expect(puissanceDepuisRpn(24000, 0.93, formuleRpn)).toBeUndefined();
303
+ });
304
+ });
305
+
306
+ describe('decalageDecimal', () => {
307
+ test('puissance saisie décalée d’une puissance de dix (69600 W pour 696 kW)', () => {
308
+ expect(decalageDecimal(69600, 696000)).toBe(true);
309
+ expect(decalageDecimal(696, 696000)).toBe(true);
310
+ expect(decalageDecimal(2018.32, 201.832)).toBe(true);
311
+ });
312
+
313
+ test('puissance identique ou sans rapport : pas de décalage', () => {
314
+ expect(decalageDecimal(696000, 696000)).toBe(false);
315
+ expect(decalageDecimal(23000, 696000)).toBe(false);
316
+ });
317
+ });
318
+
319
+ describe('puissanceRecalculeeDepuisRpn', () => {
320
+ // Valeurs du DPE 2592E2935275X (WinDpe) : pn = 69600 W, Pn réel = 696 kW
321
+ const ratio = 0.00289987937273824;
322
+ const rpn = 0.938426100909721;
323
+ const rpint = 0.998426100909721;
324
+
325
+ test('rpint du DPE cohérent avec le Pn retrouvé : Pe = a × Pn retenu', () => {
326
+ const resultat = puissanceRecalculeeDepuisRpn(
327
+ 69600,
328
+ ratio,
329
+ { rpn, rpint, qp0: 2.01832 },
330
+ condensation.caracteristiques,
331
+ formuleRpnCondensation
332
+ );
333
+ expect(resultat.convention).toBe('puissance_depuis_rpn');
334
+ expect(resultat.ratio).toBe(ratio);
335
+ /**
336
+ * Référence de régression : le logiciel a calculé rpn sur Pn = Pe / a avec Pe = 2018,32 W
337
+ * (arrondi au centième), soit Pn = 696,0014 kW ; on retrouve exactement Pe = 2018,32 W.
338
+ */
339
+ expect(resultat.pnCollectif).toBeCloseTo(2018.32 / ratio, 3);
340
+ expect(resultat.pn).toBeCloseTo(2018.32, 6);
341
+ });
342
+
343
+ test('rpint du DPE incohérent avec le Pn retrouvé : abandon', () => {
344
+ expect(
345
+ puissanceRecalculeeDepuisRpn(
346
+ 69600,
347
+ ratio,
348
+ { rpn, rpint: 0.95, qp0: 20.1832 },
349
+ condensation.caracteristiques,
350
+ formuleRpnCondensation
351
+ )
352
+ ).toBeUndefined();
353
+ });
354
+
355
+ test('sans rpint : qp0 virtualisé du DPE (W) confirme le Pn retrouvé', () => {
356
+ expect(
357
+ puissanceRecalculeeDepuisRpn(
358
+ 12345,
359
+ ratio,
360
+ { rpn, qp0: 20.1832 },
361
+ condensation.caracteristiques,
362
+ formuleRpnCondensation
363
+ )
364
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
365
+ });
366
+
367
+ test('sans rpint : qp0 non virtualisé du DPE (kW) confirme le Pn retrouvé', () => {
368
+ // qp0 = 1 % × 696 kW = 6,96 kW
369
+ expect(
370
+ puissanceRecalculeeDepuisRpn(
371
+ 12345,
372
+ ratio,
373
+ { rpn, qp0: 6.96 },
374
+ condensation.caracteristiques,
375
+ formuleRpnCondensation
376
+ )
377
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
378
+ });
379
+
380
+ test('sans rpint ni qp0 cohérent : pn saisi décalé d’une puissance de dix de Pn', () => {
381
+ expect(
382
+ puissanceRecalculeeDepuisRpn(
383
+ 69600,
384
+ ratio,
385
+ { rpn, qp0: 2.01832 },
386
+ condensation.caracteristiques,
387
+ formuleRpnCondensation
388
+ )
389
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
390
+ });
391
+
392
+ test('sans rpint ni qp0 : pn saisi décalé d’une puissance de dix de Pe', () => {
393
+ expect(
394
+ puissanceRecalculeeDepuisRpn(
395
+ 201.832,
396
+ ratio,
397
+ { rpn },
398
+ condensation.caracteristiques,
399
+ formuleRpnCondensation
400
+ )
401
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
402
+ });
403
+
404
+ test('aucune confirmation (rpint et qp0 non recalculés, pn sans rapport) : abandon', () => {
405
+ const caracteristiques = (pn, r) => ({ rpn: condensation.caracteristiques(pn, r).rpn });
406
+ expect(
407
+ puissanceRecalculeeDepuisRpn(
408
+ 12345,
409
+ ratio,
410
+ { rpn, rpint, qp0: 2.01832 },
411
+ caracteristiques,
412
+ formuleRpnCondensation
413
+ )
414
+ ).toBeUndefined();
415
+ });
416
+
417
+ test('rpn non inversible : abandon sans calcul des caractéristiques', () => {
418
+ const caracteristiques = vi.fn();
419
+ expect(
420
+ puissanceRecalculeeDepuisRpn(69600, ratio, { rpn }, caracteristiques, () => () => 0.9)
421
+ ).toBeUndefined();
422
+ expect(caracteristiques).not.toHaveBeenCalled();
423
+ });
424
+ });
425
+
426
+ describe('conventionPuissanceNominale - 4e hypothèse (pn recalculé depuis rpn)', () => {
427
+ const ratio = 0.00289987937273824;
428
+ const valeursDpe = { rpn: 0.938426100909721, rpint: 0.998426100909721, qp0: 2.01832 };
429
+
430
+ test('cas réel WinDpe 2592E2935275X : pn ni Pe ni Pn -> Pe = a × Pn(rpn) = 2018,32 W', () => {
431
+ const resultat = conventionPuissanceNominale(
432
+ 69600,
433
+ ratio,
434
+ valeursDpe,
435
+ condensation.caracteristiques,
436
+ true,
437
+ formuleRpnCondensation
438
+ );
439
+ expect(resultat).toMatchObject({ convention: 'puissance_depuis_rpn', ratio });
440
+ expect(resultat.pn).toBeCloseTo(2018.32, 6);
441
+ });
442
+
443
+ test('formule rpn non fournie : convention individualisée conservée', () => {
444
+ expect(
445
+ conventionPuissanceNominale(69600, ratio, valeursDpe, condensation.caracteristiques, true)
446
+ ).toEqual({ pn: 69600, ratio, convention: 'individualisee' });
447
+ });
448
+
449
+ test('confirmation impossible : convention individualisée conservée', () => {
450
+ expect(
451
+ conventionPuissanceNominale(
452
+ 69600,
453
+ ratio,
454
+ { ...valeursDpe, rpint: 0.95 },
455
+ condensation.caracteristiques,
456
+ true,
457
+ formuleRpnCondensation
458
+ )
459
+ ).toEqual({ pn: 69600, ratio, convention: 'individualisee' });
460
+ });
461
+
462
+ test('pn cohérent avec Pe : la 4e hypothèse n’est pas évaluée', () => {
463
+ const formuleRpn = vi.fn(formuleRpnCondensation);
464
+ expect(
465
+ conventionPuissanceNominale(
466
+ 2018.32,
467
+ ratio,
468
+ valeursDpe,
469
+ condensation.caracteristiques,
470
+ true,
471
+ formuleRpn
472
+ )
473
+ ).toMatchObject({ convention: 'individualisee' });
474
+ expect(formuleRpn).not.toHaveBeenCalled();
475
+ });
476
+ });
@@ -14,11 +14,6 @@ export function calc_sse_j(bv_list, ets, ca, zc, mois) {
14
14
  return sseBaiesExt;
15
15
  }
16
16
 
17
- // Certaines vérandas sont dupliqués dans les DPE.
18
- if (Array.isArray(ets)) {
19
- ets = ets[0];
20
- }
21
-
22
17
  /**
23
18
  * 6.3 Traitement des espaces tampons solarisés
24
19
  * Sse_ver n'est calculée que pour les baies vitrées qui séparent le logement de l'espace tampon
@@ -28,6 +23,82 @@ export function calc_sse_j(bv_list, ets, ca, zc, mois) {
28
23
  return sseBaiesExt;
29
24
  }
30
25
 
26
+ /**
27
+ * Plusieurs espaces tampons solarisés (issue #101) : T, bver, Sst et Ssd sont propres à chaque
28
+ * véranda. Sse = Sse_ext + Σ(v) Sse_veranda_v, chaque baie adjacence 10 n'étant comptée que dans
29
+ * la véranda sur laquelle elle donne (baie.reference_lnc = ets.reference).
30
+ */
31
+ const etsList = dedoublonnerEts(Array.isArray(ets) ? ets : [ets]);
32
+ const baiesParEts = rattacherBaiesAuxEts(baiesAdjVeranda, etsList);
33
+
34
+ return etsList.reduce(
35
+ (acc, etsItem, idx) => acc + calc_sse_veranda_j(etsItem, baiesParEts[idx], zc, mois),
36
+ sseBaiesExt
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Certaines vérandas sont dupliquées dans les DPE : une même véranda (même référence, ou à défaut
42
+ * même contenu) ne doit être comptée qu'une seule fois.
43
+ *
44
+ * @param etsList {EtsItem[]}
45
+ * @returns {EtsItem[]}
46
+ */
47
+ export function dedoublonnerEts(etsList) {
48
+ const vues = new Set();
49
+ return etsList.filter((etsItem) => {
50
+ if (!etsItem) return false;
51
+ const reference = etsItem.donnee_entree?.reference;
52
+ const cle = reference ? `ref:${reference}` : `json:${JSON.stringify(etsItem)}`;
53
+ if (vues.has(cle)) return false;
54
+ vues.add(cle);
55
+ return true;
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Rattache chaque baie vitrée adjacence 10 à l'espace tampon sur lequel elle donne
61
+ * (baie.donnee_entree.reference_lnc = ets.donnee_entree.reference).
62
+ *
63
+ * Une baie sans lien exploitable (reference_lnc absente ou ne correspondant à aucune véranda) est
64
+ * rattachée à la première véranda : c'est le comportement historique, et le seul possible quand le
65
+ * DPE ne comporte qu'une véranda. La méthode ne permet pas de faire mieux pour une donnée manquante.
66
+ *
67
+ * @param baiesAdjVeranda {BaieVitreeItem[]}
68
+ * @param etsList {EtsItem[]}
69
+ * @returns {BaieVitreeItem[][]} baies par véranda, dans l'ordre de etsList
70
+ */
71
+ export function rattacherBaiesAuxEts(baiesAdjVeranda, etsList) {
72
+ const baiesParEts = etsList.map(() => []);
73
+ // Aucune véranda exploitable : les baies adjacence 10 ne peuvent être rattachées à aucun ETS
74
+ if (etsList.length === 0) {
75
+ return baiesParEts;
76
+ }
77
+ baiesAdjVeranda.forEach((bv) => {
78
+ const referenceLnc = bv.donnee_entree.reference_lnc;
79
+ const idx = referenceLnc
80
+ ? etsList.findIndex((etsItem) => etsItem.donnee_entree?.reference === referenceLnc)
81
+ : -1;
82
+ baiesParEts[idx === -1 ? 0 : idx].push(bv);
83
+ });
84
+ return baiesParEts;
85
+ }
86
+
87
+ /**
88
+ * Surface sud équivalente apportée par une véranda sur le mois j (§6.3).
89
+ *
90
+ * @param ets {EtsItem}
91
+ * @param baiesAdjVeranda {BaieVitreeItem[]} baies séparant le logement de cette véranda
92
+ * @param zc {string}
93
+ * @param mois {string}
94
+ * @returns {number}
95
+ */
96
+ function calc_sse_veranda_j(ets, baiesAdjVeranda, zc, mois) {
97
+ // Véranda sans baie vitrée vers le logement : aucun apport solaire
98
+ if (baiesAdjVeranda.length === 0) {
99
+ return 0;
100
+ }
101
+
31
102
  const bver = ets.donnee_intermediaire.bver;
32
103
  const T = ets.donnee_intermediaire.coef_transparence_ets;
33
104
 
@@ -80,7 +151,7 @@ export function calc_sse_j(bv_list, ets, ca, zc, mois) {
80
151
  */
81
152
  const SseVerandaj = Ssdj + ssIndj * bver;
82
153
 
83
- return sseBaiesExt + SseVerandaj;
154
+ return SseVerandaj;
84
155
  }
85
156
 
86
157
  /**