Skip to content

Commit

Permalink
v0.75053 - only include eligible territories in U.S. NWS queries
Browse files Browse the repository at this point in the history
  • Loading branch information
FlyingFathead committed Oct 7, 2024
1 parent 8c9f07c commit 7411c43
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 92 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
261 changes: 171 additions & 90 deletions src/api_get_openweathermap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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🚨 <b>ONGOING ALERTS FROM THE U.S. NWS (weather.gov):</b>\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}. ⚠️ <b>{event}</b>\n"
f"<b>Vaara:</b> {headline}\n"
f"<b>Kuvaus:</b> {description}\n" # Adding the detailed description
f"<b>Ohjeet:</b> {instruction}\n"
f"<b>Alue:</b> {area_desc}\n"
f"<b>Vakavuus:</b> {severity}\n"
f"<b>Varmuus:</b> {certainty}\n"
f"<b>Kiireellisyys:</b> {urgency}\n"
f"<b>Voimassa alkaen:</b> {effective}\n"
f"<b>Päättyy:</b> {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🚨 <b>NWS ALERTS:</b>\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"🔥 <b>{event}</b>\n<b>Vaara:</b> {headline}\n<b>Ohjeet:</b> {instruction}\n\n"
# else:
# # Include less severe alerts if needed
# alerts_info += f"<b>{event}</b>\n{headline}\n{instruction}\n\n"
# else:
# alerts_info += "\n🚨 <b>NWS ALERTS:</b> 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 += "🌦️ <b>NWS Forecast (weather.gov):</b>\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 += "🌦️ <b>NWS Forecast (weather.gov):</b> 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 += "⏰ <b>NWS Hourly Forecast:</b>\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 += "⏰ <b>NWS Hourly Forecast:</b> Ei saatavilla.\n"
else:
nws_hourly_forecast_info = "⏰ <b>NWS Hourly Forecast:</b> 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🚨 <b>ONGOING ALERTS FROM THE U.S. NWS (weather.gov):</b>\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}. ⚠️ <b>{event}</b>\n"
f"<b>Vaara:</b> {headline}\n"
f"<b>Kuvaus:</b> {description}\n" # Adding the detailed description
f"<b>Ohjeet:</b> {instruction}\n"
f"<b>Alue:</b> {area_desc}\n"
f"<b>Vakavuus:</b> {severity}\n"
f"<b>Varmuus:</b> {certainty}\n"
f"<b>Kiireellisyys:</b> {urgency}\n"
f"<b>Voimassa alkaen:</b> {effective}\n"
f"<b>Päättyy:</b> {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}"
Expand Down
2 changes: 2 additions & 0 deletions src/config_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
Loading

0 comments on commit 7411c43

Please sign in to comment.