Overrides + debugging modules / functions in logging.
[python_utils.git] / cached / weather_data.py
1 #!/usr/bin/env python3
2
3 from dataclasses import dataclass
4 import datetime
5 import json
6 import logging
7 import os
8 from typing import Any, List
9 import urllib.request
10
11 from overrides import overrides
12
13 import argparse_utils
14 import config
15 import datetime_utils
16 import list_utils
17 import persistent
18
19 logger = logging.getLogger(__name__)
20
21 cfg = config.add_commandline_args(
22     f'Cached Weather Data List ({__file__})',
23     'Arguments controlling cached weather data',
24 )
25 cfg.add_argument(
26     '--weather_data_cachefile',
27     type=str,
28     default=f'{os.environ["HOME"]}/cache/.weather_summary_cache',
29     metavar='FILENAME',
30     help='File in which to cache weather data'
31 )
32 cfg.add_argument(
33     '--weather_data_stalest_acceptable',
34     type=argparse_utils.valid_duration,
35     default=datetime.timedelta(seconds=7200),   # 2 hours
36     metavar='DURATION',
37     help='Maximum acceptable age of cached data.  If zero, forces a refetch'
38 )
39
40
41 @dataclass
42 class WeatherData:
43     date: datetime.date              # The date
44     high: float                      # The predicted high in F
45     low: float                       # The predicted low in F
46     precipitation_inches: float      # Number of inches of precipitation / day
47     conditions: List[str]            # Conditions per ~3h window
48     most_common_condition: str       # The most common condition
49     icon: str                        # An icon to represent it
50
51
52 @persistent.persistent_autoloaded_singleton()
53 class CachedWeatherData(persistent.Persistent):
54     def __init__(self,
55                  weather_data = None):
56         if weather_data is not None:
57             self.weather_data = weather_data
58             return
59         self.weather_data = {}
60         icon_by_condition = {
61             "Thunderstorm": "⚡",
62             "Drizzle": "",
63             "Rain": "☂️",
64             "Snow": "❄️",
65             "Clear": "☀️",
66             "Clouds": "⛅",
67             "Mist": "🌫",
68             "Smoke": "🚬",
69             "Haze": "🌥️",
70             "Dust": "💨",
71             "Fog": "🌁",
72             "Sand": "🏜️",
73             "Ash": "🌋",
74             "Squall": "🌬",
75             "Tornado": "🌪️"
76         }
77         now = datetime.datetime.now()
78         dates = set()
79         highs = {}
80         lows = {}
81         conditions = {}
82         precip = {}
83         param = "id=5786882"   # Bellevue, WA
84         key = "c0b160c49743622f62a9cd3cda0270b3"
85         www = urllib.request.urlopen(
86             f'http://api.openweathermap.org/data/2.5/weather?zip=98005,us&APPID={key}&units=imperial'
87         )
88         response = www.read()
89         www.close()
90         parsed_json = json.loads(response)
91         logger.debug(parsed_json)
92         dt = datetime.datetime.fromtimestamp(parsed_json["dt"]).date()
93         dates.add(dt)
94         condition = parsed_json["weather"][0]["main"]
95         icon = icon_by_condition.get(condition, '?')
96         p = 0.0
97         if 'rain' in parsed_json:
98             if '3h' in parsed_json['rain']:
99                 p += float(parsed_json['rain']['3h'])
100             elif '1h' in parsed_json['rain']:
101                 p += float(parsed_json['rain']['1h'])
102         if 'snow' in parsed_json:
103             if '3h' in parsed_json['snow']:
104                 p += float(parsed_json['snow']['3h'])
105             elif '1h' in parsed_json['snow']:
106                 p += float(parsed_json['snow']['1h'])
107         if dt == now.date() and now.hour > 18 and condition == 'Clear':
108             icon = '🌙'
109         self.weather_data[dt] = WeatherData(
110             date = dt,
111             high = float(parsed_json["main"]["temp_max"]),
112             low = float(parsed_json["main"]["temp_min"]),
113             precipitation_inches = p / 25.4,
114             conditions = [condition],
115             most_common_condition = condition,
116             icon = icon,
117         )
118
119         www = urllib.request.urlopen(
120             f"http://api.openweathermap.org/data/2.5/forecast?{param}&APPID={key}&units=imperial"
121         )
122         response = www.read()
123         www.close()
124         parsed_json = json.loads(response)
125         logger.debug(parsed_json)
126         count = parsed_json["cnt"]
127         for x in range(count):
128             data = parsed_json["list"][x]
129             dt = datetime.datetime.strptime(data['dt_txt'], '%Y-%m-%d %H:%M:%S')
130             dt = dt.date()
131             dates.add(dt)
132             if dt not in highs:
133                 highs[dt] = None
134                 lows[dt] = None
135                 conditions[dt] = []
136             for temp in (
137                     data["main"]["temp"],
138                     data['main']['temp_min'],
139                     data['main']['temp_max'],
140             ):
141                 if highs[dt] is None or temp > highs[dt]:
142                     highs[dt] = temp
143                 if lows[dt] is None or temp < lows[dt]:
144                     lows[dt] = temp
145             cond = data["weather"][0]["main"]
146             precip[dt] = 0.0
147             if 'rain' in parsed_json:
148                 if '3h' in parsed_json['rain']:
149                     precip[dt] += float(parsed_json['rain']['3h'])
150                 elif '1h' in parsed_json['rain']:
151                     precip[dt] += float(parsed_json['rain']['1h'])
152             if 'snow' in parsed_json:
153                 if '3h' in parsed_json['snow']:
154                     precip[dt] += float(parsed_json['snow']['3h'])
155                 elif '1h' in parsed_json['snow']:
156                     precip[dt] += float(parsed_json['snow']['1h'])
157             conditions[dt].append(cond)
158
159         today = datetime_utils.now_pacific().date()
160         for dt in sorted(dates):
161             if dt == today:
162                 high = highs.get(dt, None)
163                 if (
164                         high is not None and
165                         self.weather_data[today].high < high
166                 ):
167                     self.weather_data[today].high = high
168                 continue
169             most_common_condition = list_utils.most_common(conditions[dt])
170             icon = icon_by_condition.get(most_common_condition, '?')
171             if dt == now.date() and now.hour > 18 and condition == 'Clear':
172                 icon = '🌙'
173             self.weather_data[dt] = WeatherData(
174                 date = dt,
175                 high = highs[dt],
176                 low = lows[dt],
177                 precipitation_inches = precip[dt] / 25.4,
178                 conditions = conditions[dt],
179                 most_common_condition = most_common_condition,
180                 icon = icon
181             )
182
183     @classmethod
184     @overrides
185     def load(cls) -> Any:
186         if persistent.was_file_written_within_n_seconds(
187                 config.config['weather_data_cachefile'],
188                 config.config['weather_data_stalest_acceptable'].total_seconds(),
189         ):
190             import pickle
191             with open(config.config['weather_data_cachefile'], 'rb') as rf:
192                 weather_data = pickle.load(rf)
193                 return cls(weather_data)
194         return None
195
196     @overrides
197     def save(self) -> bool:
198         import pickle
199         with open(config.config['weather_data_cachefile'], 'wb') as wf:
200             pickle.dump(
201                 self.weather_data,
202                 wf,
203                 pickle.HIGHEST_PROTOCOL,
204             )
205         return True