package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/joho/godotenv" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/alexa" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/config" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/directive" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/entities" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/haclient" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/logger" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/telemetry" ) // entityDomains are the ha-gateway entity domains alexa-bridge resolves and // controls. RemoteService/SwitchBot entities are deliberately excluded — // see plan.md's "Decisions on open questions" #3. var entityDomains = []string{"light", "switch", "climate"} var version = "dev" func main() { _ = godotenv.Load() cfg, err := config.Load() if err != nil { os.Stderr.WriteString("config error: " + err.Error() + "\n") os.Exit(1) } log := logger.New(cfg.LogFormat, cfg.LogLevel) slog.SetDefault(log) log.Info("starting alexa-bridge", "version", version, "http_port", cfg.HTTPPort, "ha_gateway_addr", cfg.HAGatewayAddr, "tls_dir", cfg.TLSDir, "entity_refresh_interval", cfg.EntityRefreshInterval.String(), "otel_endpoint", cfg.OTELEndpoint, ) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() ctx = logger.WithLogger(ctx, log) shutdown, err := telemetry.Setup(ctx, "alexa-bridge", version, cfg) if err != nil { log.Error("telemetry setup failed", "err", err) os.Exit(1) } haClient, err := haclient.New(ctx, cfg.HAGatewayAddr, cfg.TLSDir, cfg.HAGatewayServerName, log) if err != nil { log.Error("ha-gateway client setup failed", "err", err) os.Exit(1) } defer func() { if err := haClient.Close(); err != nil { log.Error("ha-gateway client close failed", "err", err) } }() entityClient := entities.NewClient(haClient.EntityServiceClient(), entityDomains) resolver := entities.NewResolver() refresher := entities.NewRefresher(entityClient, resolver, cfg.EntityRefreshInterval, log) // Blocking: without an initial entity list nothing can resolve, so a // failure here should fail startup rather than serve with an empty // resolver. Periodic refreshes after this point only log on failure. if err := refresher.Start(ctx); err != nil { log.Error("initial entity fetch failed", "err", err) os.Exit(1) } router := directive.NewRouter(resolver, haClient, log) validator := alexa.NewSignatureValidator(nil) alexaHandler := alexa.NewHandler(validator, cfg.AlexaSkillID, router) mux := http.NewServeMux() mux.Handle("/", alexaHandler) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) srv := &http.Server{ Addr: ":" + cfg.HTTPPort, Handler: mux, ReadHeaderTimeout: 5 * time.Second, } go func() { log.Info("alexa-bridge listening", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error("serve failed", "err", err) } }() <-ctx.Done() log.Info("shutdown signal received, draining") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Error("http server shutdown failed", "err", err) } log.Info("shutdown complete") if err := shutdown(context.Background()); err != nil { log.Error("telemetry shutdown error", "err", err) } }