backfill all data points
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user