Refine calendar display layout and features
Major improvements to the calendar display application: - Redesigned weather display: moved forecast to header alongside time - Added 3-day forecast with icons and high/low temps in header - Simplified main layout: removed separate weather section, calendar now full-width - Made layout more compact to fit on one screen without scrolling - Added 2 new background images for rotation - Updated image rotation interval to 1 minute - Improved responsive design and spacing The display now shows: - Header: Time/date on left, current weather + 3-day forecast on right - Main: Full-width calendar events section - Footer: Dad joke All elements now fit on a single screen with a clean, readable layout. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ A smart display application for Raspberry Pi that shows time, weather, calendar
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🕐 **Real-time Clock** - Current time and date display
|
- 🕐 **Real-time Clock** - Current time and date display
|
||||||
- 🌤️ **Weather** - Current weather and 5-day forecast for Hamilton, New Zealand (OpenWeatherMap)
|
- 🌤️ **Weather** - Current weather and 3-day forecast for Hamilton, New Zealand (OpenWeatherMap)
|
||||||
- 📅 **Google Calendar** - Family calendar events display
|
- 📅 **Google Calendar** - Family calendar events display
|
||||||
- 🖼️ **Rotating Backgrounds** - Beautiful images from local directory
|
- 🖼️ **Rotating Backgrounds** - Beautiful images from local directory
|
||||||
- 😄 **Dad Jokes** - Random jokes to brighten your day (optional)
|
- 😄 **Dad Jokes** - Random jokes to brighten your day (optional)
|
||||||
|
|||||||
6
app.py
6
app.py
@@ -44,7 +44,7 @@ def get_weather():
|
|||||||
current_response.raise_for_status()
|
current_response.raise_for_status()
|
||||||
current_data = current_response.json()
|
current_data = current_response.json()
|
||||||
|
|
||||||
# Fetch 5-day forecast
|
# Fetch 3-day forecast
|
||||||
forecast_url = f"https://api.openweathermap.org/data/2.5/forecast"
|
forecast_url = f"https://api.openweathermap.org/data/2.5/forecast"
|
||||||
forecast_response = requests.get(forecast_url, params=params, timeout=10)
|
forecast_response = requests.get(forecast_url, params=params, timeout=10)
|
||||||
forecast_response.raise_for_status()
|
forecast_response.raise_for_status()
|
||||||
@@ -54,9 +54,9 @@ def get_weather():
|
|||||||
daily_forecast = []
|
daily_forecast = []
|
||||||
seen_dates = set()
|
seen_dates = set()
|
||||||
|
|
||||||
for item in forecast_data['list'][:40]: # Next 5 days (8 forecasts per day)
|
for item in forecast_data['list'][:40]: # Next 3 days (8 forecasts per day)
|
||||||
date = datetime.fromtimestamp(item['dt']).date()
|
date = datetime.fromtimestamp(item['dt']).date()
|
||||||
if date not in seen_dates and len(daily_forecast) < 5:
|
if date not in seen_dates and len(daily_forecast) < 3:
|
||||||
seen_dates.add(date)
|
seen_dates.add(date)
|
||||||
daily_forecast.append({
|
daily_forecast.append({
|
||||||
'date': date.strftime('%a'),
|
'date': date.strftime('%a'),
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
static/backgrounds/IMG_9554.jpeg
Normal file
BIN
static/backgrounds/IMG_9554.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
@@ -46,7 +46,7 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
@@ -54,11 +54,11 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
padding: 1.5rem 2rem;
|
padding: 1rem 1.5rem;
|
||||||
border-radius: 15px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-display {
|
.time-display {
|
||||||
@@ -67,16 +67,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 5rem;
|
font-size: 3rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 1.8rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.3rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
@@ -84,25 +84,67 @@ body {
|
|||||||
.weather-summary {
|
.weather-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-weather-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-temp {
|
.current-temp {
|
||||||
font-size: 4rem;
|
font-size: 2rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-icon {
|
.weather-icon {
|
||||||
font-size: 4rem;
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-forecast {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-temps {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-temps .high {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-temps .low {
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main content */
|
/* Main content */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,44 +152,45 @@ body {
|
|||||||
section {
|
section {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
padding: 2rem;
|
padding: 1rem 1.5rem;
|
||||||
border-radius: 15px;
|
border-radius: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
section h2 {
|
section h2 {
|
||||||
font-size: 2rem;
|
font-size: 1.3rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 0.8rem;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
section h3 {
|
section h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin: 1.5rem 0 1rem;
|
margin: 1rem 0 0.5rem;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Weather Section */
|
/* Weather Section */
|
||||||
.weather-description {
|
.weather-description {
|
||||||
font-size: 1.8rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.6rem;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-details {
|
.weather-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.8rem;
|
gap: 0.4rem;
|
||||||
margin: 1.5rem 0;
|
margin: 0.8rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-detail {
|
.weather-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 1.3rem;
|
font-size: 0.95rem;
|
||||||
padding: 0.5rem 0;
|
padding: 0.3rem 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,32 +202,32 @@ section h3 {
|
|||||||
.forecast-days {
|
.forecast-days {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-day {
|
.forecast-day {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px 1fr 120px;
|
grid-template-columns: 60px 1fr 100px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 0.6rem;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
gap: 1rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-day-name {
|
.forecast-day-name {
|
||||||
font-size: 1.3rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-description {
|
.forecast-description {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-temp {
|
.forecast-temp {
|
||||||
font-size: 1.3rem;
|
font-size: 1rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,36 +244,36 @@ section h3 {
|
|||||||
.events-list {
|
.events-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
padding: 1.2rem;
|
padding: 0.8rem;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #4a9eff;
|
border-left: 3px solid #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-time {
|
.event-time {
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.3rem;
|
||||||
color: #4a9eff;
|
color: #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-title {
|
.event-title {
|
||||||
font-size: 1.4rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-location {
|
.event-location {
|
||||||
font-size: 1rem;
|
font-size: 0.85rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-placeholder {
|
.event-placeholder {
|
||||||
font-size: 1.2rem;
|
font-size: 1rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -238,15 +281,15 @@ section h3 {
|
|||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
padding: 1.5rem 2rem;
|
padding: 0.8rem 1.5rem;
|
||||||
border-radius: 15px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.joke {
|
.joke {
|
||||||
font-size: 1.2rem;
|
font-size: 0.9rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const INTERVALS = {
|
|||||||
TIME: 1000, // 1 second
|
TIME: 1000, // 1 second
|
||||||
WEATHER: 900000, // 15 minutes
|
WEATHER: 900000, // 15 minutes
|
||||||
CALENDAR: 300000, // 5 minutes
|
CALENDAR: 300000, // 5 minutes
|
||||||
BACKGROUND: 300000, // 5 minutes
|
BACKGROUND: 60000, // 1 minute
|
||||||
JOKE: 3600000 // 1 hour
|
JOKE: 3600000 // 1 hour
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,32 +65,28 @@ async function updateWeather() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current weather
|
// Update current weather in header
|
||||||
const { current, forecast } = data;
|
const { current, forecast } = data;
|
||||||
|
|
||||||
document.getElementById('current-temp').textContent = `${current.temp}°`;
|
document.getElementById('current-temp').textContent = `${current.temp}°`;
|
||||||
document.getElementById('weather-icon').textContent = WEATHER_ICONS[current.icon] || '🌤️';
|
document.getElementById('weather-icon').textContent = WEATHER_ICONS[current.icon] || '🌤️';
|
||||||
document.getElementById('weather-description').textContent = current.description;
|
|
||||||
document.getElementById('feels-like').textContent = `${current.feels_like}°`;
|
|
||||||
document.getElementById('humidity').textContent = `${current.humidity}%`;
|
|
||||||
document.getElementById('wind-speed').textContent = `${current.wind_speed} km/h`;
|
|
||||||
|
|
||||||
// Update forecast
|
// Update 3-day forecast in header
|
||||||
const forecastContainer = document.getElementById('forecast-container');
|
const headerForecast = document.getElementById('header-forecast');
|
||||||
forecastContainer.innerHTML = '';
|
headerForecast.innerHTML = '';
|
||||||
|
|
||||||
forecast.forEach(day => {
|
forecast.forEach(day => {
|
||||||
const dayElement = document.createElement('div');
|
const dayElement = document.createElement('div');
|
||||||
dayElement.className = 'forecast-day';
|
dayElement.className = 'forecast-item';
|
||||||
dayElement.innerHTML = `
|
dayElement.innerHTML = `
|
||||||
<div class="forecast-day-name">${day.date}</div>
|
<div class="weather-label">${day.date}</div>
|
||||||
<div class="forecast-description">${day.description}</div>
|
<div class="forecast-icon">${WEATHER_ICONS[day.icon] || '🌤️'}</div>
|
||||||
<div class="forecast-temp">
|
<div class="forecast-temps">
|
||||||
<span class="high">${day.temp_max}°</span>
|
<span class="high">${day.temp_max}°</span>
|
||||||
<span class="low">${day.temp_min}°</span>
|
<span class="low">${day.temp_min}°</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
forecastContainer.appendChild(dayElement);
|
headerForecast.appendChild(dayElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -19,43 +19,19 @@
|
|||||||
<div id="date" class="date">Loading...</div>
|
<div id="date" class="date">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="weather-summary">
|
<div class="weather-summary">
|
||||||
<div id="current-temp" class="current-temp">--°</div>
|
<div class="current-weather-item">
|
||||||
<div id="weather-icon" class="weather-icon"></div>
|
<div class="weather-label">Today</div>
|
||||||
|
<div id="current-temp" class="current-temp">--°</div>
|
||||||
|
<div id="weather-icon" class="weather-icon"></div>
|
||||||
|
</div>
|
||||||
|
<div id="header-forecast" class="header-forecast">
|
||||||
|
<!-- 3-day forecast will be inserted here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main content area -->
|
<!-- Main content area -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- Weather Section -->
|
|
||||||
<section class="weather-section">
|
|
||||||
<h2>Weather</h2>
|
|
||||||
<div class="current-weather">
|
|
||||||
<div id="weather-description" class="weather-description">Loading...</div>
|
|
||||||
<div class="weather-details">
|
|
||||||
<div class="weather-detail">
|
|
||||||
<span class="label">Feels like:</span>
|
|
||||||
<span id="feels-like">--°</span>
|
|
||||||
</div>
|
|
||||||
<div class="weather-detail">
|
|
||||||
<span class="label">Humidity:</span>
|
|
||||||
<span id="humidity">--%</span>
|
|
||||||
</div>
|
|
||||||
<div class="weather-detail">
|
|
||||||
<span class="label">Wind:</span>
|
|
||||||
<span id="wind-speed">-- km/h</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 5-Day Forecast -->
|
|
||||||
<div class="forecast">
|
|
||||||
<h3>Forecast</h3>
|
|
||||||
<div id="forecast-container" class="forecast-days">
|
|
||||||
<!-- Forecast items will be inserted here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Calendar Section -->
|
<!-- Calendar Section -->
|
||||||
<section class="calendar-section">
|
<section class="calendar-section">
|
||||||
<h2>Upcoming Events</h2>
|
<h2>Upcoming Events</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user