Berekening: voorbeelden in de keuzegidsen
De uitvoerbare engine-runs achter de huishoud- en productvoorbeelden over capaciteit, dynamische contracten en kopen of wachten.
Dit is de berekening zelf, ongewijzigd overgenomen uit scripts/calibration/guide-examples.ts. Je kunt hem uitvoeren met bun scripts/calibration/guide-examples.ts en de gepubliceerde getallen reproduceren.
/**
* Reproducible household examples published in the buyer guides.
*
* Each example runs the shipped engine against the researched catalogue fleet.
* The guide assertions in `published-figures.test.ts` format these live results
* and compare them with the prose, so an engine, catalogue or price change
* cannot leave an old figure published as if it were still current.
*
* Run: bun scripts/calibration/guide-examples.ts
*/
import { computeEngine } from "~/lib/engine"
import type {
BatteryResult,
EngineInput,
EngineResult,
Recommendation,
ScenarioResult,
} from "~/lib/engine/types"
import { catalogueFleet } from "~/scripts/calibration/catalogue-fleet"
export const GUIDE_EXAMPLES_REVIEWED_AT = "2026-08-01"
const completeInput = (input: EngineInput): EngineInput => {
return {
hasEV: false,
hasHeatPump: false,
hasElectricBoiler: false,
phase: "OnePhase",
goal: "besparen",
...input,
}
}
const runCatalogue = (input: EngineInput): EngineResult => {
const track = input.ownership === "huur" ? "PlugIn" : "Vast"
return computeEngine(completeInput(input), catalogueFleet(track))
}
const batteryBySlug = (result: EngineResult, slug: string): BatteryResult => {
const battery = result.batteries.find(entry => entry.battery.slug === slug)
if (!battery) {
throw new Error(`Guide example battery is no longer ranked: ${slug}`)
}
return battery
}
const baseScenario = (battery: BatteryResult): ScenarioResult => {
const scenario = battery.scenarios.find(entry => entry.scenario === "base")
if (!scenario) {
throw new Error(`Guide example has no base scenario: ${battery.battery.slug}`)
}
return scenario
}
const requiredNumber = (value: number | null, label: string): number => {
if (value === null) {
throw new Error(`Guide example has no numeric ${label}`)
}
return value
}
export type PublishedBatteryExample = {
slug: string
usableKwh: number
costCents: number
paybackBaseYears: number
paybackFavourableYears: number
paybackConservativeYears: number
shiftedYear1Kwh: number
valueYear1Cents: number
utilizationYear1KwhPerUsableKwh: number
effectiveWarrantyYears: number | null
}
const publishedBattery = (battery: BatteryResult): PublishedBatteryExample => {
const base = baseScenario(battery)
return {
slug: battery.battery.slug,
usableKwh: battery.battery.usableKwh,
costCents: requiredNumber(base.netCostCents, "cost"),
paybackBaseYears: requiredNumber(battery.paybackBaseYears, "base payback"),
paybackFavourableYears: requiredNumber(battery.paybackRangeYears.min, "favourable payback"),
paybackConservativeYears: requiredNumber(battery.paybackRangeYears.max, "conservative payback"),
shiftedYear1Kwh: requiredNumber(base.years[0]?.shiftedKwh ?? null, "year-one shift"),
valueYear1Cents: base.valueYear1Cents,
utilizationYear1KwhPerUsableKwh: battery.utilizationYear1KwhPerUsableKwh,
effectiveWarrantyYears: battery.warranty.effectiveWarrantyYears,
}
}
const capacityInput = completeInput({
annualConsumptionKwh: 3500,
pvProductionKwh: 4000,
roofOrientation: "zuid",
householdProfile: "gemengd",
contractType: "vast",
ownership: "koop",
})
const dynamicContractInput = completeInput({
annualConsumptionKwh: 3500,
pvProductionKwh: 3500,
roofOrientation: "zuid",
householdProfile: "thuis_overdag",
ownership: "huur",
})
const buyOrWaitInput = completeInput({
annualConsumptionKwh: 5000,
pvProductionKwh: 5000,
roofOrientation: "zuid",
householdProfile: "thuis_overdag",
contractType: "dynamisch",
})
type GuideExamples = {
capacity: {
input: EngineInput
sessy5: PublishedBatteryExample
sessy10: PublishedBatteryExample
rankedBest: PublishedBatteryExample
recommendation: Recommendation | null
}
dynamicContract: {
input: EngineInput
battery: PublishedBatteryExample
fixedRecommendation: Recommendation | null
dynamicRecommendation: Recommendation | null
}
buyOrWait: {
input: EngineInput
fixed: PublishedBatteryExample
plugin: PublishedBatteryExample
fixedRecommendation: Recommendation | null
pluginRecommendation: Recommendation | null
}
}
export const runGuideExamples = (): GuideExamples => {
const capacity = runCatalogue(capacityInput)
const contractFixed = runCatalogue({ ...dynamicContractInput, contractType: "vast" })
const contractDynamic = runCatalogue({ ...dynamicContractInput, contractType: "dynamisch" })
const fixed = runCatalogue({ ...buyOrWaitInput, ownership: "koop" })
const plugin = runCatalogue({ ...buyOrWaitInput, ownership: "huur" })
const fixedContractBattery = publishedBattery(
batteryBySlug(contractFixed, "marstek-venus-e-gen-3"),
)
const dynamicContractBattery = publishedBattery(
batteryBySlug(contractDynamic, "marstek-venus-e-gen-3"),
)
if (fixedContractBattery.valueYear1Cents !== dynamicContractBattery.valueYear1Cents) {
throw new Error("Contract comparison must isolate the decision path from battery economics")
}
return {
capacity: {
input: capacityInput,
sessy5: publishedBattery(batteryBySlug(capacity, "sessy-5")),
sessy10: publishedBattery(batteryBySlug(capacity, "sessy-10")),
rankedBest: publishedBattery(batteryBySlug(capacity, "solaredge-home-battery-400v")),
recommendation: capacity.recommendation,
},
dynamicContract: {
input: dynamicContractInput,
battery: fixedContractBattery,
fixedRecommendation: contractFixed.recommendation,
dynamicRecommendation: contractDynamic.recommendation,
},
buyOrWait: {
input: buyOrWaitInput,
fixed: publishedBattery(batteryBySlug(fixed, "solaredge-home-battery-400v")),
plugin: publishedBattery(batteryBySlug(plugin, "marstek-venus-e-gen-3")),
fixedRecommendation: fixed.recommendation,
pluginRecommendation: plugin.recommendation,
},
}
}
if (import.meta.main) {
console.log(JSON.stringify(runGuideExamples(), null, 2))
}