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.
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.
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
What is the point wrapping in try{}catch{} and immediately rethrowing exception?
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.
Worked great for me. Pushed a copy here.
keybase://public/cadayton/PSGallery/Scripts/Show-WeatherSummary.ps1
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
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.”
}
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.