From 57f7898d658dd95fdc465b3a2e99ade491061003 Mon Sep 17 00:00:00 2001 From: Arindy Date: Mon, 29 Dec 2025 00:57:38 +0100 Subject: [PATCH] Add Weather widget and integrate OpenWeatherMap API for dynamic updates --- apis/openweathermap/currentWeather.go | 111 +++++++++++ apis/openweathermap/main.go | 44 +++++ main.go | 8 + widgets/weather.go | 261 ++++++++++++++++++++++++++ 4 files changed, 424 insertions(+) create mode 100644 apis/openweathermap/currentWeather.go create mode 100644 apis/openweathermap/main.go create mode 100644 widgets/weather.go diff --git a/apis/openweathermap/currentWeather.go b/apis/openweathermap/currentWeather.go new file mode 100644 index 0000000..b9e8f1c --- /dev/null +++ b/apis/openweathermap/currentWeather.go @@ -0,0 +1,111 @@ +package openWeatherMap + +import "time" + +type CurrentWeather struct { + Base string `json:"base"` + Visibility int `json:"visibility"` + Dt int `json:"dt"` + Timezone int `json:"timezone"` + Id int `json:"id"` + Name string `json:"name"` + Cod int `json:"cod"` + Coordinates Coordinates `json:"coord"` + Weather []Weather `json:"weather"` + Wind Wind `json:"wind"` + Clouds Clouds `json:"clouds"` + Rain Rain `json:"rain"` + Snow Snow `json:"snow"` + Sys Sys `json:"sys"` + Main Main `json:"main"` +} + +type Coordinates struct { + Lon float64 `json:"lon"` + Lat float64 `json:"lat"` +} + +type Weather struct { + Id int `json:"id"` + Main string `json:"main"` + Description string `json:"description"` + Icon string `json:"icon"` +} + +type Wind struct { + Speed float64 `json:"speed"` + Deg float64 `json:"deg"` +} + +type Clouds struct { + All int `json:"all"` +} + +type Rain struct { + OneH float64 `json:"1h"` +} + +type Snow struct { + OneH float64 `json:"1h"` +} + +type Sys struct { + Type int `json:"type"` + Id int `json:"id"` + Country string `json:"country"` + Sunrise int64 `json:"sunrise"` + Sunset int64 `json:"sunset"` +} + +type Main struct { + Temp float64 `json:"temp"` + FeelsLike float64 `json:"feels_like"` + TempMin float64 `json:"temp_min"` + TempMax float64 `json:"temp_max"` + Pressure int `json:"pressure"` + Humidity int `json:"humidity"` + SeaLevel int `json:"sea_level"` + GrndLevel int `json:"grnd_level"` +} + +func (currentWeather *CurrentWeather) CardinalWindDirection() string { + windDeg := currentWeather.Wind.Deg + if windDeg > 11.25 && windDeg <= 33.75 { + return `NNE` + } else if windDeg > 33.75 && windDeg <= 56.25 { + return "NE" + } else if windDeg > 56.25 && windDeg <= 78.75 { + return "ENE" + } else if windDeg > 78.75 && windDeg <= 101.25 { + return "E" + } else if windDeg > 101.25 && windDeg <= 123.75 { + return "ESE" + } else if windDeg > 123.75 && windDeg <= 146.25 { + return "SE" + } else if windDeg > 146.25 && windDeg <= 168.75 { + return "SSE" + } else if windDeg > 168.75 && windDeg <= 191.25 { + return "S" + } else if windDeg > 191.25 && windDeg <= 213.75 { + return "SSW" + } else if windDeg > 213.75 && windDeg <= 236.25 { + return "SW" + } else if windDeg > 236.25 && windDeg <= 258.75 { + return "WSW" + } else if windDeg > 258.75 && windDeg <= 281.25 { + return "W" + } else if windDeg > 281.25 && windDeg <= 303.75 { + return "WNW" + } else if windDeg > 303.75 && windDeg <= 326.25 { + return "NW" + } else if windDeg > 326.25 && windDeg <= 348.75 { + return "NNW" + } + + return `N` +} + +func (currentWeather *CurrentWeather) IsDayTime() bool { + now := time.Now() + return now.After(time.Unix(currentWeather.Sys.Sunrise, 0)) && now.Before(time.Unix(currentWeather.Sys.Sunset, 0)) +} diff --git a/apis/openweathermap/main.go b/apis/openweathermap/main.go new file mode 100644 index 0000000..567df9d --- /dev/null +++ b/apis/openweathermap/main.go @@ -0,0 +1,44 @@ +package openWeatherMap + +import ( + "ArinDash/config" + "encoding/json" + "io" + "log" + "net/http" +) + +const apiBaseURL = "https://api.openweathermap.org/data/2.5/weather" + +type configFile struct { + OpenWeatherMap openWeatherMapConfig +} + +type openWeatherMapConfig struct { + LocationId string + ApiKey string +} + +func FetchCurrentWeather() CurrentWeather { + cfg := &configFile{} + config.LoadConfig(cfg) + client := &http.Client{} + currentWeather := &CurrentWeather{} + + req, err := http.NewRequest("GET", apiBaseURL+"?id="+cfg.OpenWeatherMap.LocationId+"&units=metric&lang=en&APPID="+cfg.OpenWeatherMap.ApiKey, nil) + if err != nil { + panic(err) + } + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(respBody, currentWeather) + return *currentWeather +} diff --git a/main.go b/main.go index 1316deb..6aae481 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,7 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) { widgets.New("Clock", widgets.Clock()), widgets.New("Date", widgets.Date()), widgets.New("Calendar", widgets.Calendar()), + widgets.New("Weather", widgets.Weather()), widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))), widgets.New("Wifi", widgets.WifiQRCode()), widgets.New("NetworkDevices", widgets.NetworkDevices()), @@ -104,6 +105,13 @@ func layout() []container.Option { ), ), grid.ColWidthPerc(25, + grid.RowHeightFixed(20, + grid.Widget(widgets.Get["Weather"], + container.BorderTitle("Weather"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite), + ), + ), grid.RowHeightPerc(25, grid.Widget(widgets.Get["empty"]), ), diff --git a/widgets/weather.go b/widgets/weather.go new file mode 100644 index 0000000..dab5384 --- /dev/null +++ b/widgets/weather.go @@ -0,0 +1,261 @@ +package widgets + +import ( + "ArinDash/apis/openweathermap" + "ArinDash/util" + "context" + "fmt" + "time" + + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgetapi" + "github.com/mum4k/termdash/widgets/text" +) + +type WeatherOptions struct { +} + +func Weather() WeatherOptions { + widgetOptions["WeatherOptions"] = createWeather + return WeatherOptions{} +} + +func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget { + widget := util.PanicOnErrorWithResult(text.New()) + + go util.Periodic(ctx, 1*time.Hour, func() error { + weather := openWeatherMap.FetchCurrentWeather() + widget.Reset() + + weatherIcon := getIcon(weather.Weather[0].Icon) + + if err := widget.Write(fmt.Sprintf("\n %s\n\n", weather.Name), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return err + } + + for index, line := range weatherIcon { + if err := widget.Write(fmt.Sprintf("%s ", line), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return err + } + + switch index { + case 0: + if err := printTemperature(weather.Main.Temp, widget); err != nil { + return err + } + if err := widget.Write(fmt.Sprint(" ")); err != nil { + return err + } + if err := widget.Write(weather.Weather[0].Description, text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return err + } + break + case 1: + if err := widget.Write(fmt.Sprint("Feels like: ")); err != nil { + return err + } + if err := printTemperature(weather.Main.FeelsLike, widget); err != nil { + return err + } + break + case 2: + if err := widget.Write(fmt.Sprintf("\uE34B %s %.2f km/h", weather.CardinalWindDirection(), weather.Wind.Speed*3.6)); err != nil { + return err + } + break + case 3: + if weather.Rain.OneH > 0 { + if err := widget.Write(fmt.Sprintf("Rain: %.2f mm/h ", weather.Rain.OneH)); err != nil { + return err + } + } + if weather.Snow.OneH > 0 { + if err := widget.Write(fmt.Sprintf("Snow: %.2f mm/h ", weather.Snow.OneH)); err != nil { + return err + } + } + if weather.Clouds.All > 0 { + if err := widget.Write(fmt.Sprintf("Clouds: %d %% ", weather.Clouds.All)); err != nil { + return err + } + } + break + case 4: + if err := widget.Write(fmt.Sprintf("Pr: %d hPa | Hum: %d %%", weather.Main.Pressure, weather.Main.Humidity)); err != nil { + return err + } + break + } + if err := widget.Write(fmt.Sprint("\n")); err != nil { + return err + } + } + var riseSet string + if weather.IsDayTime() { + riseSet = `Sunset: ` + time.Unix(weather.Sys.Sunset, 0).Format("15:04") + } else { + riseSet = `Sunrise: ` + time.Unix(weather.Sys.Sunrise, 0).Format("15:04") + } + if err := widget.Write(fmt.Sprintf(" %s\n\n", riseSet), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return err + } + + return nil + }) + + return widget +} + +func printTemperature(temp float64, widget *text.Text) error { + var color cell.Color + if temp < 0 { + color = cell.ColorBlue + } else if temp < 5 { + color = cell.ColorAqua + } else if temp < 15 { + color = cell.ColorGreen + } else if temp < 25 { + color = cell.ColorYellow + } else { + color = cell.ColorRed + } + if err := widget.Write(fmt.Sprintf("%.2f °C", temp), text.WriteCellOpts(cell.FgColor(color), cell.Bold())); err != nil { + return err + } + return nil +} + +// ... existing code ... +func getIcon(name string) []string { + icon := map[string][]string{ + "01d": { // wi-day-sunny + " \\ / ", + " .-. ", + " ― ( ) ― ", + " `-’ ", + " / \\ ", + }, + "01n": { // wi-night-clear + " _ ", + " ( `\\ ", + " | | ", + " (_./ ", + " ", + }, + "02d": { // wi-day-cloudy + " \\ / ", + " _ /\"\".-. ", + " \\_( ). ", + " /(___(__) ", + " ", + }, + "02n": { // wi-night-cloudy + " _ ", + " ( `\\ .-. ", + " | _/( ). ", + " (_/(___(__) ", + " ", + }, + "03d": { // wi-cloudy + " ", + " .--. ", + " .-( ). ", + " (___.__)__) ", + " ", + }, + "03n": { // wi-night-cloudy (same as 02n/04n usually) + " _ ", + " ( `\\ .-. ", + " | _/( ). ", + " (_/(___(__) ", + " ", + }, + "04d": { // wi-cloudy-windy + " .--. ", + " .-( ). __ ", + " (___.__)__) _ ", + " _ - _ - _ - ", + " ", + }, + "04n": { // wi-night-cloudy + " _ ", + " ( `\\ .-. ", + " | _/( ). ", + " (_/(___(__) ", + " ", + }, + "09d": { // wi-showers + " .-. ", + " ( ). ", + " (___(__) ", + " ‘ ‘ ‘ ‘ ", + " ‘ ‘ ‘ ‘ ", + }, + "09n": { // wi-night-showers + " _ .-. ", + " ( `\\( ). ", + " (_/(___(__) ", + " ‘ ‘ ‘ ‘ ", + " ‘ ‘ ‘ ‘ ", + }, + "10d": { // wi-rain + " .-. ", + " ( ). ", + " (___(__) ", + " ‚‘‚‘‚‘‚‘ ", + " ‚’‚’‚’‚’ ", + }, + "10n": { // wi-night-rain + " _ .-. ", + " ( `\\( ). ", + " (_/(___(__) ", + " ‚‘‚‘‚‘‚‘ ", + " ‚’‚’‚’‚’ ", + }, + "11d": { // wi-thunderstorm + " .-. ", + " ( ). ", + " (___(__) ", + " /_ /_ ", + " / / ", + }, + "11n": { // wi-night-thunderstorm + " _ .-. ", + " ( `\\( ). ", + " (_/(___(__) ", + " /_ /_ ", + " / / ", + }, + "13d": { // wi-snow + " .-. ", + " ( ). ", + " (___(__) ", + " * * * ", + " * * * ", + }, + "13n": { // wi-night-snow + " _ .-. ", + " ( `\\( ). ", + " (_/(___(__) ", + " * * * ", + " * * * ", + }, + "50d": { // wi-fog + " ", + " _ - _ - _ - ", + " _ - _ - _ ", + " _ - _ - _ - ", + " ", + }, + "50n": { // wi-night-alt-cloudy-windy + " _ ", + " ( `\\ .-. ", + " | _/( ). ", + " (_/(___(__) ", + " _ - _ - _ ", + }, + } + + return icon[name] +}