From 7411c4365251e80ec5eef9504b83f29a06e0038d Mon Sep 17 00:00:00 2001 From: FlyingFathead Date: Tue, 8 Oct 2024 00:28:03 +0300 Subject: [PATCH] `v0.75053` - only include eligible territories in U.S. NWS queries --- README.md | 2 + config/config.ini | 12 +- src/api_get_openweathermap.py | 261 ++++++++++++++++++++++------------ src/config_paths.py | 2 + src/main.py | 2 +- 5 files changed, 187 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index bd8a82c..b57ad5d 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,8 @@ If you run into any issues, consult the logs or reach out on the repository's [I --- # Changelog +- v0.75053 - only include eligible territories in U.S. NWS queries + - list of queried / eligible territories can be set in `config.ini` under the `NWS` section - v0.75052 - include the details from U.S. National Weather Service on alerts - v0.75051 - updated `config.ini` for configuring NWS weather forecasts & alerts - suggested method is to supplement via NWS the additional weather data you need diff --git a/config/config.ini b/config/config.ini index 272736e..c2ea7ef 100644 --- a/config/config.ini +++ b/config/config.ini @@ -174,7 +174,17 @@ ChunkSize = 500 # (weather.gov) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [NWS] -# Fetch NWS foreacsts and/or alerts? (true/false) +# Set NwsOnlyEligibleCountries to False if you want to include U.S. NWS calls outside of the country list. +NwsOnlyEligibleCountries = True +# 'NwsEligibleCountries' is a configurable list of countries (in ISO-3166 country code format) +# that are eligible for NWS data. You can add or remove countries from this list as needed. +# (Legend:) +# AQ: Antarctica (for U.S. research stations) +# UM: United States Minor Outlying Islands (like Wake Island, Midway Atoll, etc.) +# XW: International Waters (this isn't an official ISO code but could be used as a placeholder for maritime areas under U.S. influence or international jurisdictions) +# ZZ: Unknown or undefined region (could be used as a placeholder for situations where precise location data isn't available or relevant) +NwsEligibleCountries = US, PR, GU, AS, VI, MP, CA, MX, AQ, UM, XW, ZZ +# Fetch NWS foreacsts and/or alerts (true/false) # Note that the service can be slow and unreliable at times. # I recommand getting the alerts to supplement i.e. OpenWeatherMap. # The alerts usually work, but sadly their open API forecasts are often broken. diff --git a/src/api_get_openweathermap.py b/src/api_get_openweathermap.py index 331c7ed..42cf0fc 100644 --- a/src/api_get_openweathermap.py +++ b/src/api_get_openweathermap.py @@ -3,8 +3,8 @@ # github.com/FlyingFathead/TelegramBot-OpenAI-API/ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # -# >>> weather fetcher module version: v0.728 -# >>> (Updated July 13 2024) +# >>> weather fetcher module version: v0.75053 +# >>> (Updated Oct 8 2024) # # This API functionality requires both OpenWeatherMap and MapTiler API keys. # You can get both from the corresponding service providers. @@ -14,7 +14,10 @@ # Import the NWS data fetching function from api_get_nws_weather import get_nws_forecast, get_nws_alerts -from config_paths import NWS_USER_AGENT, NWS_RETRIES, NWS_RETRY_DELAY, FETCH_NWS_FORECAST, FETCH_NWS_ALERTS +from config_paths import NWS_USER_AGENT, NWS_RETRIES, NWS_RETRY_DELAY, FETCH_NWS_FORECAST, FETCH_NWS_ALERTS, NWS_ELIGIBLE_COUNTRIES, NWS_ONLY_ELIGIBLE_COUNTRIES + +# linter annotation (use only if you use the v2 method of `get_coordinates`) +# from typing import Optional, Tuple # date & time utils import datetime as dt @@ -82,26 +85,40 @@ async def get_weather(city_name, country, exclude='', units='metric', lang='fi') # combined_data = await combine_weather_data(city_name, resolved_country, lat, lon, current_weather_data, forecast_data, moon_phase_data, daily_forecast_data, current_weather_data_from_weatherapi, astronomy_data, additional_data) # return combined_data - # Fetch NWS data - logging.info("Fetching NWS data.") - nws_data = await get_nws_forecast(lat, lon) - if nws_data: - logging.info("NWS data fetched successfully.") - nws_forecast = nws_data.get('nws_forecast') - nws_forecast_hourly = nws_data.get('nws_forecast_hourly') - else: - logging.warning("Failed to fetch NWS data.") + # Check if NWS should only be used for eligible countries + if NWS_ONLY_ELIGIBLE_COUNTRIES and resolved_country.upper() not in NWS_ELIGIBLE_COUNTRIES: + logging.info(f"NOTE: NWS data will not be fetched as {resolved_country.upper()} is not in the eligible country list.") nws_forecast = None nws_forecast_hourly = None - - # Fetch NWS alerts data - logging.info("Fetching NWS alerts data.") - nws_alerts = await get_nws_alerts(lat, lon) - if nws_alerts: - logging.info(f"Fetched {len(nws_alerts)} active NWS alerts.") + nws_alerts = None else: - logging.info("No active NWS alerts found.") - + # Ensure that NWS API requests are made only if eligible + try: + logging.info("Fetching NWS data.") + nws_data = await get_nws_forecast(lat, lon) + if nws_data: + logging.info("NWS data fetched successfully.") + nws_forecast = nws_data.get('nws_forecast') + nws_forecast_hourly = nws_data.get('nws_forecast_hourly') + else: + logging.warning("Failed to fetch NWS data.") + nws_forecast = None + nws_forecast_hourly = None + + # Fetch NWS alerts data only if the forecast is successful + logging.info("Fetching NWS alerts data.") + nws_alerts = await get_nws_alerts(lat, lon) + if nws_alerts: + logging.info(f"Fetched {len(nws_alerts)} active NWS alerts.") + else: + logging.info("No active NWS alerts found.") + except Exception as e: + logging.error(f"Error fetching NWS data: {e}") + nws_forecast = None + nws_forecast_hourly = None + nws_alerts = None + + # combine the weather data combined_data = await combine_weather_data( city_name, resolved_country, lat, lon, current_weather_data, forecast_data, moon_phase_data, @@ -114,7 +131,49 @@ async def get_weather(city_name, country, exclude='', units='metric', lang='fi') logging.error(f"Failed to fetch weather data: {current_weather_response.text} / {forecast_response.text}") return "[Inform the user that data fetching the weather data failed, current information could not be fetched. Reply in the user's language.]" -# get coordinates +# # get coordinates (method 2; might introduce complexity; needs `Typing`) +# async def get_coordinates(city_name: str, country: Optional[str] = None) -> Tuple[Optional[float], Optional[float], Optional[str]]: +# lat: Optional[float] = None +# lon: Optional[float] = None +# resolved_country: Optional[str] = None + +# logging.info(f"Coordinates for {city_name}, {country}: Latitude: {lat}, Longitude: {lon}") +# api_key = os.getenv('MAPTILER_API_KEY') + +# if not api_key: +# logging.info("[WARNING] MapTiler API key not set. You need to set the 'MAPTILER_API_KEY' environment variable to use coordinate lookups!") +# return None, None, None + +# query = f"{city_name}" +# if country: +# query += f", {country}" + +# geocode_url = f"https://api.maptiler.com/geocoding/{query}.json?key={api_key}" +# logging.info(f"Making API request to URL: {geocode_url}") + +# async with httpx.AsyncClient() as client: +# response = await client.get(geocode_url) +# logging.info(f"Received response with status code: {response.status_code}") + +# if response.status_code == 200: +# data = response.json() +# logging.info(f"Response data: {data}") + +# if data.get('features'): +# feature = data['features'][0] +# lat = feature['geometry']['coordinates'][1] +# lon = feature['geometry']['coordinates'][0] +# resolved_country = feature['properties'].get('country_code', 'Unknown') +# logging.info(f"Coordinates for {city_name}, {resolved_country}: Latitude: {lat}, Longitude: {lon}") +# return lat, lon, resolved_country +# else: +# logging.error("No features found in the geocoding response.") +# return None, None, None +# else: +# logging.error(f"Failed to fetch coordinates: {response.text}") +# return None, None, None + +# # // (old method for country lookup) async def get_coordinates(city_name, country=None): lat = lon = None resolved_country = None @@ -208,7 +267,7 @@ def convert_to_24_hour(time_str, timezone_str): # combined weather data # async def combine_weather_data(city_name, country, lat, lon, current_weather_data, forecast_data, moon_phase_data, daily_forecast_data, current_weather_data_from_weatherapi, astronomy_data, additional_data): # Define the combine_weather_data function with NWS integration -async def combine_weather_data(city_name, country, lat, lon, current_weather_data, forecast_data, moon_phase_data, daily_forecast_data, current_weather_data_from_weatherapi, astronomy_data, additional_data, nws_forecast, nws_forecast_hourly): +async def combine_weather_data(city_name, resolved_country, lat, lon, current_weather_data, forecast_data, moon_phase_data, daily_forecast_data, current_weather_data_from_weatherapi, astronomy_data, additional_data, nws_forecast, nws_forecast_hourly): tf = TimezoneFinder() timezone_str = tf.timezone_at(lat=lat, lng=lon) local_timezone = pytz.timezone(timezone_str) @@ -360,7 +419,6 @@ async def combine_weather_data(city_name, country, lat, lon, current_weather_dat combined_info = f"{detailed_weather_info}\n\n{final_forecast}" - # Append NWS data (Forecasts) if nws_forecast: nws_forecast_info = "" @@ -398,73 +456,96 @@ async def combine_weather_data(city_name, country, lat, lon, current_weather_dat combined_info += f"\n{nws_forecast_info}\n{nws_hourly_forecast_info}" - # Fetch and append NWS Alerts - try: - # Round coordinates to 4 decimal places to comply with NWS API - lat_rounded = round(lat, 4) - lon_rounded = round(lon, 4) - alerts_url = f"https://api.weather.gov/alerts/active?point={lat_rounded},{lon_rounded}" - async with httpx.AsyncClient(follow_redirects=True) as client: - alerts_response = await client.get(alerts_url, headers={'User-Agent': NWS_USER_AGENT}) - alerts_response.raise_for_status() - alerts_data = alerts_response.json() - except httpx.HTTPStatusError as e: - logging.error(f"NWS Alerts HTTP error: {e.response.status_code} - {e.response.text}") - alerts_data = None - except Exception as e: - logging.error(f"Error fetching NWS alerts: {e}") - alerts_data = None - - alerts_info = "" - if alerts_data and 'features' in alerts_data and alerts_data['features']: - alerts_info += "[HUOM! HUOMIOI NÄMÄ! TAKE THESE INTO ACCOUNT!!! MENTION THESE TO THE USER IF THERE ARE WEATHER ALERTS -- INCLUDE ALL THE DETAILS. WHAT, WHEN, WHERE, WHAT SEVERITY, ETC.]\n🚨 ONGOING ALERTS FROM THE U.S. NWS (weather.gov):\n" - for idx, alert in enumerate(alerts_data['features'], start=1): - properties = alert.get('properties', {}) - - event = properties.get('event', 'EVENT').upper() - headline = properties.get('headline', 'HEADLINE') - description = properties.get('description', 'No further details available') # Fetching the detailed description - instruction = properties.get('instruction', 'INSTRUCTION') - severity = properties.get('severity', 'Unknown').capitalize() - certainty = properties.get('certainty', 'Unknown').capitalize() - urgency = properties.get('urgency', 'Unknown').capitalize() - area_desc = properties.get('areaDesc', 'N/A') - effective = properties.get('effective', 'N/A') - expires = properties.get('expires', 'N/A') - - alerts_info += ( - f"{idx}. ⚠️ {event}\n" - f"Vaara: {headline}\n" - f"Kuvaus: {description}\n" # Adding the detailed description - f"Ohjeet: {instruction}\n" - f"Alue: {area_desc}\n" - f"Vakavuus: {severity}\n" - f"Varmuus: {certainty}\n" - f"Kiireellisyys: {urgency}\n" - f"Voimassa alkaen: {effective}\n" - f"Päättyy: {expires}\n\n" - ) - else: - alerts_info += "\n🚨 Ei aktiivisia varoituksia U.S. NWS:n (weather.gov) mukaan.\n" - - # if alerts_data and 'features' in alerts_data and alerts_data['features']: - # alerts_info += "\n🚨 NWS ALERTS:\n" - # for alert in alerts_data['features']: - # event = alert.get('properties', {}).get('event', 'EVENT').upper() - # severity = alert.get('properties', {}).get('severity', 'SEVERITY').upper() - # headline = alert.get('properties', {}).get('headline', 'HEADLINE') - # instruction = alert.get('properties', {}).get('instruction', 'INSTRUCTION') - - # # Highlight severe alerts - # if 'HURRICANE' in event or severity in ['WATCH', 'WARNING', 'EMERGENCY']: - # alerts_info += f"🔥 {event}\nVaara: {headline}\nOhjeet: {instruction}\n\n" - # else: - # # Include less severe alerts if needed - # alerts_info += f"{event}\n{headline}\n{instruction}\n\n" - # else: - # alerts_info += "\n🚨 NWS ALERTS: Ei aktiivisia varoituksia.\n" - - combined_info += alerts_info + # Append NWS data (Forecasts) only for eligible countries + if not NWS_ONLY_ELIGIBLE_COUNTRIES or resolved_country.upper() in NWS_ELIGIBLE_COUNTRIES: + + # Append NWS Forecasts + if nws_forecast: + nws_forecast_info = "" + nws_periods = nws_forecast.get('properties', {}).get('periods', []) + if nws_periods: + nws_forecast_info += "🌦️ NWS Forecast (weather.gov):\n" + for period in nws_periods[:3]: # Limit to next 3 periods + name = period.get('name', 'N/A') + temperature = period.get('temperature', 'N/A') + temperature_unit = period.get('temperatureUnit', 'N/A') + wind_speed = period.get('windSpeed', 'N/A') + wind_direction = period.get('windDirection', 'N/A') + short_forecast = period.get('shortForecast', 'N/A') + nws_forecast_info += f"{name}: {short_forecast}, {temperature}°{temperature_unit}, Wind: {wind_speed} {wind_direction}\n" + else: + nws_forecast_info += "🌦️ NWS Forecast (weather.gov): Ei saatavilla.\n" + + if nws_forecast_hourly: + nws_hourly_forecast_info = "" + nws_hourly_periods = nws_forecast_hourly.get('properties', {}).get('periods', []) + if nws_hourly_periods: + nws_hourly_forecast_info += "⏰ NWS Hourly Forecast:\n" + for period in nws_hourly_periods[:3]: # Limit to next 3 hourly forecasts + start_time = period.get('startTime', 'N/A') + temperature = period.get('temperature', 'N/A') + temperature_unit = period.get('temperatureUnit', 'N/A') + wind_speed = period.get('windSpeed', 'N/A') + wind_direction = period.get('windDirection', 'N/A') + short_forecast = period.get('shortForecast', 'N/A') + nws_hourly_forecast_info += f"{start_time}: {short_forecast}, {temperature}°{temperature_unit}, Wind: {wind_speed} {wind_direction}\n" + else: + nws_hourly_forecast_info += "⏰ NWS Hourly Forecast: Ei saatavilla.\n" + else: + nws_hourly_forecast_info = "⏰ NWS Hourly Forecast: Ei saatavilla.\n" + + combined_info += f"\n{nws_forecast_info}\n{nws_hourly_forecast_info}" + + # Fetch and append NWS Alerts + try: + # Round coordinates to 4 decimal places to comply with NWS API + lat_rounded = round(lat, 4) + lon_rounded = round(lon, 4) + alerts_url = f"https://api.weather.gov/alerts/active?point={lat_rounded},{lon_rounded}" + async with httpx.AsyncClient(follow_redirects=True) as client: + alerts_response = await client.get(alerts_url, headers={'User-Agent': NWS_USER_AGENT}) + alerts_response.raise_for_status() + alerts_data = alerts_response.json() + except httpx.HTTPStatusError as e: + logging.error(f"NWS Alerts HTTP error: {e.response.status_code} - {e.response.text}") + alerts_data = None + except Exception as e: + logging.error(f"Error fetching NWS alerts: {e}") + alerts_data = None + + alerts_info = "" + if alerts_data and 'features' in alerts_data and alerts_data['features']: + alerts_info += "[HUOM! HUOMIOI NÄMÄ! TAKE THESE INTO ACCOUNT!!! MENTION THESE TO THE USER IF THERE ARE WEATHER ALERTS -- INCLUDE ALL THE DETAILS. WHAT, WHEN, WHERE, WHAT SEVERITY, ETC.]\n🚨 ONGOING ALERTS FROM THE U.S. NWS (weather.gov):\n" + for idx, alert in enumerate(alerts_data['features'], start=1): + properties = alert.get('properties', {}) + + event = properties.get('event', 'EVENT').upper() + headline = properties.get('headline', 'HEADLINE') + description = properties.get('description', 'No further details available') # Fetching the detailed description + instruction = properties.get('instruction', 'INSTRUCTION') + severity = properties.get('severity', 'Unknown').capitalize() + certainty = properties.get('certainty', 'Unknown').capitalize() + urgency = properties.get('urgency', 'Unknown').capitalize() + area_desc = properties.get('areaDesc', 'N/A') + effective = properties.get('effective', 'N/A') + expires = properties.get('expires', 'N/A') + + alerts_info += ( + f"{idx}. ⚠️ {event}\n" + f"Vaara: {headline}\n" + f"Kuvaus: {description}\n" # Adding the detailed description + f"Ohjeet: {instruction}\n" + f"Alue: {area_desc}\n" + f"Vakavuus: {severity}\n" + f"Varmuus: {certainty}\n" + f"Kiireellisyys: {urgency}\n" + f"Voimassa alkaen: {effective}\n" + f"Päättyy: {expires}\n\n" + ) + else: + alerts_info += "\n🚨 Ei aktiivisia varoituksia U.S. NWS:n (weather.gov) mukaan.\n" + + combined_info += alerts_info # Combine all information combined_info += f"\n{detailed_weather_info}\n\n{final_forecast}" diff --git a/src/config_paths.py b/src/config_paths.py index fc555b5..e164d8b 100644 --- a/src/config_paths.py +++ b/src/config_paths.py @@ -84,6 +84,8 @@ NWS_RETRY_DELAY = config['NWS'].getint('NWSRetryDelay', fallback=2) FETCH_NWS_FORECAST = config['NWS'].getboolean('FetchNWSForecast', fallback=True) FETCH_NWS_ALERTS = config['NWS'].getboolean('FetchNWSAlerts', fallback=True) + NWS_ONLY_ELIGIBLE_COUNTRIES = config['NWS'].getboolean('NwsOnlyEligibleCountries', fallback=True) + NWS_ELIGIBLE_COUNTRIES = config['NWS'].get('NwsEligibleCountries', fallback='US, PR, GU, AS, VI, MP').split(', ') logger.info(f"NWS Config: User-Agent={NWS_USER_AGENT}, Retries={NWS_RETRIES}, Retry Delay={NWS_RETRY_DELAY}, Fetch Forecast={FETCH_NWS_FORECAST}, Fetch Alerts={FETCH_NWS_ALERTS}") else: logger.warning("NWS section not found in config.ini. Using default NWS settings.") diff --git a/src/main.py b/src/main.py index c799e1a..5f1643f 100755 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,7 @@ # https://github.com/FlyingFathead/TelegramBot-OpenAI-API # # version of this program -version_number = "0.75052" +version_number = "0.75053" # Add the project root directory to Python's path import sys