Add weather service
This commit is contained in:
parent
859db5db39
commit
30e631b85f
7 changed files with 257 additions and 9 deletions
|
@ -36,6 +36,7 @@ services:
|
||||||
- LOG_LEVEL=debug
|
- LOG_LEVEL=debug
|
||||||
- TITLE=DEV
|
- TITLE=DEV
|
||||||
- APP_VERSION=v0.0.1-dev
|
- APP_VERSION=v0.0.1-dev
|
||||||
|
- WEATHER_KEY=${WEATHER_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app/
|
- .:/app/
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -16,11 +16,16 @@ type SystemService interface {
|
||||||
GetStaticInformation() *services.StaticInformation
|
GetStaticInformation() *services.StaticInformation
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppHandler(env *env.Config, s SystemService, b BookmarkService) *AppHandler {
|
type WeatherService interface {
|
||||||
|
GetCurrentWeather() *services.OpenWeather
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppHandler(env *env.Config, s SystemService, w WeatherService, b BookmarkService) *AppHandler {
|
||||||
|
|
||||||
return &AppHandler{
|
return &AppHandler{
|
||||||
env: env,
|
env: env,
|
||||||
SystemService: s,
|
SystemService: s,
|
||||||
|
WeatherService: w,
|
||||||
BookmarkService: b,
|
BookmarkService: b,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +33,7 @@ func NewAppHandler(env *env.Config, s SystemService, b BookmarkService) *AppHand
|
||||||
type AppHandler struct {
|
type AppHandler struct {
|
||||||
env *env.Config
|
env *env.Config
|
||||||
SystemService SystemService
|
SystemService SystemService
|
||||||
|
WeatherService WeatherService
|
||||||
BookmarkService BookmarkService
|
BookmarkService BookmarkService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +41,9 @@ func (bh *AppHandler) appHandler(c echo.Context) error {
|
||||||
bookmarks := bh.BookmarkService.GetAllBookmarks()
|
bookmarks := bh.BookmarkService.GetAllBookmarks()
|
||||||
staticSystem := bh.SystemService.GetStaticInformation()
|
staticSystem := bh.SystemService.GetStaticInformation()
|
||||||
liveSystem := bh.SystemService.GetLiveInformation()
|
liveSystem := bh.SystemService.GetLiveInformation()
|
||||||
|
weather := bh.WeatherService.GetCurrentWeather()
|
||||||
|
|
||||||
titlePage := bh.env.Title
|
titlePage := bh.env.Title
|
||||||
|
|
||||||
return renderView(c, home.HomeIndex(titlePage, bh.env.Version, bookmarks, staticSystem, liveSystem, home.Home(titlePage, bookmarks, staticSystem, liveSystem)))
|
return renderView(c, home.HomeIndex(titlePage, bh.env.Version, bookmarks, staticSystem, liveSystem, weather, home.Home(titlePage, bookmarks, staticSystem, liveSystem, weather)))
|
||||||
}
|
}
|
||||||
|
|
6
internal/env/env.go
vendored
6
internal/env/env.go
vendored
|
@ -12,6 +12,12 @@ type Config struct {
|
||||||
Title string `env:"TITLE" envDefault:"goDash"`
|
Title string `env:"TITLE" envDefault:"goDash"`
|
||||||
Port int `env:"PORT" envDefault:"4000" validate:"min=1024,max=49151"`
|
Port int `env:"PORT" envDefault:"4000" validate:"min=1024,max=49151"`
|
||||||
Version string `env:"APP_VERSION"`
|
Version string `env:"APP_VERSION"`
|
||||||
|
LocationLatitude float32 `env:"LOCATION_LATITUDE" envDefault:"48.780331609463815"`
|
||||||
|
LocationLongitude float32 `env:"LOCATION_LONGITUDE" envDefault:"9.177968320179422"`
|
||||||
|
WeatherKey string `env:"WEATHER_KEY"`
|
||||||
|
WeatherUnits string `env:"WEATHER_UNITS" envDefault:"metric"`
|
||||||
|
WeatherLanguage string `env:"WEATHER_LANG" envDefault:"en"`
|
||||||
|
WeatherDigits bool `env:"WEATHER_DIGITS" envDefault:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var errParse = errors.New("error parsing environment variables")
|
var errParse = errors.New("error parsing environment variables")
|
||||||
|
|
3
main.go
3
main.go
|
@ -42,9 +42,10 @@ func main() {
|
||||||
sse.AutoReplay = false
|
sse.AutoReplay = false
|
||||||
|
|
||||||
s := services.NewSystemService(sse)
|
s := services.NewSystemService(sse)
|
||||||
|
w := services.NewWeatherService(sse, env)
|
||||||
b := services.NewBookmarkService()
|
b := services.NewBookmarkService()
|
||||||
|
|
||||||
ah := handlers.NewAppHandler(env, s, b)
|
ah := handlers.NewAppHandler(env, s, w, b)
|
||||||
handlers.SetupRoutes(e, sse, ah)
|
handlers.SetupRoutes(e, sse, ah)
|
||||||
|
|
||||||
slog.Info("starting server", "url", fmt.Sprintf("http://localhost:%d", env.Port))
|
slog.Info("starting server", "url", fmt.Sprintf("http://localhost:%d", env.Port))
|
||||||
|
|
77
services/weather.services.go
Normal file
77
services/weather.services.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/r3labs/sse/v2"
|
||||||
|
"gitlab.unjx.de/flohoss/godash/internal/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewWeatherService(sse *sse.Server, env *env.Config) *WeatherService {
|
||||||
|
var w = WeatherService{sse: sse, env: env}
|
||||||
|
w.setWeatherUnits()
|
||||||
|
sse.CreateStream("weather")
|
||||||
|
go w.updateWeather(time.Second * 90)
|
||||||
|
return &w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeatherService) GetCurrentWeather() *OpenWeather {
|
||||||
|
return &w.CurrentWeather
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeatherService) setWeatherUnits() {
|
||||||
|
if w.env.WeatherUnits == "imperial" {
|
||||||
|
w.CurrentWeather.Units = "°F"
|
||||||
|
} else {
|
||||||
|
w.CurrentWeather.Units = "°C"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeatherService) copyWeatherValues(weatherResp *OpenWeatherApiResponse) {
|
||||||
|
myTime := time.Unix(weatherResp.Sys.Sunrise, 0)
|
||||||
|
w.CurrentWeather.Sunrise = myTime.Format("15:04")
|
||||||
|
myTime = time.Unix(weatherResp.Sys.Sunset, 0)
|
||||||
|
w.CurrentWeather.Sunset = myTime.Format("15:04")
|
||||||
|
w.CurrentWeather.Icon = weatherResp.Weather[0].Icon
|
||||||
|
if w.env.WeatherDigits {
|
||||||
|
w.CurrentWeather.Temp = weatherResp.Main.Temp
|
||||||
|
} else {
|
||||||
|
w.CurrentWeather.Temp = math.Round(weatherResp.Main.Temp)
|
||||||
|
}
|
||||||
|
w.CurrentWeather.Description = weatherResp.Weather[0].Description
|
||||||
|
w.CurrentWeather.Humidity = weatherResp.Main.Humidity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeatherService) updateWeather(interval time.Duration) {
|
||||||
|
var weatherResponse OpenWeatherApiResponse
|
||||||
|
for {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s&units=%s&lang=%s",
|
||||||
|
w.env.LocationLatitude,
|
||||||
|
w.env.LocationLongitude,
|
||||||
|
w.env.WeatherKey,
|
||||||
|
w.env.WeatherUnits,
|
||||||
|
w.env.WeatherLanguage))
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
slog.Error("weather cannot be updated, please check WEATHER_KEY")
|
||||||
|
} else {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
err = json.Unmarshal(body, &weatherResponse)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("weather cannot be processed")
|
||||||
|
} else {
|
||||||
|
w.copyWeatherValues(&weatherResponse)
|
||||||
|
slog.Info("weather updated", "temp", w.CurrentWeather.Temp)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
json, _ := json.Marshal(w.CurrentWeather)
|
||||||
|
w.sse.Publish("weather", &sse.Event{Data: json})
|
||||||
|
}
|
||||||
|
time.Sleep(interval)
|
||||||
|
}
|
||||||
|
}
|
43
services/weather.types.go
Normal file
43
services/weather.types.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/r3labs/sse/v2"
|
||||||
|
"gitlab.unjx.de/flohoss/godash/internal/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeatherService struct {
|
||||||
|
CurrentWeather OpenWeather
|
||||||
|
sse *sse.Server
|
||||||
|
env *env.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeather struct {
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Temp float64 `json:"temp"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Humidity uint8 `json:"humidity"`
|
||||||
|
Sunrise string `json:"sunrise"`
|
||||||
|
Sunset string `json:"sunset"`
|
||||||
|
Units string `json:"units"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeatherApiResponse struct {
|
||||||
|
Weather []OpenWeatherApiWeather `json:"Weather"`
|
||||||
|
Main OpenWeatherApiMain `json:"main"`
|
||||||
|
Sys OpenWeatherApiSys `json:"sys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeatherApiWeather struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeatherApiMain struct {
|
||||||
|
Temp float64 `json:"temp"`
|
||||||
|
Humidity uint8 `json:"humidity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeatherApiSys struct {
|
||||||
|
Sunrise int64 `json:"sunrise"`
|
||||||
|
Sunset int64 `json:"sunset"`
|
||||||
|
}
|
|
@ -8,8 +8,61 @@ import "gitlab.unjx.de/flohoss/godash/components/application"
|
||||||
import "gitlab.unjx.de/flohoss/godash/components/link"
|
import "gitlab.unjx.de/flohoss/godash/components/link"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
templ Home(title string, bookmarks *services.Bookmarks, static *services.StaticInformation, live *services.LiveInformation) {
|
func getIcon(icon string) string {
|
||||||
<section class="grid gap-14">
|
switch icon {
|
||||||
|
case "01d":
|
||||||
|
return "icon-[bi--sun-fill]"
|
||||||
|
case "01n":
|
||||||
|
return "icon-[bi--moon-fill]"
|
||||||
|
case "02d":
|
||||||
|
return "icon-[bi--cloud-sun-fill]"
|
||||||
|
case "02n":
|
||||||
|
return "icon-[bi--cloud-moon-fill]"
|
||||||
|
case "03d", "03n":
|
||||||
|
return "icon-[bi--cloud-fill]"
|
||||||
|
case "04d", "04n":
|
||||||
|
return "icon-[bi--clouds-fill]"
|
||||||
|
case "09d", "09n":
|
||||||
|
return "icon-[bi--cloud-rain-heavy-fill]"
|
||||||
|
case "10d", "10n":
|
||||||
|
return "icon-[bi--cloud-drizzle-fill]"
|
||||||
|
case "11d", "11n":
|
||||||
|
return "icon-[bi--cloud-lightning-rain-fill]"
|
||||||
|
case "13d", "13n":
|
||||||
|
return "icon-[bi--cloud-snow-fill]"
|
||||||
|
case "50d", "50n":
|
||||||
|
return "icon-[bi--cloud-fog2-fill]"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Home(title string, bookmarks *services.Bookmarks, static *services.StaticInformation, live *services.LiveInformation, weather *services.OpenWeather) {
|
||||||
|
<section class="grid gap-10">
|
||||||
|
<div class="flex items-center select-none">
|
||||||
|
<span id="watherIcon" class={ "extra-icon h-12 w-12 shrink-0 mr-4 md:w-14 md:h-14", getIcon(weather.Icon) }></span>
|
||||||
|
<div>
|
||||||
|
<div class="text-4xl md:text-4xl"><span id="weatherTemp">{ fmt.Sprintf("%0.2f",weather.Temp) }</span> { weather.Units }</div>
|
||||||
|
<div class="flex items-center gap-5 text-xs">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="extra-icon icon-[bi--chat-quote]"></span>
|
||||||
|
<div id="weatherDescription" class="extra-info">{ weather.Description }</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="extra-icon icon-[bi--droplet]"></span>
|
||||||
|
<div id="weatherHumidity" class="extra-info">{ fmt.Sprintf("%d %%",weather.Humidity) }</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:flex items-center">
|
||||||
|
<span class="extra-icon icon-[bi--sunrise]"></span>
|
||||||
|
<div id="weatherSunrise" class="extra-info">{ weather.Sunrise }</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:flex items-center">
|
||||||
|
<span class="extra-icon icon-[bi--sunset]"></span>
|
||||||
|
<div id="weatherSunset" class="extra-info">{ weather.Sunset }</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 select-none">
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 select-none">
|
||||||
@system.System("icon-[bi--cpu]",static.CPU.Name,"",static.CPU.Threads,"systemCpuPercentage","",live.CPU)
|
@system.System("icon-[bi--cpu]",static.CPU.Name,"",static.CPU.Threads,"systemCpuPercentage","",live.CPU)
|
||||||
@system.System("icon-[bi--nvme]",live.Disk.Value,fmt.Sprintf(" | %s", static.Disk.Total),static.Disk.Partitions,"systemDiskPercentage","systemDiskValue",live.Disk.Percentage)
|
@system.System("icon-[bi--nvme]",live.Disk.Value,fmt.Sprintf(" | %s", static.Disk.Total),static.Disk.Partitions,"systemDiskPercentage","systemDiskValue",live.Disk.Percentage)
|
||||||
|
@ -45,14 +98,30 @@ templ Home(title string, bookmarks *services.Bookmarks, static *services.StaticI
|
||||||
</section>
|
</section>
|
||||||
<script>
|
<script>
|
||||||
let systemSSESource = null;
|
let systemSSESource = null;
|
||||||
|
let weatherSSESource = null;
|
||||||
addEventListener('beforeunload', () => {
|
addEventListener('beforeunload', () => {
|
||||||
systemSSESource && systemSSESource.close();
|
systemSSESource && systemSSESource.close();
|
||||||
|
weatherSSESource && weatherSSESource.close();
|
||||||
});
|
});
|
||||||
systemSSESource = new EventSource('/sse?stream=system');
|
systemSSESource = new EventSource('/sse?stream=system');
|
||||||
systemSSESource.onmessage = (e) => {
|
systemSSESource.onmessage = (e) => {
|
||||||
const parsed = JSON.parse(e.data);
|
const parsed = JSON.parse(e.data);
|
||||||
replaceSystem(parsed);
|
replaceSystem(parsed);
|
||||||
};
|
};
|
||||||
|
weatherSSESource = new EventSource('/sse?stream=weather');
|
||||||
|
weatherSSESource.onmessage = (e) => {
|
||||||
|
const parsed = JSON.parse(e.data);
|
||||||
|
replaceWeather(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// weather elements
|
||||||
|
const weatherIcon = document.getElementById('weatherIcon');
|
||||||
|
const weatherTemp = document.getElementById('weatherTemp');
|
||||||
|
const weatherDescription = document.getElementById('weatherDescription');
|
||||||
|
const weatherHumidity = document.getElementById('weatherHumidity');
|
||||||
|
const weatherSunrise = document.getElementById('weatherSunrise');
|
||||||
|
const weatherSunset = document.getElementById('weatherSunset');
|
||||||
|
|
||||||
// system elements
|
// system elements
|
||||||
const systemCpuPercentage = document.getElementById('systemCpuPercentage');
|
const systemCpuPercentage = document.getElementById('systemCpuPercentage');
|
||||||
|
@ -66,6 +135,49 @@ templ Home(title string, bookmarks *services.Bookmarks, static *services.StaticI
|
||||||
const uptimeMinutes = document.getElementById('uptimeMinutes');
|
const uptimeMinutes = document.getElementById('uptimeMinutes');
|
||||||
const uptimeSeconds = document.getElementById('uptimeSeconds');
|
const uptimeSeconds = document.getElementById('uptimeSeconds');
|
||||||
|
|
||||||
|
function weatherClass(icon){
|
||||||
|
switch (icon) {
|
||||||
|
case "01d":
|
||||||
|
return "icon-[bi--sun-fill]"
|
||||||
|
case "01n":
|
||||||
|
return "icon-[bi--moon-fill]"
|
||||||
|
case "02d":
|
||||||
|
return "icon-[bi--cloud-sun-fill]"
|
||||||
|
case "02n":
|
||||||
|
return "icon-[bi--cloud-moon-fill]"
|
||||||
|
case "03d", "03n":
|
||||||
|
return "icon-[bi--cloud-fill]"
|
||||||
|
case "04d", "04n":
|
||||||
|
return "icon-[bi--clouds-fill]"
|
||||||
|
case "09d", "09n":
|
||||||
|
return "icon-[bi--cloud-rain-heavy-fill]"
|
||||||
|
case "10d", "10n":
|
||||||
|
return "icon-[bi--cloud-drizzle-fill]"
|
||||||
|
case "11d", "11n":
|
||||||
|
return "icon-[bi--cloud-lightning-rain-fill]"
|
||||||
|
case "13d", "13n":
|
||||||
|
return "icon-[bi--cloud-snow-fill]"
|
||||||
|
case "50d", "50n":
|
||||||
|
return "icon-[bi--cloud-fog2-fill]"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceWeather(parsed) {
|
||||||
|
weatherIcon.className.split(' ').forEach(function(className) {
|
||||||
|
if (className.startsWith('icon-')) {
|
||||||
|
weatherIcon.classList.remove(className);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
weatherIcon.classList.add(weatherClass(parsed.icon));
|
||||||
|
weatherTemp.innerText = parsed.temp;
|
||||||
|
weatherDescription.innerText = parsed.description;
|
||||||
|
weatherHumidity.innerText = parsed.humidity + '%';
|
||||||
|
weatherSunrise.innerText = parsed.sunrise;
|
||||||
|
weatherSunset.innerText = parsed.sunset;
|
||||||
|
}
|
||||||
|
|
||||||
function replaceSystem(parsed) {
|
function replaceSystem(parsed) {
|
||||||
systemCpuPercentage.style = 'width:' + parsed.cpu + '%';
|
systemCpuPercentage.style = 'width:' + parsed.cpu + '%';
|
||||||
systemRamPercentage.style = 'width:' + parsed.ram.percentage + '%';
|
systemRamPercentage.style = 'width:' + parsed.ram.percentage + '%';
|
||||||
|
@ -87,6 +199,7 @@ templ HomeIndex(
|
||||||
bookmarks *services.Bookmarks,
|
bookmarks *services.Bookmarks,
|
||||||
static *services.StaticInformation,
|
static *services.StaticInformation,
|
||||||
live *services.LiveInformation,
|
live *services.LiveInformation,
|
||||||
|
weather *services.OpenWeather,
|
||||||
cmp templ.Component,
|
cmp templ.Component,
|
||||||
) {
|
) {
|
||||||
@layout.Base(title, version) {
|
@layout.Base(title, version) {
|
||||||
|
|
Loading…
Reference in a new issue