X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=weather_renderer.py;h=5d8df1e1129ffcb237ebd9f4234e370a4e9fa4a0;hb=addd4980077f6e3857c5c035b49784dc3ceca49a;hp=46d891cf3c6497fcbd5cfdee37d0bb89c0dca7db;hpb=144f769da44d2a8411e320e2e66ad1dbc48ed091;p=kiosk.git diff --git a/weather_renderer.py b/weather_renderer.py index 46d891c..5d8df1e 100644 --- a/weather_renderer.py +++ b/weather_renderer.py @@ -1,84 +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(__file__) -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" - return "rain" - - 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 @@ -134,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) -> 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", @@ -253,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""" -

Weather at {text_location}:

+

Upcoming weather at {text_location}:


+""" + ) + f.write( + """ +
- - """ +""" ) count = parsed_json["cnt"] ts = {} highs = {} lows = {} - wind: Dict[str, List[float]] = {} - conditions: Dict[str, List[str]] = {} - rain: Dict[str, List[float]] = {} - snow: Dict[str, List[float]] = {} - 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: @@ -330,99 +240,145 @@ 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°F', text)[0] - # f.write('' % text) - # f.write('

%s

') - # f.close() - # return True - - # f.write("\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("") + f.write('
') + 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 + aggregate_daily_precip += hourly_aggregate + precip[date].append(hourly_aggregate) + logger.debug( + f"Aggregate precip on {date} was {aggregate_daily_precip} cm" + ) + 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( - '
\n' % (100 / num_days) + f'
\n' ) - f.write("
\n") # Date f.write( - " \n" + f""" + + +""" ) - # Icon + # Conditions icon + icon = weather_renderer.pick_icon( + conditions[date], rain[date], snow[date] + ) f.write( - ' \n' - % self.pick_icon(conditions[date], rain[date], snow[date]) + f""" + + +""" ) - # Low temp + # Low temp -- left color = "#000099" if lows[date] <= 32.5: color = "#009999" f.write( - ' \n' - % (color, int(lows[date])) + f""" + + +""" ) - # Total precip - precip *= 0.0393701 - if precip > 0.025: + # Total aggregate_precip in inches + aggregate_daily_precip /= 2.54 + if aggregate_daily_precip > 0.025: f.write( - ' \n' - % precip + f""" + +""" ) else: f.write(" \n") - # High temp + # High temp + precip chart color = "#800000" if highs[date] >= 80: color = "#AA0000" f.write( - ' \n' - % (color, int(highs[date])) + f""" + +""" ) - # Text "description" + # Precip graph f.write( - '\n' - % self.describe_weather( - highs[date], - lows[date], - wind[date], - conditions[date], - rain[date], - snow[date], - ) + f""" + + + + +
" - + formatted_date - + "
+
+ {formatted_date} +
+
+
+ +
+
%d°F  
+ + {int(lows[date])}°F   + +
%3.1f"
+
+ + {aggregate_daily_precip:3.1f}” + +
+
   %d°F
+ +   {int(highs[date])}°F + +
%s
+ +
+
+""" ) - f.write("
\n\n") - f.write("
") + f.write("") return True