simple_weather_app
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| simple_weather_app [2026/09/10 14:36] – [A Simple Test] walkeradmin | simple_weather_app [2026/09/10 15:03] (current) – walkeradmin | ||
|---|---|---|---|
| Line 8: | Line 8: | ||
| ===== Introduction ===== | ===== Introduction ===== | ||
| \\ | \\ | ||
| - | <code | download> | + | <color # |
| + | \\ | ||
| + | \\ | ||
| {{ : | {{ : | ||
| \\ | \\ | ||
| Line 53: | Line 55: | ||
| \\ | \\ | ||
| \\ | \\ | ||
| + | In the code, don't forget to change the following: | ||
| + | \\ | ||
| + | \\ | ||
| + | Set your Wifi Details. | ||
| + | |||
| + | < | ||
| + | # --- CONFIGURATION --- | ||
| + | WIFI_SSID = " | ||
| + | WIFI_PASS = " | ||
| + | </ | ||
| + | |||
| + | Set your Latitude and Longitude (you can leave it at London for a simple test if you want). | ||
| + | |||
| + | < | ||
| + | # Coordinates (e.g., London: lat=51.5074, | ||
| + | LATITUDE = " | ||
| + | LONGITUDE = " | ||
| + | </ | ||
| + | |||
| + | Save this code as ' | ||
| + | |||
| < | < | ||
| import network | import network | ||
| Line 118: | Line 141: | ||
| fetch_weather() | fetch_weather() | ||
| </ | </ | ||
| + | |||
| + | At the bottom of your Thonny, you should see something like the following: | ||
| + | |||
| + | <code | download> | ||
| + | Connected successfully! | ||
| + | Network status/IP: (' | ||
| + | Fetching weather data... | ||
| + | |||
| + | --- WEATHER DATA RECEIVED --- | ||
| + | |||
| + | Temperature: | ||
| + | Wind Speed: | ||
| + | Weather Code: 2 | ||
| + | ------------------------------ | ||
| + | </ | ||
| + | |||
| + | If this has worked, then fantastic, you are ready to use the full weather app code. Some information. | ||
| + | |||
| + | * Network Status is your Internal IP Range | ||
| + | * Temp and Speed is that in London (as denoted by the co-ordinates | ||
| + | * Weather Code 2 is a value for your weather, see below | ||
| + | |||
| + | So the weather code is Meteos ' | ||
| + | \\ | ||
| + | \\ | ||
| + | {{ : | ||
| + | \\ | ||
| + | Now we can move on to the full code. | ||
| + | \\ | ||
| + | \\ | ||
| + | ---- | ||
| + | ==== The Full Weather App Code ==== | ||
| + | \\ | ||
| + | Don't forget in this new code to do the following: | ||
| + | |||
| + | * Ensure | ||
| + | * Ensure that your Latitude and Longitude details are correct. | ||
| + | |||
| + | Download the following code and save it as ' | ||
| + | \\ | ||
| + | \\ | ||
| + | ---- | ||
| + | < | ||
| + | import network | ||
| + | import time | ||
| + | import urequests | ||
| + | import ujson | ||
| + | import framebuf | ||
| + | from machine import RTC, SPI, Pin | ||
| + | from st7789nf import ( | ||
| + | ST7789, COLOR_BLACK, | ||
| + | COLOR_YELLOW, | ||
| + | ) | ||
| + | |||
| + | # ========================================== | ||
| + | # CONFIGURATION | ||
| + | # ========================================== | ||
| + | WIFI_SSID = " | ||
| + | WIFI_PASS = " | ||
| + | |||
| + | # London Coordinates | ||
| + | LATITUDE = " | ||
| + | LONGITUDE = " | ||
| + | |||
| + | # BST / UK Daylight Saving Time offset (1 for BST, 0 for GMT) | ||
| + | BST_OFFSET = 1 | ||
| + | |||
| + | # Open-Meteo API Endpoint (configured for mph) | ||
| + | WEATHER_URL = ( | ||
| + | " | ||
| + | f" | ||
| + | "& | ||
| + | "& | ||
| + | "& | ||
| + | "& | ||
| + | ) | ||
| + | |||
| + | # ========================================== | ||
| + | # HARDWARE PINS (SPI1) | ||
| + | # ========================================== | ||
| + | LCD_WIDTH | ||
| + | LCD_HEIGHT = 240 | ||
| + | |||
| + | PIN_SCK | ||
| + | PIN_MOSI = 11 # GP11 | ||
| + | PIN_RST | ||
| + | PIN_DC | ||
| + | PIN_CS | ||
| + | |||
| + | # ========================================== | ||
| + | # BITMAP ICONS (16x16) | ||
| + | # ========================================== | ||
| + | ICON_SUN_16 = bytes([ | ||
| + | 0x01, 0x80, 0x41, 0x82, 0x20, 0x04, 0x10, 0x08, | ||
| + | 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0F, 0xF0, | ||
| + | 0x0F, 0xF0, 0x0F, 0xF0, 0x07, 0xE0, 0x03, 0xC0, | ||
| + | 0x10, 0x08, 0x20, 0x04, 0x41, 0x82, 0x01, 0x80 | ||
| + | ]) | ||
| + | |||
| + | ICON_CLOUD_16 = bytes([ | ||
| + | 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x03, 0xE0, | ||
| + | 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, | ||
| + | 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||
| + | 0x7F, 0xFE, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00 | ||
| + | ]) | ||
| + | |||
| + | ICON_RAIN_16 = bytes([ | ||
| + | 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, | ||
| + | 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, | ||
| + | 0x7F, 0xFE, 0x00, 0x00, 0x24, 0x48, 0x48, 0x90, | ||
| + | 0x12, 0x24, 0x24, 0x48, 0x48, 0x90, 0x00, 0x00 | ||
| + | ]) | ||
| + | |||
| + | ICON_THUNDER_16 = bytes([ | ||
| + | 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, | ||
| + | 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, | ||
| + | 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1F, 0xC0, | ||
| + | 0x03, 0x80, 0x07, 0x00, 0x06, 0x00, 0x04, 0x00 | ||
| + | ]) | ||
| + | |||
| + | # ========================================== | ||
| + | # NETWORK & API FUNCTIONS | ||
| + | # ========================================== | ||
| + | def connect_wifi(ssid, | ||
| + | wlan = network.WLAN(network.STA_IF) | ||
| + | wlan.active(True) | ||
| + | if not wlan.isconnected(): | ||
| + | print(f" | ||
| + | wlan.connect(ssid, | ||
| + | start_time = time.time() | ||
| + | while not wlan.isconnected(): | ||
| + | if time.time() - start_time > timeout: | ||
| + | print(" | ||
| + | return False | ||
| + | time.sleep(0.5) | ||
| + | print(f" | ||
| + | return True | ||
| + | |||
| + | def sync_ntp(): | ||
| + | import ntptime | ||
| + | try: | ||
| + | ntptime.settime() | ||
| + | print(" | ||
| + | return True | ||
| + | except Exception as e: | ||
| + | print(f" | ||
| + | return False | ||
| + | |||
| + | def fetch_weather(): | ||
| + | print(" | ||
| + | try: | ||
| + | response = urequests.get(WEATHER_URL) | ||
| + | if response.status_code == 200: | ||
| + | data = ujson.loads(response.text) | ||
| + | response.close() | ||
| + | | ||
| + | current = data.get(" | ||
| + | current_weather = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | | ||
| + | hourly = data.get(" | ||
| + | hourly_times = hourly.get(" | ||
| + | hourly_temps = hourly.get(" | ||
| + | hourly_codes = hourly.get(" | ||
| + | | ||
| + | rtc = RTC() | ||
| + | now_hour = rtc.datetime()[4] | ||
| + | | ||
| + | forecast = [] | ||
| + | for i in range(len(hourly_times)): | ||
| + | slot_hour = int(hourly_times[i].split(" | ||
| + | if slot_hour >= now_hour and len(forecast) < 5: | ||
| + | forecast.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | return {" | ||
| + | else: | ||
| + | response.close() | ||
| + | return None | ||
| + | except Exception as e: | ||
| + | print(f" | ||
| + | return None | ||
| + | |||
| + | # ========================================== | ||
| + | # GRAPHICS HELPER FUNCTIONS | ||
| + | # ========================================== | ||
| + | def draw_text_scaled(display, | ||
| + | """ | ||
| + | char_buf = bytearray(8 * 8 * 2) | ||
| + | char_fb = framebuf.FrameBuffer(char_buf, | ||
| + | | ||
| + | for idx, char in enumerate(text): | ||
| + | char_fb.fill(COLOR_BLACK) | ||
| + | char_fb.text(char, | ||
| + | | ||
| + | char_x = x + int(idx * 8 * scale) | ||
| + | for row in range(8): | ||
| + | for col in range(8): | ||
| + | pixel = char_fb.pixel(col, | ||
| + | if pixel != COLOR_BLACK: | ||
| + | display.fill_rect(char_x + int(col * scale), y + int(row * scale), int(scale), int(scale), color) | ||
| + | |||
| + | def draw_bitmap_scaled(display, | ||
| + | """ | ||
| + | for row in range(height): | ||
| + | for col in range(width): | ||
| + | byte_idx = (row * width + col) // 8 | ||
| + | bit_idx = 7 - (col % 8) | ||
| + | pixel = (bitmap_bytes[byte_idx] >> bit_idx) & 1 | ||
| + | if pixel: | ||
| + | if scale == 1: | ||
| + | display.pixel(x + col, y + row, fg_color) | ||
| + | else: | ||
| + | display.fill_rect(x + col * scale, y + row * scale, scale, scale, fg_color) | ||
| + | |||
| + | def get_wmo_icon_and_color(wmo_code): | ||
| + | """ | ||
| + | if wmo_code in (0, 1): | ||
| + | return ICON_SUN_16, | ||
| + | elif wmo_code in (2, 3, 45, 48): | ||
| + | return ICON_CLOUD_16, | ||
| + | elif 51 <= wmo_code <= 67 or 80 <= wmo_code <= 82: | ||
| + | return ICON_RAIN_16, | ||
| + | elif wmo_code >= 95: | ||
| + | return ICON_THUNDER_16, | ||
| + | else: | ||
| + | return ICON_CLOUD_16, | ||
| + | |||
| + | def render_ui(display, | ||
| + | """ | ||
| + | display.fill(COLOR_BLACK) | ||
| + | | ||
| + | # --- TOP BAR --- | ||
| + | display.fill_rect(0, | ||
| + | display.hline(0, | ||
| + | | ||
| + | rtc = RTC() | ||
| + | dt = rtc.datetime() | ||
| + | hour = (dt[4] + BST_OFFSET) % 24 | ||
| + | time_str = f" | ||
| + | | ||
| + | # Header Title (Larger Purbrook text) & Live Clock | ||
| + | draw_text_scaled(display, | ||
| + | draw_text_scaled(display, | ||
| + | | ||
| + | # --- CENTER PANEL --- | ||
| + | if weather_data and " | ||
| + | curr = weather_data[" | ||
| + | | ||
| + | # Main Weather Icon (Scale=3 -> 48x48) | ||
| + | icon_data, icon_color = get_wmo_icon_and_color(curr[" | ||
| + | draw_bitmap_scaled(display, | ||
| + | | ||
| + | # Temperature & Feels Like | ||
| + | draw_text_scaled(display, | ||
| + | draw_text_scaled(display, | ||
| + | | ||
| + | # Environmental Details (In mph) | ||
| + | display.hline(15, | ||
| + | draw_text_scaled(display, | ||
| + | draw_text_scaled(display, | ||
| + | |||
| + | # --- BOTTOM STRIP (5-Hour Forecast) --- | ||
| + | display.hline(0, | ||
| + | | ||
| + | if weather_data and " | ||
| + | slot_width = 320 // 5 | ||
| + | for idx, slot in enumerate(weather_data[" | ||
| + | x = idx * slot_width | ||
| + | | ||
| + | if idx > 0: | ||
| + | display.vline(x, | ||
| + | | ||
| + | # Forecast Hour Header | ||
| + | draw_text_scaled(display, | ||
| + | | ||
| + | # Forecast Icon | ||
| + | f_icon, f_color = get_wmo_icon_and_color(slot[" | ||
| + | draw_bitmap_scaled(display, | ||
| + | | ||
| + | # Forecast Temperature | ||
| + | draw_text_scaled(display, | ||
| + | |||
| + | # Push to display | ||
| + | display.show() | ||
| + | |||
| + | # ========================================== | ||
| + | # MAIN ROUTINE | ||
| + | # ========================================== | ||
| + | spi1 = SPI(1, baudrate=20_000_000, | ||
| + | |||
| + | tft = ST7789( | ||
| + | spi=spi1, | ||
| + | width=LCD_WIDTH, | ||
| + | height=LCD_HEIGHT, | ||
| + | dc=Pin(PIN_DC), | ||
| + | reset=Pin(PIN_RST), | ||
| + | cs=Pin(PIN_CS) | ||
| + | ) | ||
| + | |||
| + | if connect_wifi(WIFI_SSID, | ||
| + | sync_ntp() | ||
| + | |||
| + | current_weather_data = fetch_weather() | ||
| + | |||
| + | last_weather_fetch = time.time() | ||
| + | WEATHER_REFRESH_INTERVAL = 900 # 15 Minutes | ||
| + | |||
| + | print(" | ||
| + | |||
| + | while True: | ||
| + | now = time.time() | ||
| + | | ||
| + | if now - last_weather_fetch >= WEATHER_REFRESH_INTERVAL: | ||
| + | updated_data = fetch_weather() | ||
| + | if updated_data: | ||
| + | current_weather_data = updated_data | ||
| + | last_weather_fetch = now | ||
| + | | ||
| + | render_ui(tft, | ||
| + | time.sleep(1) | ||
| + | </ | ||
| + | \\ | ||
| + | ---- | ||
| + | |||
simple_weather_app.1789050987.txt.gz · Last modified: by walkeradmin
