Backfill NWS's missing pressure & UV from Open-Meteo (with source tooltip)
NWS doesn't publish pressure (its pressure series is empty) or a UV index. When the selected source is NWS, merge current.pressure_msl, current.uv_index and daily.uv_index_max from Open-Meteo and tag them via data._fallback so the Current + Details views show a tooltip noting the value came from the non-selected source (Open-Meteo) instead of showing 0 hPa / --.
This commit is contained in:
parent
ebe81b6bfa
commit
c0ec45f170
@ -36,6 +36,8 @@
|
|||||||
|
|
||||||
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
|
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
|
||||||
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
|
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
|
||||||
|
const fbPressure = $derived(!!data?._fallback?.pressure)
|
||||||
|
const fbUv = $derived(!!data?._fallback?.uv)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if current}
|
{#if current}
|
||||||
@ -69,12 +71,12 @@
|
|||||||
<span class="metric-value">{Math.round(current.wind_speed_10m)} {windUnit}</span>
|
<span class="metric-value">{Math.round(current.wind_speed_10m)} {windUnit}</span>
|
||||||
<span class="metric-sub">{windCompass(current.wind_direction_10m)}</span>
|
<span class="metric-sub">{windCompass(current.wind_direction_10m)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card" title={fbPressure ? 'Pressure from Open-Meteo — NWS does not provide it' : undefined}>
|
||||||
<span class="metric-icon">🌡️</span>
|
<span class="metric-icon">🌡️</span>
|
||||||
<span class="metric-label">Pressure</span>
|
<span class="metric-label">Pressure</span>
|
||||||
<span class="metric-value">{Math.round(current.pressure_msl || 0)} hPa</span>
|
<span class="metric-value">{Math.round(current.pressure_msl || 0)} hPa</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card" title={fbUv ? 'UV index from Open-Meteo — NWS does not provide it' : undefined}>
|
||||||
<span class="metric-icon">☀️</span>
|
<span class="metric-icon">☀️</span>
|
||||||
<span class="metric-label">UV Index</span>
|
<span class="metric-label">UV Index</span>
|
||||||
<span class="metric-value">{current.uv_index != null ? Math.round(current.uv_index) : '--'}</span>
|
<span class="metric-value">{current.uv_index != null ? Math.round(current.uv_index) : '--'}</span>
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
|
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
|
||||||
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
|
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
|
||||||
|
const fbPressure = $derived(!!data?._fallback?.pressure)
|
||||||
|
const fbUv = $derived(!!data?._fallback?.uv)
|
||||||
|
|
||||||
function windCompass(deg) {
|
function windCompass(deg) {
|
||||||
if (deg == null) return '--'
|
if (deg == null) return '--'
|
||||||
@ -95,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span>Pressure</span>
|
<span>Pressure</span>
|
||||||
<span class="detail-value">{Math.round(current.pressure_msl || 0)} hPa</span>
|
<span class="detail-value" title={fbPressure ? 'Pressure from Open-Meteo — NWS does not provide it' : undefined}>{Math.round(current.pressure_msl || 0)} hPa</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span>Feels Like</span>
|
<span>Feels Like</span>
|
||||||
@ -117,7 +119,7 @@
|
|||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span>UV Index</span>
|
<span>UV Index</span>
|
||||||
<span class="detail-value">
|
<span class="detail-value" title={fbUv ? 'UV index from Open-Meteo — NWS does not provide it' : undefined}>
|
||||||
{current.uv_index != null ? Math.round(current.uv_index) : '--'}
|
{current.uv_index != null ? Math.round(current.uv_index) : '--'}
|
||||||
{#if current.uv_index != null}
|
{#if current.uv_index != null}
|
||||||
<span class="uv-badge uv-{getUVLevel(current.uv_index).toLowerCase().replace(' ', '-')}">{getUVLevel(current.uv_index)}</span>
|
<span class="uv-badge uv-{getUVLevel(current.uv_index).toLowerCase().replace(' ', '-')}">{getUVLevel(current.uv_index)}</span>
|
||||||
@ -134,7 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span>Max UV Today</span>
|
<span>Max UV Today</span>
|
||||||
<span class="detail-value">{daily?.uv_index_max?.[0] != null ? Math.round(daily.uv_index_max[0]) : '--'}</span>
|
<span class="detail-value" title={fbUv ? 'UV from Open-Meteo — NWS does not provide it' : undefined}>{daily?.uv_index_max?.[0] != null ? Math.round(daily.uv_index_max[0]) : '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -252,3 +252,29 @@ export async function fetchForecastNWS(lat, lon, units = 'imperial') {
|
|||||||
|
|
||||||
return normalizeNWS({ points, forecast, hourly, grid, timeZone: tz, units })
|
return normalizeNWS({ points, forecast, hourly, grid, timeZone: tz, units })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill fields that NWS does not provide (pressure, UV index) from an
|
||||||
|
* Open-Meteo response, tagging them so the UI can show a tooltip that the
|
||||||
|
* value came from the non-selected source.
|
||||||
|
*/
|
||||||
|
export function mergeFallbackData(nwsData, omData) {
|
||||||
|
const fallback = {}
|
||||||
|
const cur = nwsData.current || {}
|
||||||
|
const omCur = omData?.current || {}
|
||||||
|
if (omCur.pressure_msl != null) {
|
||||||
|
cur.pressure_msl = omCur.pressure_msl
|
||||||
|
fallback.pressure = true
|
||||||
|
}
|
||||||
|
if (omCur.uv_index != null) {
|
||||||
|
cur.uv_index = omCur.uv_index
|
||||||
|
fallback.uv = true
|
||||||
|
}
|
||||||
|
const omDaily = omData?.daily || {}
|
||||||
|
if (omDaily.uv_index_max?.length && nwsData.daily) {
|
||||||
|
nwsData.daily.uv_index_max = omDaily.uv_index_max
|
||||||
|
fallback.uv = true
|
||||||
|
}
|
||||||
|
if (Object.keys(fallback).length) nwsData._fallback = fallback
|
||||||
|
return nwsData
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { getLocations, loadSettings } from '../storage/db.js'
|
import { getLocations, loadSettings } from '../storage/db.js'
|
||||||
import { fetchForecast } from '../api/weather.js'
|
import { fetchForecast } from '../api/weather.js'
|
||||||
import { fetchForecastNWS } from '../api/nws.js'
|
import { fetchForecastNWS, mergeFallbackData } from '../api/nws.js'
|
||||||
|
|
||||||
export class AppStore {
|
export class AppStore {
|
||||||
// Location state
|
// Location state
|
||||||
@ -116,6 +116,14 @@ export class AppStore {
|
|||||||
if (source === 'nws') {
|
if (source === 'nws') {
|
||||||
try {
|
try {
|
||||||
data = await fetchForecastNWS(location.lat, location.lon, units)
|
data = await fetchForecastNWS(location.lat, location.lon, units)
|
||||||
|
// NWS omits pressure + UV index — backfill those from Open-Meteo and
|
||||||
|
// flag them so the UI can tooltip that they came from the other source.
|
||||||
|
try {
|
||||||
|
const om = await fetchForecast(location.lat, location.lon, units)
|
||||||
|
data = mergeFallbackData(data, om)
|
||||||
|
} catch (omErr) {
|
||||||
|
console.warn('Open-Meteo fields unavailable:', omErr.message)
|
||||||
|
}
|
||||||
} catch (nwsErr) {
|
} catch (nwsErr) {
|
||||||
// NWS is US-only and can be unavailable — fall back to Open-Meteo
|
// NWS is US-only and can be unavailable — fall back to Open-Meteo
|
||||||
// so the app keeps working rather than breaking.
|
// so the app keeps working rather than breaking.
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
dateKey,
|
dateKey,
|
||||||
localTimeString,
|
localTimeString,
|
||||||
normalizeNWS,
|
normalizeNWS,
|
||||||
|
mergeFallbackData,
|
||||||
} from '../src/lib/api/nws.js'
|
} from '../src/lib/api/nws.js'
|
||||||
|
|
||||||
const FIX = (name) => JSON.parse(readFileSync(path.join('tests/fixtures/nws', name), 'utf8'))
|
const FIX = (name) => JSON.parse(readFileSync(path.join('tests/fixtures/nws', name), 'utf8'))
|
||||||
@ -103,3 +104,26 @@ describe('normalizeNWS (recorded live fixture for Rogers County, OK)', () => {
|
|||||||
expect(m.current.wind_speed_10m).toBeCloseTo(10 * 1.609344, 1)
|
expect(m.current.wind_speed_10m).toBeCloseTo(10 * 1.609344, 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('mergeFallbackData', () => {
|
||||||
|
it('backfills pressure + UV from Open-Meteo and tags them (non-selected source)', () => {
|
||||||
|
const nws = {
|
||||||
|
_source: 'nws',
|
||||||
|
current: { temperature_2m: 103, pressure_msl: null, uv_index: null },
|
||||||
|
daily: { time: ['2026-08-16'], uv_index_max: [] },
|
||||||
|
}
|
||||||
|
const om = { current: { pressure_msl: 1011.2, uv_index: 7.4 }, daily: { uv_index_max: [7.2, 6, 5] } }
|
||||||
|
const r = mergeFallbackData(nws, om)
|
||||||
|
expect(r.current.pressure_msl).toBe(1011.2)
|
||||||
|
expect(r.current.uv_index).toBe(7.4)
|
||||||
|
expect(r.daily.uv_index_max).toEqual([7.2, 6, 5])
|
||||||
|
expect(r._fallback).toMatchObject({ pressure: true, uv: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not tag when Open-Meteo lacks the fields', () => {
|
||||||
|
const nws = { current: { pressure_msl: null, uv_index: null }, daily: {} }
|
||||||
|
const om = { current: { pressure_msl: null, uv_index: null }, daily: {} }
|
||||||
|
const r = mergeFallbackData(nws, om)
|
||||||
|
expect(r._fallback).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user