feat:新增EM3395TY设备

This commit is contained in:
yarnom 2025-07-12 17:41:49 +08:00
parent 58a98edc33
commit b8819d6a5e
15 changed files with 1734 additions and 109 deletions

View File

@ -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

39
config.yaml Normal file
View File

@ -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

View File

@ -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
}

242
em3395ty/db.go Normal file
View File

@ -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, &timestamp,
&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
}

338
em3395ty/em3395ty.go Normal file
View File

@ -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
}

221
em3395ty/scheduler.go Normal file
View File

@ -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)
}
}

6
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

144
main.go
View File

@ -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
}

View File

@ -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
}

225
models/em3395ty.go Normal file
View File

@ -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 = ?
`

View File

@ -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,

View File

@ -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)

411
test-get.go.bak Normal file
View File

@ -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))
}

98
todo.md
View File

@ -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语言