This has been my main project since 2019 which started on the Adafruit Bluefruit Sense microcontroller with Arduino. I eventually ported it to Circuit Python... and I've never used Arduino since. There are many different versions of this project on my github that are either offline only, offline with GPS, offline & online, offline & online with MQTT.
The project I'm detailing today is offline & online with MQTT to AdafruitIO. This means if for whatever reason your WiFi goes down, OpenWeatherMap.org servers go down, or AdafruitIO goes down it will still display local sensor data and function in an offline capacity waiting patiently until communication is restored.
The display sits in front of my PC monitor and has been running 24/7 for about 3 years now. I've had a lot of time to debug all of the things that might cause it to crash, error, and gracefully fail in a never ending loop. It's not perfect but it's solidly coded.

This is the Feather Weather GUI. It incorporates API data from OpenWeatherMap.org (labels in blue) and real-time sensor data from an Adafruit BME280 module (labels in orange). It then collates the data from the BME280 sensor and whisks it away to my AdafruitIO dashboard using MQTT.
It has some additional features like showing the battery voltage and severe weather warnings. This project was designed as a battery powered weather station during hurricanes and storm related power outages.
Skill Level: Advanced: 600 lines of code
- This is not a beginner friendly project. This one is targeted at advanced Circuit Python users that have experience with JSON parsing, MQTT broker, and GUI design. There are potentially a lot of errors that might crop up during the course of this project that could get you stuck without prior experience. Help is always available in the Adafruit Discord if you want to tackle this project anyway.
- If this project seems too daunting and you'd like to try an easier version aimed more at beginners (because i was a beginner when I wrote it) then check out my Offline Feather Weather. It's only 200 lines of code.
Requirements:
- TFT display of your choosing (I'm using a 3.5" TFT Featherwing)
- OpenWeatherMap.org (free) account & API token
- AdafruitIO (free) account & API token (key)
Optional:
- Temperature/Pressure/Humidity Sensor (BME280)
- Display with built-in SDCard or SDCard module used for screenshots of your GUI.
- Adafruit LiPo battery (I'm using a 10,000 mah)
As of Circuit Python 8.x the current best practice has moved to using settings.toml instead of secrets.py for credential storage. If you're going to tackle this project then you should have a minimal knowledge for how to swap in code for using settings.toml fairly quickly.
secrets.py or settings.toml
Create variables in your respective file for:
- aio_username
- aio_key
- openweather_token
- openweather_lat
- openweather_lon
Latitude & Longitude are used for global compatibility. England for example does not use city/zip but lat & lon will work perfectly first try. OpenWeatherMap moved to this scheme as there became a big problem with multiple cities with the same name in the same country and only the US uses city/zip. Using lat & lon solves that problem instantly as well as providing the correct timezone and timezone offset simultaneously.
Here's an example of a settings.toml file which uses Adafruit's HQ for lat/lon by default.
WIFI_SSID = "Your Wifi SSID" WIFI_PASSWORD = "Your WiFi Password" aio_username = "Your AdafruitIO Username" aio_key = "Your AdafruitIO Key" openweather_token = "Your OpenWeatherMaps Token" openweather_lat = "40.7259" #default lat/lon is adafruit industries openweather_lon = "-74.0055"
Circuit Python 8.2.x Code
Github Repo: Feather Weather with MQTT
There are a few external libraries here to get from the Circuit Python Bundle
Notably:
- adafruit_bitmap_font (for loading custom fonts)
- adafruit_imageload (to display bmp images)
- adafruit_sdcard (to save screenshots to)
- adafruit_lc709203f (this was replaced with a MAX17048 chip on newer ESP32-S3 feathers)
- adafruit_bme280 (I2C sensor data)
- adafruit_minimqtt (MQTT broker)
- adafruit_display_text (for displaying text labels)
- adafruit_display_shapes (for creating rounded rectangle)
- adafruit_hx8357 (3.5" TFT Featherwing display driver)
The rest of the imports should be built into the Adafruit ESP32-S3 Feather itself. You might not necessarily need every single one of these imports.
Let's begin by setting up all of the library imports that will be needed in this script.
# SPDX-FileCopyrightText: 2023 DJDevon3 # SPDX-License-Identifier: MIT # Adafruit ESP32-S3 Feather Weather with MQTT # Coded for Circuit Python 8.2.x import os import supervisor import time import board import displayio import terminalio import adafruit_imageload import ssl import wifi import socketpool import adafruit_requests import ulab.numpy as np import adafruit_minimqtt.adafruit_minimqtt as MQTT from adafruit_minimqtt.adafruit_minimqtt import MMQTTException from adafruit_display_text import label from adafruit_display_shapes.roundrect import RoundRect from adafruit_bitmap_font import bitmap_font from adafruit_lc709203f import LC709203F from adafruit_bme280 import basic as adafruit_bme280 from adafruit_hx8357 import HX8357
Next we setup some display parameters, initialize the socketpool, and create the os env variables for settings.toml
# 3.5" TFT Featherwing is 480x320 displayio.release_displays() DISPLAY_WIDTH = 480 DISPLAY_HEIGHT = 320 # Initialize Web Sockets (This should always be near the top of a script!) # There can be only one pool pool = socketpool.SocketPool(wifi.radio) # Use settings.toml for credentials ssid = os.getenv("WIFI_SSID") appw = os.getenv("WIFI_PASSWORD") aio_username = os.getenv("aio_username") aio_key = os.getenv("aio_key") # Local time & weather from lat/lon OWKEY = os.getenv("openweather_token") OWLAT = os.getenv("openweather_lat") OWLON = os.getenv("openweather_lon")
MQTT Setup
You are free to customize your feed names. You don't need to manually create a feed group from AdafruitIO's website, MQTT will create it for you! Very nice feature.
# MQTT Topic # Use this format for a standard MQTT broker feed_01 = aio_username + "/feeds/BME280-Unbiased" feed_02 = aio_username + "/feeds/BME280-RealTemp" feed_03 = aio_username + "/feeds/BME280-Pressure" feed_04 = aio_username + "/feeds/BME280-Humidity" feed_05 = aio_username + "/feeds/BME280-Altitude" # Time in seconds between updates (polling) # 600 = 10 mins, 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour sleep_time = 900
Here is an example of the AdafruitIO default feeds it will automatically generate once the entire script runs for the first time.
Initialize all the things!
Because I'm using a TFT Featherwing all I have to do is plug the ESP32-S3 feather right into the back of the TFT Featherwing display, attach the BME280 sensor via STEMMA port, and it's ready to go.
This is where all the attached hardware and GPIO pins are initialized. The only caveat is
- # sea_level_pressure should be set in the while true loop
# Initialize TFT Display spi = board.SPI() tft_cs = board.D9 tft_dc = board.D10 display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs) display = HX8357(display_bus, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT) # Initialize BME280 Sensor i2c = board.STEMMA_I2C() # uses board.SCL and board.SDA bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c) # sea_level_pressure should be set in the while true loop # bme280.sea_level_pressure = bme280.pressure # print("Sea Level Pressure: ", bme280.sea_level_pressure) # print("Altitude = %0.2f meters" % bme280.altitude) # Initialize SDCard on TFT Featherwing cs = digitalio.DigitalInOut(board.D5) sdcard = adafruit_sdcard.SDCard(spi, cs) vfs = storage.VfsFat(sdcard) virtual_root = "/sd" storage.mount(vfs, virtual_root) i2c = board.I2C() battery_monitor = LC709203F(board.I2C()) # LC709203F github repo library # https://github.com/adafruit/Adafruit_CircuitPython_LC709203F/blob/main/adafruit_lc709203f.py # only up to 3000 supported, don't use PackSize if battery larger than 3000mah # battery_monitor.pack_size = PackSize.MAH3000 battery_monitor.thermistor_bconstant = 3950 battery_monitor.thermistor_enable = True
Time_Calc Function:
This is a time conversion function used for converting the sleep_time polling variable into a human readable format. Instead of saying "Next Update: 7200 seconds" it will automatically convert time to say "Next Update: 2 hours". This can be very handy if you only want to update something once a day or once a week and not have to worry about converting seconds in your head.
Then the obligatory quick colors and loading in custom fonts.
# Converts seconds in minutes/hours/days # Attribution: Written by DJDevon3 & refined by Elpekenin def time_calc(input_time): if input_time < 60: return f"{input_time:.0f} seconds" if input_time < 3600: return f"{input_time / 60:.0f} minutes" if input_time < 86400: return f"{input_time / 60 / 60:.0f} hours" return f"{input_time / 60 / 60 / 24:.1f} days" # Quick Colors for Labels TEXT_BLACK = 0x000000 TEXT_BLUE = 0x0000FF TEXT_CYAN = 0x00FFFF TEXT_GRAY = 0x8B8B8B TEXT_GREEN = 0x00FF00 TEXT_LIGHTBLUE = 0x90C7FF TEXT_MAGENTA = 0xFF00FF TEXT_ORANGE = 0xFFA500 TEXT_PURPLE = 0x800080 TEXT_RED = 0xFF0000 TEXT_WHITE = 0xFFFFFF TEXT_YELLOW = 0xFFFF00 # Fonts are optional medium_font = bitmap_font.load_font("/fonts/Arial-16.bdf") huge_font = bitmap_font.load_font("/fonts/GoodTimesRg-Regular-121.bdf")
OpenWeatherMap Data Source
This script currently uses OpenWeatherMap 2.5 API (free). The data source is just a json URL broken down into parts with variables inserted into the appropriate places per their API. You can craft the entire url and enter it into a browser and you should see a JSON parsed list.
Insert your OpenWeatherMap token. I've set the default lat/lon to Adafruit Industries HQ.
The date & time functions are for converting OpenWeatherMap.org JSON to a Circuit Python readable format.
# OpenWeather 2.5 Free API DATA_SOURCE = "https://api.openweathermap.org/data/2.5/onecall?" DATA_SOURCE += "lat=" + OWLAT DATA_SOURCE += "&lon=" + OWLON DATA_SOURCE += "&exclude=hourly,daily" DATA_SOURCE += "&appid=" + OWKEY DATA_SOURCE += "&units=imperial" def _format_date(datetime): return "{:02}/{:02}/{:02}".format( datetime.tm_year, datetime.tm_mon, datetime.tm_mday, ) def _format_time(datetime): return "{:02}:{:02}".format( datetime.tm_hour, datetime.tm_min, # datetime.tm_sec, )

Now that you can see how the JSON results are ordered it should make it easier to create the correct key:value pairs to drill down to any data you want to use. Knowing how to access the JSON data (to see how they structure their API) with a web browser will make it really easy for you to customize your project in the future. I've found their API documentation doesn't always reflect what the actual structure is showing 1:1. Like all big projects, API documentation is a chore and can be out of date. You have to be able to recognize when something is a little off and know how to make a request for the actual structure being shown.
Labels Ahoy!
Labels are the x/y position of each textual element you want to display. You'll find that x/y placement is a tedious task for GUI's constantly reloading and rearranging things until they're exactly where you want them to be.
Anecdata wrote a function that allows each line of a label to be drastically smaller. Using his make_a_label function reduced my script by over 100 lines!
# Function for minimizing labels to 1 liners # Attribution: Anecdata (thanks!) def make_my_label(font, anchor_point, anchored_position, scale, color): func_label = label.Label(font) func_label.anchor_point = anchor_point func_label.anchored_position = anchored_position func_label.scale = scale func_label.color = color return func_label # Individual customizable position labels # https://learn.adafruit.com/circuitpython-display-support-using-displayio/text # name_label (FONT, (ANCHOR POINT), (ANCHOR POSITION), SCALE, COLOR) hello_label = make_my_label( terminalio.FONT, (0.5, 1.0), (DISPLAY_WIDTH / 2, 15), 1, TEXT_WHITE ) warning_label = make_my_label( terminalio.FONT, (0.5, 1.0), (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 35), 3, TEXT_RED ) warning_text_label = make_my_label( terminalio.FONT, (0.5, 1.0), (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 5), 2, TEXT_RED ) date_label = make_my_label(medium_font, (0.0, 0.0), (5, 5), 1, TEXT_LIGHTBLUE) time_label = make_my_label(medium_font, (0.0, 0.0), (5, 25), 2, TEXT_LIGHTBLUE) temp_label = make_my_label(medium_font, (1.0, 1.0), (475, 145), 2, TEXT_ORANGE) temp_data_label = make_my_label( huge_font, (0.5, 1.0), (DISPLAY_WIDTH / 2, 200), 1, TEXT_ORANGE ) temp_data_shadow = make_my_label( huge_font, (0.5, 1.0), (DISPLAY_WIDTH / 2 + 2, 200 + 2), 1, TEXT_BLACK ) owm_temp_data_label = make_my_label( medium_font, (0.5, 1.0), (DISPLAY_WIDTH / 2, 100), 2, TEXT_LIGHTBLUE ) owm_temp_data_shadow = make_my_label( medium_font, (0.5, 1.0), (DISPLAY_WIDTH / 2 + 2, 100 + 2), 2, TEXT_BLACK ) humidity_label = make_my_label( medium_font, (0.0, 1.0), (5, DISPLAY_HEIGHT - 23), 1, TEXT_GRAY ) humidity_data_label = make_my_label( medium_font, (0.0, 1.0), (5, DISPLAY_HEIGHT), 1, TEXT_ORANGE ) owm_humidity_data_label = make_my_label( medium_font, (0.0, 1.0), (5, DISPLAY_HEIGHT - 55), 1, TEXT_LIGHTBLUE ) barometric_label = make_my_label( medium_font, (1.0, 1.0), (470, DISPLAY_HEIGHT - 27), 1, TEXT_GRAY ) barometric_data_label = make_my_label( medium_font, (1.0, 1.0), (470, DISPLAY_HEIGHT), 1, TEXT_ORANGE ) owm_barometric_data_label = make_my_label( medium_font, (1.0, 1.0), (470, DISPLAY_HEIGHT - 55), 1, TEXT_LIGHTBLUE ) owm_windspeed_label = make_my_label( medium_font, (1.0, 1.0), (DISPLAY_WIDTH - 5, 50), 1, TEXT_LIGHTBLUE ) vbat_label = make_my_label(medium_font, (1.0, 1.0), (DISPLAY_WIDTH - 15, 20), 1, None) plugbmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None) greenbmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None) bluebmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None) yellowbmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None) orangebmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None) redbmp_label = make_my_label(terminalio.FONT, (1.0, 1.0), None, 1, None)
Bitmaps, Spritesheets, and RoundRect
You can find all icons, images, and fonts in the Github repo linked at the top or bottom of this guide.
Here is where we load in our background image. The image I'm using is a 480x320 8-bit indexed BMP.
The spritesheet is for the battery gauge icon. The spritesheet will display a plug icon if it detects USB power (later in the code). The round rectangle is the background layer for the severe weather popup warning label.
# Load Bitmap to tile grid first (Background layer) DiskBMP = displayio.OnDiskBitmap("/images/Astral_Fruit_8bit.bmp") tile_grid = displayio.TileGrid( DiskBMP, pixel_shader=DiskBMP.pixel_shader, width=1, height=1, tile_width=DISPLAY_WIDTH, tile_height=DISPLAY_HEIGHT) # Load battery voltage icons (from 1 sprite sheet image) sprite_sheet, palette = adafruit_imageload.load("/icons/vbat_spritesheet.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=11, tile_height=20) sprite_group = displayio.Group(scale=1) sprite_group.append(sprite) sprite_group.x = 470 sprite_group.y = 0 # Warning label RoundRect roundrect = RoundRect(int(DISPLAY_WIDTH/2-140), int(DISPLAY_HEIGHT-75), 280, 75, 10, fill=0x0, outline=0xFFFFFF, stroke=1)
# Create subgroups text_group = displayio.Group() text_group.append(tile_grid) temp_group = displayio.Group() warning_group = displayio.Group() main_group = displayio.Group() # Add subgroups to main display group main_group.append(text_group) main_group.append(warning_group) main_group.append(temp_group) main_group.append(sprite_group) # Add warning popup group warning_group.append(roundrect) warning_group.append(warning_label) warning_group.append(warning_text_label) # Label Display Group (foreground layer) text_group.append(hello_label) text_group.append(date_label) text_group.append(time_label) temp_group.append(temp_label) temp_group.append(temp_data_shadow) temp_group.append(temp_data_label) temp_group.append(owm_temp_data_shadow) temp_group.append(owm_temp_data_label) text_group.append(humidity_label) text_group.append(humidity_data_label) text_group.append(owm_humidity_data_label) text_group.append(barometric_label) text_group.append(barometric_data_label) text_group.append(owm_barometric_data_label) text_group.append(owm_windspeed_label) text_group.append(vbat_label) text_group.append(plugbmp_label) text_group.append(greenbmp_label) text_group.append(bluebmp_label) text_group.append(yellowbmp_label) text_group.append(orangebmp_label) text_group.append(redbmp_label) display.show(main_group) def show_warning(title, text): warning_label.text = title warning_text_label.text = text warning_group.hidden = False def hide_warning(): warning_group.hidden = True
# Define callback methods when events occur def connect(mqtt_client): # This function will be called when the mqtt_client is connected # successfully to the broker. print("Connected to MQTT Broker! ✅") def disconnect(mqtt_client, userdata, rc): # This method is called when the mqtt_client disconnects # from the broker. print("Disconnected from MQTT Broker!") def subscribe(mqtt_client, userdata, topic, granted_qos): # This method is called when the mqtt_client subscribes to a new feed. print("Subscribed to {0} with QOS level {1}".format(topic, granted_qos)) def unsubscribe(mqtt_client, userdata, topic, pid): # This method is called when the mqtt_client unsubscribes from a feed. print("Unsubscribed from {0} with PID {1}".format(topic, pid)) def publish(mqtt_client, userdata, topic, pid): # This method is called when the mqtt_client publishes data to a feed. print("Published to {0} with PID {1}".format(topic, pid)) def message(client, topic, message): # Method called when a client's subscribed feed has a new value. print("New message on topic {0}: {1}".format(topic, message)) # Initialize a new MQTT Client object mqtt_client = MQTT.MQTT( broker="io.adafruit.com", port=1883, username=aio_username, password=aio_key, socket_pool=pool, ssl_context=ssl.create_default_context(), ) # Initialize an Adafruit IO MQTT Client io = IO_MQTT(mqtt_client) # Connect callback handlers to mqtt_client io.on_connect = connect io.on_disconnect = disconnect io.on_subscribe = subscribe io.on_unsubscribe = unsubscribe io.on_publish = publish io.on_message = message
NumPy Interpolation Algorithm
Last is used to check for last update vs current monotonic time. Display_temperature is used with the numpy algorithm. You can set more data points in the algorithm to adjust your temp sensor to be even more accurate.
last = 0 display_temperature = 0 # Define the input range and output range input_range = [50.0, 70, 80, 88.0, 120.0] output_range = [50.0 - 0.1, 70.0 - 2.0, 80 - 1.0, 88.0 - 0.0, 120.0 - 2.2]
adafruit_requests.Session()
It is imperative that you do not use this in the main while True: loop otherwise you will run into socket errors. You can place this anywhere above the while true loop.
# adafruit_requests.Session should always be outside the loop # otherwise you get Out of Socket errors. requests = adafruit_requests.Session(pool, ssl.create_default_context())
While True Loop
I recommend setting debug_OWM to True so you can see all of the serial output and then set it to False when you're ready to dial the serial prints back a bit.
Yes it's long but I figure it's best to keep it all in one chunk so the indentation formatting remains in place. There is a lot to digest in the main loop including USB and Battery power sensing, local sensor label updating, popup warning thresholds, WiFi connection, AdafruitIO connection, AdafruitIO publishing, error exception handling, retry handling, and screenshot capability.
while True: debug_OWM = False # Set True for Serial Print Debugging bme280.sea_level_pressure = bme280.pressure hello_label.text = "ESP32-S3 MQTT Feather Weather" print("===============================") # USB Power Sensing try: vbat_label.text = f"{battery_monitor.cell_voltage:.2f}" except (ValueError, RuntimeError, OSError) as e: print("LC709203F Error: \n", e) # Set USB plug icon and voltage label to white usb_sense = supervisor.runtime.usb_connected if debug_OWM: print("USB Sense: ", usb_sense) if usb_sense: # if on USB power show plug sprite icon vbat_label.color = TEXT_WHITE sprite[0] = 5 if not usb_sense: # if on battery power only # Changes battery voltage color depending on charge level if vbat_label.text >= "4.23": vbat_label.color = TEXT_WHITE sprite[0] = 5 elif "4.10" <= vbat_label.text <= "4.22": vbat_label.color = TEXT_GREEN sprite[0] = 0 elif "4.00" <= vbat_label.text <= "4.09": vbat_label.color = TEXT_LIGHTBLUE sprite[0] = 1 elif "3.90" <= vbat_label.text <= "3.99": vbat_label.color = TEXT_YELLOW sprite[0] = 2 elif "3.80" <= vbat_label.text <= "3.89": vbat_label.color = TEXT_ORANGE sprite[0] = 3 # TFT cutoff voltage is 3.70 elif vbat_label.text <= "3.79": vbat_label.color = TEXT_RED sprite[0] = 4 else: vbat_label.color = TEXT_WHITE # Local sensor data display temp_label.text = "°F" # Board Uptime print("Board Uptime: ", time_calc(time.monotonic())) # Account for PCB heating bias, gets slightly hotter as ambient increases temperature = bme280.temperature * 1.8 + 32 temp_round = round(temperature, 2) print("Temp: ", temperature) # biased reading display_temperature = np.interp(temperature, input_range, output_range) BME280_temperature = round(display_temperature[0], 2) print(f"Actual Temp: {BME280_temperature:.1f}") if debug_OWM: BME280_pressure = 1005 # Manually set debug warning message print(f"BME280 Pressure: {BME280_pressure}") else: BME280_pressure = round(bme280.pressure, 1) BME280_humidity = round(bme280.relative_humidity, 1) BME280_altitude = round(bme280.altitude, 2) temp_data_shadow.text = f"{BME280_temperature:.1f}" temp_data_label.text = f"{BME280_temperature:.1f}" humidity_label.text = "Humidity" humidity_data_label.text = f"{BME280_humidity:.1f} %" barometric_label.text = "Pressure" barometric_data_label.text = f"{BME280_pressure:.1f}" # Warnings based on local sensors if BME280_pressure <= 919: # pray you never see this message show_warning("HOLY COW", "Seek Shelter!") elif 920 <= BME280_pressure <= 979: show_warning("DANGER", "Major Hurricane") elif 980 <= BME280_pressure <= 989: show_warning("DANGER", "Minor Hurricane") elif 990 <= BME280_pressure <= 1001: show_warning("WARNING", "Tropical Storm") elif 1002 <= BME280_pressure <= 1009: # sudden gusty downpours show_warning("CAUTION", "Low Pressure System") elif 1019 <= BME280_pressure <= 1025: # sudden light cold rain show_warning("CAUTION", "High Pressure System") elif BME280_pressure >= 1026: show_warning("WARNING", "Hail & Tornados?") else: hide_warning() # Normal pressures: 1110-1018 (no message) print("| Connecting to WiFi...") while not wifi.radio.ipv4_address: try: wifi.radio.connect(ssid, appw) except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") time.sleep(10) print("| ✅ WiFi!") while wifi.radio.ipv4_address: try: print("| | Attempting to GET Weather!") if debug_OWM: print("Full API GET URL: ", DATA_SOURCE) print("\n===============================") with requests.get(DATA_SOURCE) as owm_request: # uncomment the 2 lines below to see full json response # warning: returns ALL JSON data, could crash your board # dump_object = json.dumps(owm_request) # print("JSON Dump: ", dump_object) try: owm_response = owm_request.json() if owm_response["message"]: print(f"| | ❌ OpenWeatherMap Error: {owm_response['message']}") owm_request.close() except (KeyError) as e: owm_response = owm_request.json() print("| | Account within Request Limit", e) print("| | ✅ Connected to OpenWeatherMap") # Timezone & offset automatically returned based on lat/lon get_timezone_offset = int(owm_response["timezone_offset"]) # 1 tz_offset_seconds = get_timezone_offset if debug_OWM: print(f"Timezone Offset (in seconds): {get_timezone_offset}") get_timestamp = int(owm_response["current"]["dt"] + int(tz_offset_seconds)) # 2 current_unix_time = time.localtime(get_timestamp) current_struct_time = time.struct_time(current_unix_time) current_date = "{}".format(_format_date(current_struct_time)) current_time = "{}".format(_format_time(current_struct_time)) sunrise = int(owm_response["current"]["sunrise"] + int(tz_offset_seconds)) # 3 sunrise_unix_time = time.localtime(sunrise) sunrise_struct_time = time.struct_time(sunrise_unix_time) sunrise_time = "{}".format(_format_time(sunrise_struct_time)) sunset = int(owm_response["current"]["sunset"] + int(tz_offset_seconds)) # 4 sunset_unix_time = time.localtime(sunset) sunset_struct_time = time.struct_time(sunset_unix_time) sunset_time = "{}".format(_format_time(sunset_struct_time)) owm_temp = owm_response["current"]["temp"] # 5 owm_pressure = owm_response["current"]["pressure"] # 6 owm_humidity = owm_response["current"]["humidity"] # 7 weather_type = owm_response["current"]["weather"][0]["main"] # 8 owm_windspeed = float(owm_response["current"]["wind_speed"]) # 9 print("| | | Sunrise:", sunrise_time) print("| | | Sunset:", sunset_time) print("| | | Temp:", owm_temp) print("| | | Pressure:", owm_pressure) print("| | | Humidity:", owm_humidity) print("| | | Weather Type:", weather_type) print("| | | Wind Speed:", owm_windspeed) print("| | | Timestamp:", current_date + " " + current_time) date_label.text = current_date time_label.text = current_time owm_windspeed_label.text = f"{owm_windspeed:.1f} mph" owm_temp_data_shadow.text = f"{owm_temp:.1f}" owm_temp_data_label.text = f"{owm_temp:.1f}" owm_humidity_data_label.text = f"{owm_humidity:.1f} %" owm_barometric_data_label.text = f"{owm_pressure:.1f}" pass except (ValueError, RuntimeError) as e: print("ValueError: Failed to get OWM data, retrying\n", e) supervisor.reload() break except OSError as g: if g.errno == -2: print("gaierror, breaking out of loop\n", g) time.sleep(240) break print("| | ✂️ Disconnected from OpenWeatherMap") # Connect to Adafruit IO try: mqtt_client.connect() mqtt_client.publish(feed_01, temp_round) # slight delay required between publishes! # otherwise only the 1st publish will succeed time.sleep(0.001) mqtt_client.publish(feed_02, BME280_temperature) time.sleep(1) mqtt_client.publish(feed_03, BME280_pressure) time.sleep(1) mqtt_client.publish(feed_04, BME280_humidity) time.sleep(1) mqtt_client.publish(feed_05, BME280_altitude) time.sleep(1) except (ValueError, RuntimeError, ConnectionError, OSError, MMQTTException) as ex: print("| | ❌ Failed to connect, retrying\n", ex) # traceback.print_exception(ex, ex, ex.__traceback__) # supervisor.reload() continue mqtt_client.disconnect() print("| ✂️ Disconnected from Wifi") print("Next Update: ", time_calc(sleep_time)) time.sleep(sleep_time) break
Finally if everything works correctly you should be able to create an AdafruitIO dashboard using the feeds generated by MQTT. You must create a Dashboard from each feed manually to get a Dashboard logging output. MQTT will only create and publish the feeds, it will not create blocks within the logging dashboard.
You should now be pulling data from OpenWeatherMap API, displaying it on the TFT, and logging your local BME280 sensor data to AdafruitIO!
The entire script is about 600 lines of code. It's taken quite a while to build up the layers of complexity. I've done a lot of projects that have been very fun but this one is by far my favorite. Year after year I keep this one updated and continually upgraded with new features.



