PicoWeatherDisplay/weather_display.py
Alexander Berry-Roe 92defbe958 Add initial weather display application with API integration
Includes weather data retrieval from Open-Meteo API and basic WiFi setup for connectivity. IntelliJ project configuration files and a `.gitignore` for IDE-specific files are also added.
2025-05-15 00:50:06 +01:00

262 lines
10 KiB
Python

import time
import uasyncio as asyncio
class WeatherDisplay:
def __init__(self, display):
self.display = display
self.text_padding = 4
self.last_displayed_data = None
self.time_update_running = False
self.time_thread = None
def weathercode_to_text(self, weathercode):
# More detailed weather code interpretations
if weathercode == 0:
return 'Clear Sky'
elif weathercode == 1:
return 'Mainly Clear'
elif weathercode == 2:
return 'Partly Cloudy'
elif weathercode == 3:
return 'Overcast'
elif weathercode in [45, 48]:
return 'Foggy'
elif weathercode in [51, 53, 55]:
return 'Light Rain'
elif weathercode in [56, 57]:
return 'Freezing Rain'
elif weathercode in [61, 63, 65]:
return 'Moderate Rain'
elif weathercode in [66, 67]:
return 'Heavy Rain'
elif weathercode in [71, 73, 75]:
return 'Snow Fall'
elif weathercode == 77:
return 'Snow Grains'
elif weathercode in [80, 81, 82]:
return 'Rain Showers'
elif weathercode in [85, 86]:
return 'Snow Showers'
elif weathercode == 95:
return 'Thunderstorm'
elif weathercode in [96, 99]:
return 'Thunderstorm with Hail'
else:
return 'Unknown Weather'
if force_full_update:
self.display.clear()
else:
# Create a clean buffer but don't send to display yet
self.display.fill(self.display.black)
# Text starts at left padding
text_x = self.text_padding
text_y = 8
# Track regions that need updating
update_regions = []
# Display temperature
temp_text = f"Temp: {weather_data.get('max_temp', 0):.0f}/{weather_data.get('min_temp', 0):.0f}C"
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('max_temp') != weather_data.get('max_temp') or \
self.last_displayed_data.get('miget_wen_temp') != weather_data.get('min_temp'):
self.display.text(temp_text, text_x, text_y)
update_regions.append((0, text_y, 16, text_y + 8))
# Display weather description
weathercode = weather_data.get('weathercode', 0)
weather_text = self.weathercode_to_text(weathercode)
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('weathercode') != weathercode:
self.display.text(f"{weather_text}", text_x, text_y + 12)
update_regions.append((0, text_y + 12, 16, text_y + 20))
# Display precipitation
precip_text = f"Precip: {weather_data.get('precip_mm', 0):.1f}mm"
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('precip_mm') != weather_data.get('precip_mm'):
self.display.text(precip_text, text_x, text_y + 24)
update_regions.append((0, text_y + 24, 16, text_y + 32))
# Display date
date_text = weather_data.get('date', '')
if date_text and len(date_text) >= 10:
year = date_text[0:4]
month = date_text[5:7]
day = date_text[8:10]
date_text = f"{day}-{month}-{year}"
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('date') != weather_data.get('date'):
self.display.text(f"Date: {date_text}", text_x, text_y + 36)
update_regions.append((0, text_y + 36, 16, text_y + 44))
# Store current data for future comparison
self.last_displayed_data = dict(weather_data)
self.last_displayed_data['view_type'] = 'detailed'
# Update display - either full or partial updates
if force_full_update:
self.display.show()
else:
# Merge overlapping regions for more efficient updates
if update_regions:
# Simple approach: just update the full range covered by all regions
min_y = min(region[1] for region in update_regions)
max_y = max(region[3] for region in update_regions)
self.display.show(0, min_y, 16, max_y)
else:
# No changes detected, no need to update
pass
def display_weather(self, weather_data):
"""Detailed view with max/min temps, description, precip, date."""
# If this is the full API response, extract today's values:
if 'daily' in weather_data:
d = weather_data['daily']
weather_data = {
'max_temp': d['temperature_2m_max'][0],
'min_temp': d['temperature_2m_min'][0],
'weathercode':d['weathercode'][0],
'precip_mm': d['precipitation_sum'][0],
'date': d['time'][0],
}
force_full_update = (
self.last_displayed_data is None or
self.last_displayed_data.get('view_type') != 'detailed'
)
if force_full_update:
self.display.clear()
else:
self.display.fill(self.display.black)
text_x = self.text_padding
text_y = 8
update_regions = []
# Temperature
temp_text = f"Temp: {weather_data.get('max_temp', 0):.0f}/" \
f"{weather_data.get('min_temp', 0):.0f}C"
if (force_full_update
or self.last_displayed_data.get('max_temp') != weather_data.get('max_temp')
or self.last_displayed_data.get('min_temp') != weather_data.get('min_temp')
):
self.display.text(temp_text, text_x, text_y)
update_regions.append((text_y, text_y + 8))
# Description
code = weather_data.get('weathercode', 0)
desc = self.weathercode_to_text(code)
if (force_full_update
or self.last_displayed_data.get('weathercode') != code
):
self.display.text(desc, text_x, text_y + 12)
update_regions.append((text_y + 12, text_y + 20))
# Precip
precip = f"Precip: {weather_data.get('precip_mm', 0):.1f}mm"
if (force_full_update
or self.last_displayed_data.get('precip_mm') != weather_data.get('precip_mm')
):
self.display.text(precip, text_x, text_y + 24)
update_regions.append((text_y + 24, text_y + 32))
# Date (YYYY-MM-DD → DD-MM-YYYY)
raw_date = weather_data.get('date', '')
if raw_date and len(raw_date) >= 10:
dd = raw_date[8:10]; mm = raw_date[5:7]; yyyy = raw_date[0:4]
formatted = f"Date: {dd}-{mm}-{yyyy}"
if (force_full_update
or self.last_displayed_data.get('date') != raw_date
):
self.display.text(formatted, text_x, text_y + 36)
update_regions.append((text_y + 36, text_y + 44))
# Save for next time
self.last_displayed_data = dict(weather_data)
self.last_displayed_data['view_type'] = 'detailed'
# If full, just show everything
if force_full_update:
self.display.show()
return
# Otherwise do a partial update over all pages
if update_regions:
min_y = min(r[0] for r in update_regions)
max_y = max(r[1] for r in update_regions)
page_count = self.display.height // 8
self.display.show(1, min_y, page_count, max_y)
# else: nothing changed
def display_simple_weather(self, weather_data):
force_full_update = self.last_displayed_data is None or self.last_displayed_data.get('view_type') != 'simple'
if force_full_update:
self.display.clear()
else:
# Create a clean buffer but don't send to display yet
self.display.fill(self.display.black)
# Text starts at left padding
text_x = self.text_padding
center_y = self.display.height // 2
# Track regions that need updating
update_regions = []
# Display temperature large
temp_text = f"{weather_data.get('current_temp', 0):.0f}C"
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('current_temp') != weather_data.get('current_temp'):
self.display.text(temp_text, text_x, center_y - 8, scale=2)
update_regions.append((0, center_y - 8, 16, center_y + 8)) # Scaled text is 16px high
# Display description
weather_desc = self.weathercode_to_text(weather_data.get('weathercode', 0))
if force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('weathercode') != weather_data.get('weathercode'):
self.display.text(weather_desc, text_x, center_y + 12)
update_regions.append((0, center_y + 12, 16, center_y + 20))
# Display current time
current_time_tuple = time.localtime(time.time())
current_time = f"{current_time_tuple[3]:02d}:{current_time_tuple[4]:02d}:{current_time_tuple[5]:02d}"
if current_time and (force_full_update or not self.last_displayed_data or \
self.last_displayed_data.get('current_time') != current_time):
self.display.text(current_time, text_x, center_y + 24)
update_regions.append((0, center_y + 24, 16, center_y + 32))
# Store current data for future comparison
self.last_displayed_data = dict(weather_data)
self.last_displayed_data['view_type'] = 'simple'
# Update display - either full or partial updates
if force_full_update:
self.display.show()
else:
# Merge overlapping regions for more efficient updates
if update_regions:
# Simple approach: just update the full range covered by all regions
min_y = min(region[1] for region in update_regions)
max_y = max(region[3] for region in update_regions)
self.display.show(0, min_y, 16, max_y)
else:
# No changes detected, no need to update
pass
def reset_display(self):
"""Clear display and reset last displayed data."""
self.display.clear()
self.display.show()
self.last_displayed_data = None