3 from dataclasses import dataclass
10 import astral # type: ignore
11 from astral.sun import sun # type: ignore
12 from bs4 import BeautifulSoup # type: ignore
13 from overrides import overrides
19 import dateparse.dateparse_utils as dp
23 logger = logging.getLogger(__name__)
25 cfg = config.add_commandline_args(
26 f'Cached Weather Forecast ({__file__})',
27 'Arguments controlling detailed weather rendering'
30 '--weather_forecast_cachefile',
32 default=f'{os.environ["HOME"]}/cache/.weather_forecast_cache',
34 help='File in which to cache weather data'
37 '--weather_forecast_stalest_acceptable',
38 type=argparse_utils.valid_duration,
39 default=datetime.timedelta(seconds=7200), # 2 hours
41 help='Maximum acceptable age of cached data. If zero, forces a refetch'
46 class WeatherForecast:
47 date: datetime.date # The date
48 sunrise: datetime.datetime # Sunrise datetime
49 sunset: datetime.datetime # Sunset datetime
50 description: str # Textual description of weather
53 @persistent.persistent_autoloaded_singleton()
54 class CachedDetailedWeatherForecast(persistent.Persistent):
55 def __init__(self, forecasts = None):
56 if forecasts is not None:
57 self.forecasts = forecasts
60 now = datetime_utils.now_pacific()
63 # Ask the raspberry pi about the outside temperature.
66 www = urllib.request.urlopen(
67 "http://10.0.0.75/~pi/outside_temp",
70 current_temp = www.read().decode("utf-8")
71 current_temp = float(current_temp)
74 current_temp = round(current_temp)
76 logger.warning('Timed out reading 10.0.0.75/~pi/outside_temp?!')
82 # Get a weather forecast for Bellevue.
83 www = urllib.request.urlopen(
84 "https://forecast.weather.gov/MapClick.php?lat=47.652775&lon=-122.170716"
86 forecast_response = www.read()
89 soup = BeautifulSoup(forecast_response, "html.parser")
90 forecast = soup.find(id='detailed-forecast-body')
91 parser = dp.DateParser()
96 for (day, txt) in zip(
97 forecast.find_all('b'),
98 forecast.find_all(class_='col-sm-10 forecast-text')
102 dt = parser.parse(day.get_text())
105 assert dt is not None
107 # Compute sunrise/sunset times on dt.
108 city = astral.LocationInfo(
109 "Bellevue", "USA", "US/Pacific", 47.653, -122.171
111 s = sun(city.observer, date=dt, tzinfo=pytz.timezone("US/Pacific"))
112 sunrise = s['sunrise']
115 if dt.date() == now.date() and not said_temp and current_temp is not None:
116 blurb = f'{day.get_text()}: The current outside tempterature is {current_temp}. '
117 blurb += txt.get_text()
120 blurb = f'{day.get_text()}: {txt.get_text()}'
121 blurb = text_utils.wrap_string(blurb, 80)
123 if dt.date() in self.forecasts:
124 self.forecasts[dt.date()].description += '\n' + blurb
126 self.forecasts[dt.date()] = WeatherForecast(
135 def load(cls) -> Any:
136 if persistent.was_file_written_within_n_seconds(
137 config.config['weather_forecast_cachefile'],
138 config.config['weather_forecast_stalest_acceptable'].total_seconds(),
141 with open(config.config['weather_forecast_cachefile'], 'rb') as rf:
142 weather_data = pickle.load(rf)
143 return cls(weather_data)
147 def save(self) -> bool:
149 with open(config.config['weather_forecast_cachefile'], 'wb') as wf:
153 pickle.HIGHEST_PROTOCOL,