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:38] – [Test Open-Meteo] walkeradmin | simple_weather_app [2026/09/10 15:03] (current) – walkeradmin | ||
|---|---|---|---|
| Line 8: | Line 8: | ||
| ===== Introduction ===== | ===== Introduction ===== | ||
| \\ | \\ | ||
| + | <color # | ||
| + | \\ | ||
| + | \\ | ||
| + | {{ : | ||
| + | \\ | ||
| + | This project is a bit of an accident. We want to make a new clock, we made one around 10 years ago from a Raspberry Pi Zero and some 7 segment LEDs. That project can be found here - [[raspberry_pi_led_clock_and_ntp_server|Raspberry Pi LED Clock and NTP Server]]. | ||
| + | \\ | ||
| + | \\ | ||
| + | Now, it has worked flawlessly for 10 years. But it is still a small computer with an OS, and you have to do some maintenance on the OS (upgrades etc) periodically. So the thinking was, a new clock using a Raspberry Pi Pico would be cool, newer hardware, no OS worries, better display (LCD rather than 7 segment) and include other things than the time like the weather, or a news feed etc. | ||
| + | \\ | ||
| + | \\ | ||
| + | The first thing we explored was how to get the weather from the internet, turns out this is pretty simple. We used something called Open Meto, which is: | ||
| + | |||
| + | * Open-Meteo is a free, open-source weather API that delivers localized weather forecasts and historical climate data without requiring an API key. | ||
| + | |||
| + | So basically a free weather service to pull information from using an API. | ||
| + | \\ | ||
| + | \\ | ||
| + | If you wish to make one of these, then great. Please check out the Project called ' | ||
| + | \\ | ||
| + | \\ | ||
| + | You will need: | ||
| + | |||
| + | * The same LCD. | ||
| + | * The Same Driver. | ||
| + | * The Test Code for the Driver | ||
| + | |||
| + | Check out this link: [[pico_with_2.4_tenstar_spi_lcd|Pico with 2.4" Tenstar SPI LCD]]. This will ensure your hardware is good, before you start to worry about the software. | ||
| + | \\ | ||
| + | \\ | ||
| + | ---- | ||
| + | ==== A Simple Test ==== | ||
| + | \\ | ||
| + | If you have completed the hardware setup, and see some test text on your LCD, then we are good to start. The first test is to get some weather data from Open-Meteo. Now, this means connecting to the internet from your Pico, so please ensure you have done the following: | ||
| + | |||
| + | * [[connect_pico_to_wifi|Connect Pico to Wifi]] | ||
| + | * [[find_pico_mac_address|Find Pico MAC Address]] | ||
| + | |||
| + | This will ensure you have tested your Pico Wifi and you have your MAC address in case your router requires this. | ||
| + | \\ | ||
| + | \\ | ||
| + | ---- | ||
| + | ==== Test Open-Meteo ==== | ||
| + | \\ | ||
| + | From Thonny, save this code to your Pico as Meteo-test and run it. | ||
| + | \\ | ||
| + | \\ | ||
| + | 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 urequests | ||
| + | import time | ||
| + | |||
| + | # --- CONFIGURATION --- | ||
| + | WIFI_SSID = " | ||
| + | WIFI_PASS = " | ||
| + | |||
| + | # Coordinates (e.g., London: lat=51.5074, | ||
| + | LATITUDE = " | ||
| + | LONGITUDE = " | ||
| + | |||
| + | # Open-Meteo API URL for temperature, | ||
| + | URL = f" | ||
| + | |||
| + | def connect_wifi(): | ||
| + | wlan = network.WLAN(network.STA_IF) | ||
| + | wlan.active(True) | ||
| + | if not wlan.isconnected(): | ||
| + | print(f" | ||
| + | wlan.connect(WIFI_SSID, | ||
| + | | ||
| + | timeout = 10 | ||
| + | while not wlan.isconnected() and timeout > 0: | ||
| + | print(" | ||
| + | time.sleep(1) | ||
| + | timeout -= 1 | ||
| + | | ||
| + | if wlan.isconnected(): | ||
| + | print(" | ||
| + | print(" | ||
| + | return True | ||
| + | else: | ||
| + | print(" | ||
| + | return False | ||
| + | |||
| + | def fetch_weather(): | ||
| + | print(" | ||
| + | try: | ||
| + | response = urequests.get(URL) | ||
| + | if response.status_code == 200: | ||
| + | data = response.json() | ||
| + | current = data.get(" | ||
| + | | ||
| + | temp = current.get(" | ||
| + | windspeed = current.get(" | ||
| + | weathercode = current.get(" | ||
| + | | ||
| + | print(" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(" | ||
| + | else: | ||
| + | print(f" | ||
| + | | ||
| + | response.close() | ||
| + | except Exception as e: | ||
| + | print(" | ||
| + | |||
| + | # --- RUN TEST --- | ||
| + | if connect_wifi(): | ||
| + | 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.1789051123.txt.gz · Last modified: by walkeradmin
