package main import ( "encoding/csv" "encoding/json" "fmt" "io" "log" "net/http" "os" "strconv" "strings" ) type coordsResponse []struct { Name string `json:"name"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` Country string `json:"country"` State string `json:"state"` } type Coords struct { Name string `json:"name"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` Country string `json:"country"` State string `json:"state"` } type Rain struct { Hour float64 `json:"1h"` } type Snow struct { Hour float64 `json:"1h"` } type WeatherResponse struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` Timezone string `json:"timezone"` TimezoneOffset int `json:"timezone_offset"` Current struct { Dt int64 `json:"dt"` Sunrise int `json:"sunrise"` Sunset int `json:"sunset"` Temp float64 `json:"temp"` FeelsLike float64 `json:"feels_like"` Pressure int `json:"pressure"` Humidity int `json:"humidity"` DewPoint float64 `json:"dew_point"` Uvi float64 `json:"uvi"` Clouds int `json:"clouds"` Visibility int `json:"visibility"` WindSpeed float64 `json:"wind_speed"` WindDeg int `json:"wind_deg"` Rain *Rain `json:"rain,omitempty"` Snow *Snow `json:"snow,omitempty"` Weather []struct { ID int `json:"id"` Main string `json:"main"` Description string `json:"description"` Icon string `json:"icon"` } `json:"weather"` } `json:"current"` Minutely []struct { Dt int64 `json:"dt"` Precipitation float64 `json:"precipitation"` } `json:"minutely"` Hourly []struct { Dt int64 `json:"dt"` Temp float64 `json:"temp"` FeelsLike float64 `json:"feels_like"` Pressure int `json:"pressure"` Humidity int `json:"humidity"` DewPoint float64 `json:"dew_point"` Uvi float64 `json:"uvi"` Clouds int `json:"clouds"` Visibility int `json:"visibility,omitempty"` WindSpeed float64 `json:"wind_speed"` WindDeg int `json:"wind_deg"` WindGust float64 `json:"wind_gust"` Weather []struct { ID int `json:"id"` Main string `json:"main"` Description string `json:"description"` Icon string `json:"icon"` } `json:"weather"` Pop float64 `json:"pop"` Rain *Rain `json:"rain,omitempty"` Snow *Snow `json:"snow,omitempty"` } `json:"hourly"` Daily []struct { Dt int64 `json:"dt"` Sunrise int `json:"sunrise"` Sunset int `json:"sunset"` Moonrise int `json:"moonrise"` Moonset int `json:"moonset"` MoonPhase float64 `json:"moon_phase"` Summary string `json:"summary"` Temp struct { Day float64 `json:"day"` Min float64 `json:"min"` Max float64 `json:"max"` Night float64 `json:"night"` Eve float64 `json:"eve"` Morn float64 `json:"morn"` } `json:"temp"` FeelsLike struct { Day float64 `json:"day"` Night float64 `json:"night"` Eve float64 `json:"eve"` Morn float64 `json:"morn"` } `json:"feels_like"` Pressure int `json:"pressure"` Humidity int `json:"humidity"` DewPoint float64 `json:"dew_point"` WindSpeed float64 `json:"wind_speed"` WindDeg int `json:"wind_deg"` WindGust float64 `json:"wind_gust"` Weather []struct { ID int `json:"id"` Main string `json:"main"` Description string `json:"description"` Icon string `json:"icon"` } `json:"weather"` Clouds int `json:"clouds"` Pop float64 `json:"pop"` Rain float64 `json:"rain,omitempty"` Uvi float64 `json:"uvi"` } `json:"daily"` } func GetWeather() (WeatherResponse, Coords) { home, err := os.UserHomeDir() if err != nil { log.Fatalf("Failed to read homedir with %v", err) } key := readFile(fmt.Sprint(home, "/.config/porch/key.txt")) var city string if len(os.Args) < 2 { city = readFile(fmt.Sprint(home, "/.config/porch/defaultCity.txt")) } else { var b strings.Builder b.WriteString(os.Args[1]) if len(os.Args) > 2 { for _, v := range os.Args[2:] { b.WriteString(fmt.Sprintf(" %s", v)) } } city = b.String() } cityMemo := fmt.Sprint(home, "/.config/porch/cities.csv") var coords Coords coords, found := memoRead(city, cityMemo) if !found { cityQuery := fmtCityQuery(strings.ReplaceAll(city, " ", "_"), key) resp := getUrlResponse(cityQuery) var coordsResp coordsResponse err := json.Unmarshal(resp, &coordsResp) if err != nil { panic(err) } if len(coordsResp) < 1 { log.Fatalf("No results found for city \"%s\"\n", city) } coords = coordsResp[0] rec := coords.toStringRec() memoWrite(rec, cityMemo) } weatherQuery := fmtWeatherQuery(coords.Lat, coords.Lon, key) resp := getUrlResponse(weatherQuery) var weatherResp WeatherResponse err = json.Unmarshal(resp, &weatherResp) if err != nil { panic(err) } return weatherResp, coords } func readFile(keyfile string) string { k, err := os.ReadFile(keyfile) if err != nil { panic(err) } return strings.TrimSpace(string(k)) } func fmtCityQuery(city string, key string) string { return fmt.Sprintf("https://api.openweathermap.org/geo/1.0/direct?q=%s&limit=1&appid=%s", city, key) } func fmtWeatherQuery(lat float64, lon float64, key string) string { return fmt.Sprintf("https://api.openweathermap.org/data/3.0/onecall?lat=%f&lon=%f&appid=%s&units=imperial", lat, lon, key) } func getUrlResponse(u string) []byte { resp, err := http.Get(u) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } return body } func (c Coords) toStringRec() []string { return []string{ c.Name, strconv.FormatFloat(c.Lat, 'f', -1, 64), strconv.FormatFloat(c.Lon, 'f', -1, 64), c.Country, c.State, } } func coordsFromStringRec(rec []string) Coords { lat, err := strconv.ParseFloat(rec[1], 64) if err != nil { panic(err) } lon, err := strconv.ParseFloat(rec[2], 64) if err != nil { panic(err) } return Coords{ rec[0], lat, lon, rec[3], rec[4], } } func memoRead(city string, memoP string) (Coords, bool) { f, err := os.OpenFile(memoP, os.O_RDONLY|os.O_CREATE, 0666) if err != nil { panic(err) } defer f.Close() reader := csv.NewReader(f) existing, err := reader.ReadAll() if err != nil { panic(err) } // Does this record already exist? for _, rec := range existing { if rec[0] == city { return coordsFromStringRec(rec), true } } return Coords{}, false } // Write a record. Does not check for duplication. func memoWrite(r []string, memoP string) { f, err := os.OpenFile(memoP, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } defer f.Close() writer := csv.NewWriter(f) writer.Write(r) writer.Flush() }