feat(scheduler): Reconciler gleicht Ist- und Soll-Display-Zustand ab
Fügt Reconcile() und desiredState() zum Scheduler-Package hinzu. Der Reconciler läuft alle 5 Minuten, berechnet den Soll-Zustand aus den konfigurierten Ein-/Ausschaltzeiten (inkl. Mitternacht-Überschreitung) und sendet bei Abweichung oder unbekanntem Ist-Zustand einen MQTT-Befehl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
88e10d1e67
commit
ccec32c832
2 changed files with 79 additions and 0 deletions
|
|
@ -146,6 +146,7 @@ func (a *App) Run() error {
|
|||
|
||||
// Display-Zeitplan-Scheduler
|
||||
go scheduler.Run(ctx, a.scheduleStore, a.screenStore, a.notifier)
|
||||
go scheduler.Reconcile(ctx, a.scheduleStore, a.screenStore, a.screenStore, a.notifier)
|
||||
|
||||
// W2: Signal-Handler für Graceful Shutdown.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ type ScreenSlugGetter interface {
|
|||
GetByID(ctx context.Context, id string) (*store.Screen, error)
|
||||
}
|
||||
|
||||
// DisplayStateGetter lädt den zuletzt gemeldeten Display-Zustand.
|
||||
type DisplayStateGetter interface {
|
||||
GetDisplayState(ctx context.Context, screenID string) (string, error)
|
||||
}
|
||||
|
||||
// Run startet den Scheduler-Loop. Blockiert bis ctx abgebrochen wird.
|
||||
func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, notifier DisplayCommander) {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
|
@ -35,6 +40,79 @@ func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens Scre
|
|||
}
|
||||
}
|
||||
|
||||
// Reconcile läuft alle 5 Minuten und gleicht Ist- und Soll-Zustand ab.
|
||||
func Reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, states DisplayStateGetter, notifier DisplayCommander) {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
reconcile(ctx, schedules, screens, states, notifier)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, states DisplayStateGetter, notifier DisplayCommander) {
|
||||
now := time.Now().Format("15:04")
|
||||
|
||||
enabled, err := schedules.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
slog.Error("reconciler: list enabled schedules failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, sc := range enabled {
|
||||
if sc.PowerOnTime == "" || sc.PowerOffTime == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
want := desiredState(sc.PowerOnTime, sc.PowerOffTime, now)
|
||||
|
||||
got, err := states.GetDisplayState(ctx, sc.ScreenID)
|
||||
if err != nil {
|
||||
slog.Warn("reconciler: get display state failed", "screen_id", sc.ScreenID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if got == want {
|
||||
continue
|
||||
}
|
||||
|
||||
screen, err := screens.GetByID(ctx, sc.ScreenID)
|
||||
if err != nil {
|
||||
slog.Warn("reconciler: screen not found", "screen_id", sc.ScreenID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
action := "display_" + want
|
||||
if err := notifier.SendDisplayCommand(screen.Slug, action); err != nil {
|
||||
slog.Error("reconciler: send command failed", "screen_id", sc.ScreenID, "action", action, "err", err)
|
||||
} else {
|
||||
slog.Info("reconciler: corrected display state", "screen_id", sc.ScreenID, "slug", screen.Slug, "was", got, "want", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// desiredState berechnet den Soll-Zustand für einen einfachen Tagesplan.
|
||||
// Unterstützt Mitternacht-Überschreitungen (z.B. onTime="22:00", offTime="06:00").
|
||||
func desiredState(onTime, offTime, now string) string {
|
||||
if onTime <= offTime {
|
||||
// Normaler Fall: z.B. 06:00–22:00
|
||||
if now >= onTime && now < offTime {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
// Mitternacht-Überschreitung: z.B. 22:00–06:00
|
||||
if now >= onTime || now < offTime {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
// check prüft alle aktiven Zeitpläne und sendet ggf. Befehle.
|
||||
func check(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, notifier DisplayCommander) {
|
||||
// Uses process-local timezone — ensure TZ env var is set in the container (e.g. Europe/Berlin).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue