X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=weather_renderer.py;h=5d8df1e1129ffcb237ebd9f4234e370a4e9fa4a0;hb=addd4980077f6e3857c5c035b49784dc3ceca49a;hp=26c49ca8a442b39374c5106159127a0ca25c630f;hpb=4b1f3d8a8b278ca6d62f461ea80c8ea21080c301;p=kiosk.git diff --git a/weather_renderer.py b/weather_renderer.py index 26c49ca..5d8df1e 100644 --- a/weather_renderer.py +++ b/weather_renderer.py @@ -1,78 +1,30 @@ +#!/usr/bin/env python3 + +import logging +import json +import urllib.request +import urllib.error +import urllib.parse from datetime import datetime +from collections import defaultdict +from typing import Dict, List + import file_writer import renderer -import json -import re -import secrets -import urllib2 -import random +import kiosk_secrets as secrets -class weather_renderer(renderer.debuggable_abstaining_renderer): - """A renderer to fetch forecast from wunderground.""" - - def __init__(self, - name_to_timeout_dict, - file_prefix): - super(weather_renderer, self).__init__(name_to_timeout_dict, False) - self.file_prefix = file_prefix +logger = logging.getLogger(__file__) - def debug_prefix(self): - return "weather(%s)" % (self.file_prefix) - - def periodic_render(self, key): - return self.fetch_weather() - - def describe_time(self, index): - if (index <= 1): - return "overnight" - elif (index <= 3): - return "morning" - elif (index <= 5): - return "afternoon" - else: - return "evening" - - def describe_wind(self, mph): - 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): - if (mm < 2): - return "light" - elif (mm < 10): - return "moderate" - else: - return "heavy" - - def describe_precip(self, rain, snow): - if rain == 0 and snow == 0: - return "no precipitation" - magnitude = rain + snow - if rain > 0 and snow > 0: - return "a %s mix of rain and snow" % self.describe_magnitude(magnitude) - elif rain > 0: - return "%s rain" % self.describe_magnitude(magnitude) - elif snow > 0: - return "%s snow" % self.describe_magnitude(magnitude) +class weather_renderer(renderer.abstaining_renderer): + """A renderer to fetch forecast from wunderground.""" - def fix_caps(self, s): - r = "" - s = s.lower() - for x in s.split("."): - x = x.strip() - r += x.capitalize() + ". " - r = r.replace(". .", ".") - return r + def __init__(self, name_to_timeout_dict: Dict[str, int], file_prefix: str) -> None: + super().__init__(name_to_timeout_dict) + self.file_prefix = file_prefix - def pick_icon(self, conditions, rain, snow): + @staticmethod + def pick_icon(conditions: List[str], rain: List[float], snow: List[float]) -> str: # rain snow clouds sun # fog.gif # hazy.gif @@ -92,135 +44,66 @@ 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 xrange(0, count): - seen_rain = rain[x] > 0; - seen_snow = snow[x] > 0; + for x in range(0, count): + seen_rain = rain[x] > 0 + seen_snow = snow[x] > 0 total_snow += snow[x] txt = conditions[x].lower() - if ("cloud" in txt): + if "cloud" in txt: cloud_count += 1 - if ("clear" in txt or "sun" in txt): + if "clear" in txt or "sun" in txt: clear_count += 1 - if (seen_rain and seen_snow): - if (total_snow < 10): + if seen_rain and seen_snow: + if total_snow < 10: return "sleet.gif" else: return "snow.gif" - if (seen_snow): - if (total_snow < 10): + if seen_snow: + if total_snow < 10: return "flurries.gif" else: return "snow.gif" - if (seen_rain): + if seen_rain: return "rain.gif" - if (cloud_count >= 6): + if cloud_count >= 6: return "mostlycloudy.gif" - elif (cloud_count >= 4): + elif cloud_count >= 4: return "partlycloudy.gif" - if (clear_count >= 7): + if clear_count >= 7: return "sunny.gif" - elif (clear_count >= 6): + elif clear_count >= 6: return "mostlysunny.gif" - elif (clear_count >= 4): + elif clear_count >= 4: return "partlysunny.gif" return "clear.gif" - def describe_weather(self, - high, low, - wind, conditions, rain, snow): - # 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 xrange(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): + 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 = urllib2.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", @@ -239,152 +122,265 @@ class weather_renderer(renderer.debuggable_abstaining_renderer): # "dt_txt":"2017-01-30 18:00:00" # }, # {"dt":1485810000,.... - f = file_writer.file_writer('weather-%s_3_10800.html' % self.file_prefix) - f.write(""" -

Weather at %s:

