Fixing 24 hour forecast
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import {queryHourly, getCoords, queryCurrent} from './sensors.mjs';
|
import {queryHourly, getCoords, queryCurrent} from './sensors.mjs';
|
||||||
import {getCelestialCurrent} from './celestial.mjs';
|
import {getCelestialCurrent} from './celestial.mjs';
|
||||||
import {getWeatherCondition} from './openweather.mjs';
|
|
||||||
import {localDateStr} from './config.mjs';
|
import {localDateStr} from './config.mjs';
|
||||||
import {uvLabel} from './openmeteo.mjs';
|
|
||||||
|
|
||||||
export let lastForecast = { ts: 0, slots: [], summary: null }
|
export let lastForecast = { ts: 0, slots: [], summary: null }
|
||||||
export const forecastTTL = 5 * 60 * 60_000
|
export const forecastTTL = 5 * 60 * 60_000
|
||||||
@@ -124,10 +122,11 @@ export async function get24HourForecast(currentSensors) {
|
|||||||
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 uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds);
|
|
||||||
|
|
||||||
const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time);
|
const celestial = getCelestialCurrent(coords.latitude, coords.longitude, time); // ← move up
|
||||||
const snapshot = {
|
const uv_index = estimateUV(celestial.sun_elevation ?? 0, clouds); // ← now safe
|
||||||
|
|
||||||
|
return {
|
||||||
time,
|
time,
|
||||||
temperature,
|
temperature,
|
||||||
humidity,
|
humidity,
|
||||||
@@ -140,12 +139,8 @@ export async function get24HourForecast(currentSensors) {
|
|||||||
clouds,
|
clouds,
|
||||||
precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100),
|
precipitation_chance: Math.round(weather.precipChance * (1 - i * 0.02) * 100),
|
||||||
uv_index,
|
uv_index,
|
||||||
uv_index_label: uvLabel(uv_index),
|
...celestial,
|
||||||
...celestial
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(snapshot, getWeatherCondition(snapshot));
|
|
||||||
return snapshot;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
@@ -216,7 +211,6 @@ function summarise(slots) {
|
|||||||
wind_speed: Math.round(Math.max(...winds) * 10) / 10,
|
wind_speed: Math.round(Math.max(...winds) * 10) / 10,
|
||||||
wind_gusts: gusts.length ? Math.round(Math.max(...gusts) * 10) / 10 : null,
|
wind_gusts: gusts.length ? Math.round(Math.max(...gusts) * 10) / 10 : null,
|
||||||
uv_index: uvs.length ? Math.max(...uvs) : 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,
|
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,
|
sunrise: slots.find(s => s.sunrise)?.sunrise ?? null,
|
||||||
sunset: slots.find(s => s.sunset)?.sunset ?? 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) }
|
lastForecast = { ts: Date.now(), slots, summary: summarise(todaySlots) }
|
||||||
return lastForecast
|
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}`};
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,22 +68,6 @@ function absoluteHumidity(tempC, humidity) {
|
|||||||
return Math.round((humidity / 100 * svp * 2165) / (tempC + 273.15) * 1000) / 1000
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Daily ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const DAILY_FIELDS = [
|
const DAILY_FIELDS = [
|
||||||
@@ -134,7 +118,6 @@ export async function dailyWeather(lat, lon, start, end) {
|
|||||||
wind_gusts: data.daily.windgusts_10m_max[i],
|
wind_gusts: data.daily.windgusts_10m_max[i],
|
||||||
wind_direction: data.daily.winddirection_10m_dominant[i],
|
wind_direction: data.daily.winddirection_10m_dominant[i],
|
||||||
uv_index: uv,
|
uv_index: uv,
|
||||||
uv_index_label: uvLabel(uv),
|
|
||||||
solar_wm2: data.daily.shortwave_radiation_sum[i],
|
solar_wm2: data.daily.shortwave_radiation_sum[i],
|
||||||
clouds: null,
|
clouds: null,
|
||||||
sunrise: data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : 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) => {
|
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] ?? {}
|
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 t = new Date(time + 'Z').getTime()
|
||||||
const daytime = sun.sunrise && sun.sunset
|
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()
|
? 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_gusts: data.hourly.windgusts_10m[i],
|
||||||
wind_direction: data.hourly.winddirection_10m[i],
|
wind_direction: data.hourly.winddirection_10m[i],
|
||||||
uv_index: uv,
|
uv_index: uv,
|
||||||
uv_index_label: uvLabel(uv),
|
|
||||||
solar_wm2: data.hourly.shortwave_radiation[i],
|
solar_wm2: data.hourly.shortwave_radiation[i],
|
||||||
clouds_label: cloudsLabel(clouds),
|
|
||||||
visibility: data.hourly.visibility[i] != null
|
visibility: data.hourly.visibility[i] != null
|
||||||
? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10
|
? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {existsSync, mkdirSync, writeFileSync} from 'fs';
|
import {existsSync, mkdirSync, writeFileSync} from 'fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
import {resolve, dirname} from 'path';
|
import {resolve, dirname} from 'path';
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import * as path from 'node:path';
|
|
||||||
|
|
||||||
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../data/icons');
|
const DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../data/icons');
|
||||||
|
|
||||||
@@ -18,28 +18,3 @@ export async function fetchIcon(icon) {
|
|||||||
await downloadIcon(icon, iconPath);
|
await downloadIcon(icon, iconPath);
|
||||||
return 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}`};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {spec} from './spec.mjs';
|
|||||||
import {existsSync} from 'fs';
|
import {existsSync} from 'fs';
|
||||||
import {getAIS} from './ais.mjs';
|
import {getAIS} from './ais.mjs';
|
||||||
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
|
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
|
||||||
import {fetchIcon, getWeatherCondition} from './openweather.mjs';
|
import {fetchIcon} from './openweather.mjs';
|
||||||
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
import {lastForecast, getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
|
||||||
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
||||||
|
|
||||||
// ── Uncaught error handlers ───────────────────────────────────────────────────
|
// ── Uncaught error handlers ───────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user