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 = `
Min Temp: %s
Max Temp: %s
Wind Speed: %s
Humidity: %d%%
Visibility: %s
Predictability: %d%%
Wind Direction: %s
Location: %s
Timezone: %s
`
// 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
}
// Returned checks whether no location was found or response
// said not found.
func (mc *MetaweatherCard) Returned() bool {
return len(mc.location) > 0 && mc.resp.Detail != "Not found."
}
// Matches determines whether the query matches the keys for
// MetaweatherCard
func (mc *MetaweatherCard) Matches() bool {
return strings.HasPrefix(mc.query, "weather in") ||
strings.HasPrefix(mc.query, "weather") ||
strings.HasSuffix(mc.query, "weather")
}
// StripKey removes keys related to MetaweatherCard
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)
}
// Content returns metaweatherContent with all information added
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,
// Write compass wind direction
mc.resp.Consolidated[0].WindCompass,
// Write title
mc.resp.Title,
// Write timezone
mc.resp.Timezone,
))
}
// Title of MetaweatherCard is "Weather"
func (mc *MetaweatherCard) Title() string {
return "Weather"
}
// Footer returns a footer with a link to metaweather
func (mc *MetaweatherCard) Footer() template.HTML {
return ``
}
// Head returns an empty string as no extra head tags
// are required for MetaweatherCard
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)
}