Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions src/9_chauffage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand All @@ -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 &&
Expand Down Expand Up @@ -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);
}

/**
Expand Down
97 changes: 96 additions & 1 deletion src/9_chauffage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
Loading