weather-station/model/weather_data.go

495 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
type WeatherData struct {
StationID string
Password string
TempF float64
Humidity int
DewpointF float64
WindchillF float64
WindDir int
WindSpeedMph float64
WindGustMph float64
RainIn float64
DailyRainIn float64
WeeklyRainIn float64
MonthlyRainIn float64
YearlyRainIn float64
TotalRainIn float64
SolarRadiation float64
UV int
IndoorTempF float64
IndoorHumidity int
AbsBarometerIn float64
BarometerIn float64
LowBattery bool
SoftwareType string
DateUTC string
Action string
RealTime int
RTFreq int
}
func ParseWeatherData(data string) (*WeatherData, error) {
if !strings.Contains(data, "GET /weatherstation/updateweatherstation.php") {
return nil, fmt.Errorf("不是气象站数据")
}
urlStart := strings.Index(data, "GET ")
if urlStart == -1 {
return nil, fmt.Errorf("无法找到URL开始位置")
}
httpVersionEnd := strings.Index(data, " HTTP")
if httpVersionEnd == -1 {
return nil, fmt.Errorf("无法找到URL结束位置")
}
urlString := data[urlStart+4 : httpVersionEnd]
queryStart := strings.Index(urlString, "?")
if queryStart == -1 {
return nil, fmt.Errorf("无法找到查询参数")
}
queryString := urlString[queryStart+1:]
values, err := url.ParseQuery(queryString)
if err != nil {
return nil, fmt.Errorf("解析查询参数失败: %v", err)
}
wd := &WeatherData{}
wd.StationID = values.Get("ID")
wd.Password = values.Get("PASSWORD")
wd.DateUTC = values.Get("dateutc")
wd.SoftwareType = values.Get("softwaretype")
wd.Action = values.Get("action")
if tempF, err := strconv.ParseFloat(values.Get("tempf"), 64); err == nil {
wd.TempF = tempF
}
if humidity, err := strconv.Atoi(values.Get("humidity")); err == nil {
wd.Humidity = humidity
}
if dewpointF, err := strconv.ParseFloat(values.Get("dewptf"), 64); err == nil {
wd.DewpointF = dewpointF
}
if windchillF, err := strconv.ParseFloat(values.Get("windchillf"), 64); err == nil {
wd.WindchillF = windchillF
}
if windDir, err := strconv.Atoi(values.Get("winddir")); err == nil {
wd.WindDir = windDir
}
if windSpeedMph, err := strconv.ParseFloat(values.Get("windspeedmph"), 64); err == nil {
wd.WindSpeedMph = windSpeedMph
}
if windGustMph, err := strconv.ParseFloat(values.Get("windgustmph"), 64); err == nil {
wd.WindGustMph = windGustMph
}
if rainIn, err := strconv.ParseFloat(values.Get("rainin"), 64); err == nil {
wd.RainIn = rainIn
}
if dailyRainIn, err := strconv.ParseFloat(values.Get("dailyrainin"), 64); err == nil {
wd.DailyRainIn = dailyRainIn
}
if weeklyRainIn, err := strconv.ParseFloat(values.Get("weeklyrainin"), 64); err == nil {
wd.WeeklyRainIn = weeklyRainIn
}
if monthlyRainIn, err := strconv.ParseFloat(values.Get("monthlyrainin"), 64); err == nil {
wd.MonthlyRainIn = monthlyRainIn
}
if yearlyRainIn, err := strconv.ParseFloat(values.Get("yearlyrainin"), 64); err == nil {
wd.YearlyRainIn = yearlyRainIn
}
if totalRainIn, err := strconv.ParseFloat(values.Get("totalrainin"), 64); err == nil {
wd.TotalRainIn = totalRainIn
}
if solarRadiation, err := strconv.ParseFloat(values.Get("solarradiation"), 64); err == nil {
wd.SolarRadiation = solarRadiation
}
if uv, err := strconv.Atoi(values.Get("UV")); err == nil {
wd.UV = uv
}
if indoorTempF, err := strconv.ParseFloat(values.Get("indoortempf"), 64); err == nil {
wd.IndoorTempF = indoorTempF
}
if indoorHumidity, err := strconv.Atoi(values.Get("indoorhumidity")); err == nil {
wd.IndoorHumidity = indoorHumidity
}
if absBarometerIn, err := strconv.ParseFloat(values.Get("absbaromin"), 64); err == nil {
wd.AbsBarometerIn = absBarometerIn
}
if barometerIn, err := strconv.ParseFloat(values.Get("baromin"), 64); err == nil {
wd.BarometerIn = barometerIn
}
if lowBatt, err := strconv.Atoi(values.Get("lowbatt")); err == nil {
wd.LowBattery = lowBatt != 0
}
if realTime, err := strconv.Atoi(values.Get("realtime")); err == nil {
wd.RealTime = realTime
}
if rtFreq, err := strconv.Atoi(values.Get("rtfreq")); err == nil {
wd.RTFreq = rtFreq
}
return wd, nil
}
func (w *WeatherData) String() string {
return fmt.Sprintf(`
站点ID: %s
温度: %.1f°F (%.1f°C)
湿度: %d%%
露点: %.1f°F (%.1f°C)
风寒指数: %.1f°F (%.1f°C)
风向: %d°
风速: %.2f mph (%.2f km/h)
阵风: %.2f mph (%.2f km/h)
降雨量: %.3f 英寸/小时 (%.2f mm/h) - 当前降雨率
日降雨量: %.3f 英寸 (%.2f mm) - 过去24小时累计
周降雨量: %.3f 英寸 (%.2f mm) - 本周累计
月降雨量: %.3f 英寸 (%.2f mm) - 本月累计
年降雨量: %.3f 英寸 (%.2f mm) - 本年累计
总降雨量: %.3f 英寸 (%.2f mm) - 自设备安装以来累计
太阳辐射: %.2f W/m²
紫外线指数: %d
室内温度: %.1f°F (%.1f°C)
室内湿度: %d%%
绝对气压: %.3f 英寸汞柱 (%.2f hPa)
相对气压: %.3f 英寸汞柱 (%.2f hPa)
低电量: %v
软件类型: %s
日期UTC: %s
`,
w.StationID,
w.TempF, (w.TempF-32)*5/9,
w.Humidity,
w.DewpointF, (w.DewpointF-32)*5/9,
w.WindchillF, (w.WindchillF-32)*5/9,
w.WindDir,
w.WindSpeedMph, w.WindSpeedMph*1.60934,
w.WindGustMph, w.WindGustMph*1.60934,
w.RainIn, w.RainIn*25.4,
w.DailyRainIn, w.DailyRainIn*25.4,
w.WeeklyRainIn, w.WeeklyRainIn*25.4,
w.MonthlyRainIn, w.MonthlyRainIn*25.4,
w.YearlyRainIn, w.YearlyRainIn*25.4,
w.TotalRainIn, w.TotalRainIn*25.4,
w.SolarRadiation,
w.UV,
w.IndoorTempF, (w.IndoorTempF-32)*5/9,
w.IndoorHumidity,
w.AbsBarometerIn, w.AbsBarometerIn*33.8639,
w.BarometerIn, w.BarometerIn*33.8639,
w.LowBattery,
w.SoftwareType,
w.DateUTC,
)
}
type WH65LPData struct {
StationID string // 设备ID (24位)
Timestamp time.Time // 数据时间戳
FamilyCode byte // 家族码 (0x24)
Temperature float64 // 温度 (°C)
Humidity int // 湿度 (%)
WindDirection int // 风向 (0-359°)
WindSpeed float64 // 风速 (m/s)
WindGust float64 // 阵风 (m/s)
Rain float64 // 降雨量 (mm)
UV int // 紫外线指数 (0-13)
Light float64 // 光照 (lux)
Pressure float64 // 气压 (hPa)
LowBattery bool // 低电量标志
WSPFlag bool // 风速标志位
}
// ParseWH65LPData 解析WH65LP设备的25字节数据
func ParseWH65LPData(data []byte) (*WH65LPData, error) {
if len(data) != 25 {
return nil, fmt.Errorf("数据长度错误期望25字节实际%d字节", len(data))
}
wd := &WH65LPData{
Timestamp: time.Now(),
}
// 1. 解析家族码 (第1字节bits 0-7)
wd.FamilyCode = data[0]
if wd.FamilyCode != 0x24 {
return nil, fmt.Errorf("无效的家族码0x%02X", wd.FamilyCode)
}
// 2. 解析设备ID (第2字节 + 第22-23字节)
idLSB := data[1]
idMSB := uint32(data[22]) | uint32(data[21])<<8
wd.StationID = fmt.Sprintf("%06X", (idMSB<<8)|uint32(idLSB))
// 3. 解析风向 (bits 16-24)
// 第3字节完整8位 + 第4字节的最低位
windDir := uint16(data[2]) | ((uint16(data[3]) & 0x01) << 8)
wd.WindDirection = int(windDir) % 360
// 4. 风速标志和温度
// WSP_FLAG 在第4字节的第2位 (bit 25)
wd.WSPFlag = (data[3]>>1)&0x01 == 1
// 低电量标志在第4字节的第4位 (bit 28)
wd.LowBattery = (data[3]>>3)&0x01 == 1
// 5. 温度 (bits 28-39)
// 从第4字节的高4位和第5字节的低3位组合出11位温度数据
tempRaw := ((uint16(data[3]) >> 4) & 0x07) | (uint16(data[4]) << 3)
wd.Temperature = float64(tempRaw-400) / 10.0
// 6. 湿度 (bits 40-47)
// 第5字节的高5位
wd.Humidity = int(data[4] >> 3)
// 7. 风速 (bits 48-55 + WSP_9,WSP_8)
windSpeedRaw := uint16(data[6])
if !wd.WSPFlag {
// 10位风速从第4字节的bits 6-7获取高2位
windSpeedRaw |= uint16((data[3]>>6)&0x03) << 8
}
wd.WindSpeed = float64(windSpeedRaw) / 8.0 * 0.51
// 8. 阵风 (bits 56-63)
wd.WindGust = float64(data[7]) * 0.51
// 9. 降雨量 (bits 64-79)
rainRaw := uint16(data[8]) | uint16(data[9])<<8
wd.Rain = float64(rainRaw) * 0.254
// 10. 紫外线 (bits 80-95)
uvRaw := uint16(data[10]) | uint16(data[11])<<8
wd.UV = getUVIndex(uvRaw)
// 11. 光照 (bits 96-119)
lightRaw := uint32(data[12]) | uint32(data[13])<<8 | uint32(data[14])<<16
wd.Light = float64(lightRaw) / 10.0
// 12. 气压 (bits 136-159)
// 从第18-20字节提取17位气压数据
pressureRaw := uint32(data[17]) | uint32(data[18])<<8 | uint32(data[19])<<16
pressureRaw &= 0x1FFFF
if pressureRaw == 0x1FFFF {
wd.Pressure = 0 // 无效值
} else {
wd.Pressure = float64(pressureRaw) / 100.0
}
// 13. 验证校验和
if !IsWH65LPData(data) {
return nil, fmt.Errorf("数据校验失败")
}
return wd, nil
}
// 调试辅助函数
func getBits(data []byte, startBit, length int) uint32 {
var result uint32
startByte := startBit / 8
startBitInByte := startBit % 8
// 从起始字节开始读取
result = uint32(data[startByte]) >> startBitInByte
bitsGot := 8 - startBitInByte
// 如果需要更多位,继续读取后续字节
for bitsGot < length {
startByte++
result |= uint32(data[startByte]) << bitsGot
bitsGot += 8
}
// 只保留需要的位数
result &= (1 << length) - 1
return result
}
// getUVIndex 根据UV原始值获取UV指数
func getUVIndex(uvRaw uint16) int {
switch {
case uvRaw <= 432:
return 0
case uvRaw <= 851:
return 1
case uvRaw <= 1210:
return 2
case uvRaw <= 1570:
return 3
case uvRaw <= 2017:
return 4
case uvRaw <= 2450:
return 5
case uvRaw <= 2761:
return 6
case uvRaw <= 3100:
return 7
case uvRaw <= 3512:
return 8
case uvRaw <= 3918:
return 9
case uvRaw <= 4277:
return 10
case uvRaw <= 4650:
return 11
case uvRaw <= 5029:
return 12
default:
return 13
}
}
// String 格式化输出WH65LP数据
func (w *WH65LPData) String() string {
return fmt.Sprintf(`
设备ID: %s
温度: %.1f°C
湿度: %d%%
风向: %d°
风速: %.2f m/s
阵风: %.2f m/s
降雨量: %.3f mm
UV指数: %d
光照: %.1f lux
气压: %.2f hPa
电池状态: %v
`,
w.StationID,
w.Temperature,
w.Humidity,
w.WindDirection,
w.WindSpeed,
w.WindGust,
w.Rain,
w.UV,
w.Light,
w.Pressure,
w.LowBattery,
)
}
// IsWH65LPData 检查数据是否是WH65LP的数据
func IsWH65LPData(data []byte) bool {
// 1. 检查数据长度
if len(data) != 25 {
return false
}
// 2. 检查家族码
if data[0] != 0x24 {
return false
}
// 3. 验证CRC (第16字节前15字节的CRC8)
if !ValidateCRC8(data[:15], data[15]) {
return false
}
// 4. 验证气压校验和 (第21字节)
pressureSum := uint8(data[17] + data[18] + data[19])
if pressureSum != data[20] {
return false
}
// 5. 验证CRC2 (第24字节前23字节的CRC8)
if !ValidateCRC8(data[:23], data[23]) {
return false
}
// 6. 验证最终校验和 (第25字节)
var sum uint8
for i := 0; i < 24; i++ {
sum += data[i]
}
if sum != data[24] {
return false
}
return true
}
// ValidateCRC8 验证CRC8校验
// 多项式0x31初始值0x00
func ValidateCRC8(data []byte, crc uint8) bool {
return crc == CalculateCRC8(data)
}
// CalculateCRC8 计算CRC8
func CalculateCRC8(data []byte) uint8 {
var crc uint8
for _, b := range data {
crc ^= b
for i := 0; i < 8; i++ {
if crc&0x80 != 0 {
crc = (crc << 1) ^ 0x31
} else {
crc <<= 1
}
}
}
return crc
}
// ParseUDP10006Data 解析UDP 10006端口的数据
func ParseUDP10006Data(data []byte) (*WH65LPData, error) {
// 1. 验证数据格式
if !IsWH65LPData(data) {
return nil, fmt.Errorf("无效的WH65LP数据格式")
}
// 2. 解析数据
return ParseWH65LPData(data)
}
// ConvertToMetric 将英制单位转换为公制单位
func ConvertToMetric(data *WeatherData) *WH65LPData {
// 用于将旧格式数据转换为WH65LP格式
return &WH65LPData{
StationID: data.StationID,
Timestamp: time.Now(),
Temperature: (data.TempF - 32) * 5 / 9,
Humidity: data.Humidity,
WindDirection: data.WindDir,
WindSpeed: data.WindSpeedMph * 0.44704, // mph转m/s
WindGust: data.WindGustMph * 0.44704, // mph转m/s
Rain: data.RainIn * 25.4, // inch转mm
UV: data.UV,
Light: data.SolarRadiation * 126.7, // W/m²转lux (近似转换)
Pressure: data.BarometerIn * 33.8639, // inHg转hPa
LowBattery: data.LowBattery,
}
}