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')),
'SEISMIC_NOISE_FLOOR': float( os.getenv('SEISMIC_NOISE_FLOOR', '0.02')),
'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_LUX_CLEAR_SKY': float( os.getenv('CLOUD_LUX_CLEAR_SKY', '100000.0')),
'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)
aqi_factor = 1.0 + ((aqi or 0) / 500.0) * 0.5
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):
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
if rh is not None:
fields['visibility_km'] = estimate_visibility(c, rh, aqi)
fields['visibility'] = estimate_visibility(c, rh, aqi)
write(c, fields)

View File

@@ -117,14 +117,15 @@ export async function get24HourForecast(currentSensors) {
const pressureDamping = Math.exp(-i * 0.08);
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 bgTemp = seedTemp + tempTrend * (i + 1);
const temperature = Math.round(bgTemp * 10) / 10;
const humidity = forecastHumidity(seedAbsHum, temperature);
const wind_speed = forecastWind(seedWind, pressureTrend);
const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time); // ← move up
const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds); // ← now safe
const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time);
const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds);
return {
time,
@@ -135,10 +136,21 @@ export async function get24HourForecast(currentSensors) {
heat_index: Math.round(heatIndex(temperature, humidity) * 100) / 100,
vapor_pressure_deficit: vaporPressureDeficit(temperature, humidity),
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_gusts: Math.round(wind_speed * 1.4 * 10) / 10,
wind_direction: currentSensors.wind_direction ?? 0,
clouds,
precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100),
accumulation: 0,
uv_index,
uv_dose: 0,
solar_wm2: 0,
daily_light_integral: 0,
lux: 0,
visibility: 50,
...celestial,
};
});
@@ -161,13 +173,20 @@ export async function get24HourForecast(currentSensors) {
slot.heat_index = Math.round(heatIndex(slot.temperature, slot.humidity) * 100) / 100;
slot.vapor_pressure_deficit = vaporPressureDeficit(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));
}
return slots;
}
// Group slots into local-time day buckets, summarise each as daytime-only
function groupByLocalDay(slots) {
const days = {}
for (const slot of slots) {
@@ -181,21 +200,13 @@ function groupByLocalDay(slots) {
function summarise(slots) {
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 source = daytime.length ? daytime : slots
const temps = source.map(s => s.temperature).filter(Number.isFinite)
const precips = source.map(s => s.precipitation_chance).filter(Number.isFinite)
const winds = source.map(s => s.wind_speed).filter(Number.isFinite)
const gusts = source.map(s => s.wind_gusts).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 avg = (arr) => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length * 100) / 100 : null
const peak = (arr) => arr.length ? Math.max(...arr) : null
const fin = (key) => source.map(s => s[key]).filter(Number.isFinite)
const finAll = (key) => slots.map(s => s[key]).filter(Number.isFinite)
const labelCount = {}
for (const s of source) labelCount[s.label] = (labelCount[s.label] || 0) + 1
@@ -203,22 +214,45 @@ function summarise(slots) {
const dominant = source.find(s => s.label === label)
return {
time: slots[0].time,
label: dominant.label,
code: dominant.code,
icon: dominant.icon,
temperature: Math.max(...temps),
temperature_max: Math.max(...allTemps),
temperature_min: Math.min(...allTemps),
precipitation_chance: Math.round(Math.max(...precips)),
wind_speed: Math.round(Math.max(...winds) * 10) / 10,
wind_gusts: gusts.length ? Math.round(Math.max(...gusts) * 10) / 10 : null,
uv_index: uvs.length ? Math.max(...uvs) : null,
solar_wm2: solar.length ? Math.round(solar.reduce((a, b) => a + b, 0) / solar.length * 10) / 10 : null,
sunrise: slots.find(s => s.sunrise)?.sunrise ?? 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,
pressure_hpa: Math.round(source.map(s => s.pressure_hpa).filter(Number.isFinite).reduce((a, b) => a + b, 0) / source.length * 10) / 10,
time: slots[0].time,
label: dominant.label,
code: dominant.code ?? null,
icon: dominant.icon,
temperature: peak(fin('temperature')),
temperature_max: peak(finAll('temperature')),
temperature_min: Math.min(...finAll('temperature')),
humidity: avg(fin('humidity')),
humidity_abs: avg(fin('humidity_abs')),
dew_point: avg(fin('dew_point')),
heat_index: peak(fin('heat_index')),
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,
sunset: slots.find(s => s.sunset)?.sunset ?? null,
daylight: slots.find(s => s.daylight)?.daylight ?? null,
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,35 +263,30 @@ export async function getForecast() {
const slots = await get24HourForecast(sensors).catch(() => null)
if (!slots) return lastForecast
// Summarise today's local-day slots only
const days = groupByLocalDay(slots)
const todayKey = localDateStr(new Date())
const days = groupByLocalDay(slots)
const todayKey = localDateStr(new Date())
const todaySlots = days[todayKey] ?? slots
lastForecast = { ts: Date.now(), slots, summary: summarise(todaySlots) }
return lastForecast
}
export function getWeatherCondition(data) {
// Determine primary condition
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.precipitation > 7.6) return {label: 'Heavy Rain', icon: '10d'};
if(data.precipitation > 2.5) return {label: 'Moderate Rain', icon: '10d'};
return {label: 'Light Rain', icon: '09d'};
if(data.precipitation > 7.6) return {label: 'Heavy Rain', icon: '10d', code: 502};
if(data.precipitation > 2.5) return {label: 'Moderate Rain', icon: '10d', code: 501};
return {label: 'Light Rain', icon: '09d', code: 500};
}
if(data.visibility < 10) {
return {label: 'Mist', icon: '50d'};
}
if(data.visibility < 10) return {label: 'Mist', icon: '50d', code: 701};
const dayNight = data.daytime ? 'd' : 'n';
if(data.clouds > 85) return {label: 'Overcast Clouds', icon: `04${dayNight}`};
if(data.clouds > 50) return {label: 'Broken Clouds', icon: `04${dayNight}`};
if(data.clouds > 25) return {label: 'Scattered Clouds', icon: `03${dayNight}`};
if(data.clouds > 10) return {label: 'Few Clouds', icon: `02${dayNight}`};
return {label: 'Clear Sky', icon: `01${dayNight}`};
if(data.clouds > 0.85) return {label: 'Overcast Clouds', icon: `04${dayNight}`, code: 804};
if(data.clouds > 0.50) return {label: 'Broken Clouds', icon: `04${dayNight}`, code: 803};
if(data.clouds > 0.25) return {label: 'Scattered Clouds', icon: `03${dayNight}`, code: 802};
if(data.clouds > 0.10) return {label: 'Few Clouds', icon: `02${dayNight}`, code: 801};
return {label: 'Clear Sky', icon: `01${dayNight}`, code: 800};
}

View File

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