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"" - - infobox += generate_condition_table(day['hourly'][time[0]], resp.search_params['language']) - - infobox += "
{time[1]}
" - - 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`_