diff --git a/README.md b/README.md new file mode 100644 index 0000000..44d43bd --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# trayweather + +Простой индикатор погоды в трее. + +Доступные источники погоды: + +* [Yandex](https://yandex.ru/pogoda) diff --git a/api.go b/api.go new file mode 100644 index 0000000..962a34c --- /dev/null +++ b/api.go @@ -0,0 +1,8 @@ +package main + +type WeatherData interface { + CurrentTemperature() float64 + FeelsLikeTemperature() float64 + Description() string + IconName() string +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..21d8b23 --- /dev/null +++ b/config.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "time" + + "github.com/ilyakaznacheev/cleanenv" + "github.com/ncruces/zenity" +) + +type Config struct { + CityName string `toml:"CityName" env:"CITY_NAME"` + UpdatePeriod time.Duration `toml:"UpdatePeriod", env:"UPDATE_PERIOD"` +} + +var config Config + +func init() { + log.SetFlags(0) + + err := cleanenv.ReadConfig("config.toml", &config) + if err != nil { + zenity.Notify("Ошибка при чтении настроек из файла config.toml:\n" + err.Error()) + log.Fatalln(err) + } + + if config.CityName == "" { + zenity.Notify("Город (поле CityName) не может быть пустым.") + log.Fatalln("Город (поле CityName) не может быть пустым.") + } + + log.Println(config.UpdatePeriod) + + if config.UpdatePeriod < time.Minute { + log.Printf("Частота обновлений слишком низкая (%s), будет установлено значение в одну минуту.") + config.UpdatePeriod = time.Minute + } +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..48827a2 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +CityName = "Moscow" +UpdatePeriod = 300_000_000_000 # Кол-во минут * 1_000_000_000 diff --git a/icons/01d.ico b/icons/01d.ico new file mode 100644 index 0000000..c996aa0 Binary files /dev/null and b/icons/01d.ico differ diff --git a/icons/02d.ico b/icons/02d.ico new file mode 100644 index 0000000..4fec3df Binary files /dev/null and b/icons/02d.ico differ diff --git a/icons/03d.ico b/icons/03d.ico new file mode 100644 index 0000000..ae0363a Binary files /dev/null and b/icons/03d.ico differ diff --git a/icons/04d.ico b/icons/04d.ico new file mode 100644 index 0000000..e8ba923 Binary files /dev/null and b/icons/04d.ico differ diff --git a/icons/09d.ico b/icons/09d.ico new file mode 100644 index 0000000..16f8a06 Binary files /dev/null and b/icons/09d.ico differ diff --git a/icons/10d.ico b/icons/10d.ico new file mode 100644 index 0000000..101b0e1 Binary files /dev/null and b/icons/10d.ico differ diff --git a/icons/11d.ico b/icons/11d.ico new file mode 100644 index 0000000..872f0ad Binary files /dev/null and b/icons/11d.ico differ diff --git a/icons/13d.ico b/icons/13d.ico new file mode 100644 index 0000000..5c633af Binary files /dev/null and b/icons/13d.ico differ diff --git a/icons/50d.ico b/icons/50d.ico new file mode 100644 index 0000000..913eb0b Binary files /dev/null and b/icons/50d.ico differ diff --git a/icons/exit.ico b/icons/exit.ico new file mode 100644 index 0000000..10b8cbc Binary files /dev/null and b/icons/exit.ico differ diff --git a/icons/unknown.ico b/icons/unknown.ico new file mode 100644 index 0000000..5cb7bb7 Binary files /dev/null and b/icons/unknown.ico differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..cf3d813 --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "embed" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/getlantern/systray" + "github.com/nxshock/trayweather/yandex" +) + +//go:embed icons/*.ico +var icons embed.FS + +func init() { + log.SetFlags(0) + + http.DefaultClient.Timeout = 10 * time.Second +} + +func main() { + systray.Run(onReady, nil) +} + +func onReady() { + setTrayIcon("unknown.ico") + go update() + + mQuit := systray.AddMenuItem("Выход", "Выйти из приложения") + exitIcon, err := icons.ReadFile("icons/exit.ico") + if err != nil { + log.Fatalln(err) + } + mQuit.SetIcon(exitIcon) + + go func() { + <-mQuit.ClickedCh + os.Exit(0) + }() +} + +func update() { + for { + c, err := yandex.Get(config.CityName) + if err != nil { + systray.SetTooltip(err.Error()) + setTrayIcon("unknown") + time.Sleep(time.Minute) + continue + } + + systray.SetTooltip(fmt.Sprintf("%s\n%.1f °C (%.1f °C)", c.Description(), c.CurrentTemperature(), c.FeelsLikeTemperature())) + setTrayIcon(c.IconName()) + time.Sleep(config.UpdatePeriod) + } +} + +func setTrayIcon(name string) error { + b, err := icons.ReadFile("icons/" + name) + if err != nil { + return err + } + systray.SetIcon(b) + + return nil +} diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..193f058 --- /dev/null +++ b/make.bat @@ -0,0 +1 @@ +go build -ldflags "-H=windowsgui -linkmode=external -s -w" -buildmode=pie -trimpath \ No newline at end of file diff --git a/yandex/api.go b/yandex/api.go new file mode 100644 index 0000000..c206563 --- /dev/null +++ b/yandex/api.go @@ -0,0 +1,90 @@ +package yandex + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +type WeatherData struct { + currentTemperature float64 + feelsLikeTemperature float64 + description string + iconURL string +} + +func (w *WeatherData) CurrentTemperature() float64 { + return w.currentTemperature +} + +func (w *WeatherData) FeelsLikeTemperature() float64 { + return w.feelsLikeTemperature +} +func (w *WeatherData) Description() string { + return w.description +} +func (w *WeatherData) IconName() string { + switch w.description { + case "Ясно": + return "01d.ico" + case "Облачно с прояснениями": + return "02d.ico" + case "Пасмурно": + return "03d.ico" + case "Небольшой снег": + return "09d.ico" + case "Снег": + return "13d.ico" + } + return "" +} + +func Get(cityName string) (*WeatherData, error) { + url := fmt.Sprintf("https://yandex.ru/pogoda/%s", strings.ToLower(cityName)) + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + doc, err := goquery.NewDocumentFromResponse(resp) + if err != nil { + return nil, err + } + + currentTemp, err := parseFloat(doc.Find("div.fact__temp > span.temp__value").Text()) + if err != nil { + return nil, fmt.Errorf("parse current temperature error: %v", err) + } + + feelsLikeTemp, err := parseFloat(doc.Find("div.fact__feels-like > div.term__value span.temp__value").Text()) + if err != nil { + return nil, fmt.Errorf("parse feels like temperature error: %v", err) + } + + wd := &WeatherData{ + currentTemperature: currentTemp, + feelsLikeTemperature: feelsLikeTemp, + description: doc.Find("div.link__condition").Text(), + iconURL: doc.Find("div.fact__temp-wrap img.fact__icon").AttrOr("src", "")} + + return wd, nil +} + +func parseFloat(s string) (float64, error) { + var b []rune + + s = strings.ReplaceAll(s, ",", ".") + s = strings.ReplaceAll(s, "−", "-") + + for _, r := range []rune(s) { + if (r >= '0' && r <= '9') || (r == '+' || r == '-') || (r == '.') { + b = append(b, r) + } + } + + return strconv.ParseFloat(string(b), 32) +}