3 # © Copyright 2021-2022, Scott Gasch
5 """How's the weather going to be tomorrow?"""
11 from dataclasses import dataclass
12 from typing import Any
14 import astral # type: ignore
16 from astral.sun import sun # type: ignore
17 from bs4 import BeautifulSoup # type: ignore
18 from overrides import overrides
22 import dateparse.dateparse_utils as dp
25 import smart_home.thermometers as temps
28 logger = logging.getLogger(__name__)
30 cfg = config.add_commandline_args(
31 f'Cached Weather Forecast ({__file__})',
32 'Arguments controlling detailed weather rendering',
35 '--weather_forecast_cachefile',
37 default=f'{os.environ["HOME"]}/cache/.weather_forecast_cache',
39 help='File in which to cache weather data',
42 '--weather_forecast_stalest_acceptable',
43 type=argparse_utils.valid_duration,
44 default=datetime.timedelta(seconds=7200), # 2 hours
46 help='Maximum acceptable age of cached data. If zero, forces a refetch',
51 class WeatherForecast:
52 date: datetime.date # The date
53 sunrise: datetime.datetime # Sunrise datetime
54 sunset: datetime.datetime # Sunset datetime
55 description: str # Textual description of weather
58 @persistent.persistent_autoloaded_singleton() # type: ignore
59 class CachedDetailedWeatherForecast(persistent.Persistent):
60 def __init__(self, forecasts=None):
61 if forecasts is not None:
62 self.forecasts = forecasts
65 now = datetime_utils.now_pacific()
68 # Ask the raspberry pi about the outside temperature.
69 current_temp = temps.ThermometerRegistry().read_temperature(
70 'house_outside', convert_to_fahrenheit=True
73 # Get a weather forecast for Bellevue.
74 www = urllib.request.urlopen(
75 "https://forecast.weather.gov/MapClick.php?lat=47.652775&lon=-122.170716"
77 forecast_response = www.read()
80 soup = BeautifulSoup(forecast_response, "html.parser")
81 forecast = soup.find(id='detailed-forecast-body')
82 parser = dp.DateParser()
87 for (day, txt) in zip(
88 forecast.find_all('b'),
89 forecast.find_all(class_='col-sm-10 forecast-text'),
93 dt = parser.parse(day.get_text())
98 # Compute sunrise/sunset times on dt.
99 city = astral.LocationInfo("Bellevue", "USA", "US/Pacific", 47.653, -122.171)
100 s = sun(city.observer, date=dt, tzinfo=pytz.timezone("US/Pacific"))
101 sunrise = s['sunrise']
104 if dt.date() == now.date() and not said_temp and current_temp is not None:
105 blurb = f'{day.get_text()}: The current outside tempterature is {current_temp}. '
106 blurb += txt.get_text()
109 blurb = f'{day.get_text()}: {txt.get_text()}'
110 blurb = text_utils.wrap_string(blurb, 80)
112 if dt.date() in self.forecasts:
113 self.forecasts[dt.date()].description += '\n' + blurb
115 self.forecasts[dt.date()] = WeatherForecast(
124 def load(cls) -> Any:
125 if persistent.was_file_written_within_n_seconds(
126 config.config['weather_forecast_cachefile'],
127 config.config['weather_forecast_stalest_acceptable'].total_seconds(),
131 with open(config.config['weather_forecast_cachefile'], 'rb') as rf:
132 weather_data = pickle.load(rf)
133 return cls(weather_data)
137 def save(self) -> bool:
140 with open(config.config['weather_forecast_cachefile'], 'wb') as wf:
144 pickle.HIGHEST_PROTOCOL,