Change locking boundaries for shared dict. Add a unit test.
[python_utils.git] / cached / weather_data.py
index 4c464483386b103c4a3618174e22a3e3f0b3e433..45b6e6efc5d9d3c4cbd07f0996633d835aa4a98a 100644 (file)
@@ -3,15 +3,21 @@
 from dataclasses import dataclass
 import datetime
 import json
-from typing import List
+import logging
+import os
+from typing import Any, List
 import urllib.request
 
+from overrides import overrides
+
 import argparse_utils
 import config
 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,7 +25,7 @@ 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'
 )
@@ -37,6 +43,7 @@ class WeatherData:
     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
@@ -72,6 +79,7 @@ class CachedWeatherData(persistent.Persistent):
         highs = {}
         lows = {}
         conditions = {}
+        precip = {}
         param = "id=5786882"   # Bellevue, WA
         key = "c0b160c49743622f62a9cd3cda0270b3"
         www = urllib.request.urlopen(
@@ -80,16 +88,29 @@ 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"]),
+            precipitation_inches = p / 25.4,
             conditions = [condition],
             most_common_condition = condition,
             icon = icon,
@@ -101,6 +122,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,12 +133,27 @@ 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()
@@ -129,7 +166,7 @@ class CachedWeatherData(persistent.Persistent):
                 ):
                     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 = '🌙'
@@ -137,13 +174,15 @@ class CachedWeatherData(persistent.Persistent):
                 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(),
@@ -154,7 +193,8 @@ class CachedWeatherData(persistent.Persistent):
                 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(
@@ -162,3 +202,4 @@ class CachedWeatherData(persistent.Persistent):
                 wf,
                 pickle.HIGHEST_PROTOCOL,
             )
+        return True