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:
bl
2026-09-14 18:03:00 +02:00
co-authored by Claude Opus 5
commit 0964cacba3
51 changed files with 10309 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
//! Henter og hurtiglagrer TMY-filer for norske kommuner.
//!
//! Datasettet «TMY files for Norway based on CERRA data from 1991-2020» (Zenodo,
//! doi 10.5281/zenodo.14779899, CC-BY-4.0) er laget i tilknytning til NS 3031:2025 og dekker
//! alle norske kommuner med timesverdier. Zip-filen lastes ned én gang til
//! `$XDG_CACHE_HOME/enok` og pakkes ut derfra.
use crate::data::normaliser_stedsnavn;
use crate::klima::Vaerdata;
use crate::Result;
use anyhow::{anyhow, bail, Context};
use std::io::Read;
use std::path::{Path, PathBuf};
const ZIP_URL: &str =
"https://zenodo.org/api/records/19566253/files/EPW-TMYNO_CERRA_1991-2020.zip/content";
const ZIP_NAVN: &str = "EPW-TMYNO_CERRA_1991-2020.zip";
pub fn hurtiglager() -> PathBuf {
std::env::var_os("ENOK_CACHE")
.map(PathBuf::from)
.or_else(|| std::env::var_os("XDG_CACHE_HOME").map(|c| PathBuf::from(c).join("enok")))
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache/enok")))
.unwrap_or_else(|| PathBuf::from(".enok-cache"))
}
fn zip_sti() -> PathBuf {
hurtiglager().join(ZIP_NAVN)
}
/// Laster ned TMY-arkivet dersom det ikke finnes lokalt.
pub fn sikre_arkiv() -> Result<PathBuf> {
let sti = zip_sti();
if sti.exists() && std::fs::metadata(&sti)?.len() > 1_000_000 {
return Ok(sti);
}
std::fs::create_dir_all(sti.parent().unwrap())?;
eprintln!(
"Laster ned TMY-klimadata (74 MB) fra Zenodo til {}",
sti.display()
);
let svar = ureq::get(ZIP_URL)
.timeout(std::time::Duration::from_secs(900))
.call()
.context("nedlasting av TMY-arkivet feilet")?;
let mut data = Vec::new();
svar.into_reader().read_to_end(&mut data)?;
if data.len() < 1_000_000 {
bail!("nedlastet arkiv er uventet lite ({} byte)", data.len());
}
std::fs::write(&sti, &data)?;
Ok(sti)
}
/// Navnene på alle kommuner i arkivet.
pub fn kommuner() -> Result<Vec<String>> {
let sti = sikre_arkiv()?;
let fil = std::fs::File::open(&sti)?;
let mut zip = zip::ZipArchive::new(fil)?;
let mut ut = Vec::new();
for i in 0..zip.len() {
let navn = zip.by_index(i)?.name().to_string();
if let Some(k) = kommune_av_filnavn(&navn) {
ut.push(k);
}
}
ut.sort();
Ok(ut)
}
fn kommune_av_filnavn(navn: &str) -> Option<String> {
let base = Path::new(navn).file_name()?.to_str()?;
let base = base.strip_suffix(".epw")?;
let base = base.strip_prefix("TMYNO_")?;
let base = base.strip_suffix("_CERRA_1991-2020")?;
Some(base.to_string())
}
/// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både
/// «Indre Østfold» og «Indre Ostfold» treffer samme fil.
pub fn for_kommune(kommune: &str) -> Result<Vaerdata> {
let sti = sikre_arkiv()?;
let fil = std::fs::File::open(&sti)?;
let mut zip = zip::ZipArchive::new(fil)?;
let mål = normaliser_stedsnavn(kommune);
let mut treff = None;
for i in 0..zip.len() {
let navn = zip.by_index(i)?.name().to_string();
if let Some(k) = kommune_av_filnavn(&navn) {
if normaliser_stedsnavn(&k) == mål {
treff = Some((i, k));
break;
}
}
}
let (indeks, navn) = treff.ok_or_else(|| {
anyhow!(
"fant ingen TMY-fil for kommunen «{}» (bruk `enok klima --liste` for gyldige navn)",
kommune
)
})?;
let mut oppf = zip.by_index(indeks)?;
let mut data = Vec::new();
oppf.read_to_end(&mut data)?;
let tekst = String::from_utf8_lossy(&data);
let mut v = Vaerdata::fra_tekst(&tekst).with_context(|| format!("TMY-fil for {navn}"))?;
v.sted = navn;
Ok(v)
}