Wednesday, January 18, 2017

Getting weather data with PowerShell and other funny things you can do in just a line of code

I am always positively impressed when I see how easy it is today to get pretty much any kind of information from the Internet in a structured manner and re-use it for your own interest. A bridge exists today between all 'those scattered data' up there and the use I can make of them. This bridge is made up of three keywords to me: RESTful API, JSON and PowerShell.

Let me show you an example.

As you already know if you have been actively following my blog, I am working on a module to manage my home pellet stove, and, I am adding to it a bunch of functions to corroborate my findings on internal temperature trends with weather data coming from external weather data providers (I don't have a weather station at home and don't want to invest on one).

It didn't take me long to discover the existence of a free weather data provider, named OpenWeatherMap, since fellow MVP Iris Classon wrote a blog post about it last November.

In her article, which is C#-oriented, she explains how to consume the provided JSON data as C# objects. In my case that part of the job is easily accomplished by using the now well-known Invoke-RestMethod cmdlet, which makes PowerShell capable of interacting with remote RESTful web services and return, oh that's so good, a ready to use PSObject, as you can read in the doc:

This means that once you have the required API key to query the OpenWeatherMap (which by the way is free as long as you stick to max 60 calls per minute), you can get the current weather for any place in the world in just one line of code. Long live all those open APIs:

Now since I want to be able to harvest those data in a more robust manner, I have decided to build a function around that line of code. It's a bit overkill, I know, but that allows me also to take advantage of the last three functions I presented on this blog post, and you'll see the result is not that bad:
  1. the function 'Start-RoboCommand' which helps me in being assured that the data I want to retrieve from the Internet are actually fetched
  2. the function 'Get-WindDirection', which I use to convert the wind direction in degrees into something more readable, like the italianate wind name
  3. the function 'Get-WindForce', which returns a description of the current wind by using the Beaufort Scale
The function I wrote, which I called Get-LiveWeather, takes three parameters, $City, which also accepts values coming through the pipeline, $Unit, whose input is validated with [ValidateSet], and $Key, which is the OpenWeatherMap API key we talked above.

Here's a screenshot of the parameter declaration:

Now let's have a look at the Process() block, which is where we provide record-by-record processing for elements coming down the pipeline:

First of all I am going with a Try{} Catch{} to try to resolve the passed city name into a OpenWeatherMap ID. If this does not succeed, then I can print an error and move on with the next city name, if any. On the other side, if the city name is correctly spelled, then I can actually fire my Start-RoboCommand function to run Invoke-RestMethod. The output is then stored in a $Weather variable. 
Starting from the raw content of that variable I build a [PSCustomObject] the way I want to present the data. This is where I make an external call to the other functions I wrote to calculate the wind force and the italianate wind name.

Here's a couple of example of how you can use this function and the output you can obtain:

Get-LiveWeather -City 'San Francisco' -key $yourkey

City_Name            : San Francisco
Temperature          : 281.64
Humidity             : 87
Pressure             : 1018
Weather-description  : {light rain, mist}
Wind_Speed           : 3.6
Wind_Direction       : South
Wind_Italianate_Name : Ostro
Wind_Degrees         : 170
Wind_Force           : Gentle breeze

Get-LiveWeather -City 'paris','london','roma','berlin' -Unit Metric -Key $yourkey | ft * -AutoSize

Nice stuff. Now I started to wonder whether there is a API out there which allows me to find all the capital cities in the world and run my function against it. The quest didn't last long since I pretty soon found out this web that provides me exactly what I need:

Invoke-RestMethod | ft * -autosize

The resulting table is here:

Happily enough there is a 'Capital' property (the last column on the right) which I can pass to my function to retrieve the weather in all the capital cities of the world in no more than a single line of code:

(Invoke-RestMethod | Get-LiveWeather -Unit Metric -Key $yourkey | ft * -AutoSize

Now this is becoming fun. I could imagine to do pretty much anything over this initial oneliner, such as to determine the hottest town of the moment:

(Invoke-RestMethod | Get-LiveWeather -Unit Metric -Key $yourkey | sort Temperature -desc | select -first 1 | ft * -AutoSize

or to find the windiest place:

(Invoke-RestMethod | Get-LiveWeather -Unit Metric -Key $yourkey | sort Wind_Speed -desc | select -first 1 | ft * -AutoSize

See the power of the shell here? I always find it awesome that we are able to get so much data in a so easy way and print it in a very readable output.

Here's the full code of my function, which is also available on GitHub:

   Get-LiveWeather is a function that retrieves current weather information from OpenWeatherMap
    Get-LiveWeather -City 'San Francisco' -key 'yourkey'
   Get-LiveWeather -City 'Paris','London','Roma','Berlin' -Unit Metric -Key 'yourkey' | ft * -AutoSize
   (Invoke-RestMethod | Get-LiveWeather -Unit Metric -Key 'yourkey' | ft * -AutoSize
function Get-LiveWeather
        # City Name

        # Standard, metric, or imperial units

        # Openweather key
    foreach($Cityname in $City)
        $ok = $false
            $city_id = (Invoke-RestMethod "$cityname&APPID=$key&units=$Unit" -ErrorAction Stop).id
            $ok = $true
            Write-Error "$Cityname not found...."
        if($ok) {
            $Weather = Start-RoboCommand -Command 'Invoke-RestMethod' `
                            -Args @{ URI = "$city_ID&APPID=$app_ID&units=$Unit" } `
                            -Count 5 -DelaySec 4 -LogFile error.log
            [double]$WeatherWind = $Weather.wind.speed
            $CityWeatherObject = [PSCustomObject]@{
                "City_Name" = $Cityname
                "Temperature" = $Weather.main.temp
                "Humidity" = $Weather.main.humidity
                "Pressure" = $Weather.main.pressure
                "Weather-description" = $
                "Wind_Speed" = $WeatherWind
                "Wind_Direction" = (Get-WindDirection $Weather.wind.deg).direction
                "Wind_Italianate_Name" = (Get-WindDirection $Weather.wind.deg).name
                "Wind_Degrees" = $Weather.wind.deg
                "Wind_Force" = Get-WindForce $WeatherWind -Language EN

Kudos to OpenWeatherMap and to for their work.

PowerShell rocks.

1 comment:

  1. Respect
    I have to admit that I am very weak with scripts in general
    so I mostly collect someone else’s “mind”.
    I found some PS scripts where "OpenWeatherMap" is used
    and surprisingly, some do and provide relevant data.
    Unfortunately, Your script only provides wind data (and only 0, for all cities).
    I would be interested in how to make the script say out loud some specific information that I specify.

    Lots of greetings


Related Posts Plugin for WordPress, Blogger...