From 34b7bf3ff271240096786a5c8af918d3750d2c85 Mon Sep 17 00:00:00 2001 From: yarnom Date: Wed, 5 Nov 2025 16:47:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=86=85=E9=83=A8?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/cmd/core-api/main.go | 2 + core/cmd/core-mqtt/main.go | 2 - core/cmd/service-mqtt-publisher/main.go | 4 +- core/internal/data/db.go | 39 ++++ core/internal/data/forecast.go | 3 +- core/internal/data/raw.go | 7 +- core/internal/data/store.go | 275 +++++++++++++++++++++++- core/internal/server/radar_handlers.go | 8 +- core/internal/server/rain_handlers.go | 8 +- go.mod | 3 + go.sum | 6 + 11 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 core/internal/data/db.go diff --git a/core/cmd/core-api/main.go b/core/cmd/core-api/main.go index 420e2d6..e9a1698 100644 --- a/core/cmd/core-api/main.go +++ b/core/cmd/core-api/main.go @@ -5,11 +5,13 @@ import ( "os" "weatherstation/core/internal/config" + "weatherstation/core/internal/data" "weatherstation/core/internal/server" ) func main() { cfg := config.Load() + _ = data.DB() r := server.NewRouter(server.Options{ UIServeDir: cfg.UIServeDir, TemplateDir: cfg.TemplateDir, diff --git a/core/cmd/core-mqtt/main.go b/core/cmd/core-mqtt/main.go index ffd55d4..a319fbc 100644 --- a/core/cmd/core-mqtt/main.go +++ b/core/cmd/core-mqtt/main.go @@ -1,7 +1,6 @@ package main import ( - "crypto/tls" "encoding/json" "fmt" "os" @@ -26,7 +25,6 @@ func main() { opts.SetKeepAlive(60 * time.Second) opts.SetAutoReconnect(true) opts.SetCleanSession(true) - opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) c := mqtt.NewClient(opts) if t := c.Connect(); t.Wait() && t.Error() != nil { diff --git a/core/cmd/service-mqtt-publisher/main.go b/core/cmd/service-mqtt-publisher/main.go index 499fecd..f003664 100644 --- a/core/cmd/service-mqtt-publisher/main.go +++ b/core/cmd/service-mqtt-publisher/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "crypto/tls" "encoding/json" "fmt" "log" @@ -19,7 +18,7 @@ const ( deviceID = "Z866" stationID = "RS485-002A6E" - // MQTT 测试配置(可替换为生产) + // MQTT brokerURL = "wss://broker.emqx.io:8084/mqtt" clientID = "core-publisher-Z866" username = "1" @@ -49,7 +48,6 @@ func main() { opts.SetKeepAlive(60 * time.Second) opts.SetAutoReconnect(true) opts.SetCleanSession(true) - opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) cli := mqtt.NewClient(opts) if tok := cli.Connect(); tok.Wait() && tok.Error() != nil { diff --git a/core/internal/data/db.go b/core/internal/data/db.go new file mode 100644 index 0000000..78b1ca0 --- /dev/null +++ b/core/internal/data/db.go @@ -0,0 +1,39 @@ +package data + +import ( + "database/sql" + "fmt" + "sync" + + legacycfg "weatherstation/internal/config" + + _ "github.com/lib/pq" +) + +var ( + dbOnce sync.Once + dbInst *sql.DB +) + +// DB returns a shared Postgres connection opened using legacy internal/config. +func DB() *sql.DB { + dbOnce.Do(func() { + cfg := legacycfg.GetConfig().Database + connStr := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, + ) + d, err := sql.Open("postgres", connStr) + if err != nil { + panic(fmt.Errorf("open DB failed: %w", err)) + } + if err := d.Ping(); err != nil { + panic(fmt.Errorf("ping DB failed: %w", err)) + } + dbInst = d + }) + if dbInst == nil { + panic("database not initialized: check config and drivers") + } + return dbInst +} diff --git a/core/internal/data/forecast.go b/core/internal/data/forecast.go index 752a675..b567777 100644 --- a/core/internal/data/forecast.go +++ b/core/internal/data/forecast.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "time" - "weatherstation/internal/database" ) type PredictPoint struct { @@ -19,7 +18,7 @@ func ForecastRainAtIssued(ctx context.Context, stationID, provider string, issue FROM forecast_hourly WHERE station_id=$1 AND provider=$2 AND issued_at=$3 ORDER BY forecast_time ASC` - rows, err := database.GetDB().QueryContext(ctx, q, stationID, provider, issuedAt) + rows, err := DB().QueryContext(ctx, q, stationID, provider, issuedAt) if err != nil { return nil, err } diff --git a/core/internal/data/raw.go b/core/internal/data/raw.go index 60e0503..3c8b299 100644 --- a/core/internal/data/raw.go +++ b/core/internal/data/raw.go @@ -5,7 +5,6 @@ import ( "database/sql" "math" "time" - "weatherstation/internal/database" ) type WindowAgg struct { @@ -34,7 +33,7 @@ func WindowAverages(ctx context.Context, stationID string, start, end time.Time) FROM rs485_weather_data WHERE station_id = $1 AND timestamp >= $2 AND timestamp < $3` var agg WindowAgg - err := database.GetDB().QueryRowContext(ctx, q, stationID, start, end).Scan( + err := DB().QueryRowContext(ctx, q, stationID, start, end).Scan( &agg.Ta, &agg.Ua, &agg.Pa, &agg.Sm, &agg.Dm, ) if err != nil { @@ -68,11 +67,11 @@ func DailyRainSinceMidnight(ctx context.Context, stationID string, now time.Time var baseline sql.NullFloat64 const qBase = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 AND timestamp <= $2 ORDER BY timestamp DESC LIMIT 1` - _ = database.GetDB().QueryRowContext(ctx, qBase, stationID, dayStart).Scan(&baseline) + _ = DB().QueryRowContext(ctx, qBase, stationID, dayStart).Scan(&baseline) var current sql.NullFloat64 const qCur = `SELECT rainfall FROM rs485_weather_data WHERE station_id=$1 ORDER BY timestamp DESC LIMIT 1` - _ = database.GetDB().QueryRowContext(ctx, qCur, stationID).Scan(¤t) + _ = DB().QueryRowContext(ctx, qCur, stationID).Scan(¤t) if !current.Valid || !baseline.Valid { return 0, nil diff --git a/core/internal/data/store.go b/core/internal/data/store.go index e0cfe05..0cadce6 100644 --- a/core/internal/data/store.go +++ b/core/internal/data/store.go @@ -1,26 +1,283 @@ package data import ( - "database/sql" + "fmt" + "log" "time" - "weatherstation/internal/database" "weatherstation/pkg/types" ) -func DB() *sql.DB { return database.GetDB() } +func OnlineDevices() int { + const query = `SELECT COUNT(DISTINCT station_id) FROM rs485_weather_data WHERE timestamp > NOW() - INTERVAL '5 minutes'` + var count int + if err := DB().QueryRow(query).Scan(&count); err != nil { + return 0 + } + return count +} -func OnlineDevices() int { return database.GetOnlineDevicesCount(DB()) } - -func Stations() ([]types.Station, error) { return database.GetStations(DB()) } +func Stations() ([]types.Station, error) { + const query = ` + SELECT DISTINCT s.station_id, + COALESCE(s.station_alias, '') as station_alias, + COALESCE(s.password, '') as station_name, + 'WH65LP' as device_type, + COALESCE(MAX(r.timestamp), '1970-01-01'::timestamp) as last_update, + COALESCE(s.latitude, 0) as latitude, + COALESCE(s.longitude, 0) as longitude, + COALESCE(s.name, '') as name, + COALESCE(s.location, '') as location, + COALESCE(s.z, 0) as z, + COALESCE(s.y, 0) as y, + COALESCE(s.x, 0) as x + FROM stations s + LEFT JOIN rs485_weather_data r ON s.station_id = r.station_id + WHERE s.station_id LIKE 'RS485-%' + GROUP BY s.station_id, s.station_alias, s.password, s.latitude, s.longitude, s.name, s.location, s.z, s.y, s.x + ORDER BY s.station_id` + rows, err := DB().Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + var stations []types.Station + for rows.Next() { + var s types.Station + var last time.Time + if err := rows.Scan(&s.StationID, &s.StationAlias, &s.StationName, &s.DeviceType, &last, &s.Latitude, &s.Longitude, &s.Name, &s.Location, &s.Z, &s.Y, &s.X); err != nil { + continue + } + s.LastUpdate = last.Format("2006-01-02 15:04:05") + stations = append(stations, s) + } + return stations, nil +} func SeriesRaw(stationID string, start, end time.Time) ([]types.WeatherPoint, error) { - return database.GetSeriesRaw(DB(), stationID, start, end) + const query = ` + SELECT + to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') AS date_time, + COALESCE(temperature, 0) AS temperature, + COALESCE(humidity, 0) AS humidity, + COALESCE(pressure, 0) AS pressure, + COALESCE(wind_speed, 0) AS wind_speed, + COALESCE(wind_direction, 0) AS wind_direction, + COALESCE(rainfall, 0) AS rainfall, + COALESCE(light, 0) AS light, + COALESCE(uv, 0) AS uv, + COALESCE(rainfall, 0) AS rain_total + FROM rs485_weather_data + WHERE station_id = $1 AND timestamp >= $2 AND timestamp <= $3 + ORDER BY timestamp` + rows, err := DB().Query(query, stationID, start, end) + if err != nil { + return nil, err + } + defer rows.Close() + var points []types.WeatherPoint + for rows.Next() { + var p types.WeatherPoint + if err := rows.Scan(&p.DateTime, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.Light, &p.UV, &p.RainTotal); err != nil { + continue + } + points = append(points, p) + } + return points, nil } func SeriesFrom10Min(stationID string, start, end time.Time, interval string) ([]types.WeatherPoint, error) { - return database.GetSeriesFrom10Min(DB(), stationID, start, end, interval) + log.Printf("查询数据: stationID=%s, start=%v, end=%v, interval=%s", stationID, start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"), interval) + var query string + switch interval { + case "10min": + query = ` + SELECT + to_char(bucket_start + interval '10 minutes', 'YYYY-MM-DD HH24:MI:SS') AS date_time, + ROUND(temp_c_x100/100.0, 2) AS temperature, + ROUND(humidity_pct::numeric, 2) AS humidity, + ROUND(pressure_hpa_x100/100.0, 2) AS pressure, + ROUND(wind_speed_ms_x1000/1000.0, 3) AS wind_speed, + ROUND(wind_dir_deg::numeric, 2) AS wind_direction, + ROUND(rain_10m_mm_x1000/1000.0, 3) AS rainfall, + ROUND(solar_wm2_x100/100.0, 2) AS light, + ROUND(uv_index::numeric, 2) AS uv, + ROUND(rain_total_mm_x1000/1000.0, 3) AS rain_total + FROM rs485_weather_10min + WHERE station_id = $1 AND bucket_start >= $2 AND bucket_start <= $3 + ORDER BY bucket_start + interval '10 minutes'` + case "30min": + query = buildAggFrom10MinQuery("30 minutes") + default: + query = buildAggFrom10MinQuery("1 hour") + } + rows, err := DB().Query(query, stationID, start, end) + if err != nil { + log.Printf("查询失败: %v", err) + return nil, err + } + defer rows.Close() + var points []types.WeatherPoint + for rows.Next() { + var p types.WeatherPoint + if err := rows.Scan(&p.DateTime, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.Light, &p.UV, &p.RainTotal); err != nil { + continue + } + points = append(points, p) + } + return points, nil +} + +func buildAggFrom10MinQuery(interval string) string { + return ` + WITH base AS ( + SELECT * FROM rs485_weather_10min + WHERE station_id = $1 AND bucket_start >= $2 AND bucket_start <= $3 + ), g AS ( + SELECT + CASE '` + interval + `' + WHEN '1 hour' THEN date_trunc('hour', bucket_start) + WHEN '30 minutes' THEN + date_trunc('hour', bucket_start) + + CASE WHEN date_part('minute', bucket_start) >= 30 + THEN '30 minutes'::interval + ELSE '0 minutes'::interval + END + END AS grp, + SUM(temp_c_x100 * sample_count)::bigint AS w_temp, + SUM(humidity_pct * sample_count)::bigint AS w_hum, + SUM(pressure_hpa_x100 * sample_count)::bigint AS w_p, + SUM(solar_wm2_x100 * sample_count)::bigint AS w_solar, + SUM(uv_index * sample_count)::bigint AS w_uv, + SUM(wind_speed_ms_x1000 * sample_count)::bigint AS w_ws, + MAX(wind_gust_ms_x1000) AS gust_max, + SUM(sin(radians(wind_dir_deg)) * sample_count)::double precision AS sin_sum, + SUM(cos(radians(wind_dir_deg)) * sample_count)::double precision AS cos_sum, + SUM(rain_10m_mm_x1000) AS rain_sum, + SUM(sample_count) AS n_sum, + MAX(rain_total_mm_x1000) AS rain_total_max + FROM base + GROUP BY 1 + ) + SELECT + to_char(grp + '` + interval + `'::interval, 'YYYY-MM-DD HH24:MI:SS') AS date_time, + ROUND((w_temp/NULLIF(n_sum,0))/100.0, 2) AS temperature, + ROUND((w_hum/NULLIF(n_sum,0))::numeric, 2) AS humidity, + ROUND((w_p/NULLIF(n_sum,0))/100.0, 2) AS pressure, + ROUND((w_ws/NULLIF(n_sum,0))/1000.0, 3) AS wind_speed, + ROUND((CASE WHEN degrees(atan2(sin_sum, cos_sum)) < 0 + THEN degrees(atan2(sin_sum, cos_sum)) + 360 + ELSE degrees(atan2(sin_sum, cos_sum)) END)::numeric, 2) AS wind_direction, + ROUND((rain_sum/1000.0)::numeric, 3) AS rainfall, + ROUND((w_solar/NULLIF(n_sum,0))/100.0, 2) AS light, + ROUND((w_uv/NULLIF(n_sum,0))::numeric, 2) AS uv, + ROUND((rain_total_max/1000.0)::numeric, 3) AS rain_total + FROM g + ORDER BY grp + '` + interval + `'::interval` } func Forecast(stationID string, start, end time.Time, provider string, versions int) ([]types.ForecastPoint, error) { - return database.GetForecastData(DB(), stationID, start, end, provider, versions) + var query string + var args []interface{} + if versions <= 0 { + versions = 1 + } + if provider != "" { + if provider == "open-meteo" { + query = ` + WITH ranked AS ( + SELECT station_id, provider, issued_at, forecast_time, + temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000, + wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100, + ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn, + CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours + FROM forecast_hourly + WHERE station_id = $1 AND provider IN ('open-meteo','open-meteo_historical') + AND forecast_time BETWEEN $2 AND $3 + ) + SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time, + provider, + to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at, + ROUND(temp_c_x100::numeric / 100.0, 2) as temperature, + humidity_pct as humidity, + ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure, + ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed, + wind_dir_deg as wind_direction, + ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall, + precip_prob_pct as precip_prob, + uv_index as uv, + lead_hours + FROM ranked WHERE rn <= $4 + ORDER BY forecast_time, issued_at DESC` + args = []interface{}{stationID, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions} + } else { + query = ` + WITH ranked AS ( + SELECT station_id, provider, issued_at, forecast_time, + temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000, + wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100, + ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn, + CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours + FROM forecast_hourly + WHERE station_id = $1 AND provider = $2 + AND forecast_time BETWEEN $3 AND $4 + ) + SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time, + provider, + to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at, + ROUND(temp_c_x100::numeric / 100.0, 2) as temperature, + humidity_pct as humidity, + ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure, + ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed, + wind_dir_deg as wind_direction, + ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall, + precip_prob_pct as precip_prob, + uv_index as uv, + lead_hours + FROM ranked WHERE rn <= $5 + ORDER BY forecast_time, issued_at DESC` + args = []interface{}{stationID, provider, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions} + } + } else { + query = ` + WITH ranked AS ( + SELECT station_id, provider, issued_at, forecast_time, + temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000, + wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100, + ROW_NUMBER() OVER (PARTITION BY provider, forecast_time ORDER BY issued_at DESC) AS rn, + CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours + FROM forecast_hourly + WHERE station_id = $1 AND forecast_time BETWEEN $2 AND $3 + ) + SELECT to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time, + provider, + to_char(issued_at, 'YYYY-MM-DD HH24:MI:SS') as issued_at, + ROUND(temp_c_x100::numeric / 100.0, 2) as temperature, + humidity_pct as humidity, + ROUND(pressure_hpa_x100::numeric / 100.0, 2) as pressure, + ROUND(wind_speed_ms_x1000::numeric / 1000.0, 2) as wind_speed, + wind_dir_deg as wind_direction, + ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall, + precip_prob_pct as precip_prob, + uv_index as uv, + lead_hours + FROM ranked WHERE rn <= $4 + ORDER BY forecast_time, provider, issued_at DESC` + args = []interface{}{stationID, start.Format("2006-01-02 15:04:05-07"), end.Format("2006-01-02 15:04:05-07"), versions} + } + rows, err := DB().Query(query, args...) + if err != nil { + return nil, fmt.Errorf("查询预报数据失败: %v", err) + } + defer rows.Close() + var points []types.ForecastPoint + for rows.Next() { + var p types.ForecastPoint + if err := rows.Scan(&p.DateTime, &p.Provider, &p.IssuedAt, &p.Temperature, &p.Humidity, &p.Pressure, &p.WindSpeed, &p.WindDir, &p.Rainfall, &p.PrecipProb, &p.UV, &p.LeadHours); err != nil { + log.Printf("数据扫描错误: %v", err) + continue + } + p.Source = "forecast" + points = append(points, p) + } + return points, nil } diff --git a/core/internal/server/radar_handlers.go b/core/internal/server/radar_handlers.go index c7cd7ad..3789215 100644 --- a/core/internal/server/radar_handlers.go +++ b/core/internal/server/radar_handlers.go @@ -5,7 +5,7 @@ import ( "encoding/binary" "net/http" "time" - "weatherstation/internal/database" + "weatherstation/core/internal/data" "github.com/gin-gonic/gin" ) @@ -61,11 +61,11 @@ func handleRadarTimes(c *gin.Context) { return } const qRange = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC` - rows, err = database.GetDB().Query(qRange, z, y, x, from, to) + rows, err = data.DB().Query(qRange, z, y, x, from, to) } else { limit := parseInt(c.Query("limit"), 48) const q = `SELECT dt FROM radar_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4` - rows, err = database.GetDB().Query(q, z, y, x, limit) + rows, err = data.DB().Query(q, z, y, x, limit) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) @@ -100,7 +100,7 @@ func handleRadarTilesAt(c *gin.Context) { } const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM radar_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x` - rows, qerr := database.GetDB().Query(q, z, dt) + rows, qerr := data.DB().Query(q, z, dt) if qerr != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"}) return diff --git a/core/internal/server/rain_handlers.go b/core/internal/server/rain_handlers.go index 10a7548..14915fa 100644 --- a/core/internal/server/rain_handlers.go +++ b/core/internal/server/rain_handlers.go @@ -5,7 +5,7 @@ import ( "encoding/binary" "net/http" "time" - "weatherstation/internal/database" + "weatherstation/core/internal/data" "github.com/gin-gonic/gin" ) @@ -61,11 +61,11 @@ func handleRainTimes(c *gin.Context) { return } const qRange = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC` - rows, err = database.GetDB().Query(qRange, z, y, x, from, to) + rows, err = data.DB().Query(qRange, z, y, x, from, to) } else { limit := parseInt(c.Query("limit"), 48) const q = `SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4` - rows, err = database.GetDB().Query(q, z, y, x, limit) + rows, err = data.DB().Query(q, z, y, x, limit) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) @@ -100,7 +100,7 @@ func handleRainTilesAt(c *gin.Context) { } const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM rain_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x` - rows, qerr := database.GetDB().Query(q, z, dt) + rows, qerr := data.DB().Query(q, z, dt) if qerr != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"}) return diff --git a/go.mod b/go.mod index 797a897..319dacd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.24.5 require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/gin-gonic/gin v1.10.1 github.com/go-sql-driver/mysql v1.8.1 github.com/lib/pq v1.10.9 @@ -23,6 +24,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -35,6 +37,7 @@ require ( golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/go.sum b/go.sum index f87f65d..126dee8 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -32,6 +34,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -76,6 +80,8 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=