Revert "feat:新增EM3395TY设备"
This reverts commit b8819d6a5e3590b145a8c785d16c44b113365e42.
This commit is contained in:
parent
b8819d6a5e
commit
f6f70a561e
18
api/api.go
18
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
|
||||
|
||||
39
config.yaml
39
config.yaml
@ -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
|
||||
|
||||
|
||||
25
db/db.go
25
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
|
||||
}
|
||||
|
||||
242
em3395ty/db.go
242
em3395ty/db.go
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
6
go.mod
6
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
144
main.go
144
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() // 这个函数会阻塞主线程
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = ?
|
||||
`
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
411
test-get.go.bak
411
test-get.go.bak
@ -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))
|
||||
}
|
||||
98
todo.md
98
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语言
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user