这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/simple/src/less/style.less
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@import "new_issue.less";
@import "stats.less";
@import "result_templates.less";
@import "weather.less";

// for index.html template
@import "index.less";
Expand Down
38 changes: 38 additions & 0 deletions client/simple/src/less/weather.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#answers .weather {
summary {
display: block;
list-style: none;
}

div.summary {
margin: 0;
padding: 0.5rem 1rem;
background-color: var(--color-header-background);
.rounded-corners-tiny;
Copy link
Member Author

@Bnyro Bnyro May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the arrow to open the weather forecast is misaligned.

2025-05-31T19:13:52,585033174+02:00

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, we can fix it by replacing the <div class="summary"> with <span class="summary">, that's the simplest fix 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your first suggestion "list-style: none" works better .. for now .. the <summary> element is the simplest solution... it's a bit impractical because you can only expand/collapse it in the header. We should perhaps turn it into a horizontal (left/right) slider in a future PR.

}

table {
font-size: 0.9rem;
table-layout: fixed;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}

td {
padding: 0;
}

img.symbol {
width: 5rem;
margin: auto;
display: block;
}

.title {
// background-color: var(--color-result-keyvalue-even);
}

.measured {
// background-color: var(--color-result-keyvalue-odd);
}
}
8 changes: 8 additions & 0 deletions docs/src/searx.weather.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _weather:

=======
Weather
=======

