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