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