feat(iot): IoT 插件化(Provider/Manager/mock 8设备/能力约束/bus 事件);feat(capability/restrict): 工具级布尔标记(群聊/陌生人可用,默认关,策略引擎雏形)+ registry ToolPolicy
This commit is contained in:
1 parent
915aa8a100
commit
b3c61aa1ff
9 files changed
+1358
No files matched your search
@@ -0,0 +1,326 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// mockProvider is the built-in deterministic Provider with eight simulated
|
||||
// devices spread across every supported device type. It enforces the
|
||||
// control_rules (capability constraints) that bound what each device type
|
||||
// supports.
|
||||
type mockProvider struct {
|
||||
mu sync.RWMutex
|
||||
log *slog.Logger
|
||||
devices map[string]*Device
|
||||
rules map[string]typeRules
|
||||
}
|
||||
|
||||
// typeRules holds the control_rules for one device type: the set of supported
|
||||
// actions and how each mutates the device state.
|
||||
type typeRules struct {
|
||||
supported map[string]applyFunc
|
||||
}
|
||||
|
||||
// applyFunc mutates d.State according to the action. It returns an error when
|
||||
// the parameters are invalid.
|
||||
type applyFunc func(d *Device, params map[string]any) error
|
||||
|
||||
const mockProviderName = "mock"
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (m *mockProvider) Name() string { return mockProviderName }
|
||||
|
||||
func newMockProvider(log *slog.Logger) (*mockProvider, error) {
|
||||
m := &mockProvider{
|
||||
log: logger.WithSource(discardLogger(log), "iot/mock"),
|
||||
devices: make(map[string]*Device, len(mockDevices())),
|
||||
rules: buildRules(),
|
||||
}
|
||||
for _, d := range mockDevices() {
|
||||
m.devices[d.ID] = d
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// List returns every device, sorted by ID for deterministic output.
|
||||
func (m *mockProvider) List(ctx context.Context) ([]Device, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]Device, 0, len(m.devices))
|
||||
for _, d := range m.devices {
|
||||
out = append(out, d.Clone())
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Status returns a clone of the named device.
|
||||
func (m *mockProvider) Status(ctx context.Context, deviceID string) (*Device, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
d, ok := m.devices[deviceID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("iot: status %q: %w", deviceID, ErrDeviceNotFound)
|
||||
}
|
||||
cp := d.Clone()
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
// Control applies action to the named device, enforcing the type's control_rules.
|
||||
func (m *mockProvider) Control(ctx context.Context, deviceID, action string, params map[string]any) (*Device, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
d, ok := m.devices[deviceID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("iot: control %q: %w", deviceID, ErrDeviceNotFound)
|
||||
}
|
||||
if !d.Online {
|
||||
return nil, fmt.Errorf("iot: control %q: %w", deviceID, ErrOffline)
|
||||
}
|
||||
tr, ok := m.rules[d.Type]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("iot: control %q: %w", deviceID, ErrUnsupported)
|
||||
}
|
||||
apply, ok := tr.supported[action]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("iot: control %q action %q: %w", deviceID, action, ErrUnsupported)
|
||||
}
|
||||
if err := apply(d, params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.log.Debug("iot mock device controlled", "device", deviceID, "action", action)
|
||||
cp := d.Clone()
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
// buildRules returns the control_rules for every supported device type.
|
||||
func buildRules() map[string]typeRules {
|
||||
return map[string]typeRules{
|
||||
"light": lightRules(),
|
||||
"ac": acRules(),
|
||||
"curtain": curtainRules(),
|
||||
"sensor": sensorRules(),
|
||||
"lock": lockRules(),
|
||||
}
|
||||
}
|
||||
|
||||
// lightRules returns the control_rules for light devices.
|
||||
func lightRules() typeRules {
|
||||
return typeRules{supported: map[string]applyFunc{
|
||||
"on": func(d *Device, _ map[string]any) error {
|
||||
d.State["on"] = true
|
||||
if brightness(d) == 0 {
|
||||
d.State["brightness"] = 100
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"off": func(d *Device, _ map[string]any) error {
|
||||
d.State["on"] = false
|
||||
return nil
|
||||
},
|
||||
"toggle": func(d *Device, _ map[string]any) error {
|
||||
d.State["on"] = !boolOf(d.State["on"])
|
||||
return nil
|
||||
},
|
||||
"set_brightness": func(d *Device, p map[string]any) error {
|
||||
b, err := intParam(p, "brightness")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.State["brightness"] = clamp(b, 0, 100)
|
||||
d.State["on"] = b > 0
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// acRules returns the control_rules for AC devices.
|
||||
func acRules() typeRules {
|
||||
return typeRules{supported: map[string]applyFunc{
|
||||
"on": func(d *Device, _ map[string]any) error {
|
||||
d.State["on"] = true
|
||||
return nil
|
||||
},
|
||||
"off": func(d *Device, _ map[string]any) error {
|
||||
d.State["on"] = false
|
||||
return nil
|
||||
},
|
||||
"set_temperature": func(d *Device, p map[string]any) error {
|
||||
t, err := intParam(p, "temperature")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.State["temperature"] = clamp(t, 16, 30)
|
||||
return nil
|
||||
},
|
||||
"set_mode": func(d *Device, p map[string]any) error {
|
||||
mode, err := strParam(p, "mode")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch mode {
|
||||
case "cool", "heat", "fan", "dry":
|
||||
d.State["mode"] = mode
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("iot: control mode %q: %w", mode, ErrUnsupported)
|
||||
}
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// curtainRules returns the control_rules for curtain devices.
|
||||
func curtainRules() typeRules {
|
||||
return typeRules{supported: map[string]applyFunc{
|
||||
"open": func(d *Device, _ map[string]any) error {
|
||||
d.State["position"] = 100
|
||||
return nil
|
||||
},
|
||||
"close": func(d *Device, _ map[string]any) error {
|
||||
d.State["position"] = 0
|
||||
return nil
|
||||
},
|
||||
"stop": func(d *Device, _ map[string]any) error { return nil },
|
||||
"set_position": func(d *Device, p map[string]any) error {
|
||||
pos, err := intParam(p, "position")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.State["position"] = clamp(pos, 0, 100)
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// sensorRules returns the control_rules for sensor devices. Sensors are
|
||||
// read-only: no action is supported, so any Control attempt returns
|
||||
// ErrUnsupported.
|
||||
func sensorRules() typeRules {
|
||||
return typeRules{supported: map[string]applyFunc{}}
|
||||
}
|
||||
|
||||
// lockRules returns the control_rules for lock devices.
|
||||
func lockRules() typeRules {
|
||||
return typeRules{supported: map[string]applyFunc{
|
||||
"lock": func(d *Device, _ map[string]any) error {
|
||||
d.State["locked"] = true
|
||||
return nil
|
||||
},
|
||||
"unlock": func(d *Device, _ map[string]any) error {
|
||||
d.State["locked"] = false
|
||||
return nil
|
||||
},
|
||||
"toggle": func(d *Device, _ map[string]any) error {
|
||||
d.State["locked"] = !boolOf(d.State["locked"])
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// mockDevices returns the eight seeded mock devices. It covers all five types;
|
||||
// ac.bedroom is seeded offline to exercise the Online field and the offline
|
||||
// control guard.
|
||||
func mockDevices() []*Device {
|
||||
return []*Device{
|
||||
{ID: "ac.bedroom", Name: "卧室空调", Type: "ac", Online: false, State: map[string]any{"on": false, "temperature": 26, "mode": "cool"}},
|
||||
{ID: "ac.living", Name: "客厅空调", Type: "ac", Online: true, State: map[string]any{"on": false, "temperature": 26, "mode": "cool"}},
|
||||
{ID: "curtain.living", Name: "客厅窗帘", Type: "curtain", Online: true, State: map[string]any{"position": 0}},
|
||||
{ID: "light.bedroom", Name: "卧室灯", Type: "light", Online: true, State: map[string]any{"on": false, "brightness": 0}},
|
||||
{ID: "light.living", Name: "客厅灯", Type: "light", Online: true, State: map[string]any{"on": false, "brightness": 0}},
|
||||
{ID: "lock.back", Name: "后门锁", Type: "lock", Online: true, State: map[string]any{"locked": true}},
|
||||
{ID: "lock.front", Name: "前门锁", Type: "lock", Online: true, State: map[string]any{"locked": true}},
|
||||
{ID: "sensor.env", Name: "环境传感器", Type: "sensor", Online: true, State: map[string]any{"temperature": 24.5, "humidity": 60.0}},
|
||||
}
|
||||
}
|
||||
|
||||
func brightness(d *Device) int { return intOf(d.State["brightness"]) }
|
||||
|
||||
func intOf(v any) int {
|
||||
f, ok := toFloat64(v)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return int(f)
|
||||
}
|
||||
|
||||
func boolOf(v any) bool {
|
||||
b, _ := v.(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
func clamp(n, lo, hi int) int {
|
||||
if n < lo {
|
||||
return lo
|
||||
}
|
||||
if n > hi {
|
||||
return hi
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// intParam reads an integer action parameter, accepting int/int64/float64/string
|
||||
// shapes so JSON-decoded params work the same as literal ones.
|
||||
func intParam(p map[string]any, key string) (int, error) {
|
||||
v, ok := p[key]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("iot: missing %q parameter: %w", key, ErrInvalidParam)
|
||||
}
|
||||
f, ok := toFloat64(v)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("iot: %q must be a number: %w", key, ErrInvalidParam)
|
||||
}
|
||||
return int(f), nil
|
||||
}
|
||||
|
||||
func strParam(p map[string]any, key string) (string, error) {
|
||||
v, ok := p[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("iot: missing %q parameter: %w", key, ErrInvalidParam)
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("iot: %q must be a string: %w", key, ErrInvalidParam)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func toFloat64(v any) (float64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n, true
|
||||
case float32:
|
||||
return float64(n), true
|
||||
case int:
|
||||
return float64(n), true
|
||||
case int8:
|
||||
return float64(n), true
|
||||
case int16:
|
||||
return float64(n), true
|
||||
case int32:
|
||||
return float64(n), true
|
||||
case int64:
|
||||
return float64(n), true
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(n, 64)
|
||||
if err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in new issue
Block a user