Merge branch 'master' of ssh://git.house:/usr/local/git/base/kiosk
[kiosk.git] / weather_renderer.py
index fbb3ed8170f873da0a3223088b8c60462a858c1a..8da01ab73bae05e880c3e3e9e3ac32191608e899 100644 (file)
@@ -1,83 +1,30 @@
 #!/usr/bin/env python3
 
-from datetime import datetime
+import logging
 import json
-import re
+import urllib.request
+import urllib.error
+import urllib.parse
+from datetime import datetime
+from collections import defaultdict
 from typing import Dict, List
-import urllib.request, urllib.error, urllib.parse
 
 import file_writer
 import renderer
-import secrets
-import random
+import kiosk_secrets as secrets
+
+logger = logging.getLogger(__name__)
 
 
-class weather_renderer(renderer.debuggable_abstaining_renderer):
+class weather_renderer(renderer.abstaining_renderer):
     """A renderer to fetch forecast from wunderground."""
 
     def __init__(self, name_to_timeout_dict: Dict[str, int], file_prefix: str) -> None:
-        super(weather_renderer, self).__init__(name_to_timeout_dict, False)
+        super().__init__(name_to_timeout_dict)
         self.file_prefix = file_prefix
 
-    def debug_prefix(self) -> str:
-        return f"weather({self.file_prefix})"
-
-    def periodic_render(self, key: str) -> bool:
-        return self.fetch_weather()
-
-    def describe_time(self, index: int) -> str:
-        if index <= 1:
-            return "overnight"
-        elif index <= 3:
-            return "morning"
-        elif index <= 5:
-            return "afternoon"
-        else:
-            return "evening"
-
-    def describe_wind(self, mph: float) -> str:
-        if mph <= 0.3:
-            return "calm"
-        elif mph <= 5.0:
-            return "light"
-        elif mph < 15.0:
-            return "breezy"
-        elif mph <= 25.0:
-            return "gusty"
-        else:
-            return "heavy"
-
-    def describe_magnitude(self, mm: float) -> str:
-        if mm < 2.0:
-            return "light"
-        elif mm < 10.0:
-            return "moderate"
-        else:
-            return "heavy"
-
-    def describe_precip(self, rain: float, snow: float) -> str:
-        if rain == 0.0 and snow == 0.0:
-            return "no precipitation"
-        magnitude = rain + snow
-        if rain > 0 and snow > 0:
-            return f"a {self.describe_magnitude(magnitude)} mix of rain and snow"
-        elif rain > 0:
-            return f"{self.describe_magnitude(magnitude)} rain"
-        elif snow > 0:
-            return f"{self.describe_magnitude(magnitude)} snow"
-
-    def fix_caps(self, s: str) -> str:
-        r = ""
-        s = s.lower()
-        for x in s.split("."):
-            x = x.strip()
-            r += x.capitalize() + ". "
-        r = r.replace(". .", ".")
-        return r
-
-    def pick_icon(
-        self, conditions: List[str], rain: List[float], snow: List[float]
-    ) -> str:
+    @staticmethod
+    def pick_icon(conditions: List[str], rain: List[float], snow: List[float]) -> str:
         #                     rain     snow    clouds    sun
         # fog.gif
         # hazy.gif
@@ -97,7 +44,7 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         seen_snow = False
         cloud_count = 0
         clear_count = 0
-        total_snow = 0
+        total_snow = 0.0
         count = min(len(conditions), len(rain), len(snow))
         for x in range(0, count):
             seen_rain = rain[x] > 0
@@ -133,107 +80,30 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
             return "partlysunny.gif"
         return "clear.gif"
 
