Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / cached / weather_data.py
index 4c464483386b103c4a3618174e22a3e3f0b3e433..29a1d544b042101042eeb9d0a1eb781de094e427 100644 (file)
@@ -1,10 +1,19 @@
 #!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# © Copyright 2021-2022, Scott Gasch
+
+"""How's the weather?"""
 
-from dataclasses import dataclass
 import datetime
 import json
-from typing import List
+import logging
+import os
 import urllib.request
+from dataclasses import dataclass
+from typing import Any, List
+
+from overrides import overrides
 
 import argparse_utils
 import config
@@ -12,6 +21,8 @@ import datetime_utils
 import list_utils
 import persistent
 
+logger = logging.getLogger(__name__)
+
 cfg = config.add_commandline_args(
     f'Cached Weather Data List ({__file__})',
     'Arguments controlling cached weather data',
@@ -19,33 +30,33 @@ cfg = config.add_commandline_args(
 cfg.add_argument(
     '--weather_data_cachefile',
     type=str,
-    default='/home/scott/.weather_summary_cache',
+    default=f'{os.environ["HOME"]}/cache/.weather_summary_cache',
     metavar='FILENAME',
-    help='File in which to cache weather data'
+    help='File in which to cache weather data',
 )
 cfg.add_argument(
     '--weather_data_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 WeatherData:
-    date: datetime.date              # The date
-    high: float                      # The predicted high in F
-    low: float                       # The predicted low in F
-    conditions: List[str]            # Conditions per ~3h window
-    most_common_condition: str       # The most common condition
-    icon: str                        # An icon to represent it
+    date: datetime.date  # The date
+    high: float  # The predicted high in F
+    low: float  # The predicted low in F
+    precipitation_inches: float  # Number of inches of precipitation / day
+    conditions: List[str]  # Conditions per ~3h window
+    most_common_condition: str  # The most common condition
+    icon: str  # An icon to represent it
 
 
[email protected]_autoloaded_singleton()
[email protected]_autoloaded_singleton()  # type: ignore
 class CachedWeatherData(persistent.Persistent):
-    def __init__(self,
-                 weather_data = None):
+    def __init__(self, weather_data=None):
         if weather_data is not None:
             self.weather_data = weather_data
             return
@@ -65,14 +76,15 @@ class CachedWeatherData(persistent.Persistent):
             "Sand": "🏜️",
             "Ash": "🌋",
             "Squall": "🌬",
-            "Tornado": "🌪️"
+            "Tornado": "🌪️",
         }
         now = datetime.datetime.now()
         dates = set()
         highs = {}
         lows = {}
         conditions = {}
-        param = "id=5786882"   # Bellevue, WA
+        precip = {}
+        param = "id=5786882"  # Bellevue, WA
         key = "c0b160c49743622f62a9cd3cda0270b3"
         www = urllib.request.urlopen(
             f'http://api.openweathermap.org/data/2.5/weather?zip=98005,us&APPID={key}&units=imperial'
@@ -80,19 +92,32 @@ class CachedWeatherData(persistent.Persistent):
         response = www.read()
         www.close()
         parsed_json = json.loads(response)
+        logger.debug(parsed_json)
         dt = datetime.datetime.fromtimestamp(parsed_json["dt"]).date()
         dates.add(dt)
         condition = parsed_json["weather"][0]["main"]
         icon = icon_by_condition.get(condition, '?')
+        p = 0.0
+        if 'rain' in parsed_json:
+            if '3h' in parsed_json['rain']:
+                p += float(parsed_json['rain']['3h'])
+            elif '1h' in parsed_json['rain']:
+                p += float(parsed_json['rain']['1h'])
+        if 'snow' in parsed_json:
+            if '3h' in parsed_json['snow']:
+                p += float(parsed_json['snow']['3h'])
+            elif '1h' in parsed_json['snow']:
+                p += float(parsed_json['snow']['1h'])
         if dt == now.date() and now.hour > 18 and condition == 'Clear':
             icon = '🌙'
         self.weather_data[dt] = WeatherData(
-            date = dt,
-            high = float(parsed_json["main"]["temp_max"]),
-            low = float(parsed_json["main"]["temp_min"]),
-            conditions = [condition],
-            most_common_condition = condition,
-            icon = icon,
+            date=dt,
+            high=float(parsed_json["main"]["temp_max"]),
+            low=float(parsed_json["main"]["temp_min"]),
+            precipitation_inches=p / 25.4,
+            conditions=[condition],
+            most_common_condition=condition,
+            icon=icon,
         )
 
         www = urllib.request.urlopen(
@@ -101,6 +126,7 @@ class CachedWeatherData(persistent.Persistent):
         response = www.read()
         www.close()
         parsed_json = json.loads(response)
+        logger.debug(parsed_json)
         count = parsed_json["cnt"]
         for x in range(count):
             data = parsed_json["list"][x]
@@ -111,54 +137,72 @@ class CachedWeatherData(persistent.Persistent):
                 highs[dt] = None
                 lows[dt] = None
                 conditions[dt] = []
-            temp = data["main"]["temp"]
-            if highs[dt] is None or temp > highs[dt]:
-                highs[dt] = temp
-            if lows[dt] is None or temp < lows[dt]:
-                lows[dt] = temp
+            for temp in (
+                data["main"]["temp"],
+                data['main']['temp_min'],
+                data['main']['temp_max'],
+            ):
+                if highs[dt] is None or temp > highs[dt]:
+                    highs[dt] = temp
+                if lows[dt] is None or temp < lows[dt]:
+                    lows[dt] = temp
             cond = data["weather"][0]["main"]
+            precip[dt] = 0.0
+            if 'rain' in parsed_json:
+                if '3h' in parsed_json['rain']:
+                    precip[dt] += float(parsed_json['rain']['3h'])
+                elif '1h' in parsed_json['rain']:
+                    precip[dt] += float(parsed_json['rain']['1h'])
+            if 'snow' in parsed_json:
+                if '3h' in parsed_json['snow']:
+                    precip[dt] += float(parsed_json['snow']['3h'])
+                elif '1h' in parsed_json['snow']:
+                    precip[dt] += float(parsed_json['snow']['1h'])
             conditions[dt].append(cond)
 
         today = datetime_utils.now_pacific().date()
         for dt in sorted(dates):
             if dt == today:
                 high = highs.get(dt, None)
-                if (
-                        high is not None and
-                        self.weather_data[today].high < high
-                ):
+                if high is not None and self.weather_data[today].high < high:
                     self.weather_data[today].high = high
                 continue
-            most_common_condition = list_utils.most_common_item(conditions[dt])
+            most_common_condition = list_utils.most_common(conditions[dt])
             icon = icon_by_condition.get(most_common_condition, '?')
             if dt == now.date() and now.hour > 18 and condition == 'Clear':
                 icon = '🌙'
             self.weather_data[dt] = WeatherData(
-                date = dt,
-                high = highs[dt],
-                low = lows[dt],
-                conditions = conditions[dt],
-                most_common_condition = most_common_condition,
-                icon = icon
+                date=dt,
+                high=highs[dt],
+                low=lows[dt],
+                precipitation_inches=precip[dt] / 25.4,
+                conditions=conditions[dt],
+                most_common_condition=most_common_condition,
+                icon=icon,
             )
 
     @classmethod
-    def load(cls):
+    @overrides
+    def load(cls) -> Any:
         if persistent.was_file_written_within_n_seconds(
-                config.config['weather_data_cachefile'],
-                config.config['weather_data_stalest_acceptable'].total_seconds(),
+            config.config['weather_data_cachefile'],
+            config.config['weather_data_stalest_acceptable'].total_seconds(),
         ):
             import pickle
+
             with open(config.config['weather_data_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_data_cachefile'], 'wb') as wf:
             pickle.dump(
                 self.weather_data,
                 wf,
                 pickle.HIGHEST_PROTOCOL,
             )
+        return True