aboutsummaryrefslogtreecommitdiffstats
path: root/lib/weather.lua
blob: f8c75178359ecc68002b8abc3468795495f115af (plain)
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-- 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
  -- `now` is guarded alongside sunrise and sunset, not just them: comparing a
  -- nil now against a number raises, and a raise here is a blank dashboard.
  -- Missing any of the three means the sun is unknown, so the day face is
  -- drawn, which is what is_day() falls back to as well.
  local day = true
  if now and 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

-- OWM's metric units report wind in m/s. The card shows km/h.
function M.kmh(ms)
  if type(ms) ~= 'number' then return nil end
  return ms * 3.6
end

-- Beaufort force from km/h. Upper bounds carried over from the polybar
-- script; a value equal to a bound stays in the lower force.
--
-- The scale starts at 0 and ipairs starts at 1, so force N's upper bound is
-- BEAUFORT[N + 1] and the loop index is already N + 1. Hence `force - 1`
-- below. The scale tops out at 12, which is why the fall-through returns it.
local BEAUFORT = { 1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117 }

function M.beaufort(kmh)
  if type(kmh) ~= 'number' then return nil end
  for force, bound in ipairs(BEAUFORT) do
    if kmh <= bound then return force - 1 end
  end
  return 12
end

-- Eight compass points. The half-step offset is what makes north straddle
-- zero: without it, 350 degrees would land in NW and 10 in NE, leaving north
-- with only half its arc.
--
-- Nothing calls compass() yet: the card draws arrow() instead. It is kept
-- because it shares arrow()'s binning exactly, so testing both pins that
-- shared logic from two angles, and swapping the card to read "S 9 km/h"
-- instead of an arrow is then a one-word change rather than new code.
local POINTS = { 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' }

function M.compass(deg)
  if type(deg) ~= 'number' then return nil end
  local i = math.floor(((deg % 360) + 22.5) / 45) % 8
  return POINTS[i + 1]
end

-- Arrow glyphs, in the same order as POINTS but rotated half a turn: OWM
-- reports the direction the wind comes FROM, and an arrow reads as the
-- direction it goes TO.
local ARROWS = { '\u{2193}', '\u{2199}', '\u{2190}', '\u{2196}',
                 '\u{2191}', '\u{2197}', '\u{2192}', '\u{2198}' }

function M.arrow(deg)
  if type(deg) ~= 'number' then return '' end
  local i = math.floor(((deg % 360) + 22.5) / 45) % 8
  return ARROWS[i + 1]
end

-- Fraction of daylight elapsed, clamped to 0..1.
--
-- OWM returns TODAY's sunrise and sunset, so before dawn this is negative and
-- after dusk it is over 1. Clamping parks the dot at the corresponding end of
-- the arc, which is the cheap correct-enough behaviour; multi-day astronomy
-- buys nothing for a dot on a curve.
function M.sun_t(now, sunrise, sunset)
  if type(now) ~= 'number' or type(sunrise) ~= 'number'
     or type(sunset) ~= 'number' then return nil end
  local span = sunset - sunrise
  if span <= 0 then return 0 end   -- polar day/night or bad data: no division
  local t = (now - sunrise) / span
  if t < 0 then return 0 elseif t > 1 then return 1 end
  return t
end

-- Whether the sun is up. Inclusive at both ends, matching the polybar script's
-- comparison, so the instant of sunrise draws the day face.
function M.is_day(now, sunrise, sunset)
  if type(now) ~= 'number' or type(sunrise) ~= 'number'
     or type(sunset) ~= 'number' then return true end
  return now >= sunrise and now <= sunset
end

return M