-    def describe_weather(
-        self,
-        high: float,
-        low: float,
-        wind: List[float],
-        conditions: List[str],
-        rain: List[float],
-        snow: List[float],
-    ) -> str:
-        # High temp: 65
-        # Low temp: 44
-        #             -onight------  -morning----- -afternoon--  -evening----
-        #             12a-3a  3a-6a  6a-9a  9a-12p 12p-3p 3p-6p  6p-9p 9p-12p
-        # Wind:       [12.1   3.06   3.47   4.12   3.69   3.31   2.73  2.1]
-        # Conditions: [Clouds Clouds Clouds Clouds Clouds Clouds Clear Clear]
-        # Rain:       [0.4    0.2    0      0      0      0      0     0]
-        # Snow:       [0      0      0      0      0      0      0     0]
-        high = int(high)
-        low = int(low)
-        count = min(len(wind), len(conditions), len(rain), len(snow))
-        descr = ""
-
-        lcondition = ""
-        lwind = ""
-        lprecip = ""
-        ltime = ""
-        for x in range(0, count):
-            time = self.describe_time(x)
-            current = ""
-            chunks = 0
-
-            txt = conditions[x]
-            if txt == "Clouds":
-                txt = "cloudy"
-            elif txt == "Rain":
-                txt = "rainy"
-
-            if txt != lcondition:
-                if txt != "Snow" and txt != "Rain":
-                    current += txt
-                    chunks += 1
-                lcondition = txt
-
-            txt = self.describe_wind(wind[x])
-            if txt != lwind:
-                if len(current) > 0:
-                    current += " with "
-                current += txt + " winds"
-                lwind = txt
-                chunks += 1
-
-            txt = self.describe_precip(rain[x], snow[x])
-            if txt != lprecip:
-                if len(current) > 0:
-                    if chunks > 1:
-                        current += " and "
-                    else:
-                        current += " with "
-                chunks += 1
-                current += txt
-                lprecip = txt
-
-            if len(current):
-                if ltime != time:
-                    if random.randint(0, 3) == 0:
-                        if time != "overnight":
-                            descr += current + " in the " + time + ". "
-                        descr += current + " overnight. "
-                    else:
-                        if time != "overnight":
-                            descr += "In the "
-                        descr += time + ", " + current + ". "
-                else:
-                    current = current.replace("cloudy", "clouds")
-                    descr += current + " developing. "
-                ltime = time
-        if ltime == "overnight" or ltime == "morning":
-            descr += "Conditions continuing the rest of the day. "
-        descr = descr.replace("with breezy winds", "and breezy")
-        descr = descr.replace("Clear developing", "Skies clearing")
-        descr = self.fix_caps(descr)
-        return descr
+    def periodic_render(self, key: str) -> bool:
+        return self.fetch_weather()
 
-    def fetch_weather(self) -> None:
+    def fetch_weather(self) -> bool:
         if self.file_prefix == "stevens":
             text_location = "Stevens Pass, WA"
-            param = "lat=47.74&lon=-121.08"
+            param = "lat=47.7322&lon=-121.1025"
         elif self.file_prefix == "telma":
             text_location = "Telma, WA"
             param = "lat=47.84&lon=-120.81"
         else:
             text_location = "Bellevue, WA"
             param = "id=5786882"
-
-        www = urllib.request.urlopen(
-            "http://api.openweathermap.org/data/2.5/forecast?%s&APPID=%s&units=imperial"
-            % (param, secrets.openweather_key)
-        )
+        secret = secrets.openweather_key
+        url = f"http://api.openweathermap.org/data/2.5/forecast?{param}&APPID={secret}&units=imperial"
+        logger.info(f"GETting {url}")
+        www = urllib.request.urlopen(url)
         response = www.read()
         www.close()
+        if www.getcode() != 200:
+            logger.error("Bad response: {response}")
+            raise Exception(response)
         parsed_json = json.loads(response)
+        logger.info("URL read ok")
 
         # https://openweathermap.org/forecast5
         # {"cod":"200",
@@ -252,52 +122,93 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         #     "dt_txt":"2017-01-30 18:00:00"
         #     },
         #     {"dt":1485810000,....
