Latest Updates done, before integrating

This commit is contained in:
2026-04-12 10:13:53 +02:00
parent db46fcf0c6
commit 5354e34055
19 changed files with 820 additions and 41 deletions

83
main.go
View File

@@ -17,6 +17,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/tb/ems/internal/actuator"
"github.com/tb/ems/internal/auth"
"github.com/tb/ems/internal/collector"
"github.com/tb/ems/internal/config"
"github.com/tb/ems/internal/engine"
@@ -134,36 +135,74 @@ func main() {
// Build sorted car option list for the status page form
carOptions := buildCarOptions(cfg.Cars)
// Auth manager — loads or generates persistent session and CSRF tokens.
// Token files live alongside other persistent state in /var/lib/ems/.
tokenDir := filepath.Dir(cfg.EMS.SessionLogFile)
authMgr := auth.New(cfg.EMS.HTTPUsername, cfg.EMS.HTTPPassword, tokenDir, logger)
if authMgr.Enabled() {
logger.Info("web UI authentication enabled", "username", cfg.EMS.HTTPUsername)
} else {
logger.Warn("web UI authentication disabled — set ems.http_username and ems.http_password to enable")
}
// Status store (shared between HTTP handler and control loop)
wwConfigured := cfg.Viessmann.InstallationID != ""
statusStore := status.NewStore(*dryRun, wwConfigured, cfg.Strategic, cfg.CarCharging, monitorMode, wwBoostDisabled, tripMgr, carOptions)
statusStore := status.NewStore(*dryRun, wwConfigured, cfg.Strategic, cfg.CarCharging, monitorMode, wwBoostDisabled, tripMgr, carOptions, authMgr.CSRFToken(), authMgr.Enabled())
// Metrics HTTP server
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// Web UI server — authenticated, serves status page and control endpoints.
uiMux := http.NewServeMux()
uiMux.HandleFunc("/login", authMgr.LoginHandler())
uiMux.HandleFunc("/logout", authMgr.LogoutHandler())
uiMux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
w.Write([]byte("ok")) //nolint:errcheck
})
mux.HandleFunc("/override", overrideHandler(act, eng, logger))
mux.HandleFunc("/monitor", monitorHandler(monitorMode, logger))
mux.HandleFunc("/trip", tripSetHandler(tripMgr, cfg, logger))
mux.HandleFunc("/trip/cancel", tripCancelHandler(tripMgr, logger))
mux.HandleFunc("/ww/reset", wwResetHandler(act, eng, cfg, logger))
mux.HandleFunc("/ww/boost", wwBoostToggleHandler(wwBoostDisabled, logger))
mux.HandleFunc("/", statusStore.Handler())
uiMux.HandleFunc("/override", overrideHandler(act, eng, logger))
uiMux.HandleFunc("/monitor", monitorHandler(monitorMode, logger))
uiMux.HandleFunc("/trip", tripSetHandler(tripMgr, cfg, logger))
uiMux.HandleFunc("/trip/cancel", tripCancelHandler(tripMgr, logger))
uiMux.HandleFunc("/ww/reset", wwResetHandler(act, eng, cfg, logger))
uiMux.HandleFunc("/ww/boost", wwBoostToggleHandler(wwBoostDisabled, logger))
uiMux.HandleFunc("/", statusStore.Handler())
srv := &http.Server{
var uiHandler http.Handler = authMgr.VerifyCSRF(uiMux)
uiHandler = authMgr.RequireAuth(uiHandler)
uiSrv := &http.Server{
Addr: cfg.EMS.ListenAddr,
Handler: mux,
Handler: uiHandler,
}
// Metrics server — no auth, for Prometheus scraping.
// Bind to MetricsAddr; if unset, skip the separate server (metrics stay on UI port).
var metricsSrv *http.Server
if cfg.EMS.MetricsAddr != "" && cfg.EMS.MetricsAddr != cfg.EMS.ListenAddr {
metMux := http.NewServeMux()
metMux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
metMux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok")) //nolint:errcheck
})
metricsSrv = &http.Server{Addr: cfg.EMS.MetricsAddr, Handler: metMux}
} else {
// Fallback: add /metrics to the UI mux (no separate port configured)
uiMux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
}
go func() {
logger.Info("metrics server listening", "addr", cfg.EMS.ListenAddr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("metrics server error", "error", err)
logger.Info("web UI listening", "addr", cfg.EMS.ListenAddr)
if err := uiSrv.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("web UI server error", "error", err)
}
}()
if metricsSrv != nil {
go func() {
logger.Info("metrics server listening", "addr", cfg.EMS.MetricsAddr)
if err := metricsSrv.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("metrics server error", "error", err)
}
}()
}
// Graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
@@ -192,7 +231,10 @@ func main() {
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
srv.Shutdown(shutdownCtx)
uiSrv.Shutdown(shutdownCtx) //nolint:errcheck
if metricsSrv != nil {
metricsSrv.Shutdown(shutdownCtx) //nolint:errcheck
}
return
}
}
@@ -235,7 +277,7 @@ func runCycle(
logger.Warn("all Shelly devices unreachable, skipping override detection", "error", err)
} else {
shellyStates = states
eng.SyncHardwareState(shellyStates, now, cfg.EMS.OverrideTimeoutParsed())
eng.SyncHardwareState(shellyStates, now, cfg.EMS.OverrideTimeoutParsed(), monitorMode.IsActive())
store.SyncDeviceStates(shellyStates)
}
@@ -751,3 +793,4 @@ func writeHeartbeat(stateFile string, logger *slog.Logger) {
logger.Warn("could not write heartbeat file", "error", err)
}
}