Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Friday Fun: PowerShell Weather Widget

Posted on February 12, 2021February 18, 2021

Recently, someone on Twitter turned me on to an resource that could be used in a PowerShell session to display weather information. This is apparently a well-established and well-regarded source. Once I worked out the basics, I naturally wanted to see what else I could do it with. Here's what I came up with.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

Everything I want to talk about today involves consuming a REST API using Invoke-RestMethod. The primary URI, which you can test in your browser is http://wttr.in. Include a location in the URI like http://wttr.in/chicago. But it gets better. The API source and documentation can be found at https://github.com/chubin/wttr.in. Once I spent a little time researching what I could do, I settled on a basic command like this:

Invoke-RestMethod -Uri http://wttr.in/Syracuse?format=2 -UseBasicParsing -DisableKeepAlive

I thought that was pretty cool. Although the emojis only display in a terminal console that supports them, like a PowerShell session running in Windows Terminal. You'll have less luck running this in the PowerShell ISE or a VS Code console.

Now I need to push this concept.

Formatting the Location

It was simple enough to build a quick script to run this command. I even added a location parameter. The location can be a ZIP code or place name. I was hoping to create output that included the location. But I wanted to make sure the location was properly formatted.

if ($Location -match '^([a-zA-Z\s\.\,])+$') {
    $Location = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ToTitleCase($location.tolower())
}

If the location is a string, like "chicago", this snippet of text will convert it to "Chicago". A string like "las vegas" becomes "Las Vegas".

Using the Local Time

As I was developing my code, I was including the current date and time. But then I realized that if I was running my script for another location, like Las Vegas, displaying my time in the Eastern time zone, might be confusing. I needed to get the time zone for the location so that I could adjust the time accordingly.

I couldn't find anything in the basic wttr.in request that would include that information until I came across the optional v2 API. The format looks like http://v2.wttr.in/chicago. Unfortunately, there is no way to get the result as structured data like JSON or XML. So I had to resort to a regular expression to parse out the time zone name. I built a helper function to do that.

Function GetTZ {
    Param([string]$location)
    Write-Verbose "Getting v2 data from wttr.in for $location"
    $tmp = Invoke-RestMethod -uri "http://v2.wttr.in/$location" -disableKeepAlive -useBasicParsing
    $rx = [System.Text.RegularExpressions.Regex]::new("\b([a-z_]+\/[a-z_]+)\b","IgnoreCase,Multiline")
    $timezone = $rx.match($tmp).Value
    Write-Verbose "Detected timezone $timezone"
    $timezone
}

This will give me a value like America/Chicago. The next step is to determine the timezone offset. Fortunately, I had another code snippet that uses a REST API at http://worldtimeapi.org. With this API, I create a custom object.

My custom object includes the local time in that time zone which I can then use in my output.

Make It Pretty with ANSI

The last step was to make it pretty. I wanted to draw colored line box around the weather and location information using ANSI and special characters. Normally, this isn't too difficult because I can calculate line lengths.

However, there is a little challenge with emoji-strings. Even though PowerShell might show identical length for two different locations, if the weather emoji was different it could throw off the spacing. This meant my closing | for the line with the weather might be off. I have not found a way to account for kerning issues when using emojis, so I resorted to using Write-Host.

With this approach, I could save the cursor position of the previous line, display the weather line, move the cursor, and then display the closing |.

Write-Host $line1
Write-Host $line2
Write-Host $line3a -NoNewline
#get the cursor position
$pos = $host.ui.RawUI.CursorPosition
#adjust it
$pos.x = $line1.Length - $boxAnsi.Length - 1
#move the cursor
$host.ui.RawUI.CursorPosition = $pos
#write the closing box element
Write-Host $line3b
Write-Host $line4

I'm not proud of the hack, but it works.

The ANSI sequences are written to run in Windows PowerShell or PowerShell 7. Here's the complete script.

#requires -version 5.1

# see https://github.com/chubin/wttr.in for API information

#this must be run in a Windows Terminal session that supports the glyphs
#or use -Force if you know you are.

[cmdletbinding()]
Param([string]$Location = "Syracuse,NY", [switch]$Force)

