package main import ( "encoding/json" "log" "net/http" "os" "os/signal" "strconv" "git.k7n.net/mats/go-telldus/pkg/config" "git.k7n.net/mats/go-telldus/pkg/datastore" "git.k7n.net/mats/go-telldus/pkg/devices" "git.k7n.net/mats/go-telldus/pkg/mqtt" "git.k7n.net/mats/go-telldus/pkg/telldus" daemon "git.k7n.net/mats/go-telldus/pkg/telldus-daemon" ) const ( httpPort = ":8080" ) var mqttClient *mqtt.Client var store *datastore.DataStore var daemonMgr *daemon.Manager var eventMgr *devices.EventManager var configParser *config.Parser var configPath string const maxEvents = 1000 func main() { // Initialize daemon manager daemonMgr = daemon.New() if err := daemonMgr.Start(); err != nil { log.Fatalf("Failed to start telldusd: %v", err) } defer daemonMgr.Stop() // Initialize Telldus telldus.Init() defer telldus.Close() // Initialize DataStore var err error store, err = datastore.New("./db/telldus.db") if err != nil { log.Fatal(err) } defer store.Close() // Sync devices and sensors syncer := devices.NewSyncer(store) if err := syncer.SyncDevices(); err != nil { log.Printf("Error syncing devices: %v", err) } if err := syncer.SyncSensors(); err != nil { log.Printf("Error syncing sensors: %v", err) } // Device reload function for config file changes reloadDevices := func() error { log.Println("Configuration file changed, restarting telldusd...") if err := daemonMgr.Restart(); err != nil { log.Printf("Failed to restart telldusd: %v", err) return err } log.Println("Reloading devices and sensors after config change...") if err := syncer.SyncDevices(); err != nil { log.Printf("Error syncing devices: %v", err) return err } if err := syncer.SyncSensors(); err != nil { log.Printf("Error syncing sensors: %v", err) return err } if err := mqttClient.PublishAllDiscovery(); err != nil { log.Printf("Error republishing discovery: %v", err) return err } if err := mqttClient.SubscribeToDeviceCommands(); err != nil { log.Printf("Error resubscribing to commands: %v", err) return err } log.Println("Successfully reloaded devices and sensors") return nil } // Initialize config parser configPath = "/etc/tellstick.conf" configParser = config.NewParser(configPath) // Start watching config file watcher := daemon.NewWatcher(configPath, reloadDevices) go func() { if err := watcher.Watch(); err != nil { log.Printf("Config watcher error: %v", err) } }() // Initialize MQTT mqttConfig := mqtt.Config{ BrokerURL: os.Getenv("MQTT_URL"), Username: os.Getenv("MQTT_USER"), Password: os.Getenv("MQTT_PASSWORD"), } mqttClient, err = mqtt.New(mqttConfig, store) if err != nil { log.Fatalf("Failed to connect to MQTT: %v", err) } defer mqttClient.Close() // Publish Home Assistant discovery if err := mqttClient.PublishAllDiscovery(); err != nil { log.Printf("Error publishing discovery: %v", err) } // Subscribe to command topics if err := mqttClient.SubscribeToDeviceCommands(); err != nil { log.Printf("Error subscribing to commands: %v", err) } // List devices and sensors syncer.ListDevices() syncer.ListSensors() // Initialize event manager and register callbacks eventMgr = devices.NewEventManager(store, mqttClient, maxEvents) eventMgr.RegisterCallbacks() // Setup graceful shutdown c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c log.Println("Shutting down gracefully...") mqttClient.Close() telldus.Close() daemonMgr.Stop() os.Exit(0) }() // Start HTTP server log.Println("Server starting on", httpPort) log.Fatal(http.ListenAndServe(httpPort, setupRoutes())) } func getRawEvents(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(eventMgr.GetRawEvents()) } func getSensorEvents(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(eventMgr.GetSensorEvents()) } func getPotentialDevices(w http.ResponseWriter, r *http.Request) { devices := []*datastore.PotentialDevice{} for device := range store.ListPotentialDevices() { devices = append(devices, device) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(devices) } func renameDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } if err := store.UpdateDeviceName(id, req.Name); err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } // Republish discovery for this device if err := mqttClient.PublishDeviceDiscovery(id); err != nil { log.Printf("Error republishing device discovery: %v", err) } w.WriteHeader(http.StatusOK) } func renameSensor(w http.ResponseWriter, r *http.Request) { sensorIdStr := r.PathValue("sensor_id") sensorId, err := strconv.Atoi(sensorIdStr) if err != nil { http.Error(w, "Invalid sensor ID", http.StatusBadRequest) return } var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } sensor, err := store.GetSensor(sensorId) if err != nil { http.Error(w, "Sensor not found", http.StatusNotFound) return } if err := store.UpdateSensorName(sensorId, req.Name); err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } // Republish discovery for this sensor if err := mqttClient.PublishSensorDiscovery(sensor); err != nil { log.Printf("Error republishing sensor discovery: %v", err) } w.WriteHeader(http.StatusOK) } func hideSensor(w http.ResponseWriter, r *http.Request) { sensorIdStr := r.PathValue("sensor_id") sensorId, err := strconv.Atoi(sensorIdStr) if err != nil { http.Error(w, "Invalid sensor ID", http.StatusBadRequest) return } if err := store.SetSensorHidden(sensorId, true); err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func unhideSensor(w http.ResponseWriter, r *http.Request) { sensorIdStr := r.PathValue("sensor_id") sensorId, err := strconv.Atoi(sensorIdStr) if err != nil { http.Error(w, "Invalid sensor ID", http.StatusBadRequest) return } if err := store.SetSensorHidden(sensorId, false); err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func setupRoutes() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/api/devices", getDevices) mux.HandleFunc("POST /api/devices/{id}/turnon", turnOnDevice) mux.HandleFunc("POST /api/devices/{id}/learn", learnDevice) mux.HandleFunc("POST /api/devices/{id}/turnoff", turnOffDevice) mux.HandleFunc("/api/sensors", getSensors) mux.HandleFunc("/api/devices/{id}", getDevice) mux.HandleFunc("PUT /api/devices/{id}", renameDevice) mux.HandleFunc("PUT /api/sensors/{sensor_id}", renameSensor) mux.HandleFunc("PUT /api/sensors/{sensor_id}/hide", hideSensor) mux.HandleFunc("PUT /api/sensors/{sensor_id}/unhide", unhideSensor) mux.HandleFunc("/api/sensors/{sensor_id}", getSensor) mux.HandleFunc("/api/events/raw", getRawEvents) mux.HandleFunc("/api/events/sensor", getSensorEvents) mux.HandleFunc("/api/potential_devices", getPotentialDevices) // Config endpoints mux.HandleFunc("GET /api/config", getConfig) mux.HandleFunc("GET /api/config/devices", getConfigDevices) mux.HandleFunc("GET /api/config/devices/{id}", getConfigDevice) mux.HandleFunc("POST /api/config/devices", createConfigDevice) mux.HandleFunc("PUT /api/config/devices/{id}", updateConfigDevice) mux.HandleFunc("DELETE /api/config/devices/{id}", deleteConfigDevice) // Serve static files for the frontend mux.Handle("/", http.FileServer(http.Dir("./dist"))) return mux } func getDevices(w http.ResponseWriter, r *http.Request) { devices := []*datastore.Device{} for device := range store.ListDevices() { devices = append(devices, device) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(devices) } type CommandResult struct { Success bool `json:"success"` Message string `json:"message,omitempty"` Code int `json:"code,omitempty"` } func turnOnDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } status := telldus.TurnOn(id) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "turned on", Code: status}) } func learnDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } status := telldus.Learn(id) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "learning started", Code: status}) } func turnOffDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } status := telldus.TurnOff(id) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CommandResult{Success: status == 0, Message: "turned off", Code: status}) } func getSensors(w http.ResponseWriter, r *http.Request) { sensors := []*datastore.Sensor{} for sensor := range store.ListSensors() { sensors = append(sensors, sensor) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(sensors) } func getDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } device, err := store.GetDevice(id) if err != nil { http.Error(w, "Device not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(device) } func getSensor(w http.ResponseWriter, r *http.Request) { sensorIdStr := r.PathValue("sensor_id") sensorId, err := strconv.Atoi(sensorIdStr) if err != nil { http.Error(w, "Invalid sensor ID", http.StatusBadRequest) return } sensor, err := store.GetSensor(sensorId) if err != nil { http.Error(w, "Sensor not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(sensor) } // Config CRUD handlers func getConfig(w http.ResponseWriter, r *http.Request) { cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(cfg) } func getConfigDevices(w http.ResponseWriter, r *http.Request) { cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(cfg.Devices) } func getConfigDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } device := cfg.GetDevice(id) if device == nil { http.Error(w, "Device not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(device) } func createConfigDevice(w http.ResponseWriter, r *http.Request) { var device config.Device if err := json.NewDecoder(r.Body).Decode(&device); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } // Auto-assign ID if not provided or if ID is 0 if device.ID == 0 { device.ID = cfg.GetNextDeviceID() } cfg.AddDevice(device) if err := configParser.Write(cfg); err != nil { http.Error(w, "Failed to write config", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(device) } func updateConfigDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } var device config.Device if err := json.NewDecoder(r.Body).Decode(&device); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } device.ID = id // Ensure ID matches path parameter cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } if !cfg.UpdateDevice(device) { http.Error(w, "Device not found", http.StatusNotFound) return } if err := configParser.Write(cfg); err != nil { http.Error(w, "Failed to write config", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(device) } func deleteConfigDevice(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid device ID", http.StatusBadRequest) return } cfg, err := configParser.Parse() if err != nil { http.Error(w, "Failed to parse config", http.StatusInternalServerError) return } if !cfg.DeleteDevice(id) { http.Error(w, "Device not found", http.StatusNotFound) return } if err := configParser.Write(cfg); err != nil { http.Error(w, "Failed to write config", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }