backfill all data points

This commit is contained in:
2026-06-27 00:49:17 -04:00
parent 943a6ee085
commit e4042f06d6
3 changed files with 82 additions and 53 deletions

View File

@@ -32,7 +32,7 @@ def cfg():
'SNOW_STRENGTH_MIN': int( os.getenv('SNOW_STRENGTH_MIN', '700')), 'SNOW_STRENGTH_MIN': int( os.getenv('SNOW_STRENGTH_MIN', '700')),
'SEISMIC_NOISE_FLOOR': float( os.getenv('SEISMIC_NOISE_FLOOR', '0.02')), 'SEISMIC_NOISE_FLOOR': float( os.getenv('SEISMIC_NOISE_FLOOR', '0.02')),
'TEMP_CALIBRATION': float( os.getenv('TEMP_CALIBRATION', '0.0')), 'TEMP_CALIBRATION': float( os.getenv('TEMP_CALIBRATION', '0.0')),
'VISIBILITY_MAX_KM': float( os.getenv('VISIBILITY_MAX_KM', '50.0')), 'VISIBILITY_MAX': float( os.getenv('VISIBILITY_MAX', '50.0')),
'CLOUD_SUN_MIN_ELEV': float( os.getenv('CLOUD_SUN_MIN_ELEV', '5.0')), 'CLOUD_SUN_MIN_ELEV': float( os.getenv('CLOUD_SUN_MIN_ELEV', '5.0')),
'CLOUD_LUX_CLEAR_SKY': float( os.getenv('CLOUD_LUX_CLEAR_SKY', '100000.0')), 'CLOUD_LUX_CLEAR_SKY': float( os.getenv('CLOUD_LUX_CLEAR_SKY', '100000.0')),
'CLOUD_HUMIDITY_WEIGHT': float( os.getenv('CLOUD_HUMIDITY_WEIGHT', '0.3')), 'CLOUD_HUMIDITY_WEIGHT': float( os.getenv('CLOUD_HUMIDITY_WEIGHT', '0.3')),
@@ -273,7 +273,7 @@ def estimate_visibility(c, rh, aqi):
beta_rh = 0.000146 * math.exp(0.06 * rh_clamped) beta_rh = 0.000146 * math.exp(0.06 * rh_clamped)
aqi_factor = 1.0 + ((aqi or 0) / 500.0) * 0.5 aqi_factor = 1.0 + ((aqi or 0) / 500.0) * 0.5
extinction = beta_rh * aqi_factor extinction = beta_rh * aqi_factor
return round(min(3.912 / extinction, c['VISIBILITY_MAX_KM']), 2) return round(min(3.912 / extinction, c['VISIBILITY_MAX']), 2)
def read_ltr(c, sensor, temp=None, dew_point=None, rh=None, aqi=None, lat=0.0, lon=0.0): def read_ltr(c, sensor, temp=None, dew_point=None, rh=None, aqi=None, lat=0.0, lon=0.0):
global uv_dose_mj, uv_dose_date, last_uv_time global uv_dose_mj, uv_dose_date, last_uv_time
@@ -315,7 +315,7 @@ def read_ltr(c, sensor, temp=None, dew_point=None, rh=None, aqi=None, lat=0.0, l
fields['sun_elevation'] = sun_elev fields['sun_elevation'] = sun_elev
if rh is not None: if rh is not None:
fields['visibility_km'] = estimate_visibility(c, rh, aqi) fields['visibility'] = estimate_visibility(c, rh, aqi)
write(c, fields) write(c, fields)

View File

@@ -117,14 +117,15 @@ export async function get24HourForecast(currentSensors) {
const pressureDamping = Math.exp(-i * 0.08); const pressureDamping = Math.exp(-i * 0.08);
const pressure = seedPressure + pressureTrend * (i + 1) * pressureDamping; const pressure = seedPressure + pressureTrend * (i + 1) * pressureDamping;
const pressure_slp = Math.round((pressure + (coords.altitude ?? 0) / 8.5) * 100) / 100;
const clouds = estimateClouds(seedHumidity + humidityTrend * i, pressureTrend); const clouds = estimateClouds(seedHumidity + humidityTrend * i, pressureTrend);
const bgTemp = seedTemp + tempTrend * (i + 1); const bgTemp = seedTemp + tempTrend * (i + 1);
const temperature = Math.round(bgTemp * 10) / 10; const temperature = Math.round(bgTemp * 10) / 10;
const humidity = forecastHumidity(seedAbsHum, temperature); const humidity = forecastHumidity(seedAbsHum, temperature);
const wind_speed = forecastWind(seedWind, pressureTrend); const wind_speed = forecastWind(seedWind, pressureTrend);
const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time); // ← move up const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time);
const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds); // ← now safe const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds);
return { return {
time, time,
@@ -135,10 +136,21 @@ export async function get24HourForecast(currentSensors) {
heat_index: Math.round(heatIndex(temperature, humidity) * 100) / 100, heat_index: Math.round(heatIndex(temperature, humidity) * 100) / 100,
vapor_pressure_deficit: vaporPressureDeficit(temperature, humidity), vapor_pressure_deficit: vaporPressureDeficit(temperature, humidity),
pressure_hpa: Math.round(pressure * 100) / 100, pressure_hpa: Math.round(pressure * 100) / 100,
pressure_slp,
pressure_rate: Math.round(pressureTrend * 100) / 100,
storm_trend: pressure3h < -2 ? 1 : pressure3h > 1.5 ? -1 : 0,
wind_speed, wind_speed,
wind_gusts: Math.round(wind_speed * 1.4 * 10) / 10,
wind_direction: currentSensors.wind_direction ?? 0,
clouds, clouds,
precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100), precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100),
accumulation: 0,
uv_index, uv_index,
uv_dose: 0,
solar_wm2: 0,
daily_light_integral: 0,
lux: 0,
visibility: 50,
...celestial, ...celestial,
}; };
}); });
@@ -161,13 +173,20 @@ export async function get24HourForecast(currentSensors) {
slot.heat_index = Math.round(heatIndex(slot.temperature, slot.humidity) * 100) / 100; slot.heat_index = Math.round(heatIndex(slot.temperature, slot.humidity) * 100) / 100;
slot.vapor_pressure_deficit = vaporPressureDeficit(slot.temperature, slot.humidity); slot.vapor_pressure_deficit = vaporPressureDeficit(slot.temperature, slot.humidity);
slot.humidity_abs = absoluteHumidity(slot.temperature, slot.humidity); slot.humidity_abs = absoluteHumidity(slot.temperature, slot.humidity);
slot.solar_wm2 = slot.sun_elevation > 0
? Math.round(Math.sin((slot.sun_elevation * Math.PI) / 180) * 1000 * (1 - slot.clouds * 0.75) * 100) / 100
: 0;
slot.daily_light_integral = Math.round(slot.solar_wm2 * 3600 / 1_000_000 * 4.57 * 100) / 100;
slot.lux = slot.solar_wm2 > 0 ? Math.round(slot.solar_wm2 * 120) : 0;
slot.visibility = slot.clouds > 0.85 ? 10 : 50;
Object.assign(slot, getWeatherCondition(slot)); Object.assign(slot, getWeatherCondition(slot));
} }
return slots; return slots;
} }
// Group slots into local-time day buckets, summarise each as daytime-only
function groupByLocalDay(slots) { function groupByLocalDay(slots) {
const days = {} const days = {}
for (const slot of slots) { for (const slot of slots) {
@@ -181,21 +200,13 @@ function groupByLocalDay(slots) {
function summarise(slots) { function summarise(slots) {
if (!slots.length) return null if (!slots.length) return null
// Use only daytime slots for representative label/stats, fallback to all if none
const daytime = slots.filter(s => s.daytime) const daytime = slots.filter(s => s.daytime)
const source = daytime.length ? daytime : slots const source = daytime.length ? daytime : slots
const temps = source.map(s => s.temperature).filter(Number.isFinite) const avg = (arr) => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length * 100) / 100 : null
const precips = source.map(s => s.precipitation_chance).filter(Number.isFinite) const peak = (arr) => arr.length ? Math.max(...arr) : null
const winds = source.map(s => s.wind_speed).filter(Number.isFinite) const fin = (key) => source.map(s => s[key]).filter(Number.isFinite)
const gusts = source.map(s => s.wind_gusts).filter(Number.isFinite) const finAll = (key) => slots.map(s => s[key]).filter(Number.isFinite)
const uvs = source.map(s => s.uv_index).filter(Number.isFinite)
const solar = source.map(s => s.solar_wm2).filter(Number.isFinite)
// Min/max temp uses ALL slots for the full day range
const allTemps = slots.map(s => s.temperature).filter(Number.isFinite)
console.log(slots);
const labelCount = {} const labelCount = {}
for (const s of source) labelCount[s.label] = (labelCount[s.label] || 0) + 1 for (const s of source) labelCount[s.label] = (labelCount[s.label] || 0) + 1
@@ -205,20 +216,43 @@ function summarise(slots) {
return { return {
time: slots[0].time, time: slots[0].time,
label: dominant.label, label: dominant.label,
code: dominant.code, code: dominant.code ?? null,
icon: dominant.icon, icon: dominant.icon,
temperature: Math.max(...temps), temperature: peak(fin('temperature')),
temperature_max: Math.max(...allTemps), temperature_max: peak(finAll('temperature')),
temperature_min: Math.min(...allTemps), temperature_min: Math.min(...finAll('temperature')),
precipitation_chance: Math.round(Math.max(...precips)), humidity: avg(fin('humidity')),
wind_speed: Math.round(Math.max(...winds) * 10) / 10, humidity_abs: avg(fin('humidity_abs')),
wind_gusts: gusts.length ? Math.round(Math.max(...gusts) * 10) / 10 : null, dew_point: avg(fin('dew_point')),
uv_index: uvs.length ? Math.max(...uvs) : null, heat_index: peak(fin('heat_index')),
solar_wm2: solar.length ? Math.round(solar.reduce((a, b) => a + b, 0) / solar.length * 10) / 10 : null, vapor_pressure_deficit: avg(fin('vapor_pressure_deficit')),
pressure_hpa: avg(fin('pressure_hpa')),
pressure_slp: avg(fin('pressure_slp')),
pressure_rate: avg(fin('pressure_rate')),
storm_trend: slots.at(-1)?.storm_trend ?? 0,
wind_speed: peak(fin('wind_speed')),
wind_gusts: peak(fin('wind_gusts')),
wind_direction: avg(fin('wind_direction')),
clouds: avg(fin('clouds')),
precipitation_chance: Math.round(peak(fin('precipitation_chance'))),
accumulation: 0,
uv_index: peak(fin('uv_index')),
uv_dose: finAll('uv_dose').reduce((a, b) => a + b, 0),
solar_wm2: avg(fin('solar_wm2')),
daily_light_integral: Math.round(finAll('daily_light_integral').reduce((a, b) => a + b, 0) * 100) / 100,
lux: peak(fin('lux')),
visibility: avg(fin('visibility')),
sunrise: slots.find(s => s.sunrise)?.sunrise ?? null, sunrise: slots.find(s => s.sunrise)?.sunrise ?? null,
sunset: slots.find(s => s.sunset)?.sunset ?? null, sunset: slots.find(s => s.sunset)?.sunset ?? null,
humidity: Math.round(source.map(s => s.humidity).filter(Number.isFinite).reduce((a, b) => a + b, 0) / source.length * 10) / 10, daylight: slots.find(s => s.daylight)?.daylight ?? null,
pressure_hpa: Math.round(source.map(s => s.pressure_hpa).filter(Number.isFinite).reduce((a, b) => a + b, 0) / source.length * 10) / 10, moon_phase: dominant.moon_phase ?? null,
moon_illumination: dominant.moon_illumination ?? null,
moon_elevation: dominant.moon_elevation ?? null,
moon_azimuth: dominant.moon_azimuth ?? null,
moon_new: dominant.moon_new ?? null,
moon_full: dominant.moon_full ?? null,
moonrise: slots.find(s => s.moonrise)?.moonrise ?? null,
moonset: slots.find(s => s.moonset)?.moonset ?? null,
} }
} }
@@ -229,7 +263,6 @@ export async function getForecast() {
const slots = await get24HourForecast(sensors).catch(() => null) const slots = await get24HourForecast(sensors).catch(() => null)
if (!slots) return lastForecast if (!slots) return lastForecast
// Summarise today's local-day slots only
const days = groupByLocalDay(slots) const days = groupByLocalDay(slots)
const todayKey = localDateStr(new Date()) const todayKey = localDateStr(new Date())
const todaySlots = days[todayKey] ?? slots const todaySlots = days[todayKey] ?? slots
@@ -238,26 +271,22 @@ export async function getForecast() {
} }
export function getWeatherCondition(data) { export function getWeatherCondition(data) {
// Determine primary condition
if(data.lightning_rate > 0 && data.raining === 'True') { if(data.lightning_rate > 0 && data.raining === 'True') {
return {label: 'Thunderstorm', icon: '11d'}; return {label: 'Thunderstorm', icon: '11d', code: 211};
} }
if(data.raining === 'True') { if(data.raining === 'True') {
if(data.precipitation > 7.6) return {label: 'Heavy Rain', icon: '10d'}; if(data.precipitation > 7.6) return {label: 'Heavy Rain', icon: '10d', code: 502};
if(data.precipitation > 2.5) return {label: 'Moderate Rain', icon: '10d'}; if(data.precipitation > 2.5) return {label: 'Moderate Rain', icon: '10d', code: 501};
return {label: 'Light Rain', icon: '09d'}; return {label: 'Light Rain', icon: '09d', code: 500};
} }
if(data.visibility < 10) { if(data.visibility < 10) return {label: 'Mist', icon: '50d', code: 701};
return {label: 'Mist', icon: '50d'};
}
const dayNight = data.daytime ? 'd' : 'n'; const dayNight = data.daytime ? 'd' : 'n';
if(data.clouds > 85) return {label: 'Overcast Clouds', icon: `04${dayNight}`}; if(data.clouds > 0.85) return {label: 'Overcast Clouds', icon: `04${dayNight}`, code: 804};
if(data.clouds > 50) return {label: 'Broken Clouds', icon: `04${dayNight}`}; if(data.clouds > 0.50) return {label: 'Broken Clouds', icon: `04${dayNight}`, code: 803};
if(data.clouds > 25) return {label: 'Scattered Clouds', icon: `03${dayNight}`}; if(data.clouds > 0.25) return {label: 'Scattered Clouds', icon: `03${dayNight}`, code: 802};
if(data.clouds > 10) return {label: 'Few Clouds', icon: `02${dayNight}`}; if(data.clouds > 0.10) return {label: 'Few Clouds', icon: `02${dayNight}`, code: 801};
return {label: 'Clear Sky', icon: `01${dayNight}`, code: 800};
return {label: 'Clear Sky', icon: `01${dayNight}`};
} }

View File

@@ -55,14 +55,14 @@ app.get('/api/current', async (req, res) => {
const forecast = await getForecast(); const forecast = await getForecast();
const merged = { const merged = {
...condition,
...sensors,
...(forecast?.summary ? { ...(forecast?.summary ? {
precipitation_chance: forecast.summary.precipitation_chance, precipitation_chance: forecast.summary.precipitation_chance,
temperature_max: forecast.summary.temperature_max, temperature_max: forecast.summary.temperature_max,
temperature_min: forecast.summary.temperature_min, temperature_min: forecast.summary.temperature_min,
uv_index_max: forecast.summary.uv_index, uv_index_max: forecast.summary.uv_index,
} : {}), } : {}),
...condition,
...sensors,
...space, ...space,
} }