diff options
| -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() | ||
