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