Skip to content

Commit e186d84

Browse files
committed
v0.7505 - integrate NWS API and handle weather alerts
1 parent 7e3e2c0 commit e186d84

File tree

4 files changed

+263
-5
lines changed

4 files changed

+263
-5
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ If you run into any issues, consult the logs or reach out on the repository's [I
236236
---
237237

238238
# Changelog
239+
- v0.7505 - U.S. NWS (National Weather Service, [weather.gov](https://weather.gov)) added as a weather data source
240+
- for additional information; **especially weather alerts**
241+
- all data will be combined from OpenWeatherMap and U.S. NWS sources by default
239242
- v0.7504 - fixed usage logs and charts directory mapping
240243
- v0.7503 - improved message formatting & error catching
241244
- v0.7502 - added `docker_setup.sh` for easier Docker-based deployment

src/api_get_nws_weather.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# api_get_nws.py
2+
#
3+
# > get the weather using the NWS (National Weather Service, US) API
4+
#
5+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6+
# github.com/FlyingFathead/TelegramBot-OpenAI-API/
7+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8+
9+
import asyncio
10+
import httpx
11+
import logging
12+
13+
# Base URL for NWS API
14+
NWS_BASE_URL = 'https://api.weather.gov'
15+
16+
# Custom User-Agent as per NWS API requirements
17+
USER_AGENT = 'ChatKekeWeather/1.0 ([email protected])'
18+
19+
# Default number of retries if not specified
20+
RETRIES = 0
21+
22+
async def get_nws_forecast(lat, lon, retries=RETRIES, delay=2):
23+
"""
24+
Fetches the forecast from the NWS API for the given latitude and longitude.
25+
26+
Args:
27+
lat (float): Latitude in decimal degrees.
28+
lon (float): Longitude in decimal degrees.
29+
retries (int): Number of retries for transient errors. Defaults to RETRIES.
30+
delay (int): Delay between retries in seconds.
31+
32+
Returns:
33+
dict: Combined forecast data or None if fetching fails.
34+
"""
35+
# Round coordinates to 4 decimal places
36+
lat = round(lat, 4)
37+
lon = round(lon, 4)
38+
points_url = f"{NWS_BASE_URL}/points/{lat},{lon}"
39+
40+
async with httpx.AsyncClient(follow_redirects=True) as client:
41+
for attempt in range(retries + 1): # Ensure at least one attempt is made
42+
try:
43+
# Step 1: Retrieve metadata for the location
44+
response = await client.get(points_url, headers={'User-Agent': USER_AGENT})
45+
response.raise_for_status()
46+
points_data = response.json()
47+
48+
# Extract forecast URLs
49+
forecast_url = points_data['properties']['forecast']
50+
forecast_hourly_url = points_data['properties'].get('forecastHourly')
51+
52+
# Step 2: Retrieve forecast data
53+
forecast_response = await client.get(forecast_url, headers={'User-Agent': USER_AGENT})
54+
forecast_response.raise_for_status()
55+
forecast_data = forecast_response.json()
56+
57+
# Step 3: Retrieve hourly forecast data
58+
forecast_hourly_data = None
59+
if forecast_hourly_url:
60+
try:
61+
forecast_hourly_response = await client.get(forecast_hourly_url, headers={'User-Agent': USER_AGENT})
62+
forecast_hourly_response.raise_for_status()
63+
forecast_hourly_data = forecast_hourly_response.json()
64+
except httpx.HTTPStatusError as e:
65+
logging.error(f"NWS Hourly Forecast HTTP error: {e.response.status_code} - {e.response.text}")
66+
67+
return {
68+
'nws_forecast': forecast_data,
69+
'nws_forecast_hourly': forecast_hourly_data
70+
}
71+
72+
except httpx.HTTPStatusError as e:
73+
if e.response.status_code >= 500 and attempt < retries:
74+
logging.warning(f"NWS API HTTP error: {e.response.status_code} - {e.response.text}. Retrying in {delay} seconds...")
75+
await asyncio.sleep(delay)
76+
else:
77+
logging.error(f"NWS API HTTP error: {e.response.status_code} - {e.response.text}")
78+
break
79+
except Exception as e:
80+
logging.error(f"Error fetching NWS forecast: {e}")
81+
break
82+
83+
return None
84+
85+
async def get_nws_alerts(lat, lon):
86+
"""
87+
Fetches active alerts from the NWS API for the given latitude and longitude.
88+
89+
Args:
90+
lat (float): Latitude in decimal degrees.
91+
lon (float): Longitude in decimal degrees.
92+
93+
Returns:
94+
list: A list of active alerts or an empty list if none are found.
95+
"""
96+
alerts_url = f"{NWS_BASE_URL}/alerts/active?point={lat},{lon}"
97+
98+
async with httpx.AsyncClient() as client:
99+
try:
100+
response = await client.get(alerts_url, headers={'User-Agent': USER_AGENT})
101+
response.raise_for_status()
102+
alerts_data = response.json()
103+
104+
# Extract alerts from GeoJSON
105+
alerts = alerts_data.get('features', [])
106+
return alerts
107+
108+
except httpx.HTTPStatusError as e:
109+
logging.error(f"NWS Alerts API HTTP error: {e.response.status_code} - {e.response.text}")
110+
except Exception as e:
111+
logging.error(f"Error fetching NWS alerts: {e}")
112+
113+
return []

src/api_get_openweathermap.py

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# export OPENWEATHERMAP_API_KEY="<your API key>"
1313
# export MAPTILER_API_KEY="<your API key>"
1414

15+
# Import the NWS data fetching function
16+
from api_get_nws_weather import get_nws_forecast, get_nws_alerts
17+
1518
# date & time utils
1619
import datetime as dt
1720
from dateutil import parser
@@ -74,8 +77,38 @@ async def get_weather(city_name, country, exclude='', units='metric', lang='fi')
7477
additional_data = await get_additional_data_dump()
7578
logging.info(f"Additional data fetched: {additional_data}")
7679

77-
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)
78-
return combined_data
80+
# // (old method)
81+
# 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)
82+
# return combined_data
83+
84+
# Fetch NWS data
85+
logging.info("Fetching NWS data.")
86+
nws_data = await get_nws_forecast(lat, lon)
87+
if nws_data:
88+
logging.info("NWS data fetched successfully.")
89+
nws_forecast = nws_data.get('nws_forecast')
90+
nws_forecast_hourly = nws_data.get('nws_forecast_hourly')
91+
else:
92+
logging.warning("Failed to fetch NWS data.")
93+
nws_forecast = None
94+
nws_forecast_hourly = None
95+
96+
# Fetch NWS alerts data
97+
logging.info("Fetching NWS alerts data.")
98+
nws_alerts = await get_nws_alerts(lat, lon)
99+
if nws_alerts:
100+
logging.info(f"Fetched {len(nws_alerts)} active NWS alerts.")
101+
else:
102+
logging.info("No active NWS alerts found.")
103+
104+
combined_data = await combine_weather_data(
105+
city_name, resolved_country, lat, lon,
106+
current_weather_data, forecast_data, moon_phase_data,
107+
daily_forecast_data, current_weather_data_from_weatherapi,
108+
astronomy_data, additional_data, nws_forecast, nws_forecast_hourly
109+
)
110+
return combined_data
111+
79112
else:
80113
logging.error(f"Failed to fetch weather data: {current_weather_response.text} / {forecast_response.text}")
81114
return "[Inform the user that data fetching the weather data failed, current information could not be fetched. Reply in the user's language.]"
@@ -172,7 +205,9 @@ def convert_to_24_hour(time_str, timezone_str):
172205
return "Invalid time"
173206

174207
# combined weather data
175-
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):
208+
# 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):
209+
# Define the combine_weather_data function with NWS integration
210+
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):
176211
tf = TimezoneFinder()
177212
timezone_str = tf.timezone_at(lat=lat, lng=lon)
178213
local_timezone = pytz.timezone(timezone_str)
@@ -285,7 +320,7 @@ async def combine_weather_data(city_name, country, lat, lon, current_weather_dat
285320
"\n".join(
286321
[f"Alert: {alert['headline']}\nDescription: {alert['desc']}\nInstructions: {alert['instruction']}\n"
287322
for alert in alerts['alert']]
288-
) if 'alert' in alerts and alerts['alert'] else "No weather alerts."
323+
) if 'alert' in alerts and alerts['alert'] else "No weather alerts according to OpenWeatherMap. NOTE: Please see other sources (i.e. NWS) to be sure."
289324
)
290325

