From 386c8dc6bc221e7733154024ac626464a85aca7a Mon Sep 17 00:00:00 2001 From: biosvos Date: Thu, 4 Dec 2025 18:37:46 +0900 Subject: [PATCH] implement openweather main --- .env.template | 1 + .gitignore | 1 + cmd/openweather/main.go | 107 ++ go.mod | 8 + go.sum | 27 +- internal/pkg/openweather/forecaster.go | 43 + internal/pkg/openweather/forecaster_test.go | 24 + internal/pkg/openweather/model.go | 70 + .../pkg/openweather/testdata/forecast.json | 1472 +++++++++++++++++ 9 files changed, 1752 insertions(+), 1 deletion(-) create mode 100644 .env.template create mode 100644 cmd/openweather/main.go create mode 100644 internal/pkg/openweather/forecaster.go create mode 100644 internal/pkg/openweather/forecaster_test.go create mode 100644 internal/pkg/openweather/model.go create mode 100644 internal/pkg/openweather/testdata/forecast.json diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..281c2db --- /dev/null +++ b/.env.template @@ -0,0 +1 @@ +OPENWEATHER_API_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 047bcf4..91fcbcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/ /seven-skies /.vscode/launch.json +/.env \ No newline at end of file diff --git a/cmd/openweather/main.go b/cmd/openweather/main.go new file mode 100644 index 0000000..f93b2c4 --- /dev/null +++ b/cmd/openweather/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "log" + "os" + "runtime/debug" + "strconv" + + "github.com/go-co-op/gocron/v2" + "github.com/joho/godotenv" + "github.com/neatflowcv/seven-skies/internal/pkg/openweather" +) + +func version() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "unknown" + } + + return info.Main.Version +} + +func main() { + godotenv.Load() + + key := os.Getenv("OPENWEATHER_API_KEY") + if key == "" { + log.Panic("OPENWEATHER_API_KEY is not set") + } + + lat := os.Getenv("OPENWEATHER_LAT") + if lat == "" { + log.Panic("OPENWEATHER_LAT is not set") + } + + lon := os.Getenv("OPENWEATHER_LON") + if lon == "" { + log.Panic("OPENWEATHER_LON is not set") + } + + log.Println("version", version()) + + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Panic("error in create scheduler", err) + } + + defer func() { + err := scheduler.Shutdown() + if err != nil { + log.Println("error in shutdown scheduler", err) + } + }() + + latFloat, err := strconv.ParseFloat(lat, 64) + if err != nil { + log.Panic("error in parse lat", err) + } + + lonFloat, err := strconv.ParseFloat(lon, 64) + if err != nil { + log.Panic("error in parse lon", err) + } + + err = Task(key, latFloat, lonFloat) + if err != nil { + log.Panic("error in task", err) + } + + job, err := scheduler.NewJob( + gocron.CronJob("5 */2 * * *", false), // openweather에서 2시간마다 데이터가 업데이트 된다. + // 5분을 준 이유는 업데이트가 바로 된다는 보장이 없어서 + gocron.NewTask( + Task, + key, + latFloat, + lonFloat, + ), + ) + if err != nil { + log.Panic("error in create job", err) + } + + scheduler.Start() + + nextRun, err := job.NextRun() + if err != nil { + log.Panic("error in get next run", err) + } + log.Println("next run", nextRun) + + select {} +} + +func Task(key string, lat float64, lon float64) error { + log.Println("get forecast") + forecast, err := openweather.Forecast(context.Background(), key, lat, lon) + if err != nil { + log.Println("error in get forecast", err) + return err + } + + log.Println("get forecast successfully", forecast) + + return nil +} diff --git a/go.mod b/go.mod index 0bc9d00..0cc0b41 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,25 @@ module github.com/neatflowcv/seven-skies go 1.25.5 require ( + github.com/go-co-op/gocron/v2 v2.18.2 + github.com/joho/godotenv v1.5.1 github.com/nats-io/nats.go v1.47.0 github.com/stretchr/testify v1.11.1 + resty.dev/v3 v3.0.0-beta.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/klauspost/compress v1.18.2 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/nats-io/nkeys v0.4.12 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 26ce701..5da393a 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,47 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/go-co-op/gocron/v2 v2.18.2 h1:+5VU41FUXPWSPKLXZQ/77SGzUiPCcakU0v7ENc2H20Q= +github.com/go-co-op/gocron/v2 v2.18.2/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= +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= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +resty.dev/v3 v3.0.0-beta.4 h1:2O77oFymtA4NT8AY87wAaSgSGUBk2yvvM1qno9VRXZU= +resty.dev/v3 v3.0.0-beta.4/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= diff --git a/internal/pkg/openweather/forecaster.go b/internal/pkg/openweather/forecaster.go new file mode 100644 index 0000000..d693c57 --- /dev/null +++ b/internal/pkg/openweather/forecaster.go @@ -0,0 +1,43 @@ +package openweather + +import ( + "context" + "errors" + "fmt" + "log" + + "resty.dev/v3" +) + +var ( + ErrForecast = errors.New("error in get forecast") +) + +func Forecast(ctx context.Context, key string, lat float64, lon float64) (*ForecastResponse, error) { + client := resty.New() + + defer func() { + err := client.Close() + if err != nil { + log.Println("error in close client", err) + } + }() + + resp, err := client.R(). + SetContext(ctx). + SetQueryParam("lat", fmt.Sprintf("%f", lat)). + SetQueryParam("lon", fmt.Sprintf("%f", lon)). + SetQueryParam("appid", key). + SetQueryParam("units", "metric"). + SetResult(&ForecastResponse{}). //nolint:exhaustruct + Get("https://api.openweathermap.org/data/2.5/forecast") + if err != nil { + return nil, fmt.Errorf("error in get forecast: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("%w: %s, %s", ErrForecast, resp.Status(), resp.String()) + } + + return resp.Result().(*ForecastResponse), nil //nolint:forcetypeassert +} diff --git a/internal/pkg/openweather/forecaster_test.go b/internal/pkg/openweather/forecaster_test.go new file mode 100644 index 0000000..8d32537 --- /dev/null +++ b/internal/pkg/openweather/forecaster_test.go @@ -0,0 +1,24 @@ +package openweather_test + +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/neatflowcv/seven-skies/internal/pkg/openweather" + "github.com/stretchr/testify/require" +) + +//go:embed testdata/forecast.json +var forecast []byte + +func TestForecast(t *testing.T) { + t.Parallel() + + var resp openweather.ForecastResponse + + err := json.Unmarshal(forecast, &resp) + + require.NoError(t, err) + require.NotNil(t, forecast) +} diff --git a/internal/pkg/openweather/model.go b/internal/pkg/openweather/model.go new file mode 100644 index 0000000..4edffaf --- /dev/null +++ b/internal/pkg/openweather/model.go @@ -0,0 +1,70 @@ +package openweather + +type ForecastResponse struct { + Cod string `json:"cod,omitempty"` + Message int `json:"message,omitempty"` + Cnt int `json:"cnt,omitempty"` + List []List `json:"list,omitempty"` + City City `json:"city,omitzero"` +} + +type Main struct { + Temp float64 `json:"temp,omitempty"` + FeelsLike float64 `json:"feels_like,omitempty"` + TempMin float64 `json:"temp_min,omitempty"` + TempMax float64 `json:"temp_max,omitempty"` + Pressure int `json:"pressure,omitempty"` + SeaLevel int `json:"sea_level,omitempty"` + GrndLevel int `json:"grnd_level,omitempty"` + Humidity int `json:"humidity,omitempty"` + TempKf float64 `json:"temp_kf,omitempty"` +} + +type Weather struct { + ID int `json:"id,omitempty"` + Main string `json:"main,omitempty"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` +} + +type Clouds struct { + All int `json:"all,omitempty"` +} + +type Wind struct { + Speed float64 `json:"speed,omitempty"` + Deg int `json:"deg,omitempty"` + Gust float64 `json:"gust,omitempty"` +} + +type Sys struct { + Pod string `json:"pod,omitempty"` +} + +type List struct { + Dt int `json:"dt,omitempty"` + Main Main `json:"main,omitzero"` + Weather []Weather `json:"weather,omitempty"` + Clouds Clouds `json:"clouds,omitzero"` + Wind Wind `json:"wind,omitzero"` + Visibility int `json:"visibility,omitempty"` + Pop float64 `json:"pop,omitempty"` + Sys Sys `json:"sys,omitzero"` + DtTxt string `json:"dt_txt,omitempty"` +} + +type Coord struct { + Lat float64 `json:"lat,omitempty"` + Lon float64 `json:"lon,omitempty"` +} + +type City struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Coord Coord `json:"coord,omitzero"` + Country string `json:"country,omitempty"` + Population int `json:"population,omitempty"` + Timezone int `json:"timezone,omitempty"` + Sunrise int `json:"sunrise,omitempty"` + Sunset int `json:"sunset,omitempty"` +} diff --git a/internal/pkg/openweather/testdata/forecast.json b/internal/pkg/openweather/testdata/forecast.json new file mode 100644 index 0000000..6b5e5c4 --- /dev/null +++ b/internal/pkg/openweather/testdata/forecast.json @@ -0,0 +1,1472 @@ +{ + "cod": "200", + "message": 0, + "cnt": 40, + "list": [ + { + "dt": 1764774000, + "main": { + "temp": -7.04, + "feels_like": -13.14, + "temp_min": -7.04, + "temp_max": -6.43, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 49, + "temp_kf": -0.61 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 4.24, + "deg": 317, + "gust": 8.22 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-03 15:00:00" + }, + { + "dt": 1764784800, + "main": { + "temp": -6.85, + "feels_like": -12.09, + "temp_min": -6.85, + "temp_max": -6.48, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 46, + "temp_kf": -0.37 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 1 + }, + "wind": { + "speed": 3.34, + "deg": 324, + "gust": 6.81 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-03 18:00:00" + }, + { + "dt": 1764795600, + "main": { + "temp": -6.53, + "feels_like": -10.41, + "temp_min": -6.53, + "temp_max": -6.28, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 45, + "temp_kf": -0.25 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 2.24, + "deg": 326, + "gust": 5.63 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-03 21:00:00" + }, + { + "dt": 1764806400, + "main": { + "temp": -5.11, + "feels_like": -5.11, + "temp_min": -5.11, + "temp_max": -5.11, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 40, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 1.09, + "deg": 347, + "gust": 2.7 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-04 00:00:00" + }, + { + "dt": 1764817200, + "main": { + "temp": -1.96, + "feels_like": -1.96, + "temp_min": -1.96, + "temp_max": -1.96, + "pressure": 1027, + "sea_level": 1027, + "grnd_level": 1019, + "humidity": 37, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 5 + }, + "wind": { + "speed": 0.63, + "deg": 162, + "gust": 1.03 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-04 03:00:00" + }, + { + "dt": 1764828000, + "main": { + "temp": 0.99, + "feels_like": -2.54, + "temp_min": 0.99, + "temp_max": 0.99, + "pressure": 1023, + "sea_level": 1023, + "grnd_level": 1016, + "humidity": 56, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 2 + }, + "wind": { + "speed": 3.28, + "deg": 195, + "gust": 6.84 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-04 06:00:00" + }, + { + "dt": 1764838800, + "main": { + "temp": 4.16, + "feels_like": -0.32, + "temp_min": 4.16, + "temp_max": 4.16, + "pressure": 1021, + "sea_level": 1021, + "grnd_level": 1014, + "humidity": 48, + "temp_kf": 0 + }, + "weather": [ + { + "id": 600, + "main": "Snow", + "description": "light snow", + "icon": "13n" + } + ], + "clouds": { + "all": 77 + }, + "wind": { + "speed": 6.37, + "deg": 261, + "gust": 12.88 + }, + "visibility": 10000, + "pop": 0.74, + "snow": { + "3h": 0.4 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-04 09:00:00" + }, + { + "dt": 1764849600, + "main": { + "temp": 1.29, + "feels_like": -3.88, + "temp_min": 1.29, + "temp_max": 1.29, + "pressure": 1024, + "sea_level": 1024, + "grnd_level": 1016, + "humidity": 55, + "temp_kf": 0 + }, + "weather": [ + { + "id": 600, + "main": "Snow", + "description": "light snow", + "icon": "13n" + } + ], + "clouds": { + "all": 91 + }, + "wind": { + "speed": 6.14, + "deg": 303, + "gust": 11.52 + }, + "visibility": 10000, + "pop": 0.88, + "snow": { + "3h": 0.44 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-04 12:00:00" + }, + { + "dt": 1764860400, + "main": { + "temp": -1.64, + "feels_like": -6.69, + "temp_min": -1.64, + "temp_max": -1.64, + "pressure": 1027, + "sea_level": 1027, + "grnd_level": 1019, + "humidity": 53, + "temp_kf": 0 + }, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "clouds": { + "all": 18 + }, + "wind": { + "speed": 4.56, + "deg": 312, + "gust": 11.62 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-04 15:00:00" + }, + { + "dt": 1764871200, + "main": { + "temp": -1.83, + "feels_like": -6.8, + "temp_min": -1.83, + "temp_max": -1.83, + "pressure": 1028, + "sea_level": 1028, + "grnd_level": 1020, + "humidity": 42, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 10 + }, + "wind": { + "speed": 4.37, + "deg": 310, + "gust": 11.25 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-04 18:00:00" + }, + { + "dt": 1764882000, + "main": { + "temp": -1.76, + "feels_like": -6.24, + "temp_min": -1.76, + "temp_max": -1.76, + "pressure": 1028, + "sea_level": 1028, + "grnd_level": 1020, + "humidity": 42, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 3.73, + "deg": 309, + "gust": 10.64 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-04 21:00:00" + }, + { + "dt": 1764892800, + "main": { + "temp": -1.14, + "feels_like": -5.14, + "temp_min": -1.14, + "temp_max": -1.14, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 41, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 3.3, + "deg": 314, + "gust": 8.35 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-05 00:00:00" + }, + { + "dt": 1764903600, + "main": { + "temp": 1.11, + "feels_like": -2.66, + "temp_min": 1.11, + "temp_max": 1.11, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 36, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 3.63, + "deg": 313, + "gust": 6.81 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-05 03:00:00" + }, + { + "dt": 1764914400, + "main": { + "temp": 2.61, + "feels_like": -0.52, + "temp_min": 2.61, + "temp_max": 2.61, + "pressure": 1026, + "sea_level": 1026, + "grnd_level": 1019, + "humidity": 37, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 3.2, + "deg": 314, + "gust": 5.68 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-05 06:00:00" + }, + { + "dt": 1764925200, + "main": { + "temp": 1.63, + "feels_like": 1.63, + "temp_min": 1.63, + "temp_max": 1.63, + "pressure": 1026, + "sea_level": 1026, + "grnd_level": 1019, + "humidity": 45, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 0.83, + "deg": 360, + "gust": 0.84 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-05 09:00:00" + }, + { + "dt": 1764936000, + "main": { + "temp": 0.77, + "feels_like": 0.77, + "temp_min": 0.77, + "temp_max": 0.77, + "pressure": 1026, + "sea_level": 1026, + "grnd_level": 1018, + "humidity": 47, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 1 + }, + "wind": { + "speed": 0.96, + "deg": 95, + "gust": 0.81 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-05 12:00:00" + }, + { + "dt": 1764946800, + "main": { + "temp": 0.3, + "feels_like": -1.17, + "temp_min": 0.3, + "temp_max": 0.3, + "pressure": 1025, + "sea_level": 1025, + "grnd_level": 1017, + "humidity": 52, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 9 + }, + "wind": { + "speed": 1.35, + "deg": 109, + "gust": 1.17 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-05 15:00:00" + }, + { + "dt": 1764957600, + "main": { + "temp": 0.01, + "feels_like": -1.96, + "temp_min": 0.01, + "temp_max": 0.01, + "pressure": 1024, + "sea_level": 1024, + "grnd_level": 1017, + "humidity": 58, + "temp_kf": 0 + }, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "clouds": { + "all": 29 + }, + "wind": { + "speed": 1.64, + "deg": 117, + "gust": 2.71 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-05 18:00:00" + }, + { + "dt": 1764968400, + "main": { + "temp": 0.47, + "feels_like": -1.99, + "temp_min": 0.47, + "temp_max": 0.47, + "pressure": 1023, + "sea_level": 1023, + "grnd_level": 1015, + "humidity": 62, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 2.07, + "deg": 115, + "gust": 4.11 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-05 21:00:00" + }, + { + "dt": 1764979200, + "main": { + "temp": 1, + "feels_like": -1.44, + "temp_min": 1, + "temp_max": 1, + "pressure": 1022, + "sea_level": 1022, + "grnd_level": 1015, + "humidity": 63, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 2.13, + "deg": 111, + "gust": 3.88 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-06 00:00:00" + }, + { + "dt": 1764990000, + "main": { + "temp": 2.89, + "feels_like": 0.64, + "temp_min": 2.89, + "temp_max": 2.89, + "pressure": 1021, + "sea_level": 1021, + "grnd_level": 1013, + "humidity": 65, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 2.25, + "deg": 131, + "gust": 5.16 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-06 03:00:00" + }, + { + "dt": 1765000800, + "main": { + "temp": 4.33, + "feels_like": 3.24, + "temp_min": 4.33, + "temp_max": 4.33, + "pressure": 1018, + "sea_level": 1018, + "grnd_level": 1011, + "humidity": 75, + "temp_kf": 0 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 1.46, + "deg": 128, + "gust": 4.4 + }, + "visibility": 10000, + "pop": 0.42, + "rain": { + "3h": 0.47 + }, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-06 06:00:00" + }, + { + "dt": 1765011600, + "main": { + "temp": 5.36, + "feels_like": 5.36, + "temp_min": 5.36, + "temp_max": 5.36, + "pressure": 1018, + "sea_level": 1018, + "grnd_level": 1011, + "humidity": 83, + "temp_kf": 0 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "clouds": { + "all": 94 + }, + "wind": { + "speed": 0.73, + "deg": 157, + "gust": 3.82 + }, + "visibility": 10000, + "pop": 1, + "rain": { + "3h": 1.26 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-06 09:00:00" + }, + { + "dt": 1765022400, + "main": { + "temp": 5.87, + "feels_like": 5.87, + "temp_min": 5.87, + "temp_max": 5.87, + "pressure": 1018, + "sea_level": 1018, + "grnd_level": 1011, + "humidity": 83, + "temp_kf": 0 + }, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 79 + }, + "wind": { + "speed": 0.95, + "deg": 190, + "gust": 3.94 + }, + "visibility": 10000, + "pop": 0.74, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-06 12:00:00" + }, + { + "dt": 1765033200, + "main": { + "temp": 6.26, + "feels_like": 5.29, + "temp_min": 6.26, + "temp_max": 6.26, + "pressure": 1019, + "sea_level": 1019, + "grnd_level": 1011, + "humidity": 84, + "temp_kf": 0 + }, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "clouds": { + "all": 50 + }, + "wind": { + "speed": 1.58, + "deg": 235, + "gust": 3.98 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-06 15:00:00" + }, + { + "dt": 1765044000, + "main": { + "temp": 5.11, + "feels_like": 5.11, + "temp_min": 5.11, + "temp_max": 5.11, + "pressure": 1019, + "sea_level": 1019, + "grnd_level": 1011, + "humidity": 79, + "temp_kf": 0 + }, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "clouds": { + "all": 32 + }, + "wind": { + "speed": 0.55, + "deg": 301, + "gust": 1.65 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-06 18:00:00" + }, + { + "dt": 1765054800, + "main": { + "temp": 4.72, + "feels_like": 4.72, + "temp_min": 4.72, + "temp_max": 4.72, + "pressure": 1019, + "sea_level": 1019, + "grnd_level": 1012, + "humidity": 81, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 2 + }, + "wind": { + "speed": 0.65, + "deg": 205, + "gust": 1.27 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-06 21:00:00" + }, + { + "dt": 1765065600, + "main": { + "temp": 5.41, + "feels_like": 5.41, + "temp_min": 5.41, + "temp_max": 5.41, + "pressure": 1021, + "sea_level": 1021, + "grnd_level": 1013, + "humidity": 76, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 2 + }, + "wind": { + "speed": 1.07, + "deg": 171, + "gust": 1.48 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-07 00:00:00" + }, + { + "dt": 1765076400, + "main": { + "temp": 10.57, + "feels_like": 9.01, + "temp_min": 10.57, + "temp_max": 10.57, + "pressure": 1020, + "sea_level": 1020, + "grnd_level": 1013, + "humidity": 51, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 1 + }, + "wind": { + "speed": 3.13, + "deg": 228, + "gust": 7.01 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-07 03:00:00" + }, + { + "dt": 1765087200, + "main": { + "temp": 11.84, + "feels_like": 10.39, + "temp_min": 11.84, + "temp_max": 11.84, + "pressure": 1019, + "sea_level": 1019, + "grnd_level": 1012, + "humidity": 50, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 5.24, + "deg": 256, + "gust": 9.77 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-07 06:00:00" + }, + { + "dt": 1765098000, + "main": { + "temp": 9.18, + "feels_like": 6.91, + "temp_min": 9.18, + "temp_max": 9.18, + "pressure": 1021, + "sea_level": 1021, + "grnd_level": 1013, + "humidity": 59, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 8 + }, + "wind": { + "speed": 4.19, + "deg": 272, + "gust": 8.7 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-07 09:00:00" + }, + { + "dt": 1765108800, + "main": { + "temp": 7.8, + "feels_like": 6.6, + "temp_min": 7.8, + "temp_max": 7.8, + "pressure": 1022, + "sea_level": 1022, + "grnd_level": 1014, + "humidity": 59, + "temp_kf": 0 + }, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 54 + }, + "wind": { + "speed": 2.03, + "deg": 299, + "gust": 5.1 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-07 12:00:00" + }, + { + "dt": 1765119600, + "main": { + "temp": 6.94, + "feels_like": 5.23, + "temp_min": 6.94, + "temp_max": 6.94, + "pressure": 1023, + "sea_level": 1023, + "grnd_level": 1016, + "humidity": 59, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 2.46, + "deg": 286, + "gust": 8.27 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-07 15:00:00" + }, + { + "dt": 1765130400, + "main": { + "temp": 6.57, + "feels_like": 5.1, + "temp_min": 6.57, + "temp_max": 6.57, + "pressure": 1024, + "sea_level": 1024, + "grnd_level": 1017, + "humidity": 47, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 100 + }, + "wind": { + "speed": 2.1, + "deg": 293, + "gust": 7.48 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-07 18:00:00" + }, + { + "dt": 1765141200, + "main": { + "temp": 5.19, + "feels_like": 2.24, + "temp_min": 5.19, + "temp_max": 5.19, + "pressure": 1025, + "sea_level": 1025, + "grnd_level": 1017, + "humidity": 52, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "clouds": { + "all": 98 + }, + "wind": { + "speed": 3.75, + "deg": 315, + "gust": 9.29 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-07 21:00:00" + }, + { + "dt": 1765152000, + "main": { + "temp": 3.12, + "feels_like": -0.77, + "temp_min": 3.12, + "temp_max": 3.12, + "pressure": 1027, + "sea_level": 1027, + "grnd_level": 1019, + "humidity": 46, + "temp_kf": 0 + }, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "clouds": { + "all": 99 + }, + "wind": { + "speed": 4.54, + "deg": 312, + "gust": 8.45 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-08 00:00:00" + }, + { + "dt": 1765162800, + "main": { + "temp": 4.55, + "feels_like": 0.79, + "temp_min": 4.55, + "temp_max": 4.55, + "pressure": 1028, + "sea_level": 1028, + "grnd_level": 1020, + "humidity": 37, + "temp_kf": 0 + }, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "clouds": { + "all": 61 + }, + "wind": { + "speed": 4.96, + "deg": 312, + "gust": 7.41 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-08 03:00:00" + }, + { + "dt": 1765173600, + "main": { + "temp": 4.92, + "feels_like": 1.12, + "temp_min": 4.92, + "temp_max": 4.92, + "pressure": 1027, + "sea_level": 1027, + "grnd_level": 1019, + "humidity": 28, + "temp_kf": 0 + }, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "clouds": { + "all": 31 + }, + "wind": { + "speed": 5.24, + "deg": 309, + "gust": 7.39 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "d" + }, + "dt_txt": "2025-12-08 06:00:00" + }, + { + "dt": 1765184400, + "main": { + "temp": 3.04, + "feels_like": -0.76, + "temp_min": 3.04, + "temp_max": 3.04, + "pressure": 1028, + "sea_level": 1028, + "grnd_level": 1020, + "humidity": 30, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 4.36, + "deg": 313, + "gust": 6.63 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-08 09:00:00" + }, + { + "dt": 1765195200, + "main": { + "temp": 0.94, + "feels_like": -2.85, + "temp_min": 0.94, + "temp_max": 0.94, + "pressure": 1029, + "sea_level": 1029, + "grnd_level": 1021, + "humidity": 30, + "temp_kf": 0 + }, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ], + "clouds": { + "all": 0 + }, + "wind": { + "speed": 3.6, + "deg": 315, + "gust": 6.75 + }, + "visibility": 10000, + "pop": 0, + "sys": { + "pod": "n" + }, + "dt_txt": "2025-12-08 12:00:00" + } + ], + "city": { + "id": 1948005, + "name": "Kwangmyŏng", + "coord": { + "lat": 37.4875, + "lon": 126.9267 + }, + "country": "KR", + "population": 357545, + "timezone": 32400, + "sunrise": 1764714595, + "sunset": 1764749666 + } +} \ No newline at end of file