diff --git a/src/9_chauffage.js b/src/9_chauffage.js index f39914ce..5e313aad 100644 --- a/src/9_chauffage.js +++ b/src/9_chauffage.js @@ -110,15 +110,18 @@ export default function calc_chauffage( * Cas particulier des PAC hybrides avec répartition forfaitaire du besoin * @type {number|number} */ - const Pnominal = gen_ch.reduce((acc, gen) => acc + (gen.donnee_intermediaire.pn || 0), 0); - du.Pnominal = Pnominal; - const nbCascadeAndCombustion = gen_ch.filter( (value) => value.donnee_utilisateur.isCombustionGenerator && Number.parseInt(de.enum_cfg_installation_ch_id) === 1 ).length; + // Cascade avec priorité : seuls les générateurs principaux assurent le besoin (et dimensionnent + // le taux de charge) ; les générateurs secondaires (relève) ont un besoin nul. + const principaux = nbCascadeAndCombustion > 1 ? generateursPrincipauxCascade(gen_ch) : gen_ch; + const Pnominal = principaux.reduce((acc, gen) => acc + (gen.donnee_intermediaire.pn || 0), 0); + du.Pnominal = Pnominal; + // Nombre de générateurs avec une consommation des auxiliaires de distribution const nbGenWithAuxConsoDistribution = gen_ch.reduce((acc, gen) => { if (hasConsoForAuxDistribution(gen.donnee_entree.enum_type_generateur_ch_id)) { @@ -148,7 +151,8 @@ export default function calc_chauffage( nbCascadeAndCombustion, nbCascadeForSameEmetteur, Pnominal, - zc + zc, + principaux.includes(gen) ); /* c8 ignore next 2 -- repli défensif inatteignable : donnee_utilisateur est toujours renseignée par la première boucle sur gen_ch ci-dessus */ @@ -194,6 +198,29 @@ export default function calc_chauffage( ch.donnee_utilisateur = du; } +/** + * 13.2.1.3 Cascade de deux générateurs à combustion avec priorité + * + * `priorite_generateur_cascade` (modèle DPE ADEME) : 1 = générateur principal, 2 = secondaire… + * « Dans le cas de deux générateurs en cascade sans priorité, les deux générateurs sont déclarés + * comme principal. » Les générateurs principaux sont ceux de plus petite priorité ; si aucune + * priorité n'est renseignée, ou si toutes sont identiques, tous les générateurs sont principaux + * (cascade sans priorité : prorata des puissances nominales). + * + * Comme le moteur de référence CSTB (Tribu, autotests État APP5-0-4/41/43), le générateur + * prioritaire assure la totalité du besoin et son taux de charge est calculé avec sa seule Pn. + * + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.3 + * @param gen_ch {GenerateurChauffageItem[]} + * @return {GenerateurChauffageItem[]} générateurs principaux + */ +export function generateursPrincipauxCascade(gen_ch) { + const priorites = gen_ch.map((gen) => Number(gen.donnee_entree.priorite_generateur_cascade)); + if (priorites.some((p) => !(p > 0))) return gen_ch; + const prioriteMin = Math.min(...priorites); + return gen_ch.filter((_gen, i) => priorites[i] === prioriteMin); +} + /** * 13.2.1.3 Cascade de deux générateurs à combustion * Une puissance relative pour chaque générateur est calculée et appliquée à la conso globale de chauffage @@ -203,7 +230,14 @@ export default function calc_chauffage( * Cas particulier des PAC hybrides avec répartition forfaitaire du besoin * @type {number|number} */ -function getProrataGenerateur(genCh, nbCascadeAndCombustion, nbGenerateurCascade, Pnominal, zc) { +function getProrataGenerateur( + genCh, + nbCascadeAndCombustion, + nbGenerateurCascade, + Pnominal, + zc, + estPrincipal +) { // IDs des pompes à chaleur hybrides if ( genCh.donnee_entree.enum_type_generateur_ch_id >= 145 && @@ -233,9 +267,11 @@ function getProrataGenerateur(genCh, nbCascadeAndCombustion, nbGenerateurCascade } } - return nbCascadeAndCombustion > 1 - ? genCh.donnee_intermediaire.pn / Pnominal - : 1 / (nbGenerateurCascade || 1); + if (nbCascadeAndCombustion > 1) { + // générateur secondaire d'une cascade avec priorité : aucun besoin assuré + return estPrincipal ? genCh.donnee_intermediaire.pn / Pnominal : 0; + } + return 1 / (nbGenerateurCascade || 1); } /** diff --git a/src/9_chauffage.spec.js b/src/9_chauffage.spec.js index f926ace4..a0ab656d 100644 --- a/src/9_chauffage.spec.js +++ b/src/9_chauffage.spec.js @@ -58,7 +58,8 @@ vi.mock('./enums.js', () => ({ const { default: calc_chauffage, tauxChargeForGenerator, - isImmeuble + isImmeuble, + generateursPrincipauxCascade } = await import('./9_chauffage.js'); const { requestInput, tv, tvColumnIDs } = await import('./utils.js'); const { calc_emetteur_ch } = await import('./9_emetteur_ch.js'); @@ -457,6 +458,100 @@ describe('calc_chauffage - prorata entre générateurs', () => { }); }); +/** + * 13.2.1.3 Cascade de deux générateurs à combustion avec priorité + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.3 + */ +describe('calc_chauffage - cascade de générateurs à combustion avec priorité', () => { + /** Deux chaudières (combustion) en cascade sur un seul émetteur. */ + function cascade(prio1, prio2) { + vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => { + du.isCombustionGenerator = true; + }); + vi.mocked(tvColumnIDs).mockReturnValue([]); + return installation( + [ + generateur({ priorite_generateur_cascade: prio1 }, { pn: 17000 }), + generateur({ priorite_generateur_cascade: prio2 }, { pn: 13000 }) + ], + [emetteur('1')] + ); + } + + test('sans priorité : prorata des puissances nominales (Pnominal = somme des Pn)', () => { + const ch = cascade(undefined, undefined); + appel(ch, { bch: 3000 }); + expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(1700, 9); + expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBeCloseTo(1300, 9); + expect(ch.donnee_utilisateur.Pnominal).toBe(30000); + }); + + test('deux générateurs déclarés principaux (même priorité) : prorata des puissances', () => { + const ch = cascade(1, 1); + appel(ch, { bch: 3000 }); + expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(1700, 9); + expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBeCloseTo(1300, 9); + }); + + test('priorité 1 / 2 : le principal assure tout le besoin, le secondaire aucun', () => { + const ch = cascade(1, 2); + appel(ch, { bch: 3000, bch_dep: 3600 }); + const [appelPrincipal, appelSecondaire] = vi.mocked(calc_generateur_ch).mock.calls; + expect(appelPrincipal[5]).toBe(3000); + expect(appelPrincipal[6]).toBe(3600); + expect(appelSecondaire[5]).toBe(0); + expect(appelSecondaire[6]).toBe(0); + // le taux de charge (cdimref) est dimensionné sur la seule Pn du principal + expect(ch.donnee_utilisateur.Pnominal).toBe(17000); + }); + + test('priorité inversée (2 / 1) : le second générateur est le principal', () => { + const ch = cascade(2, 1); + appel(ch, { bch: 3000 }); + expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(0); + expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBe(3000); + expect(ch.donnee_utilisateur.Pnominal).toBe(13000); + }); + + test('générateur unique avec priorité : besoin entier (pas de cascade)', () => { + vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => { + du.isCombustionGenerator = true; + }); + vi.mocked(tvColumnIDs).mockReturnValue([]); + const ch = installation( + [generateur({ priorite_generateur_cascade: 2 }, { pn: 17000 })], + [emetteur('1')] + ); + appel(ch, { bch: 3000 }); + expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(3000); + expect(ch.donnee_utilisateur.Pnominal).toBe(17000); + }); +}); + +describe('generateursPrincipauxCascade', () => { + const gen = (priorite) => ({ donnee_entree: { priorite_generateur_cascade: priorite } }); + + test('priorité absente sur un générateur : tous principaux', () => { + const gens = [gen(1), gen(undefined)]; + expect(generateursPrincipauxCascade(gens)).toEqual(gens); + }); + + test('priorité nulle ou invalide : tous principaux', () => { + const gens = [gen('0'), gen('2')]; + expect(generateursPrincipauxCascade(gens)).toEqual(gens); + }); + + test('priorités saisies en chaîne : plus petite priorité retenue', () => { + const gens = [gen('2'), gen('1'), gen('3')]; + expect(generateursPrincipauxCascade(gens)).toEqual([gens[1]]); + }); + + test('plusieurs générateurs de même priorité minimale : tous retenus', () => { + const gens = [gen(1), gen(1), gen(2)]; + expect(generateursPrincipauxCascade(gens)).toEqual([gens[0], gens[1]]); + }); +}); + describe('calc_chauffage - auxiliaires de distribution répartis', () => { test('répartition de la consommation d’auxiliaires sur les générateurs éligibles', () => { vi.mocked(hasConsoForAuxDistribution).mockReturnValue(true);