Getting the forecast in PowerShell

Sometimes I'm working and I need to know what the weather is so I know whether to grab a sweatshirt or jacket (especially this time of year). I realize there are literally hundreds of ways to get this information (web sites, my phone, sticking my head out the window, etc.), but sometimes I just want the quickest possible way to get the highs and lows. I've pretty much always got a console window open - how hard is it to get the weather from the command line?

Turns out, it's pretty easy.

Many months ago I put together a short PowerShell one-liner to get the current temperature. The National Weather Service provides current observations at their weather stations via simple XML. Because PowerShell is so ridiculously good about handling XML, getting the current temperature at my nearest weather station is as simple as

([xml](Invoke-WebRequest -URI http://w1.weather.gov/xml/current_obs/KBDU.xml).Content).current_observation.temperature_string

which just writes out

73.0 F (23.0 C)

to the console. Invoke-WebRequest does what it says on the tin - retrieves the XML file from the web. Casting that file's content to [xml] makes PowerShell handle all the parsing into a nice object; from there you can just dig down to the property you care about. (I had figured this out so I could use it as an example for a plug-in I wrote for a chat robot for HipChat.)

The other day I wanted to improve on this a bit - the current temperature isn't that useful for deciding what to wear. I needed highs and lows. Luckily, it turns out that the National Digital Forecast Database has a web service which provides this information. Less luckily, it's a SOAP-based web service. For a moment, I thought this might really suck (last time I dealt with SOAP was over ten years ago, and even with the convenience of Visual Studio generating most of the code it was pretty painful).

It all turned out to be pretty easy, though.

For starters, getting the SOAP request to the server via PowerShell ended up being trivial.

$uri = "http://graphical.weather.gov/xml/SOAP_server/ndfdXMLserver.php"
$body = Get-Content .\ndfd.soap
[xml]$result = Invoke-WebRequest $uri -Method post -ContentType "text/xml" -Body $body

All you have to do is put the SOAP request in a variable and use Invoke-WebRequest to POST it. Once I found out this is all it took, I figured the hard part was going to be building the request. Turns out, that's not too hard, either, because the NWS has helpfully provided sample SOAP payloads for all of their services. I was interested in calling the NDFDgenByDay() method; here's the request body sample.

So all I needed to do was put in my latitude, longitude, and the date. I cheated by writing a naive template for the request to my PowerShell folder (that's the ndfd.soap in the script above); it's basically the sample request with the latitude, longitude, and date replaced with [lat], [long], and [date]. Building the request is simply a matter of reading the file from disk and replacing the slugs with the correct values:

$lat = "40.019444"
$lon = "-105.292778"
$start =  Get-Date -format "yyyy-MM-dd"
$body = Get-Content .\ndfd.soap
$body = $body.Replace("[lat]", $lat).Replace("[lon]", $lon).Replace("[start]", $start)

After that, it's just a matter of drilling down to the properties you want in the results. The return formats from the various NWS web services are pretty complex; figuring out exactly where the data you want is located ends up being the most time-consuming part. But there's a wealth of data in the various services - you can retrieve data on precipitation, snow accumulation, weather warnings and hazards, wind, and just about anything else you can think of.

Here's my final script for getting the next 6-7 days of highs, lows, and general forecast:

$uri = "http://graphical.weather.gov/xml/SOAP_server/ndfdXMLserver.php"
$lat = "40.019444"
$lon = "-105.292778"
$start =  Get-Date -format "yyyy-MM-dd"

$body = Get-Content .\ndfd.soap
$body = $body.Replace("[lat]", $lat).Replace("[lon]", $lon).Replace("[start]", $start)

[xml]$envelope = Invoke-WebRequest $uri -Method post -ContentType "text/xml" -Body $body
[xml]$weather = $envelope.Envelope.Body.NDFDgenByDayResponse.dwmlByDayOut.'#text'

$params = $weather.dwml.data.parameters

$days = ($params.temperature | select name, value) + ($params.weather | select name, @{Name="value";Expression={$_.'weather-conditions'}})

Write-Host
"Next several days:"

for($i = 0; $i -lt 7; $i++){
    if(-not $days[0].value[$i].nil) {
        ("High of " + $days[0].value[$i] + ", low of " + $days[1].value[$i] + ", " + $days[2].value[$i].'weather-summary')
    }
}

and here's what the output looks like:

Next several days:
High of 72,  low of 49, Sunny
High of 71,  low of 48, Mostly Sunny
High of 58,  low of 43, Chance Rain Showers
High of 54,  low of 39, Chance Rain Showers
High of 63,  low of 43, Partly Sunny
High of 61,  low of 40, Partly Sunny

Update:

There's a much-improved version of this script in my follow-up post.