#!/usr/bin/env python3 from dataclasses import dataclass import datetime import logging import os 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=f'{os.environ["HOME"]}/.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 @persistent.persistent_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() said_temp = False 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 and not said_temp: blurb = f'{day.get_text()}: The current outside tempterature is {current_temp}. ' + txt.get_text() said_temp = True 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, )