PowerShell console weather script

I wanted a little more detail than some other terminal weather programs provide so I wrote one in PowerShell. I do not consider it complete as I will eventually rewrite error handling, how I handle the data, and add better documentation. It works and can be used to learn or rework into a better script.

This script uses all free APIs. Only the GEO Location API needs an account and key. Enter at the top of the script or simply add your info. manually.

Other APIs used:

IPInfo.

National weather service.

Open Meteo.

#!/usr/bin/env pwsh
<#
API services used:

https://ipinfo.io
https://www.ip2location.io  # Requires account and API key.
https://www.weather.gov/documentation/services-web-api
https://open-meteo.com

In the script folder a folder named geo will be created,
 which should contain two files. 

* Manual config: config.txt should contain:

 latitude
 longitude
 full_state_name

The second file is station.txt and contains the weather station 
closet to your location. This file gets created the first time
you fetch the weather and will limit hits to the API.

On Linux you can create a bash script to start the app and symlink
to /usr/local/bin as any name you like to call it whenever you need
it in the terminal. I used psw for ease on the memory.

* Contents of my bash script:

#!/usr/bin/env bash

pwsh /home/username/Documents/Powershell/ps-weather/psw.ps1

* The desktop file can be created in ~/.local/share/applications to add 
as an app menu item for your DE.

Contents of my desktop file:

[Desktop Entry]
Version=1.0
Name=Powershell Weather
GenericName=Weather
Comment=Weather
Keywords=weather
Exec=pwsh /home/username/Documents/Powershell/ps-weather/psw.ps1
Icon=/home/username/Documents/Powershell/ps-weather/psw.png
Terminal=true
Type=Application
Categories=Application;X-psw;

Notes: Could have better error handling and likely better routine 
to parse the API data, but it works for my needs.
#>

$IPLocationKey = "your key" # enter your api key from: https://www.ip2location.io/ip2location-documentation

$STATES = @{
    "Alabama"="AL"
    "Alaska"="AK"
    "Arizona"="AZ"
    "Arkansas"="AR"
    "California"="CA"
    "Colorado"="CO"
    "Connecticut"="CT"
    "Delaware"="DE"
    "Florida"="FL"
    "Georgia"="GA"
    "Hawaii"="HI"
    "Idaho"="ID"
    "Illinois"="IL"
    "Indiana"="IN"
    "Iowa"="IA"
    "Kansas"="KS"
    "Kentucky"="KY"
    "Louisiana"="LA"
    "Maine"="ME"
    "Maryland"="MD"
    "Massachusetts"="MA"
    "Michigan"="MI"
    "Minnesota"="MN"
    "Mississippi"="MS"
    "Missouri"="MO"
    "Montana"="MT"
    "Nebraska"="NE"
    "Nevada"="NV"
    "New Hampshire"="NH"
    "New Jersey"="NJ"
    "New Mexico"="NM"
    "New York"="NY"
    "North Carolina"="NC"
    "North Dakota"="ND"
    "Ohio"="OH"
    "Oklahoma"="OK"
    "Oregon"="OR"
    "Pennsylvania"="PA"
    "Rhode Island"="RI"
    "South Carolina"="SC"
    "South Dakota"="SD"
    "Tennessee"="TN"
    "Texas"="TX"
    "Utah"="UT"
    "Vermont"="VT"
    "Virginia"="VA"
    "Washington"="WA"
    "West Virginia"="WV"
    "Wisconsin"="WI"
    "Wyoming"="WY"
    "District of Columbia" = "DC"
    "Guam"="GU"
    "Marshall Islands"="MH"
    "Northern Mariana Island"="MP"
    "Puerto Rico"="PR"
    "Virgin Islands"="VI"
}

function Get-WindRoseDirection() {
    param (
        $degree
    )
    if ($degree -ge 0 -and $degree -lt 22.5) {
        $WindDirection =  "N"
    } elseif ($degree -ge 22.5 -and $degree -lt 45) {
        $WindDirection =  "NNE"
    } elseif ($degree -ge 45 -and $degree -lt 67.5) {
        $WindDirection =  "NE"
    } elseif ($degree -ge 67.5 -and $degree -lt 90) {
        $WindDirection =  "ENE"
    } elseif ($degree -ge 90 -and $degree -lt 112.5) {
        $WindDirection =  "E"
    } elseif ($degree -ge 112.5 -and $degree -lt 135) {
        $WindDirection =  "ESE"
    } elseif ($degree -ge 135 -and $degree -lt 157.5) {
        $WindDirection =  "SE"
    } elseif ($degree -ge 157.5 -and $degree -lt 180) {
        $WindDirection =  "SSE"
    } elseif ($degree -ge 180 -and $degree -lt 202.5) {
        $WindDirection =  "S"
    } elseif ($degree -ge 202.5 -and $degree -lt 225) {
        $WindDirection =  "SSW"
    } elseif ($degree -ge 225 -and $degree -lt 247.5) {
        $WindDirection =  "SW"
    } elseif ($degree -ge 247.5 -and $degree -lt 270) {
        $WindDirection =  "WSW"
    } elseif ($degree -ge 270 -and $degree -lt 292.5) {
        $WindDirection =  "W"
    } elseif ($degree -ge 292.5 -and $degree -lt 315) {
        $WindDirection =  "WNW"
    } elseif ($degree -ge 315 -and $degree -lt 337.5) {
        $WindDirection =  "NW"
    } elseif ($degree -ge 337.5 -and $degree -lt 360) {
        $WindDirection =  "NNW"
    } elseif ($degree -eq 360) {
        $WindDirection =  "N"
    }
    return $WindDirection
}

function Get-FrontFacingIP() {

    $StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
    $StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"

    if (Test-Path -path $StoreFile) {

        $UserIP = Get-Content -Path $StoreFile
        $UserIP = $UserIP.Trim()

    } 

    return $UserIP.Trim()
}

function Get-Geo() {
    param (
        $ip,
        $GEOKEY
    )

    $infoService = "https://api.ip2location.io/?key=$GEOKEY&ip=$ip"
    $geotable = Invoke-RestMethod -Method Get -Uri $infoService

    return $GeoTable.latitude, $GeoTable.longitude, $GeoTable.region_name  
}

function Set-Location() {
    param (
        $UserIP = $null,
        $GeoKey
    )

    $StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
    $StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"

    if (-not(Test-Path -Path $StorePath)) {
        New-Item -ItemType "directory" -Path $StorePath -Force
    }
    
    if ($UserSupplied) {
        Set-Content -Path $StoreFile -Value $UserSupplied -Force
    } else {
        $IPService = "https://ipinfo.io/ip"
        $UserIP = Invoke-RestMethod -Method Get -URI $IPService 
        Set-Content -Path $StoreFile -Value $UserIP -Force
    }
    $Lat = "" 
    $Long = ""
    $State = ""
    if ($GeoKey.Trim().Length -gt 0) {
        $Lat, $Long, $State = Get-Geo -ip $UserIP -GEOKEY $GeoKey
        Add-Content -Path $StoreFile -Value $Lat -Force
        Add-Content -Path $StoreFile -Value $Long -Force
        Add-Content -Path $StoreFile -Value $State -Force
    } else {
        Write-Host "Please provide an API key or supply latitude, longitude, and state full name manually by saving each on their own line in that order in /geo/config.txt in the script folder."
    }
    return $Lat, $Long, $State
} 

function Get-Location() {
    $StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
    $StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"

    if (Test-Path -path $StoreFile) {

        $Location = Get-Content -Path $StoreFile

    } 

    return $Location
}

function Get-Alerts() {
    param (
        $STATE
    )
    $endpoint = "https://api.weather.gov/alerts/active?area=$STATE"
    $AlertTable = Invoke-RestMethod -Method Get -Uri $endpoint
    return $AlertTable
}
function Get-Forecast() {
    param (
        $LATITUDE,
        $LONGITUDE
    )

    $StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
    $StoreFile = Join-Path -Path $StorePath -ChildPath "station.txt"

    if (Test-Path -path $StoreFile) {

        $fcendpoint = Get-Content -Path $StoreFile

    } else {

        $endpoint = "https://api.weather.gov/points/$LATITUDE,$LONGITUDE"
        $DataTable = Invoke-RestMethod -Method Get -Uri $endpoint
        $fcendpoint = $DataTable.properties.forecast

        Add-Content -Path $StoreFile -Value $fcendpoint -Force
    }

    $ForecastTable = Invoke-RestMethod -Method Get -Uri $fcendpoint.Trim()

    return $ForecastTable.properties.periods
}

