Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / cached / weather_forecast.py
index 2509f4343b237cf331098a8a65003e1043144b95..b8a20ed8caa04553f01225a5a2f8f57866858ea7 100644 (file)
@@ -1,56 +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=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
@@ -59,23 +66,9 @@ class CachedDetailedWeatherForecast(object):
         self.forecasts = {}
 
         # Ask the raspberry pi about the outside temperature.
-        www = None
-        try:
-            www = urllib.request.urlopen(
-                "http://10.0.0.75/~pi/outside_temp",
-                timeout=2,
-            )
-            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)
-        except Exception:
-            logger.warning('Timed out reading 10.0.0.75/~pi/outside_temp?!')
-            current_temp = None
-        finally:
-            if www is not None:
-                www.close()
+        current_temp = temps.ThermometerRegistry().read_temperature(
+            'house_outside', convert_to_fahrenheit=True
+        )
 
         # Get a weather forecast for Bellevue.
         www = urllib.request.urlopen(
@@ -92,8 +85,8 @@ class CachedDetailedWeatherForecast(object):
         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:
@@ -103,9 +96,7 @@ 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']
@@ -122,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