An Even Better PowerShell Forecast

After posting the last update to my PowerShell weather script, I was looking at the sheer awkwardness of the pre-made SOAP request. Basically, in order to send a SOAP request to the web service, I just kept a ready-made SOAP envelope in a file alongside the script, prepped it with some simple search-and-replace for the parameters, and used Invoke-WebRequest to POST it.

Here's the old code:

$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'

It's awkward and very lazy, but it works. Still, it was bothering me. After a little more research, it turns out that if you've got a WSDL, you can just generate a proxy straight out of PowerShell (much the same way you'd generate proxy classes to work with a SOAP service from Visual Studio). The cmdlet you need is New-WebServiceProxy (naturally). Here's the updated code using New-WebServiceProxy and dumping the pre-made SOAP request:

$uri = "http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl"
$lat = "40.0269"
$lon = "-105.251"Po
$start =  Get-Date -format "yyyy-MM-dd"

$Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy

[xml]$weather = $Proxy.NDFDgenByDay($lat, $lon, $start, 7, "e", "Item24hourly")

Just generate the proxy and call the method. Notice that this also means I get to drop the awkward line of code which extracted the XML I cared about from the return SOAP envelope. Much cleaner.

A Better PowerShell Forecast

In my last post, I wrote about getting the forecast from the National Weather Service from the command line in PowerShell. But the final version of the script I left off with wasn't that great; it basically just wrote a bunch of strings out to the console, which isn't a very PowerShell-esque way to do things. PowerShell likes objects, and it especially likes when we can commands like Where-Object and Select-Object to do interesting things with them.

So, let's update that script a bit so we get objects with properties back, instead of just strings. While we're at it, we can fix a bug with the old version - it's only checking the high temperature value for nil, and the web service we're calling can return nil for any of these values.

Here's the old version of the output loop:

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')
    }
}

Here's the new version:

$startDate = (Get-Date)

for($i = 0; $i -lt 7; $i++){
    $dayOfWeek = (($startDate).AddDays($i).DayOfWeek).ToString()

    $high = $days[0].value[$i]
    if($high.nil) { $high = "[unknown]"}

    $low = $days[1].value[$i]
    if($low.nil) { $low = "[unknown]" }

    $condition = $days[2].value[$i].'weather-summary'
    if($condition.nil) { $condition = "[unknown]"}

    New-Object -TypeName PSObject -Property ([ordered]@{"Day" = $dayOfWeek; "High" = $high; "Low" = $low; "Conditions" = $condition})
}

Let's break that down a bit.

First, we're adding $dayOfWeek to so that we can label the days; it makes the output nicer and helps me when I forget which day of the week it is.

For each of the other values we want to return (the high, low, and conditions) we're checking to see if the XML value returned by the service was nil; if so, we're just going to set the value to 'unknown' and move on.

The last line is the new and interesting bit. PowerShell lets you create custom objects on the fly with the New-Object cmdlet. And one really convenient option for that cmdlet is -Property, which lets you pass in a hash which will defined the properties of the new object. So, by calling New-Object in a loop like this, the output of the script is a list of objects which all have the properties Day, High, Low, and Conditions.

Even better, PowerShell 3 supports ordered hashes (notice the ordered keyword at the front of the hash). This forces the hash (and the object generated from it) to preserve the order in which we declared the properties.

The upshot of this is that when we run the script, the output looks like this:

Day                          High                         Low                          Conditions
---                          ----                         ---                          ----------
Sunday                       57                           34                           Chance Rain Showers
Monday                       58                           36                           Mostly Sunny
Tuesday                      73                           39                           Mostly Sunny
Wednesday                    79                           44                           Mostly Sunny
Thursday                     70                           40                           Partly Sunny
Friday                       67                           41                           Partly Sunny
Saturday                     70                           42                           Partly Sunny

And since each of those is an object, we can now pipe the output through other cmdlets, which allows things like:

.\forecast.ps1 | Where-Object {$_.Low -gt 40}

This command is piping the output through Where-Object and only returning the days with a Low greater than 40 degrees:

Day                          High                         Low                          Conditions
---                          ----                         ---                          ----------
Wednesday                    79                           44                           Mostly Sunny
Friday                       67                           41                           Partly Sunny
Saturday                     70                           42                           Partly Sunny

Much more useful. For extra fun, try .\forecast.ps1 | Out-GridView. Once you turn your script outputs into objects, you can do nearly anything with them.

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.