+ + with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f: + f.write( + f""" +

Upcoming weather at {text_location}:


+""" + ) + f.write( + """ +
- - """ % text_location) - count = parsed_json['cnt'] - - ts = {} - highs = {} - lows = {} - wind = {} - conditions = {} - rain = {} - snow = {} - for x in xrange(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 - - for x in xrange(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] - _ = data['dt'] - if (_ > ts[date]): - ts[date] = _ - temp = data["main"]["temp"] - if (highs[date] < temp): - highs[date] = temp - if (temp < lows[date]): - lows[date] = temp - wind[date].append(data["wind"]["speed"]) - conditions[date].append(data["weather"][0]["main"]) - if "rain" in data and "3h" in data["rain"]: - rain[date].append(data["rain"]["3h"]) - else: - rain[date].append(0) - if "snow" in data and "3h" in data["snow"]: - snow[date].append(data["snow"]["3h"]) - else: - snow[date].append(0) - - # {u'clouds': {u'all': 0}, - # u'sys': {u'pod': u'd'}, - # u'dt_txt': u'2019-10-09 21:00:00', - # u'weather': [ - # {u'main': u'Clear', - # u'id': 800, - # u'icon': u'01d', - # u'description': u'clear sky'} - # ], - # u'dt': 1570654800, - # u'main': { - # u'temp_kf': 0, - # u'temp': 54.74, - # u'grnd_level': 1018.95, - # u'temp_max': 54.74, - # u'sea_level': 1026.46, - # u'humidity': 37, - # u'pressure': 1026.46, - # u'temp_min': 54.74 - # }, - # 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 = {} - for date in sorted(highs.keys()): - today = datetime.fromtimestamp(ts[date]) - formatted_date = today.strftime('%a %e %b') - if (formatted_date in days_seen): - continue; - days_seen[formatted_date] = True - num_days = len(days_seen.keys()) - - days_seen = {} - for date in sorted(highs.keys()): - precip = 0.0 - for _ in rain[date]: - precip += _ - for _ in snow[date]: - precip += _ - - today = datetime.fromtimestamp(ts[date]) - formatted_date = today.strftime('%a %e %b') - if (formatted_date in days_seen): - continue; - days_seen[formatted_date] = True - f.write('\n') - f.write("
\n' % (100 / num_days)) - f.write('\n') - - # Date - f.write(' \n') - - # Icon - f.write(' \n' % - self.pick_icon(conditions[date], rain[date], snow[date])) - - # Low temp - color = "#000099" - if (lows[date] <= 32.5): - color = "#009999" - f.write(' \n' % ( - color, int(lows[date]))) - - # Total precip - precip *= 0.0393701 - if (precip > 0.025): - f.write(' \n' % precip) - else: - f.write(' \n') - - # High temp - color = "#800000" - if (highs[date] >= 80): - color = "#AA0000" - f.write(' \n' % ( - color, int(highs[date]))) - - # Text "description" - f.write('\n' % - self.describe_weather(highs[date], lows[date], wind[date], conditions[date], rain[date], snow[date])) - f.write('
' + formatted_date + '
%d°F  
%3.1f"
   %d°F
%s
\n
") +""" + ) + count = parsed_json["cnt"] + + ts = {} + highs = {} + lows = {} + 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, time) = dt.split(" ") + _ = data["dt"] + if _ not in ts or _ > ts[date]: + ts[date] = _ + temp = data["main"]["temp"] + + # High and low temp + if date not in highs or highs[date] < temp: + highs[date] = temp + 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: + rain[date].append(0) + if "snow" in data and "3h" in data["snow"]: + snow[date].append(data["snow"]["3h"]) + else: + snow[date].append(0) + + # {u'clouds': {u'all': 0}, + # u'sys': {u'pod': u'd'}, + # u'dt_txt': u'2019-10-09 21:00:00', + # u'weather': [ + # {u'main': u'Clear', + # u'id': 800, + # u'icon': u'01d', + # u'description': u'clear sky'} + # ], + # u'dt': 1570654800, + # u'main': { + # u'temp_kf': 0, + # u'temp': 54.74, + # u'grnd_level': 1018.95, + # u'temp_max': 54.74, + # u'sea_level': 1026.46, + # u'humidity': 37, + # u'pressure': 1026.46, + # u'temp_min': 54.74 + # }, + # u'wind': {u'speed': 6.31, u'deg': 10.09}} + + days_seen = set() + for date in sorted(highs.keys()): + day = datetime.fromtimestamp(ts[date]) + formatted_date = day.strftime("%a %e %b") + if formatted_date in days_seen: + continue + 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 + + day = datetime.fromtimestamp(ts[date]) + formatted_date = day.strftime("%a %e %b") + if formatted_date in days_seen: + continue + days_seen.add(formatted_date) + f.write( + f'
\n' + ) + + # Date + f.write( + f""" + + +""" + ) + + # Conditions icon + icon = weather_renderer.pick_icon( + conditions[date], rain[date], snow[date] + ) + f.write( + f""" + + +""" + ) + + # Low temp -- left + color = "#000099" + if lows[date] <= 32.5: + color = "#009999" + f.write( + f""" + + +""" + ) + + # Total aggregate_precip in inches + aggregate_daily_precip /= 2.54 + if aggregate_daily_precip > 0.025: + f.write( + f""" + +""" + ) + else: + f.write(" \n") + + # High temp + precip chart + color = "#800000" + if highs[date] >= 80: + color = "#AA0000" + f.write( + f""" + +""" + ) + + # Precip graph + f.write( + f""" + + + + +
+
+ {formatted_date} +
+
+
+ +
+
+ + {int(lows[date])}°F   + + +
+ + {aggregate_daily_precip:3.1f}” + +
+
  + +   {int(highs[date])}°F + +
+ +
+
+""" + ) + f.write("
") return True -#x = weather_renderer({"Stevens": 1000}, -# "stevens") -#x.periodic_render("Stevens") + +# x = weather_renderer({"Stevens": 1000}, "stevens") +# x.periodic_render("Stevens")