weather / app.py
bluenevus's picture
Update app.py via AI Editor
103239b
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"),
)
@app.server.after_request
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
@app.callback(
[
Output("weather-alerts-output", "children"),
Output("current-weather-output", "children"),
Output("environmental-indices-output", "children"),
Output("forecast-output", "children"),
],
[Input("location-store", "data")],
[State("session-store", "data")]
)
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.")