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  }