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.") |