// Command emulator serves a small web UI that stands in for an ESP32 // growbox controller: a sensor slot (publishes temperature/humidity // telemetry) and three actuator slots (fan/light/pump — react live to // commands the platform sends over MQTT). See README.md for the protocol // this speaks and why it exists as a standalone tool. package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "git.cactoz.su/cacto/home_automation_emulator/internal/httpserver" "git.cactoz.su/cacto/home_automation_emulator/internal/mqttclient" "git.cactoz.su/cacto/home_automation_emulator/internal/state" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) brokerURL := getEnv("MQTT_BROKER_URL", "tcp://localhost:1883") httpAddr := ":" + getEnv("HTTP_PORT", "8091") store := state.NewStore() mqttClient, err := mqttclient.Connect(mqttclient.Config{ BrokerURL: brokerURL, ClientID: "esp32-emulator", }, logger) if err != nil { logger.Error("mqtt connect failed", "broker", brokerURL, "error", err) os.Exit(1) } defer mqttClient.Close() if err := mqttClient.SubscribeCommands(func(externalID, action string, level float64, hasLevel bool) { reported, ok := store.ApplyCommand(externalID, action, level, hasLevel) if !ok { logger.Warn("command for unknown device ignored", "device_id", externalID, "action", action) return } logger.Info("command applied", "device_id", externalID, "action", action) if err := mqttClient.PublishAck(externalID, reported); err != nil { logger.Error("publish ack failed", "device_id", externalID, "error", err) } }); err != nil { logger.Error("mqtt subscribe failed", "error", err) os.Exit(1) } srv := &http.Server{ Addr: httpAddr, Handler: httpserver.New(store, mqttClient, logger), } go func() { logger.Info("http server listening", "addr", httpAddr, "mqtt_broker", brokerURL) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("http server failed", "error", err) os.Exit(1) } }() ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() <-ctx.Done() logger.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }