diff --git a/server/src/forecast.mjs b/server/src/forecast.mjs index 979a7c6..88a6f35 100644 --- a/server/src/forecast.mjs +++ b/server/src/forecast.mjs @@ -1,8 +1,6 @@ import {queryHourly, getCoords, queryCurrent} from './sensors.mjs'; import {getCelestialCurrent} from './celestial.mjs'; -import {getWeatherCondition} from './openweather.mjs'; import {localDateStr} from './config.mjs'; -import {uvLabel} from './openmeteo.mjs'; export let lastForecast = { ts: 0, slots: [], summary: null } export const forecastTTL = 5 * 60 * 60_000 @@ -118,16 +116,17 @@ export async function get24HourForecast(currentSensors) { time.setMinutes(0, 0, 0); const pressureDamping = Math.exp(-i * 0.08); - const pressure = seedPressure + pressureTrend * (i + 1) * pressureDamping; - 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 uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds); + const pressure = seedPressure + pressureTrend * (i + 1) * pressureDamping; + 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); - const snapshot = { + const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time); // ← move up + const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds); // ← now safe + + return { time, temperature, humidity, @@ -140,12 +139,8 @@ export async function get24HourForecast(currentSensors) { clouds, precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100), uv_index, - uv_index_label: uvLabel(uv_index), - ...celestial + ...celestial, }; - - Object.assign(snapshot, getWeatherCondition(snapshot)); - return snapshot; }); for (const slot of slots) { @@ -216,7 +211,6 @@ function summarise(slots) { 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, - uv_index_label: dominant.uv_index_label ?? 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, @@ -239,3 +233,28 @@ export async function getForecast() { 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'}; + } + + 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.visibility < 10) { + return {label: 'Mist', icon: '50d'}; + } + + 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}`}; +} diff --git a/server/src/openmeteo.mjs b/server/src/openmeteo.mjs index 06d8ce0..bda0206 100644 --- a/server/src/openmeteo.mjs +++ b/server/src/openmeteo.mjs @@ -68,22 +68,6 @@ function absoluteHumidity(tempC, humidity) { return Math.round((humidity / 100 * svp * 2165) / (tempC + 273.15) * 1000) / 1000 } -export function uvLabel(uv) { - if (uv < 3) return 'Low' - if (uv < 6) return 'Moderate' - if (uv < 8) return 'High' - if (uv < 11) return 'Very High' - return 'Extreme' -} - -function cloudsLabel(fraction) { - if (fraction < 0.1) return 'Clear' - if (fraction < 0.3) return 'Mostly Clear' - if (fraction < 0.6) return 'Partly Cloudy' - if (fraction < 0.9) return 'Mostly Cloudy' - return 'Overcast' -} - // ── Daily ───────────────────────────────────────────────────────────────────── const DAILY_FIELDS = [ @@ -134,7 +118,6 @@ export async function dailyWeather(lat, lon, start, end) { wind_gusts: data.daily.windgusts_10m_max[i], wind_direction: data.daily.winddirection_10m_dominant[i], uv_index: uv, - uv_index_label: uvLabel(uv), solar_wm2: data.daily.shortwave_radiation_sum[i], clouds: null, sunrise: data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null, @@ -176,11 +159,8 @@ export async function hourlyWeather(lat, lon, start, end) { } return Promise.all(data.hourly.time.map(async (time, i) => { - const dateKey = time.slice(0, 10) // "2026-03-01" from local string + const dateKey = time.slice(0, 10) const sun = sunMap[dateKey] ?? {} - // Open-Meteo local strings have no Z — append Z to treat as UTC for ms comparison - // This works because Open-Meteo already returns times in the location's local tz - // and sunrise/sunset are parsed the same way, so the comparison is consistent const t = new Date(time + 'Z').getTime() const daytime = sun.sunrise && sun.sunset ? t >= new Date(sun.sunrise.toISOString().replace('Z', '') + 'Z').getTime() && t < new Date(sun.sunset.toISOString().replace('Z', '') + 'Z').getTime() @@ -212,9 +192,7 @@ export async function hourlyWeather(lat, lon, start, end) { wind_gusts: data.hourly.windgusts_10m[i], wind_direction: data.hourly.winddirection_10m[i], uv_index: uv, - uv_index_label: uvLabel(uv), solar_wm2: data.hourly.shortwave_radiation[i], - clouds_label: cloudsLabel(clouds), visibility: data.hourly.visibility[i] != null ? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10 : null, diff --git a/server/src/openweather.mjs b/server/src/openweather.mjs index 3c53aef..6c7184d 100644 --- a/server/src/openweather.mjs +++ b/server/src/openweather.mjs @@ -1,7 +1,7 @@ import {existsSync, mkdirSync, writeFileSync} from 'fs'; +import * as path from 'node:path'; import {resolve, dirname} from 'path'; import {fileURLToPath} from 'url'; -import * as path from 'node:path'; const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../data/icons'); @@ -18,28 +18,3 @@ export async function fetchIcon(icon) { await downloadIcon(icon, iconPath); return iconPath; } - -export function getWeatherCondition(data) { - // Determine primary condition - if(data.lightning_rate > 0 && data.raining === 'True') { - return {label: 'Thunderstorm', code: 211, icon: '11d'}; - } - - if(data.raining === 'True') { - if(data.precipitation > 7.6) return {label: 'Heavy Rain', code: 502, icon: '10d'}; - if(data.precipitation > 2.5) return {label: 'Moderate Rain', code: 501, icon: '10d'}; - return {label: 'Light Rain', code: 500, icon: '09d'}; - } - - if(data.visibility < 10) { - return {label: 'Mist', code: 701, icon: '50d'}; - } - - const dayNight = data.daytime ? 'd' : 'n'; - if(data.clouds > 85) return {label: 'Overcast Clouds', code: 804, icon: `04${dayNight}`}; - if(data.clouds > 50) return {label: 'Broken Clouds', code: 803, icon: `04${dayNight}`}; - if(data.clouds > 25) return {label: 'Scattered Clouds', code: 802, icon: `03${dayNight}`}; - if(data.clouds > 10) return {label: 'Few Clouds', code: 801, icon: `02${dayNight}`}; - - return {label: 'Clear Sky', code: 800, icon: `01${dayNight}`}; -} diff --git a/server/src/server.mjs b/server/src/server.mjs index 11458f6..ed29f26 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -9,8 +9,8 @@ import {spec} from './spec.mjs'; import {existsSync} from 'fs'; import {getAIS} from './ais.mjs'; import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs'; -import {fetchIcon, getWeatherCondition} from './openweather.mjs'; -import {lastForecast, getForecast, forecastTTL} from './forecast.mjs'; +import {fetchIcon} from './openweather.mjs'; +import {lastForecast, getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; // ── Uncaught error handlers ───────────────────────────────────────────────────