124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
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
|
|
}
|