aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 09:00:40 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 09:00:40 +0200
commite6c5c5e6443345e019549d8b41ffa0c19d050c9f (patch)
tree3ccb4be91e079954086ddb8835d21ecc86716663
parente9dd31b04324d9a95acf6561a4b6ebcee9d3b8c0 (diff)
downloadconky-theme-udt-e6c5c5e6443345e019549d8b41ffa0c19d050c9f.tar.gz
conky-theme-udt-e6c5c5e6443345e019549d8b41ffa0c19d050c9f.zip
feat: parse the cached OWM response
Lua patterns instead of a JSON library: the current-weather response is flat and known, so six scalars do not justify a dependency. Every failure path (truncated write, empty file, garbage, an API error body, nil input) returns nil rather than raising, since a Lua error here is a blank dashboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--lib/weather.lua41
-rw-r--r--test/fixtures/weather.json1
-rw-r--r--test/fixtures/weather_truncated.json1
-rw-r--r--test/test_weather.lua43
4 files changed, 86 insertions, 0 deletions
diff --git a/lib/weather.lua b/lib/weather.lua
index 84f355a..a66d79a 100644
--- a/lib/weather.lua
+++ b/lib/weather.lua
@@ -142,4 +142,45 @@ function M.is_day(now, sunrise, sunset)
return now >= sunrise and now <= sunset
end
+-- Parse the cached OWM response.
+--
+-- Lua patterns, not a JSON library: the current-weather response is flat and
+-- its shape is known, so six scalars do not justify a dependency. Returns nil
+-- on anything unreadable, never an error.
+function M.parse(src)
+ if type(src) ~= 'string' or src == '' then return nil end
+
+ -- Scalars are matched inside their own object where the key would otherwise
+ -- be ambiguous. "temp" appears as a prefix of "temp_min" and "temp_max", so
+ -- it is anchored to the character that follows it.
+ local function num(pat)
+ return tonumber(src:match(pat))
+ end
+
+ local main = src:match('"main"%s*:%s*(%b{})') or ''
+ local wind = src:match('"wind"%s*:%s*(%b{})') or ''
+ local sys = src:match('"sys"%s*:%s*(%b{})') or ''
+ local cond = src:match('"weather"%s*:%s*%[%s*(%b{})') or ''
+
+ local w = {
+ id = tonumber(cond:match('"id"%s*:%s*(%-?%d+)')),
+ description = cond:match('"description"%s*:%s*"([^"]*)"'),
+ temp = tonumber(main:match('"temp"%s*:%s*(%-?[%d%.]+)')),
+ feels_like = tonumber(main:match('"feels_like"%s*:%s*(%-?[%d%.]+)')),
+ humidity = tonumber(main:match('"humidity"%s*:%s*(%d+)')),
+ wind_speed = tonumber(wind:match('"speed"%s*:%s*([%d%.]+)')),
+ wind_deg = tonumber(wind:match('"deg"%s*:%s*(%d+)')),
+ sunrise = tonumber(sys:match('"sunrise"%s*:%s*(%d+)')),
+ sunset = tonumber(sys:match('"sunset"%s*:%s*(%d+)')),
+ city = src:match('"name"%s*:%s*"([^"]*)"'),
+ dt = num('"dt"%s*:%s*(%d+)'),
+ }
+
+ -- Without these the card has nothing to draw, and a partial card is worse
+ -- than an honest "no data". An API error body fails here too: it carries a
+ -- cod and a message, no weather.
+ if not (w.id and w.temp and w.sunrise and w.sunset) then return nil end
+ return w
+end
+
return M
diff --git a/test/fixtures/weather.json b/test/fixtures/weather.json
new file mode 100644
index 0000000..12493b0
--- /dev/null
+++ b/test/fixtures/weather.json
@@ -0,0 +1 @@
+{"coord":{"lon":11.0,"lat":45.0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"base":"stations","main":{"temp":18.34,"feels_like":18.11,"temp_min":16.67,"temp_max":19.44,"pressure":1014,"humidity":72},"visibility":10000,"wind":{"speed":4.12,"deg":230},"clouds":{"all":75},"dt":1789625000,"sys":{"type":2,"id":2004688,"country":"XX","sunrise":1789600000,"sunset":1789646000},"timezone":7200,"id":1,"name":"Example City","cod":200}
diff --git a/test/fixtures/weather_truncated.json b/test/fixtures/weather_truncated.json
new file mode 100644
index 0000000..eb0099a
--- /dev/null
+++ b/test/fixtures/weather_truncated.json
@@ -0,0 +1 @@
+{"coord":{"lon":11.0,"lat":45.0},"weather":[{"id":501,"main":"Rain","descrip
diff --git a/test/test_weather.lua b/test/test_weather.lua
index 1d35748..4ea953f 100644
--- a/test/test_weather.lua
+++ b/test/test_weather.lua
@@ -153,4 +153,47 @@ assert(weather.is_day(2000, 1000, 2000) == true, 'sunset counts as day')
assert(weather.is_day(500, 1000, 2000) == false, 'before dawn is night')
assert(weather.is_day(2500, 1000, 2000) == false, 'after dusk is night')
+-- === Parsing the cache ====================================================
+-- The parser reads scalars by key with Lua patterns rather than pulling in a
+-- JSON library: the response is flat and known, and six numbers do not justify
+-- a dependency. Caching the whole response still costs nothing, since a field
+-- added later is already on disk.
+local function read(path)
+ local f = assert(io.open(path, 'r'))
+ local s = f:read('*a')
+ f:close()
+ return s
+end
+
+local w = weather.parse(read('test/fixtures/weather.json'))
+assert(w, 'the fixture must parse')
+assert(w.id == 501, 'condition id, got ' .. tostring(w.id))
+assert(w.description == 'moderate rain', 'description, got ' .. tostring(w.description))
+assert(w.temp == 18.34, 'temp, got ' .. tostring(w.temp))
+assert(w.feels_like == 18.11, 'feels like, got ' .. tostring(w.feels_like))
+assert(w.humidity == 72, 'humidity, got ' .. tostring(w.humidity))
+assert(w.wind_speed == 4.12, 'wind m/s, got ' .. tostring(w.wind_speed))
+assert(w.wind_deg == 230, 'wind direction, got ' .. tostring(w.wind_deg))
+assert(w.sunrise == 1789600000, 'sunrise, got ' .. tostring(w.sunrise))
+assert(w.sunset == 1789646000, 'sunset, got ' .. tostring(w.sunset))
+assert(w.city == 'Example City', 'city, got ' .. tostring(w.city))
+assert(w.dt == 1789625000, 'observation time, got ' .. tostring(w.dt))
+
+-- temp comes before feels_like in the response and both live under "main",
+-- so a lazy pattern would read one for the other. They differ in the fixture
+-- precisely so this is checkable.
+assert(w.temp ~= w.feels_like, 'temp and feels_like must not collapse')
+
+-- Failure modes all return nil rather than raising: a Lua error here is a
+-- blank dashboard, not a message.
+assert(weather.parse(read('test/fixtures/weather_truncated.json')) == nil,
+ 'a truncated response must give nil')
+assert(weather.parse('') == nil, 'empty input gives nil')
+assert(weather.parse('not json at all') == nil, 'garbage gives nil')
+assert(weather.parse(nil) == nil, 'nil input gives nil')
+
+-- An error body carries cod 401 and no weather. It must not parse as data.
+assert(weather.parse('{"cod":401,"message":"Invalid API key."}') == nil,
+ 'an API error body must give nil')
+
print('test_weather: all assertions passed')