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
245 lines
8.6 KiB
Rust
245 lines
8.6 KiB
Rust
//! 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));
|
|
}
|
|
}
|