@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.
@@ -44,6 +44,52 @@ export function calc_Qrec_gen_j(gen_ch, nrefj, Bch_hp_j) {
44
44
  return 0.48 * Cper * di.qp0 * Dperj || 0;
45
45
  }
46
46
 
47
+ /**
48
+ * 9.1.1 - Pertes récupérées de génération pour le chauffage sur le mois j (Wh)
49
+ * pour un générateur de la collection ECS assurant l'ECS uniquement.
50
+ *
51
+ * Pour les générateurs assurant l'ECS uniquement :
52
+ * Dperj = Nrefj * 1790 / 8760
53
+ * Qgen_rec_j = 0,48 * Cper * Qp0 * Dperj
54
+ *
55
+ * Seuls les générateurs avec des pertes à l'arrêt (Qp0 > 0), situés en volume chauffé et dont
56
+ * l'usage est 'ecs' sont concernés. Les générateurs 'chauffage + ecs' sont déjà pris en compte via
57
+ * la collection des générateurs de chauffage (cf. issue #153).
58
+ *
59
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
60
+ *
61
+ * @param gen_ecs {GenerateurEcsItem}
62
+ * @param nrefj {number}
63
+ * @returns {number}
64
+ */
65
+ export function calc_Qrec_gen_ecs_j(gen_ecs, nrefj) {
66
+ const de = gen_ecs.donnee_entree;
67
+ const di = gen_ecs.donnee_intermediaire || {};
68
+
69
+ if (!isGenerateurEcsSeulRecuperable(gen_ecs)) return 0;
70
+
71
+ const Cper = Number(de.presence_ventouse) === 1 ? 0.75 : 0.5;
72
+ return 0.48 * Cper * di.qp0 * pertes_gen_ecs(nrefj);
73
+ }
74
+
75
+ /**
76
+ * Un générateur ECS est concerné par la récupération des pertes de génération pour le chauffage
77
+ * s'il a des pertes à l'arrêt (Qp0 > 0), s'il est en volume chauffé et si son usage est 'ecs' seul
78
+ * (enum_usage_generateur_id = 2).
79
+ *
80
+ * @param gen_ecs {GenerateurEcsItem}
81
+ * @returns {boolean}
82
+ */
83
+ export function isGenerateurEcsSeulRecuperable(gen_ecs) {
84
+ const de = gen_ecs.donnee_entree || {};
85
+ const di = gen_ecs.donnee_intermediaire || {};
86
+ return (
87
+ Number(di.qp0 || 0) > 0 &&
88
+ Number(de.position_volume_chauffe ?? 0) === 1 &&
89
+ Number(de.enum_usage_generateur_id) === 2
90
+ );
91
+ }
92
+
47
93
  function tv_rendement_generation(di, de, du) {
48
94
  const matcher = {
49
95
  enum_type_generateur_ch_id: requestInputID(de, du, 'type_generateur_ch')
@@ -59,6 +59,8 @@ vi.mock('./13.2_generateur_combustion.js', () => ({
59
59
 
60
60
  const {
61
61
  calc_Qrec_gen_j,
62
+ calc_Qrec_gen_ecs_j,
63
+ isGenerateurEcsSeulRecuperable,
62
64
  type_generateur_ch,
63
65
  checkForGeneratorType,
64
66
  calc_generateur_ch,
@@ -147,6 +149,71 @@ describe('calc_Qrec_gen_j - pertes de génération récupérées', () => {
147
149
  });
148
150
  });
149
151
 
152
+ /**
153
+ * 9.1.1 Pertes récupérées de génération pour le chauffage : générateur de la collection ECS
154
+ * assurant l'ECS uniquement (issue #153).
155
+ * Dperj = Nrefj * 1790 / 8760 ; Qgen_rec_j = 0,48 * Cper * Qp0 * Dperj
156
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1
157
+ */
158
+ describe('calc_Qrec_gen_ecs_j / isGenerateurEcsSeulRecuperable - générateur ECS seul', () => {
159
+ /** Générateur ECS seul (usage 2), en volume chauffé, avec Qp0 = 100 W. */
160
+ function generateurEcs(overridesDe = {}, di = { qp0: 100 }) {
161
+ return {
162
+ donnee_entree: {
163
+ position_volume_chauffe: 1,
164
+ enum_usage_generateur_id: '2',
165
+ presence_ventouse: 0,
166
+ ...overridesDe
167
+ },
168
+ donnee_intermediaire: di
169
+ };
170
+ }
171
+
172
+ test('ECS seul, sans ventouse (Cper = 0.5) : 0.48 * 0.5 * Qp0 * Nref * 1790 / 8760', () => {
173
+ // 0.48 * 0.5 * 100 * (50 * 1790 / 8760) = 245.205479452...
174
+ expect(calc_Qrec_gen_ecs_j(generateurEcs(), 50)).toBeCloseTo(245.20547945205482, 9);
175
+ });
176
+
177
+ test('ECS seul, avec ventouse (Cper = 0.75)', () => {
178
+ // 0.48 * 0.75 * 100 * (50 * 1790 / 8760) = 367.808219178...
179
+ const gen = generateurEcs({ presence_ventouse: 1 });
180
+ expect(calc_Qrec_gen_ecs_j(gen, 50)).toBeCloseTo(367.8082191780822, 9);
181
+ });
182
+
183
+ test('la récupération est indépendante du besoin de chauffage et proportionnelle à Nref', () => {
184
+ const gen = generateurEcs();
185
+ expect(calc_Qrec_gen_ecs_j(gen, 100)).toBeCloseTo(2 * calc_Qrec_gen_ecs_j(gen, 50), 9);
186
+ });
187
+
188
+ test('générateur hors volume chauffé : aucune récupération', () => {
189
+ const gen = generateurEcs({ position_volume_chauffe: 0 });
190
+ expect(isGenerateurEcsSeulRecuperable(gen)).toBe(false);
191
+ expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
192
+ });
193
+
194
+ test('position en volume chauffé non renseignée : aucune récupération', () => {
195
+ const gen = generateurEcs({ position_volume_chauffe: undefined });
196
+ expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
197
+ });
198
+
199
+ test("usage 'chauffage + ecs' : exclu (déjà compté dans la collection chauffage)", () => {
200
+ const gen = generateurEcs({ enum_usage_generateur_id: '3' });
201
+ expect(isGenerateurEcsSeulRecuperable(gen)).toBe(false);
202
+ expect(calc_Qrec_gen_ecs_j(gen, 50)).toBe(0);
203
+ });
204
+
205
+ test('Qp0 nul ou absent : aucune récupération', () => {
206
+ expect(calc_Qrec_gen_ecs_j(generateurEcs({}, { qp0: 0 }), 50)).toBe(0);
207
+ expect(calc_Qrec_gen_ecs_j(generateurEcs({}, null), 50)).toBe(0);
208
+ expect(isGenerateurEcsSeulRecuperable({})).toBe(false);
209
+ });
210
+
211
+ test('identifiants numériques acceptés (usage 2, position 1)', () => {
212
+ const gen = generateurEcs({ enum_usage_generateur_id: 2, position_volume_chauffe: '1' });
213
+ expect(isGenerateurEcsSeulRecuperable(gen)).toBe(true);
214
+ });
215
+ });
216
+
150
217
  /**
151
218
  * Détermination du type de générateur exploitable pour un usage donné.
152
219
  */
package/README.md CHANGED
@@ -370,28 +370,28 @@ 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.7`** · branche `main` · généré le 2026-10-02
373
+ > **Version `1.8.8`** · à 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>59 740</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>66,39 %</strong><br/><sub>réussite globale</sub></td>
380
+ <td align="center"><strong>59 879</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>66,54 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
385
385
  | | Corpus | Réussite | | DPE conformes |
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,23 %** | `█████████░░░░░░░░░░░` | 4 623 / 10 000 |
388
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,76 %** | `██████████████████░░` | 9 176 / 10 000 |
389
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 833 / 9 992 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,38 %** | `██████████████████░░` | 8 838 / 10 000 |
391
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,11 %** | `███████████████░░░░░` | 7 410 / 9 999 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **69,74 %** | `██████████████░░░░░░` | 6 974 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,44 %** | `████████████░░░░░░░░` | 6 243 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,04 %** | `██████████░░░░░░░░░░` | 4 904 / 10 000 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,28 %** | `█████████░░░░░░░░░░░` | 4 628 / 10 000 |
388
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,80 %** | `██████████████████░░` | 9 180 / 10 000 |
389
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,70 %** | `██████████████████░░` | 8 863 / 9 992 |
390
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 840 / 10 000 |
391
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,14 %** | `███████████████░░░░░` | 7 413 / 9 999 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **70,61 %** | `██████████████░░░░░░` | 7 061 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,50 %** | `████████████░░░░░░░░` | 6 249 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,06 %** | `██████████░░░░░░░░░░` | 4 906 / 10 000 |
395
395
  | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,39 %** | `█████░░░░░░░░░░░░░░░` | 2 739 / 10 000 |
396
396
 
397
397
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
@@ -403,24 +403,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>4,80 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,20 ms</strong><br/><sub>médiane</sub></td>
408
- <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>518,4 ms</strong><br/><sub>max</sub></td>
406
+ <td align="center"><strong>5,31 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,65 ms</strong><br/><sub>médiane</sub></td>
408
+ <td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
409
+ <td align="center"><strong>893,4 ms</strong><br/><sub>max</sub></td>
410
410
  </tr>
411
411
  </table>
412
412
 
413
413
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
414
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 4,33 ms | 3,90 ms | 0,20 ms | 36,9 ms | 7,87 ms | 11,1 ms |
416
- | **Appartement · chauffage individuel (2025)** | 3,48 ms | 3,15 ms | 1,06 ms | 34,8 ms | 6,09 ms | 7,83 ms |
417
- | **Logement individuel (2025)** | 4,86 ms | 4,30 ms | 1,30 ms | 29,8 ms | 9,41 ms | 13,1 ms |
418
- | **Maison individuelle (2025)** | 5,48 ms | 4,96 ms | 1,65 ms | 35,8 ms | 9,87 ms | 12,8 ms |
419
- | **Immeuble · chauffage individuel** | 5,89 ms | 5,04 ms | 0,11 ms | 75,0 ms | 11,9 ms | 18,7 ms |
420
- | **Appartement · chauffage collectif (2025)** | 3,86 ms | 3,58 ms | 1,18 ms | 31,7 ms | 6,29 ms | 7,87 ms |
421
- | **Immeuble · chauffage collectif** | 5,21 ms | 4,43 ms | 0,11 ms | 110,0 ms | 9,58 ms | 16,9 ms |
422
- | **Immeuble · chauffage mixte** | 4,89 ms | 4,01 ms | 0,20 ms | 518,4 ms | 10,0 ms | 17,0 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 5,22 ms | 4,47 ms | 1,61 ms | 40,6 ms | 9,86 ms | 15,4 ms |
415
+ | **Généraliste** | 4,56 ms | 4,09 ms | 0,21 ms | 37,2 ms | 8,41 ms | 12,0 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 4,01 ms | 3,65 ms | 1,47 ms | 33,8 ms | 6,95 ms | 9,38 ms |
417
+ | **Logement individuel (2025)** | 4,98 ms | 4,43 ms | 1,62 ms | 38,0 ms | 9,30 ms | 12,8 ms |
418
+ | **Maison individuelle (2025)** | 5,92 ms | 5,33 ms | 1,85 ms | 40,3 ms | 10,6 ms | 13,7 ms |
419
+ | **Immeuble · chauffage individuel** | 6,70 ms | 5,72 ms | 0,13 ms | 136,8 ms | 13,4 ms | 22,2 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,60 ms | 4,36 ms | 1,46 ms | 37,2 ms | 7,27 ms | 9,45 ms |
421
+ | **Immeuble · chauffage collectif** | 5,31 ms | 4,38 ms | 0,12 ms | 99,5 ms | 9,95 ms | 17,7 ms |
422
+ | **Immeuble · chauffage mixte** | 5,38 ms | 4,34 ms | 0,20 ms | 893,4 ms | 11,2 ms | 18,5 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,36 ms | 5,53 ms | 1,67 ms | 50,2 ms | 12,4 ms | 19,1 ms |
424
424
 
425
425
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
426
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
package/conso.js CHANGED
@@ -122,6 +122,129 @@ function cout_electricite(cef) {
122
122
  else return 56 + 0.15989 * cef;
123
123
  }
124
124
 
125
+ /**
126
+ * Date d'entrée en vigueur du barème des prix des énergies mis à jour.
127
+ * Constatée sur les DPE publiés par l'ADEME (dataset dpe03existant) : barème 2021 jusqu'au
128
+ * 30/06/2024, barème mis à jour pour les DPE établis à partir du 01/07/2024 (et toujours en 2026).
129
+ */
130
+ export const DATE_BAREME_COUT_2024 = '2024-07-01';
131
+
132
+ function cout_gaz_naturel_2024(cef) {
133
+ if (cef < 5009) return 0.1312 * cef;
134
+ else if (cef < 50055) return 182 + 0.09488 * cef;
135
+ else return 288 + 0.09274 * cef;
136
+ }
137
+
138
+ function cout_electricite_2024(cef) {
139
+ if (cef < 1000) return 0.34721 * cef;
140
+ else if (cef < 2500) return 158 + 0.18954 * cef;
141
+ else if (cef < 5000) return 158 + 0.18949 * cef;
142
+ else if (cef < 15000) return 119 + 0.19726 * cef;
143
+ else return 78 + 0.20001 * cef;
144
+ }
145
+
146
+ /**
147
+ * Barème des prix des énergies applicable aux DPE établis à partir du 01/07/2024.
148
+ * Mêmes tranches qu'en 2021 ; valeurs calées sur les sorties Tribu (moteur CSTB) et les DPE ADEME.
149
+ */
150
+ const coef_cout_2024 = {
151
+ 'fioul domestique': 0.14821,
152
+ 'réseau de chauffage urbain': 0.08921,
153
+ propane: 0.15672,
154
+ gpl: 0.15672,
155
+ butane: 0.23429,
156
+ charbon: 0.02787,
157
+ 'bois – granulés (pellets) ou briquettes': 0.09897,
158
+ 'bois – bûches': 0.042,
159
+ 'bois – plaquettes forestières': 0.042,
160
+ 'bois – plaquettes d’industrie': 0.042,
161
+ 'gaz naturel': cout_gaz_naturel_2024,
162
+ "électricité d'origine renouvelable utilisée dans le bâtiment": cout_electricite_2024,
163
+ 'électricité ch': cout_electricite_2024,
164
+ 'électricité ecs': cout_electricite_2024,
165
+ 'électricité fr': cout_electricite_2024,
166
+ 'électricité éclairage': cout_electricite_2024,
167
+ 'électricité auxiliaire': cout_electricite_2024
168
+ };
169
+
170
+ /**
171
+ * Sélection du barème des prix des énergies selon la date d'établissement du DPE.
172
+ * @param dateDpe {string} date d'établissement du DPE (AAAA-MM-JJ)
173
+ * @returns {object} barème 2021 (Annexe 7) avant le 01/07/2024, barème mis à jour ensuite
174
+ */
175
+ export function getCoefCout(dateDpe) {
176
+ return new Date(dateDpe) >= new Date(DATE_BAREME_COUT_2024) ? coef_cout_2024 : coef_cout;
177
+ }
178
+
179
+ /** Clé (symbole) portant le barème des générateurs d'installations collectives d'un immeuble. */
180
+ const COUT_COLLECTIF = Symbol('cout_collectif');
181
+
182
+ /** Énergies dont le coût suit un barème par tranches, regroupées par énergie facturée. */
183
+ const ENERGIES_A_TRANCHES = {
184
+ 'électricité ch': 'électricité',
185
+ 'électricité ecs': 'électricité',
186
+ 'électricité fr': 'électricité',
187
+ 'électricité éclairage': 'électricité',
188
+ 'électricité auxiliaire': 'électricité',
189
+ "électricité d'origine renouvelable utilisée dans le bâtiment": 'électricité renouvelable',
190
+ 'gaz naturel': 'gaz naturel'
191
+ };
192
+
193
+ /**
194
+ * Table « masque » : consommation conservée pour les clés du groupe, annulée pour les autres.
195
+ * Permet de totaliser la consommation d'une énergie avec les fonctions d'agrégation existantes.
196
+ * @param groupe {string} énergie facturée (valeur de ENERGIES_A_TRANCHES)
197
+ * @returns {object}
198
+ */
199
+ export function masqueEnergie(groupe) {
200
+ const cles = Object.values(enums.type_energie)
201
+ .filter((label) => label !== 'électricité')
202
+ .concat(Object.keys(ENERGIES_A_TRANCHES));
203
+ return Object.fromEntries(
204
+ cles.map((cle) => [cle, ENERGIES_A_TRANCHES[cle] === groupe ? (c) => c : () => 0])
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Barème « prix moyen » pour les DPE établis à partir du 01/07/2024 (constaté sur Tribu et sur
210
+ * les DPE publiés par l'ADEME) : pour l'électricité et le gaz naturel, la tranche est déterminée
211
+ * sur la consommation annuelle TOTALE de l'énergie (tous usages) et le coût est réparti entre les
212
+ * postes au prorata de leur consommation.
213
+ * Immeuble : consommation des usages individuels rapportée au logement (N × f(C / N)) ;
214
+ * chauffage / ECS des installations collectives tarifés sur leur total (f(C)).
215
+ *
216
+ * @param base {object} barème (prix unitaires et fonctions par tranches)
217
+ * @param consoGroupe {function(masque): {total: number, collectif: number}} consommations EF
218
+ * @param nbLogements {number} nombre de logements (1 hors immeuble)
219
+ * @param genCollectifs {Set} générateurs de chauffage / ECS d'installations collectives
220
+ * @returns {object}
221
+ */
222
+ export function coefCoutProrata(base, consoGroupe, nbLogements, genCollectifs) {
223
+ const coef = { ...base };
224
+ const coefCollectif = { ...base };
225
+ new Set(Object.values(ENERGIES_A_TRANCHES)).forEach((groupe) => {
226
+ const { total, collectif } = consoGroupe(masqueEnergie(groupe));
227
+ const individuel = total - collectif;
228
+ Object.keys(ENERGIES_A_TRANCHES)
229
+ .filter((cle) => ENERGIES_A_TRANCHES[cle] === groupe)
230
+ .forEach((cle) => {
231
+ const f = base[cle];
232
+ const prix = individuel > 0 ? (nbLogements * f(individuel / nbLogements)) / individuel : 0;
233
+ const prixCollectif = collectif > 0 ? f(collectif) / collectif : 0;
234
+ coef[cle] = (c) => prix * c;
235
+ coefCollectif[cle] = (c) => prixCollectif * c;
236
+ });
237
+ });
238
+ coef[COUT_COLLECTIF] = { generateurs: genCollectifs, coef: coefCollectif };
239
+ return coef;
240
+ }
241
+
242
+ /** Barème d'un générateur : barème collectif s'il appartient à une installation collective. */
243
+ function coefPourGenerateur(coef, gen) {
244
+ const collectif = coef?.[COUT_COLLECTIF];
245
+ return collectif?.generateurs.has(gen) ? collectif.coef : coef;
246
+ }
247
+
125
248
  function getConso(coef, type_energie, conso) {
126
249
  // is coef is a function, execute it
127
250
  if (!coef) return conso;
@@ -197,7 +320,8 @@ export default function calc_conso(
197
320
  prorataECS,
198
321
  prorataChauffage,
199
322
  dateDpe,
200
- coeffEp
323
+ coeffEp,
324
+ nbLogements = 1
201
325
  ) {
202
326
  const gen_ch = ch.reduce((acc, ch) => {
203
327
  const generateur_chauffage = ch.generateur_chauffage_collection.generateur_chauffage;
@@ -237,6 +361,56 @@ export default function calc_conso(
237
361
  return acc.concat(generateur_ecs);
238
362
  }, []);
239
363
 
364
+ /**
365
+ * Barème des coûts : barème 2021 (par poste) avant le 01/07/2024 ; ensuite barème mis à jour
366
+ * avec tranche déterminée sur la consommation totale de l'énergie (voir coefCoutProrata).
367
+ */
368
+ const coefCout = () => {
369
+ const base = getCoefCout(dateDpe);
370
+ if (base === coef_cout) return coef_cout;
371
+ const genCollectifs = new Set();
372
+ if (nbLogements > 1) {
373
+ gen_ch
374
+ .filter((g) => g.donnee_entree.enum_type_installation_id !== '1')
375
+ .forEach((g) => genCollectifs.add(g));
376
+ ecs
377
+ .filter((inst) => (inst.donnee_entree.enum_type_installation_id ?? '1') !== '1')
378
+ .forEach((inst) =>
379
+ inst.generateur_ecs_collection.generateur_ecs.forEach((g) => genCollectifs.add(g))
380
+ );
381
+ }
382
+ const consoGroupe = (masque) => {
383
+ const tot = calc_conso_pond(
384
+ Sh,
385
+ zc_id,
386
+ vt,
387
+ gen_ch,
388
+ gen_ecs,
389
+ fr,
390
+ 'c',
391
+ masque,
392
+ prorataECS,
393
+ prorataChauffage,
394
+ ecs
395
+ );
396
+ const coll = calc_conso_pond(
397
+ 0,
398
+ zc_id,
399
+ [],
400
+ gen_ch.filter((g) => genCollectifs.has(g)),
401
+ gen_ecs.filter((g) => genCollectifs.has(g)),
402
+ [],
403
+ 'c',
404
+ masque,
405
+ prorataECS,
406
+ prorataChauffage,
407
+ []
408
+ );
409
+ return { total: tot.c_5_usages, collectif: coll.c_ch + coll.c_ecs };
410
+ };
411
+ return coefCoutProrata(base, consoGroupe, nbLogements, genCollectifs);
412
+ };
413
+
240
414
  const ret = {
241
415
  ef_conso: calc_conso_pond(
242
416
  Sh,
@@ -288,7 +462,7 @@ export default function calc_conso(
288
462
  gen_ecs,
289
463
  fr,
290
464
  'cout',
291
- coef_cout,
465
+ coefCout(),
292
466
  prorataECS,
293
467
  prorataChauffage,
294
468
  ecs,
@@ -424,7 +598,7 @@ function getEcsConso(gen_ecs, field, coef, prorataECS, prefix) {
424
598
  const conso = gen_ecs.donnee_intermediaire[field];
425
599
  const typeEnergie = getTypeEnergie(gen_ecs.donnee_entree, 'ecs');
426
600
 
427
- let coeffConsoEcs = { ...coef };
601
+ let coeffConsoEcs = { ...coefPourGenerateur(coef, gen_ecs) };
428
602
 
429
603
  if (prefix === 'emission_ges') {
430
604
  coeffConsoEcs[typeEnergie] = gen_ecs.donnee_utilisateur.coeffEmissionGes;
@@ -458,7 +632,7 @@ function getChauffageConso(gen_ch, field, coef, prorataChauffage, prefix) {
458
632
  ? gen_ch.donnee_entree.cle_repartition_ch || prorataChauffage
459
633
  : prorataChauffage;
460
634
 
461
- let coeffConsoChauffage = { ...coef };
635
+ let coeffConsoChauffage = { ...coefPourGenerateur(coef, gen_ch) };
462
636
 
463
637
  if (prefix === 'emission_ges') {
464
638
  coeffConsoChauffage[typeEnergie] = gen_ch.donnee_utilisateur.coeffEmissionGes;
package/conso.spec.js CHANGED
@@ -51,7 +51,11 @@ const {
51
51
  classe_bilan_dpe,
52
52
  classe_emission_ges,
53
53
  getCoefKey,
54
- coef_ep
54
+ coef_ep,
55
+ getCoefCout,
56
+ coefCoutProrata,
57
+ masqueEnergie,
58
+ DATE_BAREME_COUT_2024
55
59
  } = await import('./conso.js');
56
60
 
57
61
  /**
@@ -499,3 +503,185 @@ describe('calc_conso - agrégation des consommations', () => {
499
503
  expect(ids).toContain('1');
500
504
  });
501
505
  });
506
+
507
+ /**
508
+ * Barème des prix des énergies selon la date d'établissement du DPE.
509
+ * Barème 2021 : Annexe 7 de l'arrêté du 31/03/2021.
510
+ * Barème mis à jour (DPE à partir du 01/07/2024) : valeurs calées sur les sorties Tribu (CSTB)
511
+ * des autotests et sur les DPE publiés par l'ADEME (dataset dpe03existant), reproduites à ±1 €.
512
+ */
513
+ describe('getCoefCout - sélection du barème par date de DPE', () => {
514
+ test('date de bascule : 01/07/2024', () => {
515
+ expect(DATE_BAREME_COUT_2024).toBe('2024-07-01');
516
+ });
517
+
518
+ test('DPE établi avant le 01/07/2024 : barème 2021 inchangé', () => {
519
+ const coef = getCoefCout('2024-06-30');
520
+ // Annexe 7 : électricité 1000 kWh => 149 + 0,14066 × 1000
521
+ expect(coef['électricité ch'](1000)).toBeCloseTo(149 + 0.14066 * 1000, 9);
522
+ expect(coef['gaz naturel'](3000)).toBeCloseTo(0.11121 * 3000, 9);
523
+ expect(coef['fioul domestique']).toBe(0.09142);
524
+ expect(coef.propane).toBe(0.14305);
525
+ });
526
+
527
+ test.each(['2024-07-01', '2025-12-31', '2026-01-15'])(
528
+ 'DPE établi le %s : barème mis à jour',
529
+ (date) => {
530
+ const coef = getCoefCout(date);
531
+ // Tribu / ADEME : électricité 1000 kWh => 158 + 0,18954 × 1000
532
+ expect(coef['électricité ch'](1000)).toBeCloseTo(158 + 0.18954 * 1000, 9);
533
+ }
534
+ );
535
+
536
+ test('barème mis à jour : prix unitaires constants (fioul, propane, bois, réseau...)', () => {
537
+ const coef = getCoefCout('2026-01-15');
538
+ // Tribu : fioul 0,1482 €/kWh ; propane 0,1567 €/kWh (constants quelle que soit la conso)
539
+ expect(coef['fioul domestique']).toBe(0.14821);
540
+ expect(coef.propane).toBe(0.15672);
541
+ expect(coef.gpl).toBe(0.15672);
542
+ expect(coef.butane).toBe(0.23429);
543
+ expect(coef.charbon).toBe(0.02787);
544
+ expect(coef['réseau de chauffage urbain']).toBe(0.08921);
545
+ expect(coef['bois – granulés (pellets) ou briquettes']).toBe(0.09897);
546
+ expect(coef['bois – bûches']).toBe(0.042);
547
+ expect(coef['bois – plaquettes forestières']).toBe(0.042);
548
+ expect(coef['bois – plaquettes d’industrie']).toBe(0.042);
549
+ });
550
+
551
+ test.each([
552
+ // [conso, coût attendu] électricité, une valeur par tranche + limites
553
+ [500, 0.34721 * 500],
554
+ [999.99, 0.34721 * 999.99],
555
+ [1000, 158 + 0.18954 * 1000],
556
+ [2499.99, 158 + 0.18954 * 2499.99],
557
+ [2500, 158 + 0.18949 * 2500],
558
+ [4999.99, 158 + 0.18949 * 4999.99],
559
+ [5000, 119 + 0.19726 * 5000],
560
+ [14999.99, 119 + 0.19726 * 14999.99],
561
+ [15000, 78 + 0.20001 * 15000],
562
+ [20000, 78 + 0.20001 * 20000]
563
+ ])('barème mis à jour - électricité %s kWh => %s €', (conso, attendu) => {
564
+ const coef = getCoefCout('2026-01-15');
565
+ [
566
+ 'électricité ch',
567
+ 'électricité ecs',
568
+ 'électricité fr',
569
+ 'électricité éclairage',
570
+ 'électricité auxiliaire',
571
+ "électricité d'origine renouvelable utilisée dans le bâtiment"
572
+ ].forEach((cle) => expect(coef[cle](conso)).toBeCloseTo(attendu, 9));
573
+ });
574
+
575
+ test.each([
576
+ // [conso, coût attendu] gaz naturel, une valeur par tranche + limites
577
+ [3000, 0.1312 * 3000],
578
+ [5008.99, 0.1312 * 5008.99],
579
+ [5009, 182 + 0.09488 * 5009],
580
+ [50054.99, 182 + 0.09488 * 50054.99],
581
+ [50055, 288 + 0.09274 * 50055],
582
+ [100000, 288 + 0.09274 * 100000]
583
+ ])('barème mis à jour - gaz naturel %s kWh => %s €', (conso, attendu) => {
584
+ expect(getCoefCout('2026-01-15')['gaz naturel'](conso)).toBeCloseTo(attendu, 9);
585
+ });
586
+ });
587
+
588
+ describe("masqueEnergie / coefCoutProrata - tranche sur la conso totale de l'énergie", () => {
589
+ test('masque électricité : clés électriques conservées, autres énergies annulées', () => {
590
+ const masque = masqueEnergie('électricité');
591
+ expect(masque['électricité ch'](10)).toBe(10);
592
+ expect(masque['électricité auxiliaire'](10)).toBe(10);
593
+ expect(masque['gaz naturel'](10)).toBe(0);
594
+ expect(masque['réseau de chauffage urbain'](10)).toBe(0);
595
+ expect(masque["électricité d'origine renouvelable utilisée dans le bâtiment"](10)).toBe(0);
596
+ expect(masque['électricité']).toBeUndefined();
597
+ });
598
+
599
+ test('aucune consommation : prix moyen nul', () => {
600
+ const coef = coefCoutProrata(
601
+ getCoefCout('2026-01-15'),
602
+ () => ({ total: 0, collectif: 0 }),
603
+ 1,
604
+ new Set()
605
+ );
606
+ expect(coef['électricité ch'](100)).toBe(0);
607
+ expect(coef['gaz naturel'](100)).toBe(0);
608
+ // Prix constants conservés
609
+ expect(coef['fioul domestique']).toBe(0.14821);
610
+ });
611
+ });
612
+
613
+ describe('calc_conso - coût avec le barème mis à jour (DPE 2026)', () => {
614
+ const DATE_DPE_2026 = '2026-01-15';
615
+ let errorSpy;
616
+
617
+ beforeEach(() => {
618
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
619
+ });
620
+
621
+ afterEach(() => {
622
+ errorSpy.mockRestore();
623
+ });
624
+
625
+ test('DPE antérieur au 01/07/2024 : coût par poste au barème 2021 (inchangé)', () => {
626
+ const ch = [installCh({}, [genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 })])];
627
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, '2024-06-30', coef_ep);
628
+ // Chauffage tarifé seul : 122 + 0,15176 × 3000 ; éclairage seul : 0,29007 × 100
629
+ expect(res.cout.cout_ch).toBeCloseTo(122 + 0.15176 * 3000, 9);
630
+ expect(res.cout.cout_eclairage).toBeCloseTo(0.29007 * 100, 9);
631
+ });
632
+
633
+ test('maison tout électrique : tranche sur la conso électrique totale, coût réparti au prorata', () => {
634
+ const ch = [installCh({}, [genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 })])];
635
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE_2026, coef_ep);
636
+ // Conso élec totale = 3000 (ch) + 100 (éclairage = 1 × Sh) = 3100 kWh
637
+ const total = 158 + 0.18949 * 3100;
638
+ expect(res.cout.cout_5_usages).toBeCloseTo(total, 9);
639
+ expect(res.cout.cout_ch).toBeCloseTo((total * 3000) / 3100, 9);
640
+ expect(res.cout.cout_eclairage).toBeCloseTo((total * 100) / 3100, 9);
641
+ });
642
+
643
+ test('maison gaz + électricité : chaque énergie tarifée sur sa propre conso totale', () => {
644
+ const ch = [installCh({}, [genCh('2', { conso_ch: 8000, conso_ch_depensier: 8000 })])];
645
+ const ecs = [installEcs({}, [genEcs('2', { conso_ecs: 2000, conso_ecs_depensier: 2000 })])];
646
+ const res = calc_conso(100, 1, 1, [], ch, ecs, [], 1, 1, DATE_DPE_2026, coef_ep);
647
+ // Gaz total 10000 kWh => 182 + 0,09488 × 10000 ; élec (éclairage) 100 kWh => 0,34721 × 100
648
+ const gaz = 182 + 0.09488 * 10000;
649
+ expect(res.cout.cout_ch).toBeCloseTo((gaz * 8000) / 10000, 9);
650
+ expect(res.cout.cout_ecs).toBeCloseTo((gaz * 2000) / 10000, 9);
651
+ expect(res.cout.cout_eclairage).toBeCloseTo(0.34721 * 100, 9);
652
+ });
653
+
654
+ test('immeuble à chauffage individuel : barème appliqué par logement (N × f(C / N))', () => {
655
+ const ch = [installCh({}, [genCh('1', { conso_ch: 8000, conso_ch_depensier: 8000 })])];
656
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE_2026, coef_ep, 4);
657
+ // Élec totale 8100 kWh pour 4 logements : 4 × f(2025) = 4 × (158 + 0,18954 × 2025)
658
+ expect(res.cout.cout_5_usages).toBeCloseTo(4 * (158 + 0.18954 * 2025), 9);
659
+ });
660
+
661
+ test('immeuble à installations collectives : chauffage / ECS collectifs tarifés sur leur total', () => {
662
+ const ch = [
663
+ installCh({ enum_type_installation_id: '2' }, [
664
+ genCh('2', { conso_ch: 60000, conso_ch_depensier: 60000 })
665
+ ])
666
+ ];
667
+ const ecs = [
668
+ installEcs({ enum_type_installation_id: '2' }, [
669
+ genEcs('1', { conso_ecs: 4000, conso_ecs_depensier: 4000 })
670
+ ])
671
+ ];
672
+ const res = calc_conso(100, 1, 1, [], ch, ecs, [], 1, 1, DATE_DPE_2026, coef_ep, 4);
673
+ // Gaz collectif 60000 kWh => 288 + 0,09274 × 60000 (pas de division par logement)
674
+ expect(res.cout.cout_ch).toBeCloseTo(288 + 0.09274 * 60000, 9);
675
+ // ECS élec collective 4000 kWh => 158 + 0,18949 × 4000
676
+ expect(res.cout.cout_ecs).toBeCloseTo(158 + 0.18949 * 4000, 9);
677
+ // Éclairage (usage individuel) 100 kWh / 4 logements => 4 × 0,34721 × 25
678
+ expect(res.cout.cout_eclairage).toBeCloseTo(4 * 0.34721 * 25, 9);
679
+ });
680
+
681
+ test("immeuble, ECS sans type d'installation : considérée individuelle", () => {
682
+ const ecs = [installEcs({}, [genEcs('1', { conso_ecs: 4000, conso_ecs_depensier: 4000 })])];
683
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE_2026, coef_ep, 4);
684
+ // Élec totale 4100 kWh / 4 logements => 4 × (158 + 0,18954 × 1025)
685
+ expect(res.cout.cout_5_usages).toBeCloseTo(4 * (158 + 0.18954 * 1025), 9);
686
+ });
687
+ });