Correct 'Shade' temps for night
This commit is contained in:
fraxle
2026-08-02 17:38:59 +01:00
parent ff86b6d5ca
commit ffb5ed1d7d
8 changed files with 91 additions and 30 deletions
+20 -12
View File
@@ -14,7 +14,7 @@
import {
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
calcConcreteTempPass, calcVehicleInteriorTempPass,
calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp,
calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp, calcShadeFeltTemp,
calcFurSurfaceTempPass,
} from './physics.js';
import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel, FUR_COLORS } from './utils.js';
@@ -84,15 +84,22 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const difEnv = dif * env.difFactor;
const globEnv = glob * env.globFactor;
// concreteT is now stamped in the two-pass section below (thermal lag).
// Shade air temp: the air you'd feel in this Solar Model's typical shade
// (building shadow, beach umbrella, canopy...). Shares SunSoak's TaEnv
// baseline and env-reduced radiation, so the two columns stay consistent.
const shadeT = calcShadeAirTemp(TaEnv, dirEnv + difEnv * 0.2, va, elev, env.shade);
// vehicleT is stamped in the two-pass section below (thermal lag).
const eh = vaporPressureHpa(Ta, RH);
const Tmrt = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev);
const utci = utciApprox(TaEnv, Tmrt, va, eh);
const utciAdj = utci + precipPenalty(precip, snow, va);
// Shade air temp: what a thermometer in this Solar Model's typical shade
// (building shadow, beach umbrella, canopy...) would read. Shares
// SunSoak's TaEnv baseline and env-reduced radiation, so the two stay
// consistent. Pet Shade reads this directly; the human Shade column
// below turns it into a felt temperature first.
const shadeAirT = calcShadeAirTemp(TaEnv, dirEnv + difEnv * 0.2, va, elev, env.shade);
// Shade (felt): SunSoak with the direct beam taken away — still outdoors,
// so the same wind, humidity and rain penalty apply. See
// calcShadeFeltTemp() for why this is a felt number and not the air temp.
const shadeT = calcShadeFeltTemp(shadeAirT, difEnv, va, eh, elev, env.shade)
+ precipPenalty(precip, snow, va);
// Derived
const compass = windCompass8(wd);
const { uvA, uvB } = uvSplit(uv, elev);
@@ -154,10 +161,11 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
uv, uvA, uvB,
precip, precipProb, lightning, cape, snow,
soilT0, soilT6, soilM, shadeT,
// Pet Shade reads the same hourly value as Shade but aggregates as a
// MEAN rather than a worst case, so it needs its own field to diverge
// from shadeT at 2h/3h/4h. Same for pawT vs concreteT below.
petShadeT: shadeT,
// Pet Shade stays on the shade AIR temperature: the human Shade column
// is a UTCI felt number on a human comfort scale, which says nothing
// useful about a cat. It also aggregates as a MEAN rather than a worst
// case, so it needs its own field either way (same for pawT/concreteT).
petShadeT: shadeAirT,
effectiveRad,
elev, Tmrt, utci, utciAdj, eh, compass,
visKm, aqi,
@@ -723,7 +731,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
// ── Pets (cats / small dogs) ────────────────────────────────────────────
if (profile === 'pets') {
const peakFur = peakRow('furSurfaceT');
const peakShade = peakRow('shadeT');
const peakShade = peakRow('petShadeT');
const peakIndoor = peakRow('indoorT');
const maxPollen = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0));
const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
@@ -740,8 +748,8 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
...(show('petShadeT') ? [{
icon: '🌳',
label: 'Shade for pet',
value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-',
alert: !!(peakShade && peakShade.shadeT >= 26),
value: peakShade ? `${Math.round(peakShade.petShadeT)}° at ${hhmm(peakShade.iso)}` : '-',
alert: !!(peakShade && peakShade.petShadeT >= 26),
grp: 'ambient',
}] : []),
...(show('petHomeT') ? [{