291326
detailed_weather_info += f"\n{air_quality_info}\n{alerts_info}"
@@ -324,6 +359,113 @@ async def combine_weather_data(city_name, country, lat, lon, current_weather_dat
324359

325360
combined_info = f"{detailed_weather_info}\n\n{final_forecast}"
326361

362+
363+
# Append NWS data (Forecasts)
364+
if nws_forecast:
365+
nws_forecast_info = ""
366+
nws_periods = nws_forecast.get('properties', {}).get('periods', [])
367+
if nws_periods:
368+
nws_forecast_info += "🌦️ <b>NWS Forecast (weather.gov):</b>\n"
369+
for period in nws_periods[:3]: # Limit to next 3 periods
370+
name = period.get('name', 'N/A')
371+
temperature = period.get('temperature', 'N/A')
372+
temperature_unit = period.get('temperatureUnit', 'N/A')
373+
wind_speed = period.get('windSpeed', 'N/A')
374+
wind_direction = period.get('windDirection', 'N/A')
375+
short_forecast = period.get('shortForecast', 'N/A')
376+
nws_forecast_info += f"{name}: {short_forecast}, {temperature}°{temperature_unit}, Wind: {wind_speed} {wind_direction}\n"
377+
else:
378+
nws_forecast_info += "🌦️ <b>NWS Forecast (weather.gov):</b> Ei saatavilla.\n"
379+
380+
if nws_forecast_hourly:
381+
nws_hourly_forecast_info = ""
382+
nws_hourly_periods = nws_forecast_hourly.get('properties', {}).get('periods', [])
383+
if nws_hourly_periods:
384+
nws_hourly_forecast_info += "⏰ <b>NWS Hourly Forecast:</b>\n"
385+
for period in nws_hourly_periods[:3]: # Limit to next 3 hourly forecasts
386+
start_time = period.get('startTime', 'N/A')
387+
temperature = period.get('temperature', 'N/A')
388+
temperature_unit = period.get('temperatureUnit', 'N/A')
389+
wind_speed = period.get('windSpeed', 'N/A')
390+
wind_direction = period.get('windDirection', 'N/A')
391+
short_forecast = period.get('shortForecast', 'N/A')
392+
nws_hourly_forecast_info += f"{start_time}: {short_forecast}, {temperature}°{temperature_unit}, Wind: {wind_speed} {wind_direction}\n"
393+
else:
394+
nws_hourly_forecast_info += "⏰ <b>NWS Hourly Forecast:</b> Ei saatavilla.\n"
395+
else:
396+
nws_hourly_forecast_info = "⏰ <b>NWS Hourly Forecast:</b> Ei saatavilla.\n"
397+
398+
combined_info += f"\n{nws_forecast_info}\n{nws_hourly_forecast_info}"
399+
400+
# Fetch and append NWS Alerts
401+
try:
402+
# Round coordinates to 4 decimal places to comply with NWS API
403+
lat_rounded = round(lat, 4)
404+
lon_rounded = round(lon, 4)
405+
alerts_url = f"https://api.weather.gov/alerts/active?point={lat_rounded},{lon_rounded}"
406+
async with httpx.AsyncClient(follow_redirects=True) as client:
407+
alerts_response = await client.get(alerts_url, headers={'User-Agent': 'YourAppName ([email protected])'})
408+
alerts_response.raise_for_status()
409+
alerts_data = alerts_response.json()
410+
except httpx.HTTPStatusError as e:
411+
logging.error(f"NWS Alerts HTTP error: {e.response.status_code} - {e.response.text}")
412+
alerts_data = None
413+
except Exception as e:
414+
logging.error(f"Error fetching NWS alerts: {e}")
415+
alerts_data = None
416+
417+
alerts_info = ""
418+
if alerts_data and 'features' in alerts_data and alerts_data['features']:
419+
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"
420+
for idx, alert in enumerate(alerts_data['features'], start=1):
421+
properties = alert.get('properties', {})
422+
423+
event = properties.get('event', 'EVENT').upper()
424+
headline = properties.get('headline', 'HEADLINE')
425+
instruction = properties.get('instruction', 'INSTRUCTION')
426+
severity = properties.get('severity', 'Unknown').capitalize()
427+
certainty = properties.get('certainty', 'Unknown').capitalize()
428+
urgency = properties.get('urgency', 'Unknown').capitalize()
429+
area_desc = properties.get('areaDesc', 'N/A')
430+
effective = properties.get('effective', 'N/A')
431+
expires = properties.get('expires', 'N/A')
432+
433+
alerts_info += (
434+
f"{idx}. ⚠️ <b>{event}</b>\n"
435+
f"<b>Vaara:</b> {headline}\n"
436+
f"<b>Ohjeet:</b> {instruction}\n"
437+
f"<b>Alue:</b> {area_desc}\n"
438+
f"<b>Vakavuus:</b> {severity}\n"
439+
f"<b>Varmuus:</b> {certainty}\n"
440+
f"<b>Kiireellisyys:</b> {urgency}\n"
441+
f"<b>Voimassa alkaen:</b> {effective}\n"
442+
f"<b>Päättyy:</b> {expires}\n\n"
443+
)
444+
else:
445+
alerts_info += "\n🚨 Ei aktiivisia varoituksia U.S. NWS:n (weather.gov) mukaan.\n"
446+
447+
# if alerts_data and 'features' in alerts_data and alerts_data['features']:
448+
# alerts_info += "\n🚨 <b>NWS ALERTS:</b>\n"
449+
# for alert in alerts_data['features']:
450+
# event = alert.get('properties', {}).get('event', 'EVENT').upper()
451+
# severity = alert.get('properties', {}).get('severity', 'SEVERITY').upper()
452+
# headline = alert.get('properties', {}).get('headline', 'HEADLINE')
453+
# instruction = alert.get('properties', {}).get('instruction', 'INSTRUCTION')
454+
455+
# # Highlight severe alerts
456+
# if 'HURRICANE' in event or severity in ['WATCH', 'WARNING', 'EMERGENCY']:
457+
# alerts_info += f"🔥 <b>{event}</b>\n<b>Vaara:</b> {headline}\n<b>Ohjeet:</b> {instruction}\n\n"
458+
# else:
459+
# # Include less severe alerts if needed
460+
# alerts_info += f"<b>{event}</b>\n{headline}\n{instruction}\n\n"
461+
# else:
462+
# alerts_info += "\n🚨 <b>NWS ALERTS:</b> Ei aktiivisia varoituksia.\n"
463+
464+
combined_info += alerts_info
465+
466+
# Combine all information
467+
combined_info += f"\n{detailed_weather_info}\n\n{final_forecast}"
468+
327469
# Append additional data for Finland if available
328470
if additional_data:
329471
combined_info += f"\n\n[ Lisätiedot Suomeen (lähde: foreca.fi -- MAINITSE LÄHDE) ]\n{additional_data}"

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# https://github.com/FlyingFathead/TelegramBot-OpenAI-API
66
#
77
# version of this program
8-
version_number = "0.7504"
8+
version_number = "0.7505"
99

1010
# Add the project root directory to Python's path
1111
import sys

0 commit comments

Comments
 (0)