From 9c768c0911d490260812c3d2bedec8bf48595855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20GAVIGNET?= Date: Tue, 11 Mar 2025 11:03:02 +0100 Subject: [PATCH] refactor: besoin ch and generation ch recup service refactoring --- src/features/dpe/domain/models/dpe.model.ts | 9 + .../models/installation-chauffage.model.ts | 4 +- .../dpe/infrastructure/froid/frTv.store.js | 6 +- .../apport-et-besoin.service.js | 46 ++- .../apport-et-besoin.service.spec.js | 105 +++++- .../apport_gratuit/apport-gratuit.service.js | 20 +- .../apport-gratuit.service.spec.js | 15 +- .../apport_et_besoin/ch/besoin-ch.service.js | 150 +++++++++ .../ch/besoin-ch.service.spec.js | 145 ++++++++ .../ch/perte-ch-recup.service.js | 168 ++++++++++ .../ch/perte-ch-recup.service.spec.js | 313 ++++++++++++++++++ .../ecs/perte-ecs-recup.service.js | 20 +- .../ecs/perte-ecs-recup.service.spec.js | 41 ++- .../domain/ecs/installation-ecs.service.js | 10 +- src/features/engine/domain/engine.service.js | 15 +- 15 files changed, 1026 insertions(+), 41 deletions(-) create mode 100644 src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.js create mode 100755 src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js create mode 100644 src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.js create mode 100755 src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js diff --git a/src/features/dpe/domain/models/dpe.model.ts b/src/features/dpe/domain/models/dpe.model.ts index 1613894..b4571c0 100755 --- a/src/features/dpe/domain/models/dpe.model.ts +++ b/src/features/dpe/domain/models/dpe.model.ts @@ -159,6 +159,15 @@ export interface Logement { installation_ecs_collection: { installation_ecs: InstallationEcs[] }; installation_chauffage_collection: { installation_chauffage: InstallationChauffage[] }; sortie: Sortie; + donnees_de_calcul: LogementDonneesCalcul; +} + +export interface LogementDonneesCalcul { + apportsInterneCh: { [key: string]: number }; + apportsInterneDepensier: { [key: string]: number }; + apportsSolaire: { [key: string]: number }; + besoinChauffageHP: { [key: string]: number }; + besoinChauffageDepensierHP: { [key: string]: number }; } export interface Tertiaire extends Logement {} diff --git a/src/features/dpe/domain/models/installation-chauffage.model.ts b/src/features/dpe/domain/models/installation-chauffage.model.ts index 99ff32b..4c01514 100755 --- a/src/features/dpe/domain/models/installation-chauffage.model.ts +++ b/src/features/dpe/domain/models/installation-chauffage.model.ts @@ -71,7 +71,7 @@ export interface GenerateurChauffageDE extends DE { enum_type_generateur_ch_id: number; enum_usage_generateur_id: number; enum_type_energie_id: number; - position_volume_chauffe: boolean; + position_volume_chauffe: number; tv_rendement_generation_id?: number; tv_scop_id?: number; tv_temp_fonc_100_id?: number; @@ -81,7 +81,7 @@ export interface GenerateurChauffageDE extends DE { identifiant_reseau_chaleur?: string; date_arrete_reseau_chaleur?: string; priorite_generateur_cascade?: number; - presence_ventouse?: boolean; + presence_ventouse?: number; presence_regulation_combustion?: boolean; enum_methode_saisie_carac_sys_id: number; enum_lien_generateur_emetteur_id: number; diff --git a/src/features/dpe/infrastructure/froid/frTv.store.js b/src/features/dpe/infrastructure/froid/frTv.store.js index fff729d..3e09da9 100644 --- a/src/features/dpe/infrastructure/froid/frTv.store.js +++ b/src/features/dpe/infrastructure/froid/frTv.store.js @@ -15,8 +15,10 @@ export class FrTvStore extends TvStore { * — e_fr_28 : ensoleillement reçu en période de refroidissement pour un mois donné et une consigne de refroidissement à 28°C (comportement conventionnel). * — textmoy_clim_26 : Température extérieure moyenne pour un mois donné et une consigne de refroidissement à 26°C (comportement conventionnel). * — textmoy_clim_28 : nombre d’heures de chauffage pour un mois donné et une consigne de refroidissement à 28°C (comportement conventionnel). + * — dh19 : degrés heures de chauffage pour un mois donné à 19°C (comportement conventionnel). + * — dh21 : degrés heures de chauffage pour un mois donné à 21°C (comportement dépensier). * - * @param type {'e', nref26' | 'nref28' | 'e_fr_26' | 'e_fr_28' | 'textmoy_clim_26' | 'textmoy_clim_28'} + * @param type {'e', nref26' | 'nref28' | 'e_fr_26' | 'e_fr_28' | 'textmoy_clim_26' | 'textmoy_clim_28'| 'dh19'| 'dh21'} * @param classeAltitude {string} * @param zoneClimatique {string} * @param mois {string} @@ -31,6 +33,6 @@ export class FrTvStore extends TvStore { values = values[ilpa]; } - return values[classeAltitude][mois][zoneClimatique]; + return parseFloat(values[classeAltitude][mois][zoneClimatique]); } } diff --git a/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.js b/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.js index 298e65a..f10c06c 100755 --- a/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.js +++ b/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.js @@ -5,6 +5,8 @@ import { BesoinFroidService } from './froid/besoin-froid.service.js'; import { ApportGratuitService } from './apport_gratuit/apport-gratuit.service.js'; import { InstallationEcsService } from '../ecs/installation-ecs.service.js'; import { PerteEcsRecupService } from './ecs/perte-ecs-recup.service.js'; +import { BesoinChService } from './ch/besoin-ch.service.js'; +import { PerteChRecupService } from './ch/perte-ch-recup.service.js'; /** * Calcul des déperditions de l’enveloppe GV @@ -21,6 +23,11 @@ export class ApportEtBesoinService { */ #besoinEcsService; + /** + * @type {BesoinChService} + */ + #besoinChService; + /** * @type {InstallationEcsService} */ @@ -36,6 +43,11 @@ export class ApportEtBesoinService { */ #besoinFroidService; + /** + * @type {PerteChRecupService} + */ + #perteChRecupService; + /** * @type {ApportGratuitService} */ @@ -43,25 +55,31 @@ export class ApportEtBesoinService { /** * @param besoinEcsService {BesoinEcsService} + * @param besoinChService {BesoinChService} * @param installationEcsService {InstallationEcsService} * @param perteEcsRecupService {PerteEcsRecupService} * @param besoinFroidService {BesoinFroidService} * @param surfaceSudEquivalenteService {SurfaceSudEquivalenteService} + * @param perteChRecupService {PerteChRecupService} * @param apportGratuitService {ApportGratuitService} */ constructor( besoinEcsService = inject(BesoinEcsService), + besoinChService = inject(BesoinChService), installationEcsService = inject(InstallationEcsService), perteEcsRecupService = inject(PerteEcsRecupService), besoinFroidService = inject(BesoinFroidService), surfaceSudEquivalenteService = inject(SurfaceSudEquivalenteService), + perteChRecupService = inject(PerteChRecupService), apportGratuitService = inject(ApportGratuitService) ) { this.#besoinEcsService = besoinEcsService; + this.#besoinChService = besoinChService; this.#installationEcsService = installationEcsService; this.#perteEcsRecupService = perteEcsRecupService; this.#besoinFroidService = besoinFroidService; this.#surfaceSudEquivalenteService = surfaceSudEquivalenteService; + this.#perteChRecupService = perteChRecupService; this.#apportGratuitService = apportGratuitService; } @@ -80,6 +98,26 @@ export class ApportEtBesoinService { */ this.#installationEcsService.execute(ctx, logement, besoinEcs); + const apportsInternes = this.#apportGratuitService.apportInterne(ctx, logement); + const apportsSolaires = this.#apportGratuitService.apportSolaire(ctx, logement); + + const pertesEcsRecup = this.#perteEcsRecupService.execute(ctx, logement); + // Besoin de chauffage hors pertes récupérées + const besoinChHP = this.#besoinChService.execute(ctx, logement); + const pertesChauffageRecup = this.#perteChRecupService.execute(ctx, logement); + + const besoinCh = { + besoin_ch: besoinChHP.besoin_ch_hp, + besoin_ch_depensier: besoinChHP.besoin_ch_depensier_hp + }; + besoinCh.besoin_ch -= pertesEcsRecup.pertes_distribution_ecs_recup; + besoinCh.besoin_ch -= pertesEcsRecup.pertes_stockage_ecs_recup; + besoinCh.besoin_ch -= pertesChauffageRecup.pertes_generateur_ch_recup; + + besoinCh.besoin_ch_depensier -= pertesEcsRecup.pertes_distribution_ecs_recup_depensier; + besoinCh.besoin_ch_depensier -= pertesEcsRecup.pertes_stockage_ecs_recup_depensier; + besoinCh.besoin_ch_depensier -= pertesChauffageRecup.pertes_generateur_ch_recup_depensier; + return { ...besoinEcs, ...{ @@ -92,9 +130,11 @@ export class ApportEtBesoinService { v40_ecs_journalier_depensier: ctx.nadeq * 79 }, ...this.#besoinFroidService.execute(ctx, logement), - ...this.#apportGratuitService.apportInterne(ctx, logement), - ...this.#apportGratuitService.apportSolaire(ctx, logement), - ...this.#perteEcsRecupService.execute(ctx, logement) + ...apportsInternes, + ...apportsSolaires, + ...pertesEcsRecup, + ...pertesChauffageRecup, + ...besoinCh }; } } diff --git a/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js b/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js index 181175b..fab7603 100755 --- a/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +++ b/src/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js @@ -7,6 +7,13 @@ import { BesoinFroidService } from './froid/besoin-froid.service.js'; import { ApportGratuitService } from './apport_gratuit/apport-gratuit.service.js'; import { PerteEcsRecupService } from './ecs/perte-ecs-recup.service.js'; import { InstallationEcsService } from '../ecs/installation-ecs.service.js'; +import { PerteChRecupService } from './ch/perte-ch-recup.service.js'; +import { BesoinChService } from './ch/besoin-ch.service.js'; +import corpus from '../../../../../test/corpus-sano.json'; +import { getAdemeFileJson } from '../../../../../test/test-helpers.js'; +import { PRECISION_PERCENT } from '../../../../../test/constant.js'; +import { DpeNormalizerService } from '../../../normalizer/domain/dpe-normalizer.service.js'; +import { ContexteBuilder } from '../contexte.builder.js'; /** @type {SurfaceSudEquivalenteService} **/ let surfaceSudEquivalenteService; @@ -26,6 +33,18 @@ let perteEcsRecupService; /** @type {ApportGratuitService} **/ let apportGratuitService; +/** @type {PerteChRecupService} **/ +let perteChRecupService; + +/** @type {BesoinChService} **/ +let besoinChService; + +/** @type {DpeNormalizerService} **/ +let normalizerService; + +/** @type {ContexteBuilder} **/ +let contexteBuilder; + /** @type {ApportEtBesoinService} **/ let service; @@ -37,18 +56,24 @@ describe('Calcul des apports et besoin du logement', () => { tvStore = new BaieVitreeTvStore(); surfaceSudEquivalenteService = new SurfaceSudEquivalenteService(tvStore); besoinEcsService = new BesoinEcsService(); + besoinChService = new BesoinChService(); besoinFroidService = new BesoinFroidService(); installationEcsService = new InstallationEcsService(); perteEcsRecupService = new PerteEcsRecupService(); apportGratuitService = new ApportGratuitService(); + perteChRecupService = new PerteChRecupService(); service = new ApportEtBesoinService( besoinEcsService, + besoinChService, installationEcsService, perteEcsRecupService, besoinFroidService, surfaceSudEquivalenteService, + perteChRecupService, apportGratuitService ); + normalizerService = new DpeNormalizerService(); + contexteBuilder = new ContexteBuilder(); }); test('Determination des apports et besoin du logement', () => { @@ -57,6 +82,10 @@ describe('Calcul des apports et besoin du logement', () => { besoin_ecs: 1526, besoin_ecs_depensier: 2685.3 }); + vi.spyOn(besoinChService, 'execute').mockReturnValue({ + besoin_ch_hp: 2563, + besoin_ch_depensier_hp: 3258.6 + }); vi.spyOn(besoinFroidService, 'execute').mockReturnValue({ besoin_fr: 896, besoin_fr_depensier: 1025.3 @@ -73,7 +102,12 @@ describe('Calcul des apports et besoin du logement', () => { vi.spyOn(perteEcsRecupService, 'execute').mockReturnValue({ pertes_distribution_ecs_recup: 354.2, pertes_distribution_ecs_recup_depensier: 532.6, - pertes_stockage_ecs_recup: 25.9 + pertes_stockage_ecs_recup: 25.9, + pertes_stockage_ecs_recup_depensier: 12.9 + }); + vi.spyOn(perteChRecupService, 'execute').mockReturnValue({ + pertes_generateur_ch_recup: 102.2, + pertes_generateur_ch_recup_depensier: 153.4 }); /** @type {Contexte} */ @@ -84,6 +118,8 @@ describe('Calcul des apports et besoin du logement', () => { surface_sud_equivalente: 18.5, besoin_ecs: 1526, besoin_ecs_depensier: 2685.3, + besoin_ch: 2080.7000000000003, + besoin_ch_depensier: 2559.7, besoin_fr: 896, besoin_fr_depensier: 1025.3, apport_solaire_ch: 5236.9, @@ -95,7 +131,10 @@ describe('Calcul des apports et besoin du logement', () => { v40_ecs_journalier_depensier: 197.5, pertes_distribution_ecs_recup: 354.2, pertes_distribution_ecs_recup_depensier: 532.6, - pertes_stockage_ecs_recup: 25.9 + pertes_generateur_ch_recup: 102.2, + pertes_generateur_ch_recup_depensier: 153.4, + pertes_stockage_ecs_recup: 25.9, + pertes_stockage_ecs_recup_depensier: 12.9 }); expect(surfaceSudEquivalenteService.execute).toHaveBeenCalledWith(ctx, logement.enveloppe); expect(besoinEcsService.execute).toHaveBeenCalledWith(ctx); @@ -103,4 +142,66 @@ describe('Calcul des apports et besoin du logement', () => { expect(apportGratuitService.apportSolaire).toHaveBeenCalledWith(ctx, logement); expect(apportGratuitService.apportInterne).toHaveBeenCalledWith(ctx, logement); }); + + describe("Test d'intégration pour les besoins en chauffage", () => { + test.each(corpus)( + 'Vérification de la DI besoin_ch des besoins en chauffage pour dpe %s', + (ademeId) => { + /** + * @type {Dpe} + */ + let dpeRequest = getAdemeFileJson(ademeId); + dpeRequest = normalizerService.normalize(dpeRequest); + + dpeRequest.logement.donnees_de_calcul = { + apportsInterneDepensier: [], + apportsInterneCh: [], + apportsSolaire: [], + besoinChauffageHP: [], + besoinChauffageDepensierHP: [] + }; + + /** @type {Contexte} */ + const ctx = contexteBuilder.fromDpe(dpeRequest); + const apportEtBesoin = service.execute(ctx, dpeRequest.logement); + + expect( + Math.abs( + apportEtBesoin.besoin_ch - dpeRequest.logement.sortie.apport_et_besoin.besoin_ch + ) / (dpeRequest.logement.sortie.apport_et_besoin.besoin_ch || 1) + ).toBeLessThan(PRECISION_PERCENT); + } + ); + + test.each(corpus)( + 'Vérification de la DI besoin_ch_depensier des besoins en chauffage pour dpe %s', + (ademeId) => { + /** + * @type {Dpe} + */ + let dpeRequest = getAdemeFileJson(ademeId); + dpeRequest = normalizerService.normalize(dpeRequest); + + dpeRequest.logement.donnees_de_calcul = { + apportsInterneDepensier: [], + apportsInterneCh: [], + apportsSolaire: [], + besoinChauffageHP: [], + besoinChauffageDepensierHP: [] + }; + + /** @type {Contexte} */ + const ctx = contexteBuilder.fromDpe(dpeRequest); + + const apportEtBesoin = service.execute(ctx, dpeRequest.logement); + + expect( + Math.abs( + apportEtBesoin.besoin_ch_depensier - + dpeRequest.logement.sortie.apport_et_besoin.besoin_ch_depensier + ) / (dpeRequest.logement.sortie.apport_et_besoin.besoin_ch_depensier || 1) + ).toBeLessThan(PRECISION_PERCENT); + } + ); + }); }); diff --git a/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.js b/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.js index 1153693..5200e1b 100644 --- a/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.js +++ b/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.js @@ -46,7 +46,7 @@ export class ApportGratuitService { return mois_liste.reduce( (acc, mois) => { - acc.apport_solaire_ch += this.apportSolaireMois( + const apportSolaire = this.apportSolaireMois( ctx, logement.enveloppe, mois, @@ -58,6 +58,9 @@ export class ApportGratuitService { ctx.inertie.ilpa ) ); + acc.apport_solaire_ch += apportSolaire; + logement.donnees_de_calcul.apportsSolaire[mois] = apportSolaire; + if (clim.length > 0) { acc.apport_solaire_fr += this.apportSolaireMois( ctx, @@ -84,7 +87,7 @@ export class ApportGratuitService { return mois_liste.reduce( (acc, mois) => { - acc.apport_interne_ch += this.apportInterneMois( + const apportInterne = this.apportInterneMois( ctx, this.#frTvStore.getData( 'nref19', @@ -94,6 +97,19 @@ export class ApportGratuitService { ctx.inertie.ilpa ) ); + acc.apport_interne_ch += apportInterne; + logement.donnees_de_calcul.apportsInterneCh[mois] = apportInterne; + logement.donnees_de_calcul.apportsInterneDepensier[mois] = this.apportInterneMois( + ctx, + this.#frTvStore.getData( + 'nref21', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ) + ); + if (clim.length > 0) { acc.apport_interne_fr += this.apportInterneMois( ctx, diff --git a/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.spec.js b/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.spec.js index e19221e..b306ff2 100755 --- a/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.spec.js +++ b/src/features/engine/domain/apport_et_besoin/apport_gratuit/apport-gratuit.service.spec.js @@ -41,7 +41,10 @@ describe('Calcul des apports gratuits au logement', () => { }; /** @type { Logement } **/ - const logement = { enveloppe: { porte_collection: {} } }; + const logement = { + enveloppe: { porte_collection: {} }, + donnees_de_calcul: { apportsSolaire: {} } + }; if (withClimatisation) { logement.climatisation_collection = { climatisation: [{}] }; @@ -81,6 +84,8 @@ describe('Calcul des apports gratuits au logement', () => { mois ); } + + expect(logement.donnees_de_calcul.apportsSolaire[mois]).toBe(185000); } }); @@ -106,7 +111,10 @@ describe('Calcul des apports gratuits au logement', () => { }; /** @type { Logement } **/ - const logement = { enveloppe: { porte_collection: {} } }; + const logement = { + enveloppe: { porte_collection: {} }, + donnees_de_calcul: { apportsInterneCh: {}, apportsInterneDepensier: {} } + }; if (withClimatisation) { logement.climatisation_collection = { climatisation: [{}] }; @@ -141,6 +149,9 @@ describe('Calcul des apports gratuits au logement', () => { mois ); } + + expect(logement.donnees_de_calcul.apportsInterneCh[mois]).toBeCloseTo(1306.33, 2); + expect(logement.donnees_de_calcul.apportsInterneDepensier[mois]).toBeCloseTo(1306.33, 2); } }); diff --git a/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.js b/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.js new file mode 100644 index 0000000..57faf00 --- /dev/null +++ b/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.js @@ -0,0 +1,150 @@ +import { mois_liste } from '../../../../../utils.js'; +import { inject } from 'dioma'; +import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js'; + +/** + * Calcul du besoin en chauffage + * Chapitre 2 - Expression du besoin de chauffage + * + * Methode_de_calcul_3CL_DPE_2021 - Page 7 + * Octobre 2021 + * @see consolide_anne…arrete_du_31_03_2021_relatif_aux_methodes_et_procedures_applicables.pdf + */ +export class BesoinChService { + /** + * @type {FrTvStore} + */ + #tvStore; + + /** + * @param tvStore {FrTvStore} + */ + constructor(tvStore = inject(FrTvStore)) { + this.#tvStore = tvStore; + } + + /** + * Besoin de chauffage hors pertes récupérées + * 9.1.1 - Consommation de chauffage + * + * @param ctx {Contexte} + * @param logement {Logement} + * @return {{besoin_ch_hp: number, besoin_ch_depensier_hp: number, fraction_apport_gratuit_ch: number, fraction_apport_gratuit_depensier_ch: number}} + */ + execute(ctx, logement) { + const besoinCh = mois_liste.reduce( + (acc, mois) => { + const dh19 = this.#tvStore.getData( + 'dh19', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ); + const dh21 = this.#tvStore.getData( + 'dh21', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ); + + const besoinCh = this.besoinChHorsPertesRecuperees(ctx, logement, mois, dh19); + acc.besoin_ch_hp += besoinCh.besoinChMoisHP; + acc.fraction_apport_gratuit_ch += besoinCh.fractionApportGratuitMois; + logement.donnees_de_calcul.besoinChauffageHP[mois] = besoinCh.besoinChMoisHP; + + const besoinChDepensier = this.besoinChHorsPertesRecuperees(ctx, logement, mois, dh21); + acc.besoin_ch_depensier_hp += besoinChDepensier.besoinChMoisHP; + acc.fraction_apport_gratuit_depensier_ch += besoinChDepensier.fractionApportGratuitMois; + logement.donnees_de_calcul.besoinChauffageDepensierHP[mois] = + besoinChDepensier.besoinChMoisHP; + + acc.dh19 += dh19; + acc.dh21 += dh21; + return acc; + }, + { + besoin_ch_hp: 0, + besoin_ch_depensier_hp: 0, + fraction_apport_gratuit_ch: 0, + fraction_apport_gratuit_depensier_ch: 0, + dh19: 0, + dh21: 0 + } + ); + + besoinCh.fraction_apport_gratuit_ch /= besoinCh.dh19; + besoinCh.fraction_apport_gratuit_depensier_ch /= besoinCh.dh21; + + delete besoinCh.dh19; + delete besoinCh.dh19; + + return besoinCh; + } + + /** + * Fraction des besoins de chauffage couverts par les apports gratuits pour un mois donné + * 6.1 Calcul de F + * + * @param ctx {Contexte} + * @param logement {Logement} + * @param mois {string} + * @param dh {number} - degrés-heures de chauffage sur le mois j (°Ch) + * @returns {number} + */ + fractionBesoinCh(ctx, logement, mois, dh) { + // Apports internes dans le logement sur le mois + const Ai = logement.donnees_de_calcul.apportsInterneCh[mois]; + // Apports solaires dans le logement sur le mois durant la période de chauffe + const As = logement.donnees_de_calcul.apportsSolaire[mois]; + + if (dh === 0) return 0; + + let pow; + switch (ctx.inertie.id) { + // Inertie très lourde ou lourde + case 1: + case 2: + pow = 3.6; + break; + // Inertie moyenne + case 3: + pow = 2.9; + break; + // Inertie légère + case 4: + pow = 2.5; + break; + } + + const Xj = (As + Ai) / (logement.sortie.deperdition.deperdition_enveloppe * dh); + return (Xj - Xj ** pow) / (1 - Xj ** pow); + } + + /** + * Besoin de chauffage hors pertes récupérées sur le mois (kWh) : + * 9.1.1 - Consommation de chauffage + * + * Fraction des besoins de chauffage couverts par les apports gratuits sur le mois + * 6.1 - Calcul de F + * + * @param ctx {Contexte} + * @param logement {Logement} + * @param mois {string} + * @param dh {number} - degrés-heures de chauffage sur le mois j (°Ch) + * @returns {{besoinChMoisHP: number, fractionApportGratuitMois: number}} + */ + besoinChHorsPertesRecuperees(ctx, logement, mois, dh) { + // Fraction des besoins de chauffage couverts par les apports gratuits pour un mois donné + const F = this.fractionBesoinCh(ctx, logement, mois, dh); + + // Besoin de chauffage d’un logement par kelvin sur le mois j (W/K) + const BV = logement.sortie.deperdition.deperdition_enveloppe * (1 - F); + + return { + besoinChMoisHP: (BV * dh) / 1000, + fractionApportGratuitMois: F * dh + }; + } +} diff --git a/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js b/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js new file mode 100755 index 0000000..c2eb6a5 --- /dev/null +++ b/src/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js @@ -0,0 +1,145 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js'; +import { BesoinChService } from './besoin-ch.service.js'; +import { mois_liste } from '../../../../../utils.js'; + +/** @type {BesoinChService} **/ +let service; + +/** @type {FrTvStore} **/ +let tvStore; + +describe('Calcul des besoins en chauffage du logement', () => { + beforeEach(() => { + tvStore = new FrTvStore(); + service = new BesoinChService(tvStore); + }); + + test.each([ + { + inertie: 1, + expected: 0.24 + }, + { + inertie: 2, + expected: 0.24 + }, + { + inertie: 3, + expected: 0.23 + }, + { + inertie: 4, + expected: 0.22 + } + ])( + 'Determination de la fraction des besoins de chauffage couverts par les apports gratuits pour un mois donné avec inertie = $inertie', + ({ inertie, expected }) => { + /** @type {Contexte} */ + const ctx = { inertie: { id: inertie } }; + + /** @type { Logement } **/ + const logement = { + donnees_de_calcul: { + apportsInterneCh: { Janvier: 102.5 }, + apportsSolaire: { Janvier: 12.5 } + }, + sortie: { deperdition: { deperdition_enveloppe: 25.9 } } + }; + expect(service.fractionBesoinCh(ctx, logement, 'Janvier', 18)).toBeCloseTo(expected, 2); + } + ); + + test('Besoin de chauffage hors pertes récupérées et fraction des apports gratuits pour un mois', () => { + /** @type {Contexte} */ + const ctx = { inertie: { id: 1 } }; + + /** @type { Logement } **/ + const logement = { + donnees_de_calcul: { + apportsInterneCh: { Janvier: 102.5 }, + apportsSolaire: { Janvier: 12.5 } + }, + sortie: { deperdition: { deperdition_enveloppe: 25.9 } } + }; + + const besoinHP = service.besoinChHorsPertesRecuperees(ctx, logement, 'Janvier', 12.5); + expect(besoinHP.besoinChMoisHP).toBeCloseTo(0.2139, 4); + expect(besoinHP.fractionApportGratuitMois).toBeCloseTo(4.2412, 4); + }); + + test('Besoin total de chauffage hors pertes récupérées et fraction des apports gratuits', () => { + vi.spyOn(tvStore, 'getData').mockReturnValue(10); + + /** @type {Contexte} */ + const ctx = { + inertie: { id: 1, ilpa: 1 }, + altitude: { value: '400-800m' }, + zoneClimatique: { value: 'h3c' } + }; + + /** @type { Logement } **/ + const logement = { + donnees_de_calcul: { + apportsInterneCh: { + Janvier: 102.5, + Février: 102.5, + Mars: 102.5, + Avril: 102.5, + Mai: 102.5, + Juin: 102.5, + Juillet: 102.5, + Aout: 102.5, + Septembre: 102.5, + Octobre: 102.5, + Novembre: 102.5, + Décembre: 102.5 + }, + apportsSolaire: { + Janvier: 102.5, + Février: 102.5, + Mars: 102.5, + Avril: 102.5, + Mai: 102.5, + Juin: 102.5, + Juillet: 102.5, + Aout: 102.5, + Septembre: 102.5, + Octobre: 102.5, + Novembre: 102.5, + Décembre: 102.5 + }, + besoinChauffageHP: {}, + besoinChauffageDepensierHP: {} + }, + sortie: { deperdition: { deperdition_enveloppe: 25.9 } } + }; + + const besoinTotalHP = service.execute(ctx, logement); + expect(besoinTotalHP.besoin_ch_hp).toBeCloseTo(1.1388, 4); + expect(besoinTotalHP.besoin_ch_depensier_hp).toBeCloseTo(1.1388, 4); + expect(besoinTotalHP.fraction_apport_gratuit_ch).toBeCloseTo(0.6336, 4); + expect(besoinTotalHP.fraction_apport_gratuit_depensier_ch).toBeCloseTo(0.6336, 4); + + for (const mois of mois_liste) { + expect(tvStore.getData).toHaveBeenCalledWith( + 'dh19', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ); + + expect(tvStore.getData).toHaveBeenCalledWith( + 'dh21', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ); + + expect(logement.donnees_de_calcul.besoinChauffageHP[mois]).toBeCloseTo(0.0949, 4); + expect(logement.donnees_de_calcul.besoinChauffageDepensierHP[mois]).toBeCloseTo(0.0949, 4); + } + }); +}); diff --git a/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.js b/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.js new file mode 100644 index 0000000..9abf54c --- /dev/null +++ b/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.js @@ -0,0 +1,168 @@ +import { mois_liste } from '../../../../../utils.js'; +import { inject } from 'dioma'; +import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js'; + +/** + * Calcul des pertes récupérées de génération de chauffage + * Chapitre 9.1.1 Consommation de chauffage + * + * Données calculées + * — pertes_generateur_ch_recup : pertes récupérées de génération pour le chauffage (kWh) + * — pertes_generateur_ch_recup_depensier : pertes récupérées de génération pour le chauffage (kWh) pour le scénario dépensier + * + * Methode_de_calcul_3CL_DPE_2021 - Page 57 + * Octobre 2021 + * @see consolide_anne…arrete_du_31_03_2021_relatif_aux_methodes_et_procedures_applicables.pdf + */ +export class PerteChRecupService { + /** + * @type {FrTvStore} + */ + #tvStore; + + /** + * @param tvStore {FrTvStore} + */ + constructor(tvStore = inject(FrTvStore)) { + this.#tvStore = tvStore; + } + + /** + * Pertes récupérées des générateurs de chauffage pour le chauffage (kWh) + * + * @param ctx {Contexte} + * @param logement {Logement} + * @return {{pertes_generateur_ch_recup: number, pertes_generateur_ch_recup_depensier: number}} + */ + execute(ctx, logement) { + return { + pertes_generateur_ch_recup: this.pertesGenerateurChRecup(ctx, logement, false) / 1000, + pertes_generateur_ch_recup_depensier: this.pertesGenerateurChRecup(ctx, logement, true) / 1000 + }; + } + + /** + * Pertes récupérées des générateurs de chauffage pour le chauffage (Wh) + * 9.1.1 Consommation de chauffage + * + * @param ctx {Contexte} + * @param logement {Logement} + * @param depensier {boolean} + * @returns {number} + */ + pertesGenerateurChRecup(ctx, logement, depensier) { + const generateursWithPertesGeneration = this.generateursWithPertesGeneration(logement); + + return mois_liste.reduce((acc, mois) => { + return ( + acc + + this.Qrec( + generateursWithPertesGeneration, + this.#tvStore.getData( + depensier ? 'nref21' : 'nref19', + ctx.altitude.value, + ctx.zoneClimatique.value, + mois, + ctx.inertie.ilpa + ), + depensier + ? logement.donnees_de_calcul.besoinChauffageDepensierHP[mois] + : logement.donnees_de_calcul.besoinChauffageHP[mois] + ) + ); + }, 0); + } + + /** + * Pertes récupérées des générateurs de chauffage pour le chauffage + * 9.1.1 Consommation de chauffage + * + * Seules les pertes des générateurs en volume chauffé sont récupérables. + * Les pertes récupérées des générateurs d’air chaud sont nulles. + * + * @param logement {Logement} + */ + generateursWithPertesGeneration(logement) { + const installationsCh = + logement.installation_chauffage_collection?.installation_chauffage || []; + + return installationsCh.reduce((acc, installation) => { + // Liste des générateurs de chauffage pour lesquels il y a une récupération d'énergie + return acc.concat( + (installation.generateur_chauffage_collection?.generateur_chauffage || []).filter( + (generateurChauffage) => { + const generateurChauffageDE = generateurChauffage.donnee_entree; + + /** + * 50 - générateur à air chaud à combustion avant 2006 + * 51 - générateur à air chaud à combustion standard a partir de 2006 + * 52 - générateur à air chaud à combustion à condensation a partir de 2006 + */ + return ( + (generateurChauffageDE.position_volume_chauffe ?? 0) === 1 && + ![50, 51, 52].includes(parseInt(generateurChauffageDE.enum_type_generateur_ch_id)) + ); + } + ) + ); + }, []); + } + + /** + * Pertes récupérées de génération pour le chauffage sur le mois j (Wh) + * + * @param generateurs {GenerateurChauffage[]} + * @param nref {number} + * @param besoinChauffageMois {number} + * @constructor + */ + Qrec(generateurs, nref, besoinChauffageMois) { + return generateurs.reduce((acc, generateur) => { + const generateurChauffageDE = generateur.donnee_entree; + const generateurChauffageDI = generateur.donnee_intermediaire; + + /** + * Part des pertes par les parois égale à 0,75 pour les équipements à ventouse ou assistés par ventilateur + * et 0,5 pour les autres + * @type {number} + */ + const Cper = parseInt(generateurChauffageDE.presence_ventouse || 0) === 1 ? 0.75 : 0.5; + + /** + * Durée pendant laquelle les pertes sont récupérées sur le mois (h) + */ + let Dperj; + switch (parseInt(generateurChauffageDE.enum_usage_generateur_id)) { + case 1: + // Pour les générateurs assurant le chauffage uniquement + Dperj = Math.min( + nref, + this.#pertes_gen_ch(besoinChauffageMois, generateurChauffageDI.pn) + ); + break; + case 2: + // Pour les générateurs assurant l’ECS uniquement + Dperj = this.#pertes_gen_ecs(nref); + break; + case 3: + // Pour les générateurs assurant le chauffage et l’ECS + Dperj = Math.min( + nref, + this.#pertes_gen_ch(besoinChauffageMois, generateurChauffageDI.pn) + + this.#pertes_gen_ecs(nref) + ); + break; + } + + return acc + 0.48 * Cper * generateurChauffageDI.qp0 * Dperj || 0; + }, 0); + } + + #pertes_gen_ch(Bch, pn) { + return (1.3 * Bch * 1000) / (0.3 * pn); + } + + #pertes_gen_ecs(nref) { + return (nref * 1790) / 8760; + } +} diff --git a/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js b/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js new file mode 100755 index 0000000..bbeddc9 --- /dev/null +++ b/src/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js @@ -0,0 +1,313 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import corpus from '../../../../../../test/corpus-sano.json'; +import { expect_or, getAdemeFileJson } from '../../../../../../test/test-helpers.js'; +import { DpeNormalizerService } from '../../../../normalizer/domain/dpe-normalizer.service.js'; +import { ContexteBuilder } from '../../contexte.builder.js'; +import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js'; +import { mois_liste } from '../../../../../utils.js'; +import { PerteChRecupService } from './perte-ch-recup.service.js'; +import { BesoinChService } from './besoin-ch.service.js'; +import { ApportGratuitService } from '../apport_gratuit/apport-gratuit.service.js'; +import { PRECISION_PERCENT } from '../../../../../../test/constant.js'; + +/** @type {PerteChRecupService} **/ +let service; + +/** @type {BesoinChService} **/ +let besoinChService; + +/** @type {ApportGratuitService} **/ +let apportGratuitService; + +/** @type {FrTvStore} **/ +let tvStore; + +/** @type {DpeNormalizerService} **/ +let normalizerService; + +/** @type {ContexteBuilder} **/ +let contexteBuilder; + +describe('Calcul des pertes de génération de chauffage récupérées', () => { + beforeEach(() => { + tvStore = new FrTvStore(); + service = new PerteChRecupService(tvStore); + besoinChService = new BesoinChService(); + apportGratuitService = new ApportGratuitService(); + normalizerService = new DpeNormalizerService(); + contexteBuilder = new ContexteBuilder(); + }); + + test.each([ + { + label: 'Installation CH uniquement avec ventouse, utilisation de nref', + enumUsageGenerateurId: 1, + presenceVentouse: 1, + expected: 562.5 + }, + { + label: 'Installation CH uniquement sans ventouse, utilisation de nref', + enumUsageGenerateurId: 1, + presenceVentouse: 0, + expected: 375 + }, + { + label: "Installation CH uniquement sans ventouse, pas d'utilisation de nref", + enumUsageGenerateurId: 1, + presenceVentouse: 0, + nref: 500, + expected: 7971.08 + }, + { + label: 'Installation ECS uniquement avec ventouse', + enumUsageGenerateurId: 2, + presenceVentouse: 1, + expected: 114.94 + }, + { + label: 'Installation ECS uniquement sans ventouse', + enumUsageGenerateurId: 2, + presenceVentouse: 0, + expected: 76.627 + }, + { + label: 'Installation ECS + CH avec ventouse, utilisation de nref', + enumUsageGenerateurId: 3, + presenceVentouse: 1, + expected: 562.5 + }, + { + label: 'Installation ECS + CH sans ventouse, utilisation de nref', + enumUsageGenerateurId: 3, + presenceVentouse: 0, + expected: 375 + }, + { + label: "Installation ECS + CH sans ventouse, pas d'utilisation de nref", + enumUsageGenerateurId: 3, + presenceVentouse: 0, + nref: 450, + expected: 10729.642 + } + ])( + 'Calcul des pertes récupérées de génération pour le chauffage sur un mois pour $label', + ({ enumUsageGenerateurId, presenceVentouse, nref = undefined, expected }) => { + /** @type {GenerateurChauffage[]} */ + const generateurs = [ + { + donnee_entree: { + presence_ventouse: presenceVentouse, + enum_usage_generateur_id: enumUsageGenerateurId + }, + donnee_intermediaire: { pn: 25000, qp0: 125 } + } + ]; + + expect(service.Qrec(generateurs, nref ?? 12.5, 1532.9)).toBeCloseTo(expected, 3); + } + ); + + test('Filtre des générateurs avec pertes de génération', () => { + const generateur1 = { + donnee_entree: {} + }; + const generateur2 = { + donnee_entree: { position_volume_chauffe: 0, enum_type_generateur_ch_id: 45 } + }; + const generateur3 = { + donnee_entree: { position_volume_chauffe: 0, enum_type_generateur_ch_id: 50 } + }; + const generateur4 = { + donnee_entree: { position_volume_chauffe: 1, enum_type_generateur_ch_id: 50 } + }; + const generateur5 = { + donnee_entree: { position_volume_chauffe: 1, enum_type_generateur_ch_id: 48 } + }; + + /** @type {Logement} */ + let logement = { installation_chauffage_collection: {} }; + expect(service.generateursWithPertesGeneration(logement)).toStrictEqual([]); + + logement.installation_chauffage_collection = { installation_chauffage: [] }; + expect(service.generateursWithPertesGeneration(logement)).toStrictEqual([]); + + logement.installation_chauffage_collection = { + installation_chauffage: [{ generateur_chauffage_collection: {} }] + }; + expect(service.generateursWithPertesGeneration(logement)).toStrictEqual([]); + + logement.installation_chauffage_collection = { + installation_chauffage: [{ generateur_chauffage_collection: { generateur_chauffage: [] } }] + }; + expect(service.generateursWithPertesGeneration(logement)).toStrictEqual([]); + + logement.installation_chauffage_collection = { + installation_chauffage: [ + { + generateur_chauffage_collection: { + generateur_chauffage: [generateur1, generateur2, generateur3] + } + }, + { generateur_chauffage_collection: { generateur_chauffage: [generateur4, generateur5] } } + ] + }; + expect(service.generateursWithPertesGeneration(logement)).toStrictEqual([generateur5]); + }); + + test.each([ + { + label: 'Installation CH en mode conventionnel', + depensier: false, + expected: 288 + }, + { + label: 'Installation CH en mode dépensier', + depensier: true, + expected: 540 + } + ])('Pertes de chauffage récupérées pour $label', ({ depensier, expected }) => { + vi.spyOn(tvStore, 'getData').mockReturnValue(depensier ? 1.5 : 0.8); + + /** @type {Contexte} */ + const ctx = { + altitude: { value: '400-800m' }, + zoneClimatique: { value: 'h1a' }, + inertie: { ilpa: 1 } + }; + + /** @type {Logement} */ + const logement = { + installation_chauffage_collection: { + installation_chauffage: [ + { + generateur_chauffage_collection: { + generateur_chauffage: [ + { + donnee_entree: { + position_volume_chauffe: 1, + enum_type_generateur_ch_id: 48, + enum_usage_generateur_id: 1 + }, + donnee_intermediaire: { pn: 25000, qp0: 125 } + } + ] + } + } + ] + }, + donnees_de_calcul: { + besoinChauffageHP: { + Janvier: 102.5, + Février: 102.5, + Mars: 102.5, + Avril: 102.5, + Mai: 102.5, + Juin: 102.5, + Juillet: 102.5, + Aout: 102.5, + Septembre: 102.5, + Octobre: 102.5, + Novembre: 102.5, + Décembre: 102.5 + }, + besoinChauffageDepensierHP: { + Janvier: 112.5, + Février: 112.5, + Mars: 112.5, + Avril: 112.5, + Mai: 112.5, + Juin: 112.5, + Juillet: 112.5, + Aout: 112.5, + Septembre: 112.5, + Octobre: 112.5, + Novembre: 112.5, + Décembre: 112.5 + } + } + }; + + expect(service.pertesGenerateurChRecup(ctx, logement, depensier)).toBeCloseTo(expected, 3); + for (const mois of mois_liste) { + expect(tvStore.getData).toHaveBeenCalledWith( + depensier ? 'nref21' : 'nref19', + '400-800m', + 'h1a', + mois, + 1 + ); + } + }); + + describe("Test d'intégration des installations CH", () => { + test.each(corpus)('vérification des pertes de génération ch pour dpe %s', (ademeId) => { + let dpeRequest = getAdemeFileJson(ademeId); + dpeRequest = normalizerService.normalize(dpeRequest); + dpeRequest.logement.donnees_de_calcul = { + apportsInterneDepensier: [], + apportsInterneCh: [], + apportsSolaire: [], + besoinChauffageHP: [], + besoinChauffageDepensierHP: [] + }; + + /** @type {Contexte} */ + const ctx = contexteBuilder.fromDpe(dpeRequest); + apportGratuitService.apportInterne(ctx, dpeRequest.logement); + apportGratuitService.apportSolaire(ctx, dpeRequest.logement); + besoinChService.execute(ctx, dpeRequest.logement); + const pertesGeneration = service.execute(ctx, dpeRequest.logement); + + const expectedValue = dpeRequest.logement.sortie.apport_et_besoin.pertes_generateur_ch_recup; + const calculatedValue = pertesGeneration.pertes_generateur_ch_recup; + + expect_or( + () => + expect(Math.abs(calculatedValue - expectedValue) / (expectedValue || 1)).toBeLessThan( + PRECISION_PERCENT + ), + () => + expect( + Math.abs(calculatedValue - expectedValue * 1000) / (expectedValue * 1000 || 1) + ).toBeLessThan(PRECISION_PERCENT) + ); + }); + + test.each(corpus)( + 'vérification des pertes de génération ch depensier des installations CH pour dpe %s', + (ademeId) => { + let dpeRequest = getAdemeFileJson(ademeId); + dpeRequest = normalizerService.normalize(dpeRequest); + dpeRequest.logement.donnees_de_calcul = { + apportsInterneDepensier: [], + apportsInterneCh: [], + apportsSolaire: [], + besoinChauffageHP: [], + besoinChauffageDepensierHP: [] + }; + + /** @type {Contexte} */ + const ctx = contexteBuilder.fromDpe(dpeRequest); + apportGratuitService.apportInterne(ctx, dpeRequest.logement); + apportGratuitService.apportSolaire(ctx, dpeRequest.logement); + besoinChService.execute(ctx, dpeRequest.logement); + const pertesGeneration = service.execute(ctx, dpeRequest.logement); + + const expectedValue = + dpeRequest.logement.sortie.apport_et_besoin.pertes_generateur_ch_recup_depensier; + const calculatedValue = pertesGeneration.pertes_generateur_ch_recup_depensier; + + expect_or( + () => + expect(Math.abs(calculatedValue - expectedValue) / (expectedValue || 1)).toBeLessThan( + PRECISION_PERCENT + ), + () => + expect( + Math.abs(calculatedValue - expectedValue * 1000) / (expectedValue * 1000 || 1) + ).toBeLessThan(PRECISION_PERCENT) + ); + } + ); + }); +}); diff --git a/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.js b/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.js index 20443f7..705b210 100644 --- a/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.js +++ b/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.js @@ -29,20 +29,23 @@ export class PerteEcsRecupService { } /** + * Pertes de distribution et de stockage ECS (kWh) * @param ctx {Contexte} * @param logement {Logement} - * @return {{pertes_distribution_ecs_recup: number, pertes_distribution_ecs_recup_depensier: number, pertes_stockage_ecs_recup: number}} + * @return {{pertes_distribution_ecs_recup: number, pertes_distribution_ecs_recup_depensier: number, pertes_stockage_ecs_recup: number, pertes_stockage_ecs_recup_depensier: number}} */ execute(ctx, logement) { return { - pertes_distribution_ecs_recup: this.pertesDistributionEcsRecup(ctx, logement, false), - pertes_distribution_ecs_recup_depensier: this.pertesDistributionEcsRecup(ctx, logement, true), - pertes_stockage_ecs_recup: this.pertesStockageEcsRecup(ctx, logement) + pertes_distribution_ecs_recup: this.pertesDistributionEcsRecup(ctx, logement, false) / 1000, + pertes_distribution_ecs_recup_depensier: + this.pertesDistributionEcsRecup(ctx, logement, true) / 1000, + pertes_stockage_ecs_recup: this.pertesStockageEcsRecup(ctx, logement, false) / 1000, + pertes_stockage_ecs_recup_depensier: this.pertesStockageEcsRecup(ctx, logement, true) / 1000 }; } /** - * Pertes récupérées de distribution d’ECS pour le chauffage + * Pertes récupérées de distribution d’ECS pour le chauffage (Wh) * 9.1.1 Consommation de chauffage * * @param ctx {Contexte} @@ -89,14 +92,15 @@ export class PerteEcsRecupService { } /** - * Pertes récupérées du stockage d’ECS pour le chauffage + * Pertes récupérées du stockage d’ECS pour le chauffage (Wh) * 9.1.1 Consommation de chauffage * * @param ctx {Contexte} * @param logement {Logement} + * @param depensier {boolean} * @returns {number} */ - pertesStockageEcsRecup(ctx, logement) { + pertesStockageEcsRecup(ctx, logement, depensier) { const installationsEcs = logement.installation_ecs_collection?.installation_ecs || []; let pertesStockage = installationsEcs.reduce((acc, installation) => { @@ -111,7 +115,7 @@ export class PerteEcsRecupService { acc + pertesStockage * this.#tvStore.getData( - 'nref19', + depensier ? 'nref21' : 'nref19', ctx.altitude.value, ctx.zoneClimatique.value, mois, diff --git a/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js b/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js index 98b4b84..f6dc26d 100755 --- a/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +++ b/src/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js @@ -38,15 +38,15 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { { label: 'Installation ECS en mode conventionnel', depensier: false, - expected: 0.36 + expected: 0.192 }, { label: 'Installation ECS en mode dépensier', depensier: true, - expected: 0.72 + expected: 0.725 } ])('Calcul des pertes de distribution récupérées pour $label', ({ depensier, expected }) => { - vi.spyOn(tvStore, 'getData').mockReturnValue(1.5); + vi.spyOn(tvStore, 'getData').mockReturnValue(depensier ? 1.5 : 0.8); /** @type {Contexte} */ const ctx = { @@ -75,7 +75,7 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { } }; - expect(service.pertesDistributionEcsRecup(ctx, logement, depensier)).toBeCloseTo(expected, 2); + expect(service.pertesDistributionEcsRecup(ctx, logement, depensier)).toBeCloseTo(expected, 3); for (const mois of mois_liste) { expect(tvStore.getData).toHaveBeenCalledWith( depensier ? 'nref21' : 'nref19', @@ -87,8 +87,19 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { } }); - test('Calcul des pertes de stockage récupérées pour une installation ECS', () => { - vi.spyOn(tvStore, 'getData').mockReturnValue(1.5); + test.each([ + { + label: 'Installation ECS en mode conventionnel', + depensier: false, + expected: 63.209 + }, + { + label: 'Installation ECS en mode dépensier', + depensier: true, + expected: 118.516 + } + ])('Calcul des pertes de stockage récupérées pour $label', ({ depensier, expected }) => { + vi.spyOn(tvStore, 'getData').mockReturnValue(depensier ? 1.5 : 0.8); /** @type {Contexte} */ const ctx = { @@ -101,15 +112,21 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { const logement = { installation_ecs_collection: { installation_ecs: [ - { donnee_utilisateur: { QgwRecuperable: 120 } }, + { donnee_utilisateur: { QgwRecuperable: 120012 } }, { donnee_utilisateur: { QgwRecuperable: 150 } } ] } }; - expect(service.pertesStockageEcsRecup(ctx, logement)).toBeCloseTo(0.27); + expect(service.pertesStockageEcsRecup(ctx, logement, depensier)).toBeCloseTo(expected, 3); for (const mois of mois_liste) { - expect(tvStore.getData).toHaveBeenCalledWith('nref19', '400-800m', 'h1a', mois, 1); + expect(tvStore.getData).toHaveBeenCalledWith( + depensier ? 'nref21' : 'nref19', + '400-800m', + 'h1a', + mois, + 1 + ); } }); @@ -142,6 +159,7 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { ); } ); + test.each(corpus)( 'vérification des pertes de distribution ecs recup depensier des installations ECS pour dpe %s', (ademeId) => { @@ -170,6 +188,7 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { ); } ); + test.each(corpus)( 'vérification des pertes de stockage ecs recup des installations ECS pour dpe %s', (ademeId) => { @@ -190,10 +209,6 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => { expect(pertesStockage.pertes_stockage_ecs_recup).toBeCloseTo( dpeRequest.logement.sortie.apport_et_besoin.pertes_stockage_ecs_recup ), - () => - expect(pertesStockage.pertes_stockage_ecs_recup).toBeCloseTo( - dpeRequest.logement.sortie.apport_et_besoin.pertes_stockage_ecs_recup * 1000 - ), () => expect(pertesStockage.pertes_stockage_ecs_recup * 1000).toBeCloseTo( dpeRequest.logement.sortie.apport_et_besoin.pertes_stockage_ecs_recup diff --git a/src/features/engine/domain/ecs/installation-ecs.service.js b/src/features/engine/domain/ecs/installation-ecs.service.js index 3cdcc2a..eb71b51 100755 --- a/src/features/engine/domain/ecs/installation-ecs.service.js +++ b/src/features/engine/domain/ecs/installation-ecs.service.js @@ -58,8 +58,8 @@ export class InstallationEcsService { */ this.pertesDistributionStockageEcsInstallation( installationEcs, - besoinEcsInstallation, - besoinEcsDepensierInstallation + besoinEcsInstallation * 1000, + besoinEcsDepensierInstallation * 1000 ); }); } @@ -73,7 +73,7 @@ export class InstallationEcsService { } /** - * Return le besoin rationalisé de l'installation par rapport au besoin glbal du logement + * Return le besoin rationalisé de l'installation par rapport au besoin global du logement * * @param ctx {Contexte} * @param installationEcs {InstallationEcs} @@ -109,8 +109,8 @@ export class InstallationEcsService { * Qgw: pertes brutes annuelles de stockage (Wh) * * @param installationEcs {InstallationEcs} - * @param besoinEcsInstallation {number} - * @param besoinEcsDepensierInstallation {number} + * @param besoinEcsInstallation {number} // en Wh + * @param besoinEcsDepensierInstallation {number} // en Wh */ pertesDistributionStockageEcsInstallation( installationEcs, diff --git a/src/features/engine/domain/engine.service.js b/src/features/engine/domain/engine.service.js index 69278de..380845a 100755 --- a/src/features/engine/domain/engine.service.js +++ b/src/features/engine/domain/engine.service.js @@ -58,6 +58,14 @@ export class EngineService { confort_ete: undefined, qualite_isolation: undefined }; + proceededDpe.logement.donnees_de_calcul = { + apportsInterne: [], + apportsInterneDepensier: [], + apportsInterneCh: [], + apportsSolaire: [], + besoinChauffageHP: [], + besoinChauffageDepensierHP: [] + }; const ctx = this.#contextBuilder.fromDpe(proceededDpe); // Calcul de l'inertie @@ -101,6 +109,8 @@ export class EngineService { // Calcul du DPE dans le collectif + delete proceededDpe.logement.donnees_de_calcul; + return proceededDpe; } @@ -142,9 +152,10 @@ export class EngineService { m.emetteur_chauffage_collection.emetteur_chauffage?.map((n) => { delete n.donnee_intermediaire; }); - m.generateur_chauffage_collection.generateur_chauffage?.map((n) => { + // @todo calculer les données intermédiaires (notamment pn et qp0) + /*m.generateur_chauffage_collection.generateur_chauffage?.map((n) => { delete n.donnee_intermediaire; - }); + });*/ delete m.donnee_intermediaire; }); delete dpe.logement.production_elec_enr?.donnee_intermediaire;