At 12:30 AM each night, Wanda downloads the weather forecasts from OpenWeather and NOAA, plus the NWS Air Quality Indicies, and uses them to find the best few times today to go for a walk. The “best” times are selected based on temperature, humidity, rain/snowfall, heat index, AQIs, and sunrise / sunset. Each 3-hour block of time gets a score, or is disqualified (for example, if it will be raining or too hot during that time block). Wanda generally favors earlier in the morning, all other things being equal. The top 3 times are then emailed via ‘mutt’ to me. This is a really handy service, and one I use daily.
Note that the scripts report "feels like" temperatures, but use maximum and minimum forecast temperatures when determining the best times. NOAA max temperatures override OpenForecast maxes (the NOAA temperatures tend to be more accurate). NOAA max temperatures are the maximum of the simple max temperature, heat index, and global wet bulb temperature, whichever is hottest.
Here is an example email. It was originally formatted to be easy for Wanda to read out loud. But it's a lot of text to read, and an email is easier to deal with, so it's a speech-formatted email.
Sunrise is at 5:21 A M
Sunset is at 8:43 P M
The best time to go for a walk will be: between 7 o clock A M and 10 o clock A M.
The weather will be overcast clouds
with temperatures around 69 degrees
humidity around 88 percent
precipitation chance around 0 percent
and winds gusting to 6 miles per hour.
Air quality index from ozone will be 24 or Good.
A Q I from fine airborne particles will be 21 or Good.
Another good time to go for a walk will be: between 4 o clock P M and 7 o clock P M.
The weather will be clear sky
with temperatures around 75 degrees
humidity around 67 percent
precipitation chance around 0 percent
and winds gusting to 16 miles per hour.
Air quality index from ozone will be 31 or Good.
A Q I from fine airborne particles will be 12 or Good.
A third good time to go for a walk will be: between 5 o clock A M and 7 o clock A M.
The weather will be overcast clouds
with temperatures around 71 degrees
humidity around 85 percent
precipitation chance around 0 percent
and winds gusting to 3 miles per hour.
Air quality index from ozone will be 24 or Good.
A Q I from fine airborne particles will be 25 or Good.
Before diving into the code, please see About This Code.
Note that you will need the following:
Here is the crontab line that runs the when-walkies.sh shell script:
30 0 * * * cd /home/pipipi/billy-bin; /home/pipipi/billy-bin/when-walkies.sh >/home/pipipi/logs/when-walkies.log 2>&1
Here is the when-walkies.sh shell script that runs the Python scripts that do the real work.
# This script will figure out the best times to go for a walk today and
# will email the times to <your email address>.
#
# It requires the following, installed and hand-tested to both send and
# receive email, to work:
# - walkies-wanda.py, getaqis.py, getnws.py
# - A full email account (not forward or alias) on an IMAP mail server,
# such as Tiger Technologies:
# "wanda@youremailserver.net"
# - mutt installed on this computer via 'apt install mutt'. No Mail Transport
# Agent is needed, 'mutt' can do the IMAP and SMTP connections itself.
# - ~/.muttrc configured as follows:
#
# set imap_user = "wanda@youremailserver.net"
# set imap_pass = "your-email-password"
#
# set smtp_url = "smtps://wanda@youremailserver.net@mail.youremailserver.net:465/"
# set smtp_pass = "your-email-password"
# set ssl_starttls = yes
#
# set from = "wanda@youremailserver.net"
# set realname = "Wanda Walleye"
#
# set folder = "imaps://mail.youremailserver.net:993"
# set spoolfile = "+INBOX"
# set postponed="+Drafts"
#
# set header_cache=~/.mutt/cache/headers
# set message_cachedir=~/.mutt/cache/bodies
# set certificate_file=~/.mutt/certificates
#
# set move = no
cd /home/pipipi/billy-bin
/usr/bin/python3 /home/pipipi/billy-bin/walkies-wanda.py 2>/home/pipipi/logs/walkies-wanda.log >/home/pipipi/temp/best-walkies.txt
/usr/bin/mutt -s 'Best times to go for a walk' -- you@jyouremailserver.net < /home/pipipi/temp/best-walkies.txt
Here is the top-level Python script, walkies-wanda.py. Note that I am no Pythonista, and that there are probably better ways to do many of the things in this script. This script originally just got its info from OpenWeather, but later on folded in the NOAA / NWS forecasts, which often proved to be more accurate and provided more details. The script kept the 3-hour forecast windows from OpenWeather because they seemed like good "time chunks" for choosing best walking times, not too specific, not too broad.
#!/usr/bin/env python3
import json
import sys
import os
import random
import argparse
import urllib.request
import time
# "getaquis.py" should be in the same directory as this file.
from getaqis import *
# "getnws.py" should be in the same directory as this file.
from getnws import *
"""
walkies-wanda (A hack of walkies-mac tweaked to run more better on an Intel
NUC running Ubuntu Linux. )
Figure out when to go walkies, get in steps. Send an email with title:
"Best times to go for a walk today"
Dump the details of up to 3 good walk times into the body of the text.
Run at 12:30AM vi cron job. As inspired by 'ansiweather' shell script(!),
make an API call to OpenWeatherMap.org using urllib. Use json library to
parse the big ol' response. This call is free using my API key.
https://github.com/fcambus/ansiweather/blob/master/ansiweather
https://openweathermap.org/forecast5
See KeePass for example calls and my API key.
Enhance the decision process for picking the best walk times by using
more and better data from NOAA / NWS. Get Air Quality Index data
hour-by-hour from NWS (not JSON; needs parsing), for example:
https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=ozone01&latitude=43.5198&longitude=-90.4051®ion=CONUS&rundatetime=2024051712
See getaqis.py for more details on how this is done.
NWS has a rather exhaustive forecast JSON API for improved hour-by-hour
weather forecasts with every detail you could want (except sunrise
and sunset times, which are surely in there somewhere).
https://www.weather.gov/documentation/services-web-api#/default/alerts_query
The NWS API calls do not (yet) require an access key, but do require mapping
ZIP codes or city names to Lat/Lon coords, and then doing a double-call,
first to get the appropriate forecast link for the coords, and then
to get the forecast itself.
OpenWeather API will return a 3-hour x 5-day forecast including predicted
temperatures, humidity, percent chance of precipitation, possibly
rain / snow in mm, wind speed / gust, plus times of sunrise and sunset.
Everything needed to figure out best walkies time, in 3-hour resolution.
If run at 12:30AM, should get 40 chunks of 3 hours each: 1AM to 4AM, 4AM to
7AM, 7AM to 10AM, 10AM to 1PM, 1PM to 4PM, 4PM to 7PM, 7PM to 10PM, ...
Sunrise will probably fall sometime in the second 4AM to 7AM chunk, and
sunset in the 4PM to 7PM or 7PM to 10PM chunks. All the chunks after that
are either after dark or tomorrow and onwards, we don't need them. The
first chunk is useful for seeing if the ground will be wet/icy at sunrise.
Factors to include:
- Sunrise, sunset. Don't want to walk in the dark. Find the 3-hour
chunks that include them, keep those chunks and the ones in between,
toss the rest (tomorrow's chunks, etc.)
- How hot, will it still be hot after 5? Like, are evening walks doable today?
All other things being equal, pick the coolest chunk. Which is probably
the earliest one.
- Avoid busy sidewalk traffic times, 7:15 - 7:45AM, 3:15 - 3:45PM. Since this
needs higher resolution than 3-hour chunks, maybe just find these times
within their chunks and report only on the 1+ hour chunklets on either side
of them.
- Raining? Snowing? If the % of precipitation for a chunk is over say 50%,
toss it. Also if either of the preceding 2 chunks is over 50%. This should
also avoid early morning walks when it rained overnight. Don't want to
saturate my shoes and socks.
- Icy sidewalks? If % of precipitation anytime earlier in day is over 50%
AND temp is < 40F, might be icy. Recommend indoor walkies / bike today.
- Summer gonna be hot. Note if "best" time is still over 80F, recommend
big water chug beforehand, plenty of water ready after, think about
taking water if possible. Apply sunscreen, wear performance clothes and
hat with full brim. Go slow and easy.
April - October: Recommend apply DEET, tick death spray, wear
hat, long pants, long sleeves.
December - February: Recommend use caution, err towards inside walks
rather than risk slips. Check outside, check sidewalk with toes.
Apply all above rules, write out a file, let Mac sync it up and email it
to me if it is fresh.
"""
openWeatherURL = 'https://api.openweathermap.org/data/2.5/forecast?q=<YOUR-ZIPCODE-GOES-HERE>,US&&units=imperial&APPID=<YOUR-OPENWEATHER-API-CODE-GOES-HERE>'
def assignWalkiesScore(feelsTemp, humidity, precipitationChance, windGust, ozoneAQI, pm25AQI, startHour, endHour):
"""
Calculate and assign a "good time to go for a walk" score for a period of
time. Use the "feels like" temperature, humidity, chance of preciptation,
max predicted wind gust speed, ozone AQI, and PM2.5 AQI to generate the
score. Temperatures and AQIs are weighted more than the other values.
These scores are assigned only for time periods that have not already
been rejected due to, for example, temperatures being too hot.
"""
if feelsTemp <= 70 and feelsTemp >= 40:
tempScore = 4
elif feelsTemp <= 85 and feelsTemp >= 30:
tempScore = 2
else:
tempScore = 0
if precipitationChance < 20:
precipitationScore = 1
else:
precipitationScore = 0
if humidity <= 50:
humidityScore = 3
elif humidity < 70:
humidityScore = 2
elif humidity < 80:
humidityScore = 0
elif humidity < 90:
humidityScore = -1
else:
humidityScore = -2
if windGust <= 10:
windScore = 2
elif windGust <= 20:
windScore = 1
else:
windScore = 0
if ozoneAQI <= 50:
ozoneScore = 2
else:
ozoneScore = 0
if pm25AQI <= 50:
pm25Score = 2
else:
pm25Score = 0
if startHour >= 16 or endHour <= 11:
hourScore = 1
else:
hourScore = 0
score = (tempScore * 2) + (precipitationScore * 2) + humidityScore + \
windScore + (ozoneScore * 2) + (pm25Score * 2) + hourScore
return score
def walkiesScore(chunk):
return chunk["score"]
def printChunkTimes(chunk):
chunkStartTimeEpoch = chunk["dt"]
chunkStartLocalTime = time.localtime(chunkStartTimeEpoch)
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
if chunkStartHour < sunriseHour:
chunkStartHour = sunriseHour
if chunkStartHour > 12:
chunkStartHour = chunkStartHour - 12
chunkStartAMPM = " P M"
else:
chunkStartAMPM = " A M"
# chunkMinute = chunkStartLocalTime.tm_min
chunkMinute = "o clock"
if chunkEndHour > sunsetHour:
chunkEndHour = sunsetHour
if chunkEndHour > 12:
chunkEndHour = chunkEndHour - 12
chunkEndAMPM = " P M"
else:
chunkEndAMPM = " A M"
print(" between " + str(chunkStartHour) + " " + str(chunkMinute) +
chunkStartAMPM, end='')
print(" and " + str(chunkEndHour) + " " + str(chunkMinute) +
chunkEndAMPM + ".")
def printChunk(chunk):
"""
Print out the weather and AQI details for a period of time in a way
that is friendly to being spoken aloud and to being sent via email.
In other words, briefly and simply.
"""
# print(chunk)
printChunkTimes(chunk)
print(" The weather will be " + str(chunk["weather"][0]["description"]))
print(" with temperatures around " + str(round(chunk["main"]["feels_like"]))
+ " degrees")
print(" humidity around " + str(round(chunk["main"]["humidity"]))
+ " percent")
print(" precipitation chance around " + str(chunk["pop"] * 100) + " percent")
print(" and winds gusting to " + str(round(chunk["wind"]["gust"])) +
" miles per hour.")
print(" Air quality index from ozone will be " +
str(chunk["ozone_aqi"]) + " or " +
str(chunk["ozone_category"]) + ".")
print(" A Q I from fine airborne particles will be " +
str(chunk["pm25_aqi"]) + " or " +
str(chunk["pm25_category"]) + ".")
print(" ")
def printChunkRejectionReason(chunk):
# Before sunrise, after sunset; there are a ton of these, so just skip 'em.
if chunk["rejection_code"] <= 2:
return
printChunkTimes(chunk)
print(" will likely have " + chunk["rejection_text"])
def getChunkOzoneAQI(aqis, chunkStartLocalTime):
"""
Find the maximum raw ozone value for a time chunk, and return
it along with the corresponding ozone AQI and text category.
Return -1 / -1 / "Unknown" if AQIs aren't available or if there
aren't any that fall within the time period of this chunk.
"""
ozoneValue = -1
aqiValue = -1
aqiCategory = "Unknown"
if aqis is None:
ozone = {"ozone": ozoneValue, "aqi": aqiValue, "category": aqiCategory}
return ozone
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for ozoneAqi in aqis["ozone"]:
if (ozoneAqi["year"] == chunkYear) and \
(ozoneAqi["month"] == chunkMonth) and \
(ozoneAqi["day"] == chunkDay) and \
(ozoneAqi["hour"] >= chunkStartHour) and \
(ozoneAqi["hour"] <= chunkEndHour) and \
(ozoneAqi["ozone"] > ozoneValue):
ozoneValue = ozoneAqi["ozone"]
aqiValue = ozoneAqi["aqi_ozone"]
aqiCategory = ozoneAqi["category_ozone"]
ozone = {"ozone": ozoneValue, "aqi": aqiValue, "category": aqiCategory}
return ozone
def getChunkPM25AQI(aqis, chunkStartLocalTime):
"""
Find the maximum raw PM2.5 value for a time chunk, and return
it along with the corresponding ozone AQI and text category.
Return -1 / -1 / "Unknown" if AQIs aren't available or if there
aren't any that fall within the time period of this chunk.
"""
pm25Value = -1
aqiValue = -1
aqiCategory = "Unknown"
if aqis is None:
pm25 = {"pm25": pm25Value, "aqi": aqiValue, "category": aqiCategory}
return pm25
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for pm25Aqi in aqis["pm25"]:
if (pm25Aqi["year"] == chunkYear) and \
(pm25Aqi["month"] == chunkMonth) and \
(pm25Aqi["day"] == chunkDay) and \
(pm25Aqi["hour"] >= chunkStartHour) and \
(pm25Aqi["hour"] <= chunkEndHour) and \
(pm25Aqi["pm25"] > pm25Value):
pm25Value = pm25Aqi["pm25"]
aqiValue = pm25Aqi["aqi_pm25"]
aqiCategory = pm25Aqi["category_pm25"]
pm25 = {"pm25": pm25Value, "aqi": aqiValue, "category": aqiCategory}
return pm25
def getChunkNWSMinTemp(nws, chunkStartLocalTime):
"""
Find the minimum NWS minimum-temperature value for a time chunk, and
return it.
Return 999 if NWS min temps aren't available or if there
aren't any that fall within the time period of this chunk.
"""
mint = 999
if nws is None:
return mint
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsMinT in nws["minimumTemperature"]:
if (nwsMinT["year"] == chunkYear) and \
(nwsMinT["month"] == chunkMonth) and \
(nwsMinT["day"] == chunkDay) and \
(nwsMinT["hour"] >= chunkStartHour) and \
(nwsMinT["hour"] <= chunkEndHour) and \
(nwsMinT["mintF"] < mint):
mint = nwsMinT["mintF"]
return mint
def getChunkNWSMaxTemp(nws, chunkStartLocalTime):
"""
Find the maximum NWS maximum-temperature value for a time chunk, and
return it.
Return -999 if NWS max temps aren't available or if there
aren't any that fall within the time period of this chunk.
"""
maxt = -999
if nws is None:
return maxt
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsMaxT in nws["maximumTemperature"]:
if (nwsMaxT["year"] == chunkYear) and \
(nwsMaxT["month"] == chunkMonth) and \
(nwsMaxT["day"] == chunkDay) and \
(nwsMaxT["hour"] >= chunkStartHour) and \
(nwsMaxT["hour"] <= chunkEndHour) and \
(nwsMaxT["maxtF"] > maxt):
maxt = nwsMaxT["maxtF"]
return maxt
def getChunkNWSAppTemp(nws, chunkStartLocalTime):
"""
Find the maximum NWS apparent-temperature value for a time chunk, and
return it.
Return -999 if NWW apparent temps aren't available or if there
aren't any that fall within the time period of this chunk.
"""
appt = -999
if nws is None:
return appt
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsAppT in nws["apparentTemperature"]:
if (nwsAppT["year"] == chunkYear) and \
(nwsAppT["month"] == chunkMonth) and \
(nwsAppT["day"] == chunkDay) and \
(nwsAppT["hour"] >= chunkStartHour) and \
(nwsAppT["hour"] <= chunkEndHour) and \
(nwsAppT["apptF"] > appt):
appt = nwsAppT["apptF"]
return appt
def getChunkNWSHeatIndex(nws, chunkStartLocalTime):
"""
Find the maximum NWS heat index temperature value for a time chunk, and
return it.
Return -999 if NWW apparent temps aren't available or if there
aren't any that fall within the time period of this chunk.
"""
hi = -999
if nws is None:
return hi
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsHI in nws["heatIndex"]:
if (nwsHI["year"] == chunkYear) and \
(nwsHI["month"] == chunkMonth) and \
(nwsHI["day"] == chunkDay) and \
(nwsHI["hour"] >= chunkStartHour) and \
(nwsHI["hour"] <= chunkEndHour) and \
(nwsHI["heatIndexF"] > hi):
hi = nwsHI["heatIndexF"]
return hi
def getChunkNWSWetBulbGlobeTemp(nws, chunkStartLocalTime):
"""
Find the maximum NWS wet bulb globe temperature value for a time chunk, and
return it.
Return -999 if NWS wet bulb globe temps aren't available or if there
aren't any that fall within the time period of this chunk.
"""
wet = -999
if nws is None:
return wet
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsWet in nws["wetBulbGlobeTemperature"]:
if (nwsWet["year"] == chunkYear) and \
(nwsWet["month"] == chunkMonth) and \
(nwsWet["day"] == chunkDay) and \
(nwsWet["hour"] >= chunkStartHour) and \
(nwsWet["hour"] <= chunkEndHour) and \
(nwsWet["wgbtF"] > wet):
wet = nwsWet["wgbtF"]
return wet
def getChunkNWSProbabilityOfPrecipitation(nws, chunkStartLocalTime):
"""
Find the maximum NWS probability of precipitation value for a time chunk, and
return it.
Return 0 if NWS POPs aren't available or if there
aren't any that fall within the time period of this chunk.
"""
pop = 0
if nws is None:
return pop
chunkYear = chunkStartLocalTime.tm_year
chunkMonth = chunkStartLocalTime.tm_mon
chunkDay = chunkStartLocalTime.tm_mday
chunkStartHour = chunkStartLocalTime.tm_hour
chunkEndHour = chunkStartHour + 3
for nwsPop in nws["probabilityOfPrecipitation"]:
if (nwsPop["year"] == chunkYear) and \
(nwsPop["month"] == chunkMonth) and \
(nwsPop["day"] == chunkDay) and \
(nwsPop["hour"] >= chunkStartHour) and \
(nwsPop["hour"] <= chunkEndHour) and \
(nwsPop["pop"] > pop):
pop = nwsPop["pop"]
return pop
######################### Main code.
with urllib.request.urlopen(openWeatherURL) as json_data:
try:
forecast = json.load(json_data)
except ValueError:
print ("Can't load JSON data from " + openWeatherURL)
sys.exit()
# print(forecast)
status_code = forecast["cod"]
# print(status_code)
if status_code != "200":
print("Bad JSON data load, code:" + str(status_code));
sys.exit()
# Get the latitude and longitude for your home or area.
lat = <YOUR-LATITUDE-GOES-HERE>
lon = <YOUR-LONGITUDE-GOES-HERE>
aqis = getAQIs(lat, lon)
# print(aqis)
rawNWS = getNWSRawHourlyData(lat, lon)
# print(rawNWS)
nws = getNWSHourlyData(rawNWS)
sunriseEpoch = forecast["city"]["sunrise"]
sunsetEpoch = forecast["city"]["sunset"]
# print(sunriseEpoch)
sunriseLocalTime = time.localtime(sunriseEpoch)
sunriseHour = sunriseLocalTime.tm_hour
sunriseMinute = sunriseLocalTime.tm_min
print("Sunrise is at " + str(sunriseHour) + ":" + str(sunriseMinute) + " A M")
sunsetLocalTime = time.localtime(sunsetEpoch)
sunsetHour = sunsetLocalTime.tm_hour
sunsetMinute = sunsetLocalTime.tm_min
print("Sunset is at " + str(sunsetHour - 12) + ":" + str(sunsetMinute)+" P M")
print(" ")
numChunks = forecast["cnt"]
# print(numChunks)
goodChunks = []
likelyRainOrSnowToday = 0
priorChunkLikelyWet = 0
for chunk in forecast["list"]:
chunkStartTimeEpoch = chunk["dt"]
chunkStartLocalTime = time.localtime(chunkStartTimeEpoch)
chunkStartHour = chunkStartLocalTime.tm_hour
chunkMinute = chunkStartLocalTime.tm_min
chunkEndHour = chunkStartHour + 3
chunkMinTemp = chunk["main"]["temp_min"]
chunkMaxTemp = chunk["main"]["temp_max"]
chunkFeelsTemp = chunk["main"]["feels_like"]
chunkHumidity = chunk["main"]["humidity"]
chunkWeatherID = chunk["weather"][0]["id"]
chunkWeatherMain = chunk["weather"][0]["main"]
chunkPrecipitationChance = chunk["pop"] * 100
chunkWindGust = chunk["wind"]["gust"]
ozoneAQIValues = getChunkOzoneAQI(aqis, chunkStartLocalTime)
chunk["ozone"] = ozoneAQIValues["ozone"]
chunk["ozone_aqi"] = ozoneAQIValues["aqi"]
chunk["ozone_category"] = ozoneAQIValues["category"]
pm25AQIValues = getChunkPM25AQI(aqis, chunkStartLocalTime)
chunk["pm25"] = pm25AQIValues["pm25"]
chunk["pm25_aqi"] = pm25AQIValues["aqi"]
chunk["pm25_category"] = pm25AQIValues["category"]
chunkNWSMinTemp = getChunkNWSMinTemp(nws, chunkStartLocalTime)
if chunkNWSMinTemp < chunkMinTemp:
chunkMinTemp = chunkNWSMinTemp
chunkNWSMaxTemp = getChunkNWSMaxTemp(nws, chunkStartLocalTime)
if chunkNWSMaxTemp > chunkMaxTemp:
chunkMaxTemp = chunkNWSMaxTemp
chunkNWSHeatIndex = getChunkNWSHeatIndex(nws, chunkStartLocalTime)
if chunkNWSHeatIndex > chunkMaxTemp:
chunkMaxTemp = chunkNWSHeatIndex
chunkNWSWetTemp = getChunkNWSWetBulbGlobeTemp(nws, chunkStartLocalTime)
if chunkNWSWetTemp > chunkMaxTemp:
chunkMaxTemp = chunkNWSWetTemp
chunkNWSAppTemp = getChunkNWSAppTemp(nws, chunkStartLocalTime)
if chunkNWSAppTemp > chunkFeelsTemp:
chunkFeelsTemp = chunkNWSAppTemp
chunkNWSPop = getChunkNWSProbabilityOfPrecipitation(nws,chunkStartLocalTime)
if chunkNWSPop > chunkPrecipitationChance:
chunkPrecipitationChance = chunkNWSPop
# print("Chunk start: " + str(chunkStartHour) + ":" + str(chunkMinute))
# print(" end: " + str(chunkEndHour) + ":" + str(chunkMinute))
# print(" " + str(chunkMinTemp) + "/" + str(chunkMaxTemp))
# print(" " + str(chunkHumidity) + "% " + str(chunkPrecipitationChance))
# print(" " + str(chunkWeatherID) + " " + str(chunkWeatherMain))
# Toss all the time chunks that are problematic, not at all good
# times for going for a walk.
if chunkStartTimeEpoch > sunsetEpoch:
chunk["rejection_code"] = 1
chunk["rejection_text"] = "past sunset."
continue
if chunkPrecipitationChance > 30:
if chunkMaxTemp < 35:
chunk["rejection_text"] = "snow."
else:
chunk["rejection_text"] = "rain."
chunk["rejection_code"] = 3
likelyRainOrSnowToday = 1
priorChunkLikelyWet = 1
continue
else:
if priorChunkLikelyWet == 1:
chunk["rejection_code"] = 4
chunk["rejection_text"] = "grass and sidewalks likely wet."
priorChunkLikelyWet = 0
continue
priorChunkLikelyWet = 0
if chunkEndHour < sunriseHour:
chunk["rejection_text"] = "before sunrise."
chunk["rejection_code"] = 2
continue
if chunkMaxTemp > 90:
chunk["rejection_code"] = 5
chunk["rejection_text"] = "too hot, over 90F."
continue
if chunkWindGust > 30:
chunk["rejection_code"] = 6
chunk["rejection_text"] = "too windy, gusts over 30 MPH."
continue
if chunkMinTemp < 35 and likelyRainOrSnowToday:
chunk["rejection_code"] = 7
chunk["rejection_text"] = "slippery ice risk, under 35F."
continue
if chunk["ozone_aqi"] >= 100:
chunk["rejection_code"] = 8
chunk["rejection_text"] = "ozone AQI too high."
continue
if chunk["pm25_aqi"] >= 100:
chunk["rejection_code"] = 9
chunk["rejection_text"] = "PM25 AQI too high."
continue
if chunkMaxTemp > 75 and chunkHumidity > 85:
chunk["rejection_code"] = 10
chunk["rejection_text"] = "combo of temperature and humidity too high."
continue
chunk["rejection_text"] = "OK"
chunk["rejection_code"] = 0
# Assign a "how good a time to walk" score for every remaining good
# time chunk, and save the chunk.
chunk["score"] = assignWalkiesScore(chunkFeelsTemp, chunkHumidity,
chunkPrecipitationChance, chunkWindGust,
chunk["ozone_aqi"], chunk["pm25_aqi"], chunkStartHour, chunkEndHour)
goodChunks.append(chunk)
if len(goodChunks) < 1:
print("Sorry but there are no good times for walkies today.")
print("Please consider walking inside or riding a stationary bike.")
print("Check a weather app for possible times that are good but brief.")
print("")
print("Times not so good for a walk include:")
for chunk in forecast["list"]:
printChunkRejectionReason(chunk)
sys.exit()
# Sort the chunks by how good their scores are, highest score first.
goodChunks.sort(reverse = True, key = walkiesScore)
print("The best time to go for a walk will be:", end='')
printChunk(goodChunks[0])
if len(goodChunks) > 1:
print("Another good time to go for a walk will be:", end='')
printChunk(goodChunks[1])
if len(goodChunks) > 2:
print("A third good time to go for a walk will be:", end='')
printChunk(goodChunks[2])
Here is the script getaqis.py to get the NWS AQI info for a specific location.
NOTE:This script is hacky as heck. It uses an undocumented API call to get the AQI goods, one that is intended for NWS internal use to generate an interactive AQI map of the US. It could break at any time, but hopefully NWS will expose this info in a more official way someday.
"""
Functions to read the NWS hacky AQI URL's data and parse it into
hourly date-hours and ozone and PM2.5 values.
NOTE: The ozone and PM2.5 URL calls, while almost identical, will return
different counts of values from each other, so the resulting value
arrays can't simply be combined into one.
ALSO NOTE: The AQI weather.gov server is SLOOOOW; these calls take several
seconds each just to return a short list of date-hours and values.
THIRD NOTE: There are other AQI data that could be looked at like dust
and smoke, but it isn't clear how to interpret the values, or base any
decisions off of them. Ozone and PM2.5 have pretty clear guidelines.
BIG NOTE: The NWS URLs used are not advertised or publicly supported, and
may break or otherwise change at any time without warning.
The primary call is getAQIs(lat, lon), which returns None if something goes
wrong, or hopefully a big ol' chonk o' info.
"""
import sys
import math
import urllib.request
from datetime import datetime, timedelta
# This URL gives the raw hourly AQI data. But it is hacky AF. It is basically
# an internal NWS URL with no public support for it, and may change or break
# at any time. But it provides what is needed.
#
# orig_aqiOzoneURL = 'https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=ozone01_bc&latitude=<YOUR-LAT-HERE>&longitude=<YOUR-LON-HERE>®ion=CONUS&rundatetime=2024051712'
#
# orig_aqiPM25URL = 'https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=apm25h01_bc&latitude=<YOUR-LAT-HERE>&longitude=<YOUR-LON-HERE>®ion=CONUS&rundatetime=2024051712'
#
# Base forecast AQI data URL is
# https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?
# with:
# dataset=aqprod& (always)
# element=<element>& (see below)
# latitude=<lat>&longitude=<lon>&
# region=CONUS& (continental US)
# rundatetime=<YYYYMMDDHH>
# element of 'ozone01' is ozone concentration in Parts Per Billion.
# All returned data is hourly forecast for at least 3 days unless noted.
# '..._bc' is bias-corrected version.
# 'ozone01_bc' is "bias-corrected ozone".
# 'ozone08' is also ozone, maybe weighted over 8 hours? Different than ozoneo1.
# 'mozone01' and 'mozone08', daily forecasts, not hourly.
# 'smokes01' is "near-surface smoke".
# 'smokec01' is "column-integrated smoke".
# 'dustsdc01' is "surface dust concentration".
# 'dustvdc01' is "Lowest 5-km Dust Concentration".
# 'apm25h01' is particulate matter <= 2.5 microns diameter.
# 'apm25h01_bc' is bias-corrected PM2.5.
# Values:
# From: https://vlab.noaa.gov/web/osti-modeling/air-quality/faqs
# Ozone: 0 - 54 ppb, good. It's a great day to be active outside.
# 55 - 70, moderate, sensitive ppl consider reducing long out activity
# 71 - 85, unhealthy for sensitive ppl. Reduce / sched in early AM.
# 86 - 105, unhealthy, sensitive avoid, others reduce / sched in AM.
# 106 - 200, very unhealthy, sensitive avoid all, others avoid hard.
# 201+, hazardous, everyone avoid ALL physical outdoor activity.
# PM2.5: 0 - 12 ug/m3, good. It's a great day to be active outside.
# 12.1- 35.4, moderate, Sensitive ppl consider reduc outdoor activity.
# 35.5 - 55.4, unhealthy for sensitive ppl. Reduce / sched in early AM.
# 55.5 - 124.4, unhealthy, sensitive avoid, others reduc / sched in AM.
# 124.5 - 224.4, v. unhealthy, sensitive avoid all, others avoid hard.
# 224.5+, hazardous, everyone avoid ALL physical outdoor activity.
#
# NOTE: This is a very hacky URL; it is used internally by a NWS Javascript
# in-browser script to grab and pretty-plot AQI data. It might break
# without warning or any way to update / fix it.
# Example calls:
# https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=ozone01&latitude=<YOUR-LAT-HERE>&longitude=<YOUR-LON-HERE>®ion=CONUS&rundatetime=2024051712
# ["ozone01",["2024051717","2024051718","2024051719","2024051720","2024051721","2024051722","2024051723","2024051800","2024051801","2024051802","2024051803","2024051804","2024051805","2024051806","2024051807","2024051808","2024051809","2024051810","2024051811","2024051812","2024051813","2024051814","2024051815","2024051816","2024051817","2024051818","2024051819","2024051820","2024051821","2024051822","2024051823","2024051900","2024051901","2024051902","2024051903","2024051904","2024051905","2024051906","2024051907","2024051908","2024051909","2024051910","2024051911","2024051912","2024051913","2024051914","2024051915","2024051916","2024051917","2024051918","2024051919","2024051920","2024051921","2024051922","2024051923","2024052000","2024052001","2024052002","2024052003","2024052004","2024052005","2024052006","2024052007","2024052008","2024052009","2024052010","2024052011","2024052012"],[43,45,46,45,45,44,40,35,31,28,27,27,26,24,23,22,22,22,22,21,21,27,33,37,39,40,40,41,41,37,35,32,28,26,27,32,32,30,29,30,29,29,29,27,29,34,39,44,46,47,48,46,44,43,42,37,32,30,32,31,29,29,31,34,32,28,26,28]]
# https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=apm25h01_bc&latitude=<YOUR-LAT-HERE>&longitude=<YOUR-LON-HERE>®ion=CONUS&rundatetime=2024051712
# ["apm25h01_bc",["2024051713","2024051714","2024051715","2024051716","2024051717","2024051718","2024051719","2024051720","2024051721","2024051722","2024051723","2024051800","2024051801","2024051802","2024051803","2024051804","2024051805","2024051806","2024051807","2024051808","2024051809","2024051810","2024051811","2024051812","2024051813","2024051814","2024051815","2024051816","2024051817","2024051818","2024051819","2024051820","2024051821","2024051822","2024051823","2024051900","2024051901","2024051902","2024051903","2024051904","2024051905","2024051906","2024051907","2024051908","2024051909","2024051910","2024051911","2024051912","2024051913","2024051914","2024051915","2024051916","2024051917","2024051918","2024051919","2024051920","2024051921","2024051922","2024051923","2024052000","2024052001","2024052002","2024052003","2024052004","2024052005","2024052006","2024052007","2024052008","2024052009","2024052010","2024052011","2024052012"],[10,9,7,6,6,6,7,8,8,9,9,8,8,8,8,9,8,8,8,7,6,5,8,8,10,9,6,4,4,5,5,6,6,7,7,6,8,16,19,20,22,22,21,19,16,15,18,15,14,14,13,13,13,13,13,14,12,12,13,13,15,13,16,16,16,19,18,20,22,22,23,21]]
def computeOzoneAQI(ozonePPB):
"""
Compute the AQI score for ozone from the raw ppb data.
From pages 9 - 11 of:
https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf
aqi = (iHigh - iLow) / (bpHigh - bpLow) * (ozonePPM - bpLow) + iLow
NOTE: The table on page 10 does not define breakpoints / indicies for
1-hour ozone readings under 0.125 (score 101, unhealthy for sensitive
groups), so fill in with the 8-hour bp's and indicies, which is wrong-ish,
but better than no score at all, which is useless most of the time.
"""
# print(ozonePPB)
ozonePPM = ozonePPB / 1000.0
if (ozonePPM <= 0.054):
bpLow = 0
bpHigh = 0.054
iLow = 0
iHigh = 50
elif (ozonePPM <= 0.70):
bpLow = 0.055
bpHigh = 0.070
iLow = 51
iHigh = 100
elif (ozonePPM <= 0.164):
bpLow = 0.071
bpHigh = 0.164
iLow = 101
iHigh = 150
elif (ozonePPM <= 0.204):
bpLow = 0.165
bpHigh = 0.204
iLow = 151
iHigh = 200
elif (ozonePPM <= 0.404):
bpLow = 0.205
bpHigh = 0.404
iLow = 201
iHigh = 300
elif (ozonePPM <= 0.504):
bpLow = 0.405
bpHigh = 0.504
iLow = 301
iHigh = 400
else:
bpLow = 0.505
bpHigh = 0.604
iLow = 401
iHigh = 500
aqi = (iHigh - iLow) / (bpHigh - bpLow) * (ozonePPM - bpLow) + iLow
return(round(aqi))
def computePM25AQI(pm25):
"""
Compute the AQI score for particulate matter <= 2.5 microns from the raw data.
From pages 9 - 11 of:
https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf
aqi = (iHigh - iLow) / (bpHigh - bpLow) * (pm25 - bpLow) + iLow
"""
# print(pm25)
if (pm25 <= 12.0):
bpLow = 0
bpHigh = 12.0
iLow = 0
iHigh = 50
elif (pm25 <= 35.4):
bpLow = 12.1
bpHigh = 35.4
iLow = 51
iHigh = 100
elif (pm25 <= 55.4):
bpLow = 35.5
bpHigh = 55.4
iLow = 101
iHigh = 150
elif (pm25 <= 150.4):
bpLow = 55.5
bpHigh = 150.4
iLow = 151
iHigh = 200
elif (pm25 <= 250.4):
bpLow = 150.5
bpHigh = 250.4
iLow = 201
iHigh = 300
elif (pm25 <= 350.4):
bpLow = 250.5
bpHigh = 350.4
iLow = 301
iHigh = 400
else:
bpLow = 350.5
bpHigh = 500.4
iLow = 401
iHigh = 500
aqi = (iHigh - iLow) / (bpHigh - bpLow) * (pm25 - bpLow) + iLow
return(round(aqi))
def assignCategoryAQI(aqiScore):
"""
Assign an AQI Category title based on the AQI score.
"""
# print(aqiScore)
if (aqiScore <= 50):
return "Good"
elif (aqiScore <= 100):
return "Moderate"
elif (aqiScore <= 150):
return "Unhealthy for sensitive groups"
elif (aqiScore <= 200):
return "Unhealthy"
elif (aqiScore <= 300):
return "Very unhealthy"
else:
return "HAZARDOUS"
def getAQIs(lat, lon):
"""
Get hourly ozone and PM2.5 values, return None
if something goes wrong, otherwise return aqis
where aqis["ozone"] includes a list of hourly ozone
values and equivalent AQI values and categories:
aqis["ozone"][0...]["year"], ["month"], ["day"], ["hour"],
["ozone"] in PPB, ["aqi_ozone"] AQI value, and
["category_ozone"] AQI category name.
aqis["pm25"] is an equivalent list:
aqis["pm25"][0...]["year"], ["month"], ["day"], ["hour"],
["pm25"] in ug/m3, ["aqi_pm25"] AQI value, and
["category_pm25"] AQI category name.
"""
# These NWS AQI calls return a 500 error on calls anytime other than 12PM,
# and 500 error for any date newer than yesterday. BUT they will return
# a long list of values, including all of the ones for today, if called for
# yesterday at 12PM. OK, whatever, fine.
yesterday = datetime.now() - timedelta(1)
runDateTime = yesterday.strftime('%Y%m%d') + '12'
# runDateTime = '2024051812'
# print(runDateTime)
aqiOzoneURL = 'https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=ozone01_bc&latitude=' + str(lat) + '&longitude=' + str(lon) + '®ion=CONUS&rundatetime=' + runDateTime
aqiOzoneURL = str(aqiOzoneURL)
# print(aqiOzoneURL)
# print(orig_aqiOzoneURL)
aqiPM25URL = 'https://airquality.weather.gov/aqtable/scripts/degrib_perioddata.php?dataset=aqprod&element=apm25h01_bc&latitude=' + str(lat) + '&longitude=' + str(lon) + '®ion=CONUS&rundatetime=' + runDateTime
aqiPM25URL = str(aqiPM25URL)
# print(aqiPM25URL)
# print(orig_aqiPM25URL)
aqiOzoneFP = urllib.request.urlopen(aqiOzoneURL)
# Read in the URL contents as one big string. It'll basically be a data
# array of the form ["ozone01",["2024051511",...],[42,...]]
aqiOzoneData = str(aqiOzoneFP.read())
if len(aqiOzoneData) < 10:
print("Failed to read data from AQI ozone URL.")
return None
aqiPM25FP = urllib.request.urlopen(aqiPM25URL)
aqiPM25Data = str(aqiPM25FP.read())
if len(aqiPM25Data) < 10:
print("Failed to read data from AQI PM2.5 URL.")
return None
# First get rid of the double-quotes, square brackets, and 'b' chars.
translationTable = str.maketrans('', '', '"[]b')
aqiOzoneData = aqiOzoneData.translate(translationTable)
aqiPM25Data = aqiPM25Data.translate(translationTable)
# Now get rid of the single quotes.
translationTable = str.maketrans("", "", "'")
aqiOzoneData = aqiOzoneData.translate(translationTable)
aqiPM25Data = aqiPM25Data.translate(translationTable)
# Split the array into the title '"ozone01', the date-hours
# '2024051511', '2024051512', ..., and the ozone values '42', '43', ...
aqiOzoneRawValues = aqiOzoneData.split(',')
aqiPM25RawValues = aqiPM25Data.split(',')
# Get the count of date-hours and values.
aqiOzoneRawCount = math.floor((len(aqiOzoneRawValues) - 1) / 2)
aqiPM25RawCount = math.floor((len(aqiPM25RawValues) - 1) / 2)
# print(aqiOzoneRawValues)
# print(aqiOzoneRawCount)
# print(aqiPM25RawValues)
# print(aqiPM25RawCount)
aqiOzoneValues = []
for i in range(1, aqiOzoneRawCount + 1):
dt = datetime.strptime(aqiOzoneRawValues[i], '%Y%m%d%H')
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["ozone"] = float(aqiOzoneRawValues[i + aqiOzoneRawCount])
value["aqi_ozone"] = computeOzoneAQI(value["ozone"])
value["category_ozone"] = assignCategoryAQI(value["aqi_ozone"])
aqiOzoneValues.append(value)
aqiPM25Values = []
for i in range(1, aqiPM25RawCount + 1):
dt = datetime.strptime(aqiPM25RawValues[i], '%Y%m%d%H')
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["pm25"] = float(aqiPM25RawValues[i + aqiPM25RawCount])
value["aqi_pm25"] = computePM25AQI(value["pm25"])
value["category_pm25"] = assignCategoryAQI(value["aqi_pm25"])
aqiPM25Values.append(value)
aqiValues = {"ozone": aqiOzoneValues, "pm25": aqiPM25Values}
return aqiValues
# Test your location
# lat = <YOUR-LATITUDE>
# lon = <YOUR-LONGITUDE>
# aqis = getAQIs(lat, lon)
# print(aqis)
Here is the script getnws.py to get the firehose of NOAA / NWS weather forecast info. Only a small fraction of it is used for predicting the best walking time, but wow, there is a ton of info provided.
"""
Get hourly weather data from NOAA / NWS API.
https://www.weather.gov/documentation/services-web-api#/default/gridpoint
https://api.weather.gov/
This is a two-hop process:
1. Feed a lat,lon pair into the "points" API, which will return
lots of JSON info, including forecast URLs for 12 hour x 7 day,
1 hour x 7 days, and raw data (which is also hourly) over 7 days.
The "points" URL for your latitude and longitude:
https://api.weather.gov/points/<YOUR-LATITUDE>,<YOUR-LONGITUDE>
The raw data URL can be extracted from:
pointsInfo["properties"]["forecastGridData"]
Example:
https://api.weather.gov/gridpoints/ARX/92,49
2. Use the raw 1 hour x 7 days URL to get a big ol' firehose of weatherly
JSON data.
The grid data has '@context', 'id', 'type', 'geometry', all very nice,
and the big boy, 'properties':
data['properties']
which has DOZENS of sub-arrays: 'temperature', 'dewpint', ... 'heatIndex',
'wetBulbGlobeTemperature', 'probabilityOfPrecipitation', 'weather', ...
data['properties']['heatIndex']
with the meat in the 'values' part:
data['properties']['heatIndex']['values']
and the units in the 'uom' part:
data['properties']['heatIndex']['uom'] = "wmoUnit:degC"
Each of these will have it's own special length:
len(data['properties']['heatIndex']['values']) = 98
len(data['properties']['temperature']['values']) = 147
And each entry in the 'values' array may or may not have a valid value:
data['properties']['heatIndex']['values'][0] =
{'validTime': '2024-06-20T04:00:00+00:00/PT13H', 'value': None}
data['properties']['temperature']['values'][0] =
{'validTime': '2024-06-20T04:00:00+00:00/PT2H', 'value': 18.88888888888889}
The 'validTime' ends with, say, '13H' or '2H' which indicates the duration
of the valid time window in hours.
Then return the firehose, after doing some formatting / sprucing.
"""
import json
import sys
import os
import math
import urllib.request
from urllib.error import HTTPError, URLError
from datetime import datetime, timedelta
def getNWSRawHourlyURL(lat, lon):
baseURL = 'https://api.weather.gov/points/'
pointsURL = baseURL + str(lat) + ',' + str(lon)
# print(pointsURL)
try:
pointsFP = urllib.request.urlopen(pointsURL)
except HTTPError:
print ("HTTP error opening " + pointsURL)
return None
except URLError:
print ("URL error opening " + pointsURL)
return None
except TimeoutError:
print ("Timeout error opening " + pointsURL)
return None
# Read in the URL contents as JSON data.
try:
pointsInfo = json.load(pointsFP)
except ValueError:
print ("Can't load 'points' info JSON data from " + pointsURL)
return None
return pointsInfo["properties"]["forecastGridData"]
def getNWSRawHourlyData(lat, lon):
hourlyForecastURL = getNWSRawHourlyURL(lat, lon)
if hourlyForecastURL is None:
return None
rawDataFP = urllib.request.urlopen(hourlyForecastURL)
# Read in the URL contents as JSON data.
try:
rawData = json.load(rawDataFP)
except ValueError:
print ("Can't load raw forecast JSON data from " + hourlyForecastURL)
return None
return rawData
def getDateTimeFromNWSValidTime(vt):
# vt will be something like '2024-06-20T17:00:00+00:00/PT2H'
# Don't care about the stuff after the "+" symbol, just the date/time.
vts = vt.split("+")
dt = datetime.strptime(vts[0], '%Y-%m-%dT%H:%M:%S')
return dt
def getNWSHourlyData(rawData):
nwsHeatIndexValues = []
for hi in rawData['properties']['heatIndex']['values']:
if hi['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(hi['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["heatIndexC"] = hi['value']
value["heatIndexF"] = (hi['value'] * 1.8) + 32
nwsHeatIndexValues.append(value)
nwsMinTemperatureValues = []
for mint in rawData['properties']['minTemperature']['values']:
if mint['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(mint['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["mintC"] = mint['value']
value["mintF"] = (mint['value'] * 1.8) + 32
nwsMinTemperatureValues.append(value)
nwsMaxTemperatureValues = []
for maxt in rawData['properties']['maxTemperature']['values']:
if maxt['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(maxt['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["maxtC"] = maxt['value']
value["maxtF"] = (maxt['value'] * 1.8) + 32
nwsMaxTemperatureValues.append(value)
nwsApparentTemperatureValues = []
for appt in rawData['properties']['apparentTemperature']['values']:
if appt['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(appt['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["apptC"] = appt['value']
value["apptF"] = (appt['value'] * 1.8) + 32
nwsApparentTemperatureValues.append(value)
nwsWetBulbGlobeTemperatureValues = []
for wbgt in rawData['properties']['wetBulbGlobeTemperature']['values']:
if wbgt['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(wbgt['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["wgbtC"] = wbgt['value']
value["wgbtF"] = (wbgt['value'] * 1.8) + 32
nwsWetBulbGlobeTemperatureValues.append(value)
nwsProbabilityOfPrecipitation = []
for pop in rawData['properties']['probabilityOfPrecipitation']['values']:
if pop['value'] is None:
continue
dt = getDateTimeFromNWSValidTime(pop['validTime'])
value = {"year":dt.year, "month":dt.month, "day":dt.day, "hour":dt.hour}
value["pop"] = pop['value']
nwsProbabilityOfPrecipitation.append(value)
nwsValues = {"heatIndex": nwsHeatIndexValues, \
"minimumTemperature": nwsMinTemperatureValues, \
"maximumTemperature": nwsMaxTemperatureValues, \
"apparentTemperature": nwsApparentTemperatureValues, \
"wetBulbGlobeTemperature": nwsWetBulbGlobeTemperatureValues, \
"probabilityOfPrecipitation": nwsProbabilityOfPrecipitation}
return nwsValues
# Test at your lat,lon:
# lat = <YOUR-LATITUDE>
# lon = <YOUR-LONGITUDE>
# rawData = getNWSRawHourlyData(lat, lon)
# print(rawData)
# data = getNWSHourlyData(rawData)
# print(data)