feat(scheduler): resolveDesiredState – per-Screen, global, Wochenende, Zeitplan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 20:13:23 +01:00
parent be3a5f5aac
commit e76f89798f
2 changed files with 158 additions and 0 deletions

View file

@ -113,6 +113,35 @@ func desiredState(onTime, offTime, now string) string {
return "off"
}
// resolveDesiredState ermittelt den Soll-Zustand eines Screens unter Berücksichtigung
// aller Prioritätsstufen:
// 1. per-Screen override_on_until (höchste Priorität)
// 2. globaler Override
// 3. Wochenende (Sa/So)
// 4. normaler Zeitplan
//
// Gibt ("", false) zurück wenn keine Automatisierung aktiv ist.
func resolveDesiredState(sc store.ScreenSchedule, globalOverride *store.GlobalOverride, now time.Time) (desired string, shouldControl bool) {
// 1. Per-Screen-Override: überschreibt alles
if sc.OverrideOnUntil != nil && now.Before(*sc.OverrideOnUntil) {
return "on", true
}
// 2. Globaler Override
if globalOverride != nil && now.Before(globalOverride.Until) {
return globalOverride.Type, true
}
// 3. Wochenende: Zeitpläne werden ignoriert, Monitore bleiben aus
wd := now.Weekday()
if wd == time.Saturday || wd == time.Sunday {
return "off", true
}
// 4. Normaler Zeitplan
if !sc.ScheduleEnabled || (sc.PowerOnTime == "" && sc.PowerOffTime == "") {
return "", false
}
return desiredState(sc.PowerOnTime, sc.PowerOffTime, now.Format("15:04")), true
}
// 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).

View file

@ -0,0 +1,129 @@
package scheduler
import (
"testing"
"time"
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
)
func ptrTime(t time.Time) *time.Time { return &t }
func TestResolveDesiredState(t *testing.T) {
// Donnerstag 10:00 UTC
thu := time.Date(2026, 3, 26, 10, 0, 0, 0, time.UTC)
// Samstag 10:00 UTC
sat := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC)
tests := []struct {
name string
sc store.ScreenSchedule
override *store.GlobalOverride
now time.Time
wantDesired string
wantControl bool
}{
{
name: "per-screen override aktiv → on",
sc: store.ScreenSchedule{OverrideOnUntil: ptrTime(thu.Add(time.Hour))},
now: thu,
wantDesired: "on", wantControl: true,
},
{
name: "per-screen override abgelaufen → fällt durch zu Zeitplan",
sc: store.ScreenSchedule{
ScheduleEnabled: true,
PowerOnTime: "09:00", PowerOffTime: "17:00",
OverrideOnUntil: ptrTime(thu.Add(-time.Hour)), // abgelaufen
},
now: thu,
wantDesired: "on", wantControl: true, // Zeitplan: 09:0017:00, jetzt 10:00
},
{
name: "per-screen override schlägt globalen Override off",
sc: store.ScreenSchedule{OverrideOnUntil: ptrTime(thu.Add(time.Hour))},
override: &store.GlobalOverride{
Type: "off",
Until: thu.Add(time.Hour),
},
now: thu,
wantDesired: "on", wantControl: true,
},
{
name: "globaler Override off aktiv → off",
sc: store.ScreenSchedule{ScheduleEnabled: true, PowerOnTime: "09:00", PowerOffTime: "17:00"},
override: &store.GlobalOverride{
Type: "off",
Until: thu.Add(time.Hour),
},
now: thu,
wantDesired: "off", wantControl: true,
},
{
name: "globaler Override on aktiv → on",
sc: store.ScreenSchedule{},
override: &store.GlobalOverride{
Type: "on",
Until: thu.Add(time.Hour),
},
now: thu,
wantDesired: "on", wantControl: true,
},
{
name: "globaler Override abgelaufen → fällt durch",
sc: store.ScreenSchedule{ScheduleEnabled: true, PowerOnTime: "09:00", PowerOffTime: "17:00"},
override: &store.GlobalOverride{
Type: "off",
Until: thu.Add(-time.Hour), // abgelaufen
},
now: thu,
wantDesired: "on", wantControl: true,
},
{
name: "Wochenende → off",
sc: store.ScreenSchedule{ScheduleEnabled: true, PowerOnTime: "09:00", PowerOffTime: "17:00"},
now: sat,
wantDesired: "off", wantControl: true,
},
{
name: "Wochenende + per-screen override → on",
sc: store.ScreenSchedule{OverrideOnUntil: ptrTime(sat.Add(time.Hour))},
now: sat,
wantDesired: "on", wantControl: true,
},
{
name: "normaler Zeitplan: innerhalb → on",
sc: store.ScreenSchedule{ScheduleEnabled: true, PowerOnTime: "09:00", PowerOffTime: "17:00"},
now: thu, // 10:00
wantDesired: "on", wantControl: true,
},
{
name: "normaler Zeitplan: außerhalb → off",
sc: store.ScreenSchedule{ScheduleEnabled: true, PowerOnTime: "09:00", PowerOffTime: "17:00"},
now: time.Date(2026, 3, 26, 8, 0, 0, 0, time.UTC), // 08:00
wantDesired: "off", wantControl: true,
},
{
name: "kein Zeitplan, kein Override → keine Steuerung",
sc: store.ScreenSchedule{},
now: thu,
wantDesired: "", wantControl: false,
},
{
name: "Zeitplan deaktiviert → keine Steuerung",
sc: store.ScreenSchedule{ScheduleEnabled: false, PowerOnTime: "09:00", PowerOffTime: "17:00"},
now: thu,
wantDesired: "", wantControl: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDesired, gotControl := resolveDesiredState(tt.sc, tt.override, tt.now)
if gotDesired != tt.wantDesired || gotControl != tt.wantControl {
t.Errorf("resolveDesiredState() = (%q, %v), want (%q, %v)",
gotDesired, gotControl, tt.wantDesired, tt.wantControl)
}
})
}
}