Spaces:
Paused
Paused
import dash | |
from dash import dcc, html, Input, Output, State, clientside_callback | |
import dash_bootstrap_components as dbc | |
import plotly.graph_objs as go | |
import requests | |
from datetime import datetime | |
import os | |
from dotenv import load_dotenv | |
import threading | |
import uuid | |
import flask | |
import logging | |
load_dotenv() | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger("weather_dash") | |
session_locks = {} | |
session_data_store = {} | |
SESSION_COOKIE = "weather_dash_session_id" | |
def get_session_id(): | |
ctx = flask.request.cookies.get(SESSION_COOKIE) | |
if not ctx: | |
ctx = str(uuid.uuid4()) | |
return ctx | |
def get_session_lock(session_id): | |
if session_id not in session_locks: | |
session_locks[session_id] = threading.Lock() | |
return session_locks[session_id] | |
def get_session_data(session_id): | |
if session_id not in session_data_store: | |
session_data_store[session_id] = {} | |
return session_data_store[session_id] | |
def save_session_data(session_id, key, value): | |
session_data = get_session_data(session_id) | |
session_data[key] = value | |
session_data_store[session_id] = session_data | |
def get_data_from_session(session_id, key): | |
session_data = get_session_data(session_id) | |
return session_data.get(key, None) | |
API_KEY = os.getenv('ACCUWEATHER_API_KEY') | |
BASE_URL = "http://dataservice.accuweather.com" | |
INDEX_IDS = { | |
"Health": 10, | |
"Environmental": 5, | |
"Pollen": 30 | |
} | |
def get_location_key(lat, lon): | |
url = f"{BASE_URL}/locations/v1/cities/geoposition/search" | |
params = { | |
"apikey": API_KEY, | |
"q": f"{lat},{lon}", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
data = response.json() | |
if "Key" not in data: | |
raise ValueError("Location key not found in API response") | |
return data["Key"] | |
except requests.RequestException as e: | |
logger.error(f"Error in get_location_key: {e}") | |
return None | |
def get_current_conditions(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/currentconditions/v1/{location_key}" | |
params = { | |
"apikey": API_KEY, | |
"details": "true", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
data = response.json() | |
if not data: | |
raise ValueError("No current conditions data in API response") | |
return data[0] | |
except requests.RequestException as e: | |
logger.error(f"Error in get_current_conditions: {e}") | |
return None | |
def get_forecast_5day(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/forecasts/v1/daily/5day/{location_key}" | |
params = { | |
"apikey": API_KEY, | |
"metric": "false", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_forecast_5day: {e}") | |
return None | |
def get_forecast_1day(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/forecasts/v1/daily/1day/{location_key}" | |
params = { | |
"apikey": API_KEY, | |
"metric": "false", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_forecast_1day: {e}") | |
return None | |
def get_hourly_forecast_1hour(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/forecasts/v1/hourly/1hour/{location_key}" | |
params = { | |
"apikey": API_KEY, | |
"metric": "false", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_hourly_forecast_1hour: {e}") | |
return None | |
def get_hourly_forecast_12hour(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/forecasts/v1/hourly/12hour/{location_key}" | |
params = { | |
"apikey": API_KEY, | |
"metric": "false", | |
} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_hourly_forecast_12hour: {e}") | |
return None | |
def get_indices_1day(location_key, index_id): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/indices/v1/daily/1day/{location_key}/{index_id}" | |
params = {"apikey": API_KEY} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_indices_1day {index_id}: {e}") | |
return None | |
def get_weather_alarms_1day(location_key): | |
if location_key is None: | |
return None | |
url = f"{BASE_URL}/alarms/v1/1day/{location_key}" | |
params = {"apikey": API_KEY} | |
try: | |
response = requests.get(url, params=params) | |
response.raise_for_status() | |
return response.json() | |
except requests.RequestException as e: | |
logger.error(f"Error in get_weather_alarms_1day: {e}") | |
return None | |
def create_weather_alerts_card(alarms_data): | |
if not alarms_data or not isinstance(alarms_data, list) or len(alarms_data) == 0: | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Weather Alerts", className="card-title"), | |
html.P("No weather alerts or alarms for today.") | |
]) | |
], className="mb-4") | |
items = [] | |
for entry in alarms_data: | |
date = entry.get("Date", "N/A") | |
try: | |
date_str = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z").strftime("%A, %B %d %Y") | |
except Exception: | |
date_str = date | |
alarms = entry.get("Alarms", []) | |
if not alarms or not isinstance(alarms, list): | |
continue | |
for alarm in alarms: | |
alarm_type = alarm.get("AlarmType", "N/A") | |
value = alarm.get("Value", {}) | |
metric = value.get("Metric", {}) | |
imperial = value.get("Imperial", {}) | |
metric_val = metric.get("Value") | |
metric_unit = metric.get("Unit", "") | |
imperial_val = imperial.get("Value") | |
imperial_unit = imperial.get("Unit", "") | |
day = alarm.get("Day", {}) | |
night = alarm.get("Night", {}) | |
day_val = day.get("Imperial", {}).get("Value") | |
day_unit = day.get("Imperial", {}).get("Unit", "") | |
night_val = night.get("Imperial", {}).get("Value") | |
night_unit = night.get("Imperial", {}).get("Unit", "") | |
items.append(html.Div([ | |
html.H6(f"{alarm_type} Alert", style={"fontWeight": "bold"}), | |
html.P(f"Date: {date_str}"), | |
html.P(f"Total Day: {day_val} {day_unit}" if day_val is not None else "Day: N/A"), | |
html.P(f"Total Night: {night_val} {night_unit}" if night_val is not None else "Night: N/A"), | |
html.P(f"Total Value: {imperial_val} {imperial_unit}" if imperial_val is not None else "Value: N/A"), | |
html.Hr(style={"marginTop": "0.75rem", "marginBottom": "0.75rem"}) | |
])) | |
if not items: | |
items = [html.P("No weather alerts or alarms for today.")] | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Weather Alerts", className="card-title"), | |
*items | |
]) | |
], className="mb-4") | |
def create_current_weather_card(current): | |
if not current: | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Current Weather", className="card-title"), | |
html.P("No weather data available.") | |
]) | |
], className="mb-4") | |
uv_index = current.get("UVIndex", "N/A") | |
uv_index_text = current.get("UVIndexText", "N/A") | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Current Weather", className="card-title"), | |
html.P(f"Temperature: {current['Temperature']['Imperial']['Value']}°F"), | |
html.P(f"Condition: {current['WeatherText']}"), | |
html.P(f"Feels Like: {current['RealFeelTemperature']['Imperial']['Value']}°F"), | |
html.P(f"Wind: {current['Wind']['Speed']['Imperial']['Value']} mph"), | |
html.P(f"Humidity: {current['RelativeHumidity']}%"), | |
html.P(f"UV Index: {uv_index}"), | |
html.P(f"UV Index: {uv_index_text}") | |
]) | |
], className="mb-4") | |
def create_hourly_1hour_card(hourly): | |
if not hourly or not isinstance(hourly, list): | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Next Hour Forecast", className="card-title"), | |
html.P("No 1-hour forecast available") | |
]) | |
], className="mb-4") | |
hr = hourly[0] | |
dt = datetime.strptime(hr['DateTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%I:%M %p") | |
temp = hr['Temperature']['Value'] | |
phrase = hr['IconPhrase'] | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Next Hour Forecast", className="card-title"), | |
html.P(f"Time: {dt}"), | |
html.P(f"Temperature: {temp}°F"), | |
html.P(f"Condition: {phrase}") | |
]) | |
], className="mb-4") | |
def create_forecast_1day_card(forecast): | |
if not forecast or 'DailyForecasts' not in forecast or not forecast['DailyForecasts']: | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("1-Day Forecast", className="card-title"), | |
html.P("No 1-day forecast available.") | |
]) | |
], className="mb-4") | |
day = forecast['DailyForecasts'][0] | |
date = datetime.strptime(day['Date'], "%Y-%m-%dT%H:%M:%S%z").strftime("%A, %m-%d") | |
max_temp = day['Temperature']['Maximum']['Value'] | |
min_temp = day['Temperature']['Minimum']['Value'] | |
day_phrase = day['Day']['IconPhrase'] | |
night_phrase = day['Night']['IconPhrase'] | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("1-Day Forecast", className="card-title"), | |
html.P(f"Date: {date}"), | |
html.P(f"High: {max_temp}°F"), | |
html.P(f"Low: {min_temp}°F"), | |
html.P(f"Day: {day_phrase}"), | |
html.P(f"Night: {night_phrase}") | |
]) | |
], className="mb-4") | |
def create_forecast_5day_card(forecast): | |
if not forecast or 'DailyForecasts' not in forecast or not forecast['DailyForecasts']: | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("5-Day Forecast", className="card-title"), | |
html.P("No 5-day forecast available.") | |
]) | |
], className="mb-4") | |
daily_forecasts = forecast['DailyForecasts'] | |
dates = [datetime.strptime(day['Date'], "%Y-%m-%dT%H:%M:%S%z").strftime("%m-%d") for day in daily_forecasts] | |
max_temps = [day['Temperature']['Maximum']['Value'] for day in daily_forecasts] | |
min_temps = [day['Temperature']['Minimum']['Value'] for day in daily_forecasts] | |
fig = go.Figure() | |
fig.add_trace(go.Scatter(x=dates, y=max_temps, name="Max Temp", line=dict(color="red"))) | |
fig.add_trace(go.Scatter(x=dates, y=min_temps, name="Min Temp", line=dict(color="blue"))) | |
fig.update_layout( | |
title="5-Day Temperature Forecast", | |
xaxis_title="Date", | |
yaxis_title="Temperature (°F)", | |
legend_title="Temperature", | |
height=400 | |
) | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("5-Day Forecast", className="card-title"), | |
dcc.Graph(figure=fig) | |
]) | |
], className="mb-4") | |
def create_hourly_12hour_card(hourly_data): | |
if not hourly_data or not isinstance(hourly_data, list) or len(hourly_data) == 0: | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("12-Hour Hourly Forecast", className="card-title"), | |
html.P("No 12-hour forecast available.") | |
]) | |
], className="mb-4") | |
times = [datetime.strptime(hr['DateTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%I%p") for hr in hourly_data] | |
temps = [hr['Temperature']['Value'] for hr in hourly_data] | |
phrases = [hr['IconPhrase'] for hr in hourly_data] | |
fig = go.Figure() | |
fig.add_trace(go.Scatter(x=times, y=temps, mode="lines+markers", name="Temperature", line=dict(color="orange"))) | |
fig.update_layout( | |
title="12-Hour Temperature Forecast", | |
xaxis_title="Time", | |
yaxis_title="Temperature (°F)", | |
legend_title="Temperature", | |
height=400 | |
) | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("12-Hour Hourly Forecast", className="card-title"), | |
dcc.Graph(figure=fig) | |
]) | |
], className="mb-4") | |
def create_environmental_indices_card(indices_dict): | |
items = [] | |
index_display_names = { | |
"Health": "Health", | |
"Environmental": "Environment", | |
"Pollen": "Pollen" | |
} | |
for key in ["Health", "Environmental", "Pollen"]: | |
data = indices_dict.get(key) | |
display_name = index_display_names[key] | |
if data and isinstance(data, list) and len(data) > 0: | |
d = data[0] | |
entry = [ | |
html.H6(display_name, style={"marginBottom": "0.25rem", "fontWeight": "bold"}), | |
html.P(f"Category: {d.get('Category', 'N/A')}", style={"marginBottom": "0.25rem"}), | |
html.P(f"Value: {d.get('Value', 'N/A')}", style={"marginBottom": "0.25rem"}) | |
] | |
t = d.get("Text", None) | |
if t and t != "N/A": | |
entry.append(html.P(f"Text: {t}", style={"marginBottom": "0.75rem"})) | |
items.append(html.Div(entry)) | |
else: | |
items.append( | |
html.Div([ | |
html.H6(display_name, style={"marginBottom": "0.25rem", "fontWeight": "bold"}), | |
html.P("No data available.", style={"marginBottom": "0.75rem"}) | |
]) | |
) | |
return dbc.Card([ | |
dbc.CardBody([ | |
html.H4("Environmental Indices", className="card-title"), | |
*items | |
]) | |
], className="mb-4") | |
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) | |
server = app.server | |
app.layout = dbc.Container([ | |
dcc.Store(id="location-store", storage_type="session"), | |
dcc.Store(id="session-store", storage_type="session"), | |
dcc.Interval(id="location-interval", interval=1000, max_intervals=1), | |
dbc.Row([ | |
dbc.Col([ | |
dcc.Loading( | |
id="loading", | |
type="default", | |
children=[ | |
html.Div(id="weather-alerts-output"), | |
html.Div(id="current-weather-output"), | |
html.Div(id="environmental-indices-output") | |
], | |
style={"width": "100%"} | |
) | |
], width=4, style={"minWidth": "260px"}), | |
dbc.Col([ | |
dcc.Loading( | |
id="loading-forecast", | |
type="default", | |
children=[ | |
html.Div(id="forecast-output") | |
], | |
style={"width": "100%"} | |
) | |
], width=8) | |
], style={"marginTop": "16px"}) | |
], fluid=True, className="mt-2") | |
clientside_callback( | |
""" | |
function(n_intervals) { | |
return new Promise((resolve, reject) => { | |
if (navigator.geolocation) { | |
navigator.geolocation.getCurrentPosition( | |
(position) => { | |
resolve({ | |
latitude: position.coords.latitude, | |
longitude: position.coords.longitude | |
}); | |
}, | |
(error) => { | |
resolve({error: error.message}); | |
} | |
); | |
} else { | |
resolve({error: "Geolocation is not supported by this browser."}); | |
} | |
}); | |
} | |
""", | |
Output("location-store", "data"), | |
Input("location-interval", "n_intervals"), | |
) | |
def set_session_cookie(response): | |
session_id = flask.request.cookies.get(SESSION_COOKIE) | |
if not session_id: | |
session_id = str(uuid.uuid4()) | |
response.set_cookie(SESSION_COOKIE, session_id, max_age=60*60*24*7, path="/") | |
return response | |
def update_weather(location, session_data): | |
session_id = get_session_id() | |
lock = get_session_lock(session_id) | |
logger.info(f"Session {session_id} update_weather called.") | |
if not location or 'error' in location: | |
error_message = location.get('error', 'Waiting for location data...') if location else 'Waiting for location data...' | |
logger.warning(f"Session {session_id} waiting for location: {error_message}") | |
return [dbc.Spinner(color="primary"), "", "", ""] | |
lat, lon = location["latitude"], location["longitude"] | |
results = {"weather_alerts": "", "current": "", "indices": "", "hourly12": "", "forecast": ""} | |
def fetch_weather_data(): | |
try: | |
location_key = get_data_from_session(session_id, "location_key") | |
if not location_key: | |
location_key = get_location_key(lat, lon) | |
save_session_data(session_id, "location_key", location_key) | |
if not location_key: | |
raise ValueError("Failed to get location key") | |
current = get_current_conditions(location_key) | |
forecast_5day = get_forecast_5day(location_key) | |
hourly_12 = get_hourly_forecast_12hour(location_key) | |
alarms_1day = get_weather_alarms_1day(location_key) | |
indices_dict = {} | |
for name, idx in INDEX_IDS.items(): | |
indices_dict[name] = get_indices_1day(location_key, idx) | |
if current is None or forecast_5day is None: | |
raise ValueError("Failed to fetch weather data") | |
results["weather_alerts"] = create_weather_alerts_card(alarms_1day) | |
results["current"] = create_current_weather_card(current) | |
results["indices"] = create_environmental_indices_card(indices_dict) | |
results["hourly12"] = create_hourly_12hour_card(hourly_12) | |
results["forecast"] = create_forecast_5day_card(forecast_5day) | |
save_session_data(session_id, "weather_results", results) | |
except Exception as e: | |
logger.error(f"Session {session_id} error: {str(e)}") | |
results["weather_alerts"] = "" | |
results["current"] = "" | |
results["indices"] = "" | |
results["hourly12"] = "" | |
results["forecast"] = dbc.Card([ | |
dbc.CardBody([ | |
html.P(f"Error fetching weather data: {str(e)}", className="text-danger") | |
]) | |
], className="mb-4") | |
with lock: | |
fetch_weather_data() | |
weather_results = get_data_from_session(session_id, "weather_results") | |
if weather_results: | |
return [ | |
weather_results.get("weather_alerts", ""), | |
weather_results.get("current", ""), | |
weather_results.get("indices", ""), | |
html.Div([ | |
weather_results.get("hourly12", ""), | |
weather_results.get("forecast", "") | |
]) | |
] | |
else: | |
return [dbc.Spinner(color="primary"), "", "", ""] | |
if __name__ == '__main__': | |
print("Starting the Dash application...") | |
app.run(debug=True, host='0.0.0.0', port=7860, threaded=True) | |
print("Dash application has finished running.") |