aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 08:49:00 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 08:49:00 +0200
commit159fabf66ca1b7e0a745165de0ef56e2631f79b8 (patch)
tree40a342900c88e088fee8f83008c7860769060cb3
parentcc30f929b5f8edaed8a9f8a52cc44de71d9ebfe2 (diff)
downloadconky-theme-udt-159fabf66ca1b7e0a745165de0ef56e2631f79b8.tar.gz
conky-theme-udt-159fabf66ca1b7e0a745165de0ef56e2631f79b8.zip
feat: add OWM condition id to icon mapping
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--lib/weather.lua62
-rw-r--r--test/test_weather.lua65
2 files changed, 127 insertions, 0 deletions
diff --git a/lib/weather.lua b/lib/weather.lua
new file mode 100644
index 0000000..4eab3ac
--- /dev/null
+++ b/lib/weather.lua
@@ -0,0 +1,62 @@
+-- OpenWeatherMap domain knowledge and cache parsing.
+--
+-- Separate from lib/data.lua: that reads /proc and /sys, this reads a cached
+-- HTTP response and carries its own tables. Same testable shape though, every
+-- function takes a string or numbers and returns a value, so the tests need no
+-- filesystem and no network.
+--
+-- Nothing here raises. A Lua error in this project is a blank screen, so every
+-- parse path returns nil and lets the widget draw its "no data" state.
+
+local M = {}
+
+-- Nerd Font codepoints, all present in Inconsolata Nerd Font (card.FONT_MONO).
+-- Verified by rendering the glyphs and looking at them, not by fontconfig
+-- alone: a missing glyph draws as an invisible blank rather than an error.
+M.ICON = {
+ thunder_day = '\u{E30F}',
+ thunder_night = '\u{E32A}',
+ drizzle_day = '\u{E306}',
+ drizzle_night = '\u{E326}',
+ drizzle_heavy_day = '\u{E308}',
+ drizzle_heavy_night = '\u{E325}',
+ rain_day = '\u{E308}',
+ rain_night = '\u{E325}',
+ snow = '\u{E31A}',
+ fog = '\u{E313}',
+ tornado = '\u{E351}',
+ clear_day = '\u{E30D}',
+ clear_night = '\u{E32B}',
+ few_day = '\u{E302}',
+ few_night = '\u{E379}',
+ overcast = '\u{E312}',
+ unknown = '\u{E374}',
+ sunrise = '\u{E34C}',
+ sunset = '\u{E34D}',
+}
+
+-- Condition id to icon, by upper bound. Ported from the polybar script's
+-- accumulated knowledge; the ranges are not round hundreds.
+-- `now`, `sunrise` and `sunset` are unix timestamps and pick the day or night
+-- face for the conditions that have both.
+function M.icon(id, now, sunrise, sunset)
+ if type(id) ~= 'number' then return M.ICON.unknown end
+ local day = true
+ if sunrise and sunset then day = (now >= sunrise and now <= sunset) end
+ local function pick(d, n) return day and d or n end
+
+ if id <= 232 then return pick(M.ICON.thunder_day, M.ICON.thunder_night)
+ elseif id <= 311 then return pick(M.ICON.drizzle_day, M.ICON.drizzle_night)
+ elseif id <= 321 then return pick(M.ICON.drizzle_heavy_day, M.ICON.drizzle_heavy_night)
+ elseif id <= 531 then return pick(M.ICON.rain_day, M.ICON.rain_night)
+ elseif id <= 622 then return M.ICON.snow
+ elseif id <= 771 then return M.ICON.fog
+ elseif id == 781 then return M.ICON.tornado
+ elseif id == 800 then return pick(M.ICON.clear_day, M.ICON.clear_night)
+ elseif id == 801 then return pick(M.ICON.few_day, M.ICON.few_night)
+ elseif id <= 804 then return M.ICON.overcast
+ end
+ return M.ICON.unknown
+end
+
+return M
diff --git a/test/test_weather.lua b/test/test_weather.lua
new file mode 100644
index 0000000..87e787a
--- /dev/null
+++ b/test/test_weather.lua
@@ -0,0 +1,65 @@
+-- Checks for lib/weather.lua.
+-- Run from the repo root: lua test/test_weather.lua
+-- The domain tables are what silently misreport when a boundary is off by one,
+-- so every range boundary gets an assertion. The card itself is verified by
+-- screenshot.
+
+package.path = './?.lua;' .. package.path
+local weather = require 'lib.weather'
+
+-- === Condition id to icon =================================================
+-- OWM ids group by hundreds, but the boundaries are not round numbers: the
+-- ranges come from the polybar script that ran against this API for years.
+-- Each assertion pairs the last id in a range with the first id of the next,
+-- which is where an off-by-one would hide.
+-- Sunrise 1500, sunset 1900, so DAY must sit INSIDE that window and NIGHT
+-- outside it. Naming them without checking them against the window is how the
+-- first draft of this test made every timestamp evaluate as night.
+local DAY, NIGHT = 1700, 2000
+
+local function icon(id, now)
+ return weather.icon(id, now, 1500, 1900) -- sunrise 1500, sunset 1900
+end
+
+-- Thunderstorm: everything up to 232.
+assert(icon(200, DAY) == weather.ICON.thunder_day, 'id 200 day')
+assert(icon(232, DAY) == weather.ICON.thunder_day, 'id 232 is still thunder')
+assert(icon(232, NIGHT) == weather.ICON.thunder_night, 'id 232 night')
+
+-- Light drizzle: 233..311.
+assert(icon(233, DAY) == weather.ICON.drizzle_day, 'id 233 leaves thunder')
+assert(icon(311, DAY) == weather.ICON.drizzle_day, 'id 311 is still light drizzle')
+
+-- Heavy drizzle: 312..321.
+assert(icon(312, DAY) == weather.ICON.drizzle_heavy_day, 'id 312 is heavy drizzle')
+assert(icon(321, DAY) == weather.ICON.drizzle_heavy_day, 'id 321 is still heavy drizzle')
+
+-- Rain: 322..531.
+assert(icon(322, DAY) == weather.ICON.rain_day, 'id 322 is rain')
+assert(icon(531, DAY) == weather.ICON.rain_day, 'id 531 is still rain')
+
+-- Snow: 532..622. One icon, no day/night variant.
+assert(icon(600, DAY) == weather.ICON.snow, 'id 600 is snow')
+assert(icon(622, NIGHT) == weather.ICON.snow, 'snow is the same at night')
+
+-- Fog: 623..771.
+assert(icon(741, DAY) == weather.ICON.fog, 'id 741 is fog')
+assert(icon(771, DAY) == weather.ICON.fog, 'id 771 is still fog')
+
+-- Tornado is a single id, not a range.
+assert(icon(781, DAY) == weather.ICON.tornado, 'id 781 is tornado')
+
+-- Clear and few clouds each have a day and a night face.
+assert(icon(800, DAY) == weather.ICON.clear_day, 'id 800 day is the sun')
+assert(icon(800, NIGHT) == weather.ICON.clear_night, 'id 800 night is the moon')
+assert(icon(801, DAY) == weather.ICON.few_day, 'id 801 day')
+assert(icon(801, NIGHT) == weather.ICON.few_night, 'id 801 night')
+
+-- Overcast: 802..804, no night variant.
+assert(icon(804, DAY) == weather.ICON.overcast, 'id 804 is overcast')
+
+-- Anything outside the known ids must be visibly wrong, not silently sunny.
+assert(icon(999, DAY) == weather.ICON.unknown, 'unknown id gets the error glyph')
+assert(weather.icon(nil, DAY, 1500, 1900) == weather.ICON.unknown, 'nil id')
+
+print('test_weather: all assertions passed')