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