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:
@@ -78,6 +78,9 @@ func run(args []string) error {
|
||||
|
||||
rules := loop.DefaultRules()
|
||||
gatherer := loop.NewGatherer(st, rules)
|
||||
if cfg.QuietHours != nil {
|
||||
gatherer.SetQuietHours(cfg.QuietHours.Start, cfg.QuietHours.End)
|
||||
}
|
||||
|
||||
// ----- phraser (LLM-backed when configured, Stub floor otherwise) -----
|
||||
var phr phraser.Phraser = phraser.NewStub()
|
||||
|
||||
@@ -85,6 +85,22 @@ type Config struct {
|
||||
// To enable: voice.enabled = true AND voice.bind = an address inside
|
||||
// the wg tunnel; the daemon binds the TCP listener there.
|
||||
Voice *VoiceConfig `json:"voice,omitempty"`
|
||||
|
||||
// QuietHours — time-window schedule for quiet hours. When set, the
|
||||
// loop's gatherer sets `quiet = true` in the State during the window,
|
||||
// suppressing care nudges (sev1-2). The user can also toggle quiet
|
||||
// hours by voice ("тихий режим") — that writes a `quiet_hours` config
|
||||
// fact independently; both the schedule AND the toggle activate quiet.
|
||||
// nil ⇒ quiet hours only activate via the voice toggle.
|
||||
QuietHours *QuietHoursConfig `json:"quiet_hours,omitempty"`
|
||||
}
|
||||
|
||||
// QuietHoursConfig — a recurring daily quiet-window. Times are local to the
|
||||
// server's wall clock. A window crossing midnight (Start > End) is handled:
|
||||
// "23:00"-"08:00" means quiet from 23:00 to 08:00 the next day.
|
||||
type QuietHoursConfig struct {
|
||||
Start string `json:"start,omitempty"` // "HH:MM" local time, e.g. "23:00"
|
||||
End string `json:"end,omitempty"` // "HH:MM" local time, e.g. "08:00"
|
||||
}
|
||||
|
||||
// VoiceConfig — the client↔core TCP surface + the stt/tts worker-module
|
||||
|
||||
+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