@open3cl/engine 1.8.13 → 1.8.15
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 +69 -7
- package/13.2_generateur_combustion.spec.js +196 -0
- package/13.2_generateur_combustion_ch.js +164 -3
- package/13.2_generateur_combustion_ch.spec.js +286 -2
- package/README.md +24 -24
- package/engine.js +1 -1
- package/features/engine/domain/ch/emetteur-ch.service.js +164 -40
- package/features/engine/domain/ch/emetteur-ch.service.spec.js +240 -2
- package/features/engine/domain/ch/generateur-ch.service.js +15 -1
- package/features/engine/domain/ch/generateur-ch.service.spec.js +81 -0
- package/package.json +1 -1
|
@@ -38,8 +38,14 @@ vi.mock('./utils.js', () => ({
|
|
|
38
38
|
requestInputID: vi.fn((de, du, field) => de[`enum_${field}_id`])
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
const {
|
|
42
|
-
|
|
41
|
+
const {
|
|
42
|
+
tv_temp_fonc_30_100,
|
|
43
|
+
tempDistributionChPourTempFonc,
|
|
44
|
+
calc_generateur_combustion_ch,
|
|
45
|
+
periodeEmetteursDeduiteDuDpe,
|
|
46
|
+
periodeEmetteurAnneeConstruction,
|
|
47
|
+
PERIODES_INSTALLATION_EMETTEUR
|
|
48
|
+
} = await import('./13.2_generateur_combustion_ch.js');
|
|
43
49
|
const { tv } = await import('./utils.js');
|
|
44
50
|
|
|
45
51
|
beforeEach(() => {
|
|
@@ -194,6 +200,284 @@ describe('tv_temp_fonc_30_100 - températures de fonctionnement', () => {
|
|
|
194
200
|
);
|
|
195
201
|
});
|
|
196
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Issue #220 - période d'installation des émetteurs absente du DPE.
|
|
205
|
+
*
|
|
206
|
+
* Mode strict : « Si l'année d'installation des émetteurs est inconnue, prendre l'année de
|
|
207
|
+
* construction du bâtiment. » En mode bug_for_bug_compat uniquement, la période est retrouvée à
|
|
208
|
+
* partir des temp_fonc_30 / temp_fonc_100 d'origine stockées dans le DPE (règle de l'expert).
|
|
209
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.5
|
|
210
|
+
*/
|
|
211
|
+
describe('tv_temp_fonc_30_100 - période des émetteurs absente (issue #220)', () => {
|
|
212
|
+
/**
|
|
213
|
+
* Table de test (contrôlée, indépendante des tables réelles) pour une chaudière à condensation :
|
|
214
|
+
* [température de distribution][période] -> [id, temp_fonc_30, temp_fonc_100].
|
|
215
|
+
*/
|
|
216
|
+
const TABLE = {
|
|
217
|
+
2: {
|
|
218
|
+
'avant 1981': ['1', '32', '60'],
|
|
219
|
+
'entre 1981 et 2000': ['4', '24.5', '35'],
|
|
220
|
+
'après 2000': ['7', '24.5', '35']
|
|
221
|
+
},
|
|
222
|
+
3: {
|
|
223
|
+
'avant 1981': ['2', '38', '80'],
|
|
224
|
+
'entre 1981 et 2000': ['5', '35', '70'],
|
|
225
|
+
'après 2000': ['8', '32', '60']
|
|
226
|
+
},
|
|
227
|
+
4: {
|
|
228
|
+
'avant 1981': ['3', '38', '80'],
|
|
229
|
+
'entre 1981 et 2000': ['6', '35', '70'],
|
|
230
|
+
'après 2000': ['9', '35', '70']
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function tvTable(table, matcher) {
|
|
235
|
+
const ligne = TABLE[matcher.enum_temp_distribution_ch_id]?.[matcher.periode_emetteurs];
|
|
236
|
+
if (!ligne) return null;
|
|
237
|
+
return table === 'temp_fonc_30'
|
|
238
|
+
? { tv_temp_fonc_30_id: `30-${ligne[0]}`, temp_fonc_30: ligne[1] }
|
|
239
|
+
: { tv_temp_fonc_100_id: `100-${ligne[0]}`, temp_fonc_100: ligne[2] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function emetteur(tempDistribution, periode) {
|
|
243
|
+
const de = { enum_temp_distribution_ch_id: tempDistribution };
|
|
244
|
+
if (periode) de.periode_installation_emetteur = periode;
|
|
245
|
+
return { donnee_entree: de, donnee_utilisateur: {} };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const genCondensation = () => ({ enum_type_generateur_ch_id: '97', description: 'chaudière' });
|
|
249
|
+
|
|
250
|
+
let warnSpy;
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
tv.mockImplementation(tvTable);
|
|
253
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
254
|
+
return () => warnSpy.mockRestore();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('T1 strict : période absente, construction 1900 -> « avant 1981 » (38/80), valeurs du DPE ignorées', () => {
|
|
258
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60 };
|
|
259
|
+
const de = genCondensation();
|
|
260
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3')], 1900);
|
|
261
|
+
// Valeurs DPE (32) < table (38) : la table l'emporte, aucune déduction en mode strict
|
|
262
|
+
expect(di.temp_fonc_30).toBe(38);
|
|
263
|
+
expect(di.temp_fonc_100).toBe(80);
|
|
264
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-2');
|
|
265
|
+
expect(de.tv_temp_fonc_100_id).toBe('100-2');
|
|
266
|
+
expect(tv).not.toHaveBeenCalledWith(
|
|
267
|
+
'temp_fonc_30',
|
|
268
|
+
expect.objectContaining({ periode_emetteurs: 'après 2000' })
|
|
269
|
+
);
|
|
270
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('T2 bug_for_bug_compat : DPE à 32/60 -> période « après 2000 » déduite, valeurs et ids issus des tables', () => {
|
|
274
|
+
state.bug = true;
|
|
275
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60 };
|
|
276
|
+
const de = genCondensation();
|
|
277
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3')], 1900);
|
|
278
|
+
expect(di.temp_fonc_30).toBe(32);
|
|
279
|
+
expect(di.temp_fonc_100).toBe(60);
|
|
280
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-8');
|
|
281
|
+
expect(de.tv_temp_fonc_100_id).toBe('100-8');
|
|
282
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
283
|
+
expect.stringContaining(
|
|
284
|
+
"période d'installation des émetteurs déduite des données intermédiaires du DPE : après 2000"
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('T2 bis bug_for_bug_compat : valeurs du DPE sous forme de chaînes acceptées', () => {
|
|
290
|
+
state.bug = true;
|
|
291
|
+
const di = { temp_fonc_30: '32', temp_fonc_100: '60' };
|
|
292
|
+
tv_temp_fonc_30_100(di, genCondensation(), {}, [emetteur('3')], 1900);
|
|
293
|
+
expect(di.temp_fonc_30).toBe(32);
|
|
294
|
+
expect(di.temp_fonc_100).toBe(60);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('T3 bug_for_bug_compat : période saisie « avant 1981 » jamais écrasée (38/80 conservé)', () => {
|
|
298
|
+
state.bug = true;
|
|
299
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60 };
|
|
300
|
+
const de = genCondensation();
|
|
301
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3', 'avant 1981')], 1900);
|
|
302
|
+
expect(di.temp_fonc_30).toBe(38);
|
|
303
|
+
expect(di.temp_fonc_100).toBe(80);
|
|
304
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-2');
|
|
305
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('T4 bug_for_bug_compat : distribution haute, construction 1990, DPE 35/70 -> 2 candidates, période de construction retenue', () => {
|
|
309
|
+
state.bug = true;
|
|
310
|
+
const di = { temp_fonc_30: 35, temp_fonc_100: 70 };
|
|
311
|
+
const de = genCondensation();
|
|
312
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('4')], 1990);
|
|
313
|
+
expect(di.temp_fonc_30).toBe(35);
|
|
314
|
+
expect(di.temp_fonc_100).toBe(70);
|
|
315
|
+
// « entre 1981 et 2000 » (ids 6) et non « après 2000 » (ids 9)
|
|
316
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-6');
|
|
317
|
+
expect(de.tv_temp_fonc_100_id).toBe('100-6');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('T5 bug_for_bug_compat : DPE 33/62 hors tables -> repli sur l’année de construction et avertissement', () => {
|
|
321
|
+
state.bug = true;
|
|
322
|
+
const di = { temp_fonc_30: 33, temp_fonc_100: 62 };
|
|
323
|
+
const de = genCondensation();
|
|
324
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3')], 1990);
|
|
325
|
+
// Repli strict : « entre 1981 et 2000 » -> 35/70 (> 33/62 donc retenu)
|
|
326
|
+
expect(di.temp_fonc_30).toBe(35);
|
|
327
|
+
expect(di.temp_fonc_100).toBe(70);
|
|
328
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-5');
|
|
329
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
330
|
+
expect.stringContaining("valeurs hors tables / donnée d'entrée incohérente")
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test.each([
|
|
335
|
+
['temp_fonc_100 absente', { temp_fonc_30: 32 }],
|
|
336
|
+
['temp_fonc_30 absente', { temp_fonc_100: 60 }],
|
|
337
|
+
['temp_fonc_30 non numérique', { temp_fonc_30: 'abc', temp_fonc_100: 60 }],
|
|
338
|
+
['temp_fonc_100 vide', { temp_fonc_30: 32, temp_fonc_100: '' }],
|
|
339
|
+
['temp_fonc_30 nulle', { temp_fonc_30: null, temp_fonc_100: 60 }]
|
|
340
|
+
])(
|
|
341
|
+
'T6 bug_for_bug_compat : %s -> aucune déduction (règle de l’année de construction)',
|
|
342
|
+
(_, di) => {
|
|
343
|
+
state.bug = true;
|
|
344
|
+
tv_temp_fonc_30_100(di, genCondensation(), {}, [emetteur('3')], 1900);
|
|
345
|
+
expect(tv).not.toHaveBeenCalledWith(
|
|
346
|
+
'temp_fonc_30',
|
|
347
|
+
expect.objectContaining({ periode_emetteurs: 'après 2000' })
|
|
348
|
+
);
|
|
349
|
+
expect(tv).not.toHaveBeenCalledWith(
|
|
350
|
+
'temp_fonc_30',
|
|
351
|
+
expect.objectContaining({ periode_emetteurs: 'entre 1981 et 2000' })
|
|
352
|
+
);
|
|
353
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
test('T7 bug_for_bug_compat : deux émetteurs sans période (moyenne + haute), DPE 35/70 -> même période pour tous, règle du maximum', () => {
|
|
358
|
+
state.bug = true;
|
|
359
|
+
const di = { temp_fonc_30: 35, temp_fonc_100: 70 };
|
|
360
|
+
const de = genCondensation();
|
|
361
|
+
/**
|
|
362
|
+
* avant 1981 : max(38, 38) / max(80, 80) = 38/80 -> rejetée
|
|
363
|
+
* entre 1981 et 2000 : max(35, 35) / max(70, 70) = 35/70 -> candidate
|
|
364
|
+
* après 2000 : max(32, 35) / max(60, 70) = 35/70 -> candidate
|
|
365
|
+
* Construction 1900 (« avant 1981 ») hors candidates -> la plus récente : « après 2000 ».
|
|
366
|
+
*/
|
|
367
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3'), emetteur('4')], 1900);
|
|
368
|
+
expect(di.temp_fonc_30).toBe(35);
|
|
369
|
+
expect(di.temp_fonc_100).toBe(70);
|
|
370
|
+
// Maximum atteint par l'émetteur « haute » en « après 2000 » (ids 9)
|
|
371
|
+
expect(de.tv_temp_fonc_30_id).toBe('30-9');
|
|
372
|
+
expect(de.tv_temp_fonc_100_id).toBe('100-9');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('bug_for_bug_compat : un émetteur saisi et un émetteur sans période -> seule la période absente est déduite', () => {
|
|
376
|
+
state.bug = true;
|
|
377
|
+
const di = { temp_fonc_30: 35, temp_fonc_100: 70 };
|
|
378
|
+
const de = genCondensation();
|
|
379
|
+
// Émetteur saisi « entre 1981 et 2000 » (moyenne : 35/70) + émetteur basse sans période
|
|
380
|
+
tv_temp_fonc_30_100(di, de, {}, [emetteur('3', 'entre 1981 et 2000'), emetteur('2')], 1900);
|
|
381
|
+
expect(di.temp_fonc_30).toBe(35);
|
|
382
|
+
expect(di.temp_fonc_100).toBe(70);
|
|
383
|
+
expect(tv).toHaveBeenCalledWith(
|
|
384
|
+
'temp_fonc_30',
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
enum_temp_distribution_ch_id: '3',
|
|
387
|
+
periode_emetteurs: 'entre 1981 et 2000'
|
|
388
|
+
})
|
|
389
|
+
);
|
|
390
|
+
expect(tv).not.toHaveBeenCalledWith(
|
|
391
|
+
'temp_fonc_30',
|
|
392
|
+
expect.objectContaining({
|
|
393
|
+
enum_temp_distribution_ch_id: '3',
|
|
394
|
+
periode_emetteurs: 'après 2000'
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('bug_for_bug_compat : émetteur sans réseau de distribution (id 1) exclu', () => {
|
|
400
|
+
state.bug = true;
|
|
401
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60 };
|
|
402
|
+
tv_temp_fonc_30_100(di, genCondensation(), {}, [emetteur('1'), emetteur('3')], 1900);
|
|
403
|
+
expect(di.temp_fonc_30).toBe(32);
|
|
404
|
+
expect(di.temp_fonc_100).toBe(60);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('bug_for_bug_compat : seul un émetteur sans réseau de distribution est sans période -> aucune déduction', () => {
|
|
408
|
+
state.bug = true;
|
|
409
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60 };
|
|
410
|
+
tv_temp_fonc_30_100(
|
|
411
|
+
di,
|
|
412
|
+
genCondensation(),
|
|
413
|
+
{},
|
|
414
|
+
[emetteur('1'), emetteur('3', 'avant 1981')],
|
|
415
|
+
1900
|
|
416
|
+
);
|
|
417
|
+
expect(di.temp_fonc_30).toBe(38);
|
|
418
|
+
expect(di.temp_fonc_100).toBe(80);
|
|
419
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('bug_for_bug_compat : une seule des deux valeurs reproduite -> période rejetée (tolérance 0,01 °C)', () => {
|
|
423
|
+
state.bug = true;
|
|
424
|
+
// 32/60.02 : « après 2000 » ne reproduit que temp_fonc_30 -> aucune candidate
|
|
425
|
+
const di = { temp_fonc_30: 32, temp_fonc_100: 60.02 };
|
|
426
|
+
tv_temp_fonc_30_100(di, genCondensation(), {}, [emetteur('3')], 1900);
|
|
427
|
+
expect(di.temp_fonc_30).toBe(38);
|
|
428
|
+
expect(di.temp_fonc_100).toBe(80);
|
|
429
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('valeurs hors tables'));
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('bug_for_bug_compat : écart inférieur à la tolérance accepté', () => {
|
|
433
|
+
state.bug = true;
|
|
434
|
+
const di = { temp_fonc_30: 32.005, temp_fonc_100: 59.995 };
|
|
435
|
+
tv_temp_fonc_30_100(di, genCondensation(), {}, [emetteur('3')], 1900);
|
|
436
|
+
expect(di.temp_fonc_30).toBe(32);
|
|
437
|
+
expect(di.temp_fonc_100).toBe(60);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('periodeEmetteursDeduiteDuDpe', () => {
|
|
442
|
+
const de = { enum_type_generateur_ch_id: '97' };
|
|
443
|
+
const em = [{ donnee_entree: { enum_temp_distribution_ch_id: '3' }, donnee_utilisateur: {} }];
|
|
444
|
+
|
|
445
|
+
test('aucune ligne de table trouvée pour une période : période non candidate', () => {
|
|
446
|
+
tv.mockReturnValue(null);
|
|
447
|
+
expect(periodeEmetteursDeduiteDuDpe(32, 60, de, em, 1900)).toBeNull();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('ligne temp_fonc_100 absente : période non candidate', () => {
|
|
451
|
+
tv.mockImplementation((table) =>
|
|
452
|
+
table === 'temp_fonc_30' ? { tv_temp_fonc_30_id: '1', temp_fonc_30: '32' } : null
|
|
453
|
+
);
|
|
454
|
+
expect(periodeEmetteursDeduiteDuDpe(32, 60, de, em, 1900)).toBeNull();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('toutes les périodes candidates : période de construction retenue', () => {
|
|
458
|
+
tv.mockImplementation((table) =>
|
|
459
|
+
table === 'temp_fonc_30'
|
|
460
|
+
? { tv_temp_fonc_30_id: '1', temp_fonc_30: '32' }
|
|
461
|
+
: { tv_temp_fonc_100_id: '1', temp_fonc_100: '60' }
|
|
462
|
+
);
|
|
463
|
+
expect(periodeEmetteursDeduiteDuDpe(32, 60, de, em, 1900)).toBe(
|
|
464
|
+
PERIODES_INSTALLATION_EMETTEUR[0]
|
|
465
|
+
);
|
|
466
|
+
expect(periodeEmetteursDeduiteDuDpe(32, 60, de, em, 2010)).toBe('après 2000');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('periodeEmetteurAnneeConstruction', () => {
|
|
471
|
+
test.each([
|
|
472
|
+
[1980, 'avant 1981'],
|
|
473
|
+
[1981, 'entre 1981 et 2000'],
|
|
474
|
+
[1999, 'entre 1981 et 2000'],
|
|
475
|
+
[2000, 'après 2000']
|
|
476
|
+
])('année de construction %s : %s', (ac, periode) => {
|
|
477
|
+
expect(periodeEmetteurAnneeConstruction(ac)).toBe(periode);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
197
481
|
describe('tempDistributionChPourTempFonc', () => {
|
|
198
482
|
test('émetteur sans type d’émission : température déclarée renvoyée telle quelle', () => {
|
|
199
483
|
expect(tempDistributionChPourTempFonc({ enum_temp_distribution_ch_id: '3' }, {})).toBe('3');
|
package/README.md
CHANGED
|
@@ -370,57 +370,57 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
370
370
|
|
|
371
371
|
<!-- CORPUS:START -->
|
|
372
372
|
|
|
373
|
-
> **Version `1.8.
|
|
373
|
+
> **Version `1.8.14`** · à publier · branche `main` · généré le 2026-10-02
|
|
374
374
|
> Seuil de tolérance **5%**
|
|
375
375
|
|
|
376
376
|
<table>
|
|
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>
|
|
381
|
-
<td align="center"><strong>
|
|
380
|
+
<td align="center"><strong>60 314</strong><br/><sub>DPE conformes</sub></td>
|
|
381
|
+
<td align="center"><strong>67,02 %</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> | **
|
|
389
|
-
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **
|
|
390
|
-
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,
|
|
391
|
-
| 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **
|
|
392
|
-
| 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **
|
|
393
|
-
| 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,
|
|
394
|
-
| 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,
|
|
387
|
+
| 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,36 %** | `█████████░░░░░░░░░░░` | 4 636 / 10 000 |
|
|
388
|
+
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,42 %** | `██████████████████░░` | 9 242 / 10 000 |
|
|
389
|
+
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,42 %** | `██████████████████░░` | 8 935 / 9 992 |
|
|
390
|
+
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,50 %** | `██████████████████░░` | 8 850 / 10 000 |
|
|
391
|
+
| 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **75,85 %** | `███████████████░░░░░` | 7 584 / 9 999 |
|
|
392
|
+
| 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,18 %** | `██████████████░░░░░░` | 7 118 / 10 000 |
|
|
393
|
+
| 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,67 %** | `█████████████░░░░░░░` | 6 266 / 9 999 |
|
|
394
|
+
| 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,44 %** | `██████████░░░░░░░░░░` | 4 944 / 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 |
|
|
396
396
|
|
|
397
397
|
<sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
|
|
398
398
|
|
|
399
399
|
#### Temps d’exécution
|
|
400
400
|
|
|
401
|
-
> Durée de l’appel à `calcul_3cl` par DPE, sur 89
|
|
401
|
+
> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs.
|
|
402
402
|
> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
|
|
403
403
|
|
|
404
404
|
<table>
|
|
405
405
|
<tr>
|
|
406
|
-
<td align="center"><strong>5,
|
|
407
|
-
<td align="center"><strong>4,
|
|
406
|
+
<td align="center"><strong>5,23 ms</strong><br/><sub>moyenne</sub></td>
|
|
407
|
+
<td align="center"><strong>4,55 ms</strong><br/><sub>médiane</sub></td>
|
|
408
408
|
<td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
|
|
409
|
-
<td align="center"><strong>
|
|
409
|
+
<td align="center"><strong>622,7 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)** | 3,
|
|
417
|
-
| **Logement individuel (2025)** |
|
|
418
|
-
| **Maison individuelle (2025)** | 6,
|
|
419
|
-
| **Immeuble · chauffage individuel** | 6,
|
|
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,22 ms | 3,76 ms | 0,22 ms | 34,7 ms | 7,72 ms | 11,0 ms |
|
|
416
|
+
| **Appartement · chauffage individuel (2025)** | 3,84 ms | 3,52 ms | 1,45 ms | 30,9 ms | 6,58 ms | 8,40 ms |
|
|
417
|
+
| **Logement individuel (2025)** | 4,70 ms | 4,15 ms | 1,59 ms | 35,3 ms | 8,68 ms | 12,1 ms |
|
|
418
|
+
| **Maison individuelle (2025)** | 6,54 ms | 5,89 ms | 1,98 ms | 41,2 ms | 11,9 ms | 17,0 ms |
|
|
419
|
+
| **Immeuble · chauffage individuel** | 6,29 ms | 5,33 ms | 0,11 ms | 132,2 ms | 12,8 ms | 20,0 ms |
|
|
420
|
+
| **Appartement · chauffage collectif (2025)** | 4,41 ms | 4,00 ms | 1,48 ms | 33,8 ms | 7,37 ms | 9,64 ms |
|
|
421
|
+
| **Immeuble · chauffage collectif** | 6,38 ms | 5,37 ms | 0,20 ms | 105,0 ms | 13,4 ms | 24,1 ms |
|
|
422
|
+
| **Immeuble · chauffage mixte** | 4,95 ms | 4,03 ms | 0,20 ms | 622,7 ms | 10,3 ms | 16,3 ms |
|
|
423
|
+
| **Individuel généré depuis l'immeuble (2026)** | 5,76 ms | 4,91 ms | 1,65 ms | 48,0 ms | 11,0 ms | 16,8 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.15';
|
|
37
37
|
|
|
38
38
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
39
39
|
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { ChTvStore } from '../../../dpe/infrastructure/ch/chTv.store.js';
|
|
2
2
|
import { inject } from 'dioma';
|
|
3
|
+
import { bug_for_bug_compat } from '../../../../utils.js';
|
|
4
|
+
import { logger } from '../../../../core/util/logger/log-service.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Périodes d'installation des émetteurs (enum_periode_installation_emetteur_id), de la plus
|
|
8
|
+
* ancienne à la plus récente.
|
|
9
|
+
*/
|
|
10
|
+
export const PERIODES_INSTALLATION_EMETTEUR = [1, 2, 3];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tolérance (°C) pour considérer qu'une température de fonctionnement issue des tables reproduit
|
|
14
|
+
* celle stockée dans le DPE.
|
|
15
|
+
*/
|
|
16
|
+
export const TOLERANCE_TEMP_FONC = 0.01;
|
|
17
|
+
|
|
18
|
+
function estNumerique(valeur) {
|
|
19
|
+
return (
|
|
20
|
+
valeur !== null && valeur !== undefined && valeur !== '' && Number.isFinite(Number(valeur))
|
|
21
|
+
);
|
|
22
|
+
}
|
|
3
23
|
|
|
4
24
|
/**
|
|
5
25
|
* Calcul des données des émetteurs de chauffage
|
|
@@ -28,69 +48,173 @@ export class EmetteurChService {
|
|
|
28
48
|
*
|
|
29
49
|
* @param ctx {Contexte}
|
|
30
50
|
* @param emetteurChauffage {EmetteurChauffage}
|
|
51
|
+
* @param periodeParDefaut {number|undefined} période à retenir si elle n'est pas saisie (déduite
|
|
52
|
+
* des données intermédiaires du DPE en mode bug_for_bug_compat, cf. issue #220)
|
|
31
53
|
*/
|
|
32
|
-
periodeInstallationEmetteur(ctx, emetteurChauffage) {
|
|
54
|
+
periodeInstallationEmetteur(ctx, emetteurChauffage, periodeParDefaut = undefined) {
|
|
33
55
|
const periodeInstallationEmetteur = parseInt(
|
|
34
56
|
emetteurChauffage.donnee_entree.enum_periode_installation_emetteur_id
|
|
35
57
|
);
|
|
36
58
|
|
|
37
59
|
if (!periodeInstallationEmetteur) {
|
|
60
|
+
if (periodeParDefaut) {
|
|
61
|
+
return periodeParDefaut;
|
|
62
|
+
}
|
|
38
63
|
return ctx.anneeConstruction < 1981 ? 1 : ctx.anneeConstruction < 2000 ? 2 : 3;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
return periodeInstallationEmetteur;
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Émetteurs pris en compte pour les températures de fonctionnement : ceux disposant d'un réseau
|
|
71
|
+
* de distribution (enum_temp_distribution_ch_id ≠ 1).
|
|
72
|
+
*
|
|
73
|
+
* @param emetteursChauffage {EmetteurChauffage[]}
|
|
74
|
+
* @return {EmetteurChauffage[]}
|
|
75
|
+
*/
|
|
76
|
+
#emetteursAvecDistribution(emetteursChauffage) {
|
|
77
|
+
return emetteursChauffage.filter(
|
|
78
|
+
(emetteurChauffage) =>
|
|
79
|
+
parseInt(emetteurChauffage.donnee_entree.enum_temp_distribution_ch_id) !== 1
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mode bug_for_bug_compat uniquement (issue #220).
|
|
85
|
+
*
|
|
86
|
+
* Certains logiciels calculent les températures de fonctionnement avec une période
|
|
87
|
+
* d'installation des émetteurs qu'ils n'exportent pas, mais stockent temp_fonc_30 /
|
|
88
|
+
* temp_fonc_100 dans les données intermédiaires du générateur. Chaque période candidate est
|
|
89
|
+
* affectée à tous les émetteurs sans période saisie ; seules celles qui reproduisent les DEUX
|
|
90
|
+
* valeurs du DPE (tolérance 0,01 °C) sont retenues :
|
|
91
|
+
* - une seule candidate : retenue ;
|
|
92
|
+
* - plusieurs : celle de l'année de construction si elle en fait partie, sinon la plus récente ;
|
|
93
|
+
* - aucune : undefined (repli sur l'année de construction).
|
|
94
|
+
*
|
|
95
|
+
* Aucune déduction si le mode bug_for_bug_compat est désactivé, si l'une des deux températures
|
|
96
|
+
* du DPE est absente ou non numérique, ou si tous les émetteurs ont une période saisie.
|
|
97
|
+
*
|
|
98
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.5
|
|
99
|
+
*
|
|
100
|
+
* @param ctx {Contexte}
|
|
101
|
+
* @param generateurChauffageDE {GenerateurChauffageDE}
|
|
102
|
+
* @param emetteursChauffage {EmetteurChauffage[]}
|
|
103
|
+
* @param temperaturesDpe {{temp_fonc_30: number, temp_fonc_100: number}|undefined} valeurs
|
|
104
|
+
* d'origine des données intermédiaires du générateur dans le DPE
|
|
105
|
+
* @return {number|undefined} période déduite (enum_periode_installation_emetteur_id)
|
|
106
|
+
*/
|
|
107
|
+
periodeInstallationEmetteurDeduite(
|
|
108
|
+
ctx,
|
|
109
|
+
generateurChauffageDE,
|
|
110
|
+
emetteursChauffage,
|
|
111
|
+
temperaturesDpe
|
|
112
|
+
) {
|
|
113
|
+
if (
|
|
114
|
+
!bug_for_bug_compat ||
|
|
115
|
+
!estNumerique(temperaturesDpe?.temp_fonc_30) ||
|
|
116
|
+
!estNumerique(temperaturesDpe?.temp_fonc_100)
|
|
117
|
+
) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const emetteursSansPeriode = this.#emetteursAvecDistribution(emetteursChauffage).filter(
|
|
122
|
+
(emetteurChauffage) =>
|
|
123
|
+
!parseInt(emetteurChauffage.donnee_entree.enum_periode_installation_emetteur_id)
|
|
124
|
+
);
|
|
125
|
+
if (emetteursSansPeriode.length === 0) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tempFonc30Dpe = Number(temperaturesDpe.temp_fonc_30);
|
|
130
|
+
const tempFonc100Dpe = Number(temperaturesDpe.temp_fonc_100);
|
|
131
|
+
|
|
132
|
+
const candidates = PERIODES_INSTALLATION_EMETTEUR.filter((periode) => {
|
|
133
|
+
const { temp_fonc_30, temp_fonc_100 } = this.temperatureFonctionnement(
|
|
134
|
+
ctx,
|
|
135
|
+
generateurChauffageDE,
|
|
136
|
+
emetteursChauffage,
|
|
137
|
+
periode
|
|
138
|
+
);
|
|
139
|
+
return (
|
|
140
|
+
estNumerique(temp_fonc_30) &&
|
|
141
|
+
estNumerique(temp_fonc_100) &&
|
|
142
|
+
Math.abs(temp_fonc_30 - tempFonc30Dpe) <= TOLERANCE_TEMP_FONC &&
|
|
143
|
+
Math.abs(temp_fonc_100 - tempFonc100Dpe) <= TOLERANCE_TEMP_FONC
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (candidates.length === 0) {
|
|
148
|
+
logger.warn(
|
|
149
|
+
`période d'installation des émetteurs non déductible : valeurs hors tables / donnée d'entrée incohérente (temp_fonc_30 = ${tempFonc30Dpe}, temp_fonc_100 = ${tempFonc100Dpe}). Repli sur l'année de construction.`
|
|
150
|
+
);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const periodeConstruction = this.periodeInstallationEmetteur(ctx, { donnee_entree: {} });
|
|
155
|
+
const periode = candidates.includes(periodeConstruction)
|
|
156
|
+
? periodeConstruction
|
|
157
|
+
: candidates[candidates.length - 1];
|
|
158
|
+
|
|
159
|
+
logger.info(
|
|
160
|
+
`période d'installation des émetteurs déduite des données intermédiaires du DPE : ${periode} (temp_fonc_30 = ${tempFonc30Dpe}, temp_fonc_100 = ${tempFonc100Dpe})`
|
|
161
|
+
);
|
|
162
|
+
return periode;
|
|
163
|
+
}
|
|
164
|
+
|
|
44
165
|
/**
|
|
45
166
|
* Calcul des températures de fonctionnement à 30% ou 100%
|
|
46
167
|
*
|
|
47
168
|
* @param ctx {Contexte}
|
|
48
169
|
* @param generateurChauffageDE {GenerateurChauffageDE}
|
|
49
170
|
* @param emetteursChauffage {EmetteurChauffage[]}
|
|
171
|
+
* @param periodeParDefaut {number|undefined} période affectée aux émetteurs sans période saisie
|
|
172
|
+
* (à défaut : année de construction)
|
|
50
173
|
* @return {{temp_fonc_30: number, temp_fonc_100: number}}
|
|
51
174
|
*/
|
|
52
|
-
temperatureFonctionnement(
|
|
175
|
+
temperatureFonctionnement(
|
|
176
|
+
ctx,
|
|
177
|
+
generateurChauffageDE,
|
|
178
|
+
emetteursChauffage,
|
|
179
|
+
periodeParDefaut = undefined
|
|
180
|
+
) {
|
|
53
181
|
let temperatureFonctionnement30;
|
|
54
182
|
let temperatureFonctionnement100;
|
|
55
183
|
|
|
56
|
-
emetteursChauffage
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!temperatureFonctionnement100 || tempFonctionnement100 > temperatureFonctionnement100) {
|
|
91
|
-
temperatureFonctionnement100 = tempFonctionnement100;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
184
|
+
this.#emetteursAvecDistribution(emetteursChauffage).forEach((emetteurChauffage) => {
|
|
185
|
+
// Récupération de la température de distribution
|
|
186
|
+
const temperatureDistribution = Number(
|
|
187
|
+
emetteurChauffage.donnee_entree.enum_temp_distribution_ch_id
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Récupération de la période d'installation de distribution
|
|
191
|
+
const periodeInstallationEmetteur = this.periodeInstallationEmetteur(
|
|
192
|
+
ctx,
|
|
193
|
+
emetteurChauffage,
|
|
194
|
+
periodeParDefaut
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const tempFonctionnement30 = this.#chTvStore.temperatureFonctionnement(
|
|
198
|
+
'30',
|
|
199
|
+
generateurChauffageDE.enum_type_generateur_ch_id,
|
|
200
|
+
temperatureDistribution,
|
|
201
|
+
periodeInstallationEmetteur
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const tempFonctionnement100 = this.#chTvStore.temperatureFonctionnement(
|
|
205
|
+
'100',
|
|
206
|
+
generateurChauffageDE.enum_type_generateur_ch_id,
|
|
207
|
+
temperatureDistribution,
|
|
208
|
+
periodeInstallationEmetteur
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!temperatureFonctionnement30 || tempFonctionnement30 > temperatureFonctionnement30) {
|
|
212
|
+
temperatureFonctionnement30 = tempFonctionnement30;
|
|
213
|
+
}
|
|
214
|
+
if (!temperatureFonctionnement100 || tempFonctionnement100 > temperatureFonctionnement100) {
|
|
215
|
+
temperatureFonctionnement100 = tempFonctionnement100;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
94
218
|
|
|
95
219
|
return {
|
|
96
220
|
temp_fonc_30: temperatureFonctionnement30,
|