-        with file_writer.file_writer("weather-%s_3_10800.html" % self.file_prefix) as f:
+
+        with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f:
             f.write(
                 f"""
-<h1>Weather at {text_location}:</h1>
+<h1>Upcoming weather at {text_location}:</h1>
 <hr>
+<script src="/kiosk/Chart.js"></script>"""
+            )
+            f.write(
+                """
+<script>
+function makePrecipChart(name, xValues, yValues) {
+  const config = {
+    type: 'line',
+    data: {
+      labels: xValues,
+      datasets: [
+        {
+          fill: true,
+          lineTension: 0,
+          backgroundColor: "rgba(0,0,255,0.1)",
+          borderColor: "rgba(0,0,255,0.9)",
+          data: yValues
+        }
+      ]
+    },
+    options: {
+      plugins: {
+        legend: {
+          display: false,
+        },
+      },
+      scales: {
+        x: {
+          grid: {
+            display: false,
+          }
+        },
+        y: {
+          display: false,
+          beginAtZero: true,
+          min: 0.0,
+          max: 12.0,
+          grid: {
+            display: false,
+          },
+        }
+      }
+    },
+  };
+  return new Chart(name, config);
+}
+</script>
 <center>
-<table width=99% cellspacing=10 border=0>
-    <tr>"""
+"""
             )
             count = parsed_json["cnt"]
 
             ts = {}
             highs = {}
             lows = {}
-            wind = {}
-            conditions = {}
-            rain = {}
-            snow = {}
-            for x in range(0, count):
-                data = parsed_json["list"][x]
-                dt = data["dt_txt"]  # 2019-10-07 18:00:00
-                date = dt.split(" ")[0]
-                time = dt.split(" ")[1]
-                wind[date] = []
-                conditions[date] = []
-                highs[date] = -99999
-                lows[date] = +99999
-                rain[date] = []
-                snow[date] = []
-                ts[date] = 0
+            wind: Dict[str, List[float]] = defaultdict(list)
+            conditions: Dict[str, List[str]] = defaultdict(list)
+            rain: Dict[str, List[float]] = defaultdict(list)
+            snow: Dict[str, List[float]] = defaultdict(list)
+            precip: Dict[str, List[float]] = defaultdict(list)
 
             for x in range(0, count):
                 data = parsed_json["list"][x]
                 dt = data["dt_txt"]  # 2019-10-07 18:00:00
-                date = dt.split(" ")[0]
-                time = dt.split(" ")[1]
+                (date, time) = dt.split(" ")
                 _ = data["dt"]
-                if _ > ts[date]:
+                if _ not in ts or _ > ts[date]:
                     ts[date] = _
                 temp = data["main"]["temp"]
-                if highs[date] < temp:
+
+                # High and low temp
+                if date not in highs or highs[date] < temp:
                     highs[date] = temp
-                if temp < lows[date]:
+                if date not in lows or lows[date] > temp:
                     lows[date] = temp
+
+                # Windspeed and conditions
                 wind[date].append(data["wind"]["speed"])
                 conditions[date].append(data["weather"][0]["main"])
+
+                # 3h precipitation (rain / snow)
                 if "rain" in data and "3h" in data["rain"]:
                     rain[date].append(data["rain"]["3h"])
                 else:
@@ -329,99 +240,149 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
                 #  },
                 #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
 
-            # Next 5 half-days
-            # for x in xrange(0, 5):
-            #    fcast = parsed_json['forecast']['txt_forecast']['forecastday'][x]
-            #    text = fcast['fcttext']
-            #    text = re.subn(r' ([0-9]+)F', r' \1&deg;F', text)[0]
-            #    f.write('<td style="vertical-align:top;font-size:75%%"><P STYLE="padding:8px;">%s</P></td>' % text)
-            # f.write('</tr></table>')
-            # f.close()
-            # return True
-
-            # f.write("<table border=0 cellspacing=10>\n")
-            days_seen = {}
+            days_seen = set()
             for date in sorted(highs.keys()):
-                today = datetime.fromtimestamp(ts[date])
-                formatted_date = today.strftime("%a %e %b")
+                day = datetime.fromtimestamp(ts[date])
+                formatted_date = day.strftime("%a %e %b")
                 if formatted_date in days_seen:
                     continue
-                days_seen[formatted_date] = True
-            num_days = len(list(days_seen.keys()))
-
-            days_seen = {}
-            for date in sorted(highs.keys()):
-                precip = 0.0
-                for _ in rain[date]:
-                    precip += _
-                for _ in snow[date]:
-                    precip += _
+                days_seen.add(formatted_date)
+            total = len(days_seen)
+
+            first_day = True
+            days_seen = set()
+            for n, date in enumerate(sorted(highs.keys())):
+                if n % 3 == 0:
+                    if n > 0:
+                        f.write("</div>")
+                    f.write('<div STYLE="overflow:hidden; width:100%; height:500px">')
+                    remaining = total - n
+                    if remaining >= 3:
+                        width = "33%"
+                    else:
+                        width = f"{100/remaining}%"
+
+                aggregate_daily_precip = 0.0
+                for r, s in zip(rain[date], snow[date]):
+                    hourly_aggregate = r + s
+
+                    # The weather report is always way wrong about Stevens.
+                    if self.file_prefix == "stevens":
+                        hourly_aggregate *= 3.5
+                    aggregate_daily_precip += hourly_aggregate
+                    precip[date].append(hourly_aggregate)
+                logger.debug(
+                    f"Aggregate precip on {date} was {aggregate_daily_precip} mm"
+                )
+                if first_day:
+                    while len(precip[date]) < 8:
+                        precip[date].insert(0, 0)
+                    first_day = False
 
-                today = datetime.fromtimestamp(ts[date])
-                formatted_date = today.strftime("%a %e %b")
+                day = datetime.fromtimestamp(ts[date])
+                formatted_date = day.strftime("%a %e %b")
                 if formatted_date in days_seen:
                     continue
-                days_seen[formatted_date] = True
+                days_seen.add(formatted_date)
                 f.write(
-                    '<td width=%d%% style="vertical-align:top;">\n' % (100 / num_days)
+                    f'<div style="width:{width}; height:500px; float:left"><table>\n'
                 )
-                f.write("<table border=0>\n")
 
                 # Date
                 f.write(
-                    "  <tr><td colspan=3 height=50><b><center><font size=6>"
-                    + formatted_date
-                    + "</font></center></b></td></tr>\n"
+                    f"""
+<tr>
+  <td colspan=3 height=50>
+    <center>
+      <font size=7><b>{formatted_date}</b></font>
+    </center>
+  </td>
+</tr>"""
                 )
 
-                # Icon
+                # Conditions icon
+                icon = weather_renderer.pick_icon(
+                    conditions[date], rain[date], snow[date]
+                )
                 f.write(
-                    '  <tr><td colspan=3 height=100><center><img src="/icons/weather/%s" height=125></center></td></tr>\n'
-                    % self.pick_icon(conditions[date], rain[date], snow[date])
+                    f"""
+<tr>
+  <td colspan=3 height=100>
+    <center>
+      <img src="/kiosk/images/weather/{icon}" height=145>
+    </center>
+  </td>
+</tr>"""
                 )
 
-                # Low temp
+                # Low temp -- left
                 color = "#000099"
                 if lows[date] <= 32.5:
                     color = "#009999"
                 f.write(
-                    '  <tr><td width=33%% align=left><font color="%s"><b>%d&deg;F&nbsp;&nbsp;</b></font></td>\n'
-                    % (color, int(lows[date]))
+                    f"""
+<tr>
+  <td width=33% align=left>
+    <font color="{color}" size=6>
+      <b>{int(lows[date])}&deg;F&nbsp;&nbsp;</b>
+    </font>
+  </td>
+"""
                 )
 
-                # Total precip
-                precip *= 0.0393701
-                if precip > 0.025:
+                # Total aggregate_precip in inches (from mm)
+                aggregate_daily_precip /= 25.4
+                if aggregate_daily_precip > 0.001:
                     f.write(
-                        '      <td width=33%%><center><b><font style="background-color:#dfdfff; color:#003355">%3.1f"</font></b></center></td>\n'
-                        % precip
+                        f"""
+  <td width=33%>
+    <center>
+      <font style="background-color:#dfdfff; color:#003355" size=6>
+        <b>{aggregate_daily_precip:3.1f}&#8221;</b>
+      </font>
+    </center>
+  </td>
+"""
                     )
                 else:
                     f.write("      <td width=33%>&nbsp;</td>\n")
 
-                # High temp
+                # High temp + precip chart
                 color = "#800000"
                 if highs[date] >= 80:
                     color = "#AA0000"
                 f.write(
-                    '      <td align=right><font color="%s"><b>&nbsp;&nbsp;%d&deg;F</b></font></td></tr>\n'
-                    % (color, int(highs[date]))
+                    f"""
+  <td align=right>
+    <font color="{color}" size=6>
+      <b>&nbsp;&nbsp;{int(highs[date])}&deg;F</b>
+    </font>
+  </td>
+</tr>"""
                 )
 
-                # Text "description"
+                # Precip graph
                 f.write(
-                    '<tr><td colspan=3 style="vertical-align:top;font-size:75%%">%s</td></tr>\n'
-                    % self.describe_weather(
-                        highs[date],
-                        lows[date],
-                        wind[date],
-                        conditions[date],
-                        rain[date],
-                        snow[date],
-                    )
+                    f"""
+<tr>
+  <td colspan=3 style="vertical-align:top;">
+    <canvas id="myChart{n}" style="width:100%;max-width:400px;height:180px;"></canvas>
+  </td>
+</tr>
+<script>
+var yValues{n} = """
+                )
+                f.write(precip[date].__repr__())
+                f.write(
+                    f""";
+var xValues{n} = [ 3, 6, 9, 12, 15, 18, 21, 24 ];
+makePrecipChart("myChart{n}", xValues{n}, yValues{n});
+</script>
+</table>
+</div>
+"""
                 )
-                f.write("</table>\n</td>\n")
-            f.write("</tr></table></center>")
+            f.write("</div></center>")
         return True