Files
go-telldus-matter/main.go
2025-11-23 12:05:34 +00:00

488 lines
14 KiB
Go

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)
}