New big and small scope sunset and sunrise simulation
New calculations for the day tabs
Added missing icons to simple view
New record popups
Week ahead description popups
Camping profile
Tweak to some of the indoor tems
This commit is contained in:
fraxle
2026-10-08 09:53:41 +01:00
parent 71860b9dd9
commit 862f631beb
22 changed files with 2169 additions and 429 deletions
+189 -102
View File
@@ -233,39 +233,58 @@ export function calcConcreteTempPass(TaArr, radArr, vaArr, elevArr, uvArr, cloud
}
// -------------------------------------------------------------------
// UK HOUSE INDOOR TEMPERATURE (windows closed, no active cooling)
// UK HOUSE INDOOR TEMPERATURE - TWO-NODE THERMAL MODEL
// -------------------------------------------------------------------
// Estimates the ambient indoor air temperature of a typical UK brick
// house with windows closed and no air conditioning.
// Estimates the free-running indoor air temperature of the selected
// BUILDING_TYPES preset when EMPTY: no heating, no air conditioning, no
// occupants - only standby appliances (fridge, router) as internal gain.
//
// Two heat pathways are modelled:
// Each hour builds an outdoor-driven TARGET - where the room would settle
// if this hour's conditions held forever:
//
// 1. WALL CONDUCTION
// Heat conducts through brick cavity walls and roof. UK Part L
// compliant walls have a U-value around 0.28-0.45 W/m-K; older
// solid-brick stock runs higher. A representative mid-stock value
// is used. This drives a slow, steady heat transfer proportional
// to the difference between outdoor and indoor air temperature.
// target = outdoor ref + solar lift + standby gain
//
// 2. WINDOW SOLAR GAIN
// A typical UK semi has ~15-18% glazing ratio. Solar energy
// transmits through glass, is absorbed by floors and furniture,
// and heats the indoor air. Gain is averaged across orientations
// (not all windows face south). Diffuse radiation contributes
// regardless of sun angle.
// outdoor ref blend of outside air, the ground and heated neighbours:
// Ta x (1 - groundCoupling - neighbourCoupling)
// + ground temp x groundCoupling
// + neighbour temp x neighbourCoupling
// Heat lost through the floor goes to the ground, not the
// air, and UK ground under a building sits near 6.5-14.5 C
// through the year (ukGroundTempC). This is what keeps an
// empty house above freezing in a cold snap and a little
// cooler in a heatwave - with a FIXED gap per degree, not a
// thermostat-like one.
// Neighbours (party walls/floors of terraces and flats) are
// occupied homes: never below NEIGHBOUR_MIN_C (they heat), and
// otherwise assumed the same as this building, so no flow.
// solar lift glob x glazingRatio x gValue x orientFactor x solarScale
// (tent: glazingRatio stands in for the sunlit fabric)
// standby gain fridge/router/standby loads in an empty building (C)
//
// THERMAL LAG
// Brick and concrete have high thermal mass - the house responds
// slowly to outdoor temperature swings. Each hour builds a realistic
// target temperature from outdoor air, window solar gain, retained
// warmth, and internal gains, then the room temperature lags toward
// that target. This avoids runaway accumulation while still giving
// the characteristic late-day indoor peak.
// Call calcIndoorTempPass() on the full hourly arrays after
// building rows - it returns a per-hour indoor temp array.
// The room is then modelled as TWO coupled nodes rather than one lag:
//
// No mechanical cooling. Minimal infiltration (windows closed).
// Internal heat gains (people, appliances) are not modelled.
// 1. ROOM AIR (Ti) - fast node, time constant lagHours. Its equilibrium is
// a blend of the outdoor-driven target and the thermal mass:
// airEq = (1 - massCoupling) x target + massCoupling x Tm
// massCoupling is how strongly the walls/floors hold the air compared
// with the outdoor pull through glass, fabric and leakage.
//
// 2. THERMAL MASS (Tm) - slow node, time constant massHours. Walls, floors
// and furniture exchange heat with the room air.
//
// Why two nodes: a single exponential lag lets ~70% of the outdoor daily
// swing through for a 4 h brick constant, so a brick house swung 21-34 C on
// an 18-32 C day. Monitored UK homes swing roughly a third of the outdoor
// range and instead RATCHET UP over a multi-day heatwave as the mass warms -
// the slow node reproduces both. The effective outdoor time constant of the
// mass is massHours / (1 - massCoupling) - about 1.5 days for brick.
//
// The forecast starts at midnight with no history, so the model is spun up
// by cycling the first day SPIN_UP_DAYS times (assumes the days before the
// forecast resembled the first one) instead of starting the mass cold.
//
// Indoor CAN be cooler than outdoor (a stone cottage on a hot afternoon);
// the only clamp is a 55 C physical cap.
//
// Colour thresholds:
// < 20 -C - cool, may need heating
@@ -274,94 +293,162 @@ export function calcConcreteTempPass(TaArr, radArr, vaArr, elevArr, uvArr, cloud
// > 32 -C - hot; risk for elderly and vulnerable occupants
// -------------------------------------------------------------------
// Two-pass function: call with the full arrays of hourly Ta and globalRad.
// Returns an array of indoor temperatures, one per hour.
// buildingType must be a key of BUILDING_TYPES; defaults to 'brick'.
export function calcIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick') {
const preset = BUILDING_TYPES[buildingType] || BUILDING_TYPES.brick;
const { lagHours, glazingRatio, gValue, orientFactor, solarScale, baseTemp, internalGain, retainedScale } = preset;
// Days of the first forecast day replayed to warm up the thermal mass.
const SPIN_UP_DAYS = 5;
// Managed mode switches to the heatwave routine on any day forecast to reach
// this outdoor temperature, or whenever the room itself passes HOT_INDOOR_C.
const HOT_DAY_OUTDOOR_C = 24;
const HOT_INDOOR_C = 23;
// Windows are only opened to cool while the room is above this, so night
// ventilation never chills the house below a comfortable sleeping temp.
const VENT_FLOOR_C = 20;
// UK ground temperature at ~1 m under a building for a day of year.
// Annual mean ~10.5 C, swing +/-4 C, coldest early March, warmest early
// September (the ground lags the air by ~6 weeks). UK/northern-hemisphere
// climate assumed, matching the rest of the building model.
const GROUND_MEAN_C = 10.5;
const GROUND_SWING_C = 4;
const GROUND_COLDEST_DOY = 65;
// Occupied neighbours keep their homes at least this warm in cold weather.
const NEIGHBOUR_MIN_C = 18;
function ukGroundTempC(dayOfYear) {
return GROUND_MEAN_C - GROUND_SWING_C * Math.cos(2 * Math.PI * (dayOfYear - GROUND_COLDEST_DOY) / 365);
}
// One hour of the indoor model. Mutates and returns state { Ti, Tm }.
// managed - false: windows closed, curtains open all the time (passive)
// true : occupant follows the routine for the conditions -
// HEAT (hotDay): curtains closed while the sun is up,
// windows opened whenever outdoor air is cooler
// COOL: windows shut, curtains open by day to let the sun
// in, closed after dark to cut window heat loss
function indoorStep(state, Ta, glob, preset, managed, hotDay, groundTemp) {
const { lagHours, massHours, massCoupling, glazingRatio, gValue, orientFactor,
curtainBlock, curtainInsul, ventAlpha, solarScale, groundCoupling,
neighbourCoupling, internalGain } = preset;
const heatMode = managed && (hotDay || state.Ti >= HOT_INDOOR_C);
const coolMode = managed && !heatMode;
// Solar gain - only blocked when managing for heat and the sun is up.
const block = heatMode ? curtainBlock : 0;
const solarLift = glob * glazingRatio * gValue * orientFactor * solarScale * (1 - block);
// Outdoor pull is split between outside air, the ground under the floor
// and any heated neighbours through party walls/floors.
const neighbourTemp = Math.max(state.Ti, NEIGHBOUR_MIN_C);
const outdoorRef = (1 - groundCoupling - neighbourCoupling) * Ta
+ groundCoupling * groundTemp
+ neighbourCoupling * neighbourTemp;
let target = outdoorRef + solarLift + internalGain;
// Cool routine after dark: closed curtains cut window heat loss, so the
// room is pulled less hard toward outdoor - curtainInsul is the share of
// the outdoor heat loss they save.
if (coolMode && glob <= 0 && target < state.Ti) {
target += (state.Ti - target) * curtainInsul;
}
// Room air relaxes toward a blend of the outdoor pull and the mass.
const airEq = (1 - massCoupling) * target + massCoupling * state.Tm;
const aAir = 1 - Math.exp(-1 / lagHours);
const aMass = 1 - Math.exp(-1 / massHours);
state.Ti += aAir * (airEq - state.Ti);
// Heat routine: open windows only when outside is cooler and the room is
// still above the comfort floor; never ventilate below that floor.
if (heatMode && Ta < state.Ti && state.Ti > VENT_FLOOR_C) {
const vented = state.Ti + ventAlpha * (Ta - state.Ti);
state.Ti = Math.max(vented, VENT_FLOOR_C, Ta);
}
// Mass exchanges with the (possibly ventilated) room air - this is how
// night purging cools the fabric for the next day.
state.Tm += aMass * (state.Ti - state.Tm);
return state;
}
// Shared driver for the passive and managed passes.
// Hourly arrays are assumed to start at local midnight (forecast_days fetch),
// so hour i belongs to forecast day floor(i / 24).
// startIso is the first hour's ISO time (for the seasonal ground temp); if
// missing the annual mean ground temperature is used.
function runIndoorModel(TaArr, globArr, buildingType, managed, startIso) {
const preset = BUILDING_TYPES[buildingType] || BUILDING_TYPES.brick;
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / lagHours); // per-hour blending weight
// Seed to a plausible occupied indoor baseline rather than outdoor air.
let Ti = Math.max(TaArr[0] ?? 15, baseTemp ?? 16);
// Ground changes only ~0.5 C over a 14-day forecast - one value is enough.
const start = startIso ? new Date(startIso.slice(0, 10) + 'T00:00:00Z') : null;
const doy = start && !isNaN(start)
? Math.floor((start - Date.UTC(start.getUTCFullYear(), 0, 1)) / 86400000) + 1
: null;
const groundTemp = doy != null ? ukGroundTempC(doy) : GROUND_MEAN_C;
// Daily outdoor maxima decide the managed routine for each forecast day.
const dayMax = [];
for (let i = 0; i < n; i++) {
const d = Math.floor(i / 24);
const Ta = TaArr[i];
if (Ta != null) dayMax[d] = Math.max(dayMax[d] ?? -Infinity, Ta);
}
const isHotDay = i => (dayMax[Math.floor(i / 24)] ?? -Infinity) >= HOT_DAY_OUTDOOR_C;
// Seed both nodes at the first hour's air temp, then spin up on day one.
const seed = TaArr[0] ?? groundTemp;
const state = { Ti: seed, Tm: seed };
const spinHours = Math.min(24, n);
for (let rep = 0; rep < SPIN_UP_DAYS; rep++) {
for (let i = 0; i < spinHours; i++) {
indoorStep(state, TaArr[i] ?? state.Ti, globArr[i] ?? 0, preset, managed, isHotDay(i), groundTemp);
}
}
for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti;
const glob = globArr[i] ?? 0;
// Solar gain through windows (W/m- effective)
const solarGain = glob * glazingRatio * gValue * orientFactor;
const retainedWarmth = Math.max(0, (baseTemp ?? 16) - Ta) * (retainedScale ?? 0.35);
const target = Ta + solarGain * (solarScale ?? 0.1) + retainedWarmth + (internalGain ?? 0.7);
// Apply thermal lag: blend toward the hour's target instead of adding
// solar gain repeatedly onto the previous indoor temperature.
Ti = Ti + alpha * (target - Ti);
// Can't be colder than outdoor (house doesn't actively cool)
result[i] = Math.max(Math.min(Ti, 55), Math.min(Ta, Ti));
const Ta = TaArr[i] ?? state.Ti;
indoorStep(state, Ta, globArr[i] ?? 0, preset, managed, isHotDay(i), groundTemp);
result[i] = Math.min(state.Ti, 55);
}
return result;
}
// -------------------------------------------------------------------
// UK HOUSE INDOOR TEMPERATURE - MANAGED (curtains closed, windows open)
// -------------------------------------------------------------------
// Models the same UK brick house as calcIndoorTempPass but with two
// behavioural interventions that reflect standard heatwave advice:
//
// 1. CURTAINS CLOSED
// Thick curtains block ~80% of window solar gain before it enters
// the room. The small remaining fraction is diffuse light through
// the curtain fabric. Wall conduction is unchanged.
//
// 2. WINDOWS OPEN (smart ventilation)
// When outdoor air is cooler than the indoor air, windows are open
// and a ventilation heat exchange pulls the indoor temp toward
// outdoor. When outdoor is hotter than indoor, windows are kept
// shut - so this strategy never makes things worse, only better.
// Ventilation rate ~2 air changes/hour for a well-opened house.
//
// Same thermal lag model as calcIndoorTempPass (4 h brick time constant).
// -------------------------------------------------------------------
// PASSIVE: empty building, windows closed, curtains open, no heating or cooling.
// Returns an array of indoor temperatures, one per hour.
// buildingType must be a key of BUILDING_TYPES; defaults to 'brick'.
export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick') {
const preset = BUILDING_TYPES[buildingType] || BUILDING_TYPES.brick;
const { lagHours, glazingRatio, gValue, orientFactor, curtainBlock, ventAlpha, solarScale, baseTemp, internalGain, retainedScale } = preset;
// elevArr is accepted for signature compatibility but not needed - the
// orientFactor average already covers sun angle on the glazing.
// startIso (optional) - first hour's ISO time, sets the seasonal ground temp.
export function calcIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick', startIso) {
return runIndoorModel(TaArr, globArr, buildingType, false, startIso);
}
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / lagHours);
let Ti = Math.max(TaArr[0] ?? 15, baseTemp ?? 16);
for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti;
const glob = globArr[i] ?? 0;
// Solar gain - curtains block curtainBlock fraction
const solarGain = glob * glazingRatio * gValue * orientFactor * (1 - curtainBlock);
const retainedWarmth = Math.max(0, (baseTemp ?? 16) - Ta) * (retainedScale ?? 0.35);
const target = Ta + solarGain * (solarScale ?? 0.1) + retainedWarmth + (internalGain ?? 0.7);
// Apply thermal lag toward the managed target.
Ti = Ti + alpha * (target - Ti);
// Smart ventilation: only open windows when outside is cooler
if (Ta < Ti) {
Ti = Ti + ventAlpha * (Ta - Ti);
}
result[i] = Math.max(Math.min(Ti, 55), Math.min(Ta, Ti));
}
return result;
// -------------------------------------------------------------------
// UK HOUSE INDOOR TEMPERATURE - MANAGED
// -------------------------------------------------------------------
// The same empty building, but with its curtains and windows set sensibly
// for the conditions (by a visiting owner or smart blinds - see indoorStep):
//
// HEAT routine (day forecast >= HOT_DAY_OUTDOOR_C, or room >= HOT_INDOOR_C)
// Standard UK heatwave advice. Curtains/blinds closed while the sun is up
// (curtainBlock - internal curtains only stop ~40-55% because the heat
// is already through the glass; external shutters would be ~80%+).
// Windows opened whenever outdoor air is cooler than inside, down to a
// VENT_FLOOR_C comfort floor - so it never makes things worse.
//
// COOL routine (all other days)
// Windows shut. Curtains open in daylight to bank free solar gain,
// closed after dark to cut window heat loss (curtainInsul). Managed is
// therefore slightly WARMER than passive on cool days, not colder.
// -------------------------------------------------------------------
export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick', startIso) {
return runIndoorModel(TaArr, globArr, buildingType, true, startIso);
}
// -------------------------------------------------------------------