Function TestWT {
    $parent = Get-CimInstance -ClassName win32_process -Filter "processid=$pid"-Property ParentProcessID
    (Get-Process -Id $parent.ParentProcessId).ProcessName -eq "WindowsTerminal"
}
Function GetTZ {
    Param([string]$location)
    Write-Verbose "Getting v2 data from wttr.in for $location"
    $tmp = Invoke-RestMethod -uri "http://v2.wttr.in/$location" -disableKeepAlive -useBasicParsing
    $rx = [System.Text.RegularExpressions.Regex]::new("\b([a-z_]+\/[a-z_]+)\b","IgnoreCase,Multiline")
    $timezone = $rx.match($tmp).Value
    Write-Verbose "Detected timezone $timezone"
    $timezone
}
Function GetTZData {
    [cmdletbinding()]
    [OutputType("pscustomobject","TimeZoneData")]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline,
         HelpMessage = "Enter a timezone location like Pacific/Auckland. It is case sensitive.")]
        [string]$TimeZoneArea,
        [parameter(HelpMessage = "Return raw, unformatted data.")]
        [switch]$Raw
    )
    Begin {
        Write-Verbose "Starting $($myinvocation.mycommand)"
        $base = "http://worldtimeapi.org/api/timezone"
    } #begin

    Process {
        Write-Verbose "Getting time zone information for $TimeZoneArea"
        $target = "$base/$TimeZoneArea"
        Try {
            $data = Invoke-RestMethod -Uri $target -DisableKeepAlive -UseBasicParsing -ErrorAction Stop -ErrorVariable e
        }
        Catch {
            Throw $e.innerexception
        }
        if ($data -AND $Raw) {
            $data
        }
        elseif ($data) {
            if ($data.utc_offset -match "\+") {
                $offset = ($data.utc_offset.substring(1) -as [timespan])
            }
            else {
                $offset = ($data.utc_offset -as [timespan])
            }

            [datetime]$dt = $data.DateTime
            [pscustomobject]@{
                PSTypename         = "TimeZoneData"
                Timezone           = $data.timezone
                Abbreviation       = $data.abbreviation
                Offset             = $offset
                DaylightSavingTime = $data.dst
                Time               = $dt.ToUniversalTime().Addseconds($data.raw_offset)
            }
           # (Get-Date ($data.datetime -split "[\+-]\d{2}:\d{2}")[0])
        }
    } #process

    End {
        Write-Verbose "Ending $($myinvocation.mycommand)"

    } #end

} #close GetTZData

#characters for building a line box
$charHash = @{
    upperLeft  = [char]0x250c
    upperRight = [char]0x2510
    lowerRight = [char]0x2518
    lowerLeft  = [char]0x2514
    horizontal = [char]0x2500
    vertical   = [char]0x2502
}

Write-Verbose "Getting weather summary for $location"

#ANSI sequences
$boxAnsi = "$([char]0x1b)[38;5;11m"
$closeAnsi = "$([char]0x1b)[0m"
$textAnsi = "$([char]0x1b)[38;5;191m"

#convert location to title case if it is words
if ($Location -match '^([a-zA-Z\s\.\,])+$') {
    $Location = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ToTitleCase($location.tolower())
}
if ($Force -OR (TestWT)) {
    $w = Invoke-RestMethod -Uri "http://wttr.in/{$location}?format=2"
    Write-Verbose "Getting local time settings for $Location"
    $localtime = gettzdata (gettz $location)
    [string]$date =  "{0:g}" -f $localtime.time

    $data = ($w.trim()) -replace "\s+"," "
    #internal sum of the individual elements. Trying to figure out kerning or spacing
    #$internalLength = $($data.split() | foreach { $_.length} | measure-object -sum).sum
    $header = " {0}" -f $location
    $headerAnsi = "{0}{1}" -f $textAnsi, $header
    $value = "{0} {1}" -f $textAnsi, $data

    $line1 = "{0}{1} {2} {3}{4}" -f $boxAnsi, $charHash.upperleft, $date, ([string]$charHash.horizontal * ($data.length-$date.length)), $charhash.upperRight
    $line2 = "{0}{1}{2}{3}{4}" -f $boxAnsi, $charHash.Vertical,$headerAnsi.padright($line1.length-1), $boxAnsi,$charHash.Vertical
    $line3a = "{0}{1}{2}" -f $boxAnsi, $charHash.Vertical,$value
    #($value.padright($header.length))

    $line3b = "{0}{1}" -f $boxAnsi,$charHash.Vertical
    $line4 = "{0}{1}{2}{3}{4}" -f $boxAnsi, $charHash.Lowerleft, ([string]$charHash.horizontal * ($data.length+2)), $charhash.LowerRight, $closeAnsi

    Write-Host $line1
    Write-Host $line2
    Write-Host $line3a -NoNewline
    #get the cursor position
    $pos = $host.ui.RawUI.CursorPosition
    #adjust it
    $pos.x = $line1.Length - $boxAnsi.Length - 1
    #move the cursor
    $host.ui.RawUI.CursorPosition = $pos
    #write the closing box element
    Write-Host $line3b
    Write-Host $line4
else {
    Write-Warning "This needs to be run in a Windows Terminal session."
}
}

Next Steps

I should turn this into a function with an alias to make it easy to run. I might dig deeper into the source files for wttri.in and see if I can't make some of those calls directly. This might make it easier to grab the time zone information at the same time. I might also try adding this to my PowerShell prompt function and display it in a corner of my session.

Give it a try, have some fun, and let me know what you think.

Update

I have posted an updated version of this function on GitHub at https://gist.github.com/jdhitsolutions/f2fb0184c2dbab107f2416fb775d462b. This version accepts pipeline input.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

