diff options
| author | twells46 <tom@wellsth.com> | 2026-03-07 08:50:19 -0600 |
|---|---|---|
| committer | twells46 <tom@wellsth.com> | 2026-03-07 09:07:19 -0600 |
| commit | d2b6b3060cfbeb17047f1fd1f989677f28f677eb (patch) | |
| tree | 1f17a8d76ef752bee60f20d338d56fdd4a597fb3 /openWeather.go | |
Initial commit
Diffstat (limited to 'openWeather.go')
| -rw-r--r-- | openWeather.go | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/openWeather.go b/openWeather.go new file mode 100644 index 0000000..84f7cfd --- /dev/null +++ b/openWeather.go @@ -0,0 +1,272 @@ +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() +} |