Fix bugs
[kiosk.git] / weather_renderer.py
1 #!/usr/bin/env python3
2
3 from datetime import datetime
4 import json
5 from typing import Dict, List
6 import urllib.request
7 import urllib.error
8 import urllib.parse
9
10 import file_writer
11 import renderer
12 import kiosk_secrets as secrets
13
14
15 class weather_renderer(renderer.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().__init__(name_to_timeout_dict)
20         self.file_prefix = file_prefix
21
22     def periodic_render(self, key: str) -> bool:
23         return self.fetch_weather()
24
25     def pick_icon(
26         self, conditions: List[str], rain: List[float], snow: List[float]
27     ) -> 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 fetch_weather(self) -> bool:
84         if self.file_prefix == "stevens":
85             text_location = "Stevens Pass, WA"
86             param = "lat=47.74&lon=-121.08"
87         elif self.file_prefix == "telma":
88             text_location = "Telma, WA"
89             param = "lat=47.84&lon=-120.81"
90         else:
91             text_location = "Bellevue, WA"
92             param = "id=5786882"
93
94         www = urllib.request.urlopen(
95             "http://api.openweathermap.org/data/2.5/forecast?%s&APPID=%s&units=imperial"
96             % (param, secrets.openweather_key)
97         )
98         response = www.read()
99         www.close()
100         parsed_json = json.loads(response)
101
102         # https://openweathermap.org/forecast5
103         # {"cod":"200",
104         #  "message":0.0036,
105         #  "cnt":40,
106         #  "list":[
107         #     {"dt":1485799200,
108         #      "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},
109         #      "weather":[
110         #         {"id":800,"main":"Clear","description":"clear sky","icon":"02n"}
111         #      ],
112         #     "clouds":{"all":8},
113         #     "wind":{"speed":4.77,"deg":232.505},
114         #     "snow":{},
115         #     "sys":{"pod":"n"},
116         #     "dt_txt":"2017-01-30 18:00:00"
117         #     },
118         #     {"dt":1485810000,....
119
120         with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f:
121             f.write(f"""
122 <h1>Upcoming weather at {text_location}:</h1>
123 <hr>
124 <script src="/kiosk/Chart.js"></script>""")
125             f.write("""
126 <script>
127 function makePrecipChart(name, xValues, yValues) {
128   const config = {
129     type: 'line',
130     data: {
131       labels: xValues,
132       datasets: [
133         {
134           fill: true,
135           lineTension: 0,
136           backgroundColor: "rgba(0,0,255,0.1)",
137           borderColor: "rgba(0,0,255,0.9)",
138           data: yValues
139         }
140       ]
141     },
142     options: {
143       plugins: {
144         legend: {
145           display: false,
146         },
147       },
148       scales: {
149         x: {
150           grid: {
151             display: false,
152           }
153         },
154         y: {
155           display: false,
156           beginAtZero: true,
157           min: 0.0,
158           max: 10.0,
159           grid: {
160             display: false,
161           },
162         }
163       }
164     },
165   };
166   return new Chart(name, config);
167 }
168 </script>
169 <center>
170 """)
171             count = parsed_json["cnt"]
172
173             ts = {}
174             highs = {}
175             lows = {}
176             wind: Dict[str, List[float]] = {}
177             conditions: Dict[str, List[str]] = {}
178             rain: Dict[str, List[float]] = {}
179             snow: Dict[str, List[float]] = {}
180             precip: Dict[str, List[float]] = {}
181
182             for x in range(0, count):
183                 data = parsed_json["list"][x]
184                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
185                 (date, time) = dt.split(' ')
186                 wind[date] = []
187                 conditions[date] = []
188                 highs[date] = None
189                 lows[date] = None
190                 rain[date] = []
191                 snow[date] = []
192                 precip[date] = []
193                 ts[date] = 0
194
195             for x in range(0, count):
196                 data = parsed_json["list"][x]
197                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
198                 (date, time) = dt.split(' ')
199                 _ = data["dt"]
200                 if _ > ts[date]:
201                     ts[date] = _
202                 temp = data["main"]["temp"]
203                 if highs[date] is None or highs[date] < temp:
204                     highs[date] = temp
205                 if lows[date] is None or temp < lows[date]:
206                     lows[date] = temp
207                 wind[date].append(data["wind"]["speed"])
208                 conditions[date].append(data["weather"][0]["main"])
209                 if "rain" in data and "3h" in data["rain"]:
210                     rain[date].append(data["rain"]["3h"])
211                 else:
212                     rain[date].append(0)
213                 if "snow" in data and "3h" in data["snow"]:
214                     snow[date].append(data["snow"]["3h"])
215                 else:
216                     snow[date].append(0)
217
218                 # {u'clouds': {u'all': 0},
219                 #  u'sys': {u'pod': u'd'},
220                 #  u'dt_txt': u'2019-10-09 21:00:00',
221                 #  u'weather': [
222                 #      {u'main': u'Clear',
223                 #       u'id': 800,
224                 #       u'icon': u'01d',
225                 #       u'description': u'clear sky'}
226                 #  ],
227                 #  u'dt': 1570654800,
228                 #  u'main': {
229                 #       u'temp_kf': 0,
230                 #       u'temp': 54.74,
231                 #       u'grnd_level': 1018.95,
232                 #       u'temp_max': 54.74,
233                 #       u'sea_level': 1026.46,
234                 #       u'humidity': 37,
235                 #       u'pressure': 1026.46,
236                 #       u'temp_min': 54.74
237                 #  },
238                 #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
239
240             days_seen = set()
241             for date in sorted(highs.keys()):
242                 day = datetime.fromtimestamp(ts[date])
243                 formatted_date = day.strftime("%a %e %b")
244                 if formatted_date in days_seen:
245                     continue
246                 days_seen.add(formatted_date)
247             total = len(days_seen)
248
249             first_day = True
250             days_seen = set()
251             for n, date in enumerate(sorted(highs.keys())):
252                 if n % 3 == 0:
253                     if n > 0:
254                         f.write("</div>")
255                     f.write('<div STYLE="overflow:hidden; width:100%; height:500px">')
256                     remaining = total - n
257                     if remaining >= 3:
258                         width = "33%"
259                     else:
260                         width = f'{100/remaining}%'
261
262                 precip[date] = []
263                 aggregate_precip = 0.0
264                 for r, s in zip(rain[date], snow[date]):
265                     aggregate = r + s
266                     aggregate_precip += aggregate
267                     precip[date].append(aggregate)
268                 if first_day:
269                     while len(precip[date]) < 8:
270                         precip[date].insert(0, 0)
271                         first_day = False
272
273                 day = datetime.fromtimestamp(ts[date])
274                 formatted_date = day.strftime("%a %e %b")
275                 if formatted_date in days_seen:
276                     continue
277                 days_seen.add(formatted_date)
278                 f.write(
279                     f'<div style="width:{width}; height:500px; float:left"><table>\n'
280                 )
281
282                 # Date
283                 f.write(f'''
284 <tr>
285   <td colspan=3 height=50>
286     <center>
287       <font size=7><b>{formatted_date}</b></font>
288     </center>
289   </td>
290 </tr>''')
291
292                 # Conditions icon
293                 icon = self.pick_icon(conditions[date], rain[date], snow[date])
294                 f.write(f'''
295 <tr>
296   <td colspan=3 height=100>
297     <center>
298       <img src="/kiosk/images/weather/{icon}" height=145>
299     </center>
300   </td>
301 </tr>''')
302
303                 # Low temp -- left
304                 color = "#000099"
305                 if lows[date] <= 32.5:
306                     color = "#009999"
307                 f.write(f'''
308 <tr>
309   <td width=33% align=left>
310     <font color="{color}" size=6>
311       <b>{int(lows[date])}&deg;F&nbsp;&nbsp;</b>
312     </font>
313   </td>
314 ''')
315
316                 # Total aggregate_precip in inches
317                 aggregate_precip *= 0.0393701
318                 if aggregate_precip > 0.025:
319                     f.write(f'''
320   <td width=33%>
321     <center>
322       <font style="background-color:#dfdfff; color:#003355" size=6>
323         <b>{aggregate_precip:3.1f}&#8221;</b>
324       </font>
325     </center>
326   </td>
327 ''')
328                 else:
329                     f.write("      <td width=33%>&nbsp;</td>\n")
330
331                 # High temp + precip chart
332                 color = "#800000"
333                 if highs[date] >= 80:
334                     color = "#AA0000"
335                 f.write(f'''
336   <td align=right>
337     <font color="{color}" size=6>
338       <b>&nbsp;&nbsp;{int(highs[date])}&deg;F</b>
339     </font>
340   </td>
341 </tr>''')
342
343                 # Precip graph
344                 f.write(f'''
345 <tr>
346   <td colspan=3 style="vertical-align:top;">
347     <canvas id="myChart{n}" style="width:100%;max-width:400px;height:180px;"></canvas>
348   </td>
349 </tr>
350 <script>
351 var yValues{n} = ''')
352                 f.write(precip[date].__repr__())
353                 f.write(f''';
354 var xValues{n} = [ 3, 6, 9, 12, 15, 18, 21, 24 ];
355 makePrecipChart("myChart{n}", xValues{n}, yValues{n});
356 </script>
357 </table>
358 </div>
359 ''')
360             f.write("</div></center>")
361         return True
362
363
364 #x = weather_renderer({"Stevens": 1000}, "stevens")
365 #x.periodic_render("Stevens")