24h forecasting

This commit is contained in:
2026-06-24 13:13:39 -04:00
parent d540ab5efb
commit 89f3f156ce
2 changed files with 158 additions and 77 deletions

View File

@@ -5,7 +5,6 @@ import {cfg} from './config.mjs';
import {queryCurrent, queryHourly, queryDaily, getCoords} from './influx.mjs';
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
import {getSpaceWeather} from './space.mjs';
import {dailyForecast} from './openmeteo.mjs';
import {apiReference} from '@scalar/express-api-reference';
import {spec} from './spec.mjs';
import {existsSync} from 'fs';
@@ -16,6 +15,9 @@ const app = express();
const DIR = dirname(fileURLToPath(import.meta.url));
const CLIENT_DIST = resolve(DIR, 'public');
refreshForecast()
setInterval(refreshForecast, 10 * 60 * 1000)
app.use(express.json());
app.use('/icons', express.static(resolve(DIR, 'public', 'icons')));
app.use((req, res, next) => {
@@ -35,15 +37,110 @@ function filterArr(arr, fields) {
return arr.map(row => filterFields(row, fields));
}
// ── Sensor Data ───────────────────────────────────────────────────────────────
// ── Current ───────────────────────────────────────────────────────────────────
app.get('/api/current', async (req, res) => {
const {fields} = req.query;
const [sensors, coords] = await Promise.all([queryCurrent(), getCoords()]);
const condition = getWeatherCondition(sensors);
const space = getCelestialCurrent(coords.latitude, coords.longitude);
res.json(filterFields({...condition, ...sensors, ...space}, fields));
});
const { fields } = req.query
const [sensors, coords] = await Promise.all([queryCurrent(), getCoords()])
const condition = getWeatherCondition(sensors)
const space = getCelestialCurrent(coords.latitude, coords.longitude)
const { summary } = await ensureForecast()
// Merge forecast summary fields — sensor readings take priority
const merged = {
...(summary ? {
precipitation_chance: summary.precip_chance,
temperature_max: summary.temperature_max,
temperature_min: summary.temperature_min,
uv_index_max: summary.uv_index,
} : {}),
...condition,
...sensors,
...space,
}
res.json(filterFields(merged, fields))
})
// ── Hourly ────────────────────────────────────────────────────────────────────
app.get('/api/hourly', async (req, res) => {
const { fields } = req.query
const now = new Date()
const plus24 = new Date(now.getTime() + 24 * 3600 * 1000)
const start = req.query.start ? new Date(req.query.start) : new Date(now.setHours(0, 0, 0, 0))
const end = req.query.end ? new Date(req.query.end) : plus24
const coords = await getCoords()
const { slots } = await ensureForecast()
// Past → now: InfluxDB
const historyEnd = new Date(Math.min(now, end))
historyEnd.setMinutes(0, 0, 0)
const [historyResult] = await Promise.allSettled([
start < now ? queryHourly(start.toISOString(), historyEnd.toISOString()) : Promise.resolve([])
])
const history = historyResult.status === 'fulfilled' ? historyResult.value : []
// Now → +24h: cached physics forecast
const physics = end > now
? slots.filter(s => { const t = new Date(s.time); return t >= now && t <= new Date(Math.min(end, plus24)) })
: []
// +24h → beyond: Open-Meteo
let meteo = []
if (end > plus24) {
const [meteoResult] = await Promise.allSettled([getOpenMeteoHourly(coords.latitude, coords.longitude, plus24, end)])
meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
}
const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...physics, ...meteo])
res.json(filterArr(hourly, fields))
})
// ── Daily ─────────────────────────────────────────────────────────────────────
app.get('/api/daily', async (req, res) => {
const { fields } = req.query
const now = new Date()
now.setHours(0, 0, 0, 0)
const next = new Date(now); next.setDate(now.getDate() + 1)
const start = req.query.start ? new Date(req.query.start) : now
const end = req.query.end ? new Date(req.query.end) : next
const coords = await getCoords()
// Does the requested window overlap with today (the 24h forecast window)?
const todayStr = now.toISOString().slice(0, 10)
const { summary } = await ensureForecast()
const usePhysics = summary && start <= next && end >= now
// Past influx history (days before today)
const [sensorResult] = await Promise.allSettled([
start < now ? queryDaily(start.toISOString(), now.toISOString()) : Promise.resolve([])
])
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
// Future meteo (days after today)
const meteoStart = end > next ? next : null
const [meteoResult] = await Promise.allSettled([
meteoStart && end > next ? getOpenMeteo(coords.latitude, coords.longitude, next, end) : Promise.resolve([])
])
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
// Splice in physics summary for today, skip any meteo row that duplicates it
const todaySlot = usePhysics ? [summary] : []
const meteoClean = meteo.filter(r => r.time !== todayStr)
const daily = getCelestialForecast(
coords.latitude, coords.longitude,
[...history, ...todaySlot, ...meteoClean]
)
res.json(filterArr(daily, fields))
})
// ── Position ──────────────────────────────────────────────────────────────────
@@ -60,74 +157,6 @@ app.get('/api/space', async (req, res) => {
res.json(filterFields(await getSpaceWeather(), fields));
});
// ── Daily History/Forecast ────────────────────────────────────────────────────
app.get('/api/hourly', async (req, res) => {
const { fields } = req.query
const now = new Date()
const plus24 = new Date(now.getTime() + 24 * 3600 * 1000)
const start = req.query.start ? new Date(req.query.start) : new Date(now.setHours(0,0,0,0))
const end = req.query.end ? new Date(req.query.end) : plus24
const coords = await getCoords()
const sensors = await queryCurrent()
// ── Past → now: InfluxDB ──────────────────────────────────────────────────
const historyEnd = new Date(Math.min(now, end))
historyEnd.setMinutes(0, 0, 0)
const [historyResult] = await Promise.allSettled([
start < now ? queryHourly(start.toISOString(), historyEnd.toISOString()) : Promise.resolve([])
])
const history = historyResult.status === 'fulfilled' ? historyResult.value : []
// ── 24h Local Forecast ────────────────────────────────────────────────────
let physics = []
if (end > now) {
const forecastSlots = await get24HourForecast(sensors)
physics = forecastSlots.filter(s => {
const t = new Date(s.time)
return t >= now && t <= new Date(Math.min(end, plus24))
})
}
// ── +24h → Weather Service ────────────────────────────────────────────────
let meteo = []
if (end > plus24) {
const [meteoResult] = await Promise.allSettled([
getOpenMeteoHourly(coords.latitude, coords.longitude, plus24, end)
])
meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
}
const hourly = getCelestialForecast(
coords.latitude,
coords.longitude,
[...history, ...physics, ...meteo]
)
res.json(filterArr(hourly, fields))
})
// ── Hourly History/Forecast ───────────────────────────────────────────────────
app.get('/api/daily', async (req, res) => {
const { fields } = req.query
const now = new Date()
now.setHours(0, 0, 0, 0)
const next = new Date(now)
next.setDate(now.getDate() + 1)
const start = req.query.start ? new Date(req.query.start) : now
const end = req.query.end ? new Date(req.query.end) : next
const meteoStart = start >= next ? start : next
const coords = await getCoords()
const [sensor, meteo] = await Promise.allSettled([
start < now ? queryDaily(start.toISOString(), now.toISOString()) : Promise.resolve([]),
end > now ? dailyForecast(coords.latitude, coords.longitude, meteoStart, end) : Promise.resolve([]),
])
const history = sensor.status === 'fulfilled' ? sensor.value : []
const forecast = meteo.status === 'fulfilled' ? meteo.value : []
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast])
res.json(filterArr(daily, fields))
})
// ── ADSB Proxy ────────────────────────────────────────────────────────────────
app.get('/api/air-traffic', async (req, res) => res.json(await getAirTraffic()));