@open3cl/engine 1.8.66 → 1.8.68
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/10_clim.js +14 -2
- package/10_clim.spec.js +26 -0
- package/3.2.1_mur.js +9 -6
- package/4_ventilation.js +33 -6
- package/4_ventilation.spec.js +107 -1
- package/README.md +38 -37
- package/conso.js +14 -2
- package/conso.spec.js +45 -1
- package/dpe-sanitizer.service.js +8 -0
- package/dpe-sanitizer.service.spec.js +9 -0
- package/engine.js +13 -2
- package/engine.spec.js +32 -0
- package/package.json +2 -1
package/10_clim.js
CHANGED
|
@@ -14,10 +14,22 @@ function tv_seer(di, de, du, zc_id) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param clim {object} climatisation
|
|
19
|
+
* @param bfr {number} besoin de froid du bâtiment
|
|
20
|
+
* @param bfr_dep {number} besoin de froid dépensier du bâtiment
|
|
21
|
+
* @param zc_id {string} zone climatique
|
|
22
|
+
* @param Sh {number} surface de référence du calcul (SH immeuble pour un DPE généré depuis l'immeuble)
|
|
23
|
+
* @param prorataLogement {number} part du logement dans la consommation de froid de l'installation
|
|
24
|
+
* (Sh_logement / SH_immeuble pour un DPE appartement généré à partir de l'immeuble, 1 sinon). Les
|
|
25
|
+
* données intermédiaires restent à l'échelle de l'installation, comme dans le DPE de référence ;
|
|
26
|
+
* le prorata n'est appliqué qu'à l'agrégation des consommations du logement (conso.js).
|
|
27
|
+
* @see https://github.com/Open3CL/engine/issues/303
|
|
28
|
+
*/
|
|
29
|
+
export default function calc_clim(clim, bfr, bfr_dep, zc_id, Sh, prorataLogement = 1) {
|
|
18
30
|
const de = clim.donnee_entree;
|
|
19
31
|
const di = clim.donnee_intermediaire || {};
|
|
20
|
-
const du = {};
|
|
32
|
+
const du = { prorata_logement: prorataLogement };
|
|
21
33
|
|
|
22
34
|
const rs = de.surface_clim / Sh;
|
|
23
35
|
di.besoin_fr = bfr * rs;
|
package/10_clim.spec.js
CHANGED
|
@@ -105,6 +105,32 @@ describe('calc_clim - consommation de climatisation', () => {
|
|
|
105
105
|
expect(c.donnee_intermediaire.eer).toBe(3);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* DPE appartement généré à partir de l'immeuble : la part du logement est transmise via
|
|
110
|
+
* donnee_utilisateur, les données intermédiaires restant à l'échelle de l'installation.
|
|
111
|
+
* @see https://github.com/Open3CL/engine/issues/303
|
|
112
|
+
*/
|
|
113
|
+
test('prorata logement : mémorisé, données intermédiaires à l’échelle de l’installation', () => {
|
|
114
|
+
tv.mockReturnValue({ tv_seer_id: '5', eer: 3 });
|
|
115
|
+
const c = clim({ surface_clim: 100, enum_methode_saisie_carac_sys_id: '1' });
|
|
116
|
+
|
|
117
|
+
calc_clim(c, 1000, 1500, '1', 100, 0.25);
|
|
118
|
+
|
|
119
|
+
expect(c.donnee_utilisateur.prorata_logement).toBe(0.25);
|
|
120
|
+
// inchangé par le prorata : 0.9 * 1000 / 3
|
|
121
|
+
expect(c.donnee_intermediaire.besoin_fr).toBeCloseTo(1000, 9);
|
|
122
|
+
expect(c.donnee_intermediaire.conso_fr).toBeCloseTo(300, 9);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('prorata logement absent : 1 par défaut', () => {
|
|
126
|
+
tv.mockReturnValue({ tv_seer_id: '5', eer: 3 });
|
|
127
|
+
const c = clim({ surface_clim: 100, enum_methode_saisie_carac_sys_id: '1' });
|
|
128
|
+
|
|
129
|
+
calc_clim(c, 1000, 1500, '1', 100);
|
|
130
|
+
|
|
131
|
+
expect(c.donnee_utilisateur.prorata_logement).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
108
134
|
test('aucune ligne forfaitaire trouvée : EER indéfini, conso non finie', () => {
|
|
109
135
|
tv.mockReturnValue(null);
|
|
110
136
|
const c = clim({ surface_clim: 50, enum_methode_saisie_carac_sys_id: '1' });
|
package/3.2.1_mur.js
CHANGED
|
@@ -304,12 +304,15 @@ function calc_umur0(di, de, du, versionDpe) {
|
|
|
304
304
|
|
|
305
305
|
// Certaines descriptions contiennent des informations sur le type de doublage
|
|
306
306
|
if (bug_for_bug_compat && (type_doublage === 1 || type_doublage === 2)) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
307
|
+
const description = de.description?.toLowerCase();
|
|
308
|
+
if (description) {
|
|
309
|
+
if (description.toLowerCase().indexOf('doublage connu (plâtre, brique') !== -1) {
|
|
310
|
+
type_doublage = 5;
|
|
311
|
+
} else if (description.toLowerCase().indexOf('doublage indéterminé avec lame') !== -1) {
|
|
312
|
+
type_doublage = 4;
|
|
313
|
+
} else if (description.toLowerCase().indexOf('doublage indéterminé ou lame') !== -1) {
|
|
314
|
+
type_doublage = 3;
|
|
315
|
+
}
|
|
313
316
|
}
|
|
314
317
|
}
|
|
315
318
|
|
package/4_ventilation.js
CHANGED
|
@@ -29,6 +29,15 @@ export function presenceJointNonRenseignee(porte_list, bv_list) {
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Vrai si la ligne de la table `q4pa_conv` couvre la période de construction du DPE.
|
|
34
|
+
*/
|
|
35
|
+
export function isLigneQ4paMemePeriode(rowQ4paConv, cg) {
|
|
36
|
+
return String(rowQ4paConv?.enum_periode_construction_id ?? '')
|
|
37
|
+
.split('|')
|
|
38
|
+
.includes(String(cg.enum_periode_construction_id));
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
33
42
|
const surfaces = mur_list.concat(ph_list);
|
|
34
43
|
const surface_isolee = surfaces.reduce((acc, s) => {
|
|
@@ -91,11 +100,29 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
|
91
100
|
});
|
|
92
101
|
|
|
93
102
|
if (rowQ4paConv) {
|
|
103
|
+
let ligneDpeReprise = false;
|
|
104
|
+
/**
|
|
105
|
+
* Le moteur de référence reçoit Q4Pa_conv en entrée : c'est le logiciel de saisie qui choisit
|
|
106
|
+
* la ligne de la table. Certains logiciels (LICIEL notamment) retiennent une ligne dont le
|
|
107
|
+
* critère `isolation_surfaces` contredit l'isolation déclarée des parois (ex. ligne 10 « avant
|
|
108
|
+
* 1948, surfaces isolées », 2, alors que toutes les parois sont non isolées → ligne 1, 4,6).
|
|
109
|
+
* Si la ligne saisie correspond à la période de construction du DPE, on la reprend en entier
|
|
110
|
+
* (isolation et joints) pour reproduire le calcul du logiciel.
|
|
111
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
|
|
112
|
+
*/
|
|
94
113
|
if (rowQ4paConv.isolation_surfaces !== isolation_surfaces) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La
|
|
98
|
-
|
|
114
|
+
if (isLigneQ4paMemePeriode(rowQ4paConv, cg)) {
|
|
115
|
+
console.error(
|
|
116
|
+
`Calcul de hperm pour la ventilation ${de.description}. Le DPE considère isolation_surfaces = ${rowQ4paConv.isolation_surfaces} \n alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La ligne q4pa_conv ${de.tv_q4pa_conv_id} du DPE est conservée.`
|
|
117
|
+
);
|
|
118
|
+
isolation_surfaces = rowQ4paConv.isolation_surfaces;
|
|
119
|
+
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
|
|
120
|
+
ligneDpeReprise = true;
|
|
121
|
+
} else {
|
|
122
|
+
console.error(
|
|
123
|
+
`Calcul de hperm pour la ventilation ${de.description}. Le DPE considère isolation_surfaces = ${rowQ4paConv.isolation_surfaces} \n alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La valeur recalculée isolation_surfaces = ${isolation_surfaces} est utilisée.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
/**
|
|
@@ -105,9 +132,9 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
|
105
132
|
* sans joint.
|
|
106
133
|
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
|
|
107
134
|
*/
|
|
108
|
-
if (presenceJointNonRenseignee(porte_list, bv_list)) {
|
|
135
|
+
if (!ligneDpeReprise && presenceJointNonRenseignee(porte_list, bv_list)) {
|
|
109
136
|
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
|
|
110
|
-
} else if (rowQ4paConv.presence_joints_menuiserie !== pjt) {
|
|
137
|
+
} else if (!ligneDpeReprise && rowQ4paConv.presence_joints_menuiserie !== pjt) {
|
|
111
138
|
console.error(
|
|
112
139
|
`Calcul de hperm pour la ventilation ${de.description}. Le DPE considère presence_joints_menuiserie = ${rowQ4paConv.presence_joints_menuiserie}
|
|
113
140
|
alors que ce devrait être presence_joints_menuiserie = ${pjt}. La valeur recalculée presence_joints_menuiserie = ${pjt} est utilisée.`
|
package/4_ventilation.spec.js
CHANGED
|
@@ -22,7 +22,8 @@ vi.mock('./5_conso_ventilation.js', () => ({
|
|
|
22
22
|
const {
|
|
23
23
|
default: calc_ventilation,
|
|
24
24
|
calc_hperm,
|
|
25
|
-
presenceJointNonRenseignee
|
|
25
|
+
presenceJointNonRenseignee,
|
|
26
|
+
isLigneQ4paMemePeriode
|
|
26
27
|
} = await import('./4_ventilation.js');
|
|
27
28
|
const { tv } = await import('./utils.js');
|
|
28
29
|
const { default: calc_pvent } = await import('./5_conso_ventilation.js');
|
|
@@ -480,6 +481,86 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
|
|
|
480
481
|
errorSpy.mockRestore();
|
|
481
482
|
});
|
|
482
483
|
});
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Ligne q4pa_conv du DPE dont le critère d'isolation contredit les parois (2287E1201982M :
|
|
487
|
+
* ligne 10 « avant 1948, surfaces isolées » alors que toutes les parois sont non isolées).
|
|
488
|
+
*/
|
|
489
|
+
describe('isolation_surfaces incohérent avec la ligne du DPE', () => {
|
|
490
|
+
const murNonIsole = [
|
|
491
|
+
{
|
|
492
|
+
donnee_intermediaire: { b: 1 },
|
|
493
|
+
donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 }
|
|
494
|
+
}
|
|
495
|
+
];
|
|
496
|
+
const baieSansJoint = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 0 } }];
|
|
497
|
+
const vtLigne = (id) => ({
|
|
498
|
+
donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: id, description: 'v' }
|
|
499
|
+
});
|
|
500
|
+
const ROW_10 = {
|
|
501
|
+
q4pa_conv: '2',
|
|
502
|
+
tv_q4pa_conv_id: '10',
|
|
503
|
+
enum_periode_construction_id: '1',
|
|
504
|
+
isolation_surfaces: '1'
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
test('ligne de la même période : isolation et joints de la ligne du DPE repris', async () => {
|
|
508
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
509
|
+
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(ROW_10);
|
|
510
|
+
|
|
511
|
+
calc_ventilation_bug(vtLigne(10), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint);
|
|
512
|
+
|
|
513
|
+
expect(tvBug).toHaveBeenLastCalledWith(
|
|
514
|
+
'q4pa_conv',
|
|
515
|
+
expect.objectContaining({ isolation_surfaces: '1', presence_joints_menuiserie: '0' })
|
|
516
|
+
);
|
|
517
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
518
|
+
expect.stringContaining('La ligne q4pa_conv 10 du DPE est conservée')
|
|
519
|
+
);
|
|
520
|
+
// les joints de la ligne reprise concordent : pas de second message
|
|
521
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
522
|
+
errorSpy.mockRestore();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('ligne 12 (non isolé, avec joints) reprise en entier, malgré des baies sans joint', async () => {
|
|
526
|
+
const { calc_ventilation_bug, tvBug } = await chargerAvecBug({
|
|
527
|
+
q4pa_conv: '2.5',
|
|
528
|
+
tv_q4pa_conv_id: '12',
|
|
529
|
+
enum_periode_construction_id: '1',
|
|
530
|
+
isolation_surfaces: '0',
|
|
531
|
+
presence_joints_menuiserie: '1'
|
|
532
|
+
});
|
|
533
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
534
|
+
const { murs } = surfacesIsolantes();
|
|
535
|
+
|
|
536
|
+
calc_ventilation_bug(vtLigne(12), CG, 'th', 80, 100, murs, [], [], baieSansJoint);
|
|
537
|
+
|
|
538
|
+
expect(tvBug).toHaveBeenLastCalledWith(
|
|
539
|
+
'q4pa_conv',
|
|
540
|
+
expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '1' })
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('ligne d’une autre période : la valeur recalculée est conservée', async () => {
|
|
545
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
546
|
+
const { calc_ventilation_bug, tvBug } = await chargerAvecBug({
|
|
547
|
+
...ROW_10,
|
|
548
|
+
tv_q4pa_conv_id: '11',
|
|
549
|
+
enum_periode_construction_id: '2'
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
calc_ventilation_bug(vtLigne(11), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint);
|
|
553
|
+
|
|
554
|
+
expect(tvBug).toHaveBeenLastCalledWith(
|
|
555
|
+
'q4pa_conv',
|
|
556
|
+
expect.objectContaining({ isolation_surfaces: '0' })
|
|
557
|
+
);
|
|
558
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
559
|
+
expect.stringContaining('La valeur recalculée isolation_surfaces = 0 est utilisée')
|
|
560
|
+
);
|
|
561
|
+
errorSpy.mockRestore();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
483
564
|
});
|
|
484
565
|
|
|
485
566
|
describe('presenceJointNonRenseignee', () => {
|
|
@@ -506,3 +587,28 @@ describe('presenceJointNonRenseignee', () => {
|
|
|
506
587
|
).toBe(false);
|
|
507
588
|
});
|
|
508
589
|
});
|
|
590
|
+
|
|
591
|
+
describe('isLigneQ4paMemePeriode', () => {
|
|
592
|
+
test('période contenue dans une liste « a|b|c » : vrai', () => {
|
|
593
|
+
expect(
|
|
594
|
+
isLigneQ4paMemePeriode(
|
|
595
|
+
{ enum_periode_construction_id: '3|4|5' },
|
|
596
|
+
{ enum_periode_construction_id: 4 }
|
|
597
|
+
)
|
|
598
|
+
).toBe(true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('période différente : faux', () => {
|
|
602
|
+
expect(
|
|
603
|
+
isLigneQ4paMemePeriode(
|
|
604
|
+
{ enum_periode_construction_id: '2' },
|
|
605
|
+
{ enum_periode_construction_id: '1' }
|
|
606
|
+
)
|
|
607
|
+
).toBe(false);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('ligne sans période (ou absente) : faux', () => {
|
|
611
|
+
expect(isLigneQ4paMemePeriode({}, { enum_periode_construction_id: '1' })).toBe(false);
|
|
612
|
+
expect(isLigneQ4paMemePeriode(null, { enum_periode_construction_id: '1' })).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
});
|
package/README.md
CHANGED
|
@@ -154,11 +154,12 @@ flowchart LR
|
|
|
154
154
|
|
|
155
155
|
### L'écosystème Open3CL
|
|
156
156
|
|
|
157
|
-
| Ressource
|
|
158
|
-
|
|
|
159
|
-
| [Démo 3D](https://open3cl.github.io/engine/)
|
|
160
|
-
| [
|
|
161
|
-
| [
|
|
157
|
+
| Ressource | Description |
|
|
158
|
+
| :--------------------------------------------------------------------- | :------------------------------------------------------------------------- |
|
|
159
|
+
| [Démo 3D](https://open3cl.github.io/engine/) | Visualisation 3D des déperditions d'un DPE, recalculées dans le navigateur |
|
|
160
|
+
| [Rapports de corpus](https://open3cl.github.io/engine/reports/corpus/) | Rapport HTML sur les corpus DPE |
|
|
161
|
+
| [open3cl.fr](https://open3cl.fr) | Le site du projet |
|
|
162
|
+
| [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine) | Le paquet npm |
|
|
162
163
|
|
|
163
164
|
<p align="right">(<a href="#readme-top">Retour sommaire</a>)</p>
|
|
164
165
|
|
|
@@ -358,11 +359,11 @@ npm run test:corpus -- corpus-file-path=corpus.csv
|
|
|
358
359
|
npm run test:corpus -- dpes-code=2592E1233185X
|
|
359
360
|
```
|
|
360
361
|
|
|
361
|
-
| Argument | Description
|
|
362
|
-
| :------------------------ |
|
|
363
|
-
| `corpus-file-path=<path>` | Fichier de corpus à analyser (défaut : `test/corpus/files
|
|
364
|
-
| `dpes-folder-path=<path>` | Dossier de cache des DPE. Un DPE déjà présent n'est pas retéléchargé
|
|
365
|
-
| `dpes-code=<code>` | Ne rejoue qu'un seul DPE
|
|
362
|
+
| Argument | Description |
|
|
363
|
+
| :------------------------ | :------------------------------------------------------------------------------- |
|
|
364
|
+
| `corpus-file-path=<path>` | Fichier de corpus à analyser (défaut : tous les fichiers de `test/corpus/files`) |
|
|
365
|
+
| `dpes-folder-path=<path>` | Dossier de cache des DPE. Un DPE déjà présent n'est pas retéléchargé |
|
|
366
|
+
| `dpes-code=<code>` | Ne rejoue qu'un seul DPE |
|
|
366
367
|
|
|
367
368
|
> [!TIP]
|
|
368
369
|
> Définissez `DPE_FOLDER_PATH` une fois pour toutes plutôt que de répéter `dpes-folder-path`. Les DPE absents du cache
|
|
@@ -375,57 +376,57 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
375
376
|
|
|
376
377
|
<!-- CORPUS:START -->
|
|
377
378
|
|
|
378
|
-
> **Version `1.8.
|
|
379
|
+
> **Version `1.8.67`** · branche `main` · généré le 2026-10-16
|
|
379
380
|
> Seuil de tolérance **5%**
|
|
380
381
|
|
|
381
382
|
<table>
|
|
382
383
|
<tr>
|
|
383
384
|
<td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
|
|
384
|
-
<td align="center"><strong>
|
|
385
|
-
<td align="center"><strong>
|
|
386
|
-
<td align="center"><strong>
|
|
385
|
+
<td align="center"><strong>88 475</strong><br/><sub>DPE analysés</sub></td>
|
|
386
|
+
<td align="center"><strong>75 663</strong><br/><sub>DPE conformes</sub></td>
|
|
387
|
+
<td align="center"><strong>85,52 %</strong><br/><sub>réussite globale</sub></td>
|
|
387
388
|
</tr>
|
|
388
389
|
</table>
|
|
389
390
|
|
|
390
391
|
| | Corpus | Réussite | | DPE conformes |
|
|
391
392
|
| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
|
|
392
|
-
|
|
|
393
|
-
| 🟢 | **Appartement · chauffage
|
|
394
|
-
| 🟢 | **
|
|
395
|
-
| 🟢 | **
|
|
396
|
-
| 🟢 | **Maison individuelle (
|
|
397
|
-
| 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **
|
|
398
|
-
| 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **
|
|
399
|
-
| 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,
|
|
400
|
-
| 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **
|
|
393
|
+
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **95,39 %** | `███████████████████░` | 9 437 / 9 893 |
|
|
394
|
+
| 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **94,54 %** | `███████████████████░` | 9 384 / 9 926 |
|
|
395
|
+
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **93,29 %** | `███████████████████░` | 9 173 / 9 833 |
|
|
396
|
+
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **92,03 %** | `██████████████████░░` | 8 992 / 9 771 |
|
|
397
|
+
| 🟢 | **Maison individuelle (2026)**<br/><sub>`dpe_maison_individuelle_2026.csv`</sub> | **90,41 %** | `██████████████████░░` | 9 041 / 10 000 |
|
|
398
|
+
| 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **89,10 %** | `██████████████████░░` | 8 677 / 9 739 |
|
|
399
|
+
| 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **86,00 %** | `█████████████████░░░` | 8 535 / 9 924 |
|
|
400
|
+
| 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,48 %** | `██████████████░░░░░░` | 6 746 / 9 571 |
|
|
401
|
+
| 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **57,83 %** | `████████████░░░░░░░░` | 5 678 / 9 818 |
|
|
401
402
|
|
|
402
403
|
<sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
|
|
403
404
|
|
|
404
405
|
#### Temps d’exécution
|
|
405
406
|
|
|
406
|
-
> Durée de l’appel à `calcul_3cl` par DPE, sur
|
|
407
|
+
> Durée de l’appel à `calcul_3cl` par DPE, sur 88 460 calculs.
|
|
407
408
|
> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
|
|
408
409
|
|
|
409
410
|
<table>
|
|
410
411
|
<tr>
|
|
411
|
-
<td align="center"><strong>
|
|
412
|
-
<td align="center"><strong>
|
|
413
|
-
<td align="center"><strong>0,
|
|
414
|
-
<td align="center"><strong>1
|
|
412
|
+
<td align="center"><strong>10,2 ms</strong><br/><sub>moyenne</sub></td>
|
|
413
|
+
<td align="center"><strong>8,72 ms</strong><br/><sub>médiane</sub></td>
|
|
414
|
+
<td align="center"><strong>0,18 ms</strong><br/><sub>min</sub></td>
|
|
415
|
+
<td align="center"><strong>618,1 ms</strong><br/><sub>max</sub></td>
|
|
415
416
|
</tr>
|
|
416
417
|
</table>
|
|
417
418
|
|
|
418
419
|
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
|
|
419
420
|
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
|
|
420
|
-
| **
|
|
421
|
-
| **Appartement · chauffage
|
|
422
|
-
| **
|
|
423
|
-
| **
|
|
424
|
-
| **Maison individuelle (
|
|
425
|
-
| **Immeuble · chauffage collectif** |
|
|
426
|
-
| **Immeuble · chauffage individuel** |
|
|
427
|
-
| **Immeuble · chauffage mixte** |
|
|
428
|
-
| **Individuel généré depuis l'immeuble (2026)** |
|
|
421
|
+
| **Appartement · chauffage individuel (2025)** | 6,87 ms | 6,25 ms | 1,73 ms | 79,3 ms | 12,0 ms | 17,0 ms |
|
|
422
|
+
| **Appartement · chauffage collectif (2025)** | 7,91 ms | 6,85 ms | 2,08 ms | 66,2 ms | 15,1 ms | 21,7 ms |
|
|
423
|
+
| **Logement individuel (2025)** | 9,05 ms | 8,08 ms | 2,03 ms | 104,0 ms | 16,9 ms | 24,1 ms |
|
|
424
|
+
| **Maison individuelle (2025)** | 11,2 ms | 9,96 ms | 2,50 ms | 109,6 ms | 20,2 ms | 30,4 ms |
|
|
425
|
+
| **Maison individuelle (2026)** | 11,8 ms | 10,3 ms | 2,74 ms | 120,6 ms | 22,7 ms | 34,3 ms |
|
|
426
|
+
| **Immeuble · chauffage collectif** | 12,0 ms | 9,62 ms | 0,21 ms | 197,5 ms | 25,2 ms | 42,9 ms |
|
|
427
|
+
| **Immeuble · chauffage individuel** | 12,2 ms | 10,3 ms | 0,18 ms | 228,3 ms | 24,6 ms | 39,3 ms |
|
|
428
|
+
| **Immeuble · chauffage mixte** | 9,68 ms | 7,89 ms | 0,35 ms | 618,1 ms | 20,2 ms | 33,5 ms |
|
|
429
|
+
| **Individuel généré depuis l'immeuble (2026)** | 10,8 ms | 9,27 ms | 2,42 ms | 114,5 ms | 20,3 ms | 34,2 ms |
|
|
429
430
|
|
|
430
431
|
<sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
|
|
431
432
|
dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
|
package/conso.js
CHANGED
|
@@ -983,6 +983,16 @@ function getChauffageConso(gen_ch, field, coef, prorataChauffage, prefix, facteu
|
|
|
983
983
|
}, 0);
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Part du logement dans la consommation de froid d'une installation (1 par défaut), positionnée par
|
|
988
|
+
* calc_clim pour les DPE appartement générés à partir de l'immeuble (issue #303).
|
|
989
|
+
* @param fr {object} climatisation
|
|
990
|
+
* @return {number}
|
|
991
|
+
*/
|
|
992
|
+
export function prorataFroidLogement(fr) {
|
|
993
|
+
return fr.donnee_utilisateur?.prorata_logement ?? 1;
|
|
994
|
+
}
|
|
995
|
+
|
|
986
996
|
function calc_conso_pond(
|
|
987
997
|
Sh,
|
|
988
998
|
zc_id,
|
|
@@ -1086,13 +1096,15 @@ function calc_conso_pond(
|
|
|
1086
1096
|
ret.fr = fr_list.reduce((acc, fr) => {
|
|
1087
1097
|
const typeEnergie = getTypeEnergie(fr.donnee_entree, 'fr');
|
|
1088
1098
|
const conso =
|
|
1089
|
-
fr.donnee_intermediaire.conso_fr *
|
|
1099
|
+
fr.donnee_intermediaire.conso_fr *
|
|
1100
|
+
prorataFroidLogement(fr) *
|
|
1101
|
+
(typeEnergie === 'électricité fr' ? fAc('fr') : 1);
|
|
1090
1102
|
|
|
1091
1103
|
return acc + getConso(coef, typeEnergie, conso);
|
|
1092
1104
|
}, 0);
|
|
1093
1105
|
|
|
1094
1106
|
ret.fr_depensier = fr_list.reduce((acc, fr) => {
|
|
1095
|
-
const conso = fr.donnee_intermediaire.conso_fr_depensier;
|
|
1107
|
+
const conso = fr.donnee_intermediaire.conso_fr_depensier * prorataFroidLogement(fr);
|
|
1096
1108
|
const typeEnergie = getTypeEnergie(fr.donnee_entree, 'fr');
|
|
1097
1109
|
|
|
1098
1110
|
return acc + getConso(coef, typeEnergie, conso);
|
package/conso.spec.js
CHANGED
|
@@ -71,7 +71,8 @@ const {
|
|
|
71
71
|
ARRETES_RESEAU_CHALEUR,
|
|
72
72
|
COEF_GES_AUTRES_RESEAUX_CHALEUR,
|
|
73
73
|
indexArreteReseauChaleur,
|
|
74
|
-
getContenuCo2ReseauChaleur
|
|
74
|
+
getContenuCo2ReseauChaleur,
|
|
75
|
+
prorataFroidLogement
|
|
75
76
|
} = await import('./conso.js');
|
|
76
77
|
const { tv } = await import('./utils.js');
|
|
77
78
|
|
|
@@ -298,6 +299,49 @@ describe('calc_conso - agrégation des consommations', () => {
|
|
|
298
299
|
expect(net.cout.cout_eclairage).toBeCloseTo(brut.cout.cout_eclairage, 9);
|
|
299
300
|
});
|
|
300
301
|
|
|
302
|
+
/**
|
|
303
|
+
* DPE appartement généré à partir de l'immeuble : conso de froid ramenée au logement par le
|
|
304
|
+
* prorata mémorisé dans donnee_utilisateur.prorata_logement.
|
|
305
|
+
* @see https://github.com/Open3CL/engine/issues/303
|
|
306
|
+
*/
|
|
307
|
+
test('froid : conso et conso dépensière proratisées au logement', () => {
|
|
308
|
+
const fr = (du) => [
|
|
309
|
+
{
|
|
310
|
+
donnee_entree: { enum_type_energie_id: '1' },
|
|
311
|
+
donnee_intermediaire: { conso_fr: 400, conso_fr_depensier: 1000 },
|
|
312
|
+
donnee_utilisateur: du
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
const plein = calc_conso(100, 1, 1, [], [], [], fr(undefined), 1, 1, DATE_DPE, coef_ep);
|
|
316
|
+
const lot = calc_conso(
|
|
317
|
+
100,
|
|
318
|
+
1,
|
|
319
|
+
1,
|
|
320
|
+
[],
|
|
321
|
+
[],
|
|
322
|
+
[],
|
|
323
|
+
fr({ prorata_logement: 0.25 }),
|
|
324
|
+
1,
|
|
325
|
+
1,
|
|
326
|
+
DATE_DPE,
|
|
327
|
+
coef_ep
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(plein.ef_conso.conso_fr).toBeCloseTo(400, 9);
|
|
331
|
+
expect(lot.ef_conso.conso_fr).toBeCloseTo(100, 9);
|
|
332
|
+
expect(lot.ef_conso.conso_fr_depensier).toBeCloseTo(250, 9);
|
|
333
|
+
expect(lot.emission_ges.emission_ges_fr).toBeCloseTo(
|
|
334
|
+
plein.emission_ges.emission_ges_fr * 0.25,
|
|
335
|
+
9
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('prorataFroidLogement : valeur mémorisée, 1 par défaut', () => {
|
|
340
|
+
expect(prorataFroidLogement({ donnee_utilisateur: { prorata_logement: 0.3 } })).toBe(0.3);
|
|
341
|
+
expect(prorataFroidLogement({ donnee_utilisateur: {} })).toBe(1);
|
|
342
|
+
expect(prorataFroidLogement({})).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
301
345
|
test('autoconsommation PV : froid électrique minoré, froid non électrique non minoré', () => {
|
|
302
346
|
const frElec = [
|
|
303
347
|
{
|
package/dpe-sanitizer.service.js
CHANGED
|
@@ -2,6 +2,14 @@ import { ObjectUtil } from './core/util/infrastructure/object-util.js';
|
|
|
2
2
|
import { set, has } from 'lodash-es';
|
|
3
3
|
|
|
4
4
|
const nodesToMap = [
|
|
5
|
+
'mur',
|
|
6
|
+
'plancher_bas',
|
|
7
|
+
'plancher_haut',
|
|
8
|
+
'baie_vitree',
|
|
9
|
+
'porte',
|
|
10
|
+
'pont_thermique',
|
|
11
|
+
'ventilation',
|
|
12
|
+
'climatisation',
|
|
5
13
|
'installation_ecs',
|
|
6
14
|
'generateur_ecs',
|
|
7
15
|
'installation_chauffage',
|
|
@@ -95,6 +95,15 @@ describe('DpeSanitizerService - transformation des valeurs', () => {
|
|
|
95
95
|
expect(valueTransform(generateur, 'generateur_ecs')).toEqual([generateur]);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test.each(COLLECTIONS.map((path) => path.split('.').pop()))(
|
|
99
|
+
'encapsule dans un tableau un nœud %s unique (collection à un seul élément)',
|
|
100
|
+
(key) => {
|
|
101
|
+
const valueTransform = getValueTransform();
|
|
102
|
+
const node = { donnee_entree: { description: 'unique' } };
|
|
103
|
+
expect(valueTransform(node, key)).toEqual([node]);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
98
107
|
test('ne ré-encapsule pas un nœud déjà sous forme de tableau', () => {
|
|
99
108
|
const valueTransform = getValueTransform();
|
|
100
109
|
const deja = [{ id: 1 }];
|
package/engine.js
CHANGED
|
@@ -35,7 +35,7 @@ import DpeSanitizerService from './dpe-sanitizer.service.js';
|
|
|
35
35
|
import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
|
|
36
36
|
import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
|
|
37
37
|
|
|
38
|
-
const LIB_VERSION = '1.8.
|
|
38
|
+
const LIB_VERSION = '1.8.68';
|
|
39
39
|
|
|
40
40
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
41
41
|
|
|
@@ -330,7 +330,18 @@ export function calcul_3cl(inputDpe, options) {
|
|
|
330
330
|
|
|
331
331
|
const bfr = apport_et_besoin.besoin_fr;
|
|
332
332
|
const bfr_dep = apport_et_besoin.besoin_fr_depensier;
|
|
333
|
-
|
|
333
|
+
/**
|
|
334
|
+
* DPE appartement généré à partir de l'immeuble : besoin et conso de froid calculés à l'échelle de
|
|
335
|
+
* l'installation (immeuble), puis ramenés au logement au prorata Sh_logement / SH_immeuble, comme le
|
|
336
|
+
* chauffage et l'ECS (Tribu `Calcul_installation_froid.cs` l.62-64, `Calcul_batiment.cs`
|
|
337
|
+
* l.1114-1138).
|
|
338
|
+
* @see https://github.com/Open3CL/engine/issues/303
|
|
339
|
+
*/
|
|
340
|
+
const prorataFroid =
|
|
341
|
+
dpeGenereImmeuble && Number(ShChauffageAndEcs) > Number(Sh) && Number(Sh) > 0
|
|
342
|
+
? Number(Sh) / Number(ShChauffageAndEcs)
|
|
343
|
+
: 1;
|
|
344
|
+
clim.forEach((clim) => calc_clim(clim, bfr, bfr_dep, zc_id, ShChauffageAndEcs, prorataFroid));
|
|
334
345
|
|
|
335
346
|
/**
|
|
336
347
|
* La consommation ECS est obtenu pour certains types de DPE par virtualisation des générateurs collectifs en générateurs individuels virtuels
|
package/engine.spec.js
CHANGED
|
@@ -776,6 +776,38 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
|
|
|
776
776
|
// Auxiliaires ECS : Sh = surface logement, ramenée à l'immeuble dans le calcul.
|
|
777
777
|
expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(80);
|
|
778
778
|
expect(conso_aux_distribution_ecs.mock.calls[0][4]).toBe(200);
|
|
779
|
+
// Froid : pas un DPE généré depuis l'immeuble, conso de froid non proratisée (#303).
|
|
780
|
+
expect(calc_clim.mock.calls[0][4]).toBe(200);
|
|
781
|
+
expect(calc_clim.mock.calls[0][5]).toBe(1);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* @see https://github.com/Open3CL/engine/issues/303
|
|
786
|
+
*/
|
|
787
|
+
test.each(['10', '11', '12', '13', '33', '34', '38', '39', '40'])(
|
|
788
|
+
"DPE appartement généré à partir de l'immeuble (map %s) : conso de froid proratisée Sh / SH",
|
|
789
|
+
(mapId) => {
|
|
790
|
+
enums.methode_application_dpe_log = { [mapId]: 'dpe appartement généré immeuble' };
|
|
791
|
+
const dpe = makeDpe();
|
|
792
|
+
dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = mapId;
|
|
793
|
+
|
|
794
|
+
calcul_3cl(dpe, { sanitize: false });
|
|
795
|
+
|
|
796
|
+
// Besoin calculé sur SH immeuble (200), part du logement = 80 / 200.
|
|
797
|
+
expect(calc_clim.mock.calls[0][4]).toBe(200);
|
|
798
|
+
expect(calc_clim.mock.calls[0][5]).toBeCloseTo(0.4, 9);
|
|
799
|
+
}
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
test("DPE généré depuis l'immeuble avec SH immeuble ≤ Sh logement : prorata de froid neutre", () => {
|
|
803
|
+
enums.methode_application_dpe_log = { 10: 'dpe appartement généré immeuble' };
|
|
804
|
+
const dpe = makeDpe();
|
|
805
|
+
dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = '10';
|
|
806
|
+
dpe.logement.caracteristique_generale.surface_habitable_immeuble = 80;
|
|
807
|
+
|
|
808
|
+
calcul_3cl(dpe, { sanitize: false });
|
|
809
|
+
|
|
810
|
+
expect(calc_clim.mock.calls[0][5]).toBe(1);
|
|
779
811
|
});
|
|
780
812
|
|
|
781
813
|
test('appartement : rapport SH immeuble / SH logement transmis au calcul du coût', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open3cl/engine",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.68",
|
|
4
4
|
"description": "Open Source 3CL-DPE engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test:corpus": "node test/corpus/run_all_corpus_checks.js",
|
|
20
20
|
"test:corpus:all": "npm run reports:clean && node test/corpus/run_all_corpus_checks.js && npm run reports:readme",
|
|
21
21
|
"test:corpus:all+database": "npm run test:corpus:all && node test/corpus/corpus_database.js",
|
|
22
|
+
"test:corpus:one": "npm run test:corpus -- corpus-file-path=dpe_maison_individuelle_2026.csv",
|
|
22
23
|
"qa:lint": "eslint .",
|
|
23
24
|
"qa:lint:fix": "npm run qa:lint -- --fix",
|
|
24
25
|
"qa:duplication": "jscpd",
|