Commit: 94e09364f17c28708518a901fc46e7739181c36a Parent: bad4f967d3541666e7b2a4c858749aa3597512e2 Author: Vi Grey Date: 2024-06-19 09:10 UTC Summary: Add weather forecast, remove topbar support CHANGELOG.md => CHANGELOG.txt | 16 +++++ README.txt | 2 +- go.mod | 2 +- src/conversions.go | 15 ----- src/flags.go | 15 +++++ src/json.go | 109 +++++++++++++++++++++++++++++++++ src/weather-nws.go | 258 +++++++++++++++++++++++++++++++++++++++++++------------------------------------ 7 files changed, 283 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.txt similarity index 65% rename from CHANGELOG.md rename to CHANGELOG.txt index 31274e4..6216c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.txt @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## [0.0.2] - 2024-06-19 + +### Added +- Weather forecast if latitude/longitude included + +### Removed + +- topbar support + +### Changed + +- Mimimum Go version to 1.21 +- CHANGELOG.md to CHANGELOG.txt + + + ## [0.0.1] - 2023-08-16 ### Fixed diff --git a/README.txt b/README.txt index da4f997..eeb36fc 100644 --- a/README.txt +++ b/README.txt @@ -4,7 +4,7 @@ Get weather data in the US from the National Weather Service ## Build Dependencies -- go >= 1.20 +- go >= 1.21 ## Build diff --git a/go.mod b/go.mod index ac4ddaf..d6d7afd 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module weather-nws -go 1.20 +go 1.21 diff --git a/src/conversions.go b/src/conversions.go index 52ba162..58e73a9 100644 --- a/src/conversions.go +++ b/src/conversions.go @@ -57,21 +57,6 @@ func metarStringToWeatherData(m string) (w MetarWeather) { return } -func iconStringToWeatherIcon(w string) string { - weatherMatch := weatherRegex.FindStringSubmatch(strings.ToLower(w)) - if len(weatherMatch) < 3 { - return weatherIcons[0] - } - for i := range weatherIconRef { - for _, icon := range weatherIconRef[i] { - if icon == weatherMatch[2] { - return weatherIcons[i] - } - } - } - return weatherIcons[0] -} - func getWindChill(t, ws float64) (wc float64) { wc = t if t <= 50 && ws > 3 { diff --git a/src/flags.go b/src/flags.go new file mode 100644 index 0000000..5bcf704 --- /dev/null +++ b/src/flags.go @@ -0,0 +1,15 @@ +package main + +import "strings" + +func handleFlags(flags []string) { + for _, flag := range flags { + if len(strings.ToLower(flag)) == 4 { + stationID = strings.ToUpper(flag) + } else if strings.Contains(flag, ",") { + flagSplit := strings.Split(flag, ",") + latitude = flagSplit[0] + longitude = flagSplit[1] + } + } +} diff --git a/src/json.go b/src/json.go new file mode 100644 index 0000000..f78937e --- /dev/null +++ b/src/json.go @@ -0,0 +1,109 @@ +package main + +import "time" + +type MetarWeather struct { + windDirection float64 + windSpeed float64 + windGustSpeed float64 + temperature float64 + dewPoint float64 +} + +type WeatherResponse struct { + Properties Weather `json:"properties"` + ID string `json:"id"` + Type string `json:"type"` + Status int `json:"status"` +} + +type Weather struct { + Station string `json:"station"` + RawMessage string `json:"rawMessage"` + TextDescription string `json:"textDescription"` + Icon string `json:"icon"` + Temperature WeatherVal `json:"temperature"` + Dewpoint WeatherVal `json:"dewpoint"` + WindDirection WeatherVal `json:"windDirection"` + WindSpeed WeatherVal `json:"windSpeed"` + WindGust WeatherVal `json:"windGust"` + Timestamp time.Time `json:"timestamp"` +} + +type WeatherVal struct { + Value *float64 `json:"value"` + UnitCode string `json:"unitCode"` +} + +type AlertsResponse struct { + Features []Feature `json:"features"` + Status int `json:"status"` +} + +type Feature struct { + Properties Alert `json:"properties"` +} + +type Alert struct { + Ends time.Time `json:"ends"` + Expires time.Time `json:"expires"` + Urgency string `json:"urgency"` + Event string `json:"event"` + Headline string `json:"headline"` + Description string `json:"description"` +} + +type PointResponse struct { + Properties PointProperty `json:"properties"` +} + +type PointProperty struct { + Forecast string `json:"forecast"` + ForecastHourly string `json:"forecastHourly"` +} + +type HourlyForecastResponse struct { + Properties HourlyForecastProperty `json:"properties"` +} + +type HourlyForecastProperty struct { + Periods []HourlyForecastPeriod `json:"periods"` +} + +type HourlyForecastPeriod struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Temperature int `json:"temperature"` + Humidity HourlyForecastHumidity `json:"relativeHumidity"` + Forecast string `json:"shortForecast"` + Precipitation HourlyForecastPrecipitation `json:"probabilityOfPrecipitation"` + WindSpeed string `json:"windSpeed"` +} + +type HourlyForecastPrecipitation struct { + Value int `json:"value"` +} + +type HourlyForecastHumidity struct { + Value int `json:"value"` +} + +type ForecastResponse struct { + Properties ForecastProperty `json:"properties"` +} + +type ForecastProperty struct { + Periods []ForecastPeriod `json:"periods"` +} + +type ForecastPeriod struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Temperature int `json:"temperature"` + Forecast string `json:"shortForecast"` + Precipitation ForecastPrecipitation `json:"probabilityOfPrecipitation"` +} + +type ForecastPrecipitation struct { + Value int `json:"value"` +} diff --git a/src/weather-nws.go b/src/weather-nws.go index b6727aa..89b721c 100644 --- a/src/weather-nws.go +++ b/src/weather-nws.go @@ -6,12 +6,12 @@ import ( "io" "net/http" "os" + "strconv" "strings" "time" ) var ( - topBarFlag bool stationID, latitude, longitude string expectedANSIStart = "\x1b[104;30m " watchANSIStart = "\x1b[103;3;30m " @@ -19,69 +19,65 @@ var ( ansiEnd = " \x1b[0m" windDirections = []string{"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "N", "NNW"} - - weatherIconRef = [][]string{ - []string{"skc", "wind_skc"}, - []string{"tsra", "tsra_sct", "tsra_hi"}, - []string{"rain", "rain_showers", "rain_showers_hi"}, - []string{"rain_snow", "rain_sleet", "snow_sleet", "fzra", "rain_fzra", "snow_fzra", "sleet"}, - []string{"snow", "blizzard"}, - []string{"dust", "smoke", "haze", "fog"}, - []string{"few", "sct", "wind_few", "wind_sct"}, - []string{"bkn", "ovc", "wind_bkn", "wind_ovc"}, - } - weatherIcons = []string{"CLR", "TS", "RA", "RASN", "SN", "FG", "SCT", "OVC"} ) -type MetarWeather struct { - windDirection float64 - windSpeed float64 - windGustSpeed float64 - temperature float64 - dewPoint float64 -} - -type WeatherResponse struct { - Properties Weather `json:"properties"` - ID string `json:"id"` - Type string `json:"type"` - Status int `json:"status"` -} - -type Weather struct { - Station string `json:"station"` - RawMessage string `json:"rawMessage"` - TextDescription string `json:"textDescription"` - Icon string `json:"icon"` - Temperature WeatherVal `json:"temperature"` - Dewpoint WeatherVal `json:"dewpoint"` - WindDirection WeatherVal `json:"windDirection"` - WindSpeed WeatherVal `json:"windSpeed"` - WindGust WeatherVal `json:"windGust"` - Timestamp time.Time `json:"timestamp"` -} - -type WeatherVal struct { - Value *float64 `json:"value"` - UnitCode string `json:"unitCode"` -} - -type AlertsResponse struct { - Features []Feature `json:"features"` - Status int `json:"status"` +func getPoint() (p PointResponse) { + client := http.Client{} + locationEndPoint := fmt.Sprintf("https://api.weather.gov/points/%s,%s", + latitude, longitude) + req, err := http.NewRequest("GET", locationEndPoint, nil) + if err != nil { + return + } + req.Header = http.Header{ + "Accept": []string{"application/geo+json"}, + "User-Agent": []string{"(Weather Info, vigrey.com/git/weather-nws)"}, + } + resp, err := client.Do(req) + if err != nil { + return + } + respBody, _ := io.ReadAll(resp.Body) + json.Unmarshal(respBody, &p) + return } -type Feature struct { - Properties Alert `json:"properties"` +func getForecast(endPoint string) (f ForecastResponse) { + client := http.Client{} + req, err := http.NewRequest("GET", endPoint, nil) + if err != nil { + return + } + req.Header = http.Header{ + "Accept": []string{"application/geo+json"}, + "User-Agent": []string{"(Weather Info, vigrey.com/git/weather-nws)"}, + } + resp, err := client.Do(req) + if err != nil { + return + } + respBody, _ := io.ReadAll(resp.Body) + json.Unmarshal(respBody, &f) + return } -type Alert struct { - Ends time.Time `json:"ends"` - Expires time.Time `json:"expires"` - Urgency string `json:"urgency"` - Event string `json:"event"` - Headline string `json:"headline"` - Description string `json:"description"` +func getHourlyForecast(endPoint string) (f HourlyForecastResponse) { + client := http.Client{} + req, err := http.NewRequest("GET", endPoint, nil) + if err != nil { + return + } + req.Header = http.Header{ + "Accept": []string{"application/geo+json"}, + "User-Agent": []string{"(Weather Info, vigrey.com/git/weather-nws)"}, + } + resp, err := client.Do(req) + if err != nil { + return + } + respBody, _ := io.ReadAll(resp.Body) + json.Unmarshal(respBody, &f) + return } func getWeather() (w WeatherResponse) { @@ -94,7 +90,7 @@ func getWeather() (w WeatherResponse) { } req.Header = http.Header{ "Accept": []string{"application/geo+json"}, - "User-Agent": []string{"(Weather Info, vigrey.com/git/weather, vi@vigrey.com)"}, + "User-Agent": []string{"(Weather Info, vigrey.com/git/weather-nws)"}, } resp, err := client.Do(req) if err != nil { @@ -117,19 +113,6 @@ func getWeatherStr(w Weather, mw MetarWeather) (weatherStr string) { hi := getHeatIndex(temp, rh) wc := getWindChill(temp, ws) at := celciusToFahrenheit(getApparentTemp(tempC, rh, wsM)) - if topBarFlag { - weatherStr = fmt.Sprintf("%.0f", temp) - if int(at+0.5) != int(temp+0.5) { - weatherStr += fmt.Sprintf(" (A%.0f)", at) - } - if int(hi+0.5) > int(temp+0.5) { - weatherStr += fmt.Sprintf(" (H%.0f)", hi) - } else if int(wc+0.5) < int(temp+0.5) { - weatherStr += fmt.Sprintf(" (W%.0f)", wc) - } - weatherStr += "F " - return - } weatherStr = fmt.Sprintf("Weather Station: %s\nTime: %s\n\n"+ "Temperature: %.2fF\nApparent Temperature: %.2fF\n", stationID, w.Timestamp.Format(time.RFC1123), temp, at) @@ -168,15 +151,7 @@ func getWeatherAlerts() (a AlertsResponse) { } func getAlertStr(a Alert) (alertStr string) { - var ansiStart string - if strings.ToLower(a.Urgency) == "immediate" { - ansiStart = warningANSIStart - } else if strings.ToLower(a.Urgency) == "future" { - ansiStart = watchANSIStart - } else if strings.ToLower(a.Urgency) == "expected" { - ansiStart = expectedANSIStart - } - alertStr = ansiStart + a.Event + ansiEnd + "\n" + alertStr = a.Event + "\n" if a.Expires.After(a.Ends) { alertStr += "Expires at " + a.Expires.Format("Jan _2 3:04PM") + "\n\n" } else { @@ -187,28 +162,96 @@ func getAlertStr(a Alert) (alertStr string) { return } -func handleFlags(flags []string) { - for _, flag := range flags { - if strings.ToLower(flag) == "--topbar" { - topBarFlag = true - } else if len(strings.ToLower(flag)) == 4 { - stationID = strings.ToUpper(flag) - } else if strings.Contains(flag, ",") { - flagSplit := strings.Split(flag, ",") - latitude = flagSplit[0] - longitude = flagSplit[1] - } - } -} - func validNWSWeather(m string) bool { return m != "" } -func main() { +func init() { handleFlags(os.Args[1:]) - if stationID == "" && latitude == "" && longitude == "" { - return +} + +func main() { + if latitude != "" && longitude != "" { + p := getPoint() + hf := getHourlyForecast(p.Properties.ForecastHourly) + f := getForecast(p.Properties.Forecast) + fmt.Printf("# Weather for %s,%s:\n\n", + latitude, longitude) + + alerts := getWeatherAlerts() + if alerts.Status == 0 || alerts.Status == 200 { + if len(alerts.Features) > 0 { + fmt.Println("## Alerts:\n```") + for x, a := range alerts.Features { + fmt.Println(getAlertStr(a.Properties)) + if x != len(alerts.Features)-1 { + fmt.Println("\n") + } + } + fmt.Println("```\n") + } + } + var latestShown bool + var hourlyStarted bool + for x := 0; x < len(hf.Properties.Periods); x++ { + h := hf.Properties.Periods[x] + if h.StartTime.Before(time.Now()) && time.Now().Before(h.EndTime) { + fmt.Println("## Latest Weather:") + latestShown = true + } + if h.StartTime.Before(time.Now().Add(24*time.Hour)) && + time.Now().Before(h.EndTime) { + wS := 0 + windSpeedSplit := strings.Split(h.WindSpeed, " ") + if len(windSpeedSplit) > 0 { + wS, _ = strconv.Atoi(windSpeedSplit[0]) + } + fmt.Printf("* %s: %dF (%.0fF) - 💧 %d%% - %s\n", + h.StartTime.Format("3 PM"), h.Temperature, + getApparentTemp(float64(h.Temperature-32)*5/9, + float64(h.Humidity.Value), + miphToMps(float64(wS)))*9/5+32, + h.Precipitation.Value, h.Forecast) + } + if hourlyStarted { + x++ + } + if latestShown && !hourlyStarted { + fmt.Println("\n## Next 24 Hours:") + hourlyStarted = true + } + } + fmt.Println("\n## Next 5 Days:") + var forecastStarted bool + var day int + for x := 0; x < len(f.Properties.Periods); x++ { + fr := f.Properties.Periods[x] + if time.Now().Add(24 * time.Hour).Before(fr.StartTime) { + if !forecastStarted && fr.StartTime.Hour() != 6 { + continue + } + forecastStarted = true + forecast := fr.Forecast + temp1 := fr.Temperature + precip1 := fr.Precipitation.Value + x++ + fr = f.Properties.Periods[x] + temp2 := fr.Temperature + precip2 := fr.Precipitation.Value + fmt.Printf("* %s: %dF to %dF - 💧 %d%% - %s\n", + fr.StartTime.Format("Mon 01/02"), + min(temp1, temp2), max(temp1, temp2), + max(precip1, precip2), + forecast) + day++ + if day == 5 { + break + } + } + } + if stationID != "" { + fmt.Println("\n") + } } if stationID != "" { w := getWeather() @@ -216,26 +259,7 @@ func main() { mw := metarStringToWeatherData(w.Properties.RawMessage) var weatherStr string weatherStr = getWeatherStr(w.Properties, mw) - if topBarFlag { - iconStr := iconStringToWeatherIcon(w.Properties.Icon) - fmt.Printf(" %s %s\n", iconStr, weatherStr) - return - } fmt.Println(weatherStr) } } - if latitude != "" && longitude != "" { - alerts := getWeatherAlerts() - if alerts.Status == 0 || alerts.Status == 200 { - if len(alerts.Features) > 0 && stationID != "" { - fmt.Println("\n--------------------\n\nAlerts:\n") - } - for x, a := range alerts.Features { - fmt.Println(getAlertStr(a.Properties)) - if x != len(alerts.Features)-1 { - fmt.Println("\n") - } - } - } - } }