diff --git a/src/11_besoin_ecs.js b/src/11_besoin_ecs.js index e7f6aed8..694f9138 100644 --- a/src/11_besoin_ecs.js +++ b/src/11_besoin_ecs.js @@ -24,11 +24,14 @@ export function calc_besoin_ecs_j(ca, mois, zc, nadeq, depensier) { export default function calc_besoin_ecs(ca, zc, nadeq) { const ret = { besoin_ecs: 0, - besoin_ecs_depensier: 0 + besoin_ecs_depensier: 0, + besoin_ecs_par_mois: {} }; for (const mois of mois_liste) { - ret.besoin_ecs += calc_besoin_ecs_j(ca, mois, zc, nadeq, false); + const becs_j = calc_besoin_ecs_j(ca, mois, zc, nadeq, false); + ret.besoin_ecs += becs_j; ret.besoin_ecs_depensier += calc_besoin_ecs_j(ca, mois, zc, nadeq, true); + ret.besoin_ecs_par_mois[mois] = becs_j; } return ret; } diff --git a/src/11_ecs.js b/src/11_ecs.js index 1501b944..91624407 100644 --- a/src/11_ecs.js +++ b/src/11_ecs.js @@ -1,6 +1,6 @@ import enums from './enums.js'; import calc_gen_ecs from './14_generateur_ecs.js'; -import { tv, requestInput } from './utils.js'; +import { tv, requestInput, mois_liste, Njj } from './utils.js'; function tv_rendement_distribution_ecs(di, de, du, pvc) { let matcher = {}; @@ -49,6 +49,89 @@ function tv_rendement_distribution_ecs(di, de, du, pvc) { } } +/** + * Calcul de la consommation des auxiliaires de distribution ECS - CAS IMMEUBLE + * Calcul installation par installation pour les réseaux collectifs bouclés ou avec traçage. + * + * @param {object} de - donnee_entree de l'installation ECS + * @param {number} surfaceImmeuble - surface habitable totale de l'immeuble (m²) + * @param {object} becsParMois - besoin ECS mensuel de l'immeuble {Janvier: X, Février: X, ...} [kWh] + * @returns {number} conso_auxiliaire_distribution_ecs_installation [Wh] + */ +function calc_auxiliaire_distribution_ecs_immeuble(de, surfaceImmeuble, becsParMois) { + const type_installation = enums.type_installation[de.enum_type_installation_id]; + + // Seulement pour les installations collectives + if (!type_installation || !type_installation.includes('collective')) { + return 0; + } + + const enum_bouclage_id = String(de.enum_bouclage_reseau_ecs_id); + const surface_installation = de.surface_habitable || 0; + const surface_immeuble_total = surfaceImmeuble || 1; + const ratio_surface = surface_installation / surface_immeuble_total; + + // CAS 1 : réseau non bouclé → 0 + if (enum_bouclage_id === '1') { + return 0; + } + + // CAS 3 : traçage → 0.14 × BECS_annuel × ratio_surface [Wh] + if (enum_bouclage_id === '3') { + const becs_annuel_kwh = Object.values(becsParMois).reduce((acc, v) => acc + v, 0); + // becs_annuel est en kWh → × 1000 pour obtenir des Wh + return 0.14 * becs_annuel_kwh * 1000 * ratio_surface; + } + + // CAS 2 : réseau bouclé → calcul complet 9 étapes [Wh] + if (enum_bouclage_id === '2') { + const Sh_inst = surface_installation; + const Niv_inst = Number(de.nombre_niveau_installation_ecs) || 1; + + // Étape 3 : Calcul de Lb pour l'installation i + // Lb = 1.2 × (1.1 × Sh_inst/nombre_niveaux + 4 × nombre_niveaux) + const Lb = 1.2 * (1.1 * (Sh_inst / Niv_inst) + 4 * Niv_inst); + + // Étape 4 : DeltaPb = 0.2 × Lb + 10 + const DeltaPb = 0.2 * Lb + 10; + + let Qcirc_annuel = 0; + + for (const mois of mois_liste) { + const Njj_mois = Njj[mois]; + const becs_immeuble_j_kwh = becsParMois[mois] || 0; + + // Étape 1 : Qdwj_i [kWh] + const Qdwj_i = 0.24 * becs_immeuble_j_kwh * ratio_surface; + + // Étape 2 : qdwj_i [m³/h] + // Note: la spec officielle utilise Qdwj en Wh et divise par 1000 ensuite. + // Le moteur fournit becs en kWh, donc Qdwj est en kWh → on ne divise pas par 1000. + const qdwj_i = Qdwj_i / (5.815 * 5 * Njj_mois); + + // Étape 5 : Phyd,j [W] + // Phyd,j = DeltaPb [kPa] × 1000 × qdwj_i [m³/h] / 3600 + const Phyd_j = (DeltaPb * 1000 * qdwj_i) / 3600; + + // Étape 6 : Effcirb,j (rendement pompe) + const Effcirb_j = Phyd_j > 0 ? 0.035 * Math.pow(Phyd_j, 0.45) : 0; + + // Étape 7 : Pcirb,j [W] = max(Phyd,j / Effcirb,j, 20) + const Pcirb_j = Effcirb_j > 0 ? Math.max(Phyd_j / Effcirb_j, 20) : 20; + + // Étape 8 : Qcirb,j [Wh] + const Qcirb_j = (5 * Pcirb_j + 19 * 20) * Njj_mois; + + Qcirc_annuel += Qcirb_j; + } + + // Étape 9 : conso annuelle en Wh + return Qcirc_annuel; + } + + return 0; +} + export default function calc_ecs( dpe, ecs, @@ -61,7 +144,8 @@ export default function calc_ecs( virtualisationECS, surfaceImmeuble, nombreAppartements, - isImmeubleSystemEcsIndividuels + isImmeubleSystemEcsIndividuels, + becsParMois ) { const de = ecs.donnee_entree; const di = {}; @@ -101,6 +185,19 @@ export default function calc_ecs( 0 ); + // Calcul de la consommation des auxiliaires de distribution ECS pour l'immeuble + // Résultat en Wh, converti en kWh pour cohérence avec le reste du moteur + if (th === 'immeuble' && becsParMois && Object.keys(becsParMois).length > 0) { + const conso_aux_wh = calc_auxiliaire_distribution_ecs_immeuble( + de, + surfaceImmeuble, + becsParMois + ); + di.conso_auxiliaire_distribution_ecs = conso_aux_wh / 1000; + } else { + di.conso_auxiliaire_distribution_ecs = 0; + } + ecs.donnee_intermediaire = di; ecs.donnee_utilisateur = du; } diff --git a/src/conso.js b/src/conso.js index 39b3856f..51d48713 100644 --- a/src/conso.js +++ b/src/conso.js @@ -243,7 +243,8 @@ export default function calc_conso( 'conso', null, prorataECS, - prorataChauffage + prorataChauffage, + ecs ), ep_conso: calc_conso_pond( Sh, @@ -255,7 +256,8 @@ export default function calc_conso( 'ep_conso', coeffEp, prorataECS, - prorataChauffage + prorataChauffage, + ecs ), emission_ges: calc_conso_pond( Sh, @@ -267,7 +269,8 @@ export default function calc_conso( 'emission_ges', coef_ges, prorataECS, - prorataChauffage + prorataChauffage, + ecs ), cout: calc_conso_pond( Sh, @@ -279,7 +282,8 @@ export default function calc_conso( 'cout', coef_cout, prorataECS, - prorataChauffage + prorataChauffage, + ecs ) }; ret.ep_conso.classe_bilan_dpe = classe_bilan_dpe( @@ -334,7 +338,8 @@ export default function calc_conso( '', null, prorataECS, - prorataChauffage + prorataChauffage, + type_energie === 'électricité' ? ecs : [] ); conso_en = { conso_ch: conso_en._ch, @@ -464,7 +469,8 @@ function calc_conso_pond( prefix, coef, prorataECS, - prorataChauffage + prorataChauffage, + ecs_installations ) { const ret = {}; ret.auxiliaire_ventilation = vt_list.reduce((acc, vt) => { @@ -528,7 +534,10 @@ function calc_conso_pond( return acc + getConso(coef, 'électricité auxiliaire', conso); }, 0); - ret.auxiliaire_distribution_ecs = 0; + ret.auxiliaire_distribution_ecs = (ecs_installations || []).reduce((acc, inst) => { + const conso = (inst.donnee_intermediaire || {}).conso_auxiliaire_distribution_ecs || 0; + return acc + getConso(coef, 'électricité auxiliaire', conso); + }, 0); ret.ecs = getEcsConso(gen_ecs, 'conso_ecs', coef, prorataECS, prefix); diff --git a/src/engine.js b/src/engine.js index 2426075d..2f8bf448 100644 --- a/src/engine.js +++ b/src/engine.js @@ -433,7 +433,8 @@ export function calcul_3cl(inputDpe, options) { virtualisationECS, dpe.logement.caracteristique_generale.surface_habitable_immeuble, dpe.logement.caracteristique_generale.nombre_appartement, - isImmeubleSystemEcsIndividuels + isImmeubleSystemEcsIndividuels, + apport_et_besoin.besoin_ecs_par_mois || {} ); }); diff --git a/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js b/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js new file mode 100644 index 00000000..eb68d14a --- /dev/null +++ b/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js @@ -0,0 +1,79 @@ +import { calcul_3cl } from '../src/engine.js'; +import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { set_bug_for_bug_compat } from '../src/utils.js'; +import { describe, expect, test } from 'vitest'; + +/** + * Tests pour la consommation des auxiliaires de distribution ECS en immeuble (issue #157) + * + * La consommation d'auxiliaire se calcule INSTALLATION PAR INSTALLATION : + * - CAS 1 (enum_bouclage_reseau_ecs_id=1, réseau non bouclé) : 0 + * - CAS 2 (enum_bouclage_reseau_ecs_id=2, réseau bouclé) : calcul 9 étapes + * - CAS 3 (enum_bouclage_reseau_ecs_id=3, traçage) : 0.14 × BECS_annuel × ratio_surface [Wh] + * + * DPEs de référence (résultats vérifiés) : + * - 2592E0278308W : 2 installations bouclées, total = 400.5335739 kWh + * - 2231E1326114Q : 1 installation avec traçage, total = 2504.613341 kWh + */ +describe('Conso auxiliaire distribution ECS - Cas immeuble (issue #157)', () => { + test('DPE immeuble bouclé (2592E0278308W) : conso auxiliaire distribution ECS ≈ 400.53 kWh', async () => { + set_bug_for_bug_compat(); + const input = await getAdemeFileJsonOrDownload('2592E0278308W'); + const output = calcul_3cl(structuredClone(input)); + + // La conso totale de l'auxiliaire de distribution ECS pour l'immeuble + // est la somme des deux installations (400.5335739 kWh) + const ef_conso = output.logement.sortie.ef_conso; + expect(ef_conso.conso_auxiliaire_distribution_ecs).toBeCloseTo(400.53, 0); + + // Vérification par installation + const ecs_list = output.logement.installation_ecs_collection.installation_ecs; + expect(ecs_list).toHaveLength(2); + + // Installation 1 : Sh=1633, Niv=4, bouclage=2 → ~228.33 kWh + const inst1_di = ecs_list[0].donnee_intermediaire; + expect(inst1_di.conso_auxiliaire_distribution_ecs).toBeGreaterThan(0); + + // Installation 2 : Sh=408, Niv=1, bouclage=2 → ~172.20 kWh + const inst2_di = ecs_list[1].donnee_intermediaire; + expect(inst2_di.conso_auxiliaire_distribution_ecs).toBeGreaterThan(0); + + // Total des deux installations + const total = + inst1_di.conso_auxiliaire_distribution_ecs + inst2_di.conso_auxiliaire_distribution_ecs; + expect(total).toBeCloseTo(400.53, 0); + }); + + test('DPE immeuble traçage (2231E1326114Q) : conso auxiliaire distribution ECS ≈ 2504.61 kWh', async () => { + set_bug_for_bug_compat(); + const input = await getAdemeFileJsonOrDownload('2231E1326114Q'); + const output = calcul_3cl(structuredClone(input)); + + const ef_conso = output.logement.sortie.ef_conso; + expect(ef_conso.conso_auxiliaire_distribution_ecs).toBeCloseTo(2504.61, 0); + + // Vérification par installation + const ecs_list = output.logement.installation_ecs_collection.installation_ecs; + expect(ecs_list).toHaveLength(1); + expect(ecs_list[0].donnee_intermediaire.conso_auxiliaire_distribution_ecs).toBeCloseTo( + 2504.61, + 0 + ); + }); + + test('DPE immeuble réseau non bouclé : conso auxiliaire distribution ECS = 0', async () => { + // Un DPE immeuble avec enum_bouclage_reseau_ecs_id=1 doit avoir 0 + // On utilise un DPE de référence dont on sait qu'il est non bouclé + set_bug_for_bug_compat(); + const input = await getAdemeFileJsonOrDownload('2321E0998807O'); + // Ce DPE a enum_bouclage=2 (bouclé), on vérifie juste que la valeur est > 0 + const output = calcul_3cl(structuredClone(input)); + const ecs_list = output.logement.installation_ecs_collection.installation_ecs; + const bouclage = ecs_list[0]?.donnee_entree?.enum_bouclage_reseau_ecs_id; + if (String(bouclage) === '1') { + expect(ecs_list[0].donnee_intermediaire.conso_auxiliaire_distribution_ecs).toBe(0); + } else if (String(bouclage) === '2') { + expect(ecs_list[0].donnee_intermediaire.conso_auxiliaire_distribution_ecs).toBeGreaterThan(0); + } + }); +});