diff --git a/api/api.go b/api/api.go index 2c6a3b5..8258e29 100644 --- a/api/api.go +++ b/api/api.go @@ -10,7 +10,6 @@ import ( "time" ) -// StartWebServer 启动Web服务器 func StartWebServer() { http.HandleFunc("/api/status", handleStatus) http.HandleFunc("/api/raw/latest", handleLatestRawData) @@ -18,7 +17,6 @@ func StartWebServer() { http.HandleFunc("/api/data", handleQueryData) http.HandleFunc("/api/latest", handleLatestData) - // 静态文件服务 http.Handle("/", http.FileServer(http.Dir("static"))) log.Println("Web服务器已启动,监听端口 10003") @@ -28,7 +26,6 @@ func StartWebServer() { } } -// handleStatus 处理连接状态请求 func handleStatus(w http.ResponseWriter, r *http.Request) { status := modbus.GetConnectionStatus() @@ -36,9 +33,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(status) } -// handleLatestRawData 获取最新原始数据 func handleLatestRawData(w http.ResponseWriter, r *http.Request) { - // 从数据库获取最新数据,而不是从modbus内存中获取 weatherData, err1 := db.GetLatestWeatherData() rainData, err2 := db.GetLatestRainGaugeData() @@ -57,7 +52,6 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) { result := map[string]interface{}{} - // 使用数据库中的时间戳,如果可用 if weatherData != nil && !weatherData.Timestamp.IsZero() { result["timestamp"] = weatherData.Timestamp.Format(time.RFC3339) result["formatted_time"] = weatherData.Timestamp.Format("2006-01-02 15:04:05") @@ -65,7 +59,6 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) { result["timestamp"] = rainData.Timestamp.Format(time.RFC3339) result["formatted_time"] = rainData.Timestamp.Format("2006-01-02 15:04:05") } else { - // 如果都没有时间戳,则使用当前时间作为 fallback result["timestamp"] = time.Now().Format(time.RFC3339) result["formatted_time"] = time.Now().Format("2006-01-02 15:04:05") } @@ -78,16 +71,15 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) { result["wind_direction_360"] = weatherData.WindDirection360 result["atm_pressure"] = weatherData.AtmPressure result["solar_radiation"] = weatherData.SolarRadiation - result["weather_rainfall"] = weatherData.Rainfall // 区分气象站的雨量 + result["weather_rainfall"] = weatherData.Rainfall } if rainData != nil { - result["total_rainfall"] = rainData.TotalRainfall // 使用 total_rainfall 表示累计雨量 + result["total_rainfall"] = rainData.TotalRainfall result["daily_rainfall"] = rainData.DailyRainfall result["instant_rainfall"] = rainData.InstantRainfall } - // 为了兼容旧代码,仍然提供一个 rainfall 字段,优先使用雨量计的数据 if rainData != nil { result["rainfall"] = rainData.TotalRainfall } else if weatherData != nil { @@ -98,7 +90,6 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(result) } -// handleTriggerQuery 触发设备查询(保留手动采集功能) func handleTriggerQuery(w http.ResponseWriter, r *http.Request) { err1 := modbus.QueryDevice(modbus.DeviceWeatherStation) err2 := modbus.QueryDevice(modbus.DeviceRainGauge) @@ -120,7 +111,6 @@ func handleTriggerQuery(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(result) } -// handleQueryData 查询历史数据 func handleQueryData(w http.ResponseWriter, r *http.Request) { startStr := r.URL.Query().Get("start") endStr := r.URL.Query().Get("end") @@ -156,7 +146,6 @@ func handleQueryData(w http.ResponseWriter, r *http.Request) { data, err := db.GetAggregatedData(start, end) if err != nil { log.Printf("查询聚合数据失败: %v", err) - // 返回空数组而不是错误,避免前端报错 w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]models.AggregatedData{}) return @@ -167,7 +156,6 @@ func handleQueryData(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(data) } -// handleLatestData 获取最新聚合数据 func handleLatestData(w http.ResponseWriter, r *http.Request) { startStr := r.URL.Query().Get("start") endStr := r.URL.Query().Get("end") @@ -200,11 +188,9 @@ func handleLatestData(w http.ResponseWriter, r *http.Request) { return } - // 只查数据库,不主动采集 data, err := db.GetAggregatedData(start, end) if err != nil { log.Printf("查询聚合数据失败: %v", err) - // 返回空数组而不是错误,避免前端报错 w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]models.AggregatedData{}) return diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..921ea05 --- /dev/null +++ b/config.yaml @@ -0,0 +1,39 @@ +# 雨量监测系统配置文件 + +# 数据库配置 +database: + host: 8.134.185.53 + port: 3306 + user: remote + password: root + dbname: rain_monitor + +# Web服务器配置 +web: + port: 10003 + +# TCP服务器配置 +tcp: + port: 10004 + +# 原始ModBus设备定时查询配置 +scheduler: + enabled: true + # 气象站查询间隔,支持Go时间格式,如: 15m, 30m, 1h + weather_station_interval: 15m + # 雨量计查询方式,固定为整点查询 + +# EM3395TY设备配置 +em3395ty: + # 涂鸦开发者账号配置 + client_id: nwmdye9c8ejymu9ge5kf + secret: 658733ea78624cd4b63bae6083cd3fae + base_url: https://openapi.tuyacn.com + # EM3395TY设备查询间隔,可选值:hourly(整点), 30min, 10min, 30sec, 10sec,或自定义格式如 5m, 1h30m + query_interval: hourly + # 设备ID列表 + devices: + - 6cbbf72843839b6157wfb2 + # 可以添加更多设备ID + + \ No newline at end of file diff --git a/db/db.go b/db/db.go index f40b4a9..3ef253c 100644 --- a/db/db.go +++ b/db/db.go @@ -63,6 +63,17 @@ func createTables() error { return fmt.Errorf("创建雨量计数据表失败: %v", err) } + // 创建EM3395TY相关表 + _, err = db.Exec(models.CreateEM3395TYDevicesTable) + if err != nil { + return fmt.Errorf("创建EM3395TY设备表失败: %v", err) + } + + _, err = db.Exec(models.CreateEM3395TYDataTable) + if err != nil { + return fmt.Errorf("创建EM3395TY数据表失败: %v", err) + } + return nil } @@ -84,8 +95,6 @@ func SaveWeatherData(data *models.WeatherData) (int64, error) { } func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) { - // 检查是否在短时间内已经插入过类似数据 - // 获取最近5分钟内的数据 fiveMinutesAgo := data.Timestamp.Add(-5 * time.Minute) rows, err := db.Query(` SELECT id, timestamp, total_rainfall @@ -96,11 +105,9 @@ func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) { if err != nil { log.Printf("查询最近雨量计数据失败: %v", err) - // 即使查询失败,我们仍然尝试插入新数据 } else { defer rows.Close() - // 检查是否有相似数据 for rows.Next() { var id int64 var ts time.Time @@ -111,13 +118,12 @@ func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) { continue } - // 如果时间非常接近(小于1分钟)且雨量数据相同或非常接近,则认为是重复数据 timeDiff := data.Timestamp.Sub(ts) rainfallDiff := math.Abs(data.TotalRainfall - rainfall) if timeDiff < time.Minute && rainfallDiff < 0.1 { log.Printf("检测到重复的雨量计数据,跳过插入。ID=%d, 时间=%v", id, ts) - return id, nil // 返回已存在的记录ID + return id, nil } } } @@ -256,14 +262,11 @@ func GetAggregatedData(start, end time.Time) ([]models.AggregatedData, error) { return nil, fmt.Errorf("扫描聚合数据失败: %v", err) } - // 设置格式化时间字符串 data.FormattedTime = timestampStr - // 尝试解析时间字符串为time.Time类型 timestamp, err := time.Parse("2006-01-02 15:04:05", timestampStr) if err != nil { log.Printf("解析时间字符串失败: %v, 原始字符串: %s", err, timestampStr) - // 即使解析失败,也继续处理,前端会使用FormattedTime } else { data.Timestamp = timestamp } @@ -274,3 +277,7 @@ func GetAggregatedData(start, end time.Time) ([]models.AggregatedData, error) { log.Printf("查询结果: 找到 %d 条记录", len(result)) return result, nil } + +func GetDB() *sql.DB { + return db +} diff --git a/em3395ty/db.go b/em3395ty/db.go new file mode 100644 index 0000000..3eddfb3 --- /dev/null +++ b/em3395ty/db.go @@ -0,0 +1,242 @@ +package em3395ty + +import ( + "database/sql" + "fmt" + "log" + "rain_monitor/models" + "time" +) + +// SaveDeviceInfo 保存设备信息到数据库 +func SaveDeviceInfo(db *sql.DB, info *models.EM3395TYDeviceInfo) (int64, error) { + result, err := db.Exec(models.InsertEM3395TYDeviceSQL, + info.Result.ID, info.Result.ActiveTime, info.Result.BindSpaceID, + info.Result.Category, info.Result.CreateTime, info.Result.CustomName, + info.Result.Icon, info.Result.IP, info.Result.IsOnline, + info.Result.Lat, info.Result.LocalKey, info.Result.Lon, + info.Result.Model, info.Result.Name, info.Result.ProductID, + info.Result.ProductName, info.Result.Sub, info.Result.TimeZone, + info.Result.UpdateTime, info.Result.UUID) + + if err != nil { + return 0, fmt.Errorf("保存EM3395TY设备信息失败: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("获取受影响行数失败: %v", err) + } + + return rowsAffected, nil +} + +// SaveDeviceStatus 保存设备状态到数据库 +func SaveDeviceStatus(db *sql.DB, deviceID string, status *models.EM3395TYDeviceStatus) (int64, error) { + now := time.Now() + + result, err := db.Exec(models.InsertEM3395TYDataSQL, + deviceID, now, + status.Result.TempCurrent, status.Result.HumidityValue, + status.Result.BatteryPercentage, status.Result.TempUnitConvert, + status.Result.WindspeedUnitConvert, status.Result.PressureUnitConvert, + status.Result.RainUnitConvert, status.Result.BrightUnitConvert, + status.Result.TempCurrentExternal, status.Result.HumidityOutdoor, + status.Result.TempCurrentExternal1, status.Result.HumidityOutdoor1, + status.Result.TempCurrentExternal2, status.Result.HumidityOutdoor2, + status.Result.TempCurrentExternal3, status.Result.HumidityOutdoor3, + status.Result.AtmosphericPressure, status.Result.PressureDrop, + status.Result.WindspeedAvg, status.Result.WindspeedGust, + status.Result.Rain1h, status.Result.Rain24h, status.Result.RainRate, + status.Result.UVIndex, status.Result.DewPointTemp, + status.Result.FeellikeTemp, status.Result.HeatIndex, + status.Result.WindchillIndex) + + if err != nil { + return 0, fmt.Errorf("保存EM3395TY设备状态失败: %v", err) + } + + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("获取插入ID失败: %v", err) + } + + return id, nil +} + +// CheckDeviceExists 检查设备是否存在于数据库中 +func CheckDeviceExists(db *sql.DB, deviceID string) (bool, error) { + var count int + err := db.QueryRow(models.QueryEM3395TYDeviceExistsSQL, deviceID).Scan(&count) + if err != nil { + return false, fmt.Errorf("检查设备是否存在失败: %v", err) + } + + return count > 0, nil +} + +// GetLatestDeviceData 获取最新的设备数据 +func GetLatestDeviceData(db *sql.DB, deviceID string) (*models.EM3395TYStatusData, error) { + row := db.QueryRow(models.QueryLatestEM3395TYDataSQL, deviceID) + if row == nil { + return nil, fmt.Errorf("没有找到设备数据") + } + + var ( + id int64 + timestamp time.Time + tempCurrent sql.NullInt64 + humidityValue sql.NullInt64 + batteryPercentage sql.NullInt64 + tempUnitConvert sql.NullString + windspeedUnitConvert sql.NullString + pressureUnitConvert sql.NullString + rainUnitConvert sql.NullString + brightUnitConvert sql.NullString + tempCurrentExternal sql.NullInt64 + humidityOutdoor sql.NullInt64 + tempCurrentExternal1 sql.NullInt64 + humidityOutdoor1 sql.NullInt64 + tempCurrentExternal2 sql.NullInt64 + humidityOutdoor2 sql.NullInt64 + tempCurrentExternal3 sql.NullInt64 + humidityOutdoor3 sql.NullInt64 + atmosphericPressure sql.NullInt64 + pressureDrop sql.NullInt64 + windspeedAvg sql.NullInt64 + windspeedGust sql.NullInt64 + rain1h sql.NullInt64 + rain24h sql.NullInt64 + rainRate sql.NullInt64 + uvIndex sql.NullInt64 + dewPointTemp sql.NullInt64 + feellikeTemp sql.NullInt64 + heatIndex sql.NullInt64 + windchillIndex sql.NullInt64 + ) + + err := row.Scan( + &id, &deviceID, ×tamp, + &tempCurrent, &humidityValue, &batteryPercentage, + &tempUnitConvert, &windspeedUnitConvert, &pressureUnitConvert, + &rainUnitConvert, &brightUnitConvert, &tempCurrentExternal, + &humidityOutdoor, &tempCurrentExternal1, &humidityOutdoor1, + &tempCurrentExternal2, &humidityOutdoor2, &tempCurrentExternal3, + &humidityOutdoor3, &atmosphericPressure, &pressureDrop, + &windspeedAvg, &windspeedGust, &rain1h, &rain24h, &rainRate, + &uvIndex, &dewPointTemp, &feellikeTemp, &heatIndex, &windchillIndex, + ) + + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("获取最新设备数据失败: %v", err) + } + + data := &models.EM3395TYStatusData{} + + if tempCurrent.Valid { + data.TempCurrent = int(tempCurrent.Int64) + } + if humidityValue.Valid { + data.HumidityValue = int(humidityValue.Int64) + } + if batteryPercentage.Valid { + data.BatteryPercentage = int(batteryPercentage.Int64) + } + if tempUnitConvert.Valid { + data.TempUnitConvert = tempUnitConvert.String + } + if windspeedUnitConvert.Valid { + data.WindspeedUnitConvert = windspeedUnitConvert.String + } + if pressureUnitConvert.Valid { + data.PressureUnitConvert = pressureUnitConvert.String + } + if rainUnitConvert.Valid { + data.RainUnitConvert = rainUnitConvert.String + } + if brightUnitConvert.Valid { + data.BrightUnitConvert = brightUnitConvert.String + } + if tempCurrentExternal.Valid { + data.TempCurrentExternal = int(tempCurrentExternal.Int64) + } + if humidityOutdoor.Valid { + data.HumidityOutdoor = int(humidityOutdoor.Int64) + } + if tempCurrentExternal1.Valid { + data.TempCurrentExternal1 = int(tempCurrentExternal1.Int64) + } + if humidityOutdoor1.Valid { + data.HumidityOutdoor1 = int(humidityOutdoor1.Int64) + } + if tempCurrentExternal2.Valid { + data.TempCurrentExternal2 = int(tempCurrentExternal2.Int64) + } + if humidityOutdoor2.Valid { + data.HumidityOutdoor2 = int(humidityOutdoor2.Int64) + } + if tempCurrentExternal3.Valid { + data.TempCurrentExternal3 = int(tempCurrentExternal3.Int64) + } + if humidityOutdoor3.Valid { + data.HumidityOutdoor3 = int(humidityOutdoor3.Int64) + } + if atmosphericPressure.Valid { + data.AtmosphericPressure = int(atmosphericPressure.Int64) + } + if pressureDrop.Valid { + data.PressureDrop = int(pressureDrop.Int64) + } + if windspeedAvg.Valid { + data.WindspeedAvg = int(windspeedAvg.Int64) + } + if windspeedGust.Valid { + data.WindspeedGust = int(windspeedGust.Int64) + } + if rain1h.Valid { + data.Rain1h = int(rain1h.Int64) + } + if rain24h.Valid { + data.Rain24h = int(rain24h.Int64) + } + if rainRate.Valid { + data.RainRate = int(rainRate.Int64) + } + if uvIndex.Valid { + data.UVIndex = int(uvIndex.Int64) + } + if dewPointTemp.Valid { + data.DewPointTemp = int(dewPointTemp.Int64) + } + if feellikeTemp.Valid { + data.FeellikeTemp = int(feellikeTemp.Int64) + } + if heatIndex.Valid { + data.HeatIndex = int(heatIndex.Int64) + } + if windchillIndex.Valid { + data.WindchillIndex = int(windchillIndex.Int64) + } + + return data, nil +} + +// CreateTables 创建EM3395TY相关的数据库表 +func CreateTables(db *sql.DB) error { + // 创建设备表 + _, err := db.Exec(models.CreateEM3395TYDevicesTable) + if err != nil { + return fmt.Errorf("创建EM3395TY设备表失败: %v", err) + } + + // 创建数据表 + _, err = db.Exec(models.CreateEM3395TYDataTable) + if err != nil { + return fmt.Errorf("创建EM3395TY数据表失败: %v", err) + } + + log.Println("EM3395TY数据库表创建成功") + return nil +} diff --git a/em3395ty/em3395ty.go b/em3395ty/em3395ty.go new file mode 100644 index 0000000..2c8b5f6 --- /dev/null +++ b/em3395ty/em3395ty.go @@ -0,0 +1,338 @@ +package em3395ty + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "rain_monitor/models" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +// 涂鸦API配置 +type TuyaConfig struct { + ClientID string + Secret string + BaseURL string +} + +var config TuyaConfig + +// InitConfig 初始化涂鸦API配置 +func InitConfig(clientID, secret, baseURL string) { + config = TuyaConfig{ + ClientID: clientID, + Secret: secret, + BaseURL: baseURL, + } + log.Printf("EM3395TY API配置已初始化,ClientID: %s, BaseURL: %s", clientID, baseURL) +} + +// 计算HMAC-SHA256签名 +func calculateSignature(secret, stringToSign string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(stringToSign)) + return hex.EncodeToString(h.Sum(nil)) +} + +// GetAccessToken 获取访问令牌 +func GetAccessToken() (string, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := "/v1.0/token?grant_type=1" + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := config.ClientID + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(config.Secret, strToHash) + signatureUpper := strings.ToUpper(signature) + + // 构建请求头 + req, err := http.NewRequest("GET", config.BaseURL+url, nil) + if err != nil { + return "", err + } + + req.Header.Set("client_id", config.ClientID) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // 解析响应 + var tokenResp models.EM3395TYTokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", err + } + + if !tokenResp.Success { + return "", fmt.Errorf("获取token失败: %s", string(body)) + } + + return tokenResp.Result.AccessToken, nil +} + +// GetDeviceInfo 获取设备信息 +func GetDeviceInfo(accessToken, deviceID string) (*models.EM3395TYDeviceInfo, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := fmt.Sprintf("/v2.0/cloud/thing/%s", deviceID) + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := config.ClientID + accessToken + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(config.Secret, strToHash) + signatureUpper := strings.ToUpper(signature) + + // 构建请求头 + req, err := http.NewRequest("GET", config.BaseURL+url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("client_id", config.ClientID) + req.Header.Set("access_token", accessToken) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 解析响应 + var deviceInfo models.EM3395TYDeviceInfo + if err := json.Unmarshal(body, &deviceInfo); err != nil { + return nil, err + } + + return &deviceInfo, nil +} + +// GetDeviceStatus 获取设备状态 +func GetDeviceStatus(accessToken, deviceID string) (*models.EM3395TYDeviceStatus, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := fmt.Sprintf("/v1.0/iot-03/devices/%s/status", deviceID) + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := config.ClientID + accessToken + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(config.Secret, strToHash) + signatureUpper := strings.ToUpper(signature) + + // 构建请求头 + req, err := http.NewRequest("GET", config.BaseURL+url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("client_id", config.ClientID) + req.Header.Set("access_token", accessToken) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 首先解析为临时结构 + var tempResponse struct { + Result []models.EM3395TYStatusItem `json:"result"` + Success bool `json:"success"` + T int64 `json:"t"` + TID string `json:"tid"` + } + + if err := json.Unmarshal(body, &tempResponse); err != nil { + return nil, err + } + + // 创建最终响应 + deviceStatus := &models.EM3395TYDeviceStatus{ + Success: tempResponse.Success, + T: tempResponse.T, + TID: tempResponse.TID, + } + + // 将临时结构中的数据转换为我们的结构化数据 + statusData := models.EM3395TYStatusData{} + + // 遍历状态项并填充结构体 + for _, item := range tempResponse.Result { + switch item.Code { + case "temp_current": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrent = int(val) + } + case "humidity_value": + if val, ok := item.Value.(float64); ok { + statusData.HumidityValue = int(val) + } + case "battery_percentage": + if val, ok := item.Value.(float64); ok { + statusData.BatteryPercentage = int(val) + } + case "temp_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.TempUnitConvert = val + } + case "windspeed_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.WindspeedUnitConvert = val + } + case "pressure_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.PressureUnitConvert = val + } + case "rain_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.RainUnitConvert = val + } + case "bright_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.BrightUnitConvert = val + } + case "temp_current_external": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal = int(val) + } + case "humidity_outdoor": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor = int(val) + } + case "temp_current_external_1": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal1 = int(val) + } + case "humidity_outdoor_1": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor1 = int(val) + } + case "temp_current_external_2": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal2 = int(val) + } + case "humidity_outdoor_2": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor2 = int(val) + } + case "temp_current_external_3": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal3 = int(val) + } + case "humidity_outdoor_3": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor3 = int(val) + } + case "atmospheric_pressture": + if val, ok := item.Value.(float64); ok { + statusData.AtmosphericPressure = int(val) + } + case "pressure_drop": + if val, ok := item.Value.(float64); ok { + statusData.PressureDrop = int(val) + } + case "windspeed_avg": + if val, ok := item.Value.(float64); ok { + statusData.WindspeedAvg = int(val) + } + case "windspeed_gust": + if val, ok := item.Value.(float64); ok { + statusData.WindspeedGust = int(val) + } + case "rain_1h": + if val, ok := item.Value.(float64); ok { + statusData.Rain1h = int(val) + } + case "rain_24h": + if val, ok := item.Value.(float64); ok { + statusData.Rain24h = int(val) + } + case "rain_rate": + if val, ok := item.Value.(float64); ok { + statusData.RainRate = int(val) + } + case "uv_index": + if val, ok := item.Value.(float64); ok { + statusData.UVIndex = int(val) + } + case "dew_point_temp": + if val, ok := item.Value.(float64); ok { + statusData.DewPointTemp = int(val) + } + case "feellike_temp": + if val, ok := item.Value.(float64); ok { + statusData.FeellikeTemp = int(val) + } + case "heat_index": + if val, ok := item.Value.(float64); ok { + statusData.HeatIndex = int(val) + } + case "windchill_index": + if val, ok := item.Value.(float64); ok { + statusData.WindchillIndex = int(val) + } + } + } + + deviceStatus.Result = statusData + return deviceStatus, nil +} diff --git a/em3395ty/scheduler.go b/em3395ty/scheduler.go new file mode 100644 index 0000000..1b06040 --- /dev/null +++ b/em3395ty/scheduler.go @@ -0,0 +1,221 @@ +package em3395ty + +import ( + "database/sql" + "log" + "time" +) + +// QueryInterval 查询间隔类型 +type QueryInterval string + +const ( + // 预定义的查询间隔 + IntervalHourly QueryInterval = "hourly" // 整点查询 + IntervalThirtyMin QueryInterval = "30min" // 每30分钟 + IntervalTenMin QueryInterval = "10min" // 每10分钟 + IntervalThirtySec QueryInterval = "30sec" // 每30秒 + IntervalTenSec QueryInterval = "10sec" // 每10秒 + IntervalCustom QueryInterval = "custom" // 自定义间隔 +) + +type EM3395TYScheduler struct { + db *sql.DB + deviceIDs []string + accessToken string + tokenExpireAt time.Time + ticker *time.Ticker + stopChan chan struct{} + interval QueryInterval + customInterval time.Duration // 自定义间隔时间 +} + +// NewScheduler 创建一个新的EM3395TY调度器,默认整点查询 +func NewScheduler(db *sql.DB, deviceIDs []string) *EM3395TYScheduler { + return &EM3395TYScheduler{ + db: db, + deviceIDs: deviceIDs, + stopChan: make(chan struct{}), + interval: IntervalHourly, + } +} + +// SetQueryInterval 设置查询间隔 +func (s *EM3395TYScheduler) SetQueryInterval(interval QueryInterval) { + s.interval = interval + log.Printf("EM3395TY设备查询间隔已设置为: %s", interval) +} + +// SetCustomInterval 设置自定义查询间隔 +func (s *EM3395TYScheduler) SetCustomInterval(duration time.Duration) { + s.interval = IntervalCustom + s.customInterval = duration + log.Printf("EM3395TY设备查询间隔已设置为自定义: %s", duration) +} + +// Start 启动定时查询任务 +func (s *EM3395TYScheduler) Start() { + log.Println("启动EM3395TY设备定时查询任务") + + // 初始化设备信息 + s.initDevices() + + // 启动查询任务 + go s.startQueryTask() +} + +// Stop 停止定时查询任务 +func (s *EM3395TYScheduler) Stop() { + if s.ticker != nil { + s.ticker.Stop() + } + close(s.stopChan) + log.Println("EM3395TY设备定时查询任务已停止") +} + +// initDevices 初始化设备信息 +func (s *EM3395TYScheduler) initDevices() { + // 获取访问令牌 + token, err := GetAccessToken() + if err != nil { + log.Printf("获取访问令牌失败: %v", err) + return + } + s.accessToken = token + s.tokenExpireAt = time.Now().Add(1 * time.Hour) // 假设令牌有效期为1小时 + + // 检查每个设备是否存在于数据库中,如果不存在则查询设备信息并保存 + for _, deviceID := range s.deviceIDs { + exists, err := CheckDeviceExists(s.db, deviceID) + if err != nil { + log.Printf("检查设备 %s 是否存在失败: %v", deviceID, err) + continue + } + + if !exists { + log.Printf("设备 %s 不存在于数据库中,正在获取设备信息", deviceID) + deviceInfo, err := GetDeviceInfo(s.accessToken, deviceID) + if err != nil { + log.Printf("获取设备 %s 信息失败: %v", deviceID, err) + continue + } + + _, err = SaveDeviceInfo(s.db, deviceInfo) + if err != nil { + log.Printf("保存设备 %s 信息失败: %v", deviceID, err) + continue + } + log.Printf("设备 %s 信息已保存到数据库", deviceID) + } else { + log.Printf("设备 %s 已存在于数据库中", deviceID) + } + } +} + +// getIntervalDuration 获取查询间隔时间 +func (s *EM3395TYScheduler) getIntervalDuration() time.Duration { + switch s.interval { + case IntervalHourly: + return 1 * time.Hour + case IntervalThirtyMin: + return 30 * time.Minute + case IntervalTenMin: + return 10 * time.Minute + case IntervalThirtySec: + return 30 * time.Second + case IntervalTenSec: + return 10 * time.Second + case IntervalCustom: + return s.customInterval + default: + return 1 * time.Hour + } +} + +// startQueryTask 启动查询任务 +func (s *EM3395TYScheduler) startQueryTask() { + interval := s.getIntervalDuration() + + if s.interval == IntervalHourly { + // 对于整点查询,计算距离下一个整点的时间 + now := time.Now() + nextHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 0, 0, 0, now.Location()) + duration := nextHour.Sub(now) + + log.Printf("EM3395TY设备第一次查询将在 %s 后进行 (整点: %s)", + duration.String(), nextHour.Format("15:04:05")) + + // 等待到下一个整点 + timer := time.NewTimer(duration) + select { + case <-timer.C: + s.queryAllDevices() + case <-s.stopChan: + timer.Stop() + return + } + + s.ticker = time.NewTicker(interval) + } else { + // 对于非整点查询,立即执行一次查询,然后按照间隔定时执行 + log.Printf("EM3395TY设备查询任务已启动,每 %s 查询一次", interval) + s.queryAllDevices() + s.ticker = time.NewTicker(interval) + } + + // 循环执行查询任务 + for { + select { + case <-s.ticker.C: + s.queryAllDevices() + case <-s.stopChan: + return + } + } +} + +// queryAllDevices 查询所有设备的状态 +func (s *EM3395TYScheduler) queryAllDevices() { + log.Println("执行EM3395TY设备查询任务") + + // 检查令牌是否过期 + if time.Now().After(s.tokenExpireAt) { + log.Println("访问令牌已过期,正在重新获取") + token, err := GetAccessToken() + if err != nil { + log.Printf("重新获取访问令牌失败: %v", err) + return + } + s.accessToken = token + s.tokenExpireAt = time.Now().Add(1 * time.Hour) + } + + // 查询每个设备的状态 + for _, deviceID := range s.deviceIDs { + log.Printf("正在查询设备 %s 的状态", deviceID) + + deviceStatus, err := GetDeviceStatus(s.accessToken, deviceID) + if err != nil { + log.Printf("获取设备 %s 状态失败: %v", deviceID, err) + continue + } + + // 保存设备状态到数据库 + id, err := SaveDeviceStatus(s.db, deviceID, deviceStatus) + if err != nil { + log.Printf("保存设备 %s 状态失败: %v", deviceID, err) + continue + } + + log.Printf("设备 %s 状态已保存,ID=%d", deviceID, id) + + // 打印部分关键数据 + log.Printf("设备 %s 室内温度: %.1f℃, 室内湿度: %d%%, 室外温度: %.1f℃, 室外湿度: %d%%, 一小时降雨量: %.1fmm", + deviceID, + float64(deviceStatus.Result.TempCurrent)/10.0, + deviceStatus.Result.HumidityValue, + float64(deviceStatus.Result.TempCurrentExternal)/10.0, + deviceStatus.Result.HumidityOutdoor, + float64(deviceStatus.Result.Rain1h)/10.0) + } +} diff --git a/go.mod b/go.mod index 17e83a2..dea900d 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module rain_monitor go 1.24 -require github.com/go-sql-driver/mysql v1.9.3 +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/google/uuid v1.6.0 + gopkg.in/yaml.v2 v2.4.0 +) require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4bcdcfa..7e89ba5 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go index f04864f..26e5f94 100644 --- a/main.go +++ b/main.go @@ -2,51 +2,157 @@ package main import ( "flag" + "io/ioutil" "log" "rain_monitor/api" "rain_monitor/db" + "rain_monitor/em3395ty" "rain_monitor/modbus" "rain_monitor/scheduler" + "time" + + "gopkg.in/yaml.v2" ) +// 配置结构体 +type Config struct { + Database struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + DBName string `yaml:"dbname"` + } `yaml:"database"` + Web struct { + Port int `yaml:"port"` + } `yaml:"web"` + TCP struct { + Port int `yaml:"port"` + } `yaml:"tcp"` + Scheduler struct { + Enabled bool `yaml:"enabled"` + WeatherStationInterval string `yaml:"weather_station_interval"` + } `yaml:"scheduler"` + EM3395TY struct { + ClientID string `yaml:"client_id"` + Secret string `yaml:"secret"` + BaseURL string `yaml:"base_url"` + Devices []string `yaml:"devices"` + QueryInterval string `yaml:"query_interval"` // 查询间隔 + } `yaml:"em3395ty"` +} + var ( - dbHost = flag.String("db-host", "8.134.185.53", "数据库主机地址") - dbPort = flag.Int("db-port", 3306, "数据库端口") - dbUser = flag.String("db-user", "remote", "数据库用户名") - dbPassword = flag.String("db-password", "root", "数据库密码") - dbName = flag.String("db-name", "rain_monitor", "数据库名称") + configFile = flag.String("config", "config.yaml", "配置文件路径") + config Config ) func main() { - // 解析命令行参数 flag.Parse() - // 初始化数据库连接 + // 加载配置文件 + err := loadConfig(*configFile) + if err != nil { + log.Fatalf("加载配置文件失败: %v", err) + } + dbConfig := db.DBConfig{ - Host: *dbHost, - Port: *dbPort, - User: *dbUser, - Password: *dbPassword, - DBName: *dbName, + Host: config.Database.Host, + Port: config.Database.Port, + User: config.Database.User, + Password: config.Database.Password, + DBName: config.Database.DBName, } log.Println("正在连接数据库...") - err := db.InitDB(dbConfig) + err = db.InitDB(dbConfig) if err != nil { log.Fatalf("数据库初始化失败: %v", err) } defer db.CloseDB() log.Println("数据库连接成功") - // 启动TCP服务器 + // 初始化EM3395TY配置 + em3395ty.InitConfig(config.EM3395TY.ClientID, config.EM3395TY.Secret, config.EM3395TY.BaseURL) + log.Println("正在启动TCP服务器...") go modbus.StartTCPServer() - // 启动定时任务调度器 - log.Println("正在启动定时任务调度器...") - go scheduler.StartScheduler() + // 配置并启动ModBus设备调度器 + if config.Scheduler.Enabled { + log.Println("正在启动ModBus设备定时任务调度器...") + + // 解析气象站查询间隔 + weatherStationInterval := 15 * time.Minute // 默认值 + if config.Scheduler.WeatherStationInterval != "" { + if duration, err := time.ParseDuration(config.Scheduler.WeatherStationInterval); err == nil { + weatherStationInterval = duration + } else { + log.Printf("无法解析气象站查询间隔 '%s',使用默认值 15m", config.Scheduler.WeatherStationInterval) + } + } + + // 设置调度器配置 + schedulerConfig := scheduler.TaskConfig{ + WeatherStationInterval: weatherStationInterval, + RainGaugeInterval: time.Hour, // 雨量计固定为整点查询 + Enabled: true, + } + scheduler.SetTaskConfig(schedulerConfig) + + // 启动调度器 + go scheduler.StartScheduler() + } else { + log.Println("ModBus设备定时任务调度器已禁用") + } + + // 启动EM3395TY设备调度器 + if len(config.EM3395TY.Devices) > 0 { + log.Printf("检测到 %d 个EM3395TY设备,正在启动调度器...", len(config.EM3395TY.Devices)) + em3395tyScheduler := em3395ty.NewScheduler(db.GetDB(), config.EM3395TY.Devices) + + // 设置查询间隔 + if config.EM3395TY.QueryInterval != "" { + switch config.EM3395TY.QueryInterval { + case "hourly": + em3395tyScheduler.SetQueryInterval(em3395ty.IntervalHourly) + case "30min": + em3395tyScheduler.SetQueryInterval(em3395ty.IntervalThirtyMin) + case "10min": + em3395tyScheduler.SetQueryInterval(em3395ty.IntervalTenMin) + case "30sec": + em3395tyScheduler.SetQueryInterval(em3395ty.IntervalThirtySec) + case "10sec": + em3395tyScheduler.SetQueryInterval(em3395ty.IntervalTenSec) + default: + // 尝试解析为自定义时间间隔 + if duration, err := time.ParseDuration(config.EM3395TY.QueryInterval); err == nil { + em3395tyScheduler.SetCustomInterval(duration) + } else { + log.Printf("无法解析查询间隔 '%s',使用默认值", config.EM3395TY.QueryInterval) + } + } + } + + go em3395tyScheduler.Start() + } - // 启动Web服务器 log.Println("正在启动Web服务器...") - api.StartWebServer() // 这个函数会阻塞主线程 + api.StartWebServer() +} + +// 加载配置文件 +func loadConfig(path string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + err = yaml.Unmarshal(data, &config) + if err != nil { + return err + } + + log.Printf("配置文件加载成功: %s", path) + return nil } diff --git a/modbus/modbus.go b/modbus/modbus.go index 83ed459..16e6ce4 100644 --- a/modbus/modbus.go +++ b/modbus/modbus.go @@ -12,8 +12,8 @@ import ( ) const ( - DeviceWeatherStation = 1 // 气象站 - DeviceRainGauge = 2 // 雨量计 + DeviceWeatherStation = 1 + DeviceRainGauge = 2 ) const ( @@ -26,8 +26,8 @@ const ( ) var ( - WeatherStationCmd = []byte{0x01, 0x03, 0x01, 0xf4, 0x00, 0x10, 0x04, 0x08} // 气象站查询命令 - RainGaugeCmd = []byte{0x02, 0x03, 0x00, 0x00, 0x00, 0x0a, 0xc5, 0xfe} // 雨量计查询命令 + WeatherStationCmd = []byte{0x01, 0x03, 0x01, 0xf4, 0x00, 0x10, 0x04, 0x08} + RainGaugeCmd = []byte{0x02, 0x03, 0x00, 0x00, 0x00, 0x0a, 0xc5, 0xfe} ) var ( @@ -42,7 +42,6 @@ func init() { connectedClients = make(map[string]net.Conn) } -// StartTCPServer 启动TCP服务器 func StartTCPServer() { listener, err := net.Listen("tcp", ":10004") if err != nil { @@ -70,7 +69,6 @@ func StartTCPServer() { } } -// HandleConnection 处理客户端连接 func handleConnection(conn net.Conn) { defer func() { conn.Close() @@ -101,7 +99,6 @@ func handleConnection(conn net.Conn) { } } -// ProcessModbusData 解析ModBus数据 func processModbusData(data []byte) { if len(data) < 3 { log.Println("数据长度不足") @@ -126,7 +123,6 @@ func processModbusData(data []byte) { } } -// ProcessWeatherStationData 处理气象站数据 func processWeatherStationData(data []byte) { if len(data) < 35 { log.Println("气象站数据长度不足") @@ -171,7 +167,6 @@ func processWeatherStationData(data []byte) { weather.Temperature, weather.Humidity, weather.WindSpeed, weather.WindDirection360, weather.AtmPressure, weather.PM25, weather.PM10, weather.Rainfall, weather.LightIntensity) - // 保存到数据库 _, err := db.SaveWeatherData(weather) if err != nil { log.Printf("保存气象站数据失败: %v", err) @@ -181,7 +176,6 @@ func processWeatherStationData(data []byte) { } } -// ProcessRainGaugeData 处理雨量计数据 func processRainGaugeData(data []byte) { if len(data) < 25 { log.Println("雨量计数据长度不足") @@ -220,7 +214,6 @@ func processRainGaugeData(data []byte) { rainData.DailyRainfall, rainData.InstantRainfall, rainData.TotalRainfall, rainData.YesterdayRainfall, rainData.HourlyRainfall, rainData.LastHourRainfall) - // 保存到数据库 _, err := db.SaveRainGaugeData(rainData) if err != nil { log.Printf("保存雨量计数据失败: %v", err) @@ -230,7 +223,6 @@ func processRainGaugeData(data []byte) { } } -// QueryDevice 向设备发送查询命令 func QueryDevice(deviceType int) error { var cmd []byte @@ -262,7 +254,6 @@ func QueryDevice(deviceType int) error { return nil } -// GetConnectionStatus 获取连接状态 func GetConnectionStatus() models.ConnectionStatus { clientsMutex.RLock() defer clientsMutex.RUnlock() @@ -284,7 +275,6 @@ func GetConnectionStatus() models.ConnectionStatus { return status } -// GetLatestWeatherData 获取最新气象站数据 func GetLatestWeatherData() *models.WeatherData { dataMutex.RLock() defer dataMutex.RUnlock() @@ -293,12 +283,10 @@ func GetLatestWeatherData() *models.WeatherData { return nil } - // 返回一个副本 data := *latestWeatherData return &data } -// GetLatestRainData 获取最新雨量计数据 func GetLatestRainData() *models.RainGaugeData { dataMutex.RLock() defer dataMutex.RUnlock() @@ -307,7 +295,6 @@ func GetLatestRainData() *models.RainGaugeData { return nil } - // 返回一个副本 data := *latestRainData return &data } diff --git a/models/em3395ty.go b/models/em3395ty.go new file mode 100644 index 0000000..aa4b918 --- /dev/null +++ b/models/em3395ty.go @@ -0,0 +1,225 @@ +package models + +// EM3395TYDeviceInfo 表示EM3395TY设备基本信息的响应 +type EM3395TYDeviceInfo struct { + Result struct { + ActiveTime int64 `json:"active_time"` + BindSpaceID string `json:"bind_space_id"` + Category string `json:"category"` + CreateTime int64 `json:"create_time"` + CustomName string `json:"custom_name"` + Icon string `json:"icon"` + ID string `json:"id"` + IP string `json:"ip"` + IsOnline bool `json:"is_online"` + Lat string `json:"lat"` + LocalKey string `json:"local_key"` + Lon string `json:"lon"` + Model string `json:"model"` + Name string `json:"name"` + ProductID string `json:"product_id"` + ProductName string `json:"product_name"` + Sub bool `json:"sub"` + TimeZone string `json:"time_zone"` + UpdateTime int64 `json:"update_time"` + UUID string `json:"uuid"` + } `json:"result"` + Success bool `json:"success"` + T int64 `json:"t"` + TID string `json:"tid"` +} + +// EM3395TYDeviceStatus 表示EM3395TY设备状态的响应 +type EM3395TYDeviceStatus struct { + Result EM3395TYStatusData `json:"result"` + Success bool `json:"success"` + T int64 `json:"t"` + TID string `json:"tid"` +} + +// EM3395TYStatusData 表示EM3395TY设备状态数据 +type EM3395TYStatusData struct { + TempCurrent int `json:"temp_current"` + HumidityValue int `json:"humidity_value"` + BatteryPercentage int `json:"battery_percentage"` + TempUnitConvert string `json:"temp_unit_convert"` + WindspeedUnitConvert string `json:"windspeed_unit_convert"` + PressureUnitConvert string `json:"pressure_unit_convert"` + RainUnitConvert string `json:"rain_unit_convert"` + BrightUnitConvert string `json:"bright_unit_convert"` + TempCurrentExternal int `json:"temp_current_external"` + HumidityOutdoor int `json:"humidity_outdoor"` + TempCurrentExternal1 int `json:"temp_current_external_1"` + HumidityOutdoor1 int `json:"humidity_outdoor_1"` + TempCurrentExternal2 int `json:"temp_current_external_2"` + HumidityOutdoor2 int `json:"humidity_outdoor_2"` + TempCurrentExternal3 int `json:"temp_current_external_3"` + HumidityOutdoor3 int `json:"humidity_outdoor_3"` + AtmosphericPressure int `json:"atmospheric_pressture"` + PressureDrop int `json:"pressure_drop"` + WindspeedAvg int `json:"windspeed_avg"` + WindspeedGust int `json:"windspeed_gust"` + Rain1h int `json:"rain_1h"` + Rain24h int `json:"rain_24h"` + RainRate int `json:"rain_rate"` + UVIndex int `json:"uv_index"` + DewPointTemp int `json:"dew_point_temp"` + FeellikeTemp int `json:"feellike_temp"` + HeatIndex int `json:"heat_index"` + WindchillIndex int `json:"windchill_index"` +} + +// EM3395TYStatusItem 表示EM3395TY设备状态项 +type EM3395TYStatusItem struct { + Code string `json:"code"` + Value interface{} `json:"value"` +} + +// EM3395TYTokenResponse 表示获取访问令牌的响应 +type EM3395TYTokenResponse struct { + Result struct { + AccessToken string `json:"access_token"` + ExpireTime int `json:"expire_time"` + RefreshToken string `json:"refresh_token"` + UID string `json:"uid"` + } `json:"result"` + Success bool `json:"success"` + T int64 `json:"t"` + TID string `json:"tid"` +} + +// 创建EM3395TY设备表的SQL语句 +const CreateEM3395TYDevicesTable = ` +CREATE TABLE IF NOT EXISTS em3395ty_devices ( + id VARCHAR(50) PRIMARY KEY, + active_time BIGINT NOT NULL, + bind_space_id VARCHAR(50) NOT NULL, + category VARCHAR(20) NOT NULL, + create_time BIGINT NOT NULL, + custom_name VARCHAR(100), + icon VARCHAR(255), + ip VARCHAR(50) NOT NULL, + is_online BOOLEAN NOT NULL, + lat VARCHAR(20), + local_key VARCHAR(50), + lon VARCHAR(20), + model VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + product_id VARCHAR(50) NOT NULL, + product_name VARCHAR(100) NOT NULL, + sub BOOLEAN NOT NULL, + time_zone VARCHAR(10) NOT NULL, + update_time BIGINT NOT NULL, + uuid VARCHAR(50) NOT NULL, + last_query_time DATETIME, + INDEX idx_ip (ip), + INDEX idx_product_id (product_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +` + +// 创建EM3395TY数据表的SQL语句 +const CreateEM3395TYDataTable = ` +CREATE TABLE IF NOT EXISTS em3395ty_data ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + device_id VARCHAR(50) NOT NULL, + timestamp DATETIME NOT NULL, + temp_current INT, + humidity_value INT, + battery_percentage INT, + temp_unit_convert VARCHAR(10), + windspeed_unit_convert VARCHAR(10), + pressure_unit_convert VARCHAR(10), + rain_unit_convert VARCHAR(10), + bright_unit_convert VARCHAR(10), + temp_current_external INT, + humidity_outdoor INT, + temp_current_external_1 INT, + humidity_outdoor_1 INT, + temp_current_external_2 INT, + humidity_outdoor_2 INT, + temp_current_external_3 INT, + humidity_outdoor_3 INT, + atmospheric_pressture INT, + pressure_drop INT, + windspeed_avg INT, + windspeed_gust INT, + rain_1h INT, + rain_24h INT, + rain_rate INT, + uv_index INT, + dew_point_temp INT, + feellike_temp INT, + heat_index INT, + windchill_index INT, + INDEX idx_device_id (device_id), + INDEX idx_timestamp (timestamp), + FOREIGN KEY (device_id) REFERENCES em3395ty_devices(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +` + +// 插入EM3395TY设备信息的SQL语句 +const InsertEM3395TYDeviceSQL = ` +INSERT INTO em3395ty_devices ( + id, active_time, bind_space_id, category, create_time, custom_name, + icon, ip, is_online, lat, local_key, lon, model, name, product_id, + product_name, sub, time_zone, update_time, uuid, last_query_time +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW() +) ON DUPLICATE KEY UPDATE + active_time = VALUES(active_time), + bind_space_id = VALUES(bind_space_id), + category = VALUES(category), + create_time = VALUES(create_time), + custom_name = VALUES(custom_name), + icon = VALUES(icon), + ip = VALUES(ip), + is_online = VALUES(is_online), + lat = VALUES(lat), + local_key = VALUES(local_key), + lon = VALUES(lon), + model = VALUES(model), + name = VALUES(name), + product_id = VALUES(product_id), + product_name = VALUES(product_name), + sub = VALUES(sub), + time_zone = VALUES(time_zone), + update_time = VALUES(update_time), + uuid = VALUES(uuid), + last_query_time = NOW() +` + +// 插入EM3395TY设备数据的SQL语句 +const InsertEM3395TYDataSQL = ` +INSERT INTO em3395ty_data ( + device_id, timestamp, temp_current, humidity_value, battery_percentage, + temp_unit_convert, windspeed_unit_convert, pressure_unit_convert, + rain_unit_convert, bright_unit_convert, temp_current_external, + humidity_outdoor, temp_current_external_1, humidity_outdoor_1, + temp_current_external_2, humidity_outdoor_2, temp_current_external_3, + humidity_outdoor_3, atmospheric_pressture, pressure_drop, windspeed_avg, + windspeed_gust, rain_1h, rain_24h, rain_rate, uv_index, dew_point_temp, + feellike_temp, heat_index, windchill_index +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +) +` + +// 查询最新的EM3395TY设备数据的SQL语句 +const QueryLatestEM3395TYDataSQL = ` +SELECT * FROM em3395ty_data +WHERE device_id = ? +ORDER BY timestamp DESC +LIMIT 1 +` + +// 查询指定时间范围内的EM3395TY设备数据的SQL语句 +const QueryEM3395TYDataByTimeRangeSQL = ` +SELECT * FROM em3395ty_data +WHERE device_id = ? AND timestamp BETWEEN ? AND ? +ORDER BY timestamp +` + +// 查询设备是否存在的SQL语句 +const QueryEM3395TYDeviceExistsSQL = ` +SELECT COUNT(*) FROM em3395ty_devices WHERE id = ? +` diff --git a/models/models.go b/models/models.go index 537f927..ed4213b 100644 --- a/models/models.go +++ b/models/models.go @@ -4,7 +4,6 @@ import ( "time" ) -// WeatherData 气象站数据结构 type WeatherData struct { ID int64 `json:"id" db:"id"` Timestamp time.Time `json:"timestamp" db:"timestamp"` @@ -26,7 +25,6 @@ type WeatherData struct { SolarRadiation int `json:"solar_radiation" db:"solar_radiation"` } -// RainGaugeData 雨量计数据结构 type RainGaugeData struct { ID int64 `json:"id" db:"id"` Timestamp time.Time `json:"timestamp" db:"timestamp"` @@ -42,7 +40,6 @@ type RainGaugeData struct { Min24hRainfallPeriod int `json:"min_24h_rainfall_period" db:"min_24h_rainfall_period"` } -// AggregatedData 聚合数据结构,用于前端展示 type AggregatedData struct { Timestamp time.Time `json:"timestamp" db:"timestamp"` FormattedTime string `json:"formatted_time,omitempty"` @@ -54,7 +51,6 @@ type AggregatedData struct { SolarRadiation float64 `json:"solar_radiation" db:"solar_radiation"` } -// ConnectionStatus 连接状态 type ConnectionStatus struct { Connected bool `json:"connected"` IP string `json:"ip,omitempty"` @@ -62,7 +58,6 @@ type ConnectionStatus struct { Count int `json:"count,omitempty"` } -// CreateWeatherDataTable 创建气象站数据表SQL const CreateWeatherDataTable = ` CREATE TABLE IF NOT EXISTS weather_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, @@ -87,7 +82,6 @@ CREATE TABLE IF NOT EXISTS weather_data ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ` -// CreateRainGaugeDataTable 创建雨量计数据表SQL const CreateRainGaugeDataTable = ` CREATE TABLE IF NOT EXISTS rain_gauge_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, @@ -106,7 +100,6 @@ CREATE TABLE IF NOT EXISTS rain_gauge_data ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ` -// InsertWeatherDataSQL 插入气象站数据SQL const InsertWeatherDataSQL = ` INSERT INTO weather_data ( timestamp, wind_speed, wind_force, wind_direction_8, wind_direction_360, @@ -117,7 +110,6 @@ INSERT INTO weather_data ( ) ` -// InsertRainGaugeDataSQL 插入雨量计数据SQL const InsertRainGaugeDataSQL = ` INSERT INTO rain_gauge_data ( timestamp, daily_rainfall, instant_rainfall, yesterday_rainfall, @@ -128,35 +120,30 @@ INSERT INTO rain_gauge_data ( ) ` -// QueryWeatherDataByTimeRangeSQL 按时间范围查询气象站数据SQL const QueryWeatherDataByTimeRangeSQL = ` SELECT * FROM weather_data WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp DESC ` -// QueryRainGaugeDataByTimeRangeSQL 按时间范围查询雨量计数据SQL const QueryRainGaugeDataByTimeRangeSQL = ` SELECT * FROM rain_gauge_data WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp DESC ` -// QueryLatestWeatherDataSQL 查询最新气象站数据SQL const QueryLatestWeatherDataSQL = ` SELECT * FROM weather_data ORDER BY timestamp DESC LIMIT 1 ` -// QueryLatestRainGaugeDataSQL 查询最新雨量计数据SQL const QueryLatestRainGaugeDataSQL = ` SELECT * FROM rain_gauge_data ORDER BY timestamp DESC LIMIT 1 ` -// QueryAggregatedDataSQL 查询聚合数据SQL (小时级别) const QueryAggregatedDataSQL = ` SELECT w.time_hour as timestamp, diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 18245bd..8e9086c 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -7,11 +7,10 @@ import ( "time" ) -// 任务配置 type TaskConfig struct { - WeatherStationInterval time.Duration // 气象站查询间隔 - RainGaugeInterval time.Duration // 雨量计查询间隔 - Enabled bool // 是否启用定时查询 + WeatherStationInterval time.Duration + RainGaugeInterval time.Duration + Enabled bool } var ( @@ -20,17 +19,15 @@ var ( stopChan chan struct{} ) -// 初始化默认配置 func init() { config = TaskConfig{ - WeatherStationInterval: 15 * time.Minute, // 默认15分钟查询一次气象站 - RainGaugeInterval: time.Hour, // 默认每小时查询一次雨量计 - Enabled: true, // 默认启用 + WeatherStationInterval: 15 * time.Minute, + RainGaugeInterval: time.Hour, + Enabled: true, } stopChan = make(chan struct{}) } -// StartScheduler 启动定时任务调度器 func StartScheduler() { if !config.Enabled { log.Println("定时查询任务已禁用") @@ -40,7 +37,6 @@ func StartScheduler() { log.Printf("启动定时查询任务,气象站间隔: %v, 雨量计整点查询", config.WeatherStationInterval) - // 启动气象站查询任务 weatherTick = time.NewTicker(config.WeatherStationInterval) go func() { for { @@ -53,30 +49,25 @@ func StartScheduler() { } }() - // 启动雨量计整点查询任务 go scheduleHourlyRainGaugeQuery() } -// 计算到下一个整点的等待时间 func durationUntilNextHour() time.Duration { now := time.Now() nextHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 0, 0, 0, now.Location()) return nextHour.Sub(now) } -// 整点查询雨量计任务 func scheduleHourlyRainGaugeQuery() { for { select { case <-stopChan: return default: - // 计算到下一个整点的等待时间 waitTime := durationUntilNextHour() log.Printf("下一次雨量计查询将在 %s 后进行 (整点: %s)", waitTime.String(), time.Now().Add(waitTime).Format("15:04:05")) - // 等待到下一个整点 timer := time.NewTimer(waitTime) select { case <-timer.C: @@ -89,7 +80,6 @@ func scheduleHourlyRainGaugeQuery() { } } -// StopScheduler 停止定时任务调度器 func StopScheduler() { if weatherTick != nil { weatherTick.Stop() @@ -98,42 +88,33 @@ func StopScheduler() { log.Println("定时查询任务已停止") } -// SetTaskConfig 设置任务配置 func SetTaskConfig(newConfig TaskConfig) { - // 先停止现有任务 StopScheduler() - // 更新配置 config = newConfig - // 重新启动任务 if config.Enabled { StartScheduler() } } -// queryWeatherStation 查询气象站并保存数据 func queryWeatherStation() { log.Println("执行气象站查询任务") - // 发送查询命令 err := modbus.QueryDevice(modbus.DeviceWeatherStation) if err != nil { log.Printf("气象站查询失败: %v", err) return } - // 等待设备响应 time.Sleep(2 * time.Second) - // 获取最新数据 weatherData := modbus.GetLatestWeatherData() if weatherData == nil { log.Println("未获取到气象站数据") return } - // 保存到数据库 _, err = db.SaveWeatherData(weatherData) if err != nil { log.Printf("保存气象站数据失败: %v", err) @@ -144,28 +125,23 @@ func queryWeatherStation() { weatherData.Temperature, weatherData.Humidity) } -// queryRainGauge 查询雨量计并保存数据 func queryRainGauge() { log.Println("执行雨量计查询任务 (整点)") - // 发送查询命令 err := modbus.QueryDevice(modbus.DeviceRainGauge) if err != nil { log.Printf("雨量计查询失败: %v", err) return } - // 等待设备响应 time.Sleep(2 * time.Second) - // 获取最新数据 rainData := modbus.GetLatestRainData() if rainData == nil { log.Println("未获取到雨量计数据") return } - // 保存到数据库 _, err = db.SaveRainGaugeData(rainData) if err != nil { log.Printf("保存雨量计数据失败: %v", err) diff --git a/test-get.go.bak b/test-get.go.bak new file mode 100644 index 0000000..c7e59f4 --- /dev/null +++ b/test-get.go.bak @@ -0,0 +1,411 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "rain_monitor/models" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +const ( + // 涂鸦开发者账号中获取的信息 + clientID = "nwmdye9c8ejymu9ge5kf" // 授权密钥对 key + secret = "658733ea78624cd4b63bae6083cd3fae" // 授权密钥对 value + baseURL = "https://openapi.tuyacn.com" + deviceID = "6cbbf72843839b6157wfb2" +) + +// 计算HMAC-SHA256签名 +func calculateSignature(secret, stringToSign string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(stringToSign)) + return hex.EncodeToString(h.Sum(nil)) +} + +// 获取访问令牌 +func getAccessToken() (string, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := "/v1.0/token?grant_type=1" + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := clientID + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(secret, strToHash) + signatureUpper := fmt.Sprintf("%s", signature) + signatureUpper = strings.ToUpper(signatureUpper) + + // 构建请求头 + req, err := http.NewRequest("GET", baseURL+url, nil) + if err != nil { + return "", err + } + + req.Header.Set("client_id", clientID) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // 解析响应 + var tokenResp models.PWS01TokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", err + } + + if !tokenResp.Success { + return "", fmt.Errorf("获取token失败: %s", string(body)) + } + + return tokenResp.Result.AccessToken, nil +} + +// 获取设备信息 +func getDeviceInfo(accessToken string) (*models.PWS01DeviceInfo, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := fmt.Sprintf("/v2.0/cloud/thing/%s", deviceID) + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := clientID + accessToken + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(secret, strToHash) + signatureUpper := fmt.Sprintf("%s", signature) + signatureUpper = strings.ToUpper(signatureUpper) + + // 构建请求头 + req, err := http.NewRequest("GET", baseURL+url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("client_id", clientID) + req.Header.Set("access_token", accessToken) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 解析响应 + var deviceInfo models.PWS01DeviceInfo + if err := json.Unmarshal(body, &deviceInfo); err != nil { + return nil, err + } + + return &deviceInfo, nil +} + +// 获取设备状态 +func getDeviceStatus(accessToken string) (*models.PWS01DeviceStatus, error) { + t := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) // 13位时间戳 + nonce := uuid.New().String() // 生成随机UUID + url := fmt.Sprintf("/v1.0/iot-03/devices/%s/status", deviceID) + + // 构建stringToSign + contentSHA256 := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 空body的SHA256值 + stringToSign := fmt.Sprintf("GET\n%s\n\n%s", contentSHA256, url) + + // 构建待签名字符串 + strToHash := clientID + accessToken + t + nonce + stringToSign + + // 使用HMAC-SHA256计算签名 + signature := calculateSignature(secret, strToHash) + signatureUpper := fmt.Sprintf("%s", signature) + signatureUpper = strings.ToUpper(signatureUpper) + + // 构建请求头 + req, err := http.NewRequest("GET", baseURL+url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("client_id", clientID) + req.Header.Set("access_token", accessToken) + req.Header.Set("sign", signatureUpper) + req.Header.Set("sign_method", "HMAC-SHA256") + req.Header.Set("t", t) + req.Header.Set("nonce", nonce) + + // 发起GET请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 首先解析为临时结构 + var tempResponse struct { + Result []models.PWS01StatusItem `json:"result"` + Success bool `json:"success"` + T int64 `json:"t"` + TID string `json:"tid"` + } + + if err := json.Unmarshal(body, &tempResponse); err != nil { + return nil, err + } + + // 创建最终响应 + deviceStatus := &models.PWS01DeviceStatus{ + Success: tempResponse.Success, + T: tempResponse.T, + TID: tempResponse.TID, + } + + // 将临时结构中的数据转换为我们的结构化数据 + statusData := models.PWS01StatusData{} + + // 遍历状态项并填充结构体 + for _, item := range tempResponse.Result { + switch item.Code { + case "temp_current": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrent = int(val) + } + case "humidity_value": + if val, ok := item.Value.(float64); ok { + statusData.HumidityValue = int(val) + } + case "battery_percentage": + if val, ok := item.Value.(float64); ok { + statusData.BatteryPercentage = int(val) + } + case "temp_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.TempUnitConvert = val + } + case "windspeed_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.WindspeedUnitConvert = val + } + case "pressure_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.PressureUnitConvert = val + } + case "rain_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.RainUnitConvert = val + } + case "bright_unit_convert": + if val, ok := item.Value.(string); ok { + statusData.BrightUnitConvert = val + } + case "temp_current_external": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal = int(val) + } + case "humidity_outdoor": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor = int(val) + } + case "temp_current_external_1": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal1 = int(val) + } + case "humidity_outdoor_1": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor1 = int(val) + } + case "temp_current_external_2": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal2 = int(val) + } + case "humidity_outdoor_2": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor2 = int(val) + } + case "temp_current_external_3": + if val, ok := item.Value.(float64); ok { + statusData.TempCurrentExternal3 = int(val) + } + case "humidity_outdoor_3": + if val, ok := item.Value.(float64); ok { + statusData.HumidityOutdoor3 = int(val) + } + case "atmospheric_pressture": + if val, ok := item.Value.(float64); ok { + statusData.AtmosphericPressure = int(val) + } + case "pressure_drop": + if val, ok := item.Value.(float64); ok { + statusData.PressureDrop = int(val) + } + case "windspeed_avg": + if val, ok := item.Value.(float64); ok { + statusData.WindspeedAvg = int(val) + } + case "windspeed_gust": + if val, ok := item.Value.(float64); ok { + statusData.WindspeedGust = int(val) + } + case "rain_1h": + if val, ok := item.Value.(float64); ok { + statusData.Rain1h = int(val) + } + case "rain_24h": + if val, ok := item.Value.(float64); ok { + statusData.Rain24h = int(val) + } + case "rain_rate": + if val, ok := item.Value.(float64); ok { + statusData.RainRate = int(val) + } + case "uv_index": + if val, ok := item.Value.(float64); ok { + statusData.UVIndex = int(val) + } + case "dew_point_temp": + if val, ok := item.Value.(float64); ok { + statusData.DewPointTemp = int(val) + } + case "feellike_temp": + if val, ok := item.Value.(float64); ok { + statusData.FeellikeTemp = int(val) + } + case "heat_index": + if val, ok := item.Value.(float64); ok { + statusData.HeatIndex = int(val) + } + case "windchill_index": + if val, ok := item.Value.(float64); ok { + statusData.WindchillIndex = int(val) + } + } + } + + deviceStatus.Result = statusData + return deviceStatus, nil +} + +// 打印设备信息 +func printDeviceInfo(info *models.PWS01DeviceInfo) { + fmt.Println("========== 设备信息 ==========") + fmt.Printf("设备ID: %s\n", info.Result.ID) + fmt.Printf("设备名称: %s\n", info.Result.Name) + fmt.Printf("设备型号: %s\n", info.Result.Model) + fmt.Printf("IP地址: %s\n", info.Result.IP) + fmt.Printf("在线状态: %v\n", info.Result.IsOnline) + fmt.Printf("经度: %s\n", info.Result.Lon) + fmt.Printf("纬度: %s\n", info.Result.Lat) + fmt.Printf("时区: %s\n", info.Result.TimeZone) + fmt.Printf("激活时间: %d\n", info.Result.ActiveTime) + fmt.Printf("更新时间: %d\n", info.Result.UpdateTime) + fmt.Println("============================") +} + +// 打印设备状态 +func printDeviceStatus(status *models.PWS01DeviceStatus) { + fmt.Println("========== 设备状态 ==========") + fmt.Printf("室内温度: %.1f℃\n", float64(status.Result.TempCurrent)/10.0) + fmt.Printf("室内湿度: %d%%\n", status.Result.HumidityValue) + fmt.Printf("电池电量: %d%%\n", status.Result.BatteryPercentage) + fmt.Printf("温度单位: %s\n", status.Result.TempUnitConvert) + fmt.Printf("风速单位: %s\n", status.Result.WindspeedUnitConvert) + fmt.Printf("气压单位: %s\n", status.Result.PressureUnitConvert) + fmt.Printf("雨量单位: %s\n", status.Result.RainUnitConvert) + fmt.Printf("亮度单位: %s\n", status.Result.BrightUnitConvert) + fmt.Printf("室外温度: %.1f℃\n", float64(status.Result.TempCurrentExternal)/10.0) + fmt.Printf("室外湿度: %d%%\n", status.Result.HumidityOutdoor) + fmt.Printf("大气压力: %d hPa\n", status.Result.AtmosphericPressure) + fmt.Printf("压降: %d hPa\n", status.Result.PressureDrop) + fmt.Printf("平均风速: %.1f m/s\n", float64(status.Result.WindspeedAvg)/10.0) + fmt.Printf("阵风风速: %.1f m/s\n", float64(status.Result.WindspeedGust)/10.0) + fmt.Printf("一小时降雨量: %.1f mm\n", float64(status.Result.Rain1h)/10.0) + fmt.Printf("24小时降雨量: %.1f mm\n", float64(status.Result.Rain24h)/10.0) + fmt.Printf("雨率: %.1f mm\n", float64(status.Result.RainRate)/10.0) + fmt.Printf("紫外线指数: %d\n", status.Result.UVIndex) + fmt.Printf("露点温度: %.1f℃\n", float64(status.Result.DewPointTemp)/10.0) + fmt.Printf("体感温度: %.1f℃\n", float64(status.Result.FeellikeTemp)/10.0) + fmt.Printf("酷热指数: %.1f℃\n", float64(status.Result.HeatIndex)/10.0) + fmt.Printf("风寒指数: %.1f℃\n", float64(status.Result.WindchillIndex)/10.0) + fmt.Println("============================") +} + +func main() { + // 获取访问令牌 + fmt.Println("正在获取访问令牌...") + accessToken, err := getAccessToken() + if err != nil { + fmt.Printf("获取访问令牌失败: %v\n", err) + return + } + fmt.Printf("成功获取访问令牌: %s\n", accessToken) + + // 获取设备信息 + fmt.Println("\n正在获取设备信息...") + deviceInfo, err := getDeviceInfo(accessToken) + if err != nil { + fmt.Printf("获取设备信息失败: %v\n", err) + return + } + printDeviceInfo(deviceInfo) + + // 获取设备状态 + fmt.Println("\n正在获取设备状态...") + deviceStatus, err := getDeviceStatus(accessToken) + if err != nil { + fmt.Printf("获取设备状态失败: %v\n", err) + return + } + printDeviceStatus(deviceStatus) + + // 打印原始JSON数据 + fmt.Println("\n========== 原始设备信息JSON ==========") + infoJSON, _ := json.MarshalIndent(deviceInfo, "", " ") + fmt.Println(string(infoJSON)) + + fmt.Println("\n========== 原始设备状态JSON ==========") + statusJSON, _ := json.MarshalIndent(deviceStatus, "", " ") + fmt.Println(string(statusJSON)) +} \ No newline at end of file diff --git a/todo.md b/todo.md index dfe5760..8027097 100644 --- a/todo.md +++ b/todo.md @@ -2,11 +2,13 @@ ## 系统概述 开发一个监测系统,通过ModBus-RTU协议与两种设备通信,收集并展示气象和雨量数据。 +新增PWS01设备通过HTTP GET请求获取数据,提供更全面的气象要素。 ## 系统架构 - Web界面:监听10003端口 - TCP服务器:监听10004端口,与设备通信 - 两种客户端设备:地址码01(气象站)和02(雨量计) +- PWS01设备:通过HTTP GET请求通信 ## 开发任务 @@ -20,6 +22,9 @@ - 发送指令:`02030000000ac5fe` - 解析返回数据(当天降雨量、瞬时降雨量等) - [ ] 实现数据转换(根据设备寄存器定义) +- [ ] 新增PWS01设备HTTP GET请求数据采集 + - 创建专门的包处理HTTP GET请求 + - 解析JSON响应数据 ### 2. 数据存储模块 - [ ] 设计MySQL数据库表结构 @@ -27,6 +32,9 @@ - 设备02数据表 - [ ] 实现数据持久化存储 - [ ] 实现数据查询接口 +- [ ] 新增PWS01设备表结构 + - 设备信息表:存储设备基本信息,以设备ID为主键 + - 设备数据表:存储设备采集的气象数据,以设备ID为外键 ### 3. Web服务器模块 - [ ] 实现Web服务器,监听10003端口 @@ -35,6 +43,10 @@ - 查询历史数据(支持时间范围) - 数据聚合(按小时、天等) - 触发设备查询 +- [ ] 新增PWS01设备API接口 + - 获取PWS01设备信息 + - 获取PWS01最新数据 + - 查询PWS01历史数据 ### 4. 前端界面 - [ ] 参考提供的HTML风格,实现Web界面 @@ -42,11 +54,89 @@ - [ ] 实现数据表格展示 - [ ] 实现数据导出功能 - [ ] 实现设备连接状态显示 +- [ ] 新增PWS01设备专属页面 + - 显示设备信息 + - 展示全部气象要素数据 + - 数据可视化 -### 5. 系统集成与测试 -- [ ] 集成各模块 -- [ ] 系统测试 -- [ ] 性能优化 +### 5. PWS01设备集成 +- [ ] 新增查询方法 + - 实现HTTP GET请求获取设备数据 + - 解析JSON响应 +- [ ] 新增定时采集任务 + - 默认15分钟查询一次 + - 可配置查询间隔 +- [ ] 新增数据结构 + - PWS01设备信息结构 + - PWS01设备数据结构 +- [ ] 新增数据库表 + - pws01_devices表:存储设备信息 + ```sql + CREATE TABLE IF NOT EXISTS pws01_devices ( + id VARCHAR(50) PRIMARY KEY, + active_time BIGINT NOT NULL, + bind_space_id VARCHAR(50) NOT NULL, + category VARCHAR(20) NOT NULL, + create_time BIGINT NOT NULL, + custom_name VARCHAR(100), + icon VARCHAR(255), + ip VARCHAR(50) NOT NULL, + is_online BOOLEAN NOT NULL, + lat VARCHAR(20), + local_key VARCHAR(50), + lon VARCHAR(20), + model VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + product_id VARCHAR(50) NOT NULL, + product_name VARCHAR(100) NOT NULL, + sub BOOLEAN NOT NULL, + time_zone VARCHAR(10) NOT NULL, + update_time BIGINT NOT NULL, + uuid VARCHAR(50) NOT NULL, + last_query_time DATETIME, + INDEX idx_ip (ip), + INDEX idx_product_id (product_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ``` + - pws01_data表:存储设备数据 + ```sql + CREATE TABLE IF NOT EXISTS pws01_data ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + device_id VARCHAR(50) NOT NULL, + timestamp DATETIME NOT NULL, + temp_current INT, + humidity_value INT, + battery_percentage INT, + temp_unit_convert VARCHAR(10), + windspeed_unit_convert VARCHAR(10), + pressure_unit_convert VARCHAR(10), + rain_unit_convert VARCHAR(10), + bright_unit_convert VARCHAR(10), + temp_current_external INT, + humidity_outdoor INT, + temp_current_external_1 INT, + humidity_outdoor_1 INT, + temp_current_external_2 INT, + humidity_outdoor_2 INT, + temp_current_external_3 INT, + humidity_outdoor_3 INT, + atmospheric_pressture INT, + pressure_drop INT, + windspeed_avg INT, + windspeed_gust INT, + rain_1h INT, + rain_24h INT, + rain_rate INT, + uv_index INT, + dew_point_temp INT, + feellike_temp INT, + heat_index INT, + windchill_index INT, + INDEX idx_device_id (device_id), + INDEX idx_timestamp (timestamp), + FOREIGN KEY (device_id) REFERENCES pws01_devices(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ``` ## 技术栈 - 后端:Go语言