github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/plugins/weather_met/weather_met.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package weather_met 20 21 import ( 22 "context" 23 "encoding/json" 24 "encoding/xml" 25 "fmt" 26 "math" 27 "net/url" 28 "sort" 29 "strconv" 30 "time" 31 32 "github.com/pkg/errors" 33 34 "github.com/e154/smart-home/adaptors" 35 "github.com/e154/smart-home/common" 36 "github.com/e154/smart-home/common/apperr" 37 "github.com/e154/smart-home/common/web" 38 m "github.com/e154/smart-home/models" 39 "github.com/e154/smart-home/plugins/weather" 40 ) 41 42 // WeatherMet ... 43 type WeatherMet struct { 44 adaptors *adaptors.Adaptors 45 crawler web.Crawler 46 } 47 48 // NewWeatherMet ... 49 func NewWeatherMet(adaptors *adaptors.Adaptors, crawler web.Crawler) (weather *WeatherMet) { 50 weather = &WeatherMet{ 51 adaptors: adaptors, 52 crawler: crawler, 53 } 54 55 return 56 } 57 58 // UpdateForecast ... 59 func (p *WeatherMet) UpdateForecast(zone Zone) (forecast m.AttributeValue, err error) { 60 forecast, err = p.GetForecast(zone, time.Now()) 61 return 62 } 63 64 // GetForecast ... 65 func (p *WeatherMet) GetForecast(params Zone, now time.Time) (forecast m.AttributeValue, err error) { 66 67 var zone Zone 68 if zone, err = p.FetchData(params.Name, params.Lat, params.Lon, now); err != nil { 69 return 70 } 71 72 if forecast, err = p.getCurrentWeather(zone, now); err != nil { 73 return 74 } 75 76 rounded := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 77 78 for i := 1; i < 6; i++ { 79 t := rounded.AddDate(0, 0, i) 80 weather, err := p.getWeather(zone, t, 24) 81 if err != nil { 82 log.Error(err.Error()) 83 } 84 for name, attr := range weather { 85 forecast[fmt.Sprintf("day%d_%s", i, name)] = attr 86 } 87 } 88 89 return 90 } 91 92 // FetchData ... 93 func (p *WeatherMet) FetchData(name string, lat, lon float64, now time.Time) (zone Zone, err error) { 94 95 if lat == 0 || lon == 0 { 96 err = errors.Wrap(apperr.ErrBadRequestParams, "zero positions") 97 return 98 } 99 100 // get from storage 101 if zone, err = p.fetchFromLocalStorage(name); err == nil { 102 if zone.LoadedAt != nil && now.Sub(common.TimeValue(zone.LoadedAt)).Minutes() < 60 { 103 return 104 } 105 } else { 106 log.Info(err.Error()) 107 } 108 109 // fetch from server 110 var body []byte 111 if body, err = p.fetchFromServer(lat, lon); err != nil { 112 return 113 } 114 115 zone = Zone{ 116 Name: name, 117 Lat: lat, 118 Lon: lon, 119 } 120 zone.Weatherdata = &Weatherdata{} 121 zone.LoadedAt = common.Time(time.Now()) 122 if err = xml.Unmarshal(body, zone.Weatherdata); err != nil { 123 return 124 } 125 126 // save to storage 127 err = p.saveToLocalStorage(zone) 128 129 return 130 } 131 132 func (p *WeatherMet) fetchFromLocalStorage(name string) (zone Zone, err error) { 133 134 log.Debugf("fetch from local storage") 135 136 var variable m.Variable 137 if variable, err = p.adaptors.Variable.GetByName(context.Background(), fmt.Sprintf("weather_met.%s", name)); err != nil { 138 return 139 } 140 141 zone = Zone{} 142 err = json.Unmarshal([]byte(variable.Value), &zone) 143 144 return 145 } 146 147 func (p *WeatherMet) saveToLocalStorage(zone Zone) (err error) { 148 149 log.Debugf("save to local storage '%s'", zone.Name) 150 151 var b []byte 152 if b, err = json.Marshal(zone); err != nil { 153 return 154 } 155 156 model := m.Variable{ 157 System: true, 158 Name: fmt.Sprintf("weather_met.%s", zone.Name), 159 Value: string(b), 160 EntityId: common.NewEntityId(fmt.Sprintf("weather_met.%s", zone.Name)), 161 } 162 163 err = p.adaptors.Variable.CreateOrUpdate(context.Background(), model) 164 165 return 166 } 167 168 func (p *WeatherMet) fetchFromServer(lat, lon float64) (body []byte, err error) { 169 170 uri, _ := url.Parse(DefaultApiUrl) 171 params := url.Values{} 172 params.Add("lat", fmt.Sprintf("%f", lat)) 173 params.Add("lon", fmt.Sprintf("%f", lon)) 174 175 uri.RawQuery = params.Encode() 176 177 log.Debugf("fetch from server %s", uri.String()) 178 179 _, body, err = p.crawler.Probe(web.Request{Method: "GET", Url: uri.String()}) 180 181 return 182 } 183 184 func (p *WeatherMet) getCurrentWeather(zone Zone, now time.Time) (w6 m.AttributeValue, err error) { 185 186 if w6, err = p.getWeather(zone, now, 6); err != nil { 187 return 188 } 189 190 var w24 m.AttributeValue 191 if w24, err = p.getWeather(zone, now, 24); err != nil { 192 return 193 } 194 195 w6[weather.AttrWeatherMaxTemperature] = w24[weather.AttrWeatherMaxTemperature] 196 w6[weather.AttrWeatherMinTemperature] = w24[weather.AttrWeatherMinTemperature] 197 198 return 199 } 200 201 func (p *WeatherMet) getWeather(zone Zone, t time.Time, maxHour float64) (w m.AttributeValue, err error) { 202 203 var orderedEntries Products 204 for _, product := range zone.Weatherdata.Products { 205 if t.Sub(product.To).Minutes() > 0 { 206 continue 207 } 208 209 averageDist := product.To.Sub(t).Hours() + product.From.Sub(t).Hours() 210 211 if averageDist > maxHour { 212 continue 213 } 214 215 orderedEntries = append(orderedEntries, product) 216 } 217 218 sort.Sort(orderedEntries) 219 220 w = m.AttributeValue{ 221 weather.AttrWeatherDatetime: t, 222 weather.AttrWeatherMinTemperature: p.getMinTemperature(orderedEntries), 223 weather.AttrWeatherMaxTemperature: p.getMaxTemperature(orderedEntries), 224 weather.AttrWeatherMain: p.getCondition(orderedEntries), 225 weather.AttrWeatherPressure: p.getPressure(orderedEntries), 226 weather.AttrWeatherHumidity: p.getHumidity(orderedEntries), 227 weather.AttrWeatherWindSpeed: p.getWindSpeed(orderedEntries), 228 weather.AttrWeatherWindBearing: p.getWindDirection(orderedEntries), 229 } 230 231 if maxHour <= 6 { 232 w[weather.AttrWeatherTemperature] = p.getTemperature(orderedEntries) 233 } 234 235 w[weather.AttrWeatherAttribution] = Attribution 236 237 return 238 } 239 240 func (p *WeatherMet) getTemperature(orderedEntries []Product) (value float64) { 241 242 for _, entry := range orderedEntries { 243 if entry.Location.Temperature != nil { 244 value, _ = strconv.ParseFloat(entry.Location.Temperature.Value, 32) 245 value = math.Round(value*100) / 100 246 return 247 } 248 } 249 return 250 } 251 252 func (p *WeatherMet) getMinTemperature(orderedEntries []Product) (value float64) { 253 254 defer func() { 255 if value != 0 { 256 value = math.Round(value*100) / 100 257 } 258 }() 259 260 value = 99 261 for _, entry := range orderedEntries { 262 if entry.Location.MinTemperature != nil { 263 _value, _ := strconv.ParseFloat(entry.Location.MinTemperature.Value, 32) 264 if value == 99 { 265 value = _value 266 } 267 if _value < value { 268 value = _value 269 } 270 } 271 } 272 273 if value == 99 { 274 value = 0 275 } 276 277 return 278 } 279 280 func (p *WeatherMet) getMaxTemperature(orderedEntries []Product) (value float64) { 281 282 defer func() { 283 if value != 0 { 284 value = math.Round(value*100) / 100 285 } 286 }() 287 288 for _, entry := range orderedEntries { 289 if entry.Location.MaxTemperature != nil { 290 _value, _ := strconv.ParseFloat(entry.Location.MaxTemperature.Value, 32) 291 if _value > value { 292 value = _value 293 } 294 } 295 } 296 return 297 } 298 299 func (p *WeatherMet) getCondition(orderedEntries []Product) (value string) { 300 301 for _, entry := range orderedEntries { 302 if entry.Location.Symbol != nil { 303 num, _ := strconv.Atoi(entry.Location.Symbol.Number) 304 value = weather.GetStateByIndex(num) 305 } 306 } 307 return 308 } 309 310 func (p *WeatherMet) getPressure(orderedEntries []Product) (value float64) { 311 312 for _, entry := range orderedEntries { 313 if entry.Location.Pressure != nil { 314 _value, _ := strconv.ParseFloat(entry.Location.Pressure.Value, 32) 315 _value = math.Round(_value*100) / 100 316 if _value > value { 317 value = _value 318 } 319 } 320 } 321 return 322 } 323 324 func (p *WeatherMet) getHumidity(orderedEntries []Product) (value float64) { 325 326 for _, entry := range orderedEntries { 327 if entry.Location.Humidity != nil { 328 _value, _ := strconv.ParseFloat(entry.Location.Humidity.Value, 32) 329 _value = math.Round(_value*100) / 100 330 if _value > value { 331 value = _value 332 } 333 } 334 } 335 return 336 } 337 338 func (p *WeatherMet) getDewpointTemperature(orderedEntries []Product) (value float64) { 339 340 for _, entry := range orderedEntries { 341 if entry.Location.DewpointTemperature != nil { 342 value, _ = strconv.ParseFloat(entry.Location.DewpointTemperature.Value, 32) 343 value = math.Round(value*100) / 100 344 return 345 } 346 } 347 return 348 } 349 350 func (p *WeatherMet) getWindSpeed(orderedEntries []Product) (value float64) { 351 352 for _, entry := range orderedEntries { 353 if entry.Location.WindSpeed != nil { 354 mps, _ := strconv.ParseFloat(entry.Location.WindSpeed.Mps, 32) 355 value = mps * 3.6 356 value = math.Round(value*100) / 100 357 return 358 } 359 } 360 return 361 } 362 363 func (p *WeatherMet) getWindDirection(orderedEntries []Product) (value float64) { 364 365 for _, entry := range orderedEntries { 366 if entry.Location.WindDirection != nil { 367 value, _ = strconv.ParseFloat(entry.Location.WindDirection.Deg, 32) 368 value = math.Round(value*100) / 100 369 return 370 } 371 } 372 return 373 }