From 4ef16048adb9845660b8c95163192f8116b16eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Thu, 26 Mar 2026 23:07:14 +0100 Subject: [PATCH] feat(agent): displaycontroller Package (xset DPMS) --- .../internal/displaycontroller/controller.go | 74 +++++++++++++++++++ .../displaycontroller/controller_test.go | 18 +++++ 2 files changed, 92 insertions(+) create mode 100644 player/agent/internal/displaycontroller/controller.go create mode 100644 player/agent/internal/displaycontroller/controller_test.go diff --git a/player/agent/internal/displaycontroller/controller.go b/player/agent/internal/displaycontroller/controller.go new file mode 100644 index 0000000..66bf8a0 --- /dev/null +++ b/player/agent/internal/displaycontroller/controller.go @@ -0,0 +1,74 @@ +// Package displaycontroller steuert das physische Display per xset DPMS. +package displaycontroller + +import ( + "fmt" + "log/slog" + "os/exec" + "sync" +) + +// Controller führt xset-Befehle aus und verfolgt den letzten Display-Zustand. +// onStateChange wird nach jeder erfolgreichen Ausführung mit dem neuen Zustand +// aufgerufen (z. B. um ihn per MQTT zu publizieren). Darf nil sein. +type Controller struct { + display string + screenSlug string + onStateChange func(screenSlug, state string) + mu sync.Mutex + currentState string +} + +// New erstellt einen Controller. display ist der Wert der DISPLAY-Env-Var (z. B. ":0"). +func New(display, screenSlug string, onStateChange func(screenSlug, state string)) *Controller { + return &Controller{ + display: display, + screenSlug: screenSlug, + onStateChange: onStateChange, + currentState: "unknown", + } +} + +// Execute führt eine Display-Aktion aus. Bekannte Aktionen: "display_on", "display_off". +func (c *Controller) Execute(action string) { + switch action { + case "display_on": + if err := c.runXset("on"); err != nil { + slog.Error("display_on failed", "err", err) + return + } + c.setState("on") + case "display_off": + if err := c.runXset("off"); err != nil { + slog.Error("display_off failed", "err", err) + return + } + c.setState("off") + default: + slog.Warn("unknown display action", "action", action) + } +} + +// State gibt den zuletzt gesetzten Display-Zustand zurück: "on", "off" oder "unknown". +func (c *Controller) State() string { + c.mu.Lock() + defer c.mu.Unlock() + return c.currentState +} + +func (c *Controller) runXset(arg string) error { + out, err := exec.Command("xset", "-display", c.display, "dpms", "force", arg).CombinedOutput() + if err != nil { + return fmt.Errorf("xset dpms force %s: %w (output: %s)", arg, err, out) + } + return nil +} + +func (c *Controller) setState(state string) { + c.mu.Lock() + c.currentState = state + c.mu.Unlock() + if c.onStateChange != nil { + go c.onStateChange(c.screenSlug, state) + } +} diff --git a/player/agent/internal/displaycontroller/controller_test.go b/player/agent/internal/displaycontroller/controller_test.go new file mode 100644 index 0000000..ef6ec29 --- /dev/null +++ b/player/agent/internal/displaycontroller/controller_test.go @@ -0,0 +1,18 @@ +package displaycontroller + +import "testing" + +func TestNew_initialState(t *testing.T) { + c := New(":0", "test-screen", nil) + if got := c.State(); got != "unknown" { + t.Fatalf("initial state = %q, want %q", got, "unknown") + } +} + +func TestExecute_unknownAction(t *testing.T) { + c := New(":0", "test-screen", nil) + c.Execute("invalid_action") + if got := c.State(); got != "unknown" { + t.Fatalf("state after unknown action = %q, want %q", got, "unknown") + } +}