Add Weather widget and integrate OpenWeatherMap API for dynamic updates

This commit is contained in:
Arindy 2025-12-29 00:57:38 +01:00
parent e77e1d72b8
commit 57f7898d65
4 changed files with 424 additions and 0 deletions

View File

@ -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))
}

View File

@ -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
}

View File

@ -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"]),
),

261
widgets/weather.go Normal file
View File

@ -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]
}