package main import ( "encoding/json" "fmt" "log" "math/rand" "net/http" "os" "strings" "sync" "time" ) // DeviceType 设备类型 type DeviceType string const ( TypeLight DeviceType = "light" TypeAC DeviceType = "ac" TypeCurtain DeviceType = "curtain" TypeSensor DeviceType = "sensor" TypeLock DeviceType = "lock" ) // Device 设备状态 type Device struct { ID string `json:"id"` Name string `json:"name"` Type DeviceType `json:"type"` Status string `json:"status"` // on/off/closed/open/locked/unlocked Brightness int `json:"brightness,omitempty"` Color string `json:"color,omitempty"` Temperature float64 `json:"temperature,omitempty"` Mode string `json:"mode,omitempty"` // cool/heat/auto Position int `json:"position,omitempty"` // curtain position 0-100 Value float64 `json:"value,omitempty"` // sensor value Unit string `json:"unit,omitempty"` // sensor unit Battery int `json:"battery,omitempty"` // lock battery LastUpdated string `json:"last_updated"` History []HistoryEntry `json:"history,omitempty"` } // HistoryEntry 设备状态历史 type HistoryEntry struct { Timestamp string `json:"timestamp"` Field string `json:"field"` OldValue string `json:"old_value"` NewValue string `json:"new_value"` } // DeviceStore 设备存储(线程安全) type DeviceStore struct { mu sync.RWMutex devices map[string]*Device history map[string][]HistoryEntry } func NewDeviceStore() *DeviceStore { ds := &DeviceStore{ devices: make(map[string]*Device), history: make(map[string][]HistoryEntry), } ds.initDevices() return ds } func (ds *DeviceStore) initDevices() { now := time.Now().UTC().Format(time.RFC3339) devices := []*Device{ {ID: "light-livingroom", Name: "客厅灯", Type: TypeLight, Status: "on", Brightness: 80, Color: "warm_white", LastUpdated: now}, {ID: "light-bedroom", Name: "卧室灯", Type: TypeLight, Status: "off", Brightness: 0, Color: "warm_white", LastUpdated: now}, {ID: "ac-livingroom", Name: "客厅空调", Type: TypeAC, Status: "on", Temperature: 26, Mode: "cool", LastUpdated: now}, {ID: "ac-bedroom", Name: "卧室空调", Type: TypeAC, Status: "off", Temperature: 24, Mode: "auto", LastUpdated: now}, {ID: "curtain-livingroom", Name: "客厅窗帘", Type: TypeCurtain, Status: "closed", Position: 0, LastUpdated: now}, {ID: "sensor-temperature", Name: "温度传感器", Type: TypeSensor, Value: 25.5, Unit: "celsius", LastUpdated: now}, {ID: "sensor-humidity", Name: "湿度传感器", Type: TypeSensor, Value: 60, Unit: "percent", LastUpdated: now}, {ID: "lock-door", Name: "智能门锁", Type: TypeLock, Status: "locked", Battery: 85, LastUpdated: now}, } for _, d := range devices { ds.devices[d.ID] = d ds.history[d.ID] = make([]HistoryEntry, 0) } } // GetAll 获取所有设备 func (ds *DeviceStore) GetAll() []*Device { ds.mu.RLock() defer ds.mu.RUnlock() result := make([]*Device, 0, len(ds.devices)) for _, d := range ds.devices { cp := *d cp.History = nil // 列表不返回历史 result = append(result, &cp) } return result } // Get 获取单个设备 func (ds *DeviceStore) Get(id string) *Device { ds.mu.RLock() defer ds.mu.RUnlock() d, ok := ds.devices[id] if !ok { return nil } cp := *d // 包含最近10条历史(RLock 可重入) if h, ok := ds.history[id]; ok && len(h) > 0 { start := 0 if len(h) > 10 { start = len(h) - 10 } cp.History = make([]HistoryEntry, len(h)-start) copy(cp.History, h[start:]) } else { cp.History = []HistoryEntry{} } return &cp } // GetHistory 获取设备状态历史(最近10条) func (ds *DeviceStore) GetHistory(id string) []HistoryEntry { ds.mu.RLock() defer ds.mu.RUnlock() h, ok := ds.history[id] if !ok || len(h) == 0 { return []HistoryEntry{} } start := 0 if len(h) > 10 { start = len(h) - 10 } result := make([]HistoryEntry, len(h[start:])) copy(result, h[start:]) return result } // addHistory 添加历史记录 func (ds *DeviceStore) addHistory(id, field, oldVal, newVal string) { entry := HistoryEntry{ Timestamp: time.Now().UTC().Format(time.RFC3339), Field: field, OldValue: oldVal, NewValue: newVal, } ds.history[id] = append(ds.history[id], entry) } // Toggle 切换设备开关状态 func (ds *DeviceStore) Toggle(id string) (*Device, error) { ds.mu.Lock() defer ds.mu.Unlock() d, ok := ds.devices[id] if !ok { return nil, fmt.Errorf("设备 %s 不存在", id) } switch d.Type { case TypeLight: oldStatus := d.Status if d.Status == "on" { d.Status = "off" d.Brightness = 0 } else { d.Status = "on" d.Brightness = 80 } d.LastUpdated = time.Now().UTC().Format(time.RFC3339) ds.addHistory(id, "status", oldStatus, d.Status) case TypeAC: oldStatus := d.Status if d.Status == "on" { d.Status = "off" } else { d.Status = "on" } d.LastUpdated = time.Now().UTC().Format(time.RFC3339) ds.addHistory(id, "status", oldStatus, d.Status) case TypeCurtain: oldStatus := d.Status if d.Status == "closed" { d.Status = "open" d.Position = 100 } else { d.Status = "closed" d.Position = 0 } d.LastUpdated = time.Now().UTC().Format(time.RFC3339) ds.addHistory(id, "status", oldStatus, d.Status) case TypeLock: oldStatus := d.Status if d.Status == "locked" { d.Status = "unlocked" } else { d.Status = "locked" } d.LastUpdated = time.Now().UTC().Format(time.RFC3339) ds.addHistory(id, "status", oldStatus, d.Status) default: return nil, fmt.Errorf("设备类型 %s 不支持切换", d.Type) } cp := *d // 不能调用 ds.GetHistory(id),因为当前已持有写锁,Go 的 RWMutex 不允许写锁重入读锁 if h, ok := ds.history[id]; ok && len(h) > 0 { start := 0 if len(h) > 10 { start = len(h) - 10 } cp.History = make([]HistoryEntry, len(h)-start) copy(cp.History, h[start:]) } else { cp.History = []HistoryEntry{} } return &cp, nil } // SetProperty 设置设备属性(状态、温度、亮度、位置、模式、颜色等) func (ds *DeviceStore) SetProperty(id, field string, value interface{}) (*Device, error) { ds.mu.Lock() defer ds.mu.Unlock() d, ok := ds.devices[id] if !ok { return nil, fmt.Errorf("设备 %s 不存在", id) } now := time.Now().UTC().Format(time.RFC3339) switch field { case "status", "power": // 声明式电源控制:支持 "on"/"off"/"open"/"closed"/"locked"/"unlocked" // 同时支持中文值: "开"/"关"/"打开"/"关闭" // 支持布尔值: true/false → on/off var newStatus string switch v := value.(type) { case string: newStatus = normalizeStatus(v, d.Type) if newStatus == "" { return nil, fmt.Errorf("无效的状态值: %s (设备类型 %s 不支持此状态)", v, d.Type) } case bool: if v { newStatus = "on" } else { newStatus = "off" } case float64: if v == 0 { newStatus = "off" } else { newStatus = "on" } default: return nil, fmt.Errorf("status 需要字符串、布尔值或数字") } // 对于非 on/off 设备类型(curtain/lock),转换为对应状态 switch d.Type { case TypeCurtain: if newStatus == "on" { newStatus = "open" } else if newStatus == "off" { newStatus = "closed" } case TypeLock: if newStatus == "on" { newStatus = "unlocked" } else if newStatus == "off" { newStatus = "locked" } } if d.Status == newStatus { // 状态未变化,直接返回 cp := *d return &cp, nil } oldStatus := d.Status d.Status = newStatus // 根据设备类型设置关联属性 switch d.Type { case TypeLight: if newStatus == "off" { d.Brightness = 0 } else if d.Brightness == 0 { d.Brightness = 80 } case TypeCurtain: if newStatus == "closed" { d.Position = 0 } else { d.Position = 100 } } d.LastUpdated = now ds.addHistory(id, "status", oldStatus, newStatus) case "temperature": v, ok := toFloat64(value) if !ok { return nil, fmt.Errorf("temperature 需要数字值") } if d.Type != TypeAC { return nil, fmt.Errorf("设备 %s (类型 %s) 不支持温度调节", d.Name, d.Type) } oldVal := fmt.Sprintf("%.1f", d.Temperature) d.Temperature = v d.LastUpdated = now ds.addHistory(id, "temperature", oldVal, fmt.Sprintf("%.1f", v)) case "brightness": v, ok := toFloat64(value) if !ok { return nil, fmt.Errorf("brightness 需要数字值") } if d.Type != TypeLight { return nil, fmt.Errorf("设备 %s (类型 %s) 不支持亮度调节", d.Name, d.Type) } oldVal := fmt.Sprintf("%d", d.Brightness) d.Brightness = clampInt(int(v), 0, 100) d.LastUpdated = now ds.addHistory(id, "brightness", oldVal, fmt.Sprintf("%d", d.Brightness)) case "position": v, ok := toFloat64(value) if !ok { return nil, fmt.Errorf("position 需要数字值") } if d.Type != TypeCurtain { return nil, fmt.Errorf("设备 %s (类型 %s) 不支持位置调节", d.Name, d.Type) } oldVal := fmt.Sprintf("%d", d.Position) d.Position = clampInt(int(v), 0, 100) d.LastUpdated = now ds.addHistory(id, "position", oldVal, fmt.Sprintf("%d", d.Position)) case "mode": v, ok := value.(string) if !ok { return nil, fmt.Errorf("mode 需要字符串值") } if d.Type != TypeAC { return nil, fmt.Errorf("设备 %s (类型 %s) 不支持模式切换", d.Name, d.Type) } oldVal := d.Mode d.Mode = v d.LastUpdated = now ds.addHistory(id, "mode", oldVal, v) case "color": v, ok := value.(string) if !ok { return nil, fmt.Errorf("color 需要字符串值") } if d.Type != TypeLight { return nil, fmt.Errorf("设备 %s (类型 %s) 不支持颜色调节", d.Name, d.Type) } oldVal := d.Color d.Color = v d.LastUpdated = now ds.addHistory(id, "color", oldVal, v) default: return nil, fmt.Errorf("不支持的属性: %s", field) } cp := *d if h, ok := ds.history[id]; ok && len(h) > 0 { start := 0 if len(h) > 10 { start = len(h) - 10 } cp.History = make([]HistoryEntry, len(h)-start) copy(cp.History, h[start:]) } else { cp.History = []HistoryEntry{} } return &cp, nil } // normalizeStatus 标准化电源状态值 // 支持英文: "on"/"off"/"open"/"closed"/"locked"/"unlocked" // 支持中文: "开"/"关"/"打开"/"关闭"/"开启"/"解锁"/"锁定"/"上锁" // 支持布尔兼容: "true"/"false" // 返回空字符串表示无效值 func normalizeStatus(v string, dType DeviceType) string { switch strings.ToLower(strings.TrimSpace(v)) { case "on", "true", "开", "打开", "开启": return "on" case "off", "false", "关", "关闭": return "off" case "open": if dType == TypeCurtain { return "open" } return "on" case "closed": if dType == TypeCurtain { return "closed" } return "off" case "locked", "锁定", "上锁": return "locked" case "unlocked", "解锁": return "unlocked" default: return "" } } // toFloat64 将 interface{} 转换为 float64 func toFloat64(v interface{}) (float64, bool) { switch val := v.(type) { case float64: return val, true case float32: return float64(val), true case int: return float64(val), true case int64: return float64(val), true case json.Number: f, err := val.Float64() return f, err == nil default: return 0, false } } // clampInt 限制整数在 [min, max] 范围内 func clampInt(v, min, max int) int { if v < min { return min } if v > max { return max } return v } // SimulateFluctuation 模拟传感器随机波动 func (ds *DeviceStore) SimulateFluctuation() { ds.mu.Lock() defer ds.mu.Unlock() now := time.Now().UTC().Format(time.RFC3339) // 温度传感器: ±0.2°C if t, ok := ds.devices["sensor-temperature"]; ok { oldVal := t.Value t.Value += (rand.Float64()*0.4 - 0.2) t.Value = float64(int(t.Value*10)) / 10 // 保留一位小数 t.LastUpdated = now ds.addHistory("sensor-temperature", "value", fmt.Sprintf("%.1f", oldVal), fmt.Sprintf("%.1f", t.Value)) } // 湿度传感器: ±1% if h, ok := ds.devices["sensor-humidity"]; ok { oldVal := h.Value h.Value += float64(rand.Intn(3) - 1) // -1, 0, +1 if h.Value < 0 { h.Value = 0 } if h.Value > 100 { h.Value = 100 } h.LastUpdated = now ds.addHistory("sensor-humidity", "value", fmt.Sprintf("%.0f", oldVal), fmt.Sprintf("%.0f", h.Value)) } } func main() { port := getEnv("IOT_DEBUG_PORT", "8083") store := NewDeviceStore() // 启动传感器波动模拟(每30秒) go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { store.SimulateFluctuation() } }() mux := http.NewServeMux() // GET /api/v1/devices - 列出所有设备 mux.HandleFunc("/api/v1/devices", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } devices := store.GetAll() writeJSON(w, http.StatusOK, map[string]interface{}{ "devices": devices, "total": len(devices), }) }) // GET /api/v1/devices/{id} - 获取单个设备 // POST /api/v1/devices/{id}/toggle - 切换设备 mux.HandleFunc("/api/v1/devices/", func(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/v1/devices/") parts := strings.Split(path, "/") if len(parts) == 0 || parts[0] == "" { http.Error(w, "缺少设备ID", http.StatusBadRequest) return } deviceID := parts[0] // POST /api/v1/devices/{id}/toggle if len(parts) == 2 && parts[1] == "toggle" { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } device, err := store.Toggle(deviceID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "device": device, "action": "toggled", }) return } // POST /api/v1/devices/{id}/set - 设置设备属性 if len(parts) == 2 && parts[1] == "set" { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Field string `json:"field"` Value interface{} `json:"value"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请求格式错误"}) return } device, err := store.SetProperty(deviceID, req.Field, req.Value) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "device": device, "action": "set_" + req.Field, }) return } // GET /api/v1/devices/{id}/history if len(parts) == 2 && parts[1] == "history" { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } history := store.GetHistory(deviceID) writeJSON(w, http.StatusOK, map[string]interface{}{ "device_id": deviceID, "history": history, }) return } // GET /api/v1/devices/{id} if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } device := store.Get(deviceID) if device == nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": fmt.Sprintf("设备 %s 不存在", deviceID)}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "device": device, }) }) // 健康检查 mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{ "status": "ok", "service": "iot-debug-service", }) }) log.Printf("🔌 IoT 调试服务启动在端口 %s", port) log.Printf(" 模拟设备数: %d", len(store.GetAll())) if err := http.ListenAndServe(":"+port, mux); err != nil { log.Fatalf("服务启动失败: %v", err) } } func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }