package backthink import ( "context" "fmt" "log/slog" "strconv" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // StateProvider supplies the user's current condition for the interrupt decision // (design/12 §2). Production wires IoT/telemetry and calendar; tests inject a // mock. It must be concurrency-safe. type StateProvider interface { // Current returns the user's current UserState. A nil-able error means "use // a neutral state"; a hard error is logged by the caller and treated as // neutral. Current(ctx context.Context) (UserState, error) } // IoTStateProvider is a best-effort StateProvider built over an iot.Manager. It // scans the managed devices and reads heart-rate and sleep signals from any // device whose id/name/type mentions the corresponding keyword (e.g. "heart", // "心率", "sleep", "睡眠"). Work/busy come from a device/state flag; when nothing // matches it returns a neutral state rather than an error, so the service still // runs without full telemetry wiring. type IoTStateProvider struct { mgr *iot.Manager log *slog.Logger } // NewIoTStateProvider wraps mgr as a StateProvider. func NewIoTStateProvider(mgr *iot.Manager, log *slog.Logger) *IoTStateProvider { return &IoTStateProvider{mgr: mgr, log: logger.WithSource(baseLog(log), "services/backthink")} } // Current implements StateProvider. func (p *IoTStateProvider) Current(ctx context.Context) (UserState, error) { if p.mgr == nil { return UserState{}, fmt.Errorf("backthink: state: %w", iot.ErrProviderNotRegistered) } devices, err := p.mgr.List(ctx) if err != nil { return UserState{}, fmt.Errorf("backthink: state: list: %w", err) } var state UserState for _, d := range devices { key := strings.ToLower(d.ID + " " + d.Name + " " + d.Type) switch { case strings.Contains(key, "heart") || strings.Contains(key, "心率"): if v, ok := numState(d, "bpm", "heart_rate", "rate"); ok { state.HeartRateBPM = v } case strings.Contains(key, "sleep") || strings.Contains(key, "睡眠"): state.Sleeping = boolState(d, "sleeping", "asleep", "state") } if b, ok := boolStateOK(d, "working"); ok { state.Working = b } if b, ok := boolStateOK(d, "busy"); ok { state.Busy = b } } if p.log != nil { p.log.Debug("backthink iot state sampled", "devices", len(devices), "state", state.String()) } return state, nil } // numState reads the first numeric value among keys from a device's state map. func numState(d iot.Device, keys ...string) (int, bool) { for _, k := range keys { v, ok := d.State[k] if !ok { continue } switch n := v.(type) { case int: return n, true case int64: return int(n), true case float64: return int(n), true case string: if f, err := strconv.Atoi(n); err == nil { return f, true } } } return 0, false } // boolState reads a bool (or a "sleep"/"asleep" string) among keys. func boolState(d iot.Device, keys ...string) bool { b, _ := boolStateOK(d, keys...) return b } // boolStateOK reports whether a bool-typed state flag was found and its value. func boolStateOK(d iot.Device, keys ...string) (bool, bool) { for _, k := range keys { v, ok := d.State[k] if !ok { continue } switch n := v.(type) { case bool: return n, true case string: switch strings.ToLower(n) { case "sleep", "asleep", "busy", "working", "yes", "true": return true, true case "awake", "free", "idle", "no", "false": return false, true } } } return false, false }