feat(scheduler): resolveDesiredState – per-Screen, global, Wochenende, Zeitplan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
be3a5f5aac
commit
e76f89798f
2 changed files with 158 additions and 0 deletions
|
|
@ -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).
|
||||
|
|
|
|||
129
server/backend/internal/scheduler/scheduler_test.go
Normal file
129
server/backend/internal/scheduler/scheduler_test.go
Normal 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:00–17: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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue