Files
Cyrene_v2/internal/services/backthink/state.go
T

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
}