diff --git a/about.html b/about.html
index f3fb032..5039ba1 100644
--- a/about.html
+++ b/about.html
@@ -219,12 +219,13 @@
@@ -1246,19 +1266,19 @@ export function UTCIForecast() {
${visibleCols.air && html`| ${r.Ta.toFixed(1)} | `}
- ${visibleCols.rh && html`${Math.round(r.RH)} | `}
- ${visibleCols.dew && html`${r.dew != null ? r.dew.toFixed(1) : '—'} | `}
+ ${visibleCols.rh && html`${Math.round(r.RH)} | `}
+ ${visibleCols.dew && html`${r.dew != null ? r.dew.toFixed(1) : '—'} | `}
${visibleCols.soilT && html`
${r.soilT0 != null ? r.soilT0.toFixed(1) : '—'} | `}
${visibleCols.soilT6 && html`
- ${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'} | `}
+ ${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'} | `}
${visibleCols.soilM && html`
- ${r.soilM != null ? r.soilM.toFixed(3) : '—'} | `}
+ ${r.soilM != null ? r.soilM.toFixed(3) : '—'} | `}
${visibleCols.concreteT && html`
40 ? '#c0392b' : r.concreteT != null && r.concreteT > 30 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.concreteT) }}>
${r.concreteT != null ? r.concreteT.toFixed(1) : '—'}
| `}
- ${visibleCols.wind && html`
+ ${visibleCols.wind && html` |
${r.va.toFixed(1)}${r.gust != null && r.gust > r.va + 0.5
? html`(${r.gust.toFixed(1)})`
: ''}
@@ -1269,40 +1289,41 @@ export function UTCIForecast() {
${r.compass.label}
| `}
- ${visibleCols.cloud && html`
+ ${visibleCols.cloud && html` |
<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${28} />
${Math.round(r.cc)}
| `}
- ${visibleCols.sun && html`${r.elev > 0 ? r.elev.toFixed(1) : '—'} | `}
- ${visibleCols.direct && html`${Math.round(r.dir)} | `}
- ${visibleCols.diffuse && html`${Math.round(r.dif)} | `}
- ${visibleCols.tmrt && html`${r.Tmrt.toFixed(1)} | `}
+ ${visibleCols.sun && html`${r.elev > 0 ? r.elev.toFixed(1) : '—'} | `}
+ ${visibleCols.direct && html`${Math.round(r.dir)} | `}
+ ${visibleCols.diffuse && html`${Math.round(r.dif)} | `}
+ ${visibleCols.tmrt && html`${r.Tmrt.toFixed(1)} | `}
${visibleCols.delta && html`
3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#4a3218',
fontWeight: 600,
+ background: deltaBg(delta),
}}>
${delta > 0 ? '+' : ''}${delta.toFixed(1)}
| `}
${visibleCols.utci && html`
-
+ |
${r.utci.toFixed(1)}
| `}
${visibleCols.uvA && html`
- 0 ? '#c8922a' : '#4a3218' }}>
+ | 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}>
${r.uvA > 0 ? r.uvA.toFixed(1) : '—'}
| `}
${visibleCols.uvB && html`
- 0 ? '#c44a3a' : '#4a3218' }}>
+ | 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}>
${r.uvB > 0 ? r.uvB.toFixed(2) : '—'}
| `}
${visibleCols.burn && html`
- 0 ? (sunburnMinutes(r.uv, skinType) < 30 ? '#c44a3a' : '#c8601a') : '#4a3218' }}>
- ${burnLabel(sunburnMinutes(r.uv, skinType))}
+ | 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins) }}>
+ ${burnLabel(burnMins)}
| `}
${visibleCols.vehicleT && html`
45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.vehicleT) }}>
@@ -1317,7 +1338,7 @@ export function UTCIForecast() {
${r.managedT != null ? r.managedT.toFixed(1) : '—'}
| `}
${visibleCols.precip && html`
-
+ | 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}>
<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} />
0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}>
@@ -1326,7 +1347,7 @@ export function UTCIForecast() {
| `}
${visibleCols.utciP && html`
-
+ |
${r.utciAdj.toFixed(1)}
diff --git a/assets/js/physics.js b/assets/js/physics.js
index 46e1b70..13a1d16 100644
--- a/assets/js/physics.js
+++ b/assets/js/physics.js
@@ -86,10 +86,11 @@ export function calcConcreteTemp(Ta, globalRad, windSpeed) {
//
// THERMAL LAG
// Brick and concrete have high thermal mass — the house responds
-// slowly to outdoor temperature swings. This function takes a
-// weighted average of the current hour's heat load and the
-// previous few hours', giving the characteristic lag where indoor
-// temperature peaks 2–4 hours after the outdoor peak.
+// 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.
//
@@ -125,14 +126,14 @@ function _houseHeatLoad(Ta, globalRad, solElev) {
// 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 { uWall, lagHours, glazingRatio, gValue, orientFactor } = preset;
+ const { lagHours, glazingRatio, gValue, orientFactor, solarScale, baseTemp, internalGain, retainedScale } = preset;
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / lagHours); // per-hour blending weight
- // Seed indoor temp to first outdoor temp
- let Ti = TaArr[0] ?? 15;
+ // Seed to a plausible occupied indoor baseline rather than outdoor air.
+ let Ti = Math.max(TaArr[0] ?? 15, baseTemp ?? 16);
for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti;
@@ -141,16 +142,12 @@ export function calcIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'bric
// Solar gain through windows (W/m² effective)
const solarGain = glob * glazingRatio * gValue * orientFactor;
- // Conductive heat flow through walls: proportional to (Ta - Ti)
- const conductionGain = uWall * (Ta - Ti);
+ const retainedWarmth = Math.max(0, (baseTemp ?? 16) - Ta) * (retainedScale ?? 0.35);
+ const target = Ta + solarGain * (solarScale ?? 0.1) + retainedWarmth + (internalGain ?? 0.7);
- // Heat capacity proxy: how many °C does 1 W/m² raise the indoor air?
- // Typical 90 m² house ≈ 0.15; conservatory much lower (less thermal mass).
- const heatCapProxy = 0.15;
- const Ti_instant = Ti + (conductionGain + solarGain) * heatCapProxy;
-
- // Apply thermal lag: blend toward Ti_instant at the building's time constant
- Ti = Ti + alpha * (Ti_instant - Ti);
+ // 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));
@@ -182,13 +179,13 @@ export function calcIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'bric
// 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 { uWall, lagHours, glazingRatio, gValue, orientFactor, curtainBlock, ventAlpha } = preset;
+ const { lagHours, glazingRatio, gValue, orientFactor, curtainBlock, ventAlpha, solarScale, baseTemp, internalGain, retainedScale } = preset;
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / lagHours);
- let Ti = TaArr[0] ?? 15;
+ let Ti = Math.max(TaArr[0] ?? 15, baseTemp ?? 16);
for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti;
@@ -197,14 +194,11 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
// Solar gain — curtains block curtainBlock fraction
const solarGain = glob * glazingRatio * gValue * orientFactor * (1 - curtainBlock);
- // Wall conduction (unchanged from unmanaged model)
- const conductionGain = uWall * (Ta - Ti);
+ const retainedWarmth = Math.max(0, (baseTemp ?? 16) - Ta) * (retainedScale ?? 0.35);
+ const target = Ta + solarGain * (solarScale ?? 0.1) + retainedWarmth + (internalGain ?? 0.7);
- const heatCapProxy = 0.15;
- const Ti_instant = Ti + (conductionGain + solarGain) * heatCapProxy;
-
- // Apply thermal lag
- Ti = Ti + alpha * (Ti_instant - Ti);
+ // Apply thermal lag toward the managed target.
+ Ti = Ti + alpha * (target - Ti);
// Smart ventilation: only open windows when outside is cooler
if (Ta < Ti) {
diff --git a/assets/js/utils.js b/assets/js/utils.js
index 4dc0259..f7c82cc 100644
--- a/assets/js/utils.js
+++ b/assets/js/utils.js
@@ -166,15 +166,21 @@ export const VEHICLE_TYPES = {
// closed). Thick lined curtains ≈ 0.80; blinds ≈ 0.50.
// ventAlpha — Blending weight per hour when smart ventilation is open.
// Higher = more air changes per hour.
+// solarScale — Converts effective window solar gain into an indoor
+// temperature lift. Lower values mean more thermal mass.
+// baseTemp — Occupied/retained warmth baseline for normal homes.
+// internalGain — Small heat gain from people, appliances, and background use.
+// retainedScale — How strongly the building holds above-outdoor warmth in
+// cool conditions. Higher = better retained warmth.
// ═══════════════════════════════════════════════════════════════════
export const BUILDING_TYPES = {
- brick: { name: 'Brick (typical)', uWall: 0.35, lagHours: 4, glazingRatio: 0.16, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.25 },
- modern: { name: 'Modern / Well insulated', uWall: 0.18, lagHours: 5, glazingRatio: 0.20, gValue: 0.30, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.20 },
- victorian: { name: 'Victorian Terrace', uWall: 0.55, lagHours: 5, glazingRatio: 0.12, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.30 },
- stone: { name: 'Stone / Granite Cottage', uWall: 0.45, lagHours: 7, glazingRatio: 0.10, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.25 },
- timber: { name: 'Timber Frame / New Build', uWall: 0.22, lagHours: 2, glazingRatio: 0.22, gValue: 0.35, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.35 },
- flat: { name: 'Top-floor Flat', uWall: 0.40, lagHours: 3, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.20 },
- conservatory:{ name: 'Conservatory / Sun Room', uWall: 1.20, lagHours: 1, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, ventAlpha: 0.50 },
+ brick: { name: 'Brick (typical)', uWall: 0.35, lagHours: 4, glazingRatio: 0.16, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.25, solarScale: 0.10, baseTemp: 16.5, internalGain: 0.8, retainedScale: 0.35 },
+ modern: { name: 'Modern / Well insulated', uWall: 0.18, lagHours: 5, glazingRatio: 0.20, gValue: 0.30, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.20, solarScale: 0.08, baseTemp: 17.0, internalGain: 0.8, retainedScale: 0.55 },
+ victorian: { name: 'Victorian Terrace', uWall: 0.55, lagHours: 5, glazingRatio: 0.12, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.30, solarScale: 0.10, baseTemp: 16.0, internalGain: 0.7, retainedScale: 0.25 },
+ stone: { name: 'Stone / Granite Cottage', uWall: 0.45, lagHours: 7, glazingRatio: 0.10, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.25, solarScale: 0.08, baseTemp: 15.5, internalGain: 0.6, retainedScale: 0.40 },
+ timber: { name: 'Timber Frame / New Build', uWall: 0.22, lagHours: 2, glazingRatio: 0.22, gValue: 0.35, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.35, solarScale: 0.08, baseTemp: 17.0, internalGain: 0.8, retainedScale: 0.45 },
+ flat: { name: 'Top-floor Flat', uWall: 0.40, lagHours: 3, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.20, solarScale: 0.11, baseTemp: 17.0, internalGain: 0.9, retainedScale: 0.45 },
+ conservatory:{ name: 'Conservatory / Sun Room', uWall: 1.20, lagHours: 1, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, ventAlpha: 0.50, solarScale: 0.045, baseTemp: 12.0, internalGain: 0.2, retainedScale: 0.05 },
};
// ═══════════════════════════════════════════════════════════════════
|