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 }