2021-12-08 21:38:22 +00:00
|
|
|
/*
|
|
|
|
* Scope - A simple and minimal metasearch engine
|
|
|
|
* Copyright (C) 2021 Arsen Musayelyan
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2021-12-08 17:24:05 +00:00
|
|
|
package cards
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/bcicen/go-units"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
Register("metaweather", 1, NewMetaweatherCard)
|
|
|
|
}
|
|
|
|
|
|
|
|
const metaweatherContent = `
|
|
|
|
<div class="columns">
|
|
|
|
<div class="column">
|
|
|
|
<span class="iconify" data-width="75" data-height="75" data-icon="%s"></span><p class="subtitle">%s</p>
|
|
|
|
</div>
|
|
|
|
<div class="column has-text-right">
|
|
|
|
<p class="title has-text-weight-normal">%s</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="columns">
|
|
|
|
<div class="column has-text-centered">
|
|
|
|
<p><b>Min Temp:</b> %s</p>
|
|
|
|
<p><b>Max Temp:</b> %s</p>
|
|
|
|
<p><b>Wind Speed:</b> %s</p>
|
|
|
|
</div>
|
|
|
|
<div class="column has-text-centered">
|
|
|
|
<p><b>Humidity:</b> %d%%</p>
|
|
|
|
<p><b>Visibility:</b> %s</p>
|
|
|
|
<p><b>Predictability:</b> %d%%</p>
|
|
|
|
</div>
|
|
|
|
<div class="column has-text-centered">
|
2021-12-13 20:17:32 +00:00
|
|
|
<p><b>Air Pressure:</b> %s</p>
|
2021-12-08 17:24:05 +00:00
|
|
|
<p><b>Wind Direction:</b> %s</p>
|
|
|
|
<p><b>Location:</b> %s</p>
|
|
|
|
</div>
|
|
|
|
</div>`
|
|
|
|
|
|
|
|
// weatherIconMap maps metaweather abbreviations
|
|
|
|
// to iconify icons
|
|
|
|
var weatherIconMap = map[string]string{
|
|
|
|
"sn": "bi:cloud-snow",
|
|
|
|
"sl": "bi:cloud-sleet",
|
|
|
|
"h": "bi:cloud-hail",
|
|
|
|
"t": "bi:cloud-lightning-rain",
|
|
|
|
"hr": "bi:cloud-rain-heavy",
|
|
|
|
"lr": "bi:cloud-drizzle",
|
|
|
|
"s": "wi:day-rain",
|
|
|
|
"hc": "bi:cloud-fill",
|
|
|
|
"lc": "bi:cloud",
|
|
|
|
"c": "akar-icons:sun",
|
|
|
|
}
|
|
|
|
|
|
|
|
// MetaweatherCard represents a metaweather card
|
|
|
|
type MetaweatherCard struct {
|
|
|
|
query string
|
|
|
|
userAgent string
|
|
|
|
location []MetaweatherLocation
|
|
|
|
resp MetaweatherResponse
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewMetaweatherCard is a NewCardFunc that creates a new MetaweatherCard
|
|
|
|
func NewMetaweatherCard(query, userAgent string) Card {
|
|
|
|
return &MetaweatherCard{
|
|
|
|
query: query,
|
|
|
|
userAgent: userAgent,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MetaweatherLocation represents a location
|
|
|
|
type MetaweatherLocation struct {
|
|
|
|
Title string `json:"title"`
|
|
|
|
WOEID int `json:"woeid"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// MetaweatherResponse represents a response from
|
|
|
|
// the metaweather API
|
|
|
|
type MetaweatherResponse struct {
|
|
|
|
Detail string `json:"detail"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
Timezone string `json:"timezone"`
|
|
|
|
Consolidated []struct {
|
|
|
|
ID int `json:"id"`
|
|
|
|
State string `json:"weather_state_name"`
|
|
|
|
StateAbbr string `json:"weather_state_abbr"`
|
|
|
|
WindCompass string `json:"wind_direction_compass"`
|
|
|
|
MinTemp float64 `json:"min_temp"`
|
|
|
|
MaxTemp float64 `json:"max_temp"`
|
|
|
|
CurrentTemp float64 `json:"the_temp"`
|
|
|
|
WindSpeed float64 `json:"wind_speed"`
|
|
|
|
WindDirection float64 `json:"wind_direction"`
|
|
|
|
AirPressure float64 `json:"air_pressure"`
|
|
|
|
Visibility float64 `json:"visibility"`
|
|
|
|
Humidity int `json:"humidity"`
|
|
|
|
Predictability int `json:"predictability"`
|
|
|
|
} `json:"consolidated_weather"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// RunQuery searches for the location and then runs an API query
|
|
|
|
// using the returned WOEID
|
|
|
|
func (mc *MetaweatherCard) RunQuery() error {
|
|
|
|
http.DefaultClient.Timeout = 5 * time.Second
|
|
|
|
|
|
|
|
// Create location search request
|
|
|
|
req, err := http.NewRequest(
|
|
|
|
http.MethodGet,
|
|
|
|
"https://www.metaweather.com/api/location/search/?query="+url.QueryEscape(mc.StripKey()),
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req.Header.Set("User-Agent", mc.userAgent)
|
|
|
|
|
|
|
|
// Perform request
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode response into location
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&mc.location)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
// If no locations, search returned no results
|
|
|
|
if len(mc.location) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create request for weather data
|
|
|
|
req, err = http.NewRequest(
|
|
|
|
http.MethodGet,
|
|
|
|
"https://www.metaweather.com/api/location/"+strconv.Itoa(mc.location[0].WOEID),
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req.Header.Set("User-Agent", mc.userAgent)
|
|
|
|
|
|
|
|
// Perform request
|
|
|
|
res, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode response into response struct
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&mc.resp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Returned checks whether no location was found or response
|
|
|
|
// said not found.
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Returned() bool {
|
|
|
|
return len(mc.location) > 0 && mc.resp.Detail != "Not found."
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Matches determines whether the query matches the keys for
|
|
|
|
// MetaweatherCard
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Matches() bool {
|
|
|
|
return strings.HasPrefix(mc.query, "weather in") ||
|
|
|
|
strings.HasPrefix(mc.query, "weather") ||
|
|
|
|
strings.HasSuffix(mc.query, "weather")
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// StripKey removes keys related to MetaweatherCard
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) StripKey() string {
|
|
|
|
query := strings.TrimPrefix(mc.query, "weather in")
|
|
|
|
query = strings.TrimPrefix(query, "weather")
|
|
|
|
query = strings.TrimSuffix(query, "weather")
|
|
|
|
return strings.TrimSpace(query)
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Content returns metaweatherContent with all information added
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Content() template.HTML {
|
|
|
|
return template.HTML(fmt.Sprintf(
|
|
|
|
metaweatherContent,
|
|
|
|
// Write iconift icon for weather state
|
|
|
|
weatherIconMap[mc.resp.Consolidated[0].StateAbbr],
|
|
|
|
// Write weather state
|
|
|
|
mc.resp.Consolidated[0].State,
|
|
|
|
// Convert and write current temperature
|
|
|
|
convert(mc.resp.Consolidated[0].CurrentTemp, "temperature", units.Celsius),
|
|
|
|
// Convert and write minimum temperature
|
|
|
|
convert(mc.resp.Consolidated[0].MinTemp, "temperature", units.Celsius),
|
|
|
|
// Convert and write maximum temperature
|
|
|
|
convert(mc.resp.Consolidated[0].MaxTemp, "temperature", units.Celsius),
|
|
|
|
// Write wind speed
|
|
|
|
fmt.Sprintf("%.2f mph", mc.resp.Consolidated[0].WindSpeed),
|
|
|
|
// Write humidity percentafe
|
|
|
|
mc.resp.Consolidated[0].Humidity,
|
|
|
|
// Convert and write visibility
|
|
|
|
convert(mc.resp.Consolidated[0].Visibility, "visibility", units.Mile),
|
|
|
|
// Write predictability percentage
|
|
|
|
mc.resp.Consolidated[0].Predictability,
|
2021-12-13 20:17:32 +00:00
|
|
|
// Write air pressure
|
|
|
|
convert(mc.resp.Consolidated[0].AirPressure, "pressure", units.HectoPascal),
|
2021-12-08 17:24:05 +00:00
|
|
|
// Write compass wind direction
|
|
|
|
mc.resp.Consolidated[0].WindCompass,
|
|
|
|
// Write title
|
|
|
|
mc.resp.Title,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Title of MetaweatherCard is "Weather"
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Title() string {
|
|
|
|
return "Weather"
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Footer returns a footer with a link to metaweather
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Footer() template.HTML {
|
|
|
|
return `<div class="card-footer"><a class="card-footer-item" href="https://www.metaweather.com/">Metaweather</a></div>`
|
|
|
|
}
|
|
|
|
|
2021-12-08 21:18:14 +00:00
|
|
|
// Head returns an empty string as no extra head tags
|
|
|
|
// are required for MetaweatherCard
|
2021-12-08 17:24:05 +00:00
|
|
|
func (mc *MetaweatherCard) Head() template.HTML {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert converts a value to a unit specified in the config, and then formats it
|
|
|
|
func convert(val float64, suffix string, defaultUnit units.Unit) string {
|
|
|
|
// Find unit in config
|
|
|
|
unit, err := units.Find(viper.GetString("search.cards.weather.units." + suffix))
|
|
|
|
if err != nil {
|
|
|
|
unit = defaultUnit
|
|
|
|
}
|
|
|
|
// Convert value to specified unit
|
|
|
|
newVal, err := units.ConvertFloat(val, defaultUnit, unit)
|
|
|
|
opts := units.FmtOptions{
|
|
|
|
Label: true,
|
|
|
|
Short: true,
|
|
|
|
Precision: 2,
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return units.NewValue(val, defaultUnit).Fmt(opts)
|
|
|
|
}
|
|
|
|
return newVal.Fmt(opts)
|
|
|
|
}
|