@open3cl/engine 1.8.46 → 1.8.48

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.
@@ -256,8 +256,18 @@ export function tv_generateur_combustion(dpe, di, de, type, GV, tbase, methodeSa
256
256
 
257
257
  de.tv_generateur_combustion_id = Number(row.tv_generateur_combustion_id);
258
258
 
259
- const E = E_tab[de.presence_ventouse];
260
- const F = F_tab[de.presence_ventouse];
259
+ /**
260
+ * Les coefficients (E, F) dépendent de la présence d'un ventilateur ou autre dispositif de
261
+ * circulation d'air dans le circuit de combustion, et non de la ventouse (qui n'intervient que
262
+ * dans Cper). Le XML ne transporte pas cette donnée : elle est lue dans la fiche technique du
263
+ * générateur. Sans fiche technique, la ventouse est conservée comme approximation (choix
264
+ * Open3CL, la méthode ne prévoyant pas de valeur par défaut).
265
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.2
266
+ * @see : Tribu Calcul_generateur.cs (ventilateur_combustion)
267
+ */
268
+ const indexEF = de.ventilateurCombustion ?? de.presence_ventouse;
269
+ const E = E_tab[indexEF];
270
+ const F = F_tab[indexEF];
261
271
 
262
272
  let ratioVirtualisation = de.ratio_virtualisation || 1;
263
273
 
@@ -341,7 +351,77 @@ export function updateGenerateurCombustion(dpe, de, type) {
341
351
  updateGenerateurBouilleurs(dpe, de, type);
342
352
  updateGenerateurChaudieres(dpe, de, type);
343
353
  updateGenerateurPacs(dpe, de, type);
344
- addInfosFromFichesTechniques(dpe, de);
354
+ addInfosFromFichesTechniques(dpe, de, type);
355
+ }
356
+
357
+ /**
358
+ * Valeur de la sous-fiche « Présence d'une veilleuse » → 1 (oui), 0 (non) ou undefined (NC, autre).
359
+ * Valeurs observées : « oui », « non », « Non », « NC », « Générateur sans veilleuse ».
360
+ * @param valeur {string|number|undefined}
361
+ * @returns {1 | 0 | undefined}
362
+ */
363
+ export function valeurVeilleuse(valeur) {
364
+ const v = (valeur ?? '').toString().trim().toLowerCase();
365
+ if (v.startsWith('oui')) return 1;
366
+ if (v.startsWith('non') || v.includes('sans veilleuse') || v.startsWith('absence')) return 0;
367
+ return undefined;
368
+ }
369
+
370
+ /**
371
+ * Présence d'une veilleuse sur un générateur à combustion, lue dans la sous-fiche technique
372
+ * « Présence d'une veilleuse » (catégorie 7 pour le chauffage, 8 pour l'ECS) du générateur.
373
+ *
374
+ * Le modèle ADEME n'a pas de donnée d'entrée « veilleuse » : la fiche technique est la seule trace
375
+ * de la saisie. Le moteur de référence (Tribu) prend cette information en entrée (`Is_veilleuse`,
376
+ * DPE.DataClasses/generateur.cs) et annule Pveil en son absence
377
+ * (DPE.Core/Calcul_generateur.cs, `Pveil1 = 0` si `Is_veilleuse != oui`).
378
+ *
379
+ * Une même fiche peut décrire plusieurs générateurs à la suite (ex. LICIEL : deux chaudières dans
380
+ * la même fiche de catégorie 7). getFicheTechnique() renverrait alors la veilleuse du premier
381
+ * générateur pour tous : la sous-fiche retenue est la première « veilleuse » qui suit la sous-fiche
382
+ * dont la valeur contient la description du générateur, avant le générateur suivant. En cas de
383
+ * réponses contradictoires (descriptions identiques) ou d'absence, la présence est non renseignée.
384
+ *
385
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.4 / §14.1 (« Pveil : si veilleuse »)
386
+ *
387
+ * @param dpe {FullDpe}
388
+ * @param de {Donnee_entree}
389
+ * @param type {'ch' | 'ecs'}
390
+ * @returns {1 | 0 | undefined} 1 si veilleuse, 0 si pas de veilleuse, undefined si non renseigné
391
+ */
392
+ export function presenceVeilleuse(dpe, de, type) {
393
+ const description = (de.description ?? '').toString().toLowerCase();
394
+ if (!description) return undefined;
395
+
396
+ const categorie = type === 'ecs' ? '8' : '7';
397
+ const liste = (x) => (x === undefined || x === null ? [] : Array.isArray(x) ? x : [x]);
398
+ const valeurs = new Set();
399
+
400
+ for (const fiche of liste(dpe.fiche_technique_collection?.fiche_technique)) {
401
+ if (fiche?.enum_categorie_fiche_technique_id !== categorie) continue;
402
+ const sousFiches = liste(fiche.sous_fiche_technique_collection?.sous_fiche_technique);
403
+ const valeurMinuscule = (i) => (sousFiches[i]?.valeur ?? '').toString().toLowerCase();
404
+
405
+ for (let i = 0; i < sousFiches.length; i++) {
406
+ if (!valeurMinuscule(i).includes(description)) continue;
407
+ for (let j = i + 1; j < sousFiches.length; j++) {
408
+ const libelle = (sousFiches[j]?.description ?? '').toString().toLowerCase();
409
+ // début du générateur suivant
410
+ if (
411
+ valeurMinuscule(j).includes(description) ||
412
+ /^type (de )?g[ée]n[ée]rateur/.test(libelle)
413
+ ) {
414
+ break;
415
+ }
416
+ if (libelle.includes('veilleuse')) {
417
+ valeurs.add(valeurVeilleuse(sousFiches[j].valeur));
418
+ break;
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ return valeurs.size === 1 ? [...valeurs][0] : undefined;
345
425
  }
346
426
 
347
427
  /**
@@ -349,12 +429,19 @@ export function updateGenerateurCombustion(dpe, de, type) {
349
429
  *
350
430
  * @param dpe {FullDpe}
351
431
  * @param de {Donnee_entree}
432
+ * @param type {'ch' | 'ecs'}
352
433
  */
353
- function addInfosFromFichesTechniques(dpe, de) {
434
+ function addInfosFromFichesTechniques(dpe, de, type) {
435
+ const veilleuse = presenceVeilleuse(dpe, de, type);
436
+ if (veilleuse !== undefined) {
437
+ de.presence_veilleuse = veilleuse;
438
+ }
439
+
354
440
  // Récupération de la présence ou non d'un système de ventilation
441
+ // Fiches techniques : catégorie 7 pour le chauffage, catégorie 8 pour l'ECS
355
442
  const ficheTechnique = getFicheTechnique(
356
443
  dpe,
357
- '8',
444
+ type === 'ch' ? '7' : '8',
358
445
  'Présence ventilateur / dispositif circulation air dans circuit combustion',
359
446
  [de.description]
360
447
  )?.valeur;
@@ -362,6 +449,11 @@ function addInfosFromFichesTechniques(dpe, de) {
362
449
  if (ficheTechnique && ficheTechnique === 'oui') {
363
450
  de.presenceVentilateur = 1;
364
451
  }
452
+
453
+ // Présence d'un ventilateur dans le circuit de combustion (choix des coefficients E, F de qp0)
454
+ if (ficheTechnique === 'oui' || ficheTechnique === 'non') {
455
+ de.ventilateurCombustion = ficheTechnique === 'oui' ? 1 : 0;
456
+ }
365
457
  }
366
458
 
367
459
  /**
@@ -64,6 +64,8 @@ const {
64
64
  tv_generateur_combustion,
65
65
  findGenerateurChMixteJumeau,
66
66
  updateGenerateurCombustion,
67
+ presenceVeilleuse,
68
+ valeurVeilleuse,
67
69
  caracteristiquesGenerateurCombustion,
68
70
  puissanceFormuleQp0,
69
71
  PN_MAX_QP0_CHAUDIERE_GAZ
@@ -805,6 +807,20 @@ describe('tv_generateur_combustion - génération mixte avec plusieurs générat
805
807
  });
806
808
  });
807
809
 
810
+ /**
811
+ * Construit un DPE avec des fiches techniques (catégorie, liste [description, valeur]).
812
+ */
813
+ const dpeFiches = (...fiches) => ({
814
+ fiche_technique_collection: {
815
+ fiche_technique: fiches.map(([categorie, sousFiches]) => ({
816
+ enum_categorie_fiche_technique_id: categorie,
817
+ sous_fiche_technique_collection: {
818
+ sous_fiche_technique: sousFiches.map(([description, valeur]) => ({ description, valeur }))
819
+ }
820
+ }))
821
+ }
822
+ });
823
+
808
824
  /**
809
825
  * 13.2 - Orchestration des substitutions de générateurs et enrichissement fiches techniques.
810
826
  * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2
@@ -833,6 +849,250 @@ describe('updateGenerateurCombustion - orchestration des substitutions', () => {
833
849
  updateGenerateurCombustion({}, de, 'ch');
834
850
  expect(de.presenceVentilateur).toBeUndefined();
835
851
  });
852
+
853
+ /**
854
+ * Issue #35 : la fiche technique d'un générateur de chauffage est en catégorie 7, celle d'un
855
+ * générateur ECS en catégorie 8.
856
+ */
857
+ test('générateur de chauffage : fiche technique recherchée en catégorie 7 (#35)', () => {
858
+ getFicheTechnique.mockReturnValue(undefined);
859
+ const dpe = {};
860
+ updateGenerateurCombustion(dpe, { description: 'chaudière' }, 'ch');
861
+ expect(getFicheTechnique).toHaveBeenCalledWith(
862
+ dpe,
863
+ '7',
864
+ 'Présence ventilateur / dispositif circulation air dans circuit combustion',
865
+ ['chaudière']
866
+ );
867
+ });
868
+
869
+ test('générateur ECS : fiche technique recherchée en catégorie 8 (#35)', () => {
870
+ getFicheTechnique.mockReturnValue(undefined);
871
+ const dpe = {};
872
+ updateGenerateurCombustion(dpe, { description: 'chauffe-eau' }, 'ecs');
873
+ expect(getFicheTechnique).toHaveBeenCalledWith(
874
+ dpe,
875
+ '8',
876
+ 'Présence ventilateur / dispositif circulation air dans circuit combustion',
877
+ ['chauffe-eau']
878
+ );
879
+ });
880
+
881
+ test('ventilateur de combustion renseigné à 1 si la fiche indique « oui » (#35)', () => {
882
+ getFicheTechnique.mockReturnValue({ valeur: 'oui' });
883
+ const de = { description: 'gen' };
884
+ updateGenerateurCombustion({}, de, 'ch');
885
+ expect(de.ventilateurCombustion).toBe(1);
886
+ });
887
+
888
+ test('ventilateur de combustion renseigné à 0 si la fiche indique « non » (#35)', () => {
889
+ getFicheTechnique.mockReturnValue({ valeur: 'non' });
890
+ const de = { description: 'gen' };
891
+ updateGenerateurCombustion({}, de, 'ch');
892
+ expect(de.ventilateurCombustion).toBe(0);
893
+ });
894
+
895
+ test('ventilateur de combustion non renseigné sans fiche ou valeur inconnue (#35)', () => {
896
+ getFicheTechnique.mockReturnValue(undefined);
897
+ const deSansFiche = { description: 'gen' };
898
+ updateGenerateurCombustion({}, deSansFiche, 'ch');
899
+ expect(deSansFiche.ventilateurCombustion).toBeUndefined();
900
+
901
+ getFicheTechnique.mockReturnValue({ valeur: 'inconnu' });
902
+ const deInconnu = { description: 'gen' };
903
+ updateGenerateurCombustion({}, deInconnu, 'ch');
904
+ expect(deInconnu.ventilateurCombustion).toBeUndefined();
905
+ });
906
+
907
+ test('présence de veilleuse lue dans la fiche technique : presence_veilleuse renseigné', () => {
908
+ getFicheTechnique.mockReturnValue(null);
909
+ const dpe = dpeFiches([
910
+ '8',
911
+ [
912
+ ['Type générateur', 'Chauffe-eau gaz'],
913
+ ["Présence d'une veilleuse", 'oui']
914
+ ]
915
+ ]);
916
+ const de = { description: 'Chauffe-eau gaz' };
917
+ updateGenerateurCombustion(dpe, de, 'ecs');
918
+ expect(de.presence_veilleuse).toBe(1);
919
+ expect(de.presenceVentilateur).toBeUndefined();
920
+ });
921
+
922
+ test('fiche veilleuse absente : presence_veilleuse non renseigné', () => {
923
+ getFicheTechnique.mockReturnValue(null);
924
+ const de = { description: 'gen' };
925
+ updateGenerateurCombustion({}, de, 'ch');
926
+ expect(de).not.toHaveProperty('presence_veilleuse');
927
+ });
928
+ });
929
+
930
+ /**
931
+ * Issue #35 : les coefficients (E, F) de la formule qp0 dépendent de la présence d'un ventilateur
932
+ * dans le circuit de combustion (fiche technique), la ventouse n'étant qu'un repli.
933
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.2
934
+ */
935
+ describe('tv_generateur_combustion - coefficients (E, F) selon le ventilateur de combustion (#35)', () => {
936
+ const ROW_EF = { ...ROW_DEFAUT, rpn: 'E*40', qp0_perc: 'Pn*(E+F*log10(Pn))/100' };
937
+
938
+ test('ventouse mais pas de ventilateur : couple (E, F) sans ventilateur (2,5 ; -0,8)', () => {
939
+ tv.mockReturnValue({ ...ROW_EF });
940
+ const di = { pn: 24000 };
941
+ const de = {
942
+ enum_type_generateur_ch_id: '93',
943
+ ratio_virtualisation: 1,
944
+ presence_ventouse: 1,
945
+ ventilateurCombustion: 0
946
+ };
947
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
948
+ // E = 2.5 => rpn = 2.5*40/100 = 1
949
+ expect(di.rpn).toBeCloseTo(1, 9);
950
+ // qp0 = 24000 * (2.5 - 0.8 * log10(24)) / 100 -- valeur du DPE 2459E3195669T
951
+ expect(di.qp0).toBeCloseTo(334.99944159137164, 9);
952
+ });
953
+
954
+ test('ventilateur sans ventouse : couple (E, F) avec ventilateur (1,75 ; -0,55)', () => {
955
+ tv.mockReturnValue({ ...ROW_EF });
956
+ const di = { pn: 24000 };
957
+ const de = {
958
+ enum_type_generateur_ch_id: '93',
959
+ ratio_virtualisation: 1,
960
+ presence_ventouse: 0,
961
+ ventilateurCombustion: 1
962
+ };
963
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
964
+ expect(di.rpn).toBeCloseTo(0.7, 9);
965
+ // qp0 = 24000 * (1.75 - 0.55 * log10(24)) / 100 -- valeur de référence de régression
966
+ expect(di.qp0).toBeCloseTo(237.81211609406802, 9);
967
+ });
968
+
969
+ test('sans information de ventilateur : repli sur la présence de ventouse', () => {
970
+ tv.mockReturnValue({ ...ROW_EF });
971
+ const di = { pn: 24000 };
972
+ const de = { enum_type_generateur_ch_id: '93', ratio_virtualisation: 1, presence_ventouse: 1 };
973
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
974
+ expect(di.qp0).toBeCloseTo(237.81211609406802, 9);
975
+ });
976
+ });
977
+
978
+ /**
979
+ * Issue #39 : présence d'une veilleuse lue dans la sous-fiche technique du générateur.
980
+ * Tribu : entrée `Is_veilleuse` (DPE.DataClasses/generateur.cs), Pveil1 = 0 si absente
981
+ * (DPE.Core/Calcul_generateur.cs).
982
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.4 / §14.1
983
+ */
984
+ describe('presenceVeilleuse - fiche technique « Présence d’une veilleuse »', () => {
985
+ const ficheGen = (desc, veilleuse) => [
986
+ ['Type générateur', desc],
987
+ ["Année d'installation", '1995'],
988
+ ...(veilleuse === undefined ? [] : [['Présence d’une veilleuse', veilleuse]])
989
+ ];
990
+
991
+ test.each([
992
+ ['oui', 1],
993
+ [' Oui ', 1],
994
+ ['non', 0],
995
+ ['Non', 0],
996
+ ['Générateur sans veilleuse', 0],
997
+ ['Absence de veilleuse', 0],
998
+ ['NC', undefined],
999
+ ['', undefined],
1000
+ [undefined, undefined]
1001
+ ])('valeur « %s » -> %s', (valeur, attendu) => {
1002
+ expect(valeurVeilleuse(valeur)).toBe(attendu);
1003
+ const dpe = dpeFiches([
1004
+ '7',
1005
+ [
1006
+ ['Type générateur', 'Chaudière'],
1007
+ ['Présence d’une veilleuse', valeur]
1008
+ ]
1009
+ ]);
1010
+ expect(presenceVeilleuse(dpe, { description: 'Chaudière' }, 'ch')).toBe(attendu);
1011
+ });
1012
+
1013
+ test('catégorie 7 pour le chauffage, 8 pour l’ECS', () => {
1014
+ const dpe = dpeFiches(
1015
+ ['7', ficheGen('Chaudière gaz', 'oui')],
1016
+ ['8', ficheGen('Chaudière gaz', 'non')]
1017
+ );
1018
+ expect(presenceVeilleuse(dpe, { description: 'Chaudière gaz' }, 'ch')).toBe(1);
1019
+ expect(presenceVeilleuse(dpe, { description: 'Chaudière gaz' }, 'ecs')).toBe(0);
1020
+ });
1021
+
1022
+ test('plusieurs générateurs dans la même fiche : veilleuse du générateur décrit', () => {
1023
+ // cas réel 2551E0134306T (LICIEL) : condensation « non » puis standard « oui »
1024
+ const dpe = dpeFiches([
1025
+ '7',
1026
+ [
1027
+ ...ficheGen('Gaz Naturel - Chaudière gaz à condensation', 'non'),
1028
+ ...ficheGen('Gaz Naturel - Chaudière gaz standard', 'oui')
1029
+ ]
1030
+ ]);
1031
+ expect(
1032
+ presenceVeilleuse(dpe, { description: 'Gaz Naturel - Chaudière gaz standard' }, 'ch')
1033
+ ).toBe(1);
1034
+ expect(
1035
+ presenceVeilleuse(dpe, { description: 'Gaz Naturel - Chaudière gaz à condensation' }, 'ch')
1036
+ ).toBe(0);
1037
+ });
1038
+
1039
+ test('générateur sans ligne veilleuse : pas de reprise de celle du générateur suivant', () => {
1040
+ const dpe = dpeFiches([
1041
+ '7',
1042
+ [...ficheGen('Poêle bûche'), ...ficheGen('Chaudière standard', 'oui')]
1043
+ ]);
1044
+ expect(presenceVeilleuse(dpe, { description: 'Poêle bûche' }, 'ch')).toBeUndefined();
1045
+ });
1046
+
1047
+ test('même description répétée avec des réponses contradictoires : non renseigné', () => {
1048
+ // cas réel 2567E1314804D : deux « Chaudière standard », l'une avec, l'autre sans veilleuse
1049
+ const dpe = dpeFiches([
1050
+ '7',
1051
+ [...ficheGen('Chaudière standard', 'Oui'), ...ficheGen('Chaudière standard', 'Non')]
1052
+ ]);
1053
+ expect(presenceVeilleuse(dpe, { description: 'Chaudière standard' }, 'ch')).toBeUndefined();
1054
+ const dpe2 = dpeFiches(
1055
+ ['7', ficheGen('Chaudière standard', 'Oui')],
1056
+ ['7', ficheGen('Chaudière standard', 'Oui')]
1057
+ );
1058
+ expect(presenceVeilleuse(dpe2, { description: 'Chaudière standard' }, 'ch')).toBe(1);
1059
+ });
1060
+
1061
+ test('description absente, fiche absente ou générateur non décrit : non renseigné', () => {
1062
+ const dpe = dpeFiches(['7', ficheGen('Chaudière', 'oui')], ['2', [['x', 'Chaudière']]]);
1063
+ expect(presenceVeilleuse(dpe, {}, 'ch')).toBeUndefined();
1064
+ expect(presenceVeilleuse({}, { description: 'Chaudière' }, 'ch')).toBeUndefined();
1065
+ expect(presenceVeilleuse(dpe, { description: 'Poêle' }, 'ch')).toBeUndefined();
1066
+ const fichesUniques = {
1067
+ fiche_technique_collection: {
1068
+ fiche_technique: {
1069
+ enum_categorie_fiche_technique_id: '7',
1070
+ sous_fiche_technique_collection: {
1071
+ sous_fiche_technique: { description: 'Type', valeur: 'Chaudière' }
1072
+ }
1073
+ }
1074
+ }
1075
+ };
1076
+ expect(presenceVeilleuse(fichesUniques, { description: 'Chaudière' }, 'ch')).toBeUndefined();
1077
+ const sansSousFiche = {
1078
+ fiche_technique_collection: {
1079
+ fiche_technique: [null, { enum_categorie_fiche_technique_id: '7' }]
1080
+ }
1081
+ };
1082
+ expect(presenceVeilleuse(sansSousFiche, { description: 'Chaudière' }, 'ch')).toBeUndefined();
1083
+ const sousFichesVides = dpeFiches([
1084
+ '7',
1085
+ [
1086
+ ['Type', 'Chaudière'],
1087
+ [undefined, undefined],
1088
+ [undefined, 'x']
1089
+ ]
1090
+ ]);
1091
+ sousFichesVides.fiche_technique_collection.fiche_technique[0].sous_fiche_technique_collection.sous_fiche_technique.push(
1092
+ null
1093
+ );
1094
+ expect(presenceVeilleuse(sousFichesVides, { description: 'Chaudière' }, 'ch')).toBeUndefined();
1095
+ });
836
1096
  });
837
1097
 
838
1098
  /**
@@ -377,8 +377,14 @@ export function calc_generateur_combustion_ch(dpe, di, de, du) {
377
377
  }
378
378
  }
379
379
 
380
- // La puissance de la veilleuse est à prendre en compte seulement si elle est présente dans l'installation
381
- if (!di.pveilleuse) {
380
+ /**
381
+ * La puissance de la veilleuse n'est à prendre en compte que si une veilleuse est présente.
382
+ * Présence lue dans la fiche technique du générateur (de.presence_veilleuse, cf.
383
+ * presenceVeilleuse()) ; à défaut, déduite de la valeur intermédiaire pveilleuse du DPE.
384
+ * Tribu : DPE.Core/Calcul_generateur.cs, Pveil1 = 0 si Is_veilleuse != oui.
385
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.4
386
+ */
387
+ if (de.presence_veilleuse === 0 || (de.presence_veilleuse !== 1 && !di.pveilleuse)) {
382
388
  di.pveil = 0;
383
389
  }
384
390
 
@@ -857,6 +857,56 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
857
857
  expect(avecVeilleuse.rg).toBeLessThan((k * pmfou) / (pmcons + 0.15));
858
858
  });
859
859
 
860
+ /**
861
+ * Issue #39 : la présence de veilleuse lue dans la fiche technique (de.presence_veilleuse)
862
+ * prime sur la valeur intermédiaire pveilleuse du DPE. Sans fiche, comportement inchangé.
863
+ * Tribu : Pveil1 = 0 si Is_veilleuse != oui (DPE.Core/Calcul_generateur.cs).
864
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.4
865
+ */
866
+ describe('présence de veilleuse (issue #39)', () => {
867
+ const di = (extra) => ({
868
+ pn: 24000,
869
+ rpn: 0.87,
870
+ rpint: 0.84,
871
+ qp0: 0,
872
+ pveil: 150,
873
+ temp_fonc_30: 56,
874
+ temp_fonc_100: 70,
875
+ ...extra
876
+ });
877
+ const de = (extra) => ({
878
+ enum_type_generateur_ch_id: '86',
879
+ enum_type_energie_id: '2',
880
+ type_energie: 'gaz naturel',
881
+ presence_regulation_combustion: true,
882
+ description: 't',
883
+ ...extra
884
+ });
885
+
886
+ test('fiche « oui » sans pveilleuse exportée : Pveil de la table conservé', () => {
887
+ const d = di();
888
+ calc_generateur_combustion_ch({}, d, de({ presence_veilleuse: 1 }), { cdimref: 3 });
889
+ expect(d.pveil).toBe(150);
890
+ });
891
+
892
+ test('fiche « non » avec pveilleuse exportée : Pveil annulé', () => {
893
+ const d = di({ pveilleuse: 150 });
894
+ calc_generateur_combustion_ch({}, d, de({ presence_veilleuse: 0 }), { cdimref: 3 });
895
+ expect(d.pveil).toBe(0);
896
+ });
897
+
898
+ test('pas de fiche : Pveil conservé si pveilleuse exportée, annulé sinon', () => {
899
+ const avec = di({ pveilleuse: 150 });
900
+ calc_generateur_combustion_ch({}, avec, de(), { cdimref: 3 });
901
+ expect(avec.pveil).toBe(150);
902
+ const sans = di();
903
+ calc_generateur_combustion_ch({}, sans, de(), { cdimref: 3 });
904
+ expect(sans.pveil).toBe(0);
905
+ // la veilleuse dégrade le rendement
906
+ expect(avec.rg).toBeLessThan(sans.rg);
907
+ });
908
+ });
909
+
860
910
  test('les pertes à charge nulle QP0 réduisent le rendement de génération', () => {
861
911
  const di = () => ({
862
912
  pn: 23000,
@@ -391,8 +391,14 @@ export default function calc_gen_ecs(dpe, gen_ecs, ecs_di, ecs_de, GV, ca_id, zc
391
391
  }
392
392
  }
393
393
 
394
- // La puissance de la veilleuse est à prendre en compte seulement si elle est présente dans l'installation
395
- if (!di.pveilleuse) {
394
+ /**
395
+ * La puissance de la veilleuse n'est à prendre en compte que si une veilleuse est présente.
396
+ * Présence lue dans la fiche technique du générateur (de.presence_veilleuse, cf.
397
+ * presenceVeilleuse()) ; à défaut, déduite de la valeur intermédiaire pveilleuse du DPE.
398
+ * Tribu : DPE.Core/Calcul_generateur.cs, Pveil1 = 0 si Is_veilleuse != oui.
399
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §14.1
400
+ */
401
+ if (de.presence_veilleuse === 0 || (de.presence_veilleuse !== 1 && !di.pveilleuse)) {
396
402
  di.pveil = 0;
397
403
  }
398
404
 
@@ -548,6 +548,39 @@ describe('calc_gen_ecs - consommation par générateur', () => {
548
548
  expect(g.donnee_intermediaire.conso_ecs).toBeCloseTo(322.3456790123457, 9);
549
549
  });
550
550
 
551
+ /**
552
+ * Issue #39 : présence de veilleuse lue dans la fiche technique (de.presence_veilleuse).
553
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §14.1
554
+ */
555
+ test.each([
556
+ [1, undefined, 0.02],
557
+ [0, 1, 0],
558
+ [undefined, 1, 0.02],
559
+ [undefined, undefined, 0]
560
+ ])(
561
+ 'présence de veilleuse (fiche %s, pveilleuse %s) : pveil = %s',
562
+ (presenceVeilleuse, pveilleuse, attendu) => {
563
+ vi.mocked(tvColumnIDs).mockImplementation((table) =>
564
+ table === 'generateur_combustion' ? ['chaud'] : []
565
+ );
566
+ vi.mocked(tv_generateur_combustion).mockImplementation((dpe, di) => {
567
+ di.rpn = 0.9;
568
+ di.qp0 = 100;
569
+ di.pn = 20000;
570
+ di.pveil = 0.02;
571
+ di.pveilleuse = pveilleuse;
572
+ });
573
+ const g = gen({
574
+ type_energie: 'gaz',
575
+ enum_type_generateur_ecs_id: 'chaud',
576
+ enum_methode_saisie_carac_sys_id: '1',
577
+ presence_veilleuse: presenceVeilleuse
578
+ });
579
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'maison');
580
+ expect(g.donnee_intermediaire.pveil).toBe(attendu);
581
+ }
582
+ );
583
+
551
584
  test('chaudière combustion avec stockage (Qgw > 0) : rendement génération + stockage', () => {
552
585
  vi.mocked(tvColumnIDs).mockImplementation((table) =>
553
586
  table === 'generateur_combustion' ? ['chaud'] : []
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.45`** · branche `main` · généré le 2026-10-10
373
+ > **Version `1.8.47`** · branche `main` · généré le 2026-10-11
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 984</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>62 119</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>69,03 %</strong><br/><sub>réussite globale</sub></td>
380
+ <td align="center"><strong>62 220</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>69,15 %</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,42 %** | `█████████░░░░░░░░░░░` | 4 642 / 9 999 |
388
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,56 %** | `███████████████████░` | 9 256 / 10 000 |
389
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,71 %** | `██████████████████░░` | 8 964 / 9 992 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,66 %** | `██████████████████░░` | 8 865 / 9 999 |
391
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,23 %** | `█████████████████░░░` | 8 421 / 9 998 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,70 %** | `██████████████░░░░░░` | 7 170 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **66,40 %** | `█████████████░░░░░░░` | 6 639 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **54,14 %** | `███████████░░░░░░░░░` | 5 412 / 9 997 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,45 %** | `█████████░░░░░░░░░░░` | 4 645 / 9 999 |
388
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,84 %** | `███████████████████░` | 9 284 / 10 000 |
389
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,93 %** | `██████████████████░░` | 8 986 / 9 992 |
390
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,83 %** | `██████████████████░░` | 8 882 / 9 999 |
391
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,51 %** | `█████████████████░░░` | 8 449 / 9 998 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,71 %** | `██████████████░░░░░░` | 7 171 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **66,41 %** | `█████████████░░░░░░░` | 6 640 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **54,15 %** | `███████████░░░░░░░░░` | 5 413 / 9 997 |
395
395
  | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,50 %** | `██████░░░░░░░░░░░░░░` | 2 750 / 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>5,18 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,51 ms</strong><br/><sub>médiane</sub></td>
406
+ <td align="center"><strong>5,29 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,62 ms</strong><br/><sub>médiane</sub></td>
408
408
  <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>468,0 ms</strong><br/><sub>max</sub></td>
409
+ <td align="center"><strong>817,3 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,87 ms | 4,40 ms | 0,22 ms | 50,4 ms | 8,77 ms | 12,5 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,08 ms | 3,72 ms | 1,48 ms | 33,5 ms | 7,05 ms | 9,43 ms |
417
- | **Logement individuel (2025)** | 4,71 ms | 4,20 ms | 1,54 ms | 35,6 ms | 8,74 ms | 11,8 ms |
418
- | **Maison individuelle (2025)** | 5,56 ms | 4,98 ms | 1,88 ms | 47,7 ms | 10,1 ms | 12,9 ms |
419
- | **Immeuble · chauffage individuel** | 6,18 ms | 5,30 ms | 0,11 ms | 78,2 ms | 12,5 ms | 19,5 ms |
420
- | **Appartement · chauffage collectif (2025)** | 5,27 ms | 4,59 ms | 1,62 ms | 45,8 ms | 11,3 ms | 18,1 ms |
421
- | **Immeuble · chauffage collectif** | 5,45 ms | 4,63 ms | 0,12 ms | 98,6 ms | 10,0 ms | 18,0 ms |
422
- | **Immeuble · chauffage mixte** | 5,05 ms | 4,14 ms | 0,24 ms | 468,0 ms | 10,5 ms | 16,9 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 5,44 ms | 4,66 ms | 1,71 ms | 43,7 ms | 10,2 ms | 15,6 ms |
415
+ | **Généraliste** | 5,04 ms | 4,61 ms | 0,23 ms | 39,5 ms | 9,03 ms | 12,6 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 4,02 ms | 3,69 ms | 1,36 ms | 32,4 ms | 6,82 ms | 8,63 ms |
417
+ | **Logement individuel (2025)** | 5,35 ms | 4,77 ms | 1,73 ms | 44,9 ms | 9,93 ms | 13,3 ms |
418
+ | **Maison individuelle (2025)** | 5,82 ms | 5,20 ms | 1,91 ms | 74,9 ms | 10,4 ms | 13,9 ms |
419
+ | **Immeuble · chauffage individuel** | 6,33 ms | 5,37 ms | 0,11 ms | 115,2 ms | 12,8 ms | 20,1 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,28 ms | 3,80 ms | 1,50 ms | 38,6 ms | 7,24 ms | 9,61 ms |
421
+ | **Immeuble · chauffage collectif** | 5,52 ms | 4,68 ms | 0,13 ms | 106,4 ms | 10,2 ms | 17,1 ms |
422
+ | **Immeuble · chauffage mixte** | 5,32 ms | 4,33 ms | 0,21 ms | 817,3 ms | 10,8 ms | 18,5 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 5,91 ms | 5,10 ms | 1,83 ms | 65,4 ms | 11,3 ms | 17,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/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.46';
36
+ const LIB_VERSION = '1.8.48';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.46",
3
+ "version": "1.8.48",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {