2026-09-14 18:03:00 +02:00
|
|
|
//! 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<PathBuf>,
|
|
|
|
|
#[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),
|
2026-09-16 17:51:19 +02:00
|
|
|
/// Lager en tilleggsfil av forskjellen mellom en modell og en ferdig bygningsbeskrivelse.
|
|
|
|
|
Tillegg(TilleggAv),
|
2026-09-14 18:03:00 +02:00
|
|
|
/// 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/<bygningsnavn>`.
|
|
|
|
|
#[arg(short, long, value_name = "KATALOG")]
|
|
|
|
|
ut: Option<PathBuf>,
|
|
|
|
|
/// 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,
|
2026-09-16 14:39:58 +02:00
|
|
|
/// Tilleggsfil som flettes inn: det modellen ikke beskriver, og rettelser.
|
|
|
|
|
#[arg(short, long, value_name = "FIL")]
|
|
|
|
|
tillegg: Option<PathBuf>,
|
2026-09-14 18:03:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Args)]
|
|
|
|
|
struct Import {
|
|
|
|
|
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
|
|
|
|
|
fil: PathBuf,
|
2026-09-15 20:05:20 +02:00
|
|
|
/// Tilleggsfil som flettes inn i den importerte bygningen: det modellen ikke beskriver
|
|
|
|
|
/// (drift, varmeanlegg, scenarioer) og rettelser til det den beskriver.
|
|
|
|
|
#[arg(short, long, value_name = "FIL")]
|
|
|
|
|
tillegg: Option<PathBuf>,
|
2026-09-14 18:03:00 +02:00
|
|
|
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
|
|
|
|
#[arg(short, long, value_name = "FIL")]
|
|
|
|
|
ut: Option<PathBuf>,
|
2026-09-16 17:51:19 +02:00
|
|
|
/// Skriver over en utdatafil enok ikke har laget selv.
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
overskriv: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Args)]
|
|
|
|
|
struct TilleggAv {
|
|
|
|
|
/// BIM-modellen: .ifc, .gbxml eller .csv.
|
|
|
|
|
modell: PathBuf,
|
|
|
|
|
/// Bygningsbeskrivelsen med de manuelle endringene, som tillegget skal gjenskape.
|
|
|
|
|
bygning: PathBuf,
|
|
|
|
|
/// Utdatafil. Standard: prosjekt.toml ved siden av bygningsbeskrivelsen.
|
|
|
|
|
#[arg(short, long, value_name = "FIL")]
|
|
|
|
|
ut: Option<PathBuf>,
|
2026-09-14 18:03:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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),
|
2026-09-16 17:51:19 +02:00
|
|
|
Kommando::Tillegg(a) => tillegg(a),
|
2026-09-14 18:03:00 +02:00
|
|
|
Kommando::Klima(a) => klima(a),
|
|
|
|
|
Kommando::Skala(a) => skala(a, &kilde),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 14:39:58 +02:00
|
|
|
/// Leser en bygningsbeskrivelse eller en BIM-modell, med en valgfri tilleggsfil flettet inn.
|
|
|
|
|
///
|
|
|
|
|
/// En `.toml`-fil er bygningsbeskrivelsen selv; andre filtyper importeres. Returnerer også
|
|
|
|
|
/// merknadene fra importen, og om filen var en modell.
|
|
|
|
|
fn les_modell(
|
|
|
|
|
fil: &Path,
|
|
|
|
|
tillegg: Option<&Path>,
|
|
|
|
|
) -> Result<(enok::modell::Bygning, Vec<String>, bool)> {
|
|
|
|
|
let endelse = fil
|
|
|
|
|
.extension()
|
|
|
|
|
.and_then(|e| e.to_str())
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_lowercase();
|
|
|
|
|
let tillegg = tillegg.map(inndata::les_tillegg).transpose()?;
|
|
|
|
|
let valg = tillegg
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|t| t.import.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let (bygning, mut merknader, modell) = match endelse.as_str() {
|
|
|
|
|
"toml" => (inndata::les_bygning(fil)?, Vec::new(), false),
|
|
|
|
|
"gbxml" | "xml" => {
|
|
|
|
|
let r = inndata::gbxml::les(fil)?;
|
|
|
|
|
(r.bygning, r.merknader, true)
|
|
|
|
|
}
|
|
|
|
|
"ifc" => {
|
|
|
|
|
let r = inndata::ifc::les_med(fil, &valg)?;
|
|
|
|
|
(r.bygning, r.merknader, true)
|
|
|
|
|
}
|
|
|
|
|
"csv" => {
|
|
|
|
|
let r = inndata::regneark::les(fil)?;
|
|
|
|
|
(r.bygning, r.merknader, true)
|
|
|
|
|
}
|
|
|
|
|
annen => bail!(
|
|
|
|
|
"ukjent filtype «{annen}». enok leser bygning.toml og importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
|
|
|
|
|
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
|
|
|
|
|
}
|
|
|
|
|
let Some(t) = tillegg else {
|
|
|
|
|
return Ok((bygning, merknader, modell));
|
|
|
|
|
};
|
|
|
|
|
let (bygning, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
|
|
|
|
|
for m in bom {
|
|
|
|
|
merknader.push(format!(
|
|
|
|
|
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok((bygning, merknader, modell))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
|
2026-09-16 14:39:58 +02:00
|
|
|
let (bygning, merknader, modell) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
|
|
|
|
for m in &merknader {
|
|
|
|
|
eprintln!(" · {m}");
|
|
|
|
|
}
|
2026-09-14 21:20:34 +02:00
|
|
|
for m in enok::modell::kontroll::kontroller(&bygning) {
|
|
|
|
|
eprintln!(" ! {}: {}", m.felt, m.melding);
|
|
|
|
|
}
|
2026-09-14 18:03:00 +02:00
|
|
|
let ut =
|
|
|
|
|
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
|
|
|
|
|
std::fs::create_dir_all(&ut)?;
|
2026-09-16 14:39:58 +02:00
|
|
|
if modell {
|
|
|
|
|
// Bygningsbeskrivelsen modellen ga, skrives med, slik at beregningen kan etterprøves.
|
|
|
|
|
let sti = ut.join("bygning.toml");
|
|
|
|
|
std::fs::write(
|
|
|
|
|
&sti,
|
|
|
|
|
skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?,
|
|
|
|
|
)?;
|
|
|
|
|
eprintln!("Bygningsbeskrivelsen fra modellen: {}", sti.display());
|
|
|
|
|
}
|
2026-09-14 18:03:00 +02:00
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:36:54 +02:00
|
|
|
// Manglene er allerede skrevet ut før beregningen; her kommer bare det som kom i tillegg.
|
|
|
|
|
let alt_sagt: Vec<String> = enok::modell::kontroll::kontroller(&bygning)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|m| m.melding)
|
|
|
|
|
.collect();
|
|
|
|
|
for m in resultat.merknader.iter().filter(|m| !alt_sagt.contains(m)) {
|
2026-09-14 21:29:36 +02:00
|
|
|
eprintln!(" ! {m}");
|
|
|
|
|
}
|
2026-09-14 18:03:00 +02:00
|
|
|
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!();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:51:19 +02:00
|
|
|
const GENERERT: &str = "# Generert av enok";
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
fn import(a: Import) -> Result<()> {
|
2026-09-16 14:39:58 +02:00
|
|
|
let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
2026-09-14 18:03:00 +02:00
|
|
|
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
2026-09-16 17:51:19 +02:00
|
|
|
// En fil som ikke er skrevet av enok, kan være redigert for hånd. Da er det bedre å
|
|
|
|
|
// stoppe enn å skrive over arbeidet.
|
|
|
|
|
if !a.overskriv && ut.exists() {
|
|
|
|
|
let gammel = std::fs::read_to_string(&ut).unwrap_or_default();
|
|
|
|
|
if !gammel.starts_with(GENERERT) {
|
|
|
|
|
bail!(
|
|
|
|
|
"{} er ikke laget av enok og kan være redigert for hånd. \n\
|
|
|
|
|
Ta vare på endringene som en tilleggsfil med «enok tillegg {} {} --ut prosjekt.toml», \n\
|
|
|
|
|
og importer med «--tillegg prosjekt.toml». Bruk --overskriv for å skrive over.",
|
|
|
|
|
ut.display(),
|
|
|
|
|
a.fil.display(),
|
|
|
|
|
ut.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-16 14:39:58 +02:00
|
|
|
let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
|
2026-09-14 18:03:00 +02:00
|
|
|
std::fs::write(&ut, innhold)?;
|
|
|
|
|
println!("Skrev {}", ut.display());
|
|
|
|
|
for m in &merknader {
|
|
|
|
|
println!(" • {m}");
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 14:39:58 +02:00
|
|
|
/// Bygningsbeskrivelsen som TOML, med et hode som sier hvor den kommer fra.
|
|
|
|
|
fn skriv_bygning(
|
|
|
|
|
bygning: &enok::modell::Bygning,
|
|
|
|
|
merknader: &[String],
|
|
|
|
|
fil: &Path,
|
|
|
|
|
tillegg: Option<&Path>,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
let tekst = toml::to_string_pretty(bygning).context("serialiserer bygningen til TOML")?;
|
|
|
|
|
let mut innhold = match tillegg {
|
|
|
|
|
Some(t) => format!(
|
|
|
|
|
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
|
|
|
|
|
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
|
|
|
|
|
# Merknader fra importen:\n",
|
|
|
|
|
fil.display(),
|
|
|
|
|
t.display()
|
|
|
|
|
),
|
|
|
|
|
None => format!(
|
|
|
|
|
"# Generert av enok fra «{}».\n\
|
|
|
|
|
# Merknader fra importen:\n",
|
|
|
|
|
fil.display()
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
for m in merknader {
|
|
|
|
|
innhold.push_str(&format!("# {m}\n"));
|
|
|
|
|
}
|
|
|
|
|
innhold.push('\n');
|
|
|
|
|
innhold.push_str(&tekst);
|
|
|
|
|
Ok(innhold)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:51:19 +02:00
|
|
|
/// Skriver forskjellen mellom modellen og en ferdig bygningsbeskrivelse som en tilleggsfil.
|
|
|
|
|
fn tillegg(a: TilleggAv) -> Result<()> {
|
|
|
|
|
let (importert, _, modell) = les_modell(&a.modell, None)?;
|
|
|
|
|
if !modell {
|
|
|
|
|
bail!("{} er ikke en BIM-modell", a.modell.display());
|
|
|
|
|
}
|
|
|
|
|
let manuell = inndata::les_bygning(&a.bygning)?;
|
|
|
|
|
let fra = toml::Value::try_from(&importert).context("serialiserer den importerte bygningen")?;
|
|
|
|
|
let til = toml::Value::try_from(&manuell).context("serialiserer bygningsbeskrivelsen")?;
|
|
|
|
|
let (patch, detaljer) = inndata::differanse(&fra, &til);
|
|
|
|
|
|
|
|
|
|
// Felter modellen har og filen ikke, kan et tillegg ikke fjerne. De er som regel
|
|
|
|
|
// standardverdier importen fyller ut, så de samles i én merknad i stedet for én hver.
|
|
|
|
|
let (ufjernbare, mut merknader): (Vec<String>, Vec<String>) = detaljer
|
|
|
|
|
.into_iter()
|
|
|
|
|
.partition(|m| m.contains("finnes i modellen"));
|
|
|
|
|
if !ufjernbare.is_empty() {
|
|
|
|
|
let felter: Vec<&str> = ufjernbare
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|m| m.split('«').nth(1)?.split('»').next())
|
|
|
|
|
.collect();
|
|
|
|
|
merknader.push(format!(
|
|
|
|
|
"{} felter finnes i modellen, men ikke i bygningsbeskrivelsen, og blir stående: {}{}. En tilleggsfil kan ikke fjerne et felt.",
|
|
|
|
|
felter.len(),
|
|
|
|
|
felter.iter().take(3).copied().collect::<Vec<_>>().join(", "),
|
|
|
|
|
if felter.len() > 3 { " …" } else { "" }
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Kontroller at tillegget faktisk gjenskaper filen det er laget av.
|
|
|
|
|
let (gjenskapt, bom) = inndata::flett_tillegg(&importert, &patch)?;
|
|
|
|
|
merknader.extend(
|
|
|
|
|
bom.into_iter()
|
|
|
|
|
.map(|m| format!("Navnemønsteret «{m}» traff ingenting.")),
|
|
|
|
|
);
|
|
|
|
|
if toml::Value::try_from(&gjenskapt).ok().as_ref() != Some(&til) {
|
|
|
|
|
merknader.push(
|
|
|
|
|
"Tillegget gjenskaper ikke bygningsbeskrivelsen helt. Gå gjennom filen før den brukes."
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ut =
|
|
|
|
|
a.ut.unwrap_or_else(|| a.bygning.with_file_name("prosjekt.toml"));
|
|
|
|
|
let mut innhold = format!(
|
|
|
|
|
"# Tillegg til «{}», laget av forskjellen mot «{}».\n\
|
|
|
|
|
# Alt som ikke står i modellen, ligger her, og flettes inn ved hver import:\n\
|
|
|
|
|
# enok beregn \"{}\" --tillegg {}\n",
|
|
|
|
|
a.modell.display(),
|
|
|
|
|
a.bygning.display(),
|
|
|
|
|
a.modell.display(),
|
|
|
|
|
ut.file_name()
|
|
|
|
|
.and_then(|n| n.to_str())
|
|
|
|
|
.unwrap_or("prosjekt.toml")
|
|
|
|
|
);
|
|
|
|
|
for m in &merknader {
|
|
|
|
|
innhold.push_str(&format!("# {m}\n"));
|
|
|
|
|
}
|
|
|
|
|
innhold.push('\n');
|
|
|
|
|
innhold.push_str(&toml::to_string_pretty(&patch).context("serialiserer tillegget")?);
|
|
|
|
|
std::fs::write(&ut, innhold)?;
|
|
|
|
|
println!("Skrev {}", ut.display());
|
|
|
|
|
for m in &merknader {
|
|
|
|
|
println!(" • {m}");
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
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::<f64>() / 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::<String>()
|
|
|
|
|
.trim_matches('-')
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
fn sjekk_sti(p: &Path) -> Result<()> {
|
|
|
|
|
if !p.exists() {
|
|
|
|
|
bail!("finner ikke {}", p.display());
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|