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 https://restcountries.eu/rest/v1/all | 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 https://restcountries.eu/rest/v1/all).capital | 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 https://restcountries.eu/rest/v1/all).capital | Get-LiveWeather -Unit Metric -Key $yourkey | sort Temperature -desc | select -first 1 | ft * -AutoSize

or to find the windiest place:

(Invoke-RestMethod https://restcountries.eu/rest/v1/all).capital | 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:

<#
.Synopsis
   Get-LiveWeather is a function that retrieves current weather information from OpenWeatherMap
.EXAMPLE
    Get-LiveWeather -City 'San Francisco' -key 'yourkey'
.EXAMPLE
   Get-LiveWeather -City 'Paris','London','Roma','Berlin' -Unit Metric -Key 'yourkey' | ft * -AutoSize
.EXAMPLE
   (Invoke-RestMethod https://restcountries.eu/rest/v1/all).capital | Get-LiveWeather -Unit Metric -Key 'yourkey' | ft * -AutoSize
.NOTES
   happysysadm.com
   @sysadm2010
#>
function Get-LiveWeather
{
    [CmdletBinding()]
    Param
    (
        # City Name
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [string[]]$City,

        # Standard, metric, or imperial units
         [ValidateSet("Standard","Metric","Imperial")]
        [string]$Unit='Standard',

        # Openweather key
        [Parameter(Mandatory=$true)]
        [string]$Key
    )
    Process
    {
    foreach($Cityname in $City)
        {
        $ok = $false
        try{
            $city_id = (Invoke-RestMethod "api.openweathermap.org/data/2.5/weather?q=$cityname&APPID=$key&units=$Unit" -ErrorAction Stop).id
            $ok = $true
        }
        catch{
            Write-Error "$Cityname not found...."
        }
        if($ok) {
            $Weather = Start-RoboCommand -Command 'Invoke-RestMethod' `
                            -Args @{ URI = "api.openweathermap.org/data/2.5/weather?id=$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" = $Weather.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
                }
            $CityWeatherObject
            }
        }
    }
}

Kudos to OpenWeatherMap and to RestCountries.eu for their work.

PowerShell rocks.

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...