moving to open apis
This commit is contained in:
parent
7d14f3a9df
commit
b7c6c9e26a
185
location.go
Normal file
185
location.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// free api for reverse geocoding based on IP
|
||||||
|
const revGeoURL = "http://ip-api.com/json/"
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Lat float64
|
||||||
|
Lon float64
|
||||||
|
City string
|
||||||
|
State string
|
||||||
|
Country string
|
||||||
|
Zipcode string
|
||||||
|
// NWS specific location data
|
||||||
|
// Station string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocation() *Location {
|
||||||
|
return &Location{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Location) UnmarshalJSON(b []byte) error {
|
||||||
|
|
||||||
|
var aux struct {
|
||||||
|
Lat float64 `json:"lat,string"`
|
||||||
|
Lon float64 `json:"lon,string"`
|
||||||
|
Address struct {
|
||||||
|
City string `json:"city"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Zipcode string `json:"postcode"`
|
||||||
|
} `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &aux); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy over values
|
||||||
|
l.Lat = aux.Lat
|
||||||
|
l.Lon = aux.Lon
|
||||||
|
l.City = aux.Address.City
|
||||||
|
l.State = aux.Address.State
|
||||||
|
l.Country = aux.Address.Country
|
||||||
|
l.Zipcode = aux.Address.Zipcode
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchLocations(query string) ([]Location, error) {
|
||||||
|
// url to perform location queries against
|
||||||
|
//locations := []Location{}
|
||||||
|
queryURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search?q=%s&format=jsonv2&addressdetails=1", query)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, queryURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return []Location{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent",
|
||||||
|
"weather/0.1 (https://git.keegandeppe.com/kdeppe/weather; contact=19keegandeppe@gmail.com)")
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return []Location{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return []Location{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var locations []Location
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &locations); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// l := NewLocation()
|
||||||
|
// l.UnmarshalJSON(b)
|
||||||
|
|
||||||
|
fmt.Printf("%+v\n", locations)
|
||||||
|
|
||||||
|
return locations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (l *Location) getCoords() error {
|
||||||
|
//
|
||||||
|
// res, err := http.Get(revGeoURL)
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// body, err := io.ReadAll(res.Body)
|
||||||
|
// res.Body.Close()
|
||||||
|
// if res.StatusCode > 299 {
|
||||||
|
// errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
||||||
|
// return errors.New(errMsg)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // unmarshall response into struct
|
||||||
|
// if err := json.Unmarshal(body, l); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// l.Found = true
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (l *Location) getStation() error {
|
||||||
|
// // generate url based on coords
|
||||||
|
// url := fmt.Sprintf("https://api.weather.gov/points/%f,%f", l.Lat, l.Lon)
|
||||||
|
//
|
||||||
|
// res, err := http.Get(url)
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// body, err := io.ReadAll(res.Body)
|
||||||
|
// res.Body.Close()
|
||||||
|
// if res.StatusCode > 299 {
|
||||||
|
// errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
||||||
|
// return errors.New(errMsg)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // send another request to station URL to find closest
|
||||||
|
// stationURL := gjson.Get(string(body), "properties.observationStations")
|
||||||
|
//
|
||||||
|
// res, err = http.Get(stationURL.String())
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// body, err = io.ReadAll(res.Body)
|
||||||
|
// res.Body.Close()
|
||||||
|
// if res.StatusCode > 299 {
|
||||||
|
// errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
||||||
|
// return errors.New(errMsg)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// station := gjson.Get(string(body), "features.0.properties.stationIdentifier")
|
||||||
|
//
|
||||||
|
// if station.String() == "" {
|
||||||
|
// return errors.New("Station not found")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// l.Station = station.String()
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for debugging
|
||||||
|
func (l *Location) String() string {
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s, %s %s (%f, %f)", l.City, l.State, l.Zipcode, l.Lat, l.Lon)
|
||||||
|
}
|
||||||
224
main.go
224
main.go
@ -1,187 +1,89 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"time"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// free api for reverse geocoding based on IP
|
|
||||||
const revGeoURL = "http://ip-api.com/json/"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
SearchLocations("Somerville")
|
||||||
|
|
||||||
|
return
|
||||||
w := NewWeather()
|
w := NewWeather()
|
||||||
|
|
||||||
if err := w.getLatest(); err != nil {
|
// if err := w.getLatest(); err != nil {
|
||||||
panic(err)
|
// panic(err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
fmt.Println(w)
|
fmt.Println(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HourForecast struct {
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
Temperature float64
|
||||||
|
TemperatureUnit string
|
||||||
|
ProbabilityOfPercipitation float64
|
||||||
|
WindSpeed float64
|
||||||
|
WindDirection string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
type Weather struct {
|
type Weather struct {
|
||||||
Temperature float64
|
|
||||||
Humidity float64
|
|
||||||
Conditions string
|
|
||||||
Icon string
|
|
||||||
*Location
|
*Location
|
||||||
Static bool //
|
Forecast []*HourForecast
|
||||||
|
ForecastURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWeather() *Weather {
|
func NewWeather() *Weather {
|
||||||
l := NewLocation()
|
l := NewLocation()
|
||||||
// try to set location
|
// try to set location
|
||||||
if err := l.getCoords(); err != nil {
|
// if err := l.getCoords(); err != nil {
|
||||||
panic(err)
|
// panic(err)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if err := l.getStation(); err != nil {
|
// if err := l.getStation(); err != nil {
|
||||||
panic(err)
|
// panic(err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
return &Weather{
|
return &Weather{
|
||||||
Location: l,
|
Location: l,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Weather) getLatest() error {
|
// func (w *Weather) getLatest() error {
|
||||||
url := fmt.Sprintf("https://api.weather.gov/stations/%s/observations/latest", w.Location.Station)
|
// url := fmt.Sprintf("https://api.weather.gov/stations/%s/observations/latest", w.Location.Station)
|
||||||
|
//
|
||||||
res, err := http.Get(url)
|
// res, err := http.Get(url)
|
||||||
|
//
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
body, err := io.ReadAll(res.Body)
|
// body, err := io.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
// res.Body.Close()
|
||||||
if res.StatusCode > 299 {
|
// if res.StatusCode > 299 {
|
||||||
errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
// errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
||||||
return errors.New(errMsg)
|
// return errors.New(errMsg)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
temp := gjson.Get(string(body), "properties.temperature.value")
|
// temp := gjson.Get(string(body), "properties.temperature.value")
|
||||||
humidity := gjson.Get(string(body), "properties.relativeHumidity.value")
|
// humidity := gjson.Get(string(body), "properties.relativeHumidity.value")
|
||||||
cond := gjson.Get(string(body), "properties.textDescription")
|
// cond := gjson.Get(string(body), "properties.textDescription")
|
||||||
|
//
|
||||||
// convert to Farenheit
|
// // convert to Farenheit
|
||||||
w.Temperature = temp.Float()*(9.0/5) + 32
|
// w.Temperature = temp.Float()*(9.0/5) + 32
|
||||||
w.Humidity = humidity.Float()
|
// w.Humidity = humidity.Float()
|
||||||
w.Conditions = cond.String()
|
// w.Conditions = cond.String()
|
||||||
|
//
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (w *Weather) String() string {
|
// func (w *Weather) String() string {
|
||||||
return fmt.Sprintf("%s %.1f deg %.1f%% RH in %s, %s", w.Conditions, w.Temperature, w.Humidity, w.Location.City, w.Location.State)
|
// return fmt.Sprintf("%s %.1f deg %.1f%% RH in %s, %s", w.Conditions, w.Temperature, w.Humidity, w.Location.City, w.Location.State)
|
||||||
}
|
// }
|
||||||
|
|
||||||
type Location struct {
|
|
||||||
Lat float32 `json:"lat"`
|
|
||||||
Lon float32 `json:"lon"`
|
|
||||||
City string `json:"city"`
|
|
||||||
State string `json:"region"`
|
|
||||||
Zipcode string `json:"zip"`
|
|
||||||
Found bool
|
|
||||||
// NWS specific location data
|
|
||||||
Station string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLocation() *Location {
|
|
||||||
return &Location{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Location) getCoords() error {
|
|
||||||
|
|
||||||
res, err := http.Get(revGeoURL)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if res.StatusCode > 299 {
|
|
||||||
errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshall response into struct
|
|
||||||
if err := json.Unmarshal(body, l); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Found = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Location) getStation() error {
|
|
||||||
// generate url based on coords
|
|
||||||
url := fmt.Sprintf("https://api.weather.gov/points/%f,%f", l.Lat, l.Lon)
|
|
||||||
|
|
||||||
res, err := http.Get(url)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if res.StatusCode > 299 {
|
|
||||||
errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// send another request to station URL to find closest
|
|
||||||
stationURL := gjson.Get(string(body), "properties.observationStations")
|
|
||||||
|
|
||||||
res, err = http.Get(stationURL.String())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if res.StatusCode > 299 {
|
|
||||||
errMsg := fmt.Sprintf("Request failed with status code: %d\n", res.StatusCode)
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
station := gjson.Get(string(body), "features.0.properties.stationIdentifier")
|
|
||||||
|
|
||||||
if station.String() == "" {
|
|
||||||
return errors.New("Station not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Station = station.String()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// for debugging
|
|
||||||
func (l *Location) String() string {
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s, %s %s (%f, %f) station: %s", l.City, l.State, l.Zipcode, l.Lat, l.Lon, l.Station)
|
|
||||||
}
|
|
||||||
|
|||||||
462
weather.sh
Executable file
462
weather.sh
Executable file
@ -0,0 +1,462 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# client version, calls server for actual API info
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
usage: $0 [-a KEY][-s QUERY | -z ZIPCODE][-u <c|f>][-p][-i][-l][-h]
|
||||||
|
|
||||||
|
Queries OpenWeatherMapAPI for weather or lat/long
|
||||||
|
options:
|
||||||
|
-a, --apikey set the api key to use to KEY
|
||||||
|
-l, --location print the location being used
|
||||||
|
-p, --pretty enables nerdfont symbols
|
||||||
|
-d, --desc prints the conditions text as well
|
||||||
|
-z, --zipcode returns the weather for ZIPCODE
|
||||||
|
-s, --search returns the weather for QUERY
|
||||||
|
-u, --units sets the temperature units
|
||||||
|
-i, --icon_test tests the icons used
|
||||||
|
-r, --radar displays the radar for a given location
|
||||||
|
-f, --forecast displays the forecast a given location
|
||||||
|
-h, --help show this message
|
||||||
|
|
||||||
|
ZIPCODE sets the location to the given zipcode
|
||||||
|
to narrow results, include the ISO country code
|
||||||
|
ex) -z "02139,US"
|
||||||
|
|
||||||
|
QUERY sets the location to the search
|
||||||
|
QUERY format is "CITY,REGION,COUNTRY" where
|
||||||
|
EOF
|
||||||
|
exit $1
|
||||||
|
}
|
||||||
|
|
||||||
|
WORKING_DIR="$HOME/.local/share/weather"
|
||||||
|
WEATHER_FILE=".env"
|
||||||
|
|
||||||
|
TIMEOUT=60 # timeout for info
|
||||||
|
TIME=$(date +%s)
|
||||||
|
|
||||||
|
|
||||||
|
if [[ ! -d "$WORKING_DIR" ]] ; then
|
||||||
|
mkdir -p "$WORKING_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# going to weather directory
|
||||||
|
cd "$WORKING_DIR"
|
||||||
|
|
||||||
|
print_weather() {
|
||||||
|
# prints the weather
|
||||||
|
|
||||||
|
# check to see if it is expired
|
||||||
|
check_expiration
|
||||||
|
|
||||||
|
# checking if description enabled
|
||||||
|
if [[ "$DESCRIPTION" = true ]] ; then
|
||||||
|
DESC=$(echo "$CONDITIONS" | awk '{for(i=1;i<=NF;i++){ $i=toupper(substr($i,1,1)) substr($i,2) }}1')
|
||||||
|
DESC=$(printf '%s ' "$DESC")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PRETTY" = true ]] ; then
|
||||||
|
# print with NF icons
|
||||||
|
icon=$(get_icon "$WEATHER_ICON")
|
||||||
|
humidity_icon=$(echo -e '\ue373')
|
||||||
|
|
||||||
|
# checking units
|
||||||
|
if [[ "$UNIT" == "C" ]] ; then
|
||||||
|
temp_icon=$(echo -e '\ufa03')
|
||||||
|
else
|
||||||
|
temp_icon=$(echo -e '\ufa04')
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# default printing
|
||||||
|
humidity_icon="% RH"
|
||||||
|
if [[ "$UNIT" == "C" ]] ; then
|
||||||
|
temp_icon="*C"
|
||||||
|
else
|
||||||
|
temp_icon="*F"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# printing
|
||||||
|
printf '%s%s %.1f%s %d%s in %s\n' "$DESC" "$icon" $TEMPERATURE "$temp_icon" $HUMIDITY "$humidity_icon" "$CITY"
|
||||||
|
|
||||||
|
# save before exiting
|
||||||
|
save_info
|
||||||
|
}
|
||||||
|
|
||||||
|
print_location() {
|
||||||
|
# prints the location being used for the weather
|
||||||
|
|
||||||
|
# uses reverse geocoding
|
||||||
|
LOCATION_URL="https://api.openweathermap.org/geo/1.0/reverse?lat=$LAT&lon=$LON&appid=$API_KEY"
|
||||||
|
LOCATION=$(curl --silent -fL "$LOCATION_URL" | gojq '.[0] | del(.local_names)')
|
||||||
|
|
||||||
|
name=$(echo "$LOCATION" | gojq '.name' | tr -d '"')
|
||||||
|
state=$(echo "$LOCATION" | gojq '.state' | tr -d '"')
|
||||||
|
country=$(echo "$LOCATION" | gojq '.country' | tr -d '"')
|
||||||
|
|
||||||
|
printf 'Location is set to %s in %s, %s\n' "$name" "$state" "$country"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_expiration() {
|
||||||
|
# checks if the info is expired
|
||||||
|
if [[ $TIME -gt $EXPIRATION ]] ; then
|
||||||
|
# expired
|
||||||
|
get_weather
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_location() {
|
||||||
|
# searching for user location
|
||||||
|
if [[ -n "$SEARCH" ]] ; then
|
||||||
|
# search
|
||||||
|
LOCATION_URL="https://api.openweathermap.org/geo/1.0/$SEARCH&appid=$API_KEY&limit=5"
|
||||||
|
RESULTS=$(curl --silent -fL "$LOCATION_URL")
|
||||||
|
# check if its an array
|
||||||
|
is_array=$(echo "$RESULTS" | gojq 'if type=="array" then 1 else 0 end')
|
||||||
|
|
||||||
|
if [[ $is_array -eq 1 ]] ; then
|
||||||
|
# returned result is an array
|
||||||
|
|
||||||
|
NUM_RESULTS=$(echo "$RESULTS" | gojq '. | length')
|
||||||
|
if [[ $NUM_RESULTS -gt 1 ]] ; then
|
||||||
|
# provide menu for multiple results
|
||||||
|
for ((i=0; i < $NUM_RESULTS; i++)); do
|
||||||
|
# adding options
|
||||||
|
res=$(echo "$RESULTS" | gojq ".[$i]")
|
||||||
|
name=$(echo "$res" | gojq '.name' | tr -d '"')
|
||||||
|
state=$(echo "$res" | gojq '.state' | tr -d '"')
|
||||||
|
country=$(echo "$res" | gojq '.country' | tr -d '"')
|
||||||
|
opt=$(printf '%s %s, %s' "$name" "$state" "$country")
|
||||||
|
OPTIONS+=($(($i+1)) "$opt")
|
||||||
|
done
|
||||||
|
|
||||||
|
# executing dialog menu
|
||||||
|
exec 3>&1
|
||||||
|
SELECTION=$(dialog \
|
||||||
|
--title 'Multiple Locations Found!' \
|
||||||
|
--clear \
|
||||||
|
--cancel-label 'Exit' \
|
||||||
|
--ok-label 'Select' \
|
||||||
|
--menu 'Please select the desired location. If none of the options look correct, try making refining your search by including a country code and/or region' 0 0 4 \
|
||||||
|
"${OPTIONS[@]}" \
|
||||||
|
2>&1 1>&3)
|
||||||
|
exit_status=$?
|
||||||
|
exec 3>&-
|
||||||
|
clear
|
||||||
|
|
||||||
|
if [[ $exit_status -eq 1 || $exit_satus -eq 255 ]] ; then
|
||||||
|
# canceled or escaped
|
||||||
|
echo "Location not changed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# fix index
|
||||||
|
SELECTION=$(($SELECTION - 1))
|
||||||
|
fi
|
||||||
|
# updating result
|
||||||
|
RESULTS=$(echo "$RESULTS" | gojq ".[$SELECTION]")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update info
|
||||||
|
LAT=$(echo "$RESULTS" | gojq ".lat")
|
||||||
|
LON=$(echo "$RESULTS" | gojq ".lon")
|
||||||
|
CITY=$(echo "$RESULTS" | gojq ".name" | tr -d '"')
|
||||||
|
else
|
||||||
|
# no search, default to user IP
|
||||||
|
|
||||||
|
url="http://ip-api.com/csv/?fields=252"
|
||||||
|
res=$(curl --silent -fL "$url")
|
||||||
|
LAT=$(awk -F , '{print $5}' <<<"$res")
|
||||||
|
LON=$(awk -F , '{print $6}' <<<"$res")
|
||||||
|
CITY=$(awk -F , '{print $3}' <<<"$res")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check
|
||||||
|
if [[ -z "$LAT" || -z "$LON" ]] ; then
|
||||||
|
echo "No location found!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# force refresh
|
||||||
|
EXPIRATION=0
|
||||||
|
}
|
||||||
|
|
||||||
|
get_weather() {
|
||||||
|
# calls api for weather based on $LAT, $LONG
|
||||||
|
|
||||||
|
if [[ -z "$LAT" || -z "$LON" ]] ; then
|
||||||
|
# no lat or lon
|
||||||
|
get_location
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$UNIT" =~ ^[Cc]$ ]] ; then
|
||||||
|
# units set to metric
|
||||||
|
UNIT="C"
|
||||||
|
UNITS="metric"
|
||||||
|
elif [[ "$UNIT" =~ ^[Ff]$ || -z "$UNIT" ]] ; then
|
||||||
|
# imperial (default)
|
||||||
|
UNIT="F"
|
||||||
|
UNITS="imperial"
|
||||||
|
else
|
||||||
|
printf 'Unit %s unrecognized\n' "$UNIT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WEATHER_URL="https://api.openweathermap.org/data/2.5/weather?lat=$LAT&lon=$LON&appid=$API_KEY&units=$UNITS"
|
||||||
|
|
||||||
|
WEATHER=$(curl --silent -fL "$WEATHER_URL")
|
||||||
|
|
||||||
|
CONDITIONS=$(echo $WEATHER | gojq -r '.weather[0].description')
|
||||||
|
TEMPERATURE=$(echo $WEATHER | gojq -r '.main.temp')
|
||||||
|
HUMIDITY=$(echo $WEATHER | gojq -r '.main.humidity')
|
||||||
|
WEATHER_ICON=$(echo $WEATHER | gojq -r '.weather[0].icon')
|
||||||
|
EXPIRATION=$(($TIME+$TIMEOUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
update_key() {
|
||||||
|
# updates the API key used
|
||||||
|
|
||||||
|
printf 'Testing API key %s... ' "$API_KEY"
|
||||||
|
WEATHER_URL="https://api.openweathermap.org/data/2.5/weather?lat=42.3736&lon=71.1097&appid=$API_KEY"
|
||||||
|
CODE=$(curl --silent "$WEATHER_URL" | gojq '.cod' | tr -d '"')
|
||||||
|
|
||||||
|
if [[ $CODE -eq 401 ]] ; then
|
||||||
|
# API_KEY error
|
||||||
|
printf 'Error: Invalid API Key "%s"!\n' "$API_KEY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'Success: Key Updated!\n'
|
||||||
|
|
||||||
|
# saving new key
|
||||||
|
save_info
|
||||||
|
}
|
||||||
|
|
||||||
|
save_info() {
|
||||||
|
# saves set env vars to file
|
||||||
|
printf '%s="%s"\n' \
|
||||||
|
"API_KEY" "$API_KEY" \
|
||||||
|
"PARSER" "$PARSER" \
|
||||||
|
"LAT" "$LAT" \
|
||||||
|
"LON" "$LON" \
|
||||||
|
"CITY" "$CITY" \
|
||||||
|
"TEMPERATURE" "$TEMPERATURE" \
|
||||||
|
"HUMIDITY" "$HUMIDITY" \
|
||||||
|
"UNIT" "$UNIT" \
|
||||||
|
"WEATHER_ICON" "$WEATHER_ICON" \
|
||||||
|
"CONDITIONS" "$CONDITIONS" \
|
||||||
|
"EXPIRATION" "$EXPIRATION" > "$WEATHER_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_info() {
|
||||||
|
# loads env vars
|
||||||
|
if [[ ! -e "$WEATHER_FILE" ]] ; then
|
||||||
|
# generates blank info on fresh installs
|
||||||
|
|
||||||
|
# test for gojq
|
||||||
|
PARSER=gojq
|
||||||
|
if ! command -v $PARSER ; then
|
||||||
|
# test for jq
|
||||||
|
PARSER=jq
|
||||||
|
if ! command -v $PARSER ; then
|
||||||
|
echo "$1 depends on jq or gojq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
save_info
|
||||||
|
fi
|
||||||
|
source $WEATHER_FILE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
display_forecast() {
|
||||||
|
# two step process, gets weather station from NWS, then mpv to play radar
|
||||||
|
if [[ -z "$LAT" || -z "$LON" ]] ; then
|
||||||
|
echo "Location not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Loading weather..."
|
||||||
|
if [[ -n "$PRETTY" ]] ; then
|
||||||
|
# nerdfont
|
||||||
|
curl -fsSL "https://v2d.wttr.in/${LAT},${LON}?F" | less -R
|
||||||
|
else
|
||||||
|
curl -fsSL "https://wttr.in/${LAT},${LON}?F" | less -R
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
display_radar() {
|
||||||
|
# two step process, gets weather station from NWS, then mpv to play radar
|
||||||
|
if [[ -z "$LAT" || -z "$LON" ]] ; then
|
||||||
|
echo "Location not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# grabbing nws station
|
||||||
|
NWS=$(curl -fsSL "https://api.weather.gov/points/$LAT,$LON")
|
||||||
|
STATION=$(echo "$NWS" | gojq '.properties.radarStation' | tr -d '"')
|
||||||
|
|
||||||
|
# validation
|
||||||
|
if [[ -z "$STATION" ]] ; then
|
||||||
|
echo "NWS Station error!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# mpv to play weather in fullscreen
|
||||||
|
echo "Fetching weather for NWS Station $STATION..."
|
||||||
|
mpv --fs --loop-file "https://radar.weather.gov/ridge/standard/${STATION}_loop.gif" >/dev/null 2>&1
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get_icon() {
|
||||||
|
# sets NF symbols
|
||||||
|
tod=$(echo "$1" | sed --expression='s/[0-9]//g')
|
||||||
|
conditions=$(echo "$1" | sed --expression='s/[^0-9]//g')
|
||||||
|
|
||||||
|
# getting icon
|
||||||
|
if [[ "$tod" == "d" ]] ; then
|
||||||
|
# day icons
|
||||||
|
case "$conditions" in
|
||||||
|
"01") echo -e '\ue30d' ;; # clear
|
||||||
|
"02") echo -e '\ue30c' ;; # scattered clouds
|
||||||
|
"03") echo -e '\ue302' ;; # broken clouds
|
||||||
|
"04") echo -e '\ue312' ;; # cloudy
|
||||||
|
"09") echo -e '\ue309' ;; # showers
|
||||||
|
"10") echo -e '\ue308' ;; # rain
|
||||||
|
"11") echo -e '\ue30f' ;; # thunderstorm
|
||||||
|
"13") echo -e '\uf2dc' ;; # snow
|
||||||
|
"50") echo -e '\ue303' ;; # mist
|
||||||
|
* ) echo -e '\ue374' ;; # unknown
|
||||||
|
esac
|
||||||
|
elif [[ "$tod" == "n" ]] ; then
|
||||||
|
# night icons
|
||||||
|
case "$conditions" in
|
||||||
|
"01") echo -e '\ue32b' ;; # clear
|
||||||
|
"02") echo -e '\ue379' ;; # scattered clouds
|
||||||
|
"03") echo -e '\ue37e' ;; # broken clouds
|
||||||
|
"04") echo -e '\ue312' ;; # cloudy
|
||||||
|
"09") echo -e '\ue326' ;; # showers
|
||||||
|
"10") echo -e '\ue325' ;; # rain
|
||||||
|
"11") echo -e '\ue32a' ;; # thunderstorm
|
||||||
|
"13") echo -e '\uf2dc' ;; # snow
|
||||||
|
"50") echo -e '\ue346' ;; # mist
|
||||||
|
* ) echo -e '\ue374' ;; # unknown
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "TOD not recognized"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
icon_test() {
|
||||||
|
#tests icons
|
||||||
|
printf 'Testing weather icons\nIf any look broken, check that NerdFont is installed\n'
|
||||||
|
|
||||||
|
printf '\nDay:\n'
|
||||||
|
printf 'Clear: \ue30d\n'
|
||||||
|
printf 'Partly Cloudy: \ue30c\n'
|
||||||
|
printf 'Cloudy: \ue302\n'
|
||||||
|
printf 'Very Cloudy: \ue312\n'
|
||||||
|
printf 'Showers: \ue309\n'
|
||||||
|
printf 'Rain: \ue308\n'
|
||||||
|
printf 'Thunderstorm: \ue30f\n'
|
||||||
|
printf 'Snow: \uf2dc\n'
|
||||||
|
printf 'Fog: \ue303\n'
|
||||||
|
# night icons
|
||||||
|
printf '\nNight:\n'
|
||||||
|
printf 'Clear: \ue32b\n'
|
||||||
|
printf 'Partly Cloudy: \ue379\n'
|
||||||
|
printf 'Cloudy: \ue37e\n'
|
||||||
|
printf 'Very Cloudy: \ue312\n'
|
||||||
|
printf 'Showers: \uf2dc\n'
|
||||||
|
printf 'Rain: \ue325\n'
|
||||||
|
printf 'Thunderstorm: \ue32a\n'
|
||||||
|
printf 'Snow: \uf2dc\n'
|
||||||
|
printf 'Fog: \ue346\n'
|
||||||
|
|
||||||
|
printf '\nAssorted:\n'
|
||||||
|
printf 'Degrees (F) \ufa04\n'
|
||||||
|
printf 'Degrees (C) \ufa03\n'
|
||||||
|
printf '%% Humidity \ue373\n'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# shifting longform
|
||||||
|
for arg in "$@"; do
|
||||||
|
shift
|
||||||
|
case "$arg" in
|
||||||
|
'--zipcode') set -- "$@" "-z" ;;
|
||||||
|
'--location') set -- "$@" "-l" ;;
|
||||||
|
'--apikey') set -- "$@" "-a" ;;
|
||||||
|
'--coords') set -- "$@" "-c" ;;
|
||||||
|
'--search') set -- "$@" "-s" ;;
|
||||||
|
'--units') set -- "$@" "-u" ;;
|
||||||
|
'--pretty') set -- "$@" "-p" ;;
|
||||||
|
'--pretty') set -- "$@" "-p" ;;
|
||||||
|
'--icon_test') set -- "$@" "-i" ;;
|
||||||
|
'--radar') set -- "$@" "-r" ;;
|
||||||
|
'--forecast') set -- "$@" "-f" ;;
|
||||||
|
'--help') set -- "$@" "-h" ;;
|
||||||
|
*) set -- "$@" "$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# loading the info
|
||||||
|
load_info
|
||||||
|
|
||||||
|
while getopts "chpldrfiz:s:u:a:" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
'a' )
|
||||||
|
API_KEY="$OPTARG"
|
||||||
|
update_key
|
||||||
|
;;
|
||||||
|
'u' )
|
||||||
|
UNIT="$OPTARG"
|
||||||
|
;;
|
||||||
|
'l' )
|
||||||
|
print_location
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
'z' )
|
||||||
|
[ -n "$SEARCH" ] && usage 1 || SEARCH="/zip?zip=$OPTARG"
|
||||||
|
;;
|
||||||
|
's' )
|
||||||
|
[ -n "$SEARCH" ] && usage 1 || SEARCH="/direct?q=$OPTARG"
|
||||||
|
;;
|
||||||
|
'p' )
|
||||||
|
PRETTY=true
|
||||||
|
;;
|
||||||
|
'd' )
|
||||||
|
DESCRIPTION=true
|
||||||
|
;;
|
||||||
|
'i' )
|
||||||
|
icon_test
|
||||||
|
;;
|
||||||
|
'r' )
|
||||||
|
display_radar
|
||||||
|
;;
|
||||||
|
'f' )
|
||||||
|
display_forecast
|
||||||
|
;;
|
||||||
|
'h' )
|
||||||
|
usage 0
|
||||||
|
;;
|
||||||
|
'?' )
|
||||||
|
usage 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# test for APIKEY
|
||||||
|
if [[ -z "$API_KEY" ]] ; then
|
||||||
|
echo "No API Key found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SEARCH" ]] ; then
|
||||||
|
# perform search
|
||||||
|
get_location
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$QUIET" ]] ; then
|
||||||
|
print_weather
|
||||||
|
fi
|
||||||
Loading…
x
Reference in New Issue
Block a user