aboutsummaryrefslogtreecommitdiff
path: root/openWeather.go
diff options
context:
space:
mode:
authortwells46 <tom@wellsth.com>2026-03-07 08:50:19 -0600
committertwells46 <tom@wellsth.com>2026-03-07 09:07:19 -0600
commitd2b6b3060cfbeb17047f1fd1f989677f28f677eb (patch)
tree1f17a8d76ef752bee60f20d338d56fdd4a597fb3 /openWeather.go
Initial commit
Diffstat (limited to 'openWeather.go')
-rw-r--r--openWeather.go272
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()
+}