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"
|
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.
|
// check prüft alle aktiven Zeitpläne und sendet ggf. Befehle.
|
||||||
func check(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, notifier DisplayCommander) {
|
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).
|
// 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