function Get-Current() {
    param (
        $LATITUDE,
        $LONGITUDE
    )
    $endpoint = "https://api.open-meteo.com/v1/gfs?latitude=$LATITUDE&longitude=$LONGITUDE&current=temperature_2m,relative_humidity_2m,precipitation,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch"
    
    $DataTable = Invoke-RestMethod -Method Get -Uri $endpoint

    return $DataTable
}

clear
$UserInput = ""
While($UserInput.ToLower().Trim() -ne "q") {

    $UserInput = ""
    $UserInput = Read-Host -Prompt "> l (save location) n (now) f (forcast today) e (forcast extended) a (alerts) c (clear) q (quit)"

    if ($null -eq $UserInput) {
        $UserInput = ""
    }

    if ($UserInput.ToLower().Trim() -eq "q") {
        clear
    }

    if ($UserInput.ToLower().Trim() -eq "l") {
        $UserIP = Read-Host -Prompt "Enter your front facing IP. Leave empty to attempt automatic lookup"
        if ($UserIP) {
            $WData = Set-Location -UserIP $IPInput.Trim() -GeoKey $IPLocationKey
        } else {
            $WData = Set-Location
        }

        if ($WData[0]) { 
            Write-Host "Saved."
        } else {
            Write-Host "Unable to get lat,long data."
        }
    }

    if ($UserInput.ToLower().Trim() -eq "c") {
        clear
    }

    if ($UserInput.ToLower().Trim() -eq "a") {
        $WData = Get-Location
        $ip,$lat,$long,$state = $WData.Split(" ")
        if ($state) {
            $abv = $STATES[$state]
            $Alert = Get-Alerts -STATE $abv.Trim()
            $Alerts = $Alert.features
            if ($Alerts.Length -gt 0) {
                $Msg = ""
                ForEach ($a in $Alerts) {
                    $headline = $a.properties.headline
                    $effdate = $a.properties.effective
                    $enddate = $a.properties.expires
                    $locales = $a.properties.areaDesc
                    $desc = $a.properties.description
                    $Msg += "`n*** {0}`n`nEffective: {1}`nEnds: {2}`n`nAffects: {3}`n`nAlert: {4}`n" -f $headline,$effdate,$enddate,$locales,$desc 
                }
            } else { $Msg = "No alerts."}
            Write-Host $Msg -ForegroundColor Red
        } else {
            Write-Host "Unable to get location data." -ForegroundColor Red
        }
    }

    if ($UserInput.ToLower().Trim() -eq "e") {
        $WData = Get-Location
        $ip,$lat,$long,$state = $WData.Split(" ")
        $Forecasts = Get-Forecast -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
        $Msg = ""
        if ($lat) {
            ForEach ($periods in $Forecasts) {
                $Count = 0
                #if ($Count -eq 3) { break }
                ForEach ($period in $periods) {
                    $name = $period.name
                    $temp = "{0} {1}" -f $period.temperature, $period.temperatureUnit
                    if ($null -eq $period.dewpoint.value) {
                        $dewp = "n/a"
                    } else {
                        $dewp = "{0} {1}" -f $period.dewpoint.value.ToString("#.##"), $period.dewpoint.unitCode.Split(":")[1]
                    }
                    
                    if ($null -eq $period.relativeHumidity.value) {
                        $relhm = "0%"
                    } else {
                        $relhm = "{0}%" -f $period.relativeHumidity.value
                    }
                    $wind = "0"
                    $direction = "n/a"
                    if ($period.windSpeed) {
                        $wind = $period.windSpeed
                        $direction = $period.windDirection
                    }
                    
                    $forecast = $period.detailedForecast

                    $Msg += "`n*** {0}`n`nTemp: {1}`nDewPoint: {2}`nRel. Humidity: {3}`nWindSpeed: {4}`nDirection: {5}`n`nForecast:`n`n{6}`n" -f $name,$temp,$dewp,$relhm,$wind,$direction,$forecast
                    $Count += 1
                }
            }
            Write-Host $Msg -ForegroundColor Cyan
        } else {
            Write-Host "Unable to get location data." -ForegroundColor Red
        }
    }

    if ($UserInput.ToLower().Trim() -eq "f") {

        $WData = Get-Location
        $ip,$lat,$long,$state = $WData.Split(" ")

        if ($lat) {
            $Current = Get-Current -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()

            $Ctime = Get-Date
            $CTemp = $Current.current.temperature_2m
            $CHmd = $Current.current.relative_humidity_2m
            $CPRC = $Current.current.precipitation
            $CCld = $Current.current.cloud_cover
            $CWinds = $Current.current.wind_speed_10m
            $CWindd = $Current.current.wind_direction_10m
            $WindDir = Get-WindRoseDirection -degree $CWindd
            #$Gusts = $Current.current.wind_gusts_10m

            $CurrMsg = "`n*** Current: {0}`n`nTemp: {1}°F`nHumidity: {2}%`nPrecipitation: {3}`"`nCloud Cover: {4}%`nWind Speed: {5} mph`nDirection: {6}`nRADAR: https://radar.weather.gov/" -f $Ctime,$CTemp,$CHmd,$CPRC,$CCld,$CWinds,$WindDir

            $Forecasts = Get-Forecast -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
            $Msg = ""
            $count = 0
            ForEach ($periods in $Forecasts) {
                ForEach ($period in $periods) {

                    if ($Count -eq 2) {
                        break
                    }
                    
                    $name = $period.name
                    $temp = "{0} {1}" -f $period.temperature, $period.temperatureUnit
                    if ($null -eq $period.dewpoint.value) {
                        $dewp = "n/a"
                    } else {
                        $dewp = "{0} {1}" -f $period.dewpoint.value.ToString("#.##"), $period.dewpoint.unitCode.Split(":")[1]
                    }
                    
                    if ($null -eq $period.relativeHumidity.value) {
                        $relhm = "0%"
                    } else {
                        $relhm = "{0}%" -f $period.relativeHumidity.value
                    }
                    $wind = "0"
                    $direction = "n/a"
                    if ($period.windSpeed) {
                        $wind = $period.windSpeed
                        $direction = $period.windDirection
                    }
                    
                    $forecast = $period.detailedForecast

                    $Msg += "`n*** {0}`n`nTemp: {1}`nDewPoint: {2}`nRel. Humidity: {3}`nWindSpeed: {4}`nDirection: {5}`n`nForecast:`n`n{6}`n" -f $name,$temp,$dewp,$relhm,$wind,$direction,$forecast
                    
                    $Count += 1
                }
            }
            Write-Host $CurrMsg -ForegroundColor Green
            Write-Host $Msg -ForegroundColor Cyan
        } else {
            Write-Host "Unable to get location data." -ForegroundColor Red
        }
    }

    if ($UserInput.ToLower().Trim() -eq "n") {
        $WData = Get-Location
        $ip,$lat,$long,$state = $WData.Split(" ")

        if ($lat) {
            $Current = Get-Current -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()

            $Ctime = Get-Date
            $CTemp = $Current.current.temperature_2m
            $CHmd = $Current.current.relative_humidity_2m
            $CPRC = $Current.current.precipitation
            $CCld = $Current.current.cloud_cover
            $CWinds = $Current.current.wind_speed_10m
            $CWindd = $Current.current.wind_direction_10m
            $WindDir = Get-WindRoseDirection -degree $CWindd
            #$Gusts = $Current.current.wind_gusts_10m

            $CurrMsg = "`n*** Current: {0}`n`nTemp: {1}°F`nHumidity: {2}%`nPrecipitation: {3}`"`nCloud Cover: {4}%`nWind Speed: {5} mph`nDirection: {6}`nRADAR: https://radar.weather.gov/" -f $Ctime,$CTemp,$CHmd,$CPRC,$CCld,$CWinds,$WindDir
            Write-Host $CurrMsg -ForegroundColor Green
        } else {
            Write-Host "Unable to get location data." -ForegroundColor Red
        }
    }
}

Screenshot

PowerShell weather screenshot