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 }