various plugin refactors and fixes
This commit is contained in:
+86
-132
@@ -1,11 +1,6 @@
|
||||
"""
|
||||
Weather plugin – primary: OpenWeatherMap, fallback: Open‑Meteo.
|
||||
|
||||
Uses OpenWeatherMap when a valid API key is present and the request succeeds.
|
||||
Falls back to Open‑Meteo (no key required) otherwise.
|
||||
|
||||
Commands:
|
||||
!weather <location> e.g. !weather London or !weather "New York,US"
|
||||
Outputs a formatted code block with emojis and perfectly aligned columns.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -14,56 +9,14 @@ import aiohttp
|
||||
import simplematrixbotlib as botlib
|
||||
from dotenv import load_dotenv
|
||||
from urllib.parse import quote
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load .env (for OPENWEATHER_API_KEY)
|
||||
# ---------------------------------------------------------------------------
|
||||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(plugin_dir)
|
||||
dotenv_path = os.path.join(parent_dir, ".env")
|
||||
load_dotenv(dotenv_path)
|
||||
from plugins.common import html_escape, collapsible_summary, code_block
|
||||
|
||||
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WMO codes → description + emoji (for Open‑Meteo)
|
||||
# ---------------------------------------------------------------------------
|
||||
WMO_CODES = {
|
||||
0: ("Clear sky", "☀️"),
|
||||
1: ("Mainly clear", "🌤️"),
|
||||
2: ("Partly cloudy", "⛅"),
|
||||
3: ("Overcast", "☁️"),
|
||||
45: ("Fog", "🌫️"),
|
||||
48: ("Depositing rime fog", "🌫️"),
|
||||
51: ("Light drizzle", "🌦️"),
|
||||
53: ("Moderate drizzle", "🌦️"),
|
||||
55: ("Dense drizzle", "🌧️"),
|
||||
56: ("Light freezing drizzle", "🌧️"),
|
||||
57: ("Dense freezing drizzle", "🌧️"),
|
||||
61: ("Slight rain", "🌧️"),
|
||||
63: ("Moderate rain", "🌧️"),
|
||||
65: ("Heavy rain", "🌧️"),
|
||||
66: ("Light freezing rain", "🌧️"),
|
||||
67: ("Heavy freezing rain", "🌧️"),
|
||||
71: ("Slight snow fall", "❄️"),
|
||||
73: ("Moderate snow fall", "❄️"),
|
||||
75: ("Heavy snow fall", "❄️"),
|
||||
77: ("Snow grains", "❄️"),
|
||||
80: ("Slight rain showers", "🌦️"),
|
||||
81: ("Moderate rain showers", "🌧️"),
|
||||
82: ("Violent rain showers", "🌧️"),
|
||||
85: ("Slight snow showers", "🌨️"),
|
||||
86: ("Heavy snow showers", "🌨️"),
|
||||
95: ("Thunderstorm", "⛈️"),
|
||||
96: ("Thunderstorm with slight hail", "⛈️"),
|
||||
99: ("Thunderstorm with heavy hail", "⛈️"),
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Primary: OpenWeatherMap
|
||||
# OpenWeatherMap helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
async def openweathermap_get(session: aiohttp.ClientSession, location: str) -> dict | None:
|
||||
"""Fetch current weather from OpenWeatherMap. Returns None on failure."""
|
||||
if not OPENWEATHER_API_KEY:
|
||||
logging.info("OpenWeatherMap key missing, skipping primary")
|
||||
return None
|
||||
@@ -72,7 +25,7 @@ async def openweathermap_get(session: aiohttp.ClientSession, location: str) -> d
|
||||
params = {
|
||||
"q": location,
|
||||
"appid": OPENWEATHER_API_KEY,
|
||||
"units": "metric", # Celsius
|
||||
"units": "metric",
|
||||
}
|
||||
try:
|
||||
async with session.get(url, params=params, timeout=10) as resp:
|
||||
@@ -83,46 +36,10 @@ async def openweathermap_get(session: aiohttp.ClientSession, location: str) -> d
|
||||
logging.warning(f"OpenWeatherMap request error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def format_openweathermap(data: dict) -> str:
|
||||
"""Build the one-line weather message from OpenWeatherMap data."""
|
||||
city = data.get("name", "Unknown")
|
||||
sys_data = data.get("sys", {})
|
||||
country = sys_data.get("country", "")
|
||||
|
||||
main_data = data.get("main", {})
|
||||
temp_c = main_data.get("temp", 0)
|
||||
temp_f = round(temp_c * 9 / 5 + 32, 1)
|
||||
humidity = main_data.get("humidity", 0)
|
||||
|
||||
weather_list = data.get("weather", [])
|
||||
description = weather_list[0]["description"].capitalize() if weather_list else "Unknown"
|
||||
emoji = "🌡️"
|
||||
if weather_list:
|
||||
wmain = weather_list[0].get("main", "")
|
||||
emoji = {
|
||||
"Clear": "☀️", "Clouds": "☁️", "Rain": "🌧️", "Drizzle": "🌦️",
|
||||
"Thunderstorm": "⛈️", "Snow": "❄️", "Mist": "🌫️", "Fog": "🌫️",
|
||||
"Haze": "🌫️", "Smoke": "🌫️", "Dust": "🌫️", "Sand": "🌫️",
|
||||
"Ash": "🌫️", "Squall": "💨", "Tornado": "🌪️",
|
||||
}.get(wmain, "🌡️")
|
||||
|
||||
wind = data.get("wind", {}).get("speed", 0)
|
||||
|
||||
return (
|
||||
f"<strong>[{emoji} Weather for {city}, {country}]</strong>: "
|
||||
f"<strong>Condition:</strong> {description} | "
|
||||
f"<strong>Temperature:</strong> {temp_c:.1f}°C ({temp_f:.1f}°F) | "
|
||||
f"<strong>Humidity:</strong> {humidity}% | "
|
||||
f"<strong>Wind Speed:</strong> {wind} m/s"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fallback: Open‑Meteo (no key, free)
|
||||
# Open‑Meteo helpers (fallback)
|
||||
# ---------------------------------------------------------------------------
|
||||
async def meteo_geocode(session: aiohttp.ClientSession, location: str) -> dict | None:
|
||||
"""Geocode a city name via Open‑Meteo. Returns location info dict or None."""
|
||||
url = "https://geocoding-api.open-meteo.com/v1/search"
|
||||
params = {"name": location, "count": 1, "language": "en"}
|
||||
try:
|
||||
@@ -144,10 +61,7 @@ async def meteo_geocode(session: aiohttp.ClientSession, location: str) -> dict |
|
||||
logging.warning(f"Open‑Meteo geocode error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def meteo_weather(session: aiohttp.ClientSession, lat: float, lon: float,
|
||||
timezone: str = "auto") -> dict | None:
|
||||
"""Fetch current weather from Open‑Meteo. Returns JSON or None."""
|
||||
async def meteo_weather(session: aiohttp.ClientSession, lat: float, lon: float, timezone: str = "auto") -> dict | None:
|
||||
url = "https://api.open-meteo.com/v1/forecast"
|
||||
params = {
|
||||
"latitude": lat,
|
||||
@@ -165,35 +79,82 @@ async def meteo_weather(session: aiohttp.ClientSession, lat: float, lon: float,
|
||||
logging.warning(f"Open‑Meteo weather error: {e}")
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Formatting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def format_openweathermap(data: dict) -> str:
|
||||
"""Build a code block from OpenWeatherMap response."""
|
||||
city = data.get("name", "Unknown")
|
||||
sys_data = data.get("sys", {})
|
||||
country = sys_data.get("country", "")
|
||||
main = data.get("main", {})
|
||||
temp_c = main.get("temp", 0)
|
||||
temp_f = round(temp_c * 9 / 5 + 32, 1)
|
||||
humidity = main.get("humidity", 0)
|
||||
wind_speed = data.get("wind", {}).get("speed", 0)
|
||||
weather_list = data.get("weather", [])
|
||||
description = weather_list[0]["description"].capitalize() if weather_list else "Unknown"
|
||||
|
||||
emoji_map = {
|
||||
"Clear": "☀️", "Clouds": "☁️", "Rain": "🌧️", "Drizzle": "🌦️",
|
||||
"Thunderstorm": "⛈️", "Snow": "❄️", "Mist": "🌫️", "Fog": "🌫️",
|
||||
"Haze": "🌫️", "Smoke": "🌫️", "Dust": "🌫️", "Sand": "🌫️",
|
||||
"Ash": "🌫️", "Squall": "💨", "Tornado": "🌪️",
|
||||
}
|
||||
main_weather = weather_list[0].get("main", "") if weather_list else ""
|
||||
weather_emoji = emoji_map.get(main_weather, "🌡️")
|
||||
|
||||
location = f"{city}, {country}" if country else city
|
||||
|
||||
rows = [
|
||||
("🌍", "Location", location),
|
||||
(weather_emoji, "Condition", description),
|
||||
("🌡️", "Temperature", f"{temp_c:.1f}°C / {temp_f:.1f}°F"),
|
||||
("💧", "Humidity", f"{humidity}%"),
|
||||
("💨", "Wind Speed", f"{wind_speed} m/s"),
|
||||
]
|
||||
sections = [{"title": "", "rows": rows}]
|
||||
return code_block(f"🌤️ Weather for {location}", sections)
|
||||
|
||||
|
||||
def format_meteo(loc_info: dict, weather_data: dict) -> str:
|
||||
"""Format Open‑Meteo result into the same one‑line style."""
|
||||
"""Build a code block from Open‑Meteo response."""
|
||||
c = weather_data["current_weather"]
|
||||
code = c["weathercode"]
|
||||
desc, emoji = WMO_CODES.get(code, ("Unknown", "🌡️"))
|
||||
wmo_emoji = {
|
||||
0: ("Clear sky", "☀️"),
|
||||
1: ("Mainly clear", "🌤️"),
|
||||
2: ("Partly cloudy", "⛅"),
|
||||
3: ("Overcast", "☁️"),
|
||||
45: ("Fog", "🌫️"),
|
||||
51: ("Light drizzle", "🌦️"),
|
||||
61: ("Slight rain", "🌧️"),
|
||||
63: ("Moderate rain", "🌧️"),
|
||||
71: ("Slight snow", "❄️"),
|
||||
95: ("Thunderstorm", "⛈️"),
|
||||
}
|
||||
desc, emoji = wmo_emoji.get(code, ("Unknown", "🌡️"))
|
||||
|
||||
city = loc_info["name"]
|
||||
country = loc_info.get("country", "")
|
||||
state = loc_info.get("state", "")
|
||||
|
||||
# Build location string
|
||||
parts = [city]
|
||||
if state and state != city:
|
||||
parts.append(state)
|
||||
if country:
|
||||
parts.append(country)
|
||||
loc_str = ", ".join(parts)
|
||||
location_parts = [loc_info["name"]]
|
||||
if loc_info.get("state") and loc_info["state"] != loc_info["name"]:
|
||||
location_parts.append(loc_info["state"])
|
||||
if loc_info.get("country"):
|
||||
location_parts.append(loc_info["country"])
|
||||
location = ", ".join(location_parts)
|
||||
|
||||
temp_f = c["temperature"]
|
||||
temp_c = round((temp_f - 32) * 5 / 9, 1)
|
||||
wind = c["windspeed"]
|
||||
wind = c["windspeed"] # mph
|
||||
|
||||
return (
|
||||
f"<strong>[{emoji} Weather for {loc_str}]</strong>: "
|
||||
f"<strong>Condition:</strong> {desc} | "
|
||||
f"<strong>Temperature:</strong> {temp_c}°C ({temp_f}°F) | "
|
||||
f"<strong>Wind Speed:</strong> {wind} mph"
|
||||
)
|
||||
rows = [
|
||||
("🌍", "Location", location),
|
||||
(emoji, "Condition", desc),
|
||||
("🌡️", "Temperature", f"{temp_c}°C / {temp_f}°F"),
|
||||
("💨", "Wind Speed", f"{wind} mph"),
|
||||
]
|
||||
sections = [{"title": "", "rows": rows}]
|
||||
return code_block(f"🌤️ Weather for {location}", sections)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -218,14 +179,11 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 1. Try OpenWeatherMap
|
||||
owm_data = await openweathermap_get(session, location)
|
||||
if owm_data:
|
||||
if owm_data.get("cod") == 200:
|
||||
msg = format_openweathermap(owm_data)
|
||||
await bot.api.send_markdown_message(room.room_id, msg)
|
||||
logging.info("Sent weather via OpenWeatherMap")
|
||||
return
|
||||
# OpenWeatherMap returned an error status inside JSON (e.g., 401, 404)
|
||||
logging.info("OpenWeatherMap returned error code %s, falling back", owm_data.get("cod"))
|
||||
if owm_data and owm_data.get("cod") == 200:
|
||||
block = format_openweathermap(owm_data)
|
||||
output = collapsible_summary(f"🌤️ Weather: {html_escape(location)}", block)
|
||||
await bot.api.send_markdown_message(room.room_id, output)
|
||||
return
|
||||
|
||||
# 2. Fallback: Open‑Meteo
|
||||
logging.info("Falling back to Open‑Meteo")
|
||||
@@ -233,7 +191,7 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
if not loc_info:
|
||||
await bot.api.send_text_message(
|
||||
room.room_id,
|
||||
f"Location '{location}' not found."
|
||||
f"Location '{html_escape(location)}' not found."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -247,28 +205,24 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
)
|
||||
return
|
||||
|
||||
msg = format_meteo(loc_info, wdata)
|
||||
await bot.api.send_markdown_message(room.room_id, msg)
|
||||
block = format_meteo(loc_info, wdata)
|
||||
output = collapsible_summary(f"🌤️ Weather: {html_escape(location)}", block)
|
||||
await bot.api.send_markdown_message(room.room_id, output)
|
||||
logging.info("Sent weather via Open‑Meteo (fallback)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin setup
|
||||
# ---------------------------------------------------------------------------
|
||||
def setup(bot):
|
||||
logging.info("Weather plugin loaded (OpenWeatherMap + Open‑Meteo fallback)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin Metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__version__ = "1.1.1"
|
||||
__author__ = "Funguy Bot"
|
||||
__description__ = "Weather forecast (OWM primary, Open‑Meteo fallback)"
|
||||
__description__ = "Weather data plugin"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!weather</strong> – Current weather</summary>
|
||||
<p><code>!weather <location></code> – Shows temperature, conditions, humidity, wind.<br>
|
||||
Uses OpenWeatherMap if a valid API key is present; falls back to free Open‑Meteo otherwise.</p>
|
||||
<p><code>!weather <location></code> – Shows temperature, conditions, humidity, wind in a clean, aligned table. Uses OpenWeatherMap primary, Open‑Meteo fallback.</p>
|
||||
</details>
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user