The weather widget talks to five different APIs. Each one returns data in its own format, so before anything reaches the display layer, every response gets normalized to a single internal schema.
The normalized shape every parser targets looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| {
current: {
temp: 20.5, // always Celsius
feels_like: 18.2, // always Celsius
humidity: 65, // 0-100
pressure: 1013.25, // hPa
wind_speed: 5.5, // always m/s
wind_deg: 180, // 0-360
weather: [{ description: "Partly cloudy", icon: "02d" }]
},
daily: [
{
dt: 1234567890, // Unix timestamp
temp: { max: 25.0, min: 15.0 },
weather: [{ icon: "01d" }],
pop: 0.3, // precipitation probability, 0-1
day_detail: { weather: [...], pop: 0.3 },
night_detail: { weather: [...], pop: 0.1 }
}
// 6 more days
]
}
|
Temperature always Celsius. Wind always m/s. Icon codes, not icon URLs. Unit conversion happens at render time in the QML layer. The moment you let user preferences leak into parsing, you’re debugging localization bugs inside HTTP response handlers.
Open-Meteo is the simplest normalizer. One API call, flat response, metric units already:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| function normalizeOpenMeteo(raw) {
if (!raw.current || !raw.daily) return null
var daily = []
var dates = raw.daily.time || []
for (var i = 0; i < dates.length; i++) {
var dt = Date.parse(dates[i] + "T12:00:00Z") / 1000
daily.push({
dt: isNaN(dt) ? Math.floor(Date.now() / 1000) : Math.floor(dt),
temp: {
max: raw.daily.temperature_2m_max[i],
min: raw.daily.temperature_2m_min[i],
},
weather: [openMeteoCodeToWeather(raw.daily.weather_code[i], 1)],
pop: Number(raw.daily.precipitation_probability_max[i] || 0) / 100.0,
})
}
return {
current: {
temp: raw.current.temperature_2m,
feels_like: raw.current.apparent_temperature,
humidity: raw.current.relative_humidity_2m,
wind_speed: raw.current.wind_speed_10m,
wind_deg: raw.current.wind_direction_10m,
weather: [openMeteoCodeToWeather(raw.current.weather_code, raw.current.is_day)]
},
daily: daily
}
}
|
Read the field, slot it into the canonical shape, move on. The weather.gov normalizer is the hard case: three sequential calls, Fahrenheit responses that need conversion, day/night periods that need merging. More work, same destination.
Every normalizer can fail, though. They return null on failure. A validation step checks the canonical shape before anything reaches the UI. Missing fields produce named error messages: “missing current.temp”, “missing daily[2].weather”. When you’re integrating a new provider, the failure tells you exactly which field mapping you got wrong.
The runtime makes this trickier than it sounds. Plasma widgets have no disk I/O. No localStorage, no file system. You get XMLHttpRequest and Date(). That’s it. This matters for weather.gov because of the two-hop resolution. The points endpoint returns forecast URLs that don’t change for a given coordinate pair. Without disk, caching those URLs means a QML property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| property var weatherGovCache: ({ key: "", hourlyUrl: "", dailyUrl: "" })
function fetchWeatherGov(pointsUrl, provider) {
var cacheKey = lat + "," + lon
if (weatherGovCache.key === cacheKey && weatherGovCache.hourlyUrl) {
fetchWeatherGovForecasts(weatherGovCache.hourlyUrl, weatherGovCache.dailyUrl, ...)
return
}
// First call: resolve points -> forecast URLs
requestJson(pointsUrl, 15000, {}, function(points) {
weatherGovCache = {
key: cacheKey,
hourlyUrl: points.properties.forecastHourly,
dailyUrl: points.properties.forecast
}
fetchWeatherGovForecasts(weatherGovCache.hourlyUrl, weatherGovCache.dailyUrl, ...)
}, ...)
}
|
Keyed on "{lat},{lon}". Survives for the widget’s lifetime, clears on restart. The Plasma sandbox forced exactly the right scope: cache the URL resolution, cache nothing else.