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:
PowerShell
x
465
465
1
#!/usr/bin/env pwsh
2
<#
3
API services used:
4
5
https://ipinfo.io
6
https://www.ip2location.io # Requires account and API key.
7
https://www.weather.gov/documentation/services-web-api
8
https://open-meteo.com
9
10
In the script folder a folder named geo will be created,
11
which should contain two files.
12
13
* Manual config: config.txt should contain:
14
15
latitude
16
longitude
17
full_state_name
18
19
The second file is station.txt and contains the weather station
20
closet to your location. This file gets created the first time
21
you fetch the weather and will limit hits to the API.
22
23
On Linux you can create a bash script to start the app and symlink
24
to /usr/local/bin as any name you like to call it whenever you need
25
it in the terminal. I used psw for ease on the memory.
26
27
* Contents of my bash script:
28
29
#!/usr/bin/env bash
30
31
pwsh /home/username/Documents/Powershell/ps-weather/psw.ps1
32
33
* The desktop file can be created in ~/.local/share/applications to add
34
as an app menu item for your DE.
35
36
Contents of my desktop file:
37
38
[Desktop Entry]
39
Version=1.0
40
Name=Powershell Weather
41
GenericName=Weather
42
Comment=Weather
43
Keywords=weather
44
Exec=pwsh /home/username/Documents/Powershell/ps-weather/psw.ps1
45
Icon=/home/username/Documents/Powershell/ps-weather/psw.png
46
Terminal=true
47
Type=Application
48
Categories=Application;X-psw;
49
50
Notes: Could have better error handling and likely better routine
51
to parse the API data, but it works for my needs.
52
#>
53
54
$IPLocationKey = "your key" # enter your api key from: https://www.ip2location.io/ip2location-documentation
55
56
$STATES = @{
57
"Alabama"="AL"
58
"Alaska"="AK"
59
"Arizona"="AZ"
60
"Arkansas"="AR"
61
"California"="CA"
62
"Colorado"="CO"
63
"Connecticut"="CT"
64
"Delaware"="DE"
65
"Florida"="FL"
66
"Georgia"="GA"
67
"Hawaii"="HI"
68
"Idaho"="ID"
69
"Illinois"="IL"
70
"Indiana"="IN"
71
"Iowa"="IA"
72
"Kansas"="KS"
73
"Kentucky"="KY"
74
"Louisiana"="LA"
75
"Maine"="ME"
76
"Maryland"="MD"
77
"Massachusetts"="MA"
78
"Michigan"="MI"
79
"Minnesota"="MN"
80
"Mississippi"="MS"
81
"Missouri"="MO"
82
"Montana"="MT"
83
"Nebraska"="NE"
84
"Nevada"="NV"
85
"New Hampshire"="NH"
86
"New Jersey"="NJ"
87
"New Mexico"="NM"
88
"New York"="NY"
89
"North Carolina"="NC"
90
"North Dakota"="ND"
91
"Ohio"="OH"
92
"Oklahoma"="OK"
93
"Oregon"="OR"
94
"Pennsylvania"="PA"
95
"Rhode Island"="RI"
96
"South Carolina"="SC"
97
"South Dakota"="SD"
98
"Tennessee"="TN"
99
"Texas"="TX"
100
"Utah"="UT"
101
"Vermont"="VT"
102
"Virginia"="VA"
103
"Washington"="WA"
104
"West Virginia"="WV"
105
"Wisconsin"="WI"
106
"Wyoming"="WY"
107
"District of Columbia" = "DC"
108
"Guam"="GU"
109
"Marshall Islands"="MH"
110
"Northern Mariana Island"="MP"
111
"Puerto Rico"="PR"
112
"Virgin Islands"="VI"
113
}
114
115
function Get-WindRoseDirection() {
116
param (
117
$degree
118
)
119
if ($degree -ge 0 -and $degree -lt 22.5) {
120
$WindDirection = "N"
121
} elseif ($degree -ge 22.5 -and $degree -lt 45) {
122
$WindDirection = "NNE"
123
} elseif ($degree -ge 45 -and $degree -lt 67.5) {
124
$WindDirection = "NE"
125
} elseif ($degree -ge 67.5 -and $degree -lt 90) {
126
$WindDirection = "ENE"
127
} elseif ($degree -ge 90 -and $degree -lt 112.5) {
128
$WindDirection = "E"
129
} elseif ($degree -ge 112.5 -and $degree -lt 135) {
130
$WindDirection = "ESE"
131
} elseif ($degree -ge 135 -and $degree -lt 157.5) {
132
$WindDirection = "SE"
133
} elseif ($degree -ge 157.5 -and $degree -lt 180) {
134
$WindDirection = "SSE"
135
} elseif ($degree -ge 180 -and $degree -lt 202.5) {
136
$WindDirection = "S"
137
} elseif ($degree -ge 202.5 -and $degree -lt 225) {
138
$WindDirection = "SSW"
139
} elseif ($degree -ge 225 -and $degree -lt 247.5) {
140
$WindDirection = "SW"
141
} elseif ($degree -ge 247.5 -and $degree -lt 270) {
142
$WindDirection = "WSW"
143
} elseif ($degree -ge 270 -and $degree -lt 292.5) {
144
$WindDirection = "W"
145
} elseif ($degree -ge 292.5 -and $degree -lt 315) {
146
$WindDirection = "WNW"
147
} elseif ($degree -ge 315 -and $degree -lt 337.5) {
148
$WindDirection = "NW"
149
} elseif ($degree -ge 337.5 -and $degree -lt 360) {
150
$WindDirection = "NNW"
151
} elseif ($degree -eq 360) {
152
$WindDirection = "N"
153
}
154
return $WindDirection
155
}
156
157
function Get-FrontFacingIP() {
158
159
$StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
160
$StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"
161
162
if (Test-Path -path $StoreFile) {
163
164
$UserIP = Get-Content -Path $StoreFile
165
$UserIP = $UserIP.Trim()
166
167
}
168
169
return $UserIP.Trim()
170
}
171
172
function Get-Geo() {
173
param (
174
$ip,
175
$GEOKEY
176
)
177
178
$infoService = "https://api.ip2location.io/?key=$GEOKEY&ip=$ip"
179
$geotable = Invoke-RestMethod -Method Get -Uri $infoService
180
181
return $GeoTable.latitude, $GeoTable.longitude, $GeoTable.region_name
182
}
183
184
function Set-Location() {
185
param (
186
$UserIP = $null,
187
$GeoKey
188
)
189
190
$StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
191
$StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"
192
193
if (-not(Test-Path -Path $StorePath)) {
194
New-Item -ItemType "directory" -Path $StorePath -Force
195
}
196
197
if ($UserSupplied) {
198
Set-Content -Path $StoreFile -Value $UserSupplied -Force
199
} else {
200
$IPService = "https://ipinfo.io/ip"
201
$UserIP = Invoke-RestMethod -Method Get -URI $IPService
202
Set-Content -Path $StoreFile -Value $UserIP -Force
203
}
204
$Lat = ""
205
$Long = ""
206
$State = ""
207
if ($GeoKey.Trim().Length -gt 0) {
208
$Lat, $Long, $State = Get-Geo -ip $UserIP -GEOKEY $GeoKey
209
Add-Content -Path $StoreFile -Value $Lat -Force
210
Add-Content -Path $StoreFile -Value $Long -Force
211
Add-Content -Path $StoreFile -Value $State -Force
212
} else {
213
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."
214
}
215
return $Lat, $Long, $State
216
}
217
218
function Get-Location() {
219
$StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
220
$StoreFile = Join-Path -Path $StorePath -ChildPath "config.txt"
221
222
if (Test-Path -path $StoreFile) {
223
224
$Location = Get-Content -Path $StoreFile
225
226
}
227
228
return $Location
229
}
230
231
function Get-Alerts() {
232
param (
233
$STATE
234
)
235
$endpoint = "https://api.weather.gov/alerts/active?area=$STATE"
236
$AlertTable = Invoke-RestMethod -Method Get -Uri $endpoint
237
return $AlertTable
238
}
239
function Get-Forecast() {
240
param (
241
$LATITUDE,
242
$LONGITUDE
243
)
244
245
$StorePath = Join-Path -Path $PSScriptRoot -ChildPath "geo"
246
$StoreFile = Join-Path -Path $StorePath -ChildPath "station.txt"
247
248
if (Test-Path -path $StoreFile) {
249
250
$fcendpoint = Get-Content -Path $StoreFile
251
252
} else {
253
254
$endpoint = "https://api.weather.gov/points/$LATITUDE,$LONGITUDE"
255
$DataTable = Invoke-RestMethod -Method Get -Uri $endpoint
256
$fcendpoint = $DataTable.properties.forecast
257
258
Add-Content -Path $StoreFile -Value $fcendpoint -Force
259
}
260
261
$ForecastTable = Invoke-RestMethod -Method Get -Uri $fcendpoint.Trim()
262
263
return $ForecastTable.properties.periods
264
}
265
266
function Get-Current() {
267
param (
268
$LATITUDE,
269
$LONGITUDE
270
)
271
$endpoint = "https://api.open-meteo.com/v1/gfs?latitude=$LATITUDE&longitude=$LONGITUDE¤t=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"
272
273
$DataTable = Invoke-RestMethod -Method Get -Uri $endpoint
274
275
return $DataTable
276
}
277
278
clear
279
$UserInput = ""
280
While($UserInput.ToLower().Trim() -ne "q") {
281
282
$UserInput = ""
283
$UserInput = Read-Host -Prompt "> l (save location) n (now) f (forcast today) e (forcast extended) a (alerts) c (clear) q (quit)"
284
285
if ($null -eq $UserInput) {
286
$UserInput = ""
287
}
288
289
if ($UserInput.ToLower().Trim() -eq "q") {
290
clear
291
}
292
293
if ($UserInput.ToLower().Trim() -eq "l") {
294
$UserIP = Read-Host -Prompt "Enter your front facing IP. Leave empty to attempt automatic lookup"
295
if ($UserIP) {
296
$WData = Set-Location -UserIP $IPInput.Trim() -GeoKey $IPLocationKey
297
} else {
298
$WData = Set-Location
299
}
300
301
if ($WData[0]) {
302
Write-Host "Saved."
303
} else {
304
Write-Host "Unable to get lat,long data."
305
}
306
}
307
308
if ($UserInput.ToLower().Trim() -eq "c") {
309
clear
310
}
311
312
if ($UserInput.ToLower().Trim() -eq "a") {
313
$WData = Get-Location
314
$ip,$lat,$long,$state = $WData.Split(" ")
315
if ($state) {
316
$abv = $STATES[$state]
317
$Alert = Get-Alerts -STATE $abv.Trim()
318
$Alerts = $Alert.features
319
if ($Alerts.Length -gt 0) {
320
$Msg = ""
321
ForEach ($a in $Alerts) {
322
$headline = $a.properties.headline
323
$effdate = $a.properties.effective
324
$enddate = $a.properties.expires
325
$locales = $a.properties.areaDesc
326
$desc = $a.properties.description
327
$Msg += "`n*** {0}`n`nEffective: {1}`nEnds: {2}`n`nAffects: {3}`n`nAlert: {4}`n" -f $headline,$effdate,$enddate,$locales,$desc
328
}
329
} else { $Msg = "No alerts."}
330
Write-Host $Msg -ForegroundColor Red
331
} else {
332
Write-Host "Unable to get location data." -ForegroundColor Red
333
}
334
}
335
336
if ($UserInput.ToLower().Trim() -eq "e") {
337
$WData = Get-Location
338
$ip,$lat,$long,$state = $WData.Split(" ")
339
$Forecasts = Get-Forecast -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
340
$Msg = ""
341
if ($lat) {
342
ForEach ($periods in $Forecasts) {
343
$Count = 0
344
#if ($Count -eq 3) { break }
345
ForEach ($period in $periods) {
346
$name = $period.name
347
$temp = "{0} {1}" -f $period.temperature, $period.temperatureUnit
348
if ($null -eq $period.dewpoint.value) {
349
$dewp = "n/a"
350
} else {
351
$dewp = "{0} {1}" -f $period.dewpoint.value.ToString("#.##"), $period.dewpoint.unitCode.Split(":")[1]
352
}
353
354
if ($null -eq $period.relativeHumidity.value) {
355
$relhm = "0%"
356
} else {
357
$relhm = "{0}%" -f $period.relativeHumidity.value
358
}
359
$wind = "0"
360
$direction = "n/a"
361
if ($period.windSpeed) {
362
$wind = $period.windSpeed
363
$direction = $period.windDirection
364
}
365
366
$forecast = $period.detailedForecast
367
368
$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
369
$Count += 1
370
}
371
}
372
Write-Host $Msg -ForegroundColor Cyan
373
} else {
374
Write-Host "Unable to get location data." -ForegroundColor Red
375
}
376
}
377
378
if ($UserInput.ToLower().Trim() -eq "f") {
379
380
$WData = Get-Location
381
$ip,$lat,$long,$state = $WData.Split(" ")
382
383
if ($lat) {
384
$Current = Get-Current -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
385
386
$Ctime = Get-Date
387
$CTemp = $Current.current.temperature_2m
388
$CHmd = $Current.current.relative_humidity_2m
389
$CPRC = $Current.current.precipitation
390
$CCld = $Current.current.cloud_cover
391
$CWinds = $Current.current.wind_speed_10m
392
$CWindd = $Current.current.wind_direction_10m
393
$WindDir = Get-WindRoseDirection -degree $CWindd
394
#$Gusts = $Current.current.wind_gusts_10m
395
396
$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
397
398
$Forecasts = Get-Forecast -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
399
$Msg = ""
400
$count = 0
401
ForEach ($periods in $Forecasts) {
402
ForEach ($period in $periods) {
403
404
if ($Count -eq 2) {
405
break
406
}
407
408
$name = $period.name
409
$temp = "{0} {1}" -f $period.temperature, $period.temperatureUnit
410
if ($null -eq $period.dewpoint.value) {
411
$dewp = "n/a"
412
} else {
413
$dewp = "{0} {1}" -f $period.dewpoint.value.ToString("#.##"), $period.dewpoint.unitCode.Split(":")[1]
414
}
415
416
if ($null -eq $period.relativeHumidity.value) {
417
$relhm = "0%"
418
} else {
419
$relhm = "{0}%" -f $period.relativeHumidity.value
420
}
421
$wind = "0"
422
$direction = "n/a"
423
if ($period.windSpeed) {
424
$wind = $period.windSpeed
425
$direction = $period.windDirection
426
}
427
428
$forecast = $period.detailedForecast
429
430
$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
431
432
$Count += 1
433
}
434
}
435
Write-Host $CurrMsg -ForegroundColor Green
436
Write-Host $Msg -ForegroundColor Cyan
437
} else {
438
Write-Host "Unable to get location data." -ForegroundColor Red
439
}
440
}
441
442
if ($UserInput.ToLower().Trim() -eq "n") {
443
$WData = Get-Location
444
$ip,$lat,$long,$state = $WData.Split(" ")
445
446
if ($lat) {
447
$Current = Get-Current -LATITUDE $lat.Trim() -LONGITUDE $long.Trim()
448
449
$Ctime = Get-Date
450
$CTemp = $Current.current.temperature_2m
451
$CHmd = $Current.current.relative_humidity_2m
452
$CPRC = $Current.current.precipitation
453
$CCld = $Current.current.cloud_cover
454
$CWinds = $Current.current.wind_speed_10m
455
$CWindd = $Current.current.wind_direction_10m
456
$WindDir = Get-WindRoseDirection -degree $CWindd
457
#$Gusts = $Current.current.wind_gusts_10m
458
459
$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
460
Write-Host $CurrMsg -ForegroundColor Green
461
} else {
462
Write-Host "Unable to get location data." -ForegroundColor Red
463
}
464
}
465
}
