diff options
author | Sam Chudnick <sam@chudnick.com> | 2023-06-03 13:25:12 -0400 |
---|---|---|
committer | Sam Chudnick <sam@chudnick.com> | 2023-06-03 13:25:12 -0400 |
commit | 18483e38bbdf92be7f2e33bf5dbb1c8ffc59f018 (patch) | |
tree | b13e9ef6e4d05cd468ca7b59a5102ffeece04bd4 |
initial commit
-rw-r--r-- | library.py | 351 | ||||
-rw-r--r-- | main.py | 53 |
2 files changed, 404 insertions, 0 deletions
diff --git a/library.py b/library.py new file mode 100644 index 0000000..ff5735e --- /dev/null +++ b/library.py | |||
@@ -0,0 +1,351 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | import datetime, requests, json, pytz | ||
3 | from geopy.geocoders import Nominatim, GeoNames | ||
4 | |||
5 | def get_lat_long(location): | ||
6 | # Converts a location into latitude and longitude | ||
7 | geolocator = Nominatim(user_agent="pywttr") | ||
8 | location = geolocator.geocode(location) | ||
9 | return location.latitude, location.longitude | ||
10 | |||
11 | def get_time_zone(latitude, longitude): | ||
12 | #TODO | ||
13 | pass | ||
14 | |||
15 | |||
16 | def get_grid_data(latitude, longitude): | ||
17 | # Returns id,x,y for a given latitude and longitude | ||
18 | grid_data = json.loads(requests.get(f"https://api.weather.gov/points/{latitude},{longitude}").text) | ||
19 | grid_id = grid_data["properties"]["gridId"] | ||
20 | grid_x = grid_data["properties"]["gridX"] | ||
21 | grid_y = grid_data["properties"]["gridY"] | ||
22 | return {"grid_id": grid_id, "grid_x": grid_x, "grid_y":grid_y} | ||
23 | |||
24 | |||
25 | def get_raw_data(grid_id, grid_x, grid_y): | ||
26 | raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}").text) | ||
27 | return raw_data | ||
28 | |||
29 | |||
30 | def get_raw_forecast(grid_id, grid_x, grid_y): | ||
31 | raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}/forecast", user_agent="my-test-app").text) | ||
32 | return raw_data | ||
33 | |||
34 | |||
35 | def get_current_rounded_time(): | ||
36 | tz = pytz.timezone("America/New_York") #temp | ||
37 | cur_time = datetime.datetime.now(tz=tz) | ||
38 | cur_time_rounded = cur_time.replace(second=0, microsecond=0, minute=0, hour=cur_time.hour) + datetime.timedelta(hours=cur_time.minute//30) | ||
39 | return cur_time_rounded | ||
40 | |||
41 | |||
42 | def set_timezone(values): | ||
43 | # Takes a list of weather data values | ||
44 | # and converts all times to proper timezone | ||
45 | |||
46 | ret = [] | ||
47 | tz = pytz.timezone("America/New_York") #temp | ||
48 | for val in values: | ||
49 | val["time"] = val["time"].astimezone(tz) | ||
50 | ret.append(val) | ||
51 | return ret | ||
52 | |||
53 | |||
54 | def make_current(values): | ||
55 | # Takes a list of weather data values | ||
56 | # and removes items from before the current time | ||
57 | # (to the nearest hour) | ||
58 | ret = [] | ||
59 | cur_time_rounded = get_current_rounded_time() | ||
60 | for val in values: | ||
61 | if val["time"] >= cur_time_rounded: | ||
62 | ret.append(val) | ||
63 | return ret | ||
64 | |||
65 | |||
66 | def fill_gaps(values): | ||
67 | # Takes a list of weather data values | ||
68 | # and fills gaps left by duration periods of longer | ||
69 | # than 1 hour | ||
70 | ret = [] | ||
71 | for val in values: | ||
72 | ret.append(val) | ||
73 | duration_hours = int((val["duration"].seconds / 3600) + (val["duration"].days * 24)) | ||
74 | if duration_hours > 1: | ||
75 | for _ in range(0, duration_hours - 1): | ||
76 | copy = val.copy() | ||
77 | copy["time"] = val["time"] + datetime.timedelta(hours=1) | ||
78 | copy["duration"] = datetime.timedelta(hours=1) | ||
79 | ret.append(copy) | ||
80 | return ret | ||
81 | |||
82 | |||
83 | def normalize(values): | ||
84 | values = set_timezone(values) | ||
85 | values = make_current(values) | ||
86 | values = fill_gaps(values) | ||
87 | return values | ||
88 | |||
89 | |||
90 | def celcius_to_fahrenheit(celcius): | ||
91 | fahrenheit = int(celcius * 9/5 + 32) | ||
92 | return fahrenheit | ||
93 | |||
94 | |||
95 | def parse_duration(duration_str): | ||
96 | #Parses time duration string and returns timedelta | ||
97 | |||
98 | duration_str = duration_str[1:] # strip off leading P | ||
99 | period_str, time_str = duration_str.split('T') | ||
100 | if len(period_str) > 0: | ||
101 | days = int(period_str[0]) | ||
102 | else: | ||
103 | days = 0 | ||
104 | hours = int(time_str[0:len(time_str)-1]) | ||
105 | delta = datetime.timedelta(hours=hours, days=days) | ||
106 | return delta | ||
107 | |||
108 | |||
109 | def get_daily_highs(raw_data): | ||
110 | daily_highs_raw = raw_data["properties"]["maxTemperature"]["values"] | ||
111 | |||
112 | daily_highs = [] | ||
113 | |||
114 | for high in daily_highs_raw: | ||
115 | high_celc = high["value"] | ||
116 | high_fahr = celcius_to_fahrenheit(high_celc) | ||
117 | |||
118 | time_str, duration_str = high["validTime"].split('/') | ||
119 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
120 | duration = parse_duration(duration_str) | ||
121 | daily_highs.append({"time":time,"duration":duration,"high_celc":high_celc,"high_fahr":high_fahr}) | ||
122 | |||
123 | return daily_highs | ||
124 | |||
125 | |||
126 | def get_daily_lows(raw_data): | ||
127 | daily_lows_raw = raw_data["properties"]["minTemperature"]["values"] | ||
128 | |||
129 | daily_lows = [] | ||
130 | |||
131 | for low in daily_lows_raw: | ||
132 | low_celc = low["value"] | ||
133 | low_fahr = celcius_to_fahrenheit(low_celc) | ||
134 | time_str, duration_str = low["validTime"].split('/') | ||
135 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
136 | duration = parse_duration(duration_str) | ||
137 | daily_lows.append({"time":time,"duration":duration,"low_celc":low_celc,"low_fahr":low_far}) | ||
138 | |||
139 | return daily_lows | ||
140 | |||
141 | |||
142 | def get_temperature(raw_data): | ||
143 | raw_values = raw_data["properties"]["temperature"]["values"] | ||
144 | ret = [] | ||
145 | |||
146 | for val in raw_values: | ||
147 | val_celc = val["value"] | ||
148 | val_fahr = celcius_to_fahrenheit(val_celc) | ||
149 | time_str, duration_str = val["validTime"].split('/') | ||
150 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
151 | duration = parse_duration(duration_str) | ||
152 | ret.append({"time":time,"duration":duration,"value_celc":val_celc,"value_fahr":val_fahr}) | ||
153 | |||
154 | return normalize(ret) | ||
155 | |||
156 | |||
157 | def get_apparent_temperature(raw_data): | ||
158 | raw_values = raw_data["properties"]["apparentTemperature"]["values"] | ||
159 | ret = [] | ||
160 | |||
161 | |||
162 | for val in raw_values: | ||
163 | val_celc = val["value"] | ||
164 | val_fahr = celcius_to_fahrenheit(val_celc) | ||
165 | time_str, duration_str = val["validTime"].split('/') | ||
166 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
167 | duration = parse_duration(duration_str) | ||
168 | ret.append({"time":time,"duration":duration,"value_celc":val_celc,"value_fahr":val_far}) | ||
169 | |||
170 | return normalize(ret) | ||
171 | |||
172 | |||
173 | def get_humidity(raw_data): | ||
174 | raw_values = raw_data["properties"]["relativeHumidity"]["values"] | ||
175 | ret = [] | ||
176 | |||
177 | for val in raw_values: | ||
178 | time_str, duration_str = val["validTime"].split('/') | ||
179 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
180 | duration = parse_duration(duration_str) | ||
181 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
182 | |||
183 | return normalize(ret) | ||
184 | |||
185 | |||
186 | def get_wind_chill(raw_data): | ||
187 | raw_values = raw_data["properties"]["windChill"]["values"] | ||
188 | ret = [] | ||
189 | |||
190 | for val in raw_values: | ||
191 | val_celc = val["value"] | ||
192 | val_fahr = celcius_to_fahrenheit(val_celc) | ||
193 | time_str, duration_str = val["validTime"].split('/') | ||
194 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
195 | duration = parse_duration(duration_str) | ||
196 | ret.append({"time":time,"duration":duration,"value_celc":val_celc,"value_fahr":val_fahr}) | ||
197 | |||
198 | return normalize(ret) | ||
199 | |||
200 | |||
201 | def get_wind_speed(raw_data): | ||
202 | raw_values = raw_data["properties"]["windSpeed"]["values"] | ||
203 | ret = [] | ||
204 | |||
205 | for val in raw_values: | ||
206 | time_str, duration_str = val["validTime"].split('/') | ||
207 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
208 | duration = parse_duration(duration_str) | ||
209 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
210 | |||
211 | return normalize(ret) | ||
212 | |||
213 | |||
214 | def get_wind_gust(raw_data): | ||
215 | raw_values = raw_data["properties"]["windGust"]["values"] | ||
216 | ret = [] | ||
217 | |||
218 | for val in raw_values: | ||
219 | time_str, duration_str = val["validTime"].split('/') | ||
220 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
221 | duration = parse_duration(duration_str) | ||
222 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
223 | |||
224 | return normalize(ret) | ||
225 | |||
226 | |||
227 | def get_precip_chance(raw_data): | ||
228 | raw_values = raw_data["properties"]["probabilityOfPrecipitation"]["values"] | ||
229 | ret = [] | ||
230 | |||
231 | for val in raw_values: | ||
232 | time_str, duration_str = val["validTime"].split('/') | ||
233 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
234 | duration = parse_duration(duration_str) | ||
235 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
236 | |||
237 | return normalize(ret) | ||
238 | |||
239 | |||
240 | def get_precip_amount(raw_data): | ||
241 | raw_values = raw_data["properties"]["quantitativePrecipitation"]["values"] | ||
242 | ret = [] | ||
243 | |||
244 | for val in raw_values: | ||
245 | time_str, duration_str = val["validTime"].split('/') | ||
246 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
247 | duration = parse_duration(duration_str) | ||
248 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
249 | |||
250 | return normalize(ret) | ||
251 | |||
252 | |||
253 | def get_snowfall_amount(raw_data): | ||
254 | raw_values = raw_data["properties"]["snowfallAmount"]["values"] | ||
255 | ret = [] | ||
256 | |||
257 | for val in raw_values: | ||
258 | time_str, duration_str = val["validTime"].split('/') | ||
259 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
260 | duration = parse_duration(duration_str) | ||
261 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
262 | |||
263 | return normalize(ret) | ||
264 | |||
265 | |||
266 | def get_snow_level(raw_data): | ||
267 | raw_values = raw_data["properties"]["snowLevel"]["values"] | ||
268 | ret = [] | ||
269 | |||
270 | for val in raw_values: | ||
271 | time_str, duration_str = val["validTime"].split('/') | ||
272 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
273 | duration = parse_duration(duration_str) | ||
274 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
275 | |||
276 | return normalize(ret) | ||
277 | |||
278 | |||
279 | def get_visibility(raw_data): | ||
280 | raw_values = raw_data["properties"]["visibility"]["values"] | ||
281 | ret = [] | ||
282 | |||
283 | for val in raw_values: | ||
284 | time_str, duration_str = val["validTime"].split('/') | ||
285 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
286 | duration = parse_duration(duration_str) | ||
287 | ret.append({"time":time,"duration":duration,"value":val["value"]}) | ||
288 | |||
289 | return normalize(ret) | ||
290 | |||
291 | |||
292 | def get_wind_direction(raw_data): | ||
293 | raw_values = raw_data["properties"]["windDirection"]["values"] | ||
294 | ret = [] | ||
295 | |||
296 | for val in raw_values: | ||
297 | time_str, duration_str = val["validTime"].split('/') | ||
298 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
299 | duration = parse_duration(duration_str) | ||
300 | |||
301 | def degrees_to_cardinal(d): | ||
302 | dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] | ||
303 | ix = round(d / (360. / len(dirs))) | ||
304 | return dirs[ix % len(dirs)] | ||
305 | |||
306 | direction_str = degrees_to_cardinal(val["value"]) | ||
307 | |||
308 | ret.append({"time":time,"duration":duration,"value":direction_str}) | ||
309 | |||
310 | return normalize(ret) | ||
311 | |||
312 | |||
313 | def get_dewpoint(raw_data): | ||
314 | raw_values = raw_data["properties"]["dewpoint"]["values"] | ||
315 | ret = [] | ||
316 | |||
317 | for val in raw_values: | ||
318 | val_celc = val["value"] | ||
319 | val_fahr = celcius_to_fahrenheit(val_celc) | ||
320 | time_str, duration_str = val["validTime"].split('/') | ||
321 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
322 | duration = parse_duration(duration_str) | ||
323 | ret.append({"time":time,"duration":duration,"value_celc":val_celc,"value_fahr":val_fahr}) | ||
324 | |||
325 | return normalize(ret) | ||
326 | |||
327 | |||
328 | def get_hourly_data(raw_data, end_time): | ||
329 | temps = get_temperature(raw_data) | ||
330 | precip_chance = get_precip_chance(raw_data) | ||
331 | precip_amount = get_precip_amount(raw_data) | ||
332 | humidity = get_humidity(raw_data) | ||
333 | wind_speed = get_wind_speed(raw_data) | ||
334 | wind_direction = get_wind_direction(raw_data) | ||
335 | |||
336 | ret = [] | ||
337 | i = 0 | ||
338 | while i < len(temps) and temps[i]["time"] < end_time: | ||
339 | val_dict = { "time": temps[i]["time"], | ||
340 | "temp": int(temps[i]["value_fahr"]), | ||
341 | "humidity": int(humidity[i]["value"]), | ||
342 | "precip_chance": int(precip_chance[i]["value"]), | ||
343 | "precip_amount": int(precip_amount[i]["value"]), | ||
344 | "wind_speed": int(wind_speed[i]["value"]), | ||
345 | "wind_direction": wind_direction[i]["value"] } | ||
346 | ret.append(val_dict) | ||
347 | i+=1 | ||
348 | |||
349 | return ret | ||
350 | |||
351 | |||
@@ -0,0 +1,53 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | import json, requests, datetime, argparse, pytz | ||
3 | |||
4 | import library | ||
5 | |||
6 | def parse_args(): | ||
7 | parser = argparse.ArgumentParser() | ||
8 | parser.add_argument('-d', '--days', type=int) | ||
9 | parser.add_argument('-H', '--hours', type=int) | ||
10 | parser.add_argument('-l', '--location', type=str, required=True) | ||
11 | return parser.parse_args() | ||
12 | |||
13 | |||
14 | def hourly_forecast(args, raw_data): | ||
15 | |||
16 | init_time = library.get_current_rounded_time() | ||
17 | if args.hours is not None: | ||
18 | delta = datetime.timedelta(hours=args.hours) | ||
19 | elif args.days is not None: | ||
20 | delta = datetime.timedelta(days=args.days) | ||
21 | else: | ||
22 | delta = datetime.timedelta(hours=(24-init_time.hour)) | ||
23 | end_time = init_time + delta | ||
24 | |||
25 | hourly_data = library.get_hourly_data(raw_data, end_time) | ||
26 | |||
27 | for point in hourly_data: | ||
28 | print(point["time"].strftime("%a %x %I:%M %p")) | ||
29 | print(f'\t Temperature - { point["temp"] }°F') | ||
30 | print(f'\t Humidity - { point["humidity"] }%') | ||
31 | print(f'\t Chance of Precipitation - { point["precip_chance"] }%') | ||
32 | print(f'\t Precipitation Amount - { point["precip_amount"] } in') | ||
33 | print(f'\t Wind Speed - { point["wind_speed"] } MPH') | ||
34 | print(f'\t Wind Direction - { point["wind_direction"] }') | ||
35 | print('\n') | ||
36 | |||
37 | |||
38 | |||
39 | def daily_forecast(raw_data): | ||
40 | pass | ||
41 | |||
42 | |||
43 | def main(): | ||
44 | args = parse_args() | ||
45 | latitude, longitude = library.get_lat_long(args.location) | ||
46 | grid_data = library.get_grid_data(latitude, longitude) | ||
47 | raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) | ||
48 | hourly_forecast(args, raw_data) | ||
49 | |||
50 | |||
51 | |||
52 | if __name__ == '__main__': | ||
53 | main() | ||