[feat] wttr.in: migrate to new weather engine template (#4888)

Author Notes
- wttr.in provides 8 hourly time forecasts per day, I assumed that they're always describing the weather for 3 hours each, starting at 1 o'clock in the morning

related:
- https://github.com/searxng/searxng/pull/4663
- https://github.com/searxng/searxng/issues/4885
This commit is contained in:
Bnyro 2025-07-03 16:42:13 +02:00 committed by GitHub
parent 99033f548e
commit 0cbb4f74cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 158 additions and 163 deletions

View File

@ -1,9 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""wttr.in (weather forecast service)""" """wttr.in (weather forecast service)"""
from json import loads
from urllib.parse import quote 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 = { about = {
"website": "https://wttr.in", "website": "https://wttr.in",
@ -18,118 +20,105 @@ categories = ["weather"]
url = "https://wttr.in/{query}?format=j1&lang={lang}" url = "https://wttr.in/{query}?format=j1&lang={lang}"
# adapted from https://github.com/chubin/wttr.in/blob/master/lib/constants.py
def get_weather_condition_key(lang): WWO_TO_CONDITION: dict[str, weather.WeatherConditionType] = {
if lang == "en": "113": "clear sky",
return "weatherDesc" "116": "partly cloudy",
"119": "cloudy",
return "lang_" + lang.lower() "122": "fair",
"143": "fair",
"176": "light rain showers",
def generate_day_table(day): "179": "light snow showers",
res = "" "182": "light sleet showers",
"185": "light sleet",
res += f"<tr><td>{gettext('Average temp.')}</td><td>{day['avgtempC']}°C / {day['avgtempF']}°F</td></tr>" "200": "rain and thunder",
res += f"<tr><td>{gettext('Min temp.')}</td><td>{day['mintempC']}°C / {day['mintempF']}°F</td></tr>" "227": "light snow",
res += f"<tr><td>{gettext('Max temp.')}</td><td>{day['maxtempC']}°C / {day['maxtempF']}°F</td></tr>" "230": "heavy snow",
res += f"<tr><td>{gettext('UV index')}</td><td>{day['uvIndex']}</td></tr>" "248": "fog",
res += f"<tr><td>{gettext('Sunrise')}</td><td>{day['astronomy'][0]['sunrise']}</td></tr>" "260": "fog",
res += f"<tr><td>{gettext('Sunset')}</td><td>{day['astronomy'][0]['sunset']}</td></tr>" "263": "light rain showers",
"266": "light rain showers",
return res "281": "light sleet showers",
"284": "light snow showers",
"293": "light rain showers",
def generate_condition_table(condition, lang, current=False): "296": "light rain",
res = "" "299": "rain showers",
"302": "rain",
if current: "305": "heavy rain showers",
key = "temp_" "308": "heavy rain",
else: "311": "light sleet",
key = "temp" "314": "sleet",
"317": "light sleet",
res += ( "320": "heavy sleet",
f"<tr><td><b>{gettext('Condition')}</b></td>" "323": "light snow showers",
f"<td><b>{condition[get_weather_condition_key(lang)][0]['value']}</b></td></tr>" "326": "light snow showers",
) "329": "heavy snow showers",
res += ( "332": "heavy snow",
f"<tr><td><b>{gettext('Temperature')}</b></td>" "335": "heavy snow showers",
f"<td><b>{condition[key+'C']}°C / {condition[key+'F']}°F</b></td></tr>" "338": "heavy snow",
) "350": "light sleet",
res += ( "353": "light rain showers",
f"<tr><td>{gettext('Feels like')}</td><td>{condition['FeelsLikeC']}°C / {condition['FeelsLikeF']}°F</td></tr>" "356": "heavy rain showers",
) "359": "heavy rain",
res += ( "362": "light sleet showers",
f"<tr><td>{gettext('Wind')}</td><td>{condition['winddir16Point']}" "365": "sleet showers",
f"{condition['windspeedKmph']} km/h / {condition['windspeedMiles']} mph</td></tr>" "368": "light snow showers",
) "371": "heavy snow showers",
res += ( "374": "light sleet showers",
f"<tr><td>{gettext('Visibility')}</td><td>{condition['visibility']} km / {condition['visibilityMiles']} mi</td>" "377": "heavy sleet",
) "386": "rain showers and thunder",
res += f"<tr><td>{gettext('Humidity')}</td><td>{condition['humidity']}%</td></tr>" "389": "heavy rain showers and thunder",
"392": "snow showers and thunder",
return res "395": "heavy snow showers",
}
def request(query, params): 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["url"] = url.format(query=quote(query), lang=params["language"])
params["raise_for_httperror"] = False params["raise_for_httperror"] = False
return params return params
def response(resp): def _weather_data(location: weather.GeoLocation, data: dict):
results = [] # 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 WeatherAnswer.Item(
return [] location=location,
temperature=weather.Temperature(unit="°C", value=tempC),
result = loads(resp.text) condition=WWO_TO_CONDITION[data["weatherCode"]],
feels_like=weather.Temperature(unit="°C", value=data["FeelsLikeC"]),
current = result["current_condition"][0] wind_from=weather.Compass(int(data["winddirDegree"])),
location = result['nearest_area'][0] wind_speed=weather.WindSpeed(data["windspeedKmph"], unit="km/h"),
pressure=weather.Pressure(data["pressure"], unit="hPa"),
forecast_indices = {3: gettext('Morning'), 4: gettext('Noon'), 6: gettext('Evening'), 7: gettext('Night')} humidity=weather.RelativeHumidity(data["humidity"]),
cloud_cover=data["cloudcover"],
title = f"{location['areaName'][0]['value']}, {location['region'][0]['value']}"
infobox = f"<h3>{gettext('Current condition')}</h3><table><tbody>"
infobox += generate_condition_table(current, resp.search_params['language'], True)
infobox += "</tbody></table>"
for day in result["weather"]:
infobox += f"<h3>{day['date']}</h3>"
infobox += "<table><tbody>"
infobox += generate_day_table(day)
infobox += "</tbody></table>"
infobox += "<table><tbody>"
for time in forecast_indices.items():
infobox += f"<tr><td rowspan=\"7\"><b>{time[1]}</b></td></tr>"
infobox += generate_condition_table(day['hourly'][time[0]], resp.search_params['language'])
infobox += "</tbody></table>"
results.append(
{
"infobox": title,
"content": infobox,
}
) )
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

View File

@ -481,46 +481,49 @@ class Compass:
WeatherConditionType = typing.Literal[ WeatherConditionType = typing.Literal[
# The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel") # The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel")
"clear sky", "clear sky",
"partly cloudy",
"cloudy", "cloudy",
"fair", "fair",
"fog", "fog",
"heavy rain and thunder", # rain
"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",
"light rain and thunder", "light rain and thunder",
"light rain showers and thunder", "light rain showers and thunder",
"light rain showers", "light rain showers",
"light rain", "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 and thunder",
"rain showers and thunder", "rain showers and thunder",
"rain showers", "rain showers",
"rain", "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 and thunder",
"sleet showers and thunder", "sleet showers and thunder",
"sleet showers", "sleet showers",
"sleet", "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 and thunder",
"snow showers and thunder", "snow showers and thunder",
"snow showers", "snow showers",
"snow", "snow",
"heavy snow and thunder",
"heavy snow showers and thunder",
"heavy snow showers",
"heavy snow",
] ]
"""Standardized designations for weather conditions. The designators were """Standardized designations for weather conditions. The designators were
taken from a collaboration between NRK and Norwegian Meteorological Institute 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 = { YR_WEATHER_SYMBOL_MAP = {
"clear sky": "01d", # 01d clearsky_day "clear sky": "01d", # 01d clearsky_day
"fair": "02d", # 02d fair_day
"partly cloudy": "03d", # 03d partlycloudy_day "partly cloudy": "03d", # 03d partlycloudy_day
"cloudy": "04", # 04 cloudy "cloudy": "04", # 04 cloudy
"light rain showers": "40d", # 40d lightrainshowers_day "fair": "02d", # 02d fair_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
"fog": "15", # 15 fog "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`_ """Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_