diff options
Diffstat (limited to 'cached/weather_forecast.py')
| -rw-r--r-- | cached/weather_forecast.py | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/cached/weather_forecast.py b/cached/weather_forecast.py new file mode 100644 index 0000000..ce4725d --- /dev/null +++ b/cached/weather_forecast.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass +import datetime +import logging +import urllib.request + +import astral # type: ignore +from astral.sun import sun # type: ignore +from bs4 import BeautifulSoup # type: ignore +import pytz + +import argparse_utils +import config +import datetime_utils +import dateparse.dateparse_utils as dp +import persistent +import text_utils + +logger = logging.getLogger(__name__) + +cfg = config.add_commandline_args( + f'Cached Weather Forecast ({__file__})', + 'Arguments controlling detailed weather rendering' +) +cfg.add_argument( + '--weather_forecast_cachefile', + type=str, + default='/home/scott/.weather_forecast_cache', + metavar='FILENAME', + help='File in which to cache weather data' +) +cfg.add_argument( + '--weather_forecast_stalest_acceptable', + type=argparse_utils.valid_duration, + default=datetime.timedelta(seconds=7200), # 2 hours + metavar='DURATION', + help='Maximum acceptable age of cached data. If zero, forces a refetch' +) + + +@dataclass +class WeatherForecast: + date: datetime.date # The date + sunrise: datetime.datetime # Sunrise datetime + sunset: datetime.datetime # Sunset datetime + description: str # Textual description of weather + + [email protected]_autoloaded_singleton() +class CachedDetailedWeatherForecast(object): + def __init__(self, forecasts = None): + if forecasts is not None: + self.forecasts = forecasts + return + + now = datetime_utils.now_pacific() + self.forecasts = {} + + # Ask the raspberry pi about the outside temperature. + www = urllib.request.urlopen( + "http://10.0.0.75/~pi/outside_temp" + ) + current_temp = www.read().decode("utf-8") + current_temp = float(current_temp) + current_temp *= (9/5) + current_temp += 32.0 + current_temp = round(current_temp) + www.close() + + # Get a weather forecast for Bellevue. + www = urllib.request.urlopen( + "https://forecast.weather.gov/MapClick.php?lat=47.652775&lon=-122.170716" + ) + forecast_response = www.read() + www.close() + + soup = BeautifulSoup(forecast_response, "html.parser") + forecast = soup.find(id='detailed-forecast-body') + parser = dp.DateParser() + + last_dt = now + dt = now + for (day, txt) in zip( + forecast.find_all('b'), + forecast.find_all(class_='col-sm-10 forecast-text') + ): + last_dt = dt + try: + dt = parser.parse(day.get_text()) + except Exception: + dt = last_dt + assert dt is not None + + # Compute sunrise/sunset times on dt. + city = astral.LocationInfo( + "Bellevue", "USA", "US/Pacific", 47.653, -122.171 + ) + s = sun(city.observer, date=dt, tzinfo=pytz.timezone("US/Pacific")) + sunrise = s['sunrise'] + sunset = s['sunset'] + + if dt.date == now.date: + blurb = f'{day.get_text()}: The current outside tempterature is {current_temp}. ' + txt.get_text() + else: + blurb = f'{day.get_text()}: {txt.get_text()}' + blurb = text_utils.wrap_string(blurb, 80) + + if dt.date() in self.forecasts: + self.forecasts[dt.date()].description += '\n' + blurb + else: + self.forecasts[dt.date()] = WeatherForecast( + date = dt, + sunrise = sunrise, + sunset = sunset, + description = blurb, + ) + + @classmethod + def load(cls): + if persistent.was_file_written_within_n_seconds( + config.config['weather_forecast_cachefile'], + config.config['weather_forecast_stalest_acceptable'].total_seconds(), + ): + import pickle + with open(config.config['weather_forecast_cachefile'], 'rb') as rf: + weather_data = pickle.load(rf) + return cls(weather_data) + return None + + def save(self): + import pickle + with open(config.config['weather_forecast_cachefile'], 'wb') as wf: + pickle.dump( + self.forecasts, + wf, + pickle.HIGHEST_PROTOCOL, + ) |
