diff --git a/sensors/main.py b/sensors/main.py index 539fda4..2553ff4 100644 --- a/sensors/main.py +++ b/sensors/main.py @@ -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) diff --git a/server/src/forecast.mjs b/server/src/forecast.mjs index 740b272..a707c81 100644 --- a/server/src/forecast.mjs +++ b/server/src/forecast.mjs @@ -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}; } diff --git a/server/src/server.mjs b/server/src/server.mjs index ed29f26..0369993 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -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, }