- 5 个文件:123 行英文注释 → 99 行中文注释(中文更紧凑,内容无删减)。 - 机械校验(git diff -U0 去掉 // 行与空行)输出为空;gofmt -l 干净(改动前亦干净)。 - 验收:go build/vet/test ./internal/iot/... 全绿。
322 lines
8.7 KiB
Go
322 lines
8.7 KiB
Go
package iot
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log/slog"
|
||
"sort"
|
||
"strconv"
|
||
"sync"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
)
|
||
|
||
// mockProvider 是内置的确定性 Provider,八台模拟设备覆盖全部受支持的设备类型。
|
||
// 它施加 control_rules(capability 约束),界定每种设备类型的支持范围。
|
||
type mockProvider struct {
|
||
mu sync.RWMutex
|
||
log *slog.Logger
|
||
devices map[string]*Device
|
||
rules map[string]typeRules
|
||
}
|
||
|
||
// typeRules 保存单一设备类型的 control_rules:受支持的动作集合,以及每个动作
|
||
// 如何改动设备状态。
|
||
type typeRules struct {
|
||
supported map[string]applyFunc
|
||
}
|
||
|
||
// applyFunc 依据动作修改 d.State;参数非法时返回错误。
|
||
type applyFunc func(d *Device, params map[string]any) error
|
||
|
||
const mockProviderName = "mock"
|
||
|
||
// Name 返回 Provider 标识。
|
||
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 返回全部设备,按 ID 排序以保证输出确定。
|
||
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 返回指定设备的副本。
|
||
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 对指定设备施加 action,并强制该类型的 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 返回全部受支持设备类型的 control_rules。
|
||
func buildRules() map[string]typeRules {
|
||
return map[string]typeRules{
|
||
"light": lightRules(),
|
||
"ac": acRules(),
|
||
"curtain": curtainRules(),
|
||
"sensor": sensorRules(),
|
||
"lock": lockRules(),
|
||
}
|
||
}
|
||
|
||
// lightRules 返回灯类设备的 control_rules。
|
||
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 返回空调设备的 control_rules。
|
||
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 返回窗帘设备的 control_rules。
|
||
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 返回传感器设备的 control_rules。传感器只读:不支持任何动作,
|
||
// 因此任何 Control 尝试都会返回 ErrUnsupported。
|
||
func sensorRules() typeRules {
|
||
return typeRules{supported: map[string]applyFunc{}}
|
||
}
|
||
|
||
// lockRules 返回门锁设备的 control_rules。
|
||
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 返回八台预置的模拟设备,覆盖全部五种类型;ac.bedroom 预置为离线,
|
||
// 用于检验 Online 字段与离线控制保护逻辑。
|
||
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 读取整型动作参数,可接受 int/int64/float64/string 等多种形态,
|
||
// 使 JSON 解码得到的参数与字面量参数行为一致。
|
||
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
|
||
}
|