#!/usr/bin/env python3 from argparse import ArgumentParser import requests # doco is at https://www.weather.gov/documentation/services-web-api # openAPI spec is at https://api.weather.gov/openapi.json base_url = 'https://api.weather.gov' headers = { 'user-agent': 'meteor.py/0.1 (us@starfall.systems)', 'accept': 'application/ld+json' } # SGR decorations / ANSI escape colors class term: RESET = '\033[0m' BOLD = '\033[1m' def handle_args(): parser = ArgumentParser() parser.add_argument('lat', help='latitude') parser.add_argument('lon', help='longitude') return parser.parse_args() def print_alerts(county, zone): # in some areas, alerts are not published for the zone but only for the county # you can request both like this rather than making two requests alerts = requests.get(base_url + f'/alerts/active?zone={county},{zone}', headers=headers).json() # this title was cooler when we requested only one zone # e.g. it would end with "for Holmes County (MSC051) MS" # but now it's just "current watches, warnings, and advisories" print(f'{term.BOLD}{alerts.get("title")}') print(f'===================={term.RESET}') for alert in alerts.get('@graph'): print(f'{alert.get("headline")}') print(f' {alert.get("severity")} - {alert.get("certainty")} - {alert.get("urgency")}') print(f' Until: {alert.get("expires")}') print(f'{alert.get("description")}') # TODO description can be quite long # short desc appears to be available as parameters.NWSheadline[0] # and follow up with instruction when it exists print('--------------------') # many more cool fields available. # verious datetimes: sent, effective, onset, expires, ends # severity: Minor, Moderate, Severe, Extreme, Unknown # certainty: Possible, Likely, Observed, Unknown # urgency: Future, Expected, Immediate, Unknown # messageType: Alert, Update # can construct the headline ourseves as f'{event} issued {sent} [until {end}] by {senderName}' print() def print_forecast(forecast_url): # TODO pick up gridId AFC (AER/ALU), AFG, AJK, BOX< CAE, DLH, FSD, HGX, HNX, LIX, LWX, MAF, MFR, MLB, MRX, MTR, PIH and reduce gridX and gridY by 1 - known issue forecast = requests.get(forecast_url, headers=headers).json() # TODO retry on 500 - known issue # TODO query param units=si # TODO cache for like four hours per location, forecast doesn't change that frequently # TODO replace the whole thing with /gridpoints/{point.gridId}/{gridX},{gridY} and create our own forecast # there is lots more information there that would be useful # probabilityOfPrecipitation is the neatest one that's not in .../forecast or .../forecast/hourly # but there's also relativeHumidity, apparentTemperature (and, separately, windChill), skyCover, windGust # downside, it appears to always convert the F/mph measurements into C/kph for no reason. e.g. all temps are measured in 9ths of C print(f'{term.BOLD}forecast') print(f'===================={term.RESET}') for period in forecast.get('periods'): # TODO parse isDaytime true/false to squish a day together print(f'{term.BOLD}{period.get("name")}{term.RESET}:') print(f' {period.get("temperature")}°{period.get("temperatureUnit")}, wind {period.get("windSpeed")} {period.get("windDirection")}') print(f' {period.get("detailedForecast")}') def main(): args = handle_args() point = requests.get(base_url + f'/points/{args.lat},{args.lon}', headers=headers).json() county = point.get('county').split('/')[-1] zone = point.get('forecastZone').split('/')[-1] print_alerts(county, zone) # TODO pick out location name from somewhere. # city and state hide in point.relativeLocation.properties.city (and state) # getting the county or zone's `name (id) state` is good too # TODO can hazardous weather outlook be gotten programatically? forecast_url = point.get('forecast') print_forecast(forecast_url) # hourly_forecast_url = point.get('forecastHourly') if __name__ == '__main__': import sys sys.exit(main())