Daily forecast fix?
This commit is contained in:
@@ -10,7 +10,6 @@ export interface MetricMeta {
|
||||
export const METRICS: Record<string, MetricMeta> = {
|
||||
// Environment
|
||||
temperature: { label: 'Temperature', unit: '°C', icon: '🌡️', group: 'Environment', precision: 1, color: '#f97316' },
|
||||
env_temp_f: { label: 'Temperature', unit: '°F', icon: '🌡️', group: 'Environment', precision: 1, color: '#f97316' },
|
||||
humidity: { label: 'Humidity', unit: '%', icon: '💧', group: 'Environment', precision: 1, color: '#38bdf8' },
|
||||
dew_point: { label: 'Dew Point', unit: '°C', icon: '🌫️', group: 'Environment', precision: 1, color: '#818cf8' },
|
||||
heat_index: { label: 'Feels Like', unit: '°C', icon: '🤔', group: 'Environment', precision: 1, color: '#fb923c' },
|
||||
@@ -27,28 +26,28 @@ export const METRICS: Record<string, MetricMeta> = {
|
||||
uv_dose: { label: 'UV Dose Today', unit: ' mJ/cm²', icon: '📡', group: 'Light', precision: 2, color: '#f9a8d4' },
|
||||
light_burn_time_min: { label: 'Burn Time', unit: ' min', icon: '🔥', group: 'Light', precision: 0, color: '#fb7185' },
|
||||
clouds: { label: 'Cloud Cover', unit: '%', icon: '☁️', group: 'Light', precision: 0, color: '#94a3b8' },
|
||||
light_dli: { label: 'Daily Light Integral', unit: ' mol/m²', icon: '🌱', group: 'Light', precision: 3, color: '#86efac' },
|
||||
day_light_integral: { label: 'Daily Light Integral', unit: ' mol/m²', icon: '🌱', group: 'Light', precision: 3, color: '#86efac' },
|
||||
visibility: { label: 'Visibility', unit: ' km', icon: '👁️', group: 'Light', precision: 1, color: '#7dd3fc' },
|
||||
// Wind
|
||||
wind_speed_kmh: { label: 'Wind Speed', unit: ' km/h', icon: '💨', group: 'Wind', precision: 1, color: '#93c5fd' },
|
||||
wind_gusts_kmh: { label: 'Wind Gusts', unit: ' km/h', icon: '🌬️', group: 'Wind', precision: 1, color: '#60a5fa' },
|
||||
wind_direction_deg: { label: 'Wind Direction', unit: '°', icon: '🧭', group: 'Wind', precision: 0, color: '#818cf8' },
|
||||
wind_speed: { label: 'Wind Speed', unit: ' km/h', icon: '💨', group: 'Wind', precision: 1, color: '#93c5fd' },
|
||||
wind_gusts: { label: 'Wind Gusts', unit: ' km/h', icon: '🌬️', group: 'Wind', precision: 1, color: '#60a5fa' },
|
||||
wind_direction: { label: 'Wind Direction', unit: '°', icon: '🧭', group: 'Wind', precision: 0, color: '#818cf8' },
|
||||
// Seismic
|
||||
seismic_magnitude: { label: 'Seismic Magnitude', unit: ' g', icon: '🌍', group: 'Seismic', precision: 4, color: '#f87171' },
|
||||
seismic_ax: { label: 'Accel X', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
seismic_ay: { label: 'Accel Y', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
seismic_az: { label: 'Accel Z', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
magnitude: { label: 'Seismic Magnitude', unit: ' g', icon: '🌍', group: 'Seismic', precision: 4, color: '#f87171' },
|
||||
seismic_x: { label: 'Accel X', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
seismic_y: { label: 'Accel Y', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
seismic_z: { label: 'Accel Z', unit: ' g', icon: '📐', group: 'Seismic', precision: 3, color: '#fca5a5' },
|
||||
// Compass
|
||||
compass_heading: { label: 'Compass Heading', unit: '°', icon: '🧭', group: 'Compass', precision: 1, color: '#c084fc' },
|
||||
heading: { label: 'Compass Heading', unit: '°', icon: '🧭', group: 'Compass', precision: 1, color: '#c084fc' },
|
||||
// Ground
|
||||
ground_distance: { label: 'Ground Distance', unit: ' cm', icon: '📏', group: 'Ground', precision: 0, color: '#a3e635' },
|
||||
accumulation: { label: 'Accumulation Depth', unit: ' cm', icon: '❄️', group: 'Ground', precision: 1, color: '#bfdbfe' },
|
||||
lidar_strength: { label: 'LIDAR Strength', unit: '', icon: '📡', group: 'Ground', precision: 0, color: '#6ee7b7' },
|
||||
// Lightning
|
||||
lightning_distance: { label: 'Lightning Distance', unit: ' km', icon: '⚡', group: 'Lightning', precision: 0, color: '#fde047' },
|
||||
lightning_strikes_per_hour: { label: 'Strike Rate', unit: '/hr', icon: '⚡', group: 'Lightning', precision: 1, color: '#facc15' },
|
||||
lightning_rate: { label: 'Strike Rate', unit: '/hr', icon: '⚡', group: 'Lightning', precision: 1, color: '#facc15' },
|
||||
// Moon
|
||||
moon_illumination_pct: { label: 'Moon Illumination', unit: '%', icon: '🌙', group: 'Celestial', precision: 0, color: '#e2e8f0' },
|
||||
moon_illumination: { label: 'Moon Illumination', unit: '%', icon: '🌙', group: 'Celestial', precision: 0, color: '#e2e8f0' },
|
||||
// Sun
|
||||
sun_elevation: { label: 'Solar Elevation', unit: '°', icon: '☀️', group: 'Celestial', precision: 1, color: '#fef08a' },
|
||||
// Space
|
||||
@@ -58,11 +57,6 @@ export const METRICS: Record<string, MetricMeta> = {
|
||||
// Marine
|
||||
wave_height: { label: 'Wave Height', unit: ' m', icon: '🌊', group: 'Marine', precision: 2, color: '#38bdf8' },
|
||||
swell_wave_height: { label: 'Swell Height', unit: ' m', icon: '🌊', group: 'Marine', precision: 2, color: '#7dd3fc' },
|
||||
wave_period: { label: 'Wave Period', unit: ' s', icon: '⏱️', group: 'Marine', precision: 1, color: '#bae6fd' },
|
||||
// AQ
|
||||
pm2_5: { label: 'PM2.5', unit: ' μg/m³', icon: '🏭', group: 'Air Quality', precision: 1, color: '#fca5a5' },
|
||||
pm10: { label: 'PM10', unit: ' μg/m³', icon: '🏭', group: 'Air Quality', precision: 1, color: '#fdba74' },
|
||||
ozone: { label: 'Ozone', unit: ' μg/m³', icon: '🌐', group: 'Air Quality', precision: 1, color: '#6ee7b7' },
|
||||
// Forecast
|
||||
forecast_precipitation_mm: { label: 'Precipitation', unit: ' mm', icon: '🌧️', group: 'Forecast', precision: 1, color: '#38bdf8' },
|
||||
forecast_precipitation_probability: { label: 'Rain Chance', unit: '%', icon: '🌂', group: 'Forecast', precision: 0, color: '#60a5fa' },
|
||||
|
||||
@@ -89,21 +89,21 @@ export async function queryDaily(start, end) {
|
||||
const [means, mins, maxs] = await Promise.all([
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: mean, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: min, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start}, stop: ${end})
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> aggregateWindow(every: 1d, fn: max, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|
||||
@@ -10,81 +10,55 @@ const CACHE_MS = 15 * 60 * 1000
|
||||
|
||||
if (!existsSync(ICON_DIR)) mkdirSync(ICON_DIR, { recursive: true })
|
||||
|
||||
// WMO weather code map
|
||||
const WMO = {
|
||||
0: { label: 'Clear Sky' },
|
||||
1: { label: 'Mainly Clear' },
|
||||
2: { label: 'Partly Cloudy' },
|
||||
3: { label: 'Overcast' },
|
||||
45: { label: 'Fog' },
|
||||
48: { label: 'Icy Fog' },
|
||||
51: { label: 'Light Drizzle' },
|
||||
53: { label: 'Drizzle' },
|
||||
55: { label: 'Heavy Drizzle' },
|
||||
61: { label: 'Light Rain' },
|
||||
63: { label: 'Rain' },
|
||||
65: { label: 'Heavy Rain' },
|
||||
71: { label: 'Light Snow' },
|
||||
73: { label: 'Snow' },
|
||||
75: { label: 'Heavy Snow' },
|
||||
77: { label: 'Snow Grains' },
|
||||
80: { label: 'Light Showers' },
|
||||
81: { label: 'Showers' },
|
||||
82: { label: 'Heavy Showers' },
|
||||
85: { label: 'Snow Showers' },
|
||||
86: { label: 'Heavy Snow Showers' },
|
||||
95: { label: 'Thunderstorm' },
|
||||
96: { label: 'Thunderstorm w/ Hail' },
|
||||
99: { label: 'Thunderstorm w/ Heavy Hail' },
|
||||
0: 'Clear Sky',
|
||||
1: 'Mainly Clear',
|
||||
2: 'Partly Cloudy',
|
||||
3: 'Overcast',
|
||||
45: 'Fog',
|
||||
48: 'Icy Fog',
|
||||
51: 'Light Drizzle',
|
||||
53: 'Drizzle',
|
||||
55: 'Heavy Drizzle',
|
||||
61: 'Light Rain',
|
||||
63: 'Rain',
|
||||
65: 'Heavy Rain',
|
||||
71: 'Light Snow',
|
||||
73: 'Snow',
|
||||
75: 'Heavy Snow',
|
||||
77: 'Snow Grains',
|
||||
80: 'Light Showers',
|
||||
81: 'Showers',
|
||||
82: 'Heavy Showers',
|
||||
85: 'Snow Showers',
|
||||
86: 'Heavy Snow Showers',
|
||||
95: 'Thunderstorm',
|
||||
96: 'Thunderstorm w/ Hail',
|
||||
99: 'Thunderstorm w/ Heavy Hail',
|
||||
}
|
||||
|
||||
// OWM icon mapping by WMO code + is_day
|
||||
const OWM_ICONS = {
|
||||
0: { day: '01d', night: '01n' },
|
||||
1: { day: '01d', night: '01n' },
|
||||
2: { day: '02d', night: '02n' },
|
||||
3: { day: '04d', night: '04n' },
|
||||
45: { day: '50d', night: '50n' },
|
||||
48: { day: '50d', night: '50n' },
|
||||
51: { day: '09d', night: '09n' },
|
||||
53: { day: '09d', night: '09n' },
|
||||
55: { day: '09d', night: '09n' },
|
||||
61: { day: '10d', night: '10n' },
|
||||
63: { day: '10d', night: '10n' },
|
||||
65: { day: '10d', night: '10n' },
|
||||
71: { day: '13d', night: '13n' },
|
||||
73: { day: '13d', night: '13n' },
|
||||
75: { day: '13d', night: '13n' },
|
||||
77: { day: '13d', night: '13n' },
|
||||
80: { day: '09d', night: '09n' },
|
||||
81: { day: '09d', night: '09n' },
|
||||
82: { day: '09d', night: '09n' },
|
||||
85: { day: '13d', night: '13n' },
|
||||
86: { day: '13d', night: '13n' },
|
||||
95: { day: '11d', night: '11n' },
|
||||
96: { day: '11d', night: '11n' },
|
||||
99: { day: '11d', night: '11n' },
|
||||
0: '01d', 1: '01d', 2: '02d', 3: '04d',
|
||||
45: '50d', 48: '50d',
|
||||
51: '09d', 53: '09d', 55: '09d',
|
||||
61: '10d', 63: '10d', 65: '10d',
|
||||
71: '13d', 73: '13d', 75: '13d', 77: '13d',
|
||||
80: '09d', 81: '09d', 82: '09d',
|
||||
85: '13d', 86: '13d',
|
||||
95: '11d', 96: '11d', 99: '11d',
|
||||
}
|
||||
|
||||
async function ensureIcon(code, isDay) {
|
||||
const variant = isDay ? 'day' : 'night'
|
||||
const owmCode = OWM_ICONS[code]?.[variant] || '01d'
|
||||
async function ensureIcon(code) {
|
||||
const owmCode = OWM_ICONS[code] || '01d'
|
||||
const filename = `${owmCode}.png`
|
||||
const filepath = resolve(ICON_DIR, filename)
|
||||
if (!existsSync(filepath)) {
|
||||
const url = `https://openweathermap.org/img/wn/${owmCode}@2x.png`
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(`https://openweathermap.org/img/wn/${owmCode}@2x.png`)
|
||||
await pipeline(res.body, createWriteStream(filepath))
|
||||
}
|
||||
return `/icons/${filename}`
|
||||
}
|
||||
|
||||
export async function resolveWeather(code, isDay = 1) {
|
||||
const label = WMO[code]?.label || 'Unknown'
|
||||
const icon = await ensureIcon(code, isDay).catch(() => null)
|
||||
return { forecast_weather_label: label, forecast_weather_icon: icon, forecast_weathercode: code }
|
||||
}
|
||||
|
||||
async function cachedFetch(key, url) {
|
||||
const now = Date.now()
|
||||
if (CACHE[key] && now - CACHE[key].ts < CACHE_MS) return CACHE[key].data
|
||||
@@ -94,178 +68,46 @@ async function cachedFetch(key, url) {
|
||||
return data
|
||||
}
|
||||
|
||||
const HOURLY_WEATHER = [
|
||||
'temperature_2m', 'relative_humidity_2m', 'precipitation', 'precipitation_probability',
|
||||
'weathercode', 'windspeed_10m', 'winddirection_10m', 'windgusts_10m',
|
||||
'pressure_msl', 'cloudcover', 'visibility', 'cape', 'uv_index', 'is_day',
|
||||
'et0_fao_evapotranspiration', 'dewpoint_2m', 'apparent_temperature',
|
||||
'snowfall', 'snow_depth', 'freezinglevel_height',
|
||||
const DAILY_FIELDS = [
|
||||
'temperature_2m_max', 'temperature_2m_min',
|
||||
'precipitation_sum', 'precipitation_probability_max',
|
||||
'windspeed_10m_max', 'windgusts_10m_max', 'winddirection_10m_dominant',
|
||||
'weathercode', 'sunrise', 'sunset',
|
||||
'uv_index_max', 'shortwave_radiation_sum',
|
||||
].join(',')
|
||||
|
||||
const DAILY_WEATHER = [
|
||||
'temperature_2m_max', 'temperature_2m_min', 'precipitation_sum',
|
||||
'precipitation_probability_max', 'windspeed_10m_max', 'windgusts_10m_max',
|
||||
'winddirection_10m_dominant', 'weathercode', 'sunrise', 'sunset',
|
||||
'uv_index_max', 'snowfall_sum', 'precipitation_hours', 'shortwave_radiation_sum',
|
||||
].join(',')
|
||||
|
||||
const HOURLY_AQ = [
|
||||
'pm10', 'pm2_5', 'carbon_monoxide', 'nitrogen_dioxide', 'sulphur_dioxide',
|
||||
'ozone', 'aerosol_optical_depth', 'dust', 'uv_index',
|
||||
].join(',')
|
||||
|
||||
const HOURLY_MARINE = [
|
||||
'wave_height', 'wave_direction', 'wave_period',
|
||||
'wind_wave_height', 'swell_wave_height', 'swell_wave_direction', 'swell_wave_period',
|
||||
].join(',')
|
||||
|
||||
function zipHourly(data, prefix = '') {
|
||||
if (!data?.hourly) return []
|
||||
return data.hourly.time.map((time, i) => {
|
||||
const row = { time }
|
||||
for (const [k, v] of Object.entries(data.hourly)) {
|
||||
if (k !== 'time') row[prefix + k] = v[i]
|
||||
}
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
function zipDaily(data, prefix = '') {
|
||||
if (!data?.daily) return []
|
||||
return data.daily.time.map((time, i) => {
|
||||
const row = { time }
|
||||
for (const [k, v] of Object.entries(data.daily)) {
|
||||
if (k !== 'time') row[prefix + k] = v[i]
|
||||
}
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
// Remap Open-Meteo field names to our prefixed convention
|
||||
function remapWeatherFields(row) {
|
||||
const map = {
|
||||
temperature_2m: 'temperature',
|
||||
apparent_temperature: 'heat_index',
|
||||
dewpoint_2m: 'dew_point',
|
||||
relative_humidity_2m: 'humidity',
|
||||
pressure_msl: 'pressure_slp',
|
||||
cloudcover: 'clouds',
|
||||
uv_index: 'uv_index',
|
||||
visibility: 'visibility',
|
||||
windspeed_10m: 'wind_speed_kmh',
|
||||
winddirection_10m: 'wind_direction_deg',
|
||||
windgusts_10m: 'wind_gusts_kmh',
|
||||
weathercode: 'forecast_weathercode',
|
||||
precipitation: 'forecast_precipitation_mm',
|
||||
precipitation_probability: 'forecast_precipitation_probability',
|
||||
snowfall: 'forecast_snowfall_mm',
|
||||
snow_depth: 'forecast_snow_depth_m',
|
||||
cape: 'forecast_cape',
|
||||
is_day: 'sun_is_day',
|
||||
freezinglevel_height: 'forecast_freezing_level_m',
|
||||
et0_fao_evapotranspiration: 'forecast_evapotranspiration_mm',
|
||||
// daily
|
||||
temperature_2m_max: 'env_temp_max_c',
|
||||
temperature_2m_min: 'env_temp_min_c',
|
||||
precipitation_sum: 'forecast_precipitation_mm',
|
||||
precipitation_probability_max: 'forecast_precipitation_probability',
|
||||
windspeed_10m_max: 'wind_speed_max_kmh',
|
||||
windgusts_10m_max: 'wind_gusts_max_kmh',
|
||||
winddirection_10m_dominant: 'wind_direction_deg',
|
||||
uv_index_max: 'uv_index_max',
|
||||
snowfall_sum: 'forecast_snowfall_mm',
|
||||
shortwave_radiation_sum: 'solar_wm2_sum',
|
||||
sunrise: 'sun_sunrise',
|
||||
sunset: 'sun_sunset',
|
||||
}
|
||||
const out = {}
|
||||
for (const [k, v] of Object.entries(row)) {
|
||||
out[map[k] || k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// start & end are Date objects
|
||||
export async function getOpenMeteo(lat, lon, start, end) {
|
||||
const base = start && end
|
||||
? `&start_date=${start.slice(0,10)}&end_date=${end.slice(0,10)}`
|
||||
: `&forecast_days=7`
|
||||
const startStr = start.toISOString().slice(0, 10)
|
||||
const endStr = end.toISOString().slice(0, 10)
|
||||
|
||||
const [weather, aq, marine] = await Promise.allSettled([
|
||||
cachedFetch(`weather_${lat}_${lon}_${start}_${end}`,
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&hourly=${HOURLY_WEATHER}&daily=${DAILY_WEATHER}¤t_weather=true&timezone=auto${base}`),
|
||||
cachedFetch(`aq_${lat}_${lon}_${start}_${end}`,
|
||||
`https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}&hourly=${HOURLY_AQ}&timezone=auto${base}`),
|
||||
cachedFetch(`marine_${lat}_${lon}_${start}_${end}`,
|
||||
`https://marine-api.open-meteo.com/v1/marine?latitude=${lat}&longitude=${lon}&hourly=${HOURLY_MARINE}&daily=wave_height_max,wave_direction_dominant,wave_period_max&timezone=auto${base}`),
|
||||
])
|
||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` +
|
||||
`&daily=${DAILY_FIELDS}&timezone=auto&start_date=${startStr}&end_date=${endStr}`
|
||||
|
||||
const w = weather.status === 'fulfilled' ? weather.value : {}
|
||||
const a = aq.status === 'fulfilled' ? aq.value : {}
|
||||
const m = marine.status === 'fulfilled' ? marine.value : {}
|
||||
const data = await cachedFetch(`daily_${lat}_${lon}_${startStr}_${endStr}`, url)
|
||||
|
||||
// Build hourly merged map
|
||||
const hourlyMap = {}
|
||||
for (const row of zipHourly(w)) {
|
||||
const remapped = remapWeatherFields(row)
|
||||
hourlyMap[row.time] = remapped
|
||||
}
|
||||
for (const row of zipHourly(a)) {
|
||||
const { time, ...rest } = row
|
||||
hourlyMap[time] = { ...hourlyMap[time], ...rest }
|
||||
}
|
||||
for (const row of zipHourly(m)) {
|
||||
const { time, ...rest } = row
|
||||
hourlyMap[time] = { ...hourlyMap[time], ...rest }
|
||||
}
|
||||
if (!data?.daily?.time) return []
|
||||
|
||||
// Build daily merged map
|
||||
const dailyMap = {}
|
||||
for (const row of zipDaily(w)) {
|
||||
const remapped = remapWeatherFields(row)
|
||||
dailyMap[row.time] = remapped
|
||||
}
|
||||
for (const row of zipDaily(m)) {
|
||||
const { time, ...rest } = row
|
||||
dailyMap[time] = { ...dailyMap[time], ...rest }
|
||||
}
|
||||
|
||||
// Resolve current weather
|
||||
let current = null
|
||||
if (w.current_weather) {
|
||||
const cw = w.current_weather
|
||||
const wInfo = await resolveWeather(cw.weathercode, cw.is_day).catch(() => ({}))
|
||||
current = {
|
||||
wind_speed_kmh: cw.windspeed,
|
||||
wind_direction_deg: cw.winddirection,
|
||||
sun_is_day: cw.is_day,
|
||||
...wInfo,
|
||||
return Promise.all(data.daily.time.map(async (time, i) => {
|
||||
const code = data.daily.weathercode[i]
|
||||
const icon = await ensureIcon(code).catch(() => null)
|
||||
return {
|
||||
time,
|
||||
label: WMO[code] || 'Unknown',
|
||||
icon,
|
||||
forecast_weathercode: code,
|
||||
temperature: data.daily.temperature_2m_max[i],
|
||||
env_temp_max_c: data.daily.temperature_2m_max[i],
|
||||
env_temp_min_c: data.daily.temperature_2m_min[i],
|
||||
precipitation: data.daily.precipitation_sum[i],
|
||||
forecast_precipitation_probability: data.daily.precipitation_probability_max[i],
|
||||
wind_speed: data.daily.windspeed_10m_max[i],
|
||||
wind_gusts: data.daily.windgusts_10m_max[i],
|
||||
wind_direction: data.daily.winddirection_10m_dominant[i],
|
||||
uv_index: data.daily.uv_index_max[i],
|
||||
solar_wm2: data.daily.shortwave_radiation_sum[i],
|
||||
sunrise: data.daily.sunrise[i],
|
||||
sunset: data.daily.sunset[i],
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve weather labels + icons for all hourly/daily rows
|
||||
const hourlyArr = await Promise.all(
|
||||
Object.values(hourlyMap)
|
||||
.sort((a, b) => a.time.localeCompare(b.time))
|
||||
.map(async row => {
|
||||
if (row.forecast_weathercode != null) {
|
||||
const wInfo = await resolveWeather(row.forecast_weathercode, row.sun_is_day ?? 1).catch(() => ({}))
|
||||
return { ...row, ...wInfo }
|
||||
}
|
||||
return row
|
||||
})
|
||||
)
|
||||
|
||||
const dailyArr = await Promise.all(
|
||||
Object.values(dailyMap)
|
||||
.sort((a, b) => a.time.localeCompare(b.time))
|
||||
.map(async row => {
|
||||
if (row.forecast_weathercode != null) {
|
||||
const wInfo = await resolveWeather(row.forecast_weathercode, 1).catch(() => ({}))
|
||||
return { ...row, ...wInfo }
|
||||
}
|
||||
return row
|
||||
})
|
||||
)
|
||||
|
||||
return { current, hourly: hourlyArr, daily: dailyArr }
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -83,23 +83,26 @@ app.get('/api/hourly', async (req, res) => {
|
||||
// ── Hourly History/Forecast ───────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/daily', async (req, res) => {
|
||||
const {fields} = req.query;
|
||||
const start = new Date(req.query.start) || new Date(new Date().setHours(0, 0, 0, 0));
|
||||
let now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
let next = new Date(now);
|
||||
next.setDate(now.getDate() + 1);
|
||||
const end = new Date(req.query.end) || new Date();
|
||||
const coords = await getCoords();
|
||||
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([
|
||||
now > start ? queryDaily(start.toISOString(), now.toISOString()) : Promise.resolve([]),
|
||||
getOpenMeteo(coords.latitude, coords.longitude, (start.getTime() > next.getTime() ? start : next).toISOString(), end.toISOString()),
|
||||
]);
|
||||
const history = sensor.status === 'fulfilled' ? sensor.value : [];
|
||||
const forecast = meteo.status === 'fulfilled' ? meteo.value.daily : [];
|
||||
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast]);
|
||||
res.json(filterArr(daily, fields));
|
||||
});
|
||||
start < now ? queryDaily(start.toISOString(), now.toISOString()) : Promise.resolve([]),
|
||||
end > now ? getOpenMeteo(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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user