feat(scheduler): Reconciler iteriert alle Screens + resolveDesiredState

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 20:14:32 +01:00
parent e76f89798f
commit 81711f2f3d

View file

@ -25,6 +25,11 @@ type DisplayStateGetter interface {
GetDisplayState(ctx context.Context, screenID string) (string, error) GetDisplayState(ctx context.Context, screenID string) (string, error)
} }
// AllScreensLister lädt alle bekannten Screens.
type AllScreensLister interface {
ListAll(ctx context.Context) ([]*store.Screen, error)
}
// Run startet den Scheduler-Loop. Blockiert bis ctx abgebrochen wird. // Run startet den Scheduler-Loop. Blockiert bis ctx abgebrochen wird.
func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, notifier DisplayCommander) { func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, notifier DisplayCommander) {
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(1 * time.Minute)
@ -41,39 +46,50 @@ func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens Scre
} }
// Reconcile läuft alle 5 Minuten und gleicht Ist- und Soll-Zustand ab. // 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) { func Reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screens AllScreensLister, states DisplayStateGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
reconcile(ctx, schedules, screens, states, notifier) reconcile(ctx, schedules, screens, states, globalOverrides, notifier)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
} }
} }
func reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, states DisplayStateGetter, notifier DisplayCommander) { func reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, allScreens AllScreensLister, states DisplayStateGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
now := time.Now().Format("15:04") now := time.Now()
enabled, err := schedules.ListEnabled(ctx) screenList, err := allScreens.ListAll(ctx)
if err != nil { if err != nil {
slog.Error("reconciler: list enabled schedules failed", "err", err) slog.Error("reconciler: list all screens failed", "err", err)
return return
} }
for _, sc := range enabled { globalOverride, err := globalOverrides.Get(ctx)
if sc.PowerOnTime == "" || sc.PowerOffTime == "" { if err != nil {
slog.Warn("reconciler: get global override failed", "err", err)
// nicht fatal — ohne Override fortfahren
}
for _, screen := range screenList {
sc, err := schedules.Get(ctx, screen.ID)
if err != nil {
slog.Warn("reconciler: get schedule failed", "screen_id", screen.ID, "err", err)
continue continue
} }
want := desiredState(sc.PowerOnTime, sc.PowerOffTime, now) want, shouldControl := resolveDesiredState(*sc, globalOverride, now)
if !shouldControl {
continue
}
got, err := states.GetDisplayState(ctx, sc.ScreenID) got, err := states.GetDisplayState(ctx, screen.ID)
if err != nil { if err != nil {
slog.Warn("reconciler: get display state failed", "screen_id", sc.ScreenID, "err", err) slog.Warn("reconciler: get display state failed", "screen_id", screen.ID, "err", err)
continue continue
} }
@ -81,17 +97,11 @@ func reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screen
continue 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 action := "display_" + want
if err := notifier.SendDisplayCommand(screen.Slug, action); err != nil { if err := notifier.SendDisplayCommand(screen.Slug, action); err != nil {
slog.Error("reconciler: send command failed", "screen_id", sc.ScreenID, "action", action, "err", err) slog.Error("reconciler: send command failed", "screen_id", screen.ID, "action", action, "err", err)
} else { } else {
slog.Info("reconciler: corrected display state", "screen_id", sc.ScreenID, "slug", screen.Slug, "was", got, "want", want) slog.Info("reconciler: corrected display state", "screen_id", screen.ID, "slug", screen.Slug, "was", got, "want", want)
} }
} }
} }