mirror of
https://github.com/TheCommsChannel/TC2-BBS-mesh.git
synced 2025-07-04 10:36:47 -04:00

- Added weather plugin with functionality to fetch and display weather information using the OpenWeather API. - Integrated weather command into the main menu and updated command handlers to manage user interactions for weather queries. - Updated .gitignore to include database and configuration files. - Created a test client for interactive testing of the BBS system. - Added example configuration for the weather plugin.
154 lines
No EOL
5.2 KiB
Python
154 lines
No EOL
5.2 KiB
Python
"""
|
|
Weather Module for EDC LV BBS
|
|
|
|
This module provides weather information using the OpenWeather API.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import requests
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from configparser import ConfigParser
|
|
from requests.adapters import HTTPAdapter
|
|
from urllib3.util.retry import Retry
|
|
|
|
# OpenWeather API configuration
|
|
config = ConfigParser()
|
|
config_path = os.path.join(os.path.dirname(__file__), 'weather.ini')
|
|
|
|
if not os.path.exists(config_path):
|
|
logging.error(f"Weather configuration file not found at {config_path}")
|
|
raise FileNotFoundError(f"Weather configuration file not found at {config_path}")
|
|
|
|
try:
|
|
config.read(config_path)
|
|
except Exception as e:
|
|
logging.error(f"Error reading weather configuration: {e}")
|
|
raise
|
|
|
|
OPENWEATHER_API_KEY = config.get('openweather', 'api_key', fallback='')
|
|
WEATHER_API_URL = "http://api.openweathermap.org/data/2.5/weather"
|
|
|
|
# Rate limiting configuration
|
|
MAX_CALLS_PER_MINUTE = int(config.get('openweather', 'max_calls_per_minute', fallback='30'))
|
|
MAX_CALLS_PER_DAY = int(config.get('openweather', 'max_calls_per_day', fallback='500'))
|
|
|
|
# Create a session object for better connection handling
|
|
session = requests.Session()
|
|
retry_strategy = Retry(
|
|
total=3, # number of retries
|
|
backoff_factor=1, # wait 1, 2, 4 seconds between retries
|
|
status_forcelist=[500, 502, 503, 504] # HTTP status codes to retry on
|
|
)
|
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
session.mount("http://", adapter)
|
|
session.mount("https://", adapter)
|
|
|
|
# Rate limiting state
|
|
minute_calls = 0
|
|
day_calls = 0
|
|
last_minute = datetime.now().replace(second=0, microsecond=0)
|
|
last_day = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
def check_rate_limits():
|
|
"""
|
|
Check if we're within rate limits. Updates counters and resets them when needed.
|
|
|
|
Returns:
|
|
bool: True if we can make a call, False if we've hit a limit
|
|
"""
|
|
global minute_calls, day_calls, last_minute, last_day
|
|
|
|
now = datetime.now()
|
|
current_minute = now.replace(second=0, microsecond=0)
|
|
current_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# Reset minute counter if we're in a new minute
|
|
if current_minute > last_minute:
|
|
minute_calls = 0
|
|
last_minute = current_minute
|
|
|
|
# Reset day counter if we're in a new day
|
|
if current_day > last_day:
|
|
day_calls = 0
|
|
last_day = current_day
|
|
|
|
# Check if we've hit any limits
|
|
if minute_calls >= MAX_CALLS_PER_MINUTE:
|
|
logging.warning(f"Weather API minute limit reached ({MAX_CALLS_PER_MINUTE} calls)")
|
|
return False
|
|
|
|
if day_calls >= MAX_CALLS_PER_DAY:
|
|
logging.warning(f"Weather API daily limit reached ({MAX_CALLS_PER_DAY} calls)")
|
|
return False
|
|
|
|
# We're good to make a call
|
|
minute_calls += 1
|
|
day_calls += 1
|
|
return True
|
|
|
|
def kelvin_to_fahrenheit(kelvin):
|
|
"""Convert Kelvin to Fahrenheit."""
|
|
return (kelvin - 273.15) * 9/5 + 32
|
|
|
|
def get_weather_by_zip(zip_code, country_code='US'):
|
|
"""
|
|
Get weather information from OpenWeather API by zip code.
|
|
|
|
Args:
|
|
zip_code: ZIP code to get weather for
|
|
country_code: Country code (default: US)
|
|
|
|
Returns:
|
|
dict: Weather data if successful, None if failed
|
|
"""
|
|
if not OPENWEATHER_API_KEY:
|
|
logging.error("OpenWeather API key not configured")
|
|
return None
|
|
|
|
# Check rate limits before making the call
|
|
if not check_rate_limits():
|
|
logging.error("Weather API rate limit exceeded")
|
|
return None
|
|
|
|
try:
|
|
params = {
|
|
'zip': f"{zip_code},{country_code}",
|
|
'appid': OPENWEATHER_API_KEY
|
|
}
|
|
response = session.get(WEATHER_API_URL, params=params, timeout=5)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except requests.Timeout:
|
|
logging.error(f"Weather API request timed out after 5 seconds for zip code {zip_code}")
|
|
return {"error": "Request timed out - weather service is taking too long to respond"}
|
|
except requests.exceptions.RequestException as e:
|
|
logging.error(f"Error fetching weather data: {e}")
|
|
return None
|
|
|
|
def format_weather_response(weather_data):
|
|
"""Format weather data into a user-friendly message."""
|
|
if not weather_data:
|
|
return "❌ Failed to get weather data."
|
|
|
|
if "error" in weather_data:
|
|
return f"❌ {weather_data['error']}"
|
|
|
|
try:
|
|
temp = kelvin_to_fahrenheit(weather_data['main']['temp'])
|
|
feels_like = kelvin_to_fahrenheit(weather_data['main']['feels_like'])
|
|
humidity = weather_data['main']['humidity']
|
|
description = weather_data['weather'][0]['description'].capitalize()
|
|
city = weather_data['name']
|
|
|
|
response = f"🌡️ Weather for {city} 🌡️\n\n"
|
|
response += f"Temperature: {temp:.1f}°F\n"
|
|
response += f"Feels like: {feels_like:.1f}°F\n"
|
|
response += f"Humidity: {humidity}%\n"
|
|
response += f"Conditions: {description}\n"
|
|
|
|
return response
|
|
except (KeyError, IndexError) as e:
|
|
logging.error(f"Error formatting weather data: {e}")
|
|
return "❌ Error processing weather data" |