Energiberegning etter NS 3031:2025 med timesmetode og energirapport
enok leser en bygningsbeskrivelse i TOML, henter typisk klimaår for kommunen, simulerer 8760 timer etter NS-EN ISO 52016-1 og skriver energirapport, CSV og JSON. Motor - Femnodemodell for opake bygningsdeler, tonodemodell for vinduer, luftnode og strålingsnode per sone, implisitt løsning per time med egen tett LU-løser. - Varme- og kjølebehov ved eksakt interpolasjon mellom to løsninger, siden systemet er lineært. Vinduslufting for bygninger uten kjøleanlegg. - Solberegning etter ISO 52010-1, kontrollert mot pvlib med avvik under 1 prosent. Beregningsgang - Beregningspunktene A til E: netto, brutto, tilført, levert per energibærer og vektet levert energi, med solceller, klimakorrigering og effektbehov. - Energikarakter etter Enovas skala fra 1.1.2026, med arealkorreksjon. Inndata - TOML med scenarier for tiltak, og import fra IFC, gbXML og regneark. - Normerte verdier, karakterskala, vektingsfaktorer og klimadata i data/, som kan overstyres med --data. Kontroll - Stasjonært varmetap mot U·A·ΔT innenfor 2 prosent, energibalansen innenfor 2 prosent, varmetapsbudsjettet for TEK17-referansebygget identisk med publisert tabell. Kjent avvik i romoppvarming mot Simien 8 er dokumentert i README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! CSV-utskrift: timeserie for hele året og månedssummer.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn timeserie(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let mut w = csv::Writer::from_path(sti)?;
|
||||
w.write_record([
|
||||
"time",
|
||||
"maaned",
|
||||
"t_ute_C",
|
||||
"t_inne_C",
|
||||
"t_operativ_C",
|
||||
"sol_horisontal_W_per_m2",
|
||||
"soltilskudd_W",
|
||||
"internlast_W",
|
||||
"romoppvarming_W",
|
||||
"ventilasjonsvarme_W",
|
||||
"varmtvann_W",
|
||||
"romkjoling_W",
|
||||
"ventilasjonskjoling_W",
|
||||
"vifter_W",
|
||||
"pumper_W",
|
||||
"belysning_W",
|
||||
"utstyr_W",
|
||||
"solceller_W",
|
||||
])?;
|
||||
let sol = r.solceller.as_ref().map(|p| p.serie.as_slice());
|
||||
let ts = &r.timeserie;
|
||||
for t in 0..crate::TIMER {
|
||||
let f = |v: f64| format!("{v:.1}");
|
||||
w.write_record([
|
||||
t.to_string(),
|
||||
crate::MAANEDER[crate::maaned_for_time(t)].to_string(),
|
||||
format!("{:.2}", ts.t_ute[t]),
|
||||
format!("{:.2}", ts.t_inne[t]),
|
||||
format!("{:.2}", ts.t_operativ[t]),
|
||||
f(ts.sol_horisontal[t]),
|
||||
f(ts.soltilskudd[t]),
|
||||
f(ts.internlast[t]),
|
||||
f(ts.romoppvarming[t]),
|
||||
f(ts.ventilasjonsvarme[t]),
|
||||
f(ts.varmtvann[t]),
|
||||
f(ts.romkjoling[t]),
|
||||
f(ts.ventilasjonskjoling[t]),
|
||||
f(ts.vifter[t]),
|
||||
f(ts.pumper[t]),
|
||||
f(ts.belysning[t]),
|
||||
f(ts.utstyr[t]),
|
||||
f(sol.map(|s| s[t]).unwrap_or(0.0)),
|
||||
])?;
|
||||
}
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn maanedsverdier(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let m = r.sammendrag().maanedsverdier;
|
||||
let mut w = csv::Writer::from_path(sti)?;
|
||||
w.write_record([
|
||||
"maaned",
|
||||
"utetemperatur_C",
|
||||
"romoppvarming_kWh",
|
||||
"ventilasjonsvarme_kWh",
|
||||
"varmtvann_kWh",
|
||||
"kjoling_kWh",
|
||||
"vifter_pumper_kWh",
|
||||
"belysning_kWh",
|
||||
"utstyr_kWh",
|
||||
"sum_kWh",
|
||||
])?;
|
||||
for i in 0..12 {
|
||||
let sum = m.romoppvarming[i]
|
||||
+ m.ventilasjonsvarme[i]
|
||||
+ m.varmtvann[i]
|
||||
+ m.kjoling[i]
|
||||
+ m.vifter_pumper[i]
|
||||
+ m.belysning[i]
|
||||
+ m.utstyr[i];
|
||||
w.write_record([
|
||||
m.maaned[i].to_string(),
|
||||
format!("{:.1}", m.utetemperatur[i]),
|
||||
format!("{:.0}", m.romoppvarming[i]),
|
||||
format!("{:.0}", m.ventilasjonsvarme[i]),
|
||||
format!("{:.0}", m.varmtvann[i]),
|
||||
format!("{:.0}", m.kjoling[i]),
|
||||
format!("{:.0}", m.vifter_pumper[i]),
|
||||
format!("{:.0}", m.belysning[i]),
|
||||
format!("{:.0}", m.utstyr[i]),
|
||||
format!("{sum:.0}"),
|
||||
])?;
|
||||
}
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
//! Eksport til XML-formatet energimerkesystemet (EMS) tar imot for NS 3031:2014-beregninger.
|
||||
//!
|
||||
//! Feltnavnene følger Enovas eksempelfil i veiledningen for opplasting av flere filer.
|
||||
//! Formatet er knyttet til NS 3031:2014; Enova tar i tillegg imot JSON etter NS 3031:2025,
|
||||
//! men den spesifikasjonen er bare tilgjengelig bak ekspertinnlogging. Bruk derfor denne
|
||||
//! eksporten bevisst: tallene er beregnet etter NS 3031:2025 og skrives inn i 2014-strukturen.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::modell::{FlateType, VentilasjonType};
|
||||
use crate::systemer::Energibaerer;
|
||||
use crate::Result;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
fn esc(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
fn areal(r: &Resultat, f: impl Fn(&crate::modell::lost::LostFlate) -> bool) -> f64 {
|
||||
r.lost.flater.iter().filter(|x| f(x)).map(|x| x.areal).sum()
|
||||
}
|
||||
|
||||
fn u_snitt(r: &Resultat, f: impl Fn(&crate::modell::lost::LostFlate) -> bool + Copy) -> f64 {
|
||||
let a = areal(r, f);
|
||||
if a <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
r.lost
|
||||
.flater
|
||||
.iter()
|
||||
.filter(|x| f(x))
|
||||
.map(|x| x.areal * x.u_verdi)
|
||||
.sum::<f64>()
|
||||
/ a
|
||||
}
|
||||
|
||||
pub fn skriv(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
std::fs::write(sti, kilde(r))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kilde(r: &Resultat) -> String {
|
||||
let l = &r.lost;
|
||||
let b = &r.bygning.bygning;
|
||||
let ts = &r.timeserie;
|
||||
let kwh = crate::motor::Timeserie::sum_kwh;
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, r#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
r#"<Enheter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">"#
|
||||
);
|
||||
let _ = writeln!(s, " <Enhet>");
|
||||
|
||||
// ── adresse ────────────────────────────────────────────────────
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Adresser>\n <BygningsAdresse>\n <AdresseInfo>"
|
||||
);
|
||||
if let Some(a) = &b.adresse {
|
||||
let _ = writeln!(s, " <Gateadresse>{}</Gateadresse>", esc(a));
|
||||
}
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <BygningsbeskrivelseFritekst>{}</BygningsbeskrivelseFritekst>",
|
||||
esc(&b.navn)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" </AdresseInfo>\n </BygningsAdresse>\n </Adresser>"
|
||||
);
|
||||
|
||||
// ── inngangsverdier ────────────────────────────────────────────
|
||||
let _ = writeln!(s, " <Inngangsverdi>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Bygningskategori>{}</Bygningskategori>",
|
||||
esc(&r.karakter.kategorinavn.to_uppercase())
|
||||
);
|
||||
let _ = writeln!(s, " <Byggeaar>{}</Byggeaar>", b.byggeaar);
|
||||
let vegger = areal(r, |f| !f.glass && f.typ == FlateType::Vegg);
|
||||
let tak = areal(r, |f| !f.glass && f.typ == FlateType::Tak);
|
||||
let gulv = areal(r, |f| !f.glass && f.typ == FlateType::Gulv);
|
||||
let glass = areal(r, |f| f.glass);
|
||||
let _ = writeln!(s, " <M2Yttervegger>{vegger:.0}</M2Yttervegger>");
|
||||
let _ = writeln!(s, " <M2Tak>{tak:.0}</M2Tak>");
|
||||
let _ = writeln!(s, " <M2Gulv>{gulv:.0}</M2Gulv>");
|
||||
let _ = writeln!(s, " <M2VinduDorGlass>{glass:.0}</M2VinduDorGlass>");
|
||||
let _ = writeln!(s, " <BRAOppvarmet>{:.0}</BRAOppvarmet>", l.bra);
|
||||
let _ = writeln!(s, " <BRA>{:.0}</BRA>", l.bra);
|
||||
let _ = writeln!(s, " <M3OppvarmetLuft>{:.0}</M3OppvarmetLuft>", l.volum);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UYttervegger>{:.2}</UYttervegger>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Vegg)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UTak>{:.2}</UTak>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Tak)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UGulv>{:.2}</UGulv>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Gulv)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UVinduer>{:.2}</UVinduer>",
|
||||
u_snitt(r, |f| f.glass)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <ArealAndelVinduDorGlass>{:.1}</ArealAndelVinduDorGlass>",
|
||||
glass / l.bra * 100.0
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <NormalisertKuldebro>{:.2}</NormalisertKuldebro>",
|
||||
l.kuldebro_w_per_k / l.bra
|
||||
);
|
||||
let _ = writeln!(s, " <Lekkasjetall>{:.2}</Lekkasjetall>", l.n50);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <TempVirkningsgradVarmegjenvinner>{:.2}</TempVirkningsgradVarmegjenvinner>",
|
||||
l.ventilasjon.gjenvinning * 100.0
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <VifteeffektLuftmengderIDriftstiden>{:.2}</VifteeffektLuftmengderIDriftstiden>",
|
||||
l.ventilasjon.sfp
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <VentilasjonsluftmengdeIDriftstidenGjSn>{:.2}</VentilasjonsluftmengdeIDriftstidenGjSn>",
|
||||
l.ventilasjon.luftmengde
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <SettpunkttemperaturOppvarming>{:.1}</SettpunkttemperaturOppvarming>",
|
||||
l.settpunkt_oppvarming
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <SettpunkttemperaturKjoling>{:.1}</SettpunkttemperaturKjoling>",
|
||||
l.settpunkt_kjoling.unwrap_or(0.0)
|
||||
);
|
||||
let _ = writeln!(s, " <Driftstider>");
|
||||
let d = &l.laster.oppvarming_driftstid;
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Ventilasjon>{:.0}</Ventilasjon>",
|
||||
l.ventilasjon.driftstid.timer()
|
||||
);
|
||||
let _ = writeln!(s, " <Oppvarming>{:.0}</Oppvarming>", d.timer());
|
||||
let _ = writeln!(s, " </Driftstider>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Ventilasjonstype>{}</Ventilasjonstype>",
|
||||
match l.ventilasjon.typ {
|
||||
VentilasjonType::Balansert => "BALANSERT",
|
||||
VentilasjonType::Avtrekk => "AVTREKK",
|
||||
VentilasjonType::Naturlig => "NATURLIG",
|
||||
}
|
||||
);
|
||||
let kilder: Vec<String> = r
|
||||
.systemer
|
||||
.kilder
|
||||
.iter()
|
||||
.filter(|k| k.andel_rom > 1e-6)
|
||||
.map(|k| k.beskrivelse.clone())
|
||||
.collect();
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Oppvarmingssystem>{}</Oppvarmingssystem>",
|
||||
esc(&kilder.join("; "))
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <KlimastasjonKilde>{} (TMY CERRA 1991-2020)</KlimastasjonKilde>",
|
||||
esc(&r.klimanavn)
|
||||
);
|
||||
let _ = writeln!(s, " <Programvare>");
|
||||
let _ = writeln!(s, " <Applikasjon>enok</Applikasjon>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Versjon>{}</Versjon>",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <FriBeskrivelse>Dynamisk timesberegning etter NS 3031:2025 / NS-EN ISO 52016-1</FriBeskrivelse>"
|
||||
);
|
||||
let _ = writeln!(s, " </Programvare>");
|
||||
if let Some(e) = &b.energiraadgiver {
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Energiradgiver>\n <Navn>{}</Navn>\n </Energiradgiver>",
|
||||
esc(e)
|
||||
);
|
||||
}
|
||||
let _ = writeln!(s, " </Inngangsverdi>");
|
||||
|
||||
// ── beregningsresultat ─────────────────────────────────────────
|
||||
let _ = writeln!(s, " <Beregningsresultat>");
|
||||
let _ = writeln!(s, " <NettoEnergibudsjettPrKvm>");
|
||||
let p = |navn: &str, v: f64| format!(" <{navn}>{:.1}</{navn}>", v / l.bra);
|
||||
let _ = writeln!(s, "{}", p("Romoppvarming", kwh(&ts.romoppvarming)));
|
||||
let _ = writeln!(s, "{}", p("Ventilasjonsvarme", kwh(&ts.ventilasjonsvarme)));
|
||||
let _ = writeln!(s, "{}", p("Varmtvann", kwh(&ts.varmtvann)));
|
||||
let _ = writeln!(s, "{}", p("Vifter", kwh(&ts.vifter)));
|
||||
let _ = writeln!(s, "{}", p("Pumper", kwh(&ts.pumper)));
|
||||
let _ = writeln!(s, "{}", p("Belysning", kwh(&ts.belysning)));
|
||||
let _ = writeln!(s, "{}", p("TekniskUtstyr", kwh(&ts.utstyr)));
|
||||
let _ = writeln!(s, "{}", p("Romkjoling", kwh(&ts.romkjoling)));
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"{}",
|
||||
p("Ventilasjonskjoling", kwh(&ts.ventilasjonskjoling))
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <TotaltNettoEnergibehov>{:.1}</TotaltNettoEnergibehov>",
|
||||
r.budsjett.netto_spesifikk(l.bra)
|
||||
);
|
||||
let _ = writeln!(s, " </NettoEnergibudsjettPrKvm>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <LevertEnergiSpesifiktNormalisertKlima>{:.2}</LevertEnergiSpesifiktNormalisertKlima>",
|
||||
r.levert.spesifikk_vektet
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <LevertEnergiNormalisertKlima>{:.0}</LevertEnergiNormalisertKlima>",
|
||||
r.levert.vektet_totalt
|
||||
);
|
||||
let _ = writeln!(s, " <ForventetLevertEnergi>\n <LevertEnergi>");
|
||||
let hent = |b: Energibaerer| -> f64 {
|
||||
r.levert
|
||||
.per_baerer
|
||||
.iter()
|
||||
.find(|(x, _)| *x == b)
|
||||
.map(|(_, v)| *v)
|
||||
.unwrap_or(0.0)
|
||||
};
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Elektrisitet>{:.0}</Elektrisitet>",
|
||||
hent(Energibaerer::Elektrisitet)
|
||||
);
|
||||
let _ = writeln!(s, " <Olje>{:.0}</Olje>", hent(Energibaerer::Olje));
|
||||
let _ = writeln!(s, " <Gass>{:.0}</Gass>", hent(Energibaerer::Gass));
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Fjernvarme>{:.0}</Fjernvarme>",
|
||||
hent(Energibaerer::Fjernvarme)
|
||||
);
|
||||
let bio = hent(Energibaerer::BiobrenselLukket) + hent(Energibaerer::BiobrenselAapent);
|
||||
let _ = writeln!(s, " <Biobrensel>{bio:.0}</Biobrensel>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <AnnenEnergivare>{:.0}</AnnenEnergivare>",
|
||||
hent(Energibaerer::Annet)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Totalt>{:.0}</Totalt>",
|
||||
r.levert.levert_totalt
|
||||
);
|
||||
let _ = writeln!(s, " </LevertEnergi>\n </ForventetLevertEnergi>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Energikarakter>{}</Energikarakter>",
|
||||
r.karakter.bokstav
|
||||
);
|
||||
let _ = writeln!(s, " </Beregningsresultat>");
|
||||
let _ = writeln!(s, " </Enhet>\n</Enheter>");
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! JSON-utskrift av beregningsresultatet.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn skriv(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(&r.sammendrag())?;
|
||||
std::fs::write(sti, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn til_streng(r: &Resultat) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&r.sammendrag())?)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Rapportering: CSV-timeserier, JSON-resultat, Typst-rapport med PDF, og Enova-XML.
|
||||
|
||||
pub mod csv_ut;
|
||||
pub mod enova_xml;
|
||||
pub mod json_ut;
|
||||
pub mod svg;
|
||||
pub mod typst_ut;
|
||||
@@ -0,0 +1,260 @@
|
||||
//! Enkle SVG-figurer som bygges inn i Typst-rapporten. Diagrammene tegnes direkte som SVG
|
||||
//! slik at rapporten ikke er avhengig av eksterne Typst-pakker.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
|
||||
const BREDDE: f64 = 760.0;
|
||||
const HOYDE: f64 = 300.0;
|
||||
const VENSTRE: f64 = 58.0;
|
||||
const HOYRE: f64 = 16.0;
|
||||
const TOPP: f64 = 16.0;
|
||||
const BUNN: f64 = 42.0;
|
||||
|
||||
/// Fargene i figurene. Holdt mørke nok til å leses i svart-hvitt-utskrift.
|
||||
const FARGER: [&str; 7] = [
|
||||
"#8c2f1f", // romoppvarming
|
||||
"#c2703d", // ventilasjonsvarme
|
||||
"#3f6f8f", // varmtvann
|
||||
"#6f9ec4", // vifter og pumper
|
||||
"#d8b44a", // belysning
|
||||
"#9a9a4a", // utstyr
|
||||
"#4f8f8f", // kjøling
|
||||
];
|
||||
const NAVN: [&str; 7] = [
|
||||
"Romoppvarming",
|
||||
"Ventilasjonsvarme",
|
||||
"Varmtvann",
|
||||
"Vifter og pumper",
|
||||
"Belysning",
|
||||
"Teknisk utstyr",
|
||||
"Kjøling",
|
||||
];
|
||||
|
||||
fn esc(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn pent_maks(v: f64) -> f64 {
|
||||
if v <= 0.0 {
|
||||
return 1.0;
|
||||
}
|
||||
let eksponent = v.log10().floor();
|
||||
let skala = 10f64.powf(eksponent);
|
||||
let normalisert = v / skala;
|
||||
let trinn = [1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0];
|
||||
let valgt = trinn
|
||||
.iter()
|
||||
.find(|t| **t >= normalisert)
|
||||
.copied()
|
||||
.unwrap_or(10.0);
|
||||
valgt * skala
|
||||
}
|
||||
|
||||
/// Stablet søylediagram over månedlig energibruk, kWh.
|
||||
pub fn maanedsdiagram(r: &Resultat) -> String {
|
||||
let m = r.sammendrag().maanedsverdier;
|
||||
let serier = [
|
||||
m.romoppvarming,
|
||||
m.ventilasjonsvarme,
|
||||
m.varmtvann,
|
||||
m.vifter_pumper,
|
||||
m.belysning,
|
||||
m.utstyr,
|
||||
m.kjoling,
|
||||
];
|
||||
let mut totaler = [0.0f64; 12];
|
||||
for s in &serier {
|
||||
for i in 0..12 {
|
||||
totaler[i] += s[i];
|
||||
}
|
||||
}
|
||||
let maks = pent_maks(totaler.iter().cloned().fold(0.0, f64::max));
|
||||
let plot_b = BREDDE - VENSTRE - HOYRE;
|
||||
let plot_h = HOYDE - TOPP - BUNN;
|
||||
let sb = plot_b / 12.0;
|
||||
let bredde = sb * 0.68;
|
||||
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {BREDDE} {HOYDE}" font-family="sans-serif" font-size="10">"##
|
||||
));
|
||||
// Rutenett og akse.
|
||||
for i in 0..=4 {
|
||||
let v = maks * i as f64 / 4.0;
|
||||
let y = TOPP + plot_h - plot_h * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{y:.1}" x2="{:.1}" y2="{y:.1}" stroke="#d8d8d8" stroke-width="0.7"/>"##,
|
||||
BREDDE - HOYRE
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" text-anchor="end" fill="#555">{v:.0}</text>"##,
|
||||
VENSTRE - 6.0,
|
||||
y + 3.5
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<text x="4" y="12" fill="#555">kWh</text><line x1="{VENSTRE}" y1="{:.1}" x2="{:.1}" y2="{:.1}" stroke="#444" stroke-width="1"/>"##,
|
||||
TOPP + plot_h,
|
||||
BREDDE - HOYRE,
|
||||
TOPP + plot_h
|
||||
));
|
||||
// Søyler.
|
||||
for i in 0..12 {
|
||||
let x = VENSTRE + sb * i as f64 + (sb - bredde) / 2.0;
|
||||
let mut y = TOPP + plot_h;
|
||||
for (k, serie) in serier.iter().enumerate() {
|
||||
let h = serie[i] / maks * plot_h;
|
||||
if h <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
y -= h;
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="{x:.1}" y="{y:.1}" width="{bredde:.1}" height="{h:.1}" fill="{}"/>"##,
|
||||
FARGER[k]
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" text-anchor="middle" fill="#444">{}</text>"##,
|
||||
VENSTRE + sb * i as f64 + sb / 2.0,
|
||||
TOPP + plot_h + 14.0,
|
||||
m.maaned[i]
|
||||
));
|
||||
}
|
||||
// Tegnforklaring.
|
||||
let mut x = VENSTRE;
|
||||
let y = HOYDE - 12.0;
|
||||
for (k, navn) in NAVN.iter().enumerate() {
|
||||
if serier[k].iter().sum::<f64>() <= 0.5 {
|
||||
continue;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="{x:.1}" y="{:.1}" width="9" height="9" fill="{}"/><text x="{:.1}" y="{:.1}" fill="#333">{}</text>"##,
|
||||
y - 8.0,
|
||||
FARGER[k],
|
||||
x + 13.0,
|
||||
y,
|
||||
esc(navn)
|
||||
));
|
||||
x += 16.0 + navn.chars().count() as f64 * 5.3;
|
||||
}
|
||||
s.push_str("</svg>");
|
||||
s
|
||||
}
|
||||
|
||||
/// Varighetskurve for romoppvarming: effekt sortert fallende over årets timer.
|
||||
pub fn varighetskurve(r: &Resultat) -> String {
|
||||
let mut v: Vec<f64> = r
|
||||
.timeserie
|
||||
.romoppvarming
|
||||
.iter()
|
||||
.zip(&r.timeserie.ventilasjonsvarme)
|
||||
.map(|(a, b)| (a + b) / 1000.0)
|
||||
.collect();
|
||||
v.sort_by(|a, b| b.partial_cmp(a).unwrap());
|
||||
let maks = pent_maks(v.first().copied().unwrap_or(1.0));
|
||||
let plot_b = BREDDE - VENSTRE - HOYRE;
|
||||
let plot_h = HOYDE - TOPP - BUNN;
|
||||
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {BREDDE} {HOYDE}" font-family="sans-serif" font-size="10">"##
|
||||
));
|
||||
for i in 0..=4 {
|
||||
let val = maks * i as f64 / 4.0;
|
||||
let y = TOPP + plot_h - plot_h * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{y:.1}" x2="{:.1}" y2="{y:.1}" stroke="#d8d8d8" stroke-width="0.7"/><text x="{:.1}" y="{:.1}" text-anchor="end" fill="#555">{val:.1}</text>"##,
|
||||
BREDDE - HOYRE,
|
||||
VENSTRE - 6.0,
|
||||
y + 3.5
|
||||
));
|
||||
}
|
||||
s.push_str(r##"<text x="4" y="12" fill="#555">kW</text>"##);
|
||||
let mut d = String::from("M");
|
||||
for (i, verdi) in v.iter().enumerate().step_by(4) {
|
||||
let x = VENSTRE + plot_b * i as f64 / v.len() as f64;
|
||||
let y = TOPP + plot_h - verdi / maks * plot_h;
|
||||
d.push_str(&format!(" {x:.1},{y:.1}"));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<path d="{d}" fill="none" stroke="{}" stroke-width="1.6"/>"##,
|
||||
FARGER[0]
|
||||
));
|
||||
for i in 0..=4 {
|
||||
let timer = 8760 * i / 4;
|
||||
let x = VENSTRE + plot_b * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{x:.1}" y="{:.1}" text-anchor="middle" fill="#444">{timer}</text>"##,
|
||||
TOPP + plot_h + 14.0
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{:.1}" x2="{:.1}" y2="{:.1}" stroke="#444" stroke-width="1"/><text x="{:.1}" y="{:.1}" text-anchor="middle" fill="#555">timer per år</text></svg>"##,
|
||||
TOPP + plot_h,
|
||||
BREDDE - HOYRE,
|
||||
TOPP + plot_h,
|
||||
VENSTRE + plot_b / 2.0,
|
||||
HOYDE - 10.0
|
||||
));
|
||||
s
|
||||
}
|
||||
|
||||
/// Karakterskalaen A–G med bygningens plassering markert.
|
||||
pub fn karakterstige(r: &Resultat) -> String {
|
||||
let k = &r.karakter;
|
||||
let farger = [
|
||||
"#1d7a35", "#4aab3a", "#a8c93a", "#f2d21e", "#f2a01e", "#e0661e", "#cf3030",
|
||||
];
|
||||
let rad_h = 30.0;
|
||||
let h = rad_h * 7.0 + 30.0;
|
||||
let b = 470.0;
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {b} {h}" font-family="sans-serif" font-size="11">"##
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="0" y="12" fill="#555">Spesifikk vektet levert energi, kWh/(m²·år) — {}</text>"##,
|
||||
esc(&k.kategorinavn)
|
||||
));
|
||||
for (i, bokstav) in crate::merking::skala::BOKSTAVER.iter().enumerate() {
|
||||
let y = 24.0 + rad_h * i as f64;
|
||||
let bredde = 120.0 + 28.0 * i as f64;
|
||||
let valgt = *bokstav == k.bokstav;
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="0" y="{y:.1}" width="{bredde:.1}" height="{:.1}" rx="3" fill="{}"/>"##,
|
||||
rad_h - 5.0,
|
||||
farger[i]
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="12" y="{:.1}" font-size="15" font-weight="bold" fill="#ffffff">{bokstav}</text>"##,
|
||||
y + 17.0
|
||||
));
|
||||
let tekst = if i < 6 {
|
||||
format!("≤ {:.0}", k.grenser[i])
|
||||
} else {
|
||||
format!("over {:.0}", k.grenser[5])
|
||||
};
|
||||
s.push_str(&format!(
|
||||
r##"<text x="36" y="{:.1}" fill="#ffffff">{tekst}</text>"##,
|
||||
y + 16.0
|
||||
));
|
||||
if valgt {
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="-2.5" y="{:.1}" width="{:.1}" height="{:.1}" rx="4" fill="none" stroke="#111" stroke-width="2"/>"##,
|
||||
y - 2.5,
|
||||
bredde + 5.0,
|
||||
rad_h
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" font-size="13" font-weight="bold" fill="#111">◀ {:.0} kWh/(m²·år)</text>"##,
|
||||
bredde + 14.0,
|
||||
y + 17.0,
|
||||
k.spesifikk_vektet
|
||||
));
|
||||
}
|
||||
}
|
||||
s.push_str("</svg>");
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
//! Genererer energirapporten som Typst-kilde og kompilerer den til PDF.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::merking::skala::BOKSTAVER;
|
||||
use crate::modell::{FlateType, Mot};
|
||||
use crate::rapport::svg;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
const STIL: &str = include_str!("../../typst/stil.typ");
|
||||
|
||||
const KARAKTERFARGER: [&str; 7] = [
|
||||
"#1d7a35", "#4aab3a", "#a8c93a", "#f2d21e", "#f2a01e", "#e0661e", "#cf3030",
|
||||
];
|
||||
|
||||
/// Escaper tekst slik at den kan settes inn i Typst-markup.
|
||||
fn e(s: &str) -> String {
|
||||
let mut ut = String::with_capacity(s.len() + 8);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' | '#' | '$' | '*' | '_' | '`' | '<' | '>' | '@' | '[' | ']' | '"' => {
|
||||
ut.push('\\');
|
||||
ut.push(c);
|
||||
}
|
||||
_ => ut.push(c),
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn tall(v: f64, desimaler: usize) -> String {
|
||||
// Norsk desimalkomma og tusenskille med tynt mellomrom.
|
||||
let s = format!("{v:.desimaler$}");
|
||||
let (heltall, rest) = match s.split_once('.') {
|
||||
Some((a, b)) => (a.to_string(), Some(b.to_string())),
|
||||
None => (s.clone(), None),
|
||||
};
|
||||
let negativ = heltall.starts_with('-');
|
||||
let siffer: Vec<char> = heltall.trim_start_matches('-').chars().collect();
|
||||
let mut gruppert = String::new();
|
||||
for (i, c) in siffer.iter().enumerate() {
|
||||
if i > 0 && (siffer.len() - i).is_multiple_of(3) {
|
||||
gruppert.push('\u{202f}');
|
||||
}
|
||||
gruppert.push(*c);
|
||||
}
|
||||
let mut ut = String::new();
|
||||
if negativ {
|
||||
ut.push('−');
|
||||
}
|
||||
ut.push_str(&gruppert);
|
||||
if let Some(r) = rest {
|
||||
ut.push(',');
|
||||
ut.push_str(&r);
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn rad(celler: &[String]) -> String {
|
||||
let inner: Vec<String> = celler.iter().map(|c| format!("[{c}]")).collect();
|
||||
format!("({}),", inner.join(", "))
|
||||
}
|
||||
|
||||
/// Skriver `rapport.typ`, `stil.typ` og figurene til katalogen.
|
||||
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
||||
std::fs::create_dir_all(katalog)?;
|
||||
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
||||
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
|
||||
std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?;
|
||||
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
||||
std::fs::write(katalog.join("rapport.typ"), kilde(r, scenarier))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kompilerer rapporten med `typst`. Returnerer stien til PDF-en.
|
||||
pub fn kompiler(katalog: &Path) -> Result<std::path::PathBuf> {
|
||||
let typ = katalog.join("rapport.typ");
|
||||
let pdf = katalog.join("rapport.pdf");
|
||||
let ut = std::process::Command::new("typst")
|
||||
.arg("compile")
|
||||
.arg(&typ)
|
||||
.arg(&pdf)
|
||||
.output()
|
||||
.context("kunne ikke kjøre `typst`. Installer Typst, eller bruk --ingen-pdf")?;
|
||||
if !ut.status.success() {
|
||||
bail!(
|
||||
"typst klarte ikke å kompilere rapporten:\n{}",
|
||||
String::from_utf8_lossy(&ut.stderr)
|
||||
);
|
||||
}
|
||||
Ok(pdf)
|
||||
}
|
||||
|
||||
fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
let l = &r.lost;
|
||||
let k = &r.karakter;
|
||||
let b = &r.bygning.bygning;
|
||||
let farge = KARAKTERFARGER[BOKSTAVER.iter().position(|x| *x == k.bokstav).unwrap_or(6)];
|
||||
let mut s = String::new();
|
||||
|
||||
s.push_str("#import \"stil.typ\": *\n\n");
|
||||
s.push_str(&format!(
|
||||
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n",
|
||||
e(&l.navn),
|
||||
e(&bunntekst(r))
|
||||
));
|
||||
|
||||
// ── forside ─────────────────────────────────────────────────────
|
||||
s.push_str(&format!(
|
||||
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\
|
||||
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
||||
e(&l.navn)
|
||||
));
|
||||
if let Some(a) = &b.adresse {
|
||||
s.push_str(&format!("#text(size: 11pt, fill: mid)[{}]\n\n", e(a)));
|
||||
}
|
||||
s.push_str("#v(0.8cm)\n");
|
||||
s.push_str(&format!(
|
||||
"#grid(columns: (auto, 1fr), gutter: 18pt,\n karakterrute(\"{}\", \"{}\", rgb(\"{}\")),\n faktaboks[\n",
|
||||
k.bokstav,
|
||||
tall(k.spesifikk_vektet, 0),
|
||||
farge
|
||||
));
|
||||
s.push_str(&format!(
|
||||
" *Energikarakter {}* for {}. \\\n",
|
||||
k.bokstav,
|
||||
e(&k.kategorinavn.to_lowercase())
|
||||
));
|
||||
s.push_str(
|
||||
" Karakteren bygger på beregnet klimakorrigert normert energiytelse etter NS 3031:2025, \
|
||||
oppgitt som vektet levert energi per kvadratmeter oppvarmet BRA. \\\n",
|
||||
);
|
||||
if let Some(t) = k.til_neste {
|
||||
s.push_str(&format!(
|
||||
" Energiytelsen må ned {} kWh/(m²·år) for å nå neste karakter. \\\n",
|
||||
tall(t, 0)
|
||||
));
|
||||
} else {
|
||||
s.push_str(" Dette er den beste karakteren på skalaen. \\\n");
|
||||
}
|
||||
s.push_str(" ]\n)\n\n#v(0.6cm)\n");
|
||||
|
||||
// Nøkkeltall.
|
||||
let nøkkel = vec![
|
||||
("Bygningskategori".to_string(), e(&k.kategorinavn)),
|
||||
("Byggeår".to_string(), l.byggeaar.to_string()),
|
||||
(
|
||||
"Oppvarmet BRA".to_string(),
|
||||
format!("{} m²", tall(l.bra, 0)),
|
||||
),
|
||||
(
|
||||
"Netto energibehov".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(r.budsjett.netto_spesifikk(l.bra), 1)),
|
||||
),
|
||||
(
|
||||
"Levert energi".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(r.levert.levert_totalt / l.bra, 1)),
|
||||
),
|
||||
(
|
||||
"Vektet levert energi".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(k.spesifikk_vektet, 1)),
|
||||
),
|
||||
("Klimadata".to_string(), e(&r.klimanavn)),
|
||||
(
|
||||
"Varmetapstall".to_string(),
|
||||
format!("{} W/(m²K)", tall(r.varmetap.totalt, 2)),
|
||||
),
|
||||
];
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto),\n justering: (left, right),\n overskrift: (\"Nøkkeltall\", \"\"),\n rader: (\n");
|
||||
for (a, b2) in &nøkkel {
|
||||
s.push_str(&format!(" {}\n", rad(&[a.clone(), b2.clone()])));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
if let Some(kor) = &r.levert.klimakorrigering {
|
||||
s.push_str(&format!(
|
||||
"#merknad[Beregningen er gjort med klimadata for {}. De klimaavhengige energipostene er \
|
||||
korrigert til referanseklima Oslo med klimakorrigeringsfaktor {} etter NS 3031:2025, \
|
||||
slik energimerkeforskriften krever.]\n\n",
|
||||
e(&kor.sted),
|
||||
tall(kor.faktor, 3)
|
||||
));
|
||||
}
|
||||
|
||||
s.push_str("#pagebreak()\n\n");
|
||||
|
||||
// ── energikarakter ──────────────────────────────────────────────
|
||||
s.push_str("= Energikarakter\n\n");
|
||||
s.push_str("#image(\"fig-karakter.svg\", width: 100%)\n\n");
|
||||
s.push_str("Skalaen følger Enovas fastsettelse fra 1. januar 2026, jf. energimerkeforskriften § 10b. \
|
||||
Nedre grense for karakter A tilsvarer energikravene i TEK17 fratrukket 15 prosent, og et bygg \
|
||||
som innfrir TEK17 får karakter B. De 15 prosent minst energieffektive bygningene i hver \
|
||||
kategori får karakter G.\n\n");
|
||||
if k.grenser[0] != r.karakter.grenser[0] || l.skala_areal != l.bra {
|
||||
s.push_str(&format!(
|
||||
"Grensene er arealkorrigert med {} m² oppvarmet BRA, slik at små og store bygninger med \
|
||||
samme energikvaliteter får samme karakter.\n\n",
|
||||
tall(l.skala_areal, 0)
|
||||
));
|
||||
}
|
||||
|
||||
// ── varmetapsbudsjett ───────────────────────────────────────────
|
||||
let v = &r.varmetap;
|
||||
s.push_str("= Varmetapsbudsjett\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto),\n justering: (center, left, right),\n overskrift: (\"\", \"Varmetapspost\", \"Varmetapstall, W/(m²K)\"),\n sum-rader: (5, 7, 9),\n rader: (\n");
|
||||
let poster: Vec<(String, String, f64)> = vec![
|
||||
("1".into(), "Vegger".into(), v.vegger),
|
||||
("2".into(), "Tak".into(), v.tak),
|
||||
("3".into(), "Gulv".into(), v.gulv),
|
||||
("4".into(), "Vinduer og dører".into(), v.vinduer_dorer),
|
||||
("5".into(), "Kuldebroer".into(), v.kuldebroer),
|
||||
(
|
||||
"".into(),
|
||||
"Varmetapstall, transmisjon".into(),
|
||||
v.transmisjon,
|
||||
),
|
||||
("6".into(), "Infiltrasjon".into(), v.infiltrasjon),
|
||||
(
|
||||
"".into(),
|
||||
"Varmetapstall, klimaskjerm".into(),
|
||||
v.klimaskjerm,
|
||||
),
|
||||
("7".into(), "Ventilasjon".into(), v.ventilasjon),
|
||||
("".into(), "Varmetapstall, totalt".into(), v.totalt),
|
||||
];
|
||||
for (nr, navn, verdi) in &poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[nr.clone(), e(navn), tall(*verdi, 2)])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
s.push_str(&format!(
|
||||
"Klimaskjermarealet er {} m², som gir formfaktor {} m²/m². Vindus- og dørarealet utgjør \
|
||||
{} prosent av oppvarmet BRA. Lekkasjetallet er satt til {} luftvekslinger per time ved 50 Pa.\n\n",
|
||||
tall(l.klimaskjermareal(), 0),
|
||||
tall(l.formfaktor(), 2),
|
||||
tall(l.vindusareal() / l.bra * 100.0, 0),
|
||||
tall(l.n50, 1)
|
||||
));
|
||||
|
||||
// ── energibudsjett ──────────────────────────────────────────────
|
||||
s.push_str("= Energibudsjett\n\n== Netto energibehov (beregningspunkt A)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto, auto),\n justering: (center, left, right, right),\n overskrift: (\"\", \"Energipost\", \"kWh/år\", \"kWh/(m²·år)\"),\n rader: (\n");
|
||||
for p in &r.budsjett.poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
p.nr.to_string(),
|
||||
e(p.navn),
|
||||
tall(p.netto, 0),
|
||||
tall(p.netto / l.bra, 1)
|
||||
])
|
||||
));
|
||||
}
|
||||
let sumrad = r.budsjett.poster.len();
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({sumrad},),\n)\n\n",
|
||||
rad(&[
|
||||
"".into(),
|
||||
"Totalt netto energibehov".into(),
|
||||
tall(r.budsjett.netto_totalt, 0),
|
||||
tall(r.budsjett.netto_spesifikk(l.bra), 1)
|
||||
])
|
||||
));
|
||||
|
||||
s.push_str("== Fra netto til tilført energi (beregningspunkt A–C)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto, auto, auto, auto, auto),\n justering: (center, left, right, right, right, right, right),\n overskrift: (\"\", \"Energipost\", \"Netto\", \"Tap\", \"Brutto\", \"SPF\", \"Tilført\"),\n rader: (\n");
|
||||
for p in &r.budsjett.poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
p.nr.to_string(),
|
||||
e(p.navn),
|
||||
tall(p.netto / l.bra, 1),
|
||||
tall(p.tap / l.bra, 1),
|
||||
tall(p.brutto / l.bra, 1),
|
||||
if p.spf > 0.0 {
|
||||
tall(p.spf, 2)
|
||||
} else {
|
||||
"—".into()
|
||||
},
|
||||
tall(p.tilfort / l.bra, 1),
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({sumrad},),\n)\n\n",
|
||||
rad(&[
|
||||
"".into(),
|
||||
"Sum".into(),
|
||||
tall(r.budsjett.netto_totalt / l.bra, 1),
|
||||
tall(
|
||||
(r.budsjett.brutto_totalt - r.budsjett.netto_totalt) / l.bra,
|
||||
1
|
||||
),
|
||||
tall(r.budsjett.brutto_totalt / l.bra, 1),
|
||||
"".into(),
|
||||
tall(r.budsjett.tilfort_totalt / l.bra, 1),
|
||||
])
|
||||
));
|
||||
s.push_str("Alle tall i kWh per kvadratmeter oppvarmet BRA og år. SPF er systemets sesongjusterte ytelsesfaktor: forholdet mellom avgitt energi og tilført energi.\n\n");
|
||||
|
||||
// ── levert og vektet ────────────────────────────────────────────
|
||||
s.push_str("== Levert og vektet energi (beregningspunkt D og E)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto),\n justering: (left, right, right, right),\n overskrift: (\"Energibærer\", \"kWh/år\", \"Vektingsfaktor\", \"Vektet, kWh/(m²·år)\"),\n rader: (\n");
|
||||
for (baerer, levert, vektet) in &r.levert.vektet {
|
||||
let faktor = if *levert > 1e-9 { vektet / levert } else { 0.0 };
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(baerer.navn()),
|
||||
tall(*levert, 0),
|
||||
tall(faktor, 2),
|
||||
tall(vektet / l.bra, 1)
|
||||
])
|
||||
));
|
||||
}
|
||||
let mut n_rader = r.levert.vektet.len();
|
||||
if r.levert.eksportert > 0.1 {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
"Egenprodusert el til eksport".into(),
|
||||
format!("−{}", tall(r.levert.eksportert, 0)),
|
||||
"0,00".into(),
|
||||
"0,0".into()
|
||||
])
|
||||
));
|
||||
n_rader += 1;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({n_rader},),\n)\n\n",
|
||||
rad(&[
|
||||
"Totalt vektet levert energi".into(),
|
||||
tall(r.levert.levert_totalt, 0),
|
||||
"".into(),
|
||||
tall(r.levert.spesifikk_vektet, 1)
|
||||
])
|
||||
));
|
||||
|
||||
if let Some(p) = &r.solceller {
|
||||
s.push_str(&format!(
|
||||
"Solcelleanlegget produserer {} kWh i året. Av dette dekker {} kWh byggets eget forbruk time \
|
||||
for time, mens {} kWh eksporteres. Eksportert energi har vektingsfaktor 0 og teller derfor \
|
||||
ikke med i energikarakteren.\n\n",
|
||||
tall(p.produsert_kwh, 0),
|
||||
tall(p.egenbruk_kwh, 0),
|
||||
tall(p.eksportert_kwh, 0)
|
||||
));
|
||||
}
|
||||
|
||||
// ── figurer ─────────────────────────────────────────────────────
|
||||
s.push_str("= Energibruk gjennom året\n\n#image(\"fig-maaned.svg\", width: 100%)\n\n");
|
||||
s.push_str(&format!(
|
||||
"Figuren viser netto energibehov per måned. Årsmiddeltemperaturen i klimadataene er {} °C, \
|
||||
og det er {} graddager under 17 °C.\n\n",
|
||||
tall(r.vaerdata.aarsmiddeltemperatur(), 1),
|
||||
tall(r.vaerdata.graddager(17.0), 0)
|
||||
));
|
||||
s.push_str("#image(\"fig-varighet.svg\", width: 100%)\n\n");
|
||||
s.push_str("Varighetskurven viser romoppvarming og ventilasjonsvarme sortert fallende over årets timer.\n\n");
|
||||
|
||||
let ef = &r.effektbehov;
|
||||
s.push_str("== Effektbehov\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto),\n justering: (left, right, right, left),\n overskrift: (\"Effektpost\", \"kW\", \"W/m²\", \"Tidspunkt\"),\n rader: (\n");
|
||||
s.push_str(&format!(
|
||||
" {}\n {}\n {}\n ),\n sum-rader: (2,),\n)\n\n",
|
||||
rad(&[
|
||||
"Maksimalt samtidig brutto termisk effektbehov".into(),
|
||||
tall(ef.termisk_w / 1000.0, 1),
|
||||
tall(ef.termisk_w / l.bra, 1),
|
||||
e(&ef.beskriv_termisk())
|
||||
]),
|
||||
rad(&[
|
||||
"Maksimalt samtidig el-spesifikt effektbehov".into(),
|
||||
tall(ef.elspesifikk_w / 1000.0, 1),
|
||||
tall(ef.elspesifikk_w / l.bra, 1),
|
||||
"".into()
|
||||
]),
|
||||
rad(&[
|
||||
"Maksimalt samtidig brutto effektbehov".into(),
|
||||
tall(ef.samlet_w / 1000.0, 1),
|
||||
tall(ef.samlet_w / l.bra, 1),
|
||||
e(&ef.beskriv_samlet())
|
||||
])
|
||||
));
|
||||
|
||||
// ── inndata ─────────────────────────────────────────────────────
|
||||
s.push_str("#pagebreak()\n\n= Inndata\n\n== Bygningskropp\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto, auto),\n justering: (left, left, right, right, right, left),\n overskrift: (\"Bygningsdel\", \"Type\", \"Areal, m²\", \"U-verdi\", \"Orientering\", \"Mot\"),\n rader: (\n");
|
||||
for f in &l.flater {
|
||||
let typ = if f.glass {
|
||||
"Vindu/dør"
|
||||
} else {
|
||||
match f.typ {
|
||||
FlateType::Vegg => "Vegg",
|
||||
FlateType::Tak => "Tak",
|
||||
FlateType::Gulv => "Gulv",
|
||||
}
|
||||
};
|
||||
let mot = match f.mot {
|
||||
Mot::Ute => "det fri",
|
||||
Mot::Grunn => "grunnen",
|
||||
Mot::Uoppvarmet => "uoppvarmet",
|
||||
};
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(&f.navn),
|
||||
typ.into(),
|
||||
tall(f.areal, 1),
|
||||
tall(f.u_verdi, 2),
|
||||
himmelretning(f.orientering, f.helning),
|
||||
mot.into()
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
s.push_str("== Varmeforsyning\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto),\n justering: (left, right, right, right, left),\n overskrift: (\"Varmekilde\", \"Romvarme\", \"Vent.varme\", \"Tappevann\", \"SPF\"),\n rader: (\n");
|
||||
for kilde in &r.systemer.kilder {
|
||||
if kilde.andel_rom + kilde.andel_ventilasjon + kilde.andel_tappevann < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(&kilde.beskrivelse),
|
||||
format!("{} %", tall(kilde.andel_rom * 100.0, 0)),
|
||||
format!("{} %", tall(kilde.andel_ventilasjon * 100.0, 0)),
|
||||
format!("{} %", tall(kilde.andel_tappevann * 100.0, 0)),
|
||||
format!("{} ({})", tall(kilde.spf, 2), e(&kilde.spf_kilde)),
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
// ── forutsetninger ──────────────────────────────────────────────
|
||||
s.push_str("= Forutsetninger og normerte verdier\n\n");
|
||||
s.push_str(
|
||||
"Energimerkeberegningen bruker normerte inndata for drift og interne laster, slik at bygninger \
|
||||
kan sammenlignes på egenskaper og ikke på hvordan de brukes. Beregnet levert energi vil derfor \
|
||||
normalt avvike fra målt energiforbruk.\n\n",
|
||||
);
|
||||
if l.forutsetninger.is_empty() {
|
||||
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
||||
} else {
|
||||
s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
||||
for f in &l.forutsetninger {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[e(&f.felt), e(&f.verdi), e(&f.kilde)])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
}
|
||||
for m in &r.merknader {
|
||||
s.push_str(&format!("#merknad[{}]\n\n", e(m)));
|
||||
}
|
||||
|
||||
// ── scenarier ───────────────────────────────────────────────────
|
||||
if !scenarier.is_empty() {
|
||||
s.push_str("#pagebreak()\n\n= Tiltak\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto),\n justering: (left, right, right, center, right),\n overskrift: (\"Alternativ\", \"Netto, kWh/(m²·år)\", \"Vektet levert, kWh/(m²·år)\", \"Karakter\", \"Endring\"),\n rader: (\n");
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
"Dagens situasjon".into(),
|
||||
tall(r.budsjett.netto_spesifikk(l.bra), 1),
|
||||
tall(r.levert.spesifikk_vektet, 1),
|
||||
r.karakter.bokstav.into(),
|
||||
"—".into()
|
||||
])
|
||||
));
|
||||
for (navn, sr) in scenarier {
|
||||
let endring = sr.levert.spesifikk_vektet - r.levert.spesifikk_vektet;
|
||||
let prosent = endring / r.levert.spesifikk_vektet * 100.0;
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(navn),
|
||||
tall(sr.budsjett.netto_spesifikk(sr.lost.bra), 1),
|
||||
tall(sr.levert.spesifikk_vektet, 1),
|
||||
sr.karakter.bokstav.into(),
|
||||
format!("{} %", tall(prosent, 1))
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
for (navn, sr) in scenarier {
|
||||
if let Some(besk) = r
|
||||
.bygning
|
||||
.scenario
|
||||
.iter()
|
||||
.find(|x| &x.navn == navn)
|
||||
.and_then(|x| x.beskrivelse.as_ref())
|
||||
{
|
||||
s.push_str(&format!("*{}.* {}\n\n", e(navn), e(besk)));
|
||||
}
|
||||
let _ = sr;
|
||||
}
|
||||
}
|
||||
|
||||
s.push_str(&format!(
|
||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet med enok {} etter NS 3031:2025 (timesmetode, \
|
||||
NS-EN ISO 52016-1). Karakterskala fastsatt av Enova SF, gjeldende fra 1. januar 2026. \
|
||||
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
s
|
||||
}
|
||||
|
||||
fn bunntekst(r: &Resultat) -> String {
|
||||
let b = &r.bygning.bygning;
|
||||
let mut deler = Vec::new();
|
||||
if let Some(a) = &b.adresse {
|
||||
deler.push(a.clone());
|
||||
}
|
||||
if let Some(g) = &b.gnr_bnr {
|
||||
deler.push(g.clone());
|
||||
}
|
||||
if let Some(e) = &b.energiraadgiver {
|
||||
deler.push(e.clone());
|
||||
}
|
||||
deler.join(" · ")
|
||||
}
|
||||
|
||||
fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||
if helning < 20.0 {
|
||||
return "horisontal".into();
|
||||
}
|
||||
if helning > 160.0 {
|
||||
return "mot grunn".into();
|
||||
}
|
||||
let retninger = [
|
||||
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||||
];
|
||||
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
||||
retninger[i].into()
|
||||
}
|
||||
Reference in New Issue
Block a user