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) }