quiet-hours: configurable time-window schedule
Adds QuietHours config section (start/end as HH:MM local time). The loop's gatherer checks the window each tick: if now falls within [start, end), the State.QuietHours flag is set to true regardless of the config fact (which the voice toggle writes independently). Both sources activate quiet — schedule AND toggle. Handles midnight-crossing windows (23:00-08:00). The gate already reads State.QuietHours for care nudge suppression — no gate change needed.
This commit is contained in:
+48
-2
@@ -18,14 +18,22 @@ import (
|
||||
// Gatherer — holds nothing mutable; the Store is the only dependency. The
|
||||
// daemon runs one Gatherer per tick.
|
||||
type Gatherer struct {
|
||||
store *store.Store
|
||||
rules []Rule
|
||||
store *store.Store
|
||||
rules []Rule
|
||||
quietStart string // "HH:MM" local time, "" = disabled
|
||||
quietEnd string // "HH:MM" local time, "" = disabled
|
||||
}
|
||||
|
||||
func NewGatherer(s *store.Store, rules []Rule) *Gatherer {
|
||||
return &Gatherer{store: s, rules: rules}
|
||||
}
|
||||
|
||||
// SetQuietHours enables the time-window schedule check. Times are local.
|
||||
func (g *Gatherer) SetQuietHours(start, end string) {
|
||||
g.quietStart = start
|
||||
g.quietEnd = end
|
||||
}
|
||||
|
||||
// GatherState — reads the store ONCE and assembles the snapshot the pure Tick
|
||||
// will operate on.
|
||||
//
|
||||
@@ -116,6 +124,12 @@ func (g *Gatherer) GatherState(ctx context.Context, now time.Time) (State, []sto
|
||||
if f, ok := readFact(ctx, g.store, "quiet_hours"); ok {
|
||||
quiet = f.Value == "true" || f.Value == `"true"`
|
||||
}
|
||||
// Schedule-based quiet hours: if a time window is configured AND we're
|
||||
// inside it, quiet is true regardless of the config fact. The voice
|
||||
// toggle extends activation independently — both can fire at once.
|
||||
if g.quietStart != "" && g.quietEnd != "" && inQuietWindow(now, g.quietStart, g.quietEnd) {
|
||||
quiet = true
|
||||
}
|
||||
var calBusy bool
|
||||
if f, ok := readFact(ctx, g.store, "calendar_busy"); ok {
|
||||
calBusy = f.Value == "true" || f.Value == `"true"`
|
||||
@@ -141,6 +155,38 @@ func (g *Gatherer) GatherState(ctx context.Context, now time.Time) (State, []sto
|
||||
return s, due, nil
|
||||
}
|
||||
|
||||
// inQuietWindow returns true when `now` falls within the daily window
|
||||
// [start, end). start/end are "HH:MM" in local time. Windows that cross
|
||||
// midnight (start > end) are handled: "23:00"-"08:00" means quiet from
|
||||
// 23:00 to 08:00 the next day.
|
||||
func inQuietWindow(now time.Time, start, end string) bool {
|
||||
startH, startM, ok1 := parseHHMM(start)
|
||||
endH, endM, ok2 := parseHHMM(end)
|
||||
if !ok1 || !ok2 {
|
||||
return false
|
||||
}
|
||||
nowMin := now.Hour()*60 + now.Minute()
|
||||
startMin := startH*60 + startM
|
||||
endMin := endH*60 + endM
|
||||
if startMin < endMin {
|
||||
return nowMin >= startMin && nowMin < endMin
|
||||
}
|
||||
// Crossing midnight: e.g., 23:00-08:00.
|
||||
return nowMin >= startMin || nowMin < endMin
|
||||
}
|
||||
|
||||
func parseHHMM(s string) (hour, min int, ok bool) {
|
||||
if len(s) != 5 || s[2] != ':' {
|
||||
return 0, 0, false
|
||||
}
|
||||
h := int(s[0]-'0')*10 + int(s[1]-'0')
|
||||
m := int(s[3]-'0')*10 + int(s[4]-'0')
|
||||
if h < 0 || h > 23 || m < 0 || m > 59 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return h, m, true
|
||||
}
|
||||
|
||||
func readFact(ctx context.Context, s *store.Store, key string) (store.Fact, bool) {
|
||||
f, err := s.LatestFact(ctx, key)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user