summaryrefslogtreecommitdiff
path: root/cached/weather_forecast.py
diff options
context:
space:
mode:
Diffstat (limited to 'cached/weather_forecast.py')
-rw-r--r--cached/weather_forecast.py138
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,
+ )