diff --git a/searx/engines/wttr.py b/searx/engines/wttr.py
index 11a2d27cc..aec4d1075 100644
--- a/searx/engines/wttr.py
+++ b/searx/engines/wttr.py
@@ -1,9 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""wttr.in (weather forecast service)"""
-from json import loads
from urllib.parse import quote
-from flask_babel import gettext
+from datetime import datetime
+
+from searx.result_types import EngineResults, WeatherAnswer
+from searx import weather
about = {
"website": "https://wttr.in",
@@ -18,118 +20,105 @@ categories = ["weather"]
url = "https://wttr.in/{query}?format=j1&lang={lang}"
-
-def get_weather_condition_key(lang):
- if lang == "en":
- return "weatherDesc"
-
- return "lang_" + lang.lower()
-
-
-def generate_day_table(day):
- res = ""
-
- res += f"
{gettext('Average temp.')} | {day['avgtempC']}°C / {day['avgtempF']}°F |
"
- res += f"{gettext('Min temp.')} | {day['mintempC']}°C / {day['mintempF']}°F |
"
- res += f"{gettext('Max temp.')} | {day['maxtempC']}°C / {day['maxtempF']}°F |
"
- res += f"{gettext('UV index')} | {day['uvIndex']} |
"
- res += f"{gettext('Sunrise')} | {day['astronomy'][0]['sunrise']} |
"
- res += f"{gettext('Sunset')} | {day['astronomy'][0]['sunset']} |
"
-
- return res
-
-
-def generate_condition_table(condition, lang, current=False):
- res = ""
-
- if current:
- key = "temp_"
- else:
- key = "temp"
-
- res += (
- f"{gettext('Condition')} | "
- f"{condition[get_weather_condition_key(lang)][0]['value']} |
"
- )
- res += (
- f"{gettext('Temperature')} | "
- f"{condition[key+'C']}°C / {condition[key+'F']}°F |
"
- )
- res += (
- f"{gettext('Feels like')} | {condition['FeelsLikeC']}°C / {condition['FeelsLikeF']}°F |
"
- )
- res += (
- f"{gettext('Wind')} | {condition['winddir16Point']} — "
- f"{condition['windspeedKmph']} km/h / {condition['windspeedMiles']} mph |
"
- )
- res += (
- f"{gettext('Visibility')} | {condition['visibility']} km / {condition['visibilityMiles']} mi | "
- )
- res += f"
{gettext('Humidity')} | {condition['humidity']}% |
"
-
- return res
+# adapted from https://github.com/chubin/wttr.in/blob/master/lib/constants.py
+WWO_TO_CONDITION: dict[str, weather.WeatherConditionType] = {
+ "113": "clear sky",
+ "116": "partly cloudy",
+ "119": "cloudy",
+ "122": "fair",
+ "143": "fair",
+ "176": "light rain showers",
+ "179": "light snow showers",
+ "182": "light sleet showers",
+ "185": "light sleet",
+ "200": "rain and thunder",
+ "227": "light snow",
+ "230": "heavy snow",
+ "248": "fog",
+ "260": "fog",
+ "263": "light rain showers",
+ "266": "light rain showers",
+ "281": "light sleet showers",
+ "284": "light snow showers",
+ "293": "light rain showers",
+ "296": "light rain",
+ "299": "rain showers",
+ "302": "rain",
+ "305": "heavy rain showers",
+ "308": "heavy rain",
+ "311": "light sleet",
+ "314": "sleet",
+ "317": "light sleet",
+ "320": "heavy sleet",
+ "323": "light snow showers",
+ "326": "light snow showers",
+ "329": "heavy snow showers",
+ "332": "heavy snow",
+ "335": "heavy snow showers",
+ "338": "heavy snow",
+ "350": "light sleet",
+ "353": "light rain showers",
+ "356": "heavy rain showers",
+ "359": "heavy rain",
+ "362": "light sleet showers",
+ "365": "sleet showers",
+ "368": "light snow showers",
+ "371": "heavy snow showers",
+ "374": "light sleet showers",
+ "377": "heavy sleet",
+ "386": "rain showers and thunder",
+ "389": "heavy rain showers and thunder",
+ "392": "snow showers and thunder",
+ "395": "heavy snow showers",
+}
def request(query, params):
- if query.replace('/', '') in [":help", ":bash.function", ":translation"]:
- return None
-
- if params["language"] == "all":
- params["language"] = "en"
- else:
- params["language"] = params["language"].split("-")[0]
-
params["url"] = url.format(query=quote(query), lang=params["language"])
-
params["raise_for_httperror"] = False
return params
-def response(resp):
- results = []
+def _weather_data(location: weather.GeoLocation, data: dict):
+ # the naming between different data objects is inconsitent, thus temp_C and
+ # tempC are possible
+ tempC: float = data.get("temp_C") or data.get("tempC") # type: ignore
- if resp.status_code == 404:
- return []
-
- result = loads(resp.text)
-
- current = result["current_condition"][0]
- location = result['nearest_area'][0]
-
- forecast_indices = {3: gettext('Morning'), 4: gettext('Noon'), 6: gettext('Evening'), 7: gettext('Night')}
-
- title = f"{location['areaName'][0]['value']}, {location['region'][0]['value']}"
-
- infobox = f"{gettext('Current condition')}
"
-
- infobox += generate_condition_table(current, resp.search_params['language'], True)
-
- infobox += "
"
-
- for day in result["weather"]:
- infobox += f"{day['date']}
"
-
- infobox += ""
-
- infobox += generate_day_table(day)
-
- infobox += "
"
-
- infobox += ""
-
- for time in forecast_indices.items():
- infobox += f"{time[1]} |
"
-
- infobox += generate_condition_table(day['hourly'][time[0]], resp.search_params['language'])
-
- infobox += "
"
-
- results.append(
- {
- "infobox": title,
- "content": infobox,
- }
+ return WeatherAnswer.Item(
+ location=location,
+ temperature=weather.Temperature(unit="°C", value=tempC),
+ condition=WWO_TO_CONDITION[data["weatherCode"]],
+ feels_like=weather.Temperature(unit="°C", value=data["FeelsLikeC"]),
+ wind_from=weather.Compass(int(data["winddirDegree"])),
+ wind_speed=weather.WindSpeed(data["windspeedKmph"], unit="km/h"),
+ pressure=weather.Pressure(data["pressure"], unit="hPa"),
+ humidity=weather.RelativeHumidity(data["humidity"]),
+ cloud_cover=data["cloudcover"],
)
- return results
+
+def response(resp):
+ res = EngineResults()
+
+ if resp.status_code == 404:
+ return res
+
+ json_data = resp.json()
+ geoloc = weather.GeoLocation.by_query(resp.search_params["query"])
+
+ weather_answer = WeatherAnswer(
+ current=_weather_data(geoloc, json_data["current_condition"][0]),
+ service="wttr.in",
+ )
+
+ for day in json_data["weather"]:
+ date = datetime.fromisoformat(day["date"])
+ time_slot_len = 24 // len(day["hourly"])
+ for index, forecast in enumerate(day["hourly"]):
+ forecast_data = _weather_data(geoloc, forecast)
+ forecast_data.datetime = weather.DateTime(date.replace(hour=index * time_slot_len + 1))
+ weather_answer.forecasts.append(forecast_data)
+
+ res.add(weather_answer)
+ return res
diff --git a/searx/weather.py b/searx/weather.py
index fb62515b6..793dc09c4 100644
--- a/searx/weather.py
+++ b/searx/weather.py
@@ -481,46 +481,49 @@ class Compass:
WeatherConditionType = typing.Literal[
# The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel")
"clear sky",
+ "partly cloudy",
"cloudy",
"fair",
"fog",
- "heavy rain and thunder",
- "heavy rain showers and thunder",
- "heavy rain showers",
- "heavy rain",
- "heavy sleet and thunder",
- "heavy sleet showers and thunder",
- "heavy sleet showers",
- "heavy sleet",
- "heavy snow and thunder",
- "heavy snow showers and thunder",
- "heavy snow showers",
- "heavy snow",
+ # rain
"light rain and thunder",
"light rain showers and thunder",
"light rain showers",
"light rain",
- "light sleet and thunder",
- "light sleet showers and thunder",
- "light sleet showers",
- "light sleet",
- "light snow and thunder",
- "light snow showers and thunder",
- "light snow showers",
- "light snow",
- "partly cloudy",
"rain and thunder",
"rain showers and thunder",
"rain showers",
"rain",
+ "heavy rain and thunder",
+ "heavy rain showers and thunder",
+ "heavy rain showers",
+ "heavy rain",
+ # sleet
+ "light sleet and thunder",
+ "light sleet showers and thunder",
+ "light sleet showers",
+ "light sleet",
"sleet and thunder",
"sleet showers and thunder",
"sleet showers",
"sleet",
+ "heavy sleet and thunder",
+ "heavy sleet showers and thunder",
+ "heavy sleet showers",
+ "heavy sleet",
+ # snow
+ "light snow and thunder",
+ "light snow showers and thunder",
+ "light snow showers",
+ "light snow",
"snow and thunder",
"snow showers and thunder",
"snow showers",
"snow",
+ "heavy snow and thunder",
+ "heavy snow showers and thunder",
+ "heavy snow showers",
+ "heavy snow",
]
"""Standardized designations for weather conditions. The designators were
taken from a collaboration between NRK and Norwegian Meteorological Institute
@@ -535,46 +538,49 @@ taken from a collaboration between NRK and Norwegian Meteorological Institute
YR_WEATHER_SYMBOL_MAP = {
"clear sky": "01d", # 01d clearsky_day
- "fair": "02d", # 02d fair_day
"partly cloudy": "03d", # 03d partlycloudy_day
"cloudy": "04", # 04 cloudy
- "light rain showers": "40d", # 40d lightrainshowers_day
- "rain showers": "05d", # 05d rainshowers_day
- "heavy rain showers": "41d", # 41d heavyrainshowers_day
- "light rain showers and thunder": "24d", # 24d lightrainshowersandthunder_day
- "rain showers and thunder": "06d", # 06d rainshowersandthunder_day
- "heavy rain showers and thunder": "25d", # 25d heavyrainshowersandthunder_day
- "light sleet showers": "42d", # 42d lightsleetshowers_day
- "sleet showers": "07d", # 07d sleetshowers_day
- "heavy sleet showers": "43d", # 43d heavysleetshowers_day
- "light sleet showers and thunder": "26d", # 26d lightssleetshowersandthunder_day
- "sleet showers and thunder": "20d", # 20d sleetshowersandthunder_day
- "heavy sleet showers and thunder": "27d", # 27d heavysleetshowersandthunder_day
- "light snow showers": "44d", # 44d lightsnowshowers_day
- "snow showers": "08d", # 08d snowshowers_day
- "heavy snow showers": "45d", # 45d heavysnowshowers_day
- "light snow showers and thunder": "28d", # 28d lightssnowshowersandthunder_day
- "snow showers and thunder": "21d", # 21d snowshowersandthunder_day
- "heavy snow showers and thunder": "29d", # 29d heavysnowshowersandthunder_day
- "light rain": "46", # 46 lightrain
- "rain": "09", # 09 rain
- "heavy rain": "10", # 10 heavyrain
- "light rain and thunder": "30", # 30 lightrainandthunder
- "rain and thunder": "22", # 22 rainandthunder
- "heavy rain and thunder": "11", # 11 heavyrainandthunder
- "light sleet": "47", # 47 lightsleet
- "sleet": "12", # 12 sleet
- "heavy sleet": "48", # 48 heavysleet
- "light sleet and thunder": "31", # 31 lightsleetandthunder
- "sleet and thunder": "23", # 23 sleetandthunder
- "heavy sleet and thunder": "32", # 32 heavysleetandthunder
- "light snow": "49", # 49 lightsnow
- "snow": "13", # 13 snow
- "heavy snow": "50", # 50 heavysnow
- "light snow and thunder": "33", # 33 lightsnowandthunder
- "snow and thunder": "14", # 14 snowandthunder
- "heavy snow and thunder": "34", # 34 heavysnowandthunder
+ "fair": "02d", # 02d fair_day
"fog": "15", # 15 fog
+ # rain
+ "light rain and thunder": "30", # 30 lightrainandthunder
+ "light rain showers and thunder": "24d", # 24d lightrainshowersandthunder_day
+ "light rain showers": "40d", # 40d lightrainshowers_day
+ "light rain": "46", # 46 lightrain
+ "rain and thunder": "22", # 22 rainandthunder
+ "rain showers and thunder": "06d", # 06d rainshowersandthunder_day
+ "rain showers": "05d", # 05d rainshowers_day
+ "rain": "09", # 09 rain
+ "heavy rain and thunder": "11", # 11 heavyrainandthunder
+ "heavy rain showers and thunder": "25d", # 25d heavyrainshowersandthunder_day
+ "heavy rain showers": "41d", # 41d heavyrainshowers_day
+ "heavy rain": "10", # 10 heavyrain
+ # sleet
+ "light sleet and thunder": "31", # 31 lightsleetandthunder
+ "light sleet showers and thunder": "26d", # 26d lightssleetshowersandthunder_day
+ "light sleet showers": "42d", # 42d lightsleetshowers_day
+ "light sleet": "47", # 47 lightsleet
+ "sleet and thunder": "23", # 23 sleetandthunder
+ "sleet showers and thunder": "20d", # 20d sleetshowersandthunder_day
+ "sleet showers": "07d", # 07d sleetshowers_day
+ "sleet": "12", # 12 sleet
+ "heavy sleet and thunder": "32", # 32 heavysleetandthunder
+ "heavy sleet showers and thunder": "27d", # 27d heavysleetshowersandthunder_day
+ "heavy sleet showers": "43d", # 43d heavysleetshowers_day
+ "heavy sleet": "48", # 48 heavysleet
+ # snow
+ "light snow and thunder": "33", # 33 lightsnowandthunder
+ "light snow showers and thunder": "28d", # 28d lightssnowshowersandthunder_day
+ "light snow showers": "44d", # 44d lightsnowshowers_day
+ "light snow": "49", # 49 lightsnow
+ "snow and thunder": "14", # 14 snowandthunder
+ "snow showers and thunder": "21d", # 21d snowshowersandthunder_day
+ "snow showers": "08d", # 08d snowshowers_day
+ "snow": "13", # 13 snow
+ "heavy snow and thunder": "34", # 34 heavysnowandthunder
+ "heavy snow showers and thunder": "29d", # 29d heavysnowshowersandthunder_day
+ "heavy snow showers": "45d", # 45d heavysnowshowers_day
+ "heavy snow": "50", # 50 heavysnow
}
"""Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_