7 thoughts on “Friday Fun: PowerShell Weather Widget”

  1. Gary Smith says:
    February 12, 2021 at 7:11 pm

    I have a slightly different way of getting weather information using xml data from the National Weather Service:

    Function Get-GGSCurrentWeatherCondition {

    D:\WindowsPowerShell\Scripts\Get-GGSCurrentWeatherConditionv4.ps1
    WARNING: 02/12/2021 19:07:40

    Location Station ID Observation time Weather Temperature Wind Chill Wind Pressure (in) Pressure (mb) Rel Humidity (%) Visibility (mi)
    ——– ———- —————- ——- ———– ———- —- ————- ————- —————- —————
    Buffalo NY KBUF 5:54 pm EST Mostly Cloudy 18.0 F (-7.8 C) 5 F (-15 C) Northeast at 11.5 MPH (10 KT) 30.31 1028.0 62 10.00
    Syracuse NY KSYR 5:54 pm EST A Few Clouds 15.0 F (-9.4 C) Calm 30.37 1029.0 38 10.00
    Binghamton NY KBGM 6:53 pm EST Overcast 16.0 F (-8.9 C) North at 3.5 MPH (3 KT) 30.21 1026.5 49 10.00
    State College – University Park Airport KUNV 5:53 pm EST Overcast 21.0 F (-6.0 C) 12 F (-11 C) Northeast at 6.9 MPH (6 KT) 30.22 58 10.00
    Albany International Airport KALB 6:51 pm EST Partly Cloudy 15.0 F (-9.4 C) 8 F (-13 C) Northeast at 4.6 MPH (4 KT) 30.32 1027.4 44 10.00
    New York NY KLGA 5:51 pm EST Overcast 30.0 F (-1.1 C) 23 F (-5 C) North at 6.9 MPH (6 KT) 30.32 1026.5 33 10.00
    Script execution took 1.4029227 seconds

    .INPUTS
    Get-CurrentWeatherCondition accepts pipeline input

    .OUTPUTS
    Custom Object containing current weather conditions at specific sites

    #>
    [CmdletBinding()]
    Param (
    [Parameter(Mandatory=$True,
    ValueFromPipeline=$True,
    HelpMessage=”4 Character Weather Station ID”)]
    [string[]]$StationID

    )
    Begin {
    Write-Warning (Get-Date)
    $StopWatch = [System.diagnostics.stopwatch]::startNew()
    }

    Process{
    Foreach ($station in $StationID) {
    Try {
    $co = ([xml](Invoke-WebRequest -URI http://w1.weather.gov/xml/current_obs/$Station.xml -ErrorAction Stop).Content).current_observation
    } # end try

    Catch {
    Write-Warning “$Station $_.”

    } # end Catch

    $properties = [ordered]@{‘Location’ = ($co.location -split “, “)[0]+” “+($co.location -split “, “)[2];
    ‘Station ID’ = $station;
    ‘Observation time’ = ($co.observation_time -split “, “)[1];
    ‘Weather’ = $co.Weather;
    ‘Temperature’ = $co.Temperature_string;
    ‘Wind Chill’ = $co.windchill_string;
    ‘Wind’ = $co.wind_string;
    ‘Pressure (in)’ = $co.pressure_in;
    ‘Pressure (mb)’ = $co.pressure_mb;
    ‘Rel Humidity (%)’ = $co.relative_humidity;
    ‘Visibility (mi)’ = $co.visibility_mi
    }

    $obj = New-Object -TypeName PSObject -Property $properties
    Write-Output $obj

    } # end Foreach
    } # end Process

    End {
    $Stopwatch.Stop()
    “Script execution took $($Stopwatch.Elapsed.TotalSeconds) seconds”
    }

    } # end function

    Get-GGSCurrentWeatherCondition -StationID KBUF,KSYR,KBGM, KUNV, KALB, KLGA | format-Table * -Autosize

  2. Gregory Suvalian says:
    February 13, 2021 at 9:01 am

    What is the point wrapping in try{}catch{} and immediately rethrowing exception?

    1. Jeffery Hicks says:
      February 15, 2021 at 8:41 am

      Probably standard form, or consistency more than anything. Sometimes I want to take other action in the Catch block. Plus, I think it makes it very clear how I am handling exceptions.

  3. Craig says:
    February 13, 2021 at 12:34 pm

    Worked great for me. Pushed a copy here.

    keybase://public/cadayton/PSGallery/Scripts/Show-WeatherSummary.ps1

  4. Craig says:
    February 13, 2021 at 2:12 pm

    Made a few tweaks.
    1). -F switch to display forecast of specified location too.
    2). Location specified without a “,” will assume a region
    Overview of the region weather displayed in the browser
    eg .\Show-WeatherSummary Washington
    eg .\Show-WeatherSummary WA

  5. Al says:
    February 17, 2021 at 11:06 am

    For me – the script wouldn’t run without an error till the final closing curly brace was moved up above the else statement

    Write-Host $line4
    }
    else {
    Write-Warning “This needs to be run in a Windows Terminal session.”
    }

    1. Jeffery Hicks says:
      February 17, 2021 at 2:16 pm

      Sorry about that. I realized I hadn’t gotten all of the code when I copy and pasted until after I hit post. The current version should be complete.

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d