aboutsummaryrefslogtreecommitdiffstats
path: root/lib/weather.lua
blob: ac1f4cf3197c2c7e3dd3bbb5df4f7ed228f4dc5b (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
-- 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

return M