weather / app.py
bluenevus's picture
Update app.py via AI Editor
926cfd7
raw
history blame
16.4 kB
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 json
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,
"Mosquito": 59,
"Pest": 61
}
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_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 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")
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']}%"),
])
], 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_environmental_api_card(environmental_data):
if not environmental_data or not isinstance(environmental_data, list) or not environmental_data[0]:
return dbc.Card([
dbc.CardBody([
html.H4("Environmental API", className="card-title"),
html.P("No environmental data available.")
])
], className="mb-4")
info = environmental_data[0]
return dbc.Card([
dbc.CardBody([
html.H4("Environmental API", className="card-title"),
html.P(f"Name: {info.get('Name', 'N/A')}"),
html.P(f"Category: {info.get('Category', 'N/A')}"),
html.P(f"Value: {info.get('Value', 'N/A')}"),
html.P(f"Category Value: {info.get('CategoryValue', 'N/A')}"),
html.P(f"Text: {info.get('Text', 'N/A')}")
])
], className="mb-4")
def create_environmental_indices_card(indices_dict):
items = []
index_display_names = {
"Health": "Health API",
"Environmental": "Environmental API",
"Pollen": "Pollen",
"Pest": "Pest",
"Mosquito": "Mosquito"
}
for key in ["Health", "Environmental", "Pollen", "Pest", "Mosquito"]:
data = indices_dict.get(key)
display_name = index_display_names[key]
if data and isinstance(data, list) and len(data) > 0:
d = data[0]
items.append(
html.Div([
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"}),
html.P(f"Text: {d.get('Text', 'N/A')}", style={"marginBottom": "0.75rem"})
])
)
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([
html.H1("Weather Dashboard", className="text-center mb-4"),
], width=12)
]),
dbc.Row([
dbc.Col([
dcc.Loading(
id="loading",
type="default",
children=[
html.Div(id="current-weather-output"),
html.Div(id="environmental-indices-output"),
html.Div(id="environmental-api-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("current-weather-output", "children"),
Output("environmental-indices-output", "children"),
Output("environmental-api-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 = {"current": "", "indices": "", "environmental": "", "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)
environmental_data = get_indices_1day(location_key, INDEX_IDS["Environmental"])
# Environmental Indices
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["current"] = create_current_weather_card(current)
results["indices"] = create_environmental_indices_card(indices_dict)
results["environmental"] = create_environmental_api_card(environmental_data)
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["current"] = ""
results["indices"] = ""
results["environmental"] = ""
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("current", ""),
weather_results.get("indices", ""),
weather_results.get("environmental", ""),
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.")