@open3cl/engine 1.8.8 → 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/13.2_generateur_combustion.js +116 -31
- package/13.2_generateur_combustion.spec.js +286 -2
- package/13.2_generateur_combustion_pn.js +248 -0
- package/13.2_generateur_combustion_pn.spec.js +476 -0
- 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 +24 -24
- package/conso.js +178 -4
- package/conso.spec.js +187 -1
- package/engine.js +7 -4
- 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/utils.js +5 -0
- package/utils.spec.js +53 -0
|
@@ -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
|
+
});
|
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
|
+
});
|