from flask import Flask, render_template, jsonify import requests import os import random from datetime import datetime, timedelta, date from icalendar import Calendar from config import Config import pytz import recurring_ical_events app = Flask(__name__) app.config.from_object(Config) # Cache for API responses to avoid rate limiting weather_cache = {'data': None, 'timestamp': None} calendar_cache = {'data': None, 'timestamp': None} joke_cache = {'data': None, 'timestamp': None} @app.route('/') def index(): """Render the main display page.""" return render_template('index.html') @app.route('/api/weather') def get_weather(): """Fetch weather data from OpenWeatherMap API.""" global weather_cache # Check cache now = datetime.now() if (weather_cache['data'] and weather_cache['timestamp'] and (now - weather_cache['timestamp']).total_seconds() < app.config['WEATHER_UPDATE_INTERVAL']): return jsonify(weather_cache['data']) try: # Fetch current weather current_url = f"https://api.openweathermap.org/data/2.5/weather" params = { 'lat': app.config['WEATHER_LAT'], 'lon': app.config['WEATHER_LON'], 'appid': app.config['OPENWEATHER_API_KEY'], 'units': app.config['WEATHER_UNITS'] } current_response = requests.get(current_url, params=params, timeout=10) current_response.raise_for_status() current_data = current_response.json() # Fetch 3-day forecast forecast_url = f"https://api.openweathermap.org/data/2.5/forecast" forecast_response = requests.get(forecast_url, params=params, timeout=10) forecast_response.raise_for_status() forecast_data = forecast_response.json() # Process forecast to get daily summaries daily_forecast = [] seen_dates = set() for item in forecast_data['list'][:40]: # Next 3 days (8 forecasts per day) date = datetime.fromtimestamp(item['dt']).date() if date not in seen_dates and len(daily_forecast) < 3: seen_dates.add(date) daily_forecast.append({ 'date': date.strftime('%a'), 'temp_max': round(item['main']['temp_max']), 'temp_min': round(item['main']['temp_min']), 'description': item['weather'][0]['description'], 'icon': item['weather'][0]['icon'] }) weather_data = { 'current': { 'temp': round(current_data['main']['temp']), 'feels_like': round(current_data['main']['feels_like']), 'description': current_data['weather'][0]['description'], 'icon': current_data['weather'][0]['icon'], 'humidity': current_data['main']['humidity'], 'wind_speed': round(current_data['wind']['speed'] * 3.6, 1) # Convert m/s to km/h }, 'forecast': daily_forecast } # Update cache weather_cache = {'data': weather_data, 'timestamp': now} return jsonify(weather_data) except Exception as e: app.logger.error(f"Error fetching weather: {str(e)}") # Return cached data if available, otherwise return error if weather_cache['data']: return jsonify(weather_cache['data']) return jsonify({'error': 'Unable to fetch weather data'}), 500 @app.route('/api/calendar') def get_calendar(): """Fetch Google Calendar events from iCal feed.""" global calendar_cache # Check cache - use timezone-aware datetime nz_tz = pytz.timezone('Pacific/Auckland') now = datetime.now(nz_tz) if (calendar_cache['data'] and calendar_cache['timestamp'] and (now - calendar_cache['timestamp']).total_seconds() < app.config['CALENDAR_UPDATE_INTERVAL']): return jsonify(calendar_cache['data']) try: # Fetch iCal feed ical_url = app.config.get('GOOGLE_CALENDAR_ICAL_URL') if not ical_url: return jsonify([]) response = requests.get(ical_url, timeout=10) response.raise_for_status() # Parse iCal data cal = Calendar.from_ical(response.content) # Use recurring_ical_events to get all events in the date range (including recurring ones) cutoff_date = now + timedelta(days=app.config['CALENDAR_DAYS_AHEAD']) # Get all events between now and cutoff_date recurring_events = recurring_ical_events.of(cal).between(now, cutoff_date) events = [] for component in recurring_events: dtstart = component.get('dtstart') dtend = component.get('dtend') summary = str(component.get('summary', 'No Title')) if dtstart and dtstart.dt: # Handle both datetime and date objects if isinstance(dtstart.dt, datetime): event_start = dtstart.dt # Make sure event_start is timezone-aware if event_start.tzinfo is None: event_start = nz_tz.localize(event_start) elif isinstance(dtstart.dt, date): # For date-only events, create a timezone-aware datetime event_start = nz_tz.localize(datetime.combine(dtstart.dt, datetime.min.time())) else: continue # Handle end time if dtend and dtend.dt: if isinstance(dtend.dt, datetime): event_end = dtend.dt # Make sure event_end is timezone-aware if event_end.tzinfo is None: event_end = nz_tz.localize(event_end) elif isinstance(dtend.dt, date): event_end = nz_tz.localize(datetime.combine(dtend.dt, datetime.min.time())) else: event_end = event_start + timedelta(hours=1) else: event_end = event_start + timedelta(hours=1) events.append({ 'title': summary, 'start': event_start.isoformat(), 'end': event_end.isoformat(), 'location': str(component.get('location', '')) }) # Sort events by start time events.sort(key=lambda x: x['start']) calendar_cache = {'data': events, 'timestamp': now} return jsonify(events) except Exception as e: app.logger.error(f"Error fetching calendar: {str(e)}") if calendar_cache['data']: return jsonify(calendar_cache['data']) return jsonify([]) @app.route('/api/background') def get_background(): """Get a random background image.""" try: backgrounds_dir = app.config['BACKGROUNDS_DIR'] # Get list of image files if os.path.exists(backgrounds_dir): image_files = [f for f in os.listdir(backgrounds_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp'))] if image_files: random_image = random.choice(image_files) return jsonify({'image': f'/static/backgrounds/{random_image}'}) # Return a default color if no images return jsonify({'image': None, 'color': '#1a1a2e'}) except Exception as e: app.logger.error(f"Error getting background: {str(e)}") return jsonify({'image': None, 'color': '#1a1a2e'}) @app.route('/api/joke') def get_joke(): """Fetch a dad joke.""" global joke_cache # Check cache now = datetime.now() if (joke_cache['data'] and joke_cache['timestamp'] and (now - joke_cache['timestamp']).total_seconds() < app.config['JOKE_UPDATE_INTERVAL']): return jsonify(joke_cache['data']) try: response = requests.get( 'https://icanhazdadjoke.com/', headers={'Accept': 'application/json'}, timeout=10 ) response.raise_for_status() joke_data = response.json() joke_cache = {'data': {'joke': joke_data['joke']}, 'timestamp': now} return jsonify(joke_cache['data']) except Exception as e: app.logger.error(f"Error fetching joke: {str(e)}") if joke_cache['data']: return jsonify(joke_cache['data']) return jsonify({'joke': 'Why did the developer go broke? Because he used up all his cache!'}) if __name__ == '__main__': # Create backgrounds directory if it doesn't exist os.makedirs(app.config['BACKGROUNDS_DIR'], exist_ok=True) os.makedirs(app.config['CREDENTIALS_DIR'], exist_ok=True) # Run the app app.run(host='0.0.0.0', port=5002, debug=False, use_reloader=False)