.. automodule:: searx.weather
:members:
14 changes: 11 additions & 3 deletions searx/babel_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ def extract(
namespace = {}
exec(fileobj.read(), {}, namespace) # pylint: disable=exec-used

for name in namespace['__all__']:
for k, v in namespace[name].items():
yield 0, '_', v, ["%s['%s']" % (name, k)]
for obj_name in namespace['__all__']:
obj = namespace[obj_name]
if isinstance(obj, list):
for msg in obj:
# (lineno, funcname, message, comments)
yield 0, '_', msg, [f"{obj_name}"]
elif isinstance(obj, dict):
for k, msg in obj.items():
yield 0, '_', msg, [f"{obj_name}['{k}']"]
else:
raise ValueError(f"{obj_name} should be list or dict")
2 changes: 1 addition & 1 deletion searx/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class ExpireCacheSQLite(sqlitedb.SQLiteAppl, ExpireCache):
# The key/value tables will be created on demand by self.create_table
DDL_CREATE_TABLES = {}

CACHE_TABLE_PREFIX = "CACHE-TABLE-"
CACHE_TABLE_PREFIX = "CACHE-TABLE"

def __init__(self, cfg: ExpireCacheCfg):
"""An instance of the SQLite expire cache is build up from a
Expand Down
189 changes: 111 additions & 78 deletions searx/engines/open_meteo.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Open Meteo (weather)"""

from urllib.parse import urlencode, quote_plus
from urllib.parse import urlencode
from datetime import datetime
from flask_babel import gettext

from searx.network import get
from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults, WeatherAnswer
from searx import weather


about = {
"website": 'https://open-meteo.com',
"website": "https://open-meteo.com",
"wikidata_id": None,
"official_api_documentation": 'https://open-meteo.com/en/docs',
"official_api_documentation": "https://open-meteo.com/en/docs",
"use_official_api": True,
"require_api_key": False,
"results": "JSON",
Expand All @@ -22,97 +22,130 @@
geo_url = "https://geocoding-api.open-meteo.com"
api_url = "https://api.open-meteo.com"

data_of_interest = "temperature_2m,relative_humidity_2m,apparent_temperature,cloud_cover,pressure_msl,wind_speed_10m,wind_direction_10m" # pylint: disable=line-too-long
data_of_interest = (
"temperature_2m",
"apparent_temperature",
"relative_humidity_2m",
"apparent_temperature",
"cloud_cover",
"pressure_msl",
"wind_speed_10m",
"wind_direction_10m",
"weather_code",
# "visibility",
# "is_day",
)


def request(query, params):
location_url = f"{geo_url}/v1/search?name={quote_plus(query)}"

resp = get(location_url)
if resp.status_code != 200:
raise SearxEngineAPIException("invalid geo location response code")

json_locations = resp.json().get("results", [])
if len(json_locations) == 0:
raise SearxEngineAPIException("location not found")
try:
location = weather.GeoLocation.by_query(query)
except ValueError:
return

location = json_locations[0]
args = {
'latitude': location['latitude'],
'longitude': location['longitude'],
'timeformat': 'unixtime',
'format': 'json',
'current': data_of_interest,
'forecast_days': 7,
'hourly': data_of_interest,
"latitude": location.latitude,
"longitude": location.longitude,
"timeformat": "unixtime",
"timezone": "auto", # use timezone of the location
"format": "json",
"current": ",".join(data_of_interest),
"forecast_days": 3,
"hourly": ",".join(data_of_interest),
}

params['url'] = f"{api_url}/v1/forecast?{urlencode(args)}"

return params


def c_to_f(temperature):
return "%.2f" % ((temperature * 1.8) + 32)


def get_direction(degrees):
if degrees < 45 or degrees >= 315:
return "N"

if 45 <= degrees < 135:
return "O"

if 135 <= degrees < 225:
return "S"

return "W"


def generate_condition_table(condition):
res = ""
params["url"] = f"{api_url}/v1/forecast?{urlencode(args)}"


# https://open-meteo.com/en/docs#weather_variable_documentation
# https://nrkno.github.io/yr-weather-symbols/

WMO_TO_CONDITION: dict[int, weather.WeatherConditionType] = {
# 0 Clear sky
0: "clear sky",
# 1, 2, 3 Mainly clear, partly cloudy, and overcast
1: "fair",
2: "partly cloudy",
3: "cloudy",
# 45, 48 Fog and depositing rime fog
45: "fog",
48: "fog",
# 51, 53, 55 Drizzle: Light, moderate, and dense intensity
51: "light rain",
53: "light rain",
55: "light rain",
# 56, 57 Freezing Drizzle: Light and dense intensity
56: "light sleet showers",
57: "light sleet",
# 61, 63, 65 Rain: Slight, moderate and heavy intensity
61: "light rain",
63: "rain",
65: "heavy rain",
# 66, 67 Freezing Rain: Light and heavy intensity
66: "light sleet showers",
67: "light sleet",
# 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
71: "light sleet",
73: "sleet",
75: "heavy sleet",
# 77 Snow grains
77: "snow",
# 80, 81, 82 Rain showers: Slight, moderate, and violent
80: "light rain showers",
81: "rain showers",
82: "heavy rain showers",
# 85, 86 Snow showers slight and heavy
85: "snow showers",
86: "heavy snow showers",
# 95 Thunderstorm: Slight or moderate
95: "rain and thunder",
# 96, 99 Thunderstorm with slight and heavy hail
96: "light snow and thunder",
99: "heavy snow and thunder",
}

res += (
f"<tr><td><b>{gettext('Temperature')}</b></td>"
f"<td><b>{condition['temperature_2m']}°C / {c_to_f(condition['temperature_2m'])}°F</b></td></tr>"
)

res += (
f"<tr><td>{gettext('Feels like')}</td><td>{condition['apparent_temperature']}°C / "
f"{c_to_f(condition['apparent_temperature'])}°F</td></tr>"
)
def _weather_data(location: weather.GeoLocation, data: dict):

res += (
f"<tr><td>{gettext('Wind')}</td><td>{get_direction(condition['wind_direction_10m'])}, "
f"{condition['wind_direction_10m']}° — "
f"{condition['wind_speed_10m']} km/h</td></tr>"
return WeatherAnswer.Item(
location=location,
temperature=weather.Temperature(unit="°C", value=data["temperature_2m"]),
condition=WMO_TO_CONDITION[data["weather_code"]],
feels_like=weather.Temperature(unit="°C", value=data["apparent_temperature"]),
wind_from=weather.Compass(data["wind_direction_10m"]),
wind_speed=weather.WindSpeed(data["wind_speed_10m"], unit="km/h"),
pressure=weather.Pressure(data["pressure_msl"], unit="hPa"),
humidity=weather.RelativeHumidity(data["relative_humidity_2m"]),
cloud_cover=data["cloud_cover"],
)

res += f"<tr><td>{gettext('Cloud cover')}</td><td>{condition['cloud_cover']}%</td>"

res += f"<tr><td>{gettext('Humidity')}</td><td>{condition['relative_humidity_2m']}%</td></tr>"

res += f"<tr><td>{gettext('Pressure')}</td><td>{condition['pressure_msl']}hPa</td></tr>"

return res


def response(resp):
data = resp.json()
location = weather.GeoLocation.by_query(resp.search_params["query"])

table_content = generate_condition_table(data['current'])
res = EngineResults()
json_data = resp.json()

infobox = f"<table><tbody>{table_content}</tbody></table>"
weather_answer = WeatherAnswer(
current=_weather_data(location, json_data["current"]),
service="Open-meteo",
# url="https://open-meteo.com/en/docs",
)

for index, time in enumerate(data['hourly']['time']):
hourly_data = {}
for index, time in enumerate(json_data["hourly"]["time"]):

for key in data_of_interest.split(","):
hourly_data[key] = data['hourly'][key][index]
if time < json_data["current"]["time"]:
# Cut off the hours that are already in the past
continue

table_content = generate_condition_table(hourly_data)
hourly_data = {}
for key in data_of_interest:
hourly_data[key] = json_data["hourly"][key][index]

infobox += f"<h3>{datetime.fromtimestamp(time).strftime('%Y-%m-%d %H:%M')}</h3>"
infobox += f"<table><tbody>{table_content}</tbody></table>"
forecast_data = _weather_data(location, hourly_data)
forecast_data.datetime = weather.DateTime(datetime.fromtimestamp(time))
weather_answer.forecasts.append(forecast_data)

return [{'infobox': 'Open Meteo', 'content': infobox}]
res.add(weather_answer)
return res
Loading