//! enok — årlig energiberegning og energirapport for bygninger etter NS 3031. use anyhow::{bail, Context, Result}; use clap::{Args, Parser, Subcommand}; use enok::data::Datakilde; use enok::inndata; use enok::kjor; use enok::rapport; use std::path::{Path, PathBuf}; #[derive(Parser)] #[command( name = "enok", version, about = "Årlig energiberegning og energirapport for bygninger etter NS 3031:2025", long_about = "Beregner timesvis energibehov for et helt år etter NS 3031:2025 (NS-EN ISO 52016-1), \ fastsetter energikarakter etter Enovas skala fra 1.1.2026, og skriver rapport som \ Typst/PDF med CSV- og JSON-vedlegg." )] struct Cli { /// Katalog som overstyrer de innebygde datafilene (skala, normerte verdier, materialer). #[arg(long, global = true, value_name = "KATALOG")] data: Option, #[command(subcommand)] kommando: Kommando, } #[derive(Subcommand)] enum Kommando { /// Beregner energibehov og skriver rapport. Beregn(Beregn), /// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml. Import(Import), /// Viser tilgjengelige klimasteder og nøkkeltall for ett sted. Klima(Klima), /// Viser energikarakterskalaen for en bygningskategori. Skala(Skala), } #[derive(Args)] struct Beregn { /// Bygningsbeskrivelse (bygning.toml). fil: PathBuf, /// Utdatakatalog. Standard: `ut/`. #[arg(short, long, value_name = "KATALOG")] ut: Option, /// Hopper over PDF-kompilering (skriver bare rapport.typ). #[arg(long)] ingen_pdf: bool, /// Skriver også XML for Enovas energimerkesystem. #[arg(long)] enova_xml: bool, /// Hopper over timeserie.csv (8760 rader). #[arg(long)] ingen_timeserie: bool, /// Beregner bare hovedalternativet, uten scenarioene i filen. #[arg(long)] uten_scenarier: bool, } #[derive(Args)] struct Import { /// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet. fil: PathBuf, /// Utdatafil. Standard: samme navn med endelsen .toml. #[arg(short, long, value_name = "FIL")] ut: Option, } #[derive(Args)] struct Klima { /// Lister alle kommuner med klimadata. #[arg(long)] liste: bool, /// Viser nøkkeltall for én kommune. #[arg(long, value_name = "KOMMUNE")] kommune: Option, } #[derive(Args)] struct Skala { /// Bygningskategori, for eksempel smaahus eller boligblokk. #[arg(default_value = "smaahus")] kategori: String, /// Oppvarmet BRA for arealkorreksjonen, m². #[arg(long, default_value_t = 160.0)] areal: f64, } fn main() -> Result<()> { let cli = Cli::parse(); let kilde = Datakilde::ny(cli.data.clone()); match cli.kommando { Kommando::Beregn(a) => beregn(a, &kilde), Kommando::Import(a) => import(a), Kommando::Klima(a) => klima(a), Kommando::Skala(a) => skala(a, &kilde), } } fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> { let bygning = inndata::les_bygning(&a.fil)?; let ut = a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn))); std::fs::create_dir_all(&ut)?; eprintln!("Henter klimadata …"); let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?; eprintln!("Beregner {} …", bygning.bygning.navn); let lost = enok::Lost::fra(&bygning, kilde)?; let resultat = kjor::kjor_med_klima( &bygning, kilde, lost, vaerdata.clone(), klimanavn.clone(), er_referanse, )?; let mut scenarier = Vec::new(); if !a.uten_scenarier { for s in &bygning.scenario { eprintln!("Beregner scenario «{}» …", s.navn); let endret = inndata::slaa_sammen_scenario(&bygning, s)?; let lost = enok::Lost::fra(&endret, kilde)?; let r = kjor::kjor_med_klima( &endret, kilde, lost, vaerdata.clone(), klimanavn.clone(), er_referanse, )?; scenarier.push((s.navn.clone(), r)); } } rapport::json_ut::skriv(&resultat, &ut.join("resultat.json"))?; rapport::csv_ut::maanedsverdier(&resultat, &ut.join("maaned.csv"))?; if !a.ingen_timeserie { rapport::csv_ut::timeserie(&resultat, &ut.join("timeserie.csv"))?; } if a.enova_xml { rapport::enova_xml::skriv(&resultat, &ut.join("enova-2014.xml"))?; } rapport::typst_ut::skriv(&resultat, &ut, &scenarier)?; if !a.ingen_pdf { let pdf = rapport::typst_ut::kompiler(&ut)?; eprintln!("Rapport: {}", pdf.display()); } skriv_sammendrag(&resultat, &scenarier); eprintln!("Filer skrevet til {}", ut.display()); Ok(()) } fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)]) { let l = &r.lost; println!(); println!( " {} ({}, {} m², byggeår {})", l.navn, r.karakter.kategorinavn, l.bra.round(), l.byggeaar ); println!(" Klimadata: {}", r.klimanavn); println!(); println!( " Varmetapstall {:>8.2} W/(m²K)", r.varmetap.totalt ); println!( " Netto energibehov {:>8.1} kWh/(m²·år)", r.budsjett.netto_spesifikk(l.bra) ); println!( " Levert energi {:>8.1} kWh/(m²·år)", r.levert.levert_totalt / l.bra ); println!( " Vektet levert energi {:>8.1} kWh/(m²·år)", r.levert.spesifikk_vektet ); println!(" Energikarakter {}", r.karakter.bokstav); if let Some(t) = r.karakter.til_neste { println!(" ({:.0} kWh/(m²·år) fra neste karakter)", t); } if !scenarier.is_empty() { println!(); println!(" Tiltak:"); for (navn, s) in scenarier { println!( " {:<34} {:>6.1} kWh/(m²·år) {}", navn, s.levert.spesifikk_vektet, s.karakter.bokstav ); } } println!(); } fn import(a: Import) -> Result<()> { let endelse = a .fil .extension() .and_then(|e| e.to_str()) .unwrap_or("") .to_lowercase(); let (bygning, merknader) = match endelse.as_str() { "gbxml" | "xml" => { let r = inndata::gbxml::les(&a.fil)?; (r.bygning, r.merknader) } "ifc" => { let r = inndata::ifc::les(&a.fil)?; (r.bygning, r.merknader) } "csv" => { let r = inndata::regneark::les(&a.fil)?; (r.bygning, r.merknader) } annen => bail!( "ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)" ), }; let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml")); let tekst = toml::to_string_pretty(&bygning).context("serialiserer bygningen til TOML")?; let mut innhold = String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n"); for m in &merknader { innhold.push_str(&format!("# {m}\n")); } innhold.push('\n'); innhold.push_str(&tekst); std::fs::write(&ut, innhold)?; println!("Skrev {}", ut.display()); for m in &merknader { println!(" • {m}"); } Ok(()) } fn klima(a: Klima) -> Result<()> { if a.liste { for k in enok::klima::tmy::kommuner()? { println!("{k}"); } return Ok(()); } let kommune = a .kommune .unwrap_or_else(|| enok::klima::REFERANSEKLIMA.to_string()); let v = enok::klima::tmy::for_kommune(&kommune)?; let m = v.maanedsmiddeltemperatur(); println!( "{} ({:.4}° N, {:.4}° Ø, {:.0} moh.)", v.sted, v.bredde, v.lengde, v.hoyde ); println!("Årsmiddeltemperatur {:.1} °C", v.aarsmiddeltemperatur()); println!("Graddager under 17 °C {:.0}", v.graddager(17.0)); println!( "Global solinnstråling {:.0} kWh/(m²·år)", v.ghi.iter().sum::() / 1000.0 ); println!(); println!("Månedsmiddeltemperatur, °C"); for (i, navn) in enok::MAANEDER.iter().enumerate() { println!(" {navn} {:>6.1}", m[i]); } Ok(()) } fn skala(a: Skala, kilde: &Datakilde) -> Result<()> { let skala = kilde.skala()?; let k = skala .0 .get(a.kategori.as_str()) .with_context(|| format!("ukjent kategori «{}»", a.kategori))?; let kar = enok::merking::karakter(k, f64::NAN, a.areal); println!("{} (arealkorreksjon for {:.0} m²)", k.navn, a.areal); println!(); for (i, bokstav) in enok::merking::skala::BOKSTAVER.iter().enumerate() { if i < 6 { println!(" {bokstav} ≤ {:>6.1} kWh/(m²·år)", kar.grenser[i]); } else { println!(" {bokstav} > {:>6.1} kWh/(m²·år)", kar.grenser[5]); } } Ok(()) } fn filnavnvennlig(s: &str) -> String { s.chars() .map(|c| match c { 'æ' => 'a', 'ø' => 'o', 'å' => 'a', 'Æ' => 'A', 'Ø' => 'O', 'Å' => 'A', c if c.is_alphanumeric() => c.to_ascii_lowercase(), _ => '-', }) .collect::() .trim_matches('-') .to_string() } #[allow(dead_code)] fn sjekk_sti(p: &Path) -> Result<()> { if !p.exists() { bail!("finner ikke {}", p.display()); } Ok(()) }