github.com/deadlysurgeon/weather@v0.0.0-20240402201029-3925d9f784b1/weather/impl.go (about) 1 package weather 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "time" 8 9 "github.com/deadlysurgeon/weather/settings" 10 ) 11 12 type impl struct { 13 endpoint string 14 apiKey string 15 client *http.Client 16 } 17 18 // New creates a weather service implementation 19 func New(config settings.App) (Service, error) { 20 service := &impl{ 21 endpoint: "https://api.openweathermap.org/data/3.0/onecall", // ?lat={lat}&lon={lon}&exclude={part}&appid={API key} 22 client: &http.Client{ 23 Timeout: 5 * time.Second, 24 }, 25 } 26 27 if service.apiKey = config.Weather.APIKey; service.apiKey == "" { 28 return nil, fmt.Errorf("no api key specified") 29 } 30 31 if config.Weather.Endpoint != "" { 32 service.endpoint = config.Weather.Endpoint 33 } 34 35 return service, nil 36 } 37 38 func (s *impl) At(lat, lon string) (Report, error) { 39 report := Report{ 40 Temperature: "unknown", 41 Condition: "unknown", 42 TemperatureRaw: 0, 43 } 44 45 req, err := s.formRequest(lat, lon) 46 if err != nil { 47 return report, err 48 } 49 50 resp, err := s.client.Do(req) 51 if err != nil { 52 return report, err 53 } 54 defer resp.Body.Close() 55 56 // TODO: 57 // - Double check how they return errors such as overuse. 58 59 if resp.StatusCode != http.StatusOK { 60 return report, fmt.Errorf("bad status code: %d", resp.StatusCode) 61 } 62 63 var owmResp openweathermapResponse 64 if err = json.NewDecoder(resp.Body).Decode(&owmResp); err != nil { 65 return report, fmt.Errorf("failed to read response: %w", err) 66 } 67 68 report.TemperatureRaw = owmResp.Current.FeelsLike 69 report.Temperature = feelsLike(report.TemperatureRaw) 70 if len(owmResp.Current.Weather) > 0 { 71 report.Condition = owmResp.Current.Weather[0].Main 72 } 73 74 return report, nil 75 } 76 77 func feelsLike(temp float32) string { 78 switch { 79 case temp <= 0: 80 return "freezing" 81 case temp <= 10: 82 return "cold" 83 case temp <= 20: 84 return "moderate" 85 case temp <= 30: 86 return "hot" 87 default: 88 return "burning" 89 } 90 } 91 92 func (s *impl) formRequest(lat, lon string) (*http.Request, error) { 93 req, err := http.NewRequest(http.MethodGet, s.endpoint, nil) 94 if err != nil { 95 return nil, err 96 } 97 98 q := req.URL.Query() 99 q.Add("appid", s.apiKey) 100 q.Add("units", "metric") 101 q.Add("lat", lat) 102 q.Add("lon", lon) 103 req.URL.RawQuery = q.Encode() 104 105 return req, nil 106 } 107 108 // Only grab the fields we care about. If we needed more from the service we 109 // would need to break this out into its own models file and each sub struct be 110 // exportable. 111 type openweathermapResponse struct { 112 Current struct { 113 FeelsLike float32 `json:"feels_like"` 114 Weather []struct { 115 Main string `json:"main"` 116 } `json:"weather"` 117 } `json:"current"` 118 }