diff --git a/api/api.go b/api/api.go index 8258e29..2c6a3b5 100644 --- a/api/api.go +++ b/api/api.go @@ -10,6 +10,7 @@ import ( "time" ) +// StartWebServer 启动Web服务器 func StartWebServer() { http.HandleFunc("/api/status", handleStatus) http.HandleFunc("/api/raw/latest", handleLatestRawData) @@ -17,6 +18,7 @@ func StartWebServer() { http.HandleFunc("/api/data", handleQueryData) http.HandleFunc("/api/latest", handleLatestData) + // 静态文件服务 http.Handle("/", http.FileServer(http.Dir("static"))) log.Println("Web服务器已启动,监听端口 10003") @@ -26,6 +28,7 @@ func StartWebServer() { } } +// handleStatus 处理连接状态请求 func handleStatus(w http.ResponseWriter, r *http.Request) { status := modbus.GetConnectionStatus() @@ -33,7 +36,9 @@ 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() @@ -52,6 +57,7 @@ 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") @@ -59,6 +65,7 @@ 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") } @@ -71,15 +78,16 @@ 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 + result["total_rainfall"] = rainData.TotalRainfall // 使用 total_rainfall 表示累计雨量 result["daily_rainfall"] = rainData.DailyRainfall result["instant_rainfall"] = rainData.InstantRainfall } + // 为了兼容旧代码,仍然提供一个 rainfall 字段,优先使用雨量计的数据 if rainData != nil { result["rainfall"] = rainData.TotalRainfall } else if weatherData != nil { @@ -90,6 +98,7 @@ 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) @@ -111,6 +120,7 @@ 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") @@ -146,6 +156,7 @@ 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 @@ -156,6 +167,7 @@ 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") @@ -188,9 +200,11 @@ 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 deleted file mode 100644 index 921ea05..0000000 --- a/config.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# 雨量监测系统配置文件 - -# 数据库配置 -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 3ef253c..f40b4a9 100644 --- a/db/db.go +++ b/db/db.go @@ -63,17 +63,6 @@ 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 } @@ -95,6 +84,8 @@ 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 @@ -105,9 +96,11 @@ 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 @@ -118,12 +111,13 @@ 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 + return id, nil // 返回已存在的记录ID } } } @@ -262,11 +256,14 @@ 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 } @@ -277,7 +274,3 @@ 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 deleted file mode 100644 index 3eddfb3..0000000 --- a/em3395ty/db.go +++ /dev/null @@ -1,242 +0,0 @@ -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 deleted file mode 100644 index 2c8b5f6..0000000 --- a/em3395ty/em3395ty.go +++ /dev/null @@ -1,338 +0,0 @@ -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 deleted file mode 100644 index 1b06040..0000000 --- a/em3395ty/scheduler.go +++ /dev/null @@ -1,221 +0,0 @@ -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 dea900d..17e83a2 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,6 @@ module rain_monitor go 1.24 -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 github.com/go-sql-driver/mysql v1.9.3 require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 7e89ba5..4bcdcfa 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,3 @@ 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 26e5f94..f04864f 100644 --- a/main.go +++ b/main.go @@ -2,157 +2,51 @@ 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 ( - configFile = flag.String("config", "config.yaml", "配置文件路径") - config Config + 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", "数据库名称") ) func main() { + // 解析命令行参数 flag.Parse() - // 加载配置文件 - err := loadConfig(*configFile) - if err != nil { - log.Fatalf("加载配置文件失败: %v", err) - } - + // 初始化数据库连接 dbConfig := db.DBConfig{ - Host: config.Database.Host, - Port: config.Database.Port, - User: config.Database.User, - Password: config.Database.Password, - DBName: config.Database.DBName, + Host: *dbHost, + Port: *dbPort, + User: *dbUser, + Password: *dbPassword, + DBName: *dbName, } log.Println("正在连接数据库...") - err = db.InitDB(dbConfig) + err := db.InitDB(dbConfig) if err != nil { log.Fatalf("数据库初始化失败: %v", err) } defer db.CloseDB() log.Println("数据库连接成功") - // 初始化EM3395TY配置 - em3395ty.InitConfig(config.EM3395TY.ClientID, config.EM3395TY.Secret, config.EM3395TY.BaseURL) - + // 启动TCP服务器 log.Println("正在启动TCP服务器...") go modbus.StartTCPServer() - // 配置并启动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() - } + // 启动定时任务调度器 + log.Println("正在启动定时任务调度器...") + go scheduler.StartScheduler() + // 启动Web服务器 log.Println("正在启动Web服务器...") - 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 + api.StartWebServer() // 这个函数会阻塞主线程 } diff --git a/modbus/modbus.go b/modbus/modbus.go index 16e6ce4..83ed459 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,6 +42,7 @@ func init() { connectedClients = make(map[string]net.Conn) } +// StartTCPServer 启动TCP服务器 func StartTCPServer() { listener, err := net.Listen("tcp", ":10004") if err != nil { @@ -69,6 +70,7 @@ func StartTCPServer() { } } +// HandleConnection 处理客户端连接 func handleConnection(conn net.Conn) { defer func() { conn.Close() @@ -99,6 +101,7 @@ func handleConnection(conn net.Conn) { } } +// ProcessModbusData 解析ModBus数据 func processModbusData(data []byte) { if len(data) < 3 { log.Println("数据长度不足") @@ -123,6 +126,7 @@ func processModbusData(data []byte) { } } +// ProcessWeatherStationData 处理气象站数据 func processWeatherStationData(data []byte) { if len(data) < 35 { log.Println("气象站数据长度不足") @@ -167,6 +171,7 @@ 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) @@ -176,6 +181,7 @@ func processWeatherStationData(data []byte) { } } +// ProcessRainGaugeData 处理雨量计数据 func processRainGaugeData(data []byte) { if len(data) < 25 { log.Println("雨量计数据长度不足") @@ -214,6 +220,7 @@ 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) @@ -223,6 +230,7 @@ func processRainGaugeData(data []byte) { } } +// QueryDevice 向设备发送查询命令 func QueryDevice(deviceType int) error { var cmd []byte @@ -254,6 +262,7 @@ func QueryDevice(deviceType int) error { return nil } +// GetConnectionStatus 获取连接状态 func GetConnectionStatus() models.ConnectionStatus { clientsMutex.RLock() defer clientsMutex.RUnlock() @@ -275,6 +284,7 @@ func GetConnectionStatus() models.ConnectionStatus { return status } +// GetLatestWeatherData 获取最新气象站数据 func GetLatestWeatherData() *models.WeatherData { dataMutex.RLock() defer dataMutex.RUnlock() @@ -283,10 +293,12 @@ func GetLatestWeatherData() *models.WeatherData { return nil } + // 返回一个副本 data := *latestWeatherData return &data } +// GetLatestRainData 获取最新雨量计数据 func GetLatestRainData() *models.RainGaugeData { dataMutex.RLock() defer dataMutex.RUnlock() @@ -295,6 +307,7 @@ func GetLatestRainData() *models.RainGaugeData { return nil } + // 返回一个副本 data := *latestRainData return &data } diff --git a/models/em3395ty.go b/models/em3395ty.go deleted file mode 100644 index aa4b918..0000000 --- a/models/em3395ty.go +++ /dev/null @@ -1,225 +0,0 @@ -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 ed4213b..537f927 100644 --- a/models/models.go +++ b/models/models.go @@ -4,6 +4,7 @@ import ( "time" ) +// WeatherData 气象站数据结构 type WeatherData struct { ID int64 `json:"id" db:"id"` Timestamp time.Time `json:"timestamp" db:"timestamp"` @@ -25,6 +26,7 @@ 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"` @@ -40,6 +42,7 @@ 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"` @@ -51,6 +54,7 @@ type AggregatedData struct { SolarRadiation float64 `json:"solar_radiation" db:"solar_radiation"` } +// ConnectionStatus 连接状态 type ConnectionStatus struct { Connected bool `json:"connected"` IP string `json:"ip,omitempty"` @@ -58,6 +62,7 @@ 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, @@ -82,6 +87,7 @@ 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, @@ -100,6 +106,7 @@ 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, @@ -110,6 +117,7 @@ INSERT INTO weather_data ( ) ` +// InsertRainGaugeDataSQL 插入雨量计数据SQL const InsertRainGaugeDataSQL = ` INSERT INTO rain_gauge_data ( timestamp, daily_rainfall, instant_rainfall, yesterday_rainfall, @@ -120,30 +128,35 @@ 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 8e9086c..18245bd 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -7,10 +7,11 @@ import ( "time" ) +// 任务配置 type TaskConfig struct { - WeatherStationInterval time.Duration - RainGaugeInterval time.Duration - Enabled bool + WeatherStationInterval time.Duration // 气象站查询间隔 + RainGaugeInterval time.Duration // 雨量计查询间隔 + Enabled bool // 是否启用定时查询 } var ( @@ -19,15 +20,17 @@ var ( stopChan chan struct{} ) +// 初始化默认配置 func init() { config = TaskConfig{ - WeatherStationInterval: 15 * time.Minute, - RainGaugeInterval: time.Hour, - Enabled: true, + WeatherStationInterval: 15 * time.Minute, // 默认15分钟查询一次气象站 + RainGaugeInterval: time.Hour, // 默认每小时查询一次雨量计 + Enabled: true, // 默认启用 } stopChan = make(chan struct{}) } +// StartScheduler 启动定时任务调度器 func StartScheduler() { if !config.Enabled { log.Println("定时查询任务已禁用") @@ -37,6 +40,7 @@ func StartScheduler() { log.Printf("启动定时查询任务,气象站间隔: %v, 雨量计整点查询", config.WeatherStationInterval) + // 启动气象站查询任务 weatherTick = time.NewTicker(config.WeatherStationInterval) go func() { for { @@ -49,25 +53,30 @@ 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: @@ -80,6 +89,7 @@ func scheduleHourlyRainGaugeQuery() { } } +// StopScheduler 停止定时任务调度器 func StopScheduler() { if weatherTick != nil { weatherTick.Stop() @@ -88,33 +98,42 @@ 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) @@ -125,23 +144,28 @@ 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 deleted file mode 100644 index c7e59f4..0000000 --- a/test-get.go.bak +++ /dev/null @@ -1,411 +0,0 @@ -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 8027097..dfe5760 100644 --- a/todo.md +++ b/todo.md @@ -2,13 +2,11 @@ ## 系统概述 开发一个监测系统,通过ModBus-RTU协议与两种设备通信,收集并展示气象和雨量数据。 -新增PWS01设备通过HTTP GET请求获取数据,提供更全面的气象要素。 ## 系统架构 - Web界面:监听10003端口 - TCP服务器:监听10004端口,与设备通信 - 两种客户端设备:地址码01(气象站)和02(雨量计) -- PWS01设备:通过HTTP GET请求通信 ## 开发任务 @@ -22,9 +20,6 @@ - 发送指令:`02030000000ac5fe` - 解析返回数据(当天降雨量、瞬时降雨量等) - [ ] 实现数据转换(根据设备寄存器定义) -- [ ] 新增PWS01设备HTTP GET请求数据采集 - - 创建专门的包处理HTTP GET请求 - - 解析JSON响应数据 ### 2. 数据存储模块 - [ ] 设计MySQL数据库表结构 @@ -32,9 +27,6 @@ - 设备02数据表 - [ ] 实现数据持久化存储 - [ ] 实现数据查询接口 -- [ ] 新增PWS01设备表结构 - - 设备信息表:存储设备基本信息,以设备ID为主键 - - 设备数据表:存储设备采集的气象数据,以设备ID为外键 ### 3. Web服务器模块 - [ ] 实现Web服务器,监听10003端口 @@ -43,10 +35,6 @@ - 查询历史数据(支持时间范围) - 数据聚合(按小时、天等) - 触发设备查询 -- [ ] 新增PWS01设备API接口 - - 获取PWS01设备信息 - - 获取PWS01最新数据 - - 查询PWS01历史数据 ### 4. 前端界面 - [ ] 参考提供的HTML风格,实现Web界面 @@ -54,89 +42,11 @@ - [ ] 实现数据表格展示 - [ ] 实现数据导出功能 - [ ] 实现设备连接状态显示 -- [ ] 新增PWS01设备专属页面 - - 显示设备信息 - - 展示全部气象要素数据 - - 数据可视化 -### 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; - ``` +### 5. 系统集成与测试 +- [ ] 集成各模块 +- [ ] 系统测试 +- [ ] 性能优化 ## 技术栈 - 后端:Go语言