diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..ba91c2f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.25.4-bookworm + +# Install dependencies for telldus-core +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + libssl-dev \ + libavahi-client-dev \ + libglib2.0-dev \ + libftdi-dev \ + libconfuse-dev \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +# Build telldus-core +COPY ./telldus-core /usr/src/telldus-core +WORKDIR /usr/src/telldus-core +RUN cmake . \ + && make \ + && make install \ + && ldconfig + +# Set workdir for the project +WORKDIR /workspaces/go-telldus-matter + +ENV GO111MODULE=on +ENV CGO_ENABLED=1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c89dcf0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +{ + "name": "Go Telldus Matter Dev", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "workspaceFolder": "/workspaces/go-telldus-matter", + "runArgs": [ + "--privileged" + ], + "mounts": [ + "source=/dev/bus/usb,target=/dev/bus/usb,type=bind", + "source=${localWorkspaceFolder}/tellstick.conf,target=/etc/tellstick.conf,type=bind" + ], + "containerEnv": { + "MQTT_URL": "10.10.3.12:1883", + "MQTT_USER": "telldus-dev", + "MQTT_PASSWORD": "" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.Go", + "ms-vscode.vscode-typescript-next" + ], + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true + } + } + }, + "postCreateCommand": "go mod download && cd frontend && npm install" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8a20968..e88d973 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,17 +50,7 @@ RUN cp -r dist /go/src/app/ # Set workdir back WORKDIR /go/src/app -# Create startup script -RUN echo '#!/bin/bash\n\ -/usr/local/sbin/telldusd --nodaemon &\n\ -TELLDUSD_PID=$!\n\ -./main &\n\ -MAIN_PID=$!\n\ -trap "kill $MAIN_PID $TELLDUSD_PID; exit" INT TERM\n\ -wait $MAIN_PID\n\ -kill $TELLDUSD_PID' > start.sh && chmod +x start.sh - EXPOSE 8080 -# Run the application -CMD ["./start.sh"] \ No newline at end of file +# Run the application (it will manage telldusd internally) +CMD ["./main"] \ No newline at end of file diff --git a/chip b/chip deleted file mode 160000 index 9548aca..0000000 --- a/chip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9548aca151d8b629ce208f9304a2f80e27be405b diff --git a/db/telldus.db b/db/telldus.db index 0153ce0..c6143a2 100644 Binary files a/db/telldus.db and b/db/telldus.db differ diff --git a/go.mod b/go.mod index c70f740..97a3549 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,14 @@ module app go 1.25.4 require ( - github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sync v0.17.0 // indirect + github.com/eclipse/paho.mqtt.golang v1.5.1 + github.com/fsnotify/fsnotify v1.7.0 + github.com/mattn/go-sqlite3 v1.14.32 +) + +require ( + github.com/gorilla/websocket v1.5.3 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 6b68644..313c2e5 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/main.go b/main.go index 8014540..af17918 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,16 @@ package main import ( "app/telldus" + "bufio" "database/sql" "encoding/json" "fmt" "log" "net/http" "os" + "os/exec" "os/signal" + "path/filepath" "strconv" "strings" "sync" @@ -17,6 +20,7 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" _ "github.com/mattn/go-sqlite3" + "github.com/fsnotify/fsnotify" ) const ( @@ -25,6 +29,8 @@ const ( var mqttDev *mqttDevice var db *sql.DB +var telldusCmd *exec.Cmd +var telldusMu sync.Mutex type mqttDevice struct { client mqtt.Client @@ -216,7 +222,189 @@ var mu sync.Mutex const maxEvents = 1000 +// startTelldusd starts the telldusd daemon and captures its output +func startTelldusd() error { + telldusMu.Lock() + defer telldusMu.Unlock() + + if telldusCmd != nil && telldusCmd.Process != nil { + log.Println("Telldusd already running") + return nil + } + + log.Println("Starting telldusd...") + cmd := exec.Command("/usr/local/sbin/telldusd", "--nodaemon") + + // Capture stdout + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to get stdout pipe: %v", err) + } + + // Capture stderr + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to get stderr pipe: %v", err) + } + + // Start the command + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start telldusd: %v", err) + } + + telldusCmd = cmd + + // Log stdout in a goroutine + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + log.Printf("[telldusd] %s", scanner.Text()) + } + }() + + // Log stderr in a goroutine + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + log.Printf("[telldusd] ERROR: %s", scanner.Text()) + } + }() + + // Monitor process in a goroutine + go func() { + err := cmd.Wait() + telldusMu.Lock() + telldusCmd = nil + telldusMu.Unlock() + if err != nil { + log.Printf("Telldusd exited with error: %v", err) + } else { + log.Println("Telldusd exited normally") + } + }() + + // Give telldusd a moment to start + time.Sleep(500 * time.Millisecond) + log.Println("Telldusd started successfully") + return nil +} + +// stopTelldusd stops the telldusd daemon +func stopTelldusd() error { + telldusMu.Lock() + defer telldusMu.Unlock() + + if telldusCmd == nil || telldusCmd.Process == nil { + log.Println("Telldusd not running") + return nil + } + + log.Println("Stopping telldusd...") + + // Send SIGTERM + if err := telldusCmd.Process.Signal(syscall.SIGTERM); err != nil { + log.Printf("Failed to send SIGTERM to telldusd: %v", err) + // Try SIGKILL as fallback + if err := telldusCmd.Process.Kill(); err != nil { + return fmt.Errorf("failed to kill telldusd: %v", err) + } + } + + // Wait for process to exit (with timeout) + done := make(chan error, 1) + go func() { + done <- telldusCmd.Wait() + }() + + select { + case <-done: + log.Println("Telldusd stopped successfully") + case <-time.After(5 * time.Second): + log.Println("Telldusd did not stop gracefully, killing...") + telldusCmd.Process.Kill() + } + + telldusCmd = nil + return nil +} + +// restartTelldusd restarts the telldusd daemon +func restartTelldusd() error { + log.Println("Restarting telldusd due to configuration change...") + + if err := stopTelldusd(); err != nil { + log.Printf("Error stopping telldusd: %v", err) + } + + // Give it a moment to fully stop + time.Sleep(1 * time.Second) + + // Close and reinitialize telldus library + telldus.Close() + time.Sleep(500 * time.Millisecond) + + if err := startTelldusd(); err != nil { + return err + } + + // Reinitialize telldus library + telldus.Init() + + log.Println("Telldusd restarted successfully") + return nil +} + +// watchConfigFile watches for changes to the tellstick.conf file +func watchConfigFile(configPath string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Failed to create file watcher: %v", err) + return + } + defer watcher.Close() + + // Watch the parent directory since file operations might replace the file + configDir := filepath.Dir(configPath) + if err := watcher.Add(configDir); err != nil { + log.Printf("Failed to watch config directory: %v", err) + return + } + + log.Printf("Watching for changes to %s", configPath) + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + // Check if the event is for our config file + if event.Name == configPath && (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) { + log.Printf("Configuration file changed: %s", event.Op.String()) + if err := restartTelldusd(); err != nil { + log.Printf("Failed to restart telldusd: %v", err) + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("File watcher error: %v", err) + } + } +} + func main() { + // Start telldusd daemon + if err := startTelldusd(); err != nil { + log.Fatalf("Failed to start telldusd: %v", err) + } + defer stopTelldusd() + + // Start watching config file + configPath := "/etc/tellstick.conf" + go watchConfigFile(configPath) + // Initialize Telldus telldus.Init() defer telldus.Close() @@ -328,6 +516,7 @@ func main() { log.Println("Shutting down gracefully...") mqttDev.client.Disconnect(250) telldus.Close() + stopTelldusd() os.Exit(0) }()