Local forecasting
This commit is contained in:
158
server/src/server.mjs
Normal file
158
server/src/server.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import express from 'express';
|
||||
import {resolve, dirname} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
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';
|
||||
import {getAirTraffic, getAirTrafficHistory, getShapes} from './airtraffic.mjs';
|
||||
import {getWeatherCondition} from './openweather.mjs';
|
||||
|
||||
const app = express();
|
||||
const DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const CLIENT_DIST = resolve(DIR, 'public');
|
||||
|
||||
app.use(express.json());
|
||||
app.use('/icons', express.static(resolve(DIR, 'public', 'icons')));
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
next();
|
||||
});
|
||||
app.use(express.static(CLIENT_DIST));
|
||||
|
||||
function filterFields(obj, fields) {
|
||||
if(!fields) return obj;
|
||||
const keys = new Set(fields.split(',').map(f => f.trim()));
|
||||
return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.has(k)));
|
||||
}
|
||||
|
||||
function filterArr(arr, fields) {
|
||||
if(!fields) return arr;
|
||||
return arr.map(row => filterFields(row, fields));
|
||||
}
|
||||
|
||||
// ── Sensor Data ───────────────────────────────────────────────────────────────
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
// ── Position ──────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/position', async (req, res) => {
|
||||
const {fields} = req.query;
|
||||
const data = await getCoords();
|
||||
res.json(filterFields(data, fields));
|
||||
});
|
||||
|
||||
// ── Space ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/space', async (req, res) => {
|
||||
const {fields} = req.query;
|
||||
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()));
|
||||
app.get('/api/air-traffic/:icao', async (req, res) => res.json(await getAirTrafficHistory(req.params.icao)));
|
||||
app.get('/api/air-traffic-shapes', async (req, res) => res.json(await getShapes()));
|
||||
|
||||
// ── DOCS ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.use('/docs', apiReference({spec: {url: '/openapi.json'}, theme: 'default'}));
|
||||
app.get('/openapi.json', (req, res) => res.json(spec));
|
||||
app.get('/openapi.yaml', async (req, res) => {
|
||||
const {stringify} = await import('yaml');
|
||||
res.setHeader('Content-Type', 'text/yaml');
|
||||
res.send(stringify(spec));
|
||||
});
|
||||
|
||||
// ── Website ───────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
const index = resolve(CLIENT_DIST, 'index.html');
|
||||
if(existsSync(index)) res.sendFile(index);
|
||||
else res.status(404).send('Client not built yet — run npm run build in /client');
|
||||
});
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const c = cfg();
|
||||
app.listen(c.PORT, () => console.log(`🌦 Weather API — http://localhost:${c.PORT}`));
|
||||
Reference in New Issue
Block a user