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,244 @@
|
||||
//! Import fra regneark (CSV), for eksempel en Vectorworks-arbeidstegning eksportert med
|
||||
//! «Export Worksheet». Formatet er bevisst enkelt, slik at det kan settes opp som en
|
||||
//! rapport i BIM-verktøyet og eksporteres direkte.
|
||||
//!
|
||||
//! Hver rad begynner med en type:
|
||||
//!
|
||||
//! ```text
|
||||
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||
//! bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
||||
//! vegg,Yttervegg sør,32,1.0,180,90,ute,,
|
||||
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
|
||||
//! ```
|
||||
//!
|
||||
//! For `bygning`-raden brukes kolonnene: navn, oppvarmet BRA, (tom), (tom), (tom),
|
||||
//! kategori, byggeår.
|
||||
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
fra_tekst(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
)
|
||||
}
|
||||
|
||||
fn skilletegn(tekst: &str) -> u8 {
|
||||
let forste = tekst.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
|
||||
if forste.matches(';').count() > forste.matches(',').count() {
|
||||
b';'
|
||||
} else {
|
||||
b','
|
||||
}
|
||||
}
|
||||
|
||||
fn tall(s: Option<&str>) -> Option<f64> {
|
||||
let s = s?.trim().replace(',', ".");
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
s.parse().ok()
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut leser = csv::ReaderBuilder::new()
|
||||
.delimiter(skilletegn(tekst))
|
||||
.flexible(true)
|
||||
.has_headers(true)
|
||||
.from_reader(tekst.as_bytes());
|
||||
|
||||
let mut merknader = Vec::new();
|
||||
let mut flater = Vec::new();
|
||||
let mut vinduer = Vec::new();
|
||||
let mut navn = standardnavn.to_string();
|
||||
let mut kategori = Kategori::Smaahus;
|
||||
let mut byggeaar = 2020;
|
||||
let mut bra = 0.0;
|
||||
|
||||
for (nr, rad) in leser.records().enumerate() {
|
||||
let rad = rad.with_context(|| format!("rad {}", nr + 2))?;
|
||||
let felt = |i: usize| rad.get(i).map(|s| s.trim()).filter(|s| !s.is_empty());
|
||||
let Some(typ) = felt(0).map(|s| s.to_lowercase()) else {
|
||||
continue;
|
||||
};
|
||||
let radnavn = felt(1).unwrap_or("uten navn").to_string();
|
||||
match typ.as_str() {
|
||||
"bygning" => {
|
||||
navn = radnavn;
|
||||
bra = tall(felt(2)).unwrap_or(0.0);
|
||||
if let Some(k) = felt(6) {
|
||||
kategori = match k.to_lowercase().as_str() {
|
||||
"smaahus" | "småhus" | "enebolig" => Kategori::Smaahus,
|
||||
"boligblokk" | "leilighet" => Kategori::Boligblokk,
|
||||
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
|
||||
"skolebygg" | "skole" => Kategori::Skolebygg,
|
||||
"barnehage" => Kategori::Barnehage,
|
||||
annen => {
|
||||
merknader
|
||||
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
|
||||
Kategori::Smaahus
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(a) = tall(felt(7)) {
|
||||
byggeaar = a as i32;
|
||||
}
|
||||
}
|
||||
"vegg" | "tak" | "gulv" => {
|
||||
let ft = match typ.as_str() {
|
||||
"vegg" => FlateType::Vegg,
|
||||
"tak" => FlateType::Tak,
|
||||
_ => FlateType::Gulv,
|
||||
};
|
||||
let mot = match felt(6).map(|s| s.to_lowercase()).as_deref() {
|
||||
Some("grunn") | Some("grunnen") => Mot::Grunn,
|
||||
Some("uoppvarmet") | Some("kjeller") | Some("loft") => Mot::Uoppvarmet,
|
||||
_ => Mot::Ute,
|
||||
};
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
flater.push(Flate {
|
||||
navn: radnavn,
|
||||
typ: ft,
|
||||
areal,
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)),
|
||||
u_verdi: tall(felt(3)),
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
"vindu" | "dør" | "dor" => {
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let Some(u) = tall(felt(3)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler U-verdi og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
vinduer.push(Vindu {
|
||||
navn: radnavn,
|
||||
areal,
|
||||
antall: tall(felt(8)).unwrap_or(1.0),
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)).unwrap_or(90.0),
|
||||
u_verdi: u,
|
||||
g_verdi: tall(felt(7)),
|
||||
karmandel: None,
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor: typ != "vindu",
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
annen => merknader.push(format!("Ukjent radtype «{annen}» på rad {}.", nr + 2)),
|
||||
}
|
||||
}
|
||||
|
||||
if flater.is_empty() {
|
||||
bail!("regnearket inneholder ingen flater (rader som begynner med vegg, tak eller gulv)");
|
||||
}
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum();
|
||||
merknader.push(format!(
|
||||
"Regnearket mangler en bygning-rad med oppvarmet BRA; satt til {bra:.0} m² av gulvarealet."
|
||||
));
|
||||
}
|
||||
merknader.push(
|
||||
"Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori,
|
||||
byggeaar,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: None,
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: None,
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra regneark".into()),
|
||||
energiraadgiver: None,
|
||||
oppdragsgiver: None,
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
ventilasjon: Ventilasjon::default(),
|
||||
tappevann: Tappevann::default(),
|
||||
oppvarming: Oppvarming::default(),
|
||||
distribusjon: Distribusjon::default(),
|
||||
solcelle: None,
|
||||
kjoling: Kjoling::default(),
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn leser_enkelt_regneark() {
|
||||
let csv = "type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall\n\
|
||||
bygning,Testhus,140,,,,smaahus,1948,\n\
|
||||
vegg,Yttervegg sør,32,1.0,180,90,ute,,\n\
|
||||
gulv,Kjellergulv,70,0.8,,180,grunn,,\n\
|
||||
vindu,Stue sør,2.1,2.8,180,90,,0.75,2\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.bygning.navn, "Testhus");
|
||||
assert_eq!(r.bygning.bygning.oppvarmet_bra, 140.0);
|
||||
assert_eq!(r.bygning.bygning.byggeaar, 1948);
|
||||
assert_eq!(r.bygning.flate.len(), 2);
|
||||
assert_eq!(r.bygning.flate[1].mot, Mot::Grunn);
|
||||
assert_eq!(r.bygning.vindu.len(), 1);
|
||||
assert_eq!(r.bygning.vindu[0].antall, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn godtar_semikolon_og_desimalkomma() {
|
||||
let csv = "type;navn;areal;u_verdi\nvegg;Vegg;32,5;1,0\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.flate[0].areal, 32.5);
|
||||
assert_eq!(r.bygning.flate[0].u_verdi, Some(1.0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user