Улучшенная работа погоды + ускорение работы + фикс неработоспособности после пары часов

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-02 21:06:14 +03:00
parent 2d40bc0f9b
commit 845ef7c531
7 changed files with 661 additions and 72 deletions

View File

@@ -41,21 +41,360 @@ def get_wmo_description(code: int) -> str:
}
return codes.get(code, "осадки")
def get_weather_report() -> str:
def get_temperature_text(temp: int) -> str:
"""
Returns the correct Russian form for temperature degrees based on the number.
Handles proper Russian grammar cases (падежи) for temperature values.
"""
# Get the absolute value to handle negative temperatures
abs_temp = abs(temp)
# Get the last digit
last_digit = abs_temp % 10
# Get the last two digits to handle special cases like 11-14
last_two_digits = abs_temp % 100
# Special cases for numbers ending in 11-14 (e.g., 11, 12, 13, 14, 111, 112, etc.)
if 11 <= last_two_digits <= 14:
return f"{temp} градусов"
# Cases based on the last digit
if last_digit == 1:
return f"{temp} градус"
elif 2 <= last_digit <= 4:
return f"{temp} градуса"
else: # 5-9, 0
return f"{temp} градусов"
def normalize_city_name(city_name: str) -> str:
"""
Converts city names from various grammatical cases to the base form for geocoding.
Handles common Russian grammatical cases (падежи) for city names.
"""
# Convert to lowercase for comparison
lower_city = city_name.lower()
# Remove common Russian location descriptors that might be included by mistake
# For example, if someone says "в городе Волгоград", the city_name might be "городе волгоград"
# So we want to extract just "волгоград"
if 'городе' in lower_city:
# Extract the part after "городе"
parts = lower_city.split('городе')
if len(parts) > 1:
lower_city = parts[1].strip()
elif 'город' in lower_city:
# Extract the part after "город"
parts = lower_city.split('город')
if len(parts) > 1:
lower_city = parts[1].strip()
# Common endings for different cases in Russian
# Prepositional case endings (-е, -и, -у, etc.)
prepositional_endings = ['е', 'и', 'у', 'о', 'й']
genitive_endings = ['а', 'я', 'ов', 'ев', 'ин', 'ын']
instrumental_endings = ['ом', 'ем', 'ой', 'ей']
# If the city ends with a prepositional ending, try removing it to get the base form
if lower_city.endswith(tuple(prepositional_endings)):
# Try to remove the ending and see if we get a valid base form
base_form = lower_city
# Try removing 1-2 characters to get the base form
for i in range(2, 0, -1): # Try removing 2 chars, then 1 char
if len(base_form) > i:
potential_base = base_form[:-i]
# Check if the removed part is a common ending
if base_form[-i:] in ['ке', 'ме', 'не', 'ве', 'ге', 'де', 'те']:
base_form = potential_base
break
elif base_form[-1] in prepositional_endings:
base_form = base_form[:-1]
break
# Special handling for common patterns
if base_form.endswith('йорке'): # "нью-йорке" -> "нью-йорк"
base_form = base_form[:-1] + 'к'
elif base_form.endswith('ске'): # "москве" -> "москва", "париже" -> "париж"
# This is more complex, but for "москве" -> "москва", "париже" -> "париж"
# We'll handle the most common cases
if base_form == 'москве':
base_form = 'москва'
elif base_form == 'париже':
base_form = 'париж'
elif base_form == 'лондоне':
base_form = 'лондон'
elif base_form == 'берлине':
base_form = 'берлин'
elif base_form == 'токио': # токио stays токио
base_form = 'токио'
else:
# General rule: replace -е with -а or -ь
if base_form.endswith('ске'):
base_form = base_form[:-1] + 'а'
elif base_form.endswith('ие'):
base_form = base_form[:-2] + 'ия'
# Capitalize appropriately
if base_form != lower_city:
return base_form.capitalize()
# Dictionary mapping specific known variations
case_variations = {
"нью-йорке": "Нью-Йорк",
"нью-йорка": "Нью-Йорк",
"нью-йорком": "Нью-Йорк",
"москве": "Москва",
"москвы": "Москва",
"москвой": "Москва",
"москву": "Москва",
"лондоне": "Лондон",
"лондона": "Лондон",
"лондоном": "Лондон",
"париже": "Париж",
"парижа": "Париж",
"парижем": "Париж",
"берлине": "Берлин",
"берлина": "Берлин",
"берлином": "Берлин",
"пекине": "Пекин",
"пекина": "Пекин",
"пекином": "Пекин",
"роме": "Рим",
"рима": "Рим",
"римом": "Рим",
"мадриде": "Мадрид",
"мадрида": "Мадрид",
"мадридом": "Мадрид",
"сиднее": "Сидней",
"сиднея": "Сидней",
"сиднеем": "Сидней",
"вашингтоне": "Вашингтон",
"вашингтона": "Вашингтон",
"вашингтоном": "Вашингтон",
"лос-анджелесе": "Лос-Анджелес",
"лос-анджелеса": "Лос-Анджелес",
"лос-анджелесом": "Лос-Анджелес",
"сиэтле": "Сиэтл",
"сиэтла": "Сиэтл",
"сиэтлом": "Сиэтл",
"бостоне": "Бостон",
"бостона": "Бостон",
"бостоном": "Бостон",
"денвере": "Денвер",
"денвера": "Денвер",
"денвером": "Денвер",
"хьюстоне": "Хьюстон",
"хьюстона": "Хьюстон",
"хьюстоном": "Хьюстон",
"фениксе": "Феникс",
"феникса": "Феникс",
"фениксом": "Феникс",
"атланте": "Атланта",
"атланты": "Атланта",
"атлантой": "Атланта",
"портленде": "Портленд",
"портленда": "Портленд",
"портлендом": "Портленд",
"остине": "Остин",
"остина": "Остин",
"остином": "Остин",
"нэшвилле": "Нэшвилл",
"нэшвилла": "Нэшвилл",
"нэшвиллом": "Нэшвилл",
"сан-франциско": "Сан-Франциско",
"токио": "Токио",
"торонто": "Торонто",
"чикаго": "Чикаго",
"майами": "Майами",
}
return case_variations.get(lower_city, city_name)
def get_coordinates_by_city(city_name: str) -> tuple:
"""
Gets coordinates (lat, lon) for a given city name using Open-Meteo geocoding API.
Returns (lat, lon, city_display_name) or (None, None, None) if not found.
"""
# First try with the original name
try_names = [city_name]
# Add normalized version
normalized_city = normalize_city_name(city_name)
if normalized_city != city_name:
try_names.append(normalized_city)
# Also try with English version if it's a known translation
city_to_eng = {
"москва": "Moscow",
"санкт-петербург": "Saint Petersburg",
"новосибирск": "Novosibirsk",
"екатеринбург": "Yekaterinburg",
"казань": "Kazan",
"нижний новгород": "Nizhny Novgorod",
"челябинск": "Chelyabinsk",
"омск": "Omsk",
"самара": "Samara",
"ростов-на-дону": "Rostov-on-Don",
"уфа": "Ufa",
"красноярск": "Krasnoyarsk",
"владивосток": "Vladivostok",
"сочи": "Sochi",
"новокузнецк": "Novokuznetsk",
"ярославль": "Yaroslavl",
"владикавказ": "Vladikavkaz",
"магнитогорск": "Magnitogorsk",
"иркутск": "Irkutsk",
"хабаровск": "Khabarovsk",
"оренбург": "Orenburg",
"калининград": "Kaliningrad",
"пермь": "Perm",
"волгоград": "Volgograd",
"волгограде": "Volgograd",
"краснодар": "Krasnodar",
"саратов": "Saratov",
"тында": "Tynda",
"тольятти": "Tolyatti",
"барнаул": "Barnaul",
"улан-удэ": "Ulan-Ude",
"иваново": "Ivanovo",
"мурманск": "Murmansk",
"кузнецк": "Kuznetsk",
"архангельск": "Arkhangelsk",
"владимир": "Vladimir",
"калининград": "Kaliningrad",
"смоленск": "Smolensk",
"калука": "Kaluga",
"воронеж": "Voronezh",
"курск": "Kursk",
"астрахань": "Astrakhan",
"липецк": "Lipetsk",
"тамбов": "Tambov",
"курган": "Kurgan",
"пенза": "Penza",
"рязн": "Ryazan",
"орёл": "Oryol",
"якутск": "Yakutsk",
"владикавказ": "Vladikavkaz",
"магас": "Magas",
"нарьян-мар": "Naryan-Mar",
"ханты-мансийск": "Khanty-Mansiysk",
"анадырь": "Anadyr",
"салехард": "Salekhard",
"лондон": "London",
"нью-йорк": "New York",
"токио": "Tokyo",
"париж": "Paris",
"берлин": "Berlin",
"мадрид": "Madrid",
"рим": "Rome",
"милан": "Milan",
"венеция": "Venice",
"амстердам": "Amsterdam",
"прага": "Prague",
"будапешт": "Budapest",
"вена": "Vienna",
"варшава": "Warsaw",
"киев": "Kyiv",
"минск": "Minsk",
"ташкент": "Tashkent",
"алматы": "Almaty",
"астана": "Astana",
"баку": "Baku",
"ереван": "Yerevan",
"тбилиси": "Tbilisi",
"софия": "Sofia",
"белград": "Belgrade",
"любляна": "Ljubljana",
"загреб": "Zagreb",
"рекьявик": "Reykjavik",
"осло": "Oslo",
"стокгольм": "Stockholm",
"копенгаген": "Copenhagen",
"хельсинки": "Helsinki",
"дублин": "Dublin",
"эдинбург": "Edinburgh",
"манчестер": "Manchester",
"бirmingham": "Birmingham",
"ливерпуль": "Liverpool",
"глазго": "Glasgow",
"брюссель": "Brussels",
"цюрих": "Zurich",
"женева": "Geneva",
"осака": "Osaka",
"киото": "Kyoto",
"сингапур": "Singapore",
"бангкок": "Bangkok",
"пекин": "Beijing",
"шанхай": "Shanghai",
"гонконг": "Hong Kong",
"сеул": "Seoul",
"дели": "Delhi",
"мумбаи": "Mumbai",
"бомбей": "Mumbai",
}
eng_name = city_to_eng.get(city_name.lower())
if eng_name:
try_names.append(eng_name)
# Try each name in sequence
for name_to_try in try_names:
try:
# Use Open-Meteo's geocoding API
geocode_url = "https://geocoding-api.open-meteo.com/v1/search"
params = {
"name": name_to_try,
"count": 1,
"language": "ru",
"format": "json"
}
response = requests.get(geocode_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if "results" in data and len(data["results"]) > 0:
result = data["results"][0]
lat = result["latitude"]
lon = result["longitude"]
display_name = result.get("name", name_to_try) # Use the name from the API if available
return lat, lon, display_name
except Exception as e:
print(f"❌ Ошибка при поиске координат для города {name_to_try}: {e}")
continue # Try the next name
return None, None, None
def get_weather_report(requested_city: str = None) -> str:
"""
Fetches detailed weather report.
Structure:
1. Current temp and precipitation.
2. Today's min/max temp.
3. Next 4 hours forecast (temp + precipitation).
Args:
requested_city: Optional city name to get weather for. If None, uses default city.
"""
if not all([WEATHER_LAT, WEATHER_LON, WEATHER_CITY]):
return "Настройки погоды не найдены. Проверьте конфигурацию."
# Determine which city to use
if requested_city:
# Try to get coordinates for the requested city
lat, lon, city_display_name = get_coordinates_by_city(requested_city)
if lat is None or lon is None:
return f"Не удалось найти город {requested_city}. Проверьте название и попробуйте снова."
else:
# Use default city from config
if not all([WEATHER_LAT, WEATHER_LON, WEATHER_CITY]):
return "Настройки погоды не найдены. Проверьте конфигурацию."
lat = float(WEATHER_LAT)
lon = float(WEATHER_LON)
city_display_name = WEATHER_CITY
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": WEATHER_LAT,
"longitude": WEATHER_LON,
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,precipitation,weather_code",
"hourly": "temperature_2m,precipitation,weather_code",
"daily": "temperature_2m_max,temperature_2m_min",
@@ -64,7 +403,7 @@ def get_weather_report() -> str:
}
try:
response = requests.get(url, params=params, timeout=5)
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
@@ -75,7 +414,7 @@ def get_weather_report() -> str:
code_now = curr["weather_code"]
desc_now = get_wmo_description(code_now)
report = f"Сейчас в городе {WEATHER_CITY} {temp_now} градусов, {desc_now}."
report = f"Сейчас в городе {city_display_name} {get_temperature_text(temp_now)}, {desc_now}."
if precip_now > 0:
report += f" Выпало {precip_now} миллиметров осадков."
@@ -84,41 +423,33 @@ def get_weather_report() -> str:
daily = data["daily"]
t_max = round(daily["temperature_2m_max"][0])
t_min = round(daily["temperature_2m_min"][0])
report += f" Сегодня температура будет от {t_min} до {t_max} градусов."
report += f" Сегодня температура будет от {get_temperature_text(t_min)} до {get_temperature_text(t_max)}."
# --- 3. Forecast Next 4 Hours ---
current_hour = datetime.now().hour
hourly_temps = data["hourly"]["temperature_2m"]
hourly_precip = data["hourly"]["precipitation"]
hourly_codes = data["hourly"]["weather_code"]
# Start from next hour
start_idx = current_hour + 1
end_idx = min(start_idx + 4, len(hourly_temps))
next_temps = hourly_temps[start_idx:end_idx]
next_precip = hourly_precip[start_idx:end_idx]
next_codes = hourly_codes[start_idx:end_idx]
if next_temps:
report += " Прогноз на ближайшие 4 часа: "
# Group by roughly similar weather to avoid repetition?
# Or just list them simply.
# "В 14:00 -5, ясно. В 15:00 -5, снег." -> a bit verbose.
# Simplified: "Температура около -5, возможен слабый снег."
# Let's verify if weather changes significantly.
# If consistent, summarize. If not, list.
# Simple approach for TTS:
avg_temp = round(sum(next_temps) / len(next_temps))
# Check if any precipitation is expected
will_precip = any(p > 0 for p in next_precip)
unique_codes = set(next_codes)
# Determine dominant weather description
if len(unique_codes) == 1:
weather_desc = get_wmo_description(list(unique_codes)[0])
@@ -130,15 +461,27 @@ def get_weather_report() -> str:
else:
weather_desc = "переменная облачность"
report += f"температура около {avg_temp} градусов, {weather_desc}."
report += f"температура около {get_temperature_text(avg_temp)}, {weather_desc}."
if will_precip:
report += " Ожидаются осадки."
else:
report += " Без существенных осадков."
return report
except requests.exceptions.ConnectionError:
print(f"❌ Ошибка подключения к сервису погоды: невозможно подключиться к серверу")
return f"Не удалось подключиться к сервису погоды. Проверьте интернет-соединение."
except requests.exceptions.Timeout:
print(f"❌ Таймаут запроса к сервису погоды")
return f"Время ожидания ответа от сервиса погоды истекло."
except requests.exceptions.HTTPError as e:
print(f"❌ HTTP ошибка при получении погоды: {e}")
return f"Ошибка при получении данных о погоде: {e}"
except KeyError as e:
print(f"❌ Ошибка структуры данных погоды: {e}")
return f"Получены некорректные данные о погоде."
except Exception as e:
print(f"❌ Ошибка получения погоды: {e}")
return "Не удалось получить полные данные о погоде."