feat:新增EM3395TY设备
This commit is contained in:
parent
58a98edc33
commit
b8819d6a5e
18
api/api.go
18
api/api.go
@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartWebServer 启动Web服务器
|
|
||||||
func StartWebServer() {
|
func StartWebServer() {
|
||||||
http.HandleFunc("/api/status", handleStatus)
|
http.HandleFunc("/api/status", handleStatus)
|
||||||
http.HandleFunc("/api/raw/latest", handleLatestRawData)
|
http.HandleFunc("/api/raw/latest", handleLatestRawData)
|
||||||
@ -18,7 +17,6 @@ func StartWebServer() {
|
|||||||
http.HandleFunc("/api/data", handleQueryData)
|
http.HandleFunc("/api/data", handleQueryData)
|
||||||
http.HandleFunc("/api/latest", handleLatestData)
|
http.HandleFunc("/api/latest", handleLatestData)
|
||||||
|
|
||||||
// 静态文件服务
|
|
||||||
http.Handle("/", http.FileServer(http.Dir("static")))
|
http.Handle("/", http.FileServer(http.Dir("static")))
|
||||||
|
|
||||||
log.Println("Web服务器已启动,监听端口 10003")
|
log.Println("Web服务器已启动,监听端口 10003")
|
||||||
@ -28,7 +26,6 @@ func StartWebServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStatus 处理连接状态请求
|
|
||||||
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
status := modbus.GetConnectionStatus()
|
status := modbus.GetConnectionStatus()
|
||||||
|
|
||||||
@ -36,9 +33,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(status)
|
json.NewEncoder(w).Encode(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLatestRawData 获取最新原始数据
|
|
||||||
func handleLatestRawData(w http.ResponseWriter, r *http.Request) {
|
func handleLatestRawData(w http.ResponseWriter, r *http.Request) {
|
||||||
// 从数据库获取最新数据,而不是从modbus内存中获取
|
|
||||||
weatherData, err1 := db.GetLatestWeatherData()
|
weatherData, err1 := db.GetLatestWeatherData()
|
||||||
rainData, err2 := db.GetLatestRainGaugeData()
|
rainData, err2 := db.GetLatestRainGaugeData()
|
||||||
|
|
||||||
@ -57,7 +52,6 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
result := map[string]interface{}{}
|
result := map[string]interface{}{}
|
||||||
|
|
||||||
// 使用数据库中的时间戳,如果可用
|
|
||||||
if weatherData != nil && !weatherData.Timestamp.IsZero() {
|
if weatherData != nil && !weatherData.Timestamp.IsZero() {
|
||||||
result["timestamp"] = weatherData.Timestamp.Format(time.RFC3339)
|
result["timestamp"] = weatherData.Timestamp.Format(time.RFC3339)
|
||||||
result["formatted_time"] = weatherData.Timestamp.Format("2006-01-02 15:04:05")
|
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["timestamp"] = rainData.Timestamp.Format(time.RFC3339)
|
||||||
result["formatted_time"] = rainData.Timestamp.Format("2006-01-02 15:04:05")
|
result["formatted_time"] = rainData.Timestamp.Format("2006-01-02 15:04:05")
|
||||||
} else {
|
} else {
|
||||||
// 如果都没有时间戳,则使用当前时间作为 fallback
|
|
||||||
result["timestamp"] = time.Now().Format(time.RFC3339)
|
result["timestamp"] = time.Now().Format(time.RFC3339)
|
||||||
result["formatted_time"] = time.Now().Format("2006-01-02 15:04:05")
|
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["wind_direction_360"] = weatherData.WindDirection360
|
||||||
result["atm_pressure"] = weatherData.AtmPressure
|
result["atm_pressure"] = weatherData.AtmPressure
|
||||||
result["solar_radiation"] = weatherData.SolarRadiation
|
result["solar_radiation"] = weatherData.SolarRadiation
|
||||||
result["weather_rainfall"] = weatherData.Rainfall // 区分气象站的雨量
|
result["weather_rainfall"] = weatherData.Rainfall
|
||||||
}
|
}
|
||||||
|
|
||||||
if rainData != nil {
|
if rainData != nil {
|
||||||
result["total_rainfall"] = rainData.TotalRainfall // 使用 total_rainfall 表示累计雨量
|
result["total_rainfall"] = rainData.TotalRainfall
|
||||||
result["daily_rainfall"] = rainData.DailyRainfall
|
result["daily_rainfall"] = rainData.DailyRainfall
|
||||||
result["instant_rainfall"] = rainData.InstantRainfall
|
result["instant_rainfall"] = rainData.InstantRainfall
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为了兼容旧代码,仍然提供一个 rainfall 字段,优先使用雨量计的数据
|
|
||||||
if rainData != nil {
|
if rainData != nil {
|
||||||
result["rainfall"] = rainData.TotalRainfall
|
result["rainfall"] = rainData.TotalRainfall
|
||||||
} else if weatherData != nil {
|
} else if weatherData != nil {
|
||||||
@ -98,7 +90,6 @@ func handleLatestRawData(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTriggerQuery 触发设备查询(保留手动采集功能)
|
|
||||||
func handleTriggerQuery(w http.ResponseWriter, r *http.Request) {
|
func handleTriggerQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
err1 := modbus.QueryDevice(modbus.DeviceWeatherStation)
|
err1 := modbus.QueryDevice(modbus.DeviceWeatherStation)
|
||||||
err2 := modbus.QueryDevice(modbus.DeviceRainGauge)
|
err2 := modbus.QueryDevice(modbus.DeviceRainGauge)
|
||||||
@ -120,7 +111,6 @@ func handleTriggerQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleQueryData 查询历史数据
|
|
||||||
func handleQueryData(w http.ResponseWriter, r *http.Request) {
|
func handleQueryData(w http.ResponseWriter, r *http.Request) {
|
||||||
startStr := r.URL.Query().Get("start")
|
startStr := r.URL.Query().Get("start")
|
||||||
endStr := r.URL.Query().Get("end")
|
endStr := r.URL.Query().Get("end")
|
||||||
@ -156,7 +146,6 @@ func handleQueryData(w http.ResponseWriter, r *http.Request) {
|
|||||||
data, err := db.GetAggregatedData(start, end)
|
data, err := db.GetAggregatedData(start, end)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询聚合数据失败: %v", err)
|
log.Printf("查询聚合数据失败: %v", err)
|
||||||
// 返回空数组而不是错误,避免前端报错
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode([]models.AggregatedData{})
|
json.NewEncoder(w).Encode([]models.AggregatedData{})
|
||||||
return
|
return
|
||||||
@ -167,7 +156,6 @@ func handleQueryData(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLatestData 获取最新聚合数据
|
|
||||||
func handleLatestData(w http.ResponseWriter, r *http.Request) {
|
func handleLatestData(w http.ResponseWriter, r *http.Request) {
|
||||||
startStr := r.URL.Query().Get("start")
|
startStr := r.URL.Query().Get("start")
|
||||||
endStr := r.URL.Query().Get("end")
|
endStr := r.URL.Query().Get("end")
|
||||||
@ -200,11 +188,9 @@ func handleLatestData(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只查数据库,不主动采集
|
|
||||||
data, err := db.GetAggregatedData(start, end)
|
data, err := db.GetAggregatedData(start, end)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询聚合数据失败: %v", err)
|
log.Printf("查询聚合数据失败: %v", err)
|
||||||
// 返回空数组而不是错误,避免前端报错
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode([]models.AggregatedData{})
|
json.NewEncoder(w).Encode([]models.AggregatedData{})
|
||||||
return
|
return
|
||||||
|
|||||||
39
config.yaml
Normal file
39
config.yaml
Normal 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
|
||||||
|
|
||||||
|
|
||||||
25
db/db.go
25
db/db.go
@ -63,6 +63,17 @@ func createTables() error {
|
|||||||
return fmt.Errorf("创建雨量计数据表失败: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +95,6 @@ func SaveWeatherData(data *models.WeatherData) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) {
|
func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) {
|
||||||
// 检查是否在短时间内已经插入过类似数据
|
|
||||||
// 获取最近5分钟内的数据
|
|
||||||
fiveMinutesAgo := data.Timestamp.Add(-5 * time.Minute)
|
fiveMinutesAgo := data.Timestamp.Add(-5 * time.Minute)
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT id, timestamp, total_rainfall
|
SELECT id, timestamp, total_rainfall
|
||||||
@ -96,11 +105,9 @@ func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询最近雨量计数据失败: %v", err)
|
log.Printf("查询最近雨量计数据失败: %v", err)
|
||||||
// 即使查询失败,我们仍然尝试插入新数据
|
|
||||||
} else {
|
} else {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
// 检查是否有相似数据
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
var ts time.Time
|
var ts time.Time
|
||||||
@ -111,13 +118,12 @@ func SaveRainGaugeData(data *models.RainGaugeData) (int64, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果时间非常接近(小于1分钟)且雨量数据相同或非常接近,则认为是重复数据
|
|
||||||
timeDiff := data.Timestamp.Sub(ts)
|
timeDiff := data.Timestamp.Sub(ts)
|
||||||
rainfallDiff := math.Abs(data.TotalRainfall - rainfall)
|
rainfallDiff := math.Abs(data.TotalRainfall - rainfall)
|
||||||
|
|
||||||
if timeDiff < time.Minute && rainfallDiff < 0.1 {
|
if timeDiff < time.Minute && rainfallDiff < 0.1 {
|
||||||
log.Printf("检测到重复的雨量计数据,跳过插入。ID=%d, 时间=%v", id, ts)
|
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)
|
return nil, fmt.Errorf("扫描聚合数据失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置格式化时间字符串
|
|
||||||
data.FormattedTime = timestampStr
|
data.FormattedTime = timestampStr
|
||||||
|
|
||||||
// 尝试解析时间字符串为time.Time类型
|
|
||||||
timestamp, err := time.Parse("2006-01-02 15:04:05", timestampStr)
|
timestamp, err := time.Parse("2006-01-02 15:04:05", timestampStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("解析时间字符串失败: %v, 原始字符串: %s", err, timestampStr)
|
log.Printf("解析时间字符串失败: %v, 原始字符串: %s", err, timestampStr)
|
||||||
// 即使解析失败,也继续处理,前端会使用FormattedTime
|
|
||||||
} else {
|
} else {
|
||||||
data.Timestamp = timestamp
|
data.Timestamp = timestamp
|
||||||
}
|
}
|
||||||
@ -274,3 +277,7 @@ func GetAggregatedData(start, end time.Time) ([]models.AggregatedData, error) {
|
|||||||
log.Printf("查询结果: 找到 %d 条记录", len(result))
|
log.Printf("查询结果: 找到 %d 条记录", len(result))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDB() *sql.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|||||||
242
em3395ty/db.go
Normal file
242
em3395ty/db.go
Normal 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, ×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
|
||||||
|
}
|
||||||
338
em3395ty/em3395ty.go
Normal file
338
em3395ty/em3395ty.go
Normal 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
221
em3395ty/scheduler.go
Normal 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
6
go.mod
@ -2,6 +2,10 @@ module rain_monitor
|
|||||||
|
|
||||||
go 1.24
|
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
|
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -2,3 +2,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
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=
|
||||||
|
|||||||
146
main.go
146
main.go
@ -2,51 +2,157 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"rain_monitor/api"
|
"rain_monitor/api"
|
||||||
"rain_monitor/db"
|
"rain_monitor/db"
|
||||||
|
"rain_monitor/em3395ty"
|
||||||
"rain_monitor/modbus"
|
"rain_monitor/modbus"
|
||||||
"rain_monitor/scheduler"
|
"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 (
|
var (
|
||||||
dbHost = flag.String("db-host", "8.134.185.53", "数据库主机地址")
|
configFile = flag.String("config", "config.yaml", "配置文件路径")
|
||||||
dbPort = flag.Int("db-port", 3306, "数据库端口")
|
config Config
|
||||||
dbUser = flag.String("db-user", "remote", "数据库用户名")
|
|
||||||
dbPassword = flag.String("db-password", "root", "数据库密码")
|
|
||||||
dbName = flag.String("db-name", "rain_monitor", "数据库名称")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 解析命令行参数
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// 初始化数据库连接
|
// 加载配置文件
|
||||||
|
err := loadConfig(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("加载配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
dbConfig := db.DBConfig{
|
dbConfig := db.DBConfig{
|
||||||
Host: *dbHost,
|
Host: config.Database.Host,
|
||||||
Port: *dbPort,
|
Port: config.Database.Port,
|
||||||
User: *dbUser,
|
User: config.Database.User,
|
||||||
Password: *dbPassword,
|
Password: config.Database.Password,
|
||||||
DBName: *dbName,
|
DBName: config.Database.DBName,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("正在连接数据库...")
|
log.Println("正在连接数据库...")
|
||||||
err := db.InitDB(dbConfig)
|
err = db.InitDB(dbConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("数据库初始化失败: %v", err)
|
log.Fatalf("数据库初始化失败: %v", err)
|
||||||
}
|
}
|
||||||
defer db.CloseDB()
|
defer db.CloseDB()
|
||||||
log.Println("数据库连接成功")
|
log.Println("数据库连接成功")
|
||||||
|
|
||||||
// 启动TCP服务器
|
// 初始化EM3395TY配置
|
||||||
|
em3395ty.InitConfig(config.EM3395TY.ClientID, config.EM3395TY.Secret, config.EM3395TY.BaseURL)
|
||||||
|
|
||||||
log.Println("正在启动TCP服务器...")
|
log.Println("正在启动TCP服务器...")
|
||||||
go modbus.StartTCPServer()
|
go modbus.StartTCPServer()
|
||||||
|
|
||||||
// 启动定时任务调度器
|
// 配置并启动ModBus设备调度器
|
||||||
log.Println("正在启动定时任务调度器...")
|
if config.Scheduler.Enabled {
|
||||||
go scheduler.StartScheduler()
|
log.Println("正在启动ModBus设备定时任务调度器...")
|
||||||
|
|
||||||
// 启动Web服务器
|
// 解析气象站查询间隔
|
||||||
log.Println("正在启动Web服务器...")
|
weatherStationInterval := 15 * time.Minute // 默认值
|
||||||
api.StartWebServer() // 这个函数会阻塞主线程
|
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("正在启动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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DeviceWeatherStation = 1 // 气象站
|
DeviceWeatherStation = 1
|
||||||
DeviceRainGauge = 2 // 雨量计
|
DeviceRainGauge = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -26,8 +26,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
WeatherStationCmd = []byte{0x01, 0x03, 0x01, 0xf4, 0x00, 0x10, 0x04, 0x08} // 气象站查询命令
|
WeatherStationCmd = []byte{0x01, 0x03, 0x01, 0xf4, 0x00, 0x10, 0x04, 0x08}
|
||||||
RainGaugeCmd = []byte{0x02, 0x03, 0x00, 0x00, 0x00, 0x0a, 0xc5, 0xfe} // 雨量计查询命令
|
RainGaugeCmd = []byte{0x02, 0x03, 0x00, 0x00, 0x00, 0x0a, 0xc5, 0xfe}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -42,7 +42,6 @@ func init() {
|
|||||||
connectedClients = make(map[string]net.Conn)
|
connectedClients = make(map[string]net.Conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartTCPServer 启动TCP服务器
|
|
||||||
func StartTCPServer() {
|
func StartTCPServer() {
|
||||||
listener, err := net.Listen("tcp", ":10004")
|
listener, err := net.Listen("tcp", ":10004")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,7 +69,6 @@ func StartTCPServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleConnection 处理客户端连接
|
|
||||||
func handleConnection(conn net.Conn) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer func() {
|
defer func() {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -101,7 +99,6 @@ func handleConnection(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessModbusData 解析ModBus数据
|
|
||||||
func processModbusData(data []byte) {
|
func processModbusData(data []byte) {
|
||||||
if len(data) < 3 {
|
if len(data) < 3 {
|
||||||
log.Println("数据长度不足")
|
log.Println("数据长度不足")
|
||||||
@ -126,7 +123,6 @@ func processModbusData(data []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessWeatherStationData 处理气象站数据
|
|
||||||
func processWeatherStationData(data []byte) {
|
func processWeatherStationData(data []byte) {
|
||||||
if len(data) < 35 {
|
if len(data) < 35 {
|
||||||
log.Println("气象站数据长度不足")
|
log.Println("气象站数据长度不足")
|
||||||
@ -171,7 +167,6 @@ func processWeatherStationData(data []byte) {
|
|||||||
weather.Temperature, weather.Humidity, weather.WindSpeed, weather.WindDirection360, weather.AtmPressure,
|
weather.Temperature, weather.Humidity, weather.WindSpeed, weather.WindDirection360, weather.AtmPressure,
|
||||||
weather.PM25, weather.PM10, weather.Rainfall, weather.LightIntensity)
|
weather.PM25, weather.PM10, weather.Rainfall, weather.LightIntensity)
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
_, err := db.SaveWeatherData(weather)
|
_, err := db.SaveWeatherData(weather)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("保存气象站数据失败: %v", err)
|
log.Printf("保存气象站数据失败: %v", err)
|
||||||
@ -181,7 +176,6 @@ func processWeatherStationData(data []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessRainGaugeData 处理雨量计数据
|
|
||||||
func processRainGaugeData(data []byte) {
|
func processRainGaugeData(data []byte) {
|
||||||
if len(data) < 25 {
|
if len(data) < 25 {
|
||||||
log.Println("雨量计数据长度不足")
|
log.Println("雨量计数据长度不足")
|
||||||
@ -220,7 +214,6 @@ func processRainGaugeData(data []byte) {
|
|||||||
rainData.DailyRainfall, rainData.InstantRainfall, rainData.TotalRainfall,
|
rainData.DailyRainfall, rainData.InstantRainfall, rainData.TotalRainfall,
|
||||||
rainData.YesterdayRainfall, rainData.HourlyRainfall, rainData.LastHourRainfall)
|
rainData.YesterdayRainfall, rainData.HourlyRainfall, rainData.LastHourRainfall)
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
_, err := db.SaveRainGaugeData(rainData)
|
_, err := db.SaveRainGaugeData(rainData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("保存雨量计数据失败: %v", err)
|
log.Printf("保存雨量计数据失败: %v", err)
|
||||||
@ -230,7 +223,6 @@ func processRainGaugeData(data []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryDevice 向设备发送查询命令
|
|
||||||
func QueryDevice(deviceType int) error {
|
func QueryDevice(deviceType int) error {
|
||||||
var cmd []byte
|
var cmd []byte
|
||||||
|
|
||||||
@ -262,7 +254,6 @@ func QueryDevice(deviceType int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnectionStatus 获取连接状态
|
|
||||||
func GetConnectionStatus() models.ConnectionStatus {
|
func GetConnectionStatus() models.ConnectionStatus {
|
||||||
clientsMutex.RLock()
|
clientsMutex.RLock()
|
||||||
defer clientsMutex.RUnlock()
|
defer clientsMutex.RUnlock()
|
||||||
@ -284,7 +275,6 @@ func GetConnectionStatus() models.ConnectionStatus {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestWeatherData 获取最新气象站数据
|
|
||||||
func GetLatestWeatherData() *models.WeatherData {
|
func GetLatestWeatherData() *models.WeatherData {
|
||||||
dataMutex.RLock()
|
dataMutex.RLock()
|
||||||
defer dataMutex.RUnlock()
|
defer dataMutex.RUnlock()
|
||||||
@ -293,12 +283,10 @@ func GetLatestWeatherData() *models.WeatherData {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回一个副本
|
|
||||||
data := *latestWeatherData
|
data := *latestWeatherData
|
||||||
return &data
|
return &data
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestRainData 获取最新雨量计数据
|
|
||||||
func GetLatestRainData() *models.RainGaugeData {
|
func GetLatestRainData() *models.RainGaugeData {
|
||||||
dataMutex.RLock()
|
dataMutex.RLock()
|
||||||
defer dataMutex.RUnlock()
|
defer dataMutex.RUnlock()
|
||||||
@ -307,7 +295,6 @@ func GetLatestRainData() *models.RainGaugeData {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回一个副本
|
|
||||||
data := *latestRainData
|
data := *latestRainData
|
||||||
return &data
|
return &data
|
||||||
}
|
}
|
||||||
|
|||||||
225
models/em3395ty.go
Normal file
225
models/em3395ty.go
Normal 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 = ?
|
||||||
|
`
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WeatherData 气象站数据结构
|
|
||||||
type WeatherData struct {
|
type WeatherData struct {
|
||||||
ID int64 `json:"id" db:"id"`
|
ID int64 `json:"id" db:"id"`
|
||||||
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
||||||
@ -26,7 +25,6 @@ type WeatherData struct {
|
|||||||
SolarRadiation int `json:"solar_radiation" db:"solar_radiation"`
|
SolarRadiation int `json:"solar_radiation" db:"solar_radiation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RainGaugeData 雨量计数据结构
|
|
||||||
type RainGaugeData struct {
|
type RainGaugeData struct {
|
||||||
ID int64 `json:"id" db:"id"`
|
ID int64 `json:"id" db:"id"`
|
||||||
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
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"`
|
Min24hRainfallPeriod int `json:"min_24h_rainfall_period" db:"min_24h_rainfall_period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AggregatedData 聚合数据结构,用于前端展示
|
|
||||||
type AggregatedData struct {
|
type AggregatedData struct {
|
||||||
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
||||||
FormattedTime string `json:"formatted_time,omitempty"`
|
FormattedTime string `json:"formatted_time,omitempty"`
|
||||||
@ -54,7 +51,6 @@ type AggregatedData struct {
|
|||||||
SolarRadiation float64 `json:"solar_radiation" db:"solar_radiation"`
|
SolarRadiation float64 `json:"solar_radiation" db:"solar_radiation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionStatus 连接状态
|
|
||||||
type ConnectionStatus struct {
|
type ConnectionStatus struct {
|
||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
IP string `json:"ip,omitempty"`
|
IP string `json:"ip,omitempty"`
|
||||||
@ -62,7 +58,6 @@ type ConnectionStatus struct {
|
|||||||
Count int `json:"count,omitempty"`
|
Count int `json:"count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWeatherDataTable 创建气象站数据表SQL
|
|
||||||
const CreateWeatherDataTable = `
|
const CreateWeatherDataTable = `
|
||||||
CREATE TABLE IF NOT EXISTS weather_data (
|
CREATE TABLE IF NOT EXISTS weather_data (
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@ -87,7 +82,6 @@ CREATE TABLE IF NOT EXISTS weather_data (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
`
|
`
|
||||||
|
|
||||||
// CreateRainGaugeDataTable 创建雨量计数据表SQL
|
|
||||||
const CreateRainGaugeDataTable = `
|
const CreateRainGaugeDataTable = `
|
||||||
CREATE TABLE IF NOT EXISTS rain_gauge_data (
|
CREATE TABLE IF NOT EXISTS rain_gauge_data (
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@ -106,7 +100,6 @@ CREATE TABLE IF NOT EXISTS rain_gauge_data (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
`
|
`
|
||||||
|
|
||||||
// InsertWeatherDataSQL 插入气象站数据SQL
|
|
||||||
const InsertWeatherDataSQL = `
|
const InsertWeatherDataSQL = `
|
||||||
INSERT INTO weather_data (
|
INSERT INTO weather_data (
|
||||||
timestamp, wind_speed, wind_force, wind_direction_8, wind_direction_360,
|
timestamp, wind_speed, wind_force, wind_direction_8, wind_direction_360,
|
||||||
@ -117,7 +110,6 @@ INSERT INTO weather_data (
|
|||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
// InsertRainGaugeDataSQL 插入雨量计数据SQL
|
|
||||||
const InsertRainGaugeDataSQL = `
|
const InsertRainGaugeDataSQL = `
|
||||||
INSERT INTO rain_gauge_data (
|
INSERT INTO rain_gauge_data (
|
||||||
timestamp, daily_rainfall, instant_rainfall, yesterday_rainfall,
|
timestamp, daily_rainfall, instant_rainfall, yesterday_rainfall,
|
||||||
@ -128,35 +120,30 @@ INSERT INTO rain_gauge_data (
|
|||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
// QueryWeatherDataByTimeRangeSQL 按时间范围查询气象站数据SQL
|
|
||||||
const QueryWeatherDataByTimeRangeSQL = `
|
const QueryWeatherDataByTimeRangeSQL = `
|
||||||
SELECT * FROM weather_data
|
SELECT * FROM weather_data
|
||||||
WHERE timestamp BETWEEN ? AND ?
|
WHERE timestamp BETWEEN ? AND ?
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
// QueryRainGaugeDataByTimeRangeSQL 按时间范围查询雨量计数据SQL
|
|
||||||
const QueryRainGaugeDataByTimeRangeSQL = `
|
const QueryRainGaugeDataByTimeRangeSQL = `
|
||||||
SELECT * FROM rain_gauge_data
|
SELECT * FROM rain_gauge_data
|
||||||
WHERE timestamp BETWEEN ? AND ?
|
WHERE timestamp BETWEEN ? AND ?
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
// QueryLatestWeatherDataSQL 查询最新气象站数据SQL
|
|
||||||
const QueryLatestWeatherDataSQL = `
|
const QueryLatestWeatherDataSQL = `
|
||||||
SELECT * FROM weather_data
|
SELECT * FROM weather_data
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
// QueryLatestRainGaugeDataSQL 查询最新雨量计数据SQL
|
|
||||||
const QueryLatestRainGaugeDataSQL = `
|
const QueryLatestRainGaugeDataSQL = `
|
||||||
SELECT * FROM rain_gauge_data
|
SELECT * FROM rain_gauge_data
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
// QueryAggregatedDataSQL 查询聚合数据SQL (小时级别)
|
|
||||||
const QueryAggregatedDataSQL = `
|
const QueryAggregatedDataSQL = `
|
||||||
SELECT
|
SELECT
|
||||||
w.time_hour as timestamp,
|
w.time_hour as timestamp,
|
||||||
|
|||||||
@ -7,11 +7,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 任务配置
|
|
||||||
type TaskConfig struct {
|
type TaskConfig struct {
|
||||||
WeatherStationInterval time.Duration // 气象站查询间隔
|
WeatherStationInterval time.Duration
|
||||||
RainGaugeInterval time.Duration // 雨量计查询间隔
|
RainGaugeInterval time.Duration
|
||||||
Enabled bool // 是否启用定时查询
|
Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -20,17 +19,15 @@ var (
|
|||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 初始化默认配置
|
|
||||||
func init() {
|
func init() {
|
||||||
config = TaskConfig{
|
config = TaskConfig{
|
||||||
WeatherStationInterval: 15 * time.Minute, // 默认15分钟查询一次气象站
|
WeatherStationInterval: 15 * time.Minute,
|
||||||
RainGaugeInterval: time.Hour, // 默认每小时查询一次雨量计
|
RainGaugeInterval: time.Hour,
|
||||||
Enabled: true, // 默认启用
|
Enabled: true,
|
||||||
}
|
}
|
||||||
stopChan = make(chan struct{})
|
stopChan = make(chan struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartScheduler 启动定时任务调度器
|
|
||||||
func StartScheduler() {
|
func StartScheduler() {
|
||||||
if !config.Enabled {
|
if !config.Enabled {
|
||||||
log.Println("定时查询任务已禁用")
|
log.Println("定时查询任务已禁用")
|
||||||
@ -40,7 +37,6 @@ func StartScheduler() {
|
|||||||
log.Printf("启动定时查询任务,气象站间隔: %v, 雨量计整点查询",
|
log.Printf("启动定时查询任务,气象站间隔: %v, 雨量计整点查询",
|
||||||
config.WeatherStationInterval)
|
config.WeatherStationInterval)
|
||||||
|
|
||||||
// 启动气象站查询任务
|
|
||||||
weatherTick = time.NewTicker(config.WeatherStationInterval)
|
weatherTick = time.NewTicker(config.WeatherStationInterval)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@ -53,30 +49,25 @@ func StartScheduler() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 启动雨量计整点查询任务
|
|
||||||
go scheduleHourlyRainGaugeQuery()
|
go scheduleHourlyRainGaugeQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算到下一个整点的等待时间
|
|
||||||
func durationUntilNextHour() time.Duration {
|
func durationUntilNextHour() time.Duration {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nextHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 0, 0, 0, now.Location())
|
nextHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 0, 0, 0, now.Location())
|
||||||
return nextHour.Sub(now)
|
return nextHour.Sub(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整点查询雨量计任务
|
|
||||||
func scheduleHourlyRainGaugeQuery() {
|
func scheduleHourlyRainGaugeQuery() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// 计算到下一个整点的等待时间
|
|
||||||
waitTime := durationUntilNextHour()
|
waitTime := durationUntilNextHour()
|
||||||
log.Printf("下一次雨量计查询将在 %s 后进行 (整点: %s)",
|
log.Printf("下一次雨量计查询将在 %s 后进行 (整点: %s)",
|
||||||
waitTime.String(), time.Now().Add(waitTime).Format("15:04:05"))
|
waitTime.String(), time.Now().Add(waitTime).Format("15:04:05"))
|
||||||
|
|
||||||
// 等待到下一个整点
|
|
||||||
timer := time.NewTimer(waitTime)
|
timer := time.NewTimer(waitTime)
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
@ -89,7 +80,6 @@ func scheduleHourlyRainGaugeQuery() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopScheduler 停止定时任务调度器
|
|
||||||
func StopScheduler() {
|
func StopScheduler() {
|
||||||
if weatherTick != nil {
|
if weatherTick != nil {
|
||||||
weatherTick.Stop()
|
weatherTick.Stop()
|
||||||
@ -98,42 +88,33 @@ func StopScheduler() {
|
|||||||
log.Println("定时查询任务已停止")
|
log.Println("定时查询任务已停止")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTaskConfig 设置任务配置
|
|
||||||
func SetTaskConfig(newConfig TaskConfig) {
|
func SetTaskConfig(newConfig TaskConfig) {
|
||||||
// 先停止现有任务
|
|
||||||
StopScheduler()
|
StopScheduler()
|
||||||
|
|
||||||
// 更新配置
|
|
||||||
config = newConfig
|
config = newConfig
|
||||||
|
|
||||||
// 重新启动任务
|
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
StartScheduler()
|
StartScheduler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryWeatherStation 查询气象站并保存数据
|
|
||||||
func queryWeatherStation() {
|
func queryWeatherStation() {
|
||||||
log.Println("执行气象站查询任务")
|
log.Println("执行气象站查询任务")
|
||||||
|
|
||||||
// 发送查询命令
|
|
||||||
err := modbus.QueryDevice(modbus.DeviceWeatherStation)
|
err := modbus.QueryDevice(modbus.DeviceWeatherStation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("气象站查询失败: %v", err)
|
log.Printf("气象站查询失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待设备响应
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// 获取最新数据
|
|
||||||
weatherData := modbus.GetLatestWeatherData()
|
weatherData := modbus.GetLatestWeatherData()
|
||||||
if weatherData == nil {
|
if weatherData == nil {
|
||||||
log.Println("未获取到气象站数据")
|
log.Println("未获取到气象站数据")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
_, err = db.SaveWeatherData(weatherData)
|
_, err = db.SaveWeatherData(weatherData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("保存气象站数据失败: %v", err)
|
log.Printf("保存气象站数据失败: %v", err)
|
||||||
@ -144,28 +125,23 @@ func queryWeatherStation() {
|
|||||||
weatherData.Temperature, weatherData.Humidity)
|
weatherData.Temperature, weatherData.Humidity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryRainGauge 查询雨量计并保存数据
|
|
||||||
func queryRainGauge() {
|
func queryRainGauge() {
|
||||||
log.Println("执行雨量计查询任务 (整点)")
|
log.Println("执行雨量计查询任务 (整点)")
|
||||||
|
|
||||||
// 发送查询命令
|
|
||||||
err := modbus.QueryDevice(modbus.DeviceRainGauge)
|
err := modbus.QueryDevice(modbus.DeviceRainGauge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("雨量计查询失败: %v", err)
|
log.Printf("雨量计查询失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待设备响应
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// 获取最新数据
|
|
||||||
rainData := modbus.GetLatestRainData()
|
rainData := modbus.GetLatestRainData()
|
||||||
if rainData == nil {
|
if rainData == nil {
|
||||||
log.Println("未获取到雨量计数据")
|
log.Println("未获取到雨量计数据")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
_, err = db.SaveRainGaugeData(rainData)
|
_, err = db.SaveRainGaugeData(rainData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("保存雨量计数据失败: %v", err)
|
log.Printf("保存雨量计数据失败: %v", err)
|
||||||
|
|||||||
411
test-get.go.bak
Normal file
411
test-get.go.bak
Normal 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
98
todo.md
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
## 系统概述
|
## 系统概述
|
||||||
开发一个监测系统,通过ModBus-RTU协议与两种设备通信,收集并展示气象和雨量数据。
|
开发一个监测系统,通过ModBus-RTU协议与两种设备通信,收集并展示气象和雨量数据。
|
||||||
|
新增PWS01设备通过HTTP GET请求获取数据,提供更全面的气象要素。
|
||||||
|
|
||||||
## 系统架构
|
## 系统架构
|
||||||
- Web界面:监听10003端口
|
- Web界面:监听10003端口
|
||||||
- TCP服务器:监听10004端口,与设备通信
|
- TCP服务器:监听10004端口,与设备通信
|
||||||
- 两种客户端设备:地址码01(气象站)和02(雨量计)
|
- 两种客户端设备:地址码01(气象站)和02(雨量计)
|
||||||
|
- PWS01设备:通过HTTP GET请求通信
|
||||||
|
|
||||||
## 开发任务
|
## 开发任务
|
||||||
|
|
||||||
@ -20,6 +22,9 @@
|
|||||||
- 发送指令:`02030000000ac5fe`
|
- 发送指令:`02030000000ac5fe`
|
||||||
- 解析返回数据(当天降雨量、瞬时降雨量等)
|
- 解析返回数据(当天降雨量、瞬时降雨量等)
|
||||||
- [ ] 实现数据转换(根据设备寄存器定义)
|
- [ ] 实现数据转换(根据设备寄存器定义)
|
||||||
|
- [ ] 新增PWS01设备HTTP GET请求数据采集
|
||||||
|
- 创建专门的包处理HTTP GET请求
|
||||||
|
- 解析JSON响应数据
|
||||||
|
|
||||||
### 2. 数据存储模块
|
### 2. 数据存储模块
|
||||||
- [ ] 设计MySQL数据库表结构
|
- [ ] 设计MySQL数据库表结构
|
||||||
@ -27,6 +32,9 @@
|
|||||||
- 设备02数据表
|
- 设备02数据表
|
||||||
- [ ] 实现数据持久化存储
|
- [ ] 实现数据持久化存储
|
||||||
- [ ] 实现数据查询接口
|
- [ ] 实现数据查询接口
|
||||||
|
- [ ] 新增PWS01设备表结构
|
||||||
|
- 设备信息表:存储设备基本信息,以设备ID为主键
|
||||||
|
- 设备数据表:存储设备采集的气象数据,以设备ID为外键
|
||||||
|
|
||||||
### 3. Web服务器模块
|
### 3. Web服务器模块
|
||||||
- [ ] 实现Web服务器,监听10003端口
|
- [ ] 实现Web服务器,监听10003端口
|
||||||
@ -35,6 +43,10 @@
|
|||||||
- 查询历史数据(支持时间范围)
|
- 查询历史数据(支持时间范围)
|
||||||
- 数据聚合(按小时、天等)
|
- 数据聚合(按小时、天等)
|
||||||
- 触发设备查询
|
- 触发设备查询
|
||||||
|
- [ ] 新增PWS01设备API接口
|
||||||
|
- 获取PWS01设备信息
|
||||||
|
- 获取PWS01最新数据
|
||||||
|
- 查询PWS01历史数据
|
||||||
|
|
||||||
### 4. 前端界面
|
### 4. 前端界面
|
||||||
- [ ] 参考提供的HTML风格,实现Web界面
|
- [ ] 参考提供的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语言
|
- 后端:Go语言
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user