weather/main.go
2024-04-09 11:53:44 -04:00

188 lines
3.5 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"github.com/tidwall/gjson"
)
// free api for reverse geocoding based on IP
const revGeoURL = "http://ip-api.com/json/"
func main() {
w := NewWeather()
if err := w.getLatest(); err != nil {
panic(err)
}
fmt.Println(w)
}
type Weather struct {
Temperature float64
Humidity float64
Conditions string
Icon string
*Location
Static bool //
}
func NewWeather() *Weather {
l := NewLocation()
// try to set location
if err := l.getCoords(); err != nil {
panic(err)
}
if err := l.getStation(); err != nil {
panic(err)
}
return &Weather{
Location: l,
}
}
func (w *Weather) getLatest() error {
url := fmt.Sprintf("https://api.weather.gov/stations/%s/observations/latest", w.Location.Station)
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
}
temp := gjson.Get(string(body), "properties.temperature.value")
humidity := gjson.Get(string(body), "properties.relativeHumidity.value")
cond := gjson.Get(string(body), "properties.textDescription")
// convert to Farenheit
w.Temperature = temp.Float()*(9.0/5) + 32
w.Humidity = humidity.Float()
w.Conditions = cond.String()
return nil
}
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)
}
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)
}