Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / cached / weather_forecast.py
index ce4725d64a6c43699ab702b85d60d1f070366cf5..b8a20ed8caa04553f01225a5a2f8f57866858ea7 100644 (file)
@@ -1,55 +1,63 @@
 #!/usr/bin/env python3
 
-from dataclasses import dataclass
+# © Copyright 2021-2022, Scott Gasch
+
+"""How's the weather going to be tomorrow?"""
+
 import datetime
 import logging
+import os
 import urllib.request
+from dataclasses import dataclass
+from typing import Any
 
 import astral  # type: ignore
+import pytz
 from astral.sun import sun  # type: ignore
 from bs4 import BeautifulSoup  # type: ignore
-import pytz
+from overrides import overrides
 
 import argparse_utils
 import config
-import datetime_utils
 import dateparse.dateparse_utils as dp
+import datetime_utils
 import persistent
+import smart_home.thermometers as temps
 import text_utils
 
 logger = logging.getLogger(__name__)
 
 cfg = config.add_commandline_args(
     f'Cached Weather Forecast ({__file__})',
-    'Arguments controlling detailed weather rendering'
+    'Arguments controlling detailed weather rendering',
 )
 cfg.add_argument(
     '--weather_forecast_cachefile',
     type=str,
-    default='/home/scott/.weather_forecast_cache',
+    default=f'{os.environ["HOME"]}/cache/.weather_forecast_cache',
     metavar='FILENAME',
-    help='File in which to cache weather data'
+    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
+    default=datetime.timedelta(seconds=7200),  # 2 hours
     metavar='DURATION',
-    help='Maximum acceptable age of cached data.  If zero, forces a refetch'
+    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
+    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):
[email protected]_autoloaded_singleton()  # type: ignore
+class CachedDetailedWeatherForecast(persistent.Persistent):
+    def __init__(self, forecasts=None):
         if forecasts is not None:
             self.forecasts = forecasts
             return
@@ -58,15 +66,9 @@ class CachedDetailedWeatherForecast(object):
         self.forecasts = {}
 
         # Ask the raspberry pi about the outside temperature.
-        www = urllib.request.urlopen(
-            "http://10.0.0.75/~pi/outside_temp"
+        current_temp = temps.ThermometerRegistry().read_temperature(
+            'house_outside', convert_to_fahrenheit=True
         )
-        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(
@@ -79,11 +81,12 @@ class CachedDetailedWeatherForecast(object):
         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')
+            forecast.find_all('b'),
+            forecast.find_all(class_='col-sm-10 forecast-text'),
         ):
             last_dt = dt
             try:
@@ -93,15 +96,15 @@ class CachedDetailedWeatherForecast(object):
             assert dt is not None
 
             # Compute sunrise/sunset times on dt.
-            city = astral.LocationInfo(
-                "Bellevue", "USA", "US/Pacific", 47.653, -122.171
-            )
+            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()
+            if dt.date() == now.date() and not said_temp and current_temp is not None:
+                blurb = f'{day.get_text()}: The current outside tempterature is {current_temp}. '
+                blurb += txt.get_text()
+                said_temp = True
             else:
                 blurb = f'{day.get_text()}: {txt.get_text()}'
             blurb = text_utils.wrap_string(blurb, 80)
@@ -110,29 +113,34 @@ class CachedDetailedWeatherForecast(object):
                 self.forecasts[dt.date()].description += '\n' + blurb
             else:
                 self.forecasts[dt.date()] = WeatherForecast(
-                    date = dt,
-                    sunrise = sunrise,
-                    sunset = sunset,
-                    description = blurb,
+                    date=dt,
+                    sunrise=sunrise,
+                    sunset=sunset,
+                    description=blurb,
                 )
 
     @classmethod
-    def load(cls):
+    @overrides
+    def load(cls) -> Any:
         if persistent.was_file_written_within_n_seconds(
-                config.config['weather_forecast_cachefile'],
-                config.config['weather_forecast_stalest_acceptable'].total_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):
+    @overrides
+    def save(self) -> bool:
         import pickle
+
         with open(config.config['weather_forecast_cachefile'], 'wb') as wf:
             pickle.dump(
                 self.forecasts,
                 wf,
                 pickle.HIGHEST_PROTOCOL,
             )
+        return True