#!/usr/bin/env python3 from datetime import datetime import json import re from typing import Dict, List import urllib.request, urllib.error, urllib.parse import file_writer import renderer import kiosk_secrets as secrets import random class weather_renderer(renderer.debuggable_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) 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: # rain snow clouds sun # fog.gif # hazy.gif # clear.gif # mostlycloudy.gif F F 6+ X # partlycloudy.gif F F 4+ 4- # cloudy.gif # partlysunny.gif F F X 5+ # mostlysunny.gif F F X 6+ # rain.gif T F X X # sleet.gif T T X X # flurries.gif F T X X (<1") # snow.gif F T X X (else) # sunny.gif F F X 7+ # tstorms.gif seen_rain = False seen_snow = False cloud_count = 0 clear_count = 0 total_snow = 0.0 count = min(len(conditions), len(rain), len(snow)) 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: cloud_count += 1 if "clear" in txt or "sun" in txt: clear_count += 1 if seen_rain and seen_snow: if total_snow < 10: return "sleet.gif" else: return "snow.gif" if seen_snow: if total_snow < 10: return "flurries.gif" else: return "snow.gif" if seen_rain: return "rain.gif" if cloud_count >= 6: return "mostlycloudy.gif" elif cloud_count >= 4: return "partlycloudy.gif" if clear_count >= 7: return "sunny.gif" elif clear_count >= 6: return "mostlysunny.gif" elif clear_count >= 4: 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 fetch_weather(self) -> bool: if self.file_prefix == "stevens": text_location = "Stevens Pass, WA" param = "lat=47.74&lon=-121.08" 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) ) response = www.read() www.close() parsed_json = json.loads(response) # https://openweathermap.org/forecast5 # {"cod":"200", # "message":0.0036, # "cnt":40, # "list":[ # {"dt":1485799200, # "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}, # "weather":[ # {"id":800,"main":"Clear","description":"clear sky","icon":"02n"} # ], # "clouds":{"all":8}, # "wind":{"speed":4.77,"deg":232.505}, # "snow":{}, # "sys":{"pod":"n"}, # "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: f.write( f"""

Weather at {text_location}:


""" ) 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 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] _ = 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(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 += _ 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
") return True # x = weather_renderer({"Stevens": 1000}, "stevens") # x.periodic_render("Stevens")