Fix wakeword.
[kiosk.git] / weather_renderer.py
1 #!/usr/bin/env python3
2
3 import logging
4 import json
5 import urllib.request
6 import urllib.error
7 import urllib.parse
8 from datetime import datetime
9 from collections import defaultdict
10 from typing import Dict, List
11
12 import file_writer
13 import renderer
14 import kiosk_secrets as secrets
15
16 logger = logging.getLogger(__name__)
17
18
19 class weather_renderer(renderer.abstaining_renderer):
20     """A renderer to fetch forecast from wunderground."""
21
22     def __init__(self, name_to_timeout_dict: Dict[str, int], file_prefix: str) -> None:
23         super().__init__(name_to_timeout_dict)
24         self.file_prefix = file_prefix
25
26     @staticmethod
27     def pick_icon(conditions: List[str], rain: List[float], snow: List[float]) -> str:
28         #                     rain     snow    clouds    sun
29         # fog.gif
30         # hazy.gif
31         # clear.gif
32         # mostlycloudy.gif     F         F        6+      X
33         # partlycloudy.gif     F         F        4+      4-
34         # cloudy.gif
35         # partlysunny.gif      F         F        X       5+
36         # mostlysunny.gif      F         F        X       6+
37         # rain.gif             T         F        X       X
38         # sleet.gif            T         T        X       X
39         # flurries.gif         F         T        X       X    (<1")
40         # snow.gif             F         T        X       X    (else)
41         # sunny.gif            F         F        X       7+
42         # tstorms.gif
43         seen_rain = False
44         seen_snow = False
45         cloud_count = 0
46         clear_count = 0
47         total_snow = 0.0
48         count = min(len(conditions), len(rain), len(snow))
49         for x in range(0, count):
50             seen_rain = rain[x] > 0
51             seen_snow = snow[x] > 0
52             total_snow += snow[x]
53             txt = conditions[x].lower()
54             if "cloud" in txt:
55                 cloud_count += 1
56             if "clear" in txt or "sun" in txt:
57                 clear_count += 1
58
59         if seen_rain and seen_snow:
60             if total_snow < 10:
61                 return "sleet.gif"
62             else:
63                 return "snow.gif"
64         if seen_snow:
65             if total_snow < 10:
66                 return "flurries.gif"
67             else:
68                 return "snow.gif"
69         if seen_rain:
70             return "rain.gif"
71         if cloud_count >= 6:
72             return "mostlycloudy.gif"
73         elif cloud_count >= 4:
74             return "partlycloudy.gif"
75         if clear_count >= 7:
76             return "sunny.gif"
77         elif clear_count >= 6:
78             return "mostlysunny.gif"
79         elif clear_count >= 4:
80             return "partlysunny.gif"
81         return "clear.gif"
82
83     def periodic_render(self, key: str) -> bool:
84         return self.fetch_weather()
85
86     def fetch_weather(self) -> bool:
87         if self.file_prefix == "stevens":
88             text_location = "Stevens Pass, WA"
89             param = "lat=47.7322&lon=-121.1025"
90         elif self.file_prefix == "telma":
91             text_location = "Telma, WA"
92             param = "lat=47.84&lon=-120.81"
93         else:
94             text_location = "Bellevue, WA"
95             param = "id=5786882"
96         secret = secrets.openweather_key
97         url = f"http://api.openweathermap.org/data/2.5/forecast?{param}&APPID={secret}&units=imperial"
98         logger.info(f"GETting {url}")
99         www = urllib.request.urlopen(url)
100         response = www.read()
101         www.close()
102         if www.getcode() != 200:
103             logger.error("Bad response: {response}")
104             raise Exception(response)
105         parsed_json = json.loads(response)
106         logger.info("URL read ok")
107
108         # https://openweathermap.org/forecast5
109         # {"cod":"200",
110         #  "message":0.0036,
111         #  "cnt":40,
112         #  "list":[
113         #     {"dt":1485799200,
114         #      "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},
115         #      "weather":[
116         #         {"id":800,"main":"Clear","description":"clear sky","icon":"02n"}
117         #      ],
118         #     "clouds":{"all":8},
119         #     "wind":{"speed":4.77,"deg":232.505},
120         #     "snow":{},
121         #     "sys":{"pod":"n"},
122         #     "dt_txt":"2017-01-30 18:00:00"
123         #     },
124         #     {"dt":1485810000,....
125
126         with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f:
127             f.write(
128                 f"""
129 <h1>Upcoming weather at {text_location}:</h1>
130 <hr>
131 <script src="/kiosk/Chart.js"></script>"""
132             )
133             f.write(
134                 """
135 <script>
136 function makePrecipChart(name, xValues, yValues) {
137   const config = {
138     type: 'line',
139     data: {
140       labels: xValues,
141       datasets: [
142         {
143           fill: true,
144           lineTension: 0,
145           backgroundColor: "rgba(0,0,255,0.1)",
146           borderColor: "rgba(0,0,255,0.9)",
147           data: yValues
148         }
149       ]
150     },
151     options: {
152       plugins: {
153         legend: {
154           display: false,
155         },
156       },
157       scales: {
158         x: {
159           grid: {
160             display: false,
161           }
162         },
163         y: {
164           display: false,
165           beginAtZero: true,
166           min: 0.0,
167           max: 12.0,
168           grid: {
169             display: false,
170           },
171         }
172       }
173     },
174   };
175   return new Chart(name, config);
176 }
177 </script>
178 <center>
179 """
180             )
181             count = parsed_json["cnt"]
182
183             ts = {}
184             highs = {}
185             lows = {}
186             wind: Dict[str, List[float]] = defaultdict(list)
187             conditions: Dict[str, List[str]] = defaultdict(list)
188             rain: Dict[str, List[float]] = defaultdict(list)
189             snow: Dict[str, List[float]] = defaultdict(list)
190             precip: Dict[str, List[float]] = defaultdict(list)
191
192             for x in range(0, count):
193                 data = parsed_json["list"][x]
194                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
195                 (date, time) = dt.split(" ")
196                 _ = data["dt"]
197                 if _ not in ts or _ > ts[date]:
198                     ts[date] = _
199                 temp = data["main"]["temp"]
200
201                 # High and low temp
202                 if date not in highs or highs[date] < temp:
203                     highs[date] = temp
204                 if date not in lows or lows[date] > temp:
205                     lows[date] = temp
206
207                 # Windspeed and conditions
208                 wind[date].append(data["wind"]["speed"])
209                 conditions[date].append(data["weather"][0]["main"])
210
211                 # 3h precipitation (rain / snow)
212                 if "rain" in data and "3h" in data["rain"]:
213                     rain[date].append(data["rain"]["3h"])
214                 else:
215                     rain[date].append(0)
216                 if "snow" in data and "3h" in data["snow"]:
217                     snow[date].append(data["snow"]["3h"])
218                 else:
219                     snow[date].append(0)
220
221                 # {u'clouds': {u'all': 0},
222                 #  u'sys': {u'pod': u'd'},
223                 #  u'dt_txt': u'2019-10-09 21:00:00',
224                 #  u'weather': [
225                 #      {u'main': u'Clear',
226                 #       u'id': 800,
227                 #       u'icon': u'01d',
228                 #       u'description': u'clear sky'}
229                 #  ],
230                 #  u'dt': 1570654800,
231                 #  u'main': {
232                 #       u'temp_kf': 0,
233                 #       u'temp': 54.74,
234                 #       u'grnd_level': 1018.95,
235                 #       u'temp_max': 54.74,
236                 #       u'sea_level': 1026.46,
237                 #       u'humidity': 37,
238                 #       u'pressure': 1026.46,
239                 #       u'temp_min': 54.74
240                 #  },
241                 #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
242
243             days_seen = set()
244             for date in sorted(highs.keys()):
245                 day = datetime.fromtimestamp(ts[date])
246                 formatted_date = day.strftime("%a %e %b")
247                 if formatted_date in days_seen:
248                     continue
249                 days_seen.add(formatted_date)
250             total = len(days_seen)
251
252             first_day = True
253             days_seen = set()
254             for n, date in enumerate(sorted(highs.keys())):
255                 if n % 3 == 0:
256                     if n > 0:
257                         f.write("</div>")
258                     f.write('<div STYLE="overflow:hidden; width:100%; height:500px">')
259                     remaining = total - n
260                     if remaining >= 3:
261                         width = "33%"
262                     else:
263                         width = f"{100/remaining}%"
264
265                 aggregate_daily_precip = 0.0
266                 for r, s in zip(rain[date], snow[date]):
267                     hourly_aggregate = r + s
268
269                     # The weather report is always way wrong about Stevens.
270                     if self.file_prefix == "stevens":
271                         hourly_aggregate *= 3.5
272                     aggregate_daily_precip += hourly_aggregate
273                     precip[date].append(hourly_aggregate)
274                 logger.debug(
275                     f"Aggregate precip on {date} was {aggregate_daily_precip} mm"
276                 )
277                 if first_day:
278                     while len(precip[date]) < 8:
279                         precip[date].insert(0, 0)
280                     first_day = False
281
282                 day = datetime.fromtimestamp(ts[date])
283                 formatted_date = day.strftime("%a %e %b")
284                 if formatted_date in days_seen:
285                     continue
286                 days_seen.add(formatted_date)
287                 f.write(
288                     f'<div style="width:{width}; height:500px; float:left"><table>\n'
289                 )
290
291                 # Date
292                 f.write(
293                     f"""
294 <tr>
295   <td colspan=3 height=50>
296     <center>
297       <font size=7><b>{formatted_date}</b></font>
298     </center>
299   </td>
300 </tr>"""
301                 )
302
303                 # Conditions icon
304                 icon = weather_renderer.pick_icon(
305                     conditions[date], rain[date], snow[date]
306                 )
307                 f.write(
308                     f"""
309 <tr>
310   <td colspan=3 height=100>
311     <center>
312       <img src="/kiosk/images/weather/{icon}" height=145>
313     </center>
314   </td>
315 </tr>"""
316                 )
317
318                 # Low temp -- left
319                 color = "#000099"
320                 if lows[date] <= 32.5:
321                     color = "#009999"
322                 f.write(
323                     f"""
324 <tr>
325   <td width=33% align=left>
326     <font color="{color}" size=6>
327       <b>{int(lows[date])}&deg;F&nbsp;&nbsp;</b>
328     </font>
329   </td>
330 """
331                 )
332
333                 # Total aggregate_precip in inches (from mm)
334                 aggregate_daily_precip /= 25.4
335                 if aggregate_daily_precip > 0.001:
336                     f.write(
337                         f"""
338   <td width=33%>
339     <center>
340       <font style="background-color:#dfdfff; color:#003355" size=6>
341         <b>{aggregate_daily_precip:3.1f}&#8221;</b>
342       </font>
343     </center>
344   </td>
345 """
346                     )
347                 else:
348                     f.write("      <td width=33%>&nbsp;</td>\n")
349
350                 # High temp + precip chart
351                 color = "#800000"
352                 if highs[date] >= 80:
353                     color = "#AA0000"
354                 f.write(
355                     f"""
356   <td align=right>
357     <font color="{color}" size=6>
358       <b>&nbsp;&nbsp;{int(highs[date])}&deg;F</b>
359     </font>
360   </td>
361 </tr>"""
362                 )
363
364                 # Precip graph
365                 f.write(
366                     f"""
367 <tr>
368   <td colspan=3 style="vertical-align:top;">
369     <canvas id="myChart{n}" style="width:100%;max-width:400px;height:180px;"></canvas>
370   </td>
371 </tr>
372 <script>
373 var yValues{n} = """
374                 )
375                 f.write(precip[date].__repr__())
376                 f.write(
377                     f""";
378 var xValues{n} = [ 3, 6, 9, 12, 15, 18, 21, 24 ];
379 makePrecipChart("myChart{n}", xValues{n}, yValues{n});
380 </script>
381 </table>
382 </div>
383 """
384                 )
385             f.write("</div></center>")
386         return True
387
388
389 # x = weather_renderer({"Stevens": 1000}, "stevens")
390 # x.periodic_render("Stevens")