Fix gkeep renderer's f-strings.
[kiosk.git] / weather_renderer.py
1 #!/usr/bin/env python3
2
3 from datetime import datetime
4 import json
5 import re
6 from typing import Dict, List
7 import urllib.request, urllib.error, urllib.parse
8
9 import file_writer
10 import renderer
11 import secrets
12 import random
13
14
15 class weather_renderer(renderer.debuggable_abstaining_renderer):
16     """A renderer to fetch forecast from wunderground."""
17
18     def __init__(self, name_to_timeout_dict: Dict[str, int], file_prefix: str) -> None:
19         super(weather_renderer, self).__init__(name_to_timeout_dict, False)
20         self.file_prefix = file_prefix
21
22     def debug_prefix(self) -> str:
23         return f"weather({self.file_prefix})"
24
25     def periodic_render(self, key: str) -> bool:
26         return self.fetch_weather()
27
28     def describe_time(self, index: int) -> str:
29         if index <= 1:
30             return "overnight"
31         elif index <= 3:
32             return "morning"
33         elif index <= 5:
34             return "afternoon"
35         else:
36             return "evening"
37
38     def describe_wind(self, mph: float) -> str:
39         if mph <= 0.3:
40             return "calm"
41         elif mph <= 5.0:
42             return "light"
43         elif mph < 15.0:
44             return "breezy"
45         elif mph <= 25.0:
46             return "gusty"
47         else:
48             return "heavy"
49
50     def describe_magnitude(self, mm: float) -> str:
51         if mm < 2.0:
52             return "light"
53         elif mm < 10.0:
54             return "moderate"
55         else:
56             return "heavy"
57
58     def describe_precip(self, rain: float, snow: float) -> str:
59         if rain == 0.0 and snow == 0.0:
60             return "no precipitation"
61         magnitude = rain + snow
62         if rain > 0 and snow > 0:
63             return f"a {self.describe_magnitude(magnitude)} mix of rain and snow"
64         elif rain > 0:
65             return f"{self.describe_magnitude(magnitude)} rain"
66         elif snow > 0:
67             return f"{self.describe_magnitude(magnitude)} snow"
68
69     def fix_caps(self, s: str) -> str:
70         r = ""
71         s = s.lower()
72         for x in s.split("."):
73             x = x.strip()
74             r += x.capitalize() + ". "
75         r = r.replace(". .", ".")
76         return r
77
78     def pick_icon(
79         self, conditions: List[str], rain: List[float], snow: List[float]
80     ) -> str:
81         #                     rain     snow    clouds    sun
82         # fog.gif
83         # hazy.gif
84         # clear.gif
85         # mostlycloudy.gif     F         F        6+      X
86         # partlycloudy.gif     F         F        4+      4-
87         # cloudy.gif
88         # partlysunny.gif      F         F        X       5+
89         # mostlysunny.gif      F         F        X       6+
90         # rain.gif             T         F        X       X
91         # sleet.gif            T         T        X       X
92         # flurries.gif         F         T        X       X    (<1")
93         # snow.gif             F         T        X       X    (else)
94         # sunny.gif            F         F        X       7+
95         # tstorms.gif
96         seen_rain = False
97         seen_snow = False
98         cloud_count = 0
99         clear_count = 0
100         total_snow = 0
101         count = min(len(conditions), len(rain), len(snow))
102         for x in range(0, count):
103             seen_rain = rain[x] > 0
104             seen_snow = snow[x] > 0
105             total_snow += snow[x]
106             txt = conditions[x].lower()
107             if "cloud" in txt:
108                 cloud_count += 1
109             if "clear" in txt or "sun" in txt:
110                 clear_count += 1
111
112         if seen_rain and seen_snow:
113             if total_snow < 10:
114                 return "sleet.gif"
115             else:
116                 return "snow.gif"
117         if seen_snow:
118             if total_snow < 10:
119                 return "flurries.gif"
120             else:
121                 return "snow.gif"
122         if seen_rain:
123             return "rain.gif"
124         if cloud_count >= 6:
125             return "mostlycloudy.gif"
126         elif cloud_count >= 4:
127             return "partlycloudy.gif"
128         if clear_count >= 7:
129             return "sunny.gif"
130         elif clear_count >= 6:
131             return "mostlysunny.gif"
132         elif clear_count >= 4:
133             return "partlysunny.gif"
134         return "clear.gif"
135
136     def describe_weather(
137         self,
138         high: float,
139         low: float,
140         wind: List[float],
141         conditions: List[str],
142         rain: List[float],
143         snow: List[float],
144     ) -> str:
145         # High temp: 65
146         # Low temp: 44
147         #             -onight------  -morning----- -afternoon--  -evening----
148         #             12a-3a  3a-6a  6a-9a  9a-12p 12p-3p 3p-6p  6p-9p 9p-12p
149         # Wind:       [12.1   3.06   3.47   4.12   3.69   3.31   2.73  2.1]
150         # Conditions: [Clouds Clouds Clouds Clouds Clouds Clouds Clear Clear]
151         # Rain:       [0.4    0.2    0      0      0      0      0     0]
152         # Snow:       [0      0      0      0      0      0      0     0]
153         high = int(high)
154         low = int(low)
155         count = min(len(wind), len(conditions), len(rain), len(snow))
156         descr = ""
157
158         lcondition = ""
159         lwind = ""
160         lprecip = ""
161         ltime = ""
162         for x in range(0, count):
163             time = self.describe_time(x)
164             current = ""
165             chunks = 0
166
167             txt = conditions[x]
168             if txt == "Clouds":
169                 txt = "cloudy"
170             elif txt == "Rain":
171                 txt = "rainy"
172
173             if txt != lcondition:
174                 if txt != "Snow" and txt != "Rain":
175                     current += txt
176                     chunks += 1
177                 lcondition = txt
178
179             txt = self.describe_wind(wind[x])
180             if txt != lwind:
181                 if len(current) > 0:
182                     current += " with "
183                 current += txt + " winds"
184                 lwind = txt
185                 chunks += 1
186
187             txt = self.describe_precip(rain[x], snow[x])
188             if txt != lprecip:
189                 if len(current) > 0:
190                     if chunks > 1:
191                         current += " and "
192                     else:
193                         current += " with "
194                 chunks += 1
195                 current += txt
196                 lprecip = txt
197
198             if len(current):
199                 if ltime != time:
200                     if random.randint(0, 3) == 0:
201                         if time != "overnight":
202                             descr += current + " in the " + time + ". "
203                         descr += current + " overnight. "
204                     else:
205                         if time != "overnight":
206                             descr += "In the "
207                         descr += time + ", " + current + ". "
208                 else:
209                     current = current.replace("cloudy", "clouds")
210                     descr += current + " developing. "
211                 ltime = time
212         if ltime == "overnight" or ltime == "morning":
213             descr += "Conditions continuing the rest of the day. "
214         descr = descr.replace("with breezy winds", "and breezy")
215         descr = descr.replace("Clear developing", "Skies clearing")
216         descr = self.fix_caps(descr)
217         return descr
218
219     def fetch_weather(self) -> None:
220         if self.file_prefix == "stevens":
221             text_location = "Stevens Pass, WA"
222             param = "lat=47.74&lon=-121.08"
223         elif self.file_prefix == "telma":
224             text_location = "Telma, WA"
225             param = "lat=47.84&lon=-120.81"
226         else:
227             text_location = "Bellevue, WA"
228             param = "id=5786882"
229
230         www = urllib.request.urlopen(
231             "http://api.openweathermap.org/data/2.5/forecast?%s&APPID=%s&units=imperial"
232             % (param, secrets.openweather_key)
233         )
234         response = www.read()
235         www.close()
236         parsed_json = json.loads(response)
237
238         # https://openweathermap.org/forecast5
239         # {"cod":"200",
240         #  "message":0.0036,
241         #  "cnt":40,
242         #  "list":[
243         #     {"dt":1485799200,
244         #      "main":{"temp":261.45,"temp_min":259.086,"temp_max":261.45,"pressure":1023.48,"sea_level":1045.39,"grnd_level":1023.48,"humidity":79,"temp_kf":2.37},
245         #      "weather":[
246         #         {"id":800,"main":"Clear","description":"clear sky","icon":"02n"}
247         #      ],
248         #     "clouds":{"all":8},
249         #     "wind":{"speed":4.77,"deg":232.505},
250         #     "snow":{},
251         #     "sys":{"pod":"n"},
252         #     "dt_txt":"2017-01-30 18:00:00"
253         #     },
254         #     {"dt":1485810000,....
255         with file_writer.file_writer("weather-%s_3_10800.html" % self.file_prefix) as f:
256             f.write(
257                 f"""
258 <h1>Weather at {text_location}:</h1>
259 <hr>
260 <center>
261 <table width=99% cellspacing=10 border=0>
262     <tr>"""
263             )
264             count = parsed_json["cnt"]
265
266             ts = {}
267             highs = {}
268             lows = {}
269             wind = {}
270             conditions = {}
271             rain = {}
272             snow = {}
273             for x in range(0, count):
274                 data = parsed_json["list"][x]
275                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
276                 date = dt.split(" ")[0]
277                 time = dt.split(" ")[1]
278                 wind[date] = []
279                 conditions[date] = []
280                 highs[date] = -99999
281                 lows[date] = +99999
282                 rain[date] = []
283                 snow[date] = []
284                 ts[date] = 0
285
286             for x in range(0, count):
287                 data = parsed_json["list"][x]
288                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
289                 date = dt.split(" ")[0]
290                 time = dt.split(" ")[1]
291                 _ = data["dt"]
292                 if _ > ts[date]:
293                     ts[date] = _
294                 temp = data["main"]["temp"]
295                 if highs[date] < temp:
296                     highs[date] = temp
297                 if temp < lows[date]:
298                     lows[date] = temp
299                 wind[date].append(data["wind"]["speed"])
300                 conditions[date].append(data["weather"][0]["main"])
301                 if "rain" in data and "3h" in data["rain"]:
302                     rain[date].append(data["rain"]["3h"])
303                 else:
304                     rain[date].append(0)
305                 if "snow" in data and "3h" in data["snow"]:
306                     snow[date].append(data["snow"]["3h"])
307                 else:
308                     snow[date].append(0)
309
310                 # {u'clouds': {u'all': 0},
311                 #  u'sys': {u'pod': u'd'},
312                 #  u'dt_txt': u'2019-10-09 21:00:00',
313                 #  u'weather': [
314                 #      {u'main': u'Clear',
315                 #       u'id': 800,
316                 #       u'icon': u'01d',
317                 #       u'description': u'clear sky'}
318                 #  ],
319                 #  u'dt': 1570654800,
320                 #  u'main': {
321                 #       u'temp_kf': 0,
322                 #       u'temp': 54.74,
323                 #       u'grnd_level': 1018.95,
324                 #       u'temp_max': 54.74,
325                 #       u'sea_level': 1026.46,
326                 #       u'humidity': 37,
327                 #       u'pressure': 1026.46,
328                 #       u'temp_min': 54.74
329                 #  },
330                 #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
331
332             # Next 5 half-days
333             # for x in xrange(0, 5):
334             #    fcast = parsed_json['forecast']['txt_forecast']['forecastday'][x]
335             #    text = fcast['fcttext']
336             #    text = re.subn(r' ([0-9]+)F', r' \1&deg;F', text)[0]
337             #    f.write('<td style="vertical-align:top;font-size:75%%"><P STYLE="padding:8px;">%s</P></td>' % text)
338             # f.write('</tr></table>')
339             # f.close()
340             # return True
341
342             # f.write("<table border=0 cellspacing=10>\n")
343             days_seen = {}
344             for date in sorted(highs.keys()):
345                 today = datetime.fromtimestamp(ts[date])
346                 formatted_date = today.strftime("%a %e %b")
347                 if formatted_date in days_seen:
348                     continue
349                 days_seen[formatted_date] = True
350             num_days = len(list(days_seen.keys()))
351
352             days_seen = {}
353             for date in sorted(highs.keys()):
354                 precip = 0.0
355                 for _ in rain[date]:
356                     precip += _
357                 for _ in snow[date]:
358                     precip += _
359
360                 today = datetime.fromtimestamp(ts[date])
361                 formatted_date = today.strftime("%a %e %b")
362                 if formatted_date in days_seen:
363                     continue
364                 days_seen[formatted_date] = True
365                 f.write(
366                     '<td width=%d%% style="vertical-align:top;">\n' % (100 / num_days)
367                 )
368                 f.write("<table border=0>\n")
369
370                 # Date
371                 f.write(
372                     "  <tr><td colspan=3 height=50><b><center><font size=6>"
373                     + formatted_date
374                     + "</font></center></b></td></tr>\n"
375                 )
376
377                 # Icon
378                 f.write(
379                     '  <tr><td colspan=3 height=100><center><img src="/icons/weather/%s" height=125></center></td></tr>\n'
380                     % self.pick_icon(conditions[date], rain[date], snow[date])
381                 )
382
383                 # Low temp
384                 color = "#000099"
385                 if lows[date] <= 32.5:
386                     color = "#009999"
387                 f.write(
388                     '  <tr><td width=33%% align=left><font color="%s"><b>%d&deg;F&nbsp;&nbsp;</b></font></td>\n'
389                     % (color, int(lows[date]))
390                 )
391
392                 # Total precip
393                 precip *= 0.0393701
394                 if precip > 0.025:
395                     f.write(
396                         '      <td width=33%%><center><b><font style="background-color:#dfdfff; color:#003355">%3.1f"</font></b></center></td>\n'
397                         % precip
398                     )
399                 else:
400                     f.write("      <td width=33%>&nbsp;</td>\n")
401
402                 # High temp
403                 color = "#800000"
404                 if highs[date] >= 80:
405                     color = "#AA0000"
406                 f.write(
407                     '      <td align=right><font color="%s"><b>&nbsp;&nbsp;%d&deg;F</b></font></td></tr>\n'
408                     % (color, int(highs[date]))
409                 )
410
411                 # Text "description"
412                 f.write(
413                     '<tr><td colspan=3 style="vertical-align:top;font-size:75%%">%s</td></tr>\n'
414                     % self.describe_weather(
415                         highs[date],
416                         lows[date],
417                         wind[date],
418                         conditions[date],
419                         rain[date],
420                         snow[date],
421                     )
422                 )
423                 f.write("</table>\n</td>\n")
424             f.write("</tr></table></center>")
425         return True
426
427
428 # x = weather_renderer({"Stevens": 1000}, "stevens")
429 # x.periodic_render("Stevens")