[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/252/original/AQ_Display.jpg?1770395478","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eAdafruit IO Adds a New Power-up: AirQuality\u003c/h1\u003e\n\u003cp\u003e\u003cspan\u003eAdafruit IO recently added a new Power‑Up for IO+ subscribers: \u003cstrong\u003eAir Quality\u003c/strong\u003e. It works much like the existing Weather Power‑Up, pulling data from either \u003cstrong\u003eOpen‑Meteo\u003c/strong\u003e (global coverage) or \u003cstrong\u003eAirNow\u003c/strong\u003e (official US EPA data). Naturally, I wanted to integrate it into my projects — and assumed it would be as simple as calling a new helper function in the \u003ccode\u003eadafruit_io\u003c/code\u003e library.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eExcept… that function didn’t exist yet.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eSo, this became a perfect opportunity to walk through \u003cstrong\u003ehow to explore a CircuitPython library\u003c/strong\u003e, understand how it works under the hood, and extend it safely when needed.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n        \u003ch1\u003eDiving Into the Library\u003c/h1\u003e\n\u003ch2\u003eWhat is a library really?\u003c/h2\u003e\n\u003cp\u003e\u003cspan\u003eWhen \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003efirst \u003c/span\u003e\u003cspan\u003estarted \u003c/span\u003e\u003cspan\u003ewith \u003c/span\u003e\u003cspan\u003eCircuitPython, \u003c/span\u003e\u003cspan\u003elibraries \u003c/span\u003e\u003cspan\u003efelt \u003c/span\u003e\u003cspan\u003emysterious \u003c/span\u003e\u003cspan\u003e— \u003c/span\u003e\u003ccode\u003e\u003cspan\u003e.mpy\u003c/span\u003e\u003c/code\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003efiles \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003ecouldn’t \u003c/span\u003e\u003cspan\u003eopen, \u003c/span\u003e\u003cspan\u003edoing \u003c/span\u003e\u003cspan\u003ecomplex \u003c/span\u003e\u003cspan\u003ethings \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003edidn’t \u003c/span\u003e\u003cspan\u003eneed \u003c/span\u003e\u003cspan\u003eto \u003c/span\u003e\u003cspan\u003eunderstand. \u003c/span\u003e\u003cspan\u003eEventually \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003ediscovered \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cstrong\u003e\u003cspan\u003ePython \u003c/span\u003e\u003cspan\u003esource \u003c/span\u003e\u003cspan\u003ebundles\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eon \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cspan\u003eLibraries \u003c/span\u003e\u003cspan\u003epage, \u003c/span\u003e\u003cspan\u003eand \u003c/span\u003e\u003cspan\u003eeverything \u003c/span\u003e\u003cspan\u003eclicked: \u003c/span\u003e\u003cspan\u003ethese \u003c/span\u003e\u003cspan\u003eare \u003c/span\u003e\u003cspan\u003ejust \u003c/span\u003e\u003cspan\u003enormal \u003c/span\u003e\u003cspan\u003ePython \u003c/span\u003e\u003cspan\u003efiles, \u003c/span\u003e\u003cspan\u003ecompiled \u003c/span\u003e\u003cspan\u003efor \u003c/span\u003e\u003cspan\u003eefficiency.\u003c/span\u003e\u003c/p\u003e\n\u003ch2\u003eFinding the AQI Function\u003c/h2\u003e\n\u003cp\u003e\u003cspan\u003e\u003cspan\u003eFor \u003c/span\u003e\u003cspan\u003ethis \u003c/span\u003e\u003cspan\u003eproject, \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003eopened \u003c/span\u003e\u003ccode\u003e\u003cspan\u003eadafruit_io.py\u003c/span\u003e\u003c/code\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eand \u003c/span\u003e\u003cspan\u003esearched \u003c/span\u003e\u003cspan\u003efor \u003c/span\u003e\u003cspan\u003efunctions \u003c/span\u003e\u003cspan\u003ebeginning \u003c/span\u003e\u003cspan\u003ewith \u003c/span\u003e\u003ccode\u003e\u003cspan\u003ereceive_\u003c/span\u003e\u003c/code\u003e\u003cspan\u003e, \u003c/span\u003e\u003cspan\u003esince \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cspan\u003eWeather \u003c/span\u003e\u003cspan\u003ePower‑Up \u003c/span\u003e\u003cspan\u003euses:\u003c/span\u003e \u003c/span\u003e\u003cem\u003ereceive_weather().\u0026nbsp;\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eI found functions for weather, time, random data, and more — but nothing for the new Air Quality Power‑Up. That meant the library hadn’t been updated yet.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eTime to roll up my sleeves.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"alert","content":"\u003cp\u003eBy the time you read this it is quite possible that they adafruit_io library has been updated with new functions to use the new Power-Up, so you likely do not really need this hack, but I thought that this is a good example of how to customize libraries in general for the next time you want to go the next level with libraries.\u003c/p\u003e","metadata":{"markdown":"By the time you read this it is quite possible that they adafruit_io library has been updated with new functions to use the new Power-Up, so you likely do not really need this hack, but I thought that this is a good example of how to customize libraries in general for the next time you want to go the next level with libraries.","alert_type":"primary","icon":"thinking"}},{"element_type":"text","content":"\n  \n  \n        \u003cp\u003eThe Weather Power-Up used this endpoint:\u0026nbsp;\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003cem\u003e\u003cspan class=\"url\"\u003e/api/v2/:username/integrations/\u003cstrong\u003eweather\u003c/strong\u003e\u003c/span\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"url\"\u003eAnd the AQI Power-Up uses:\u003c/span\u003e\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003cem\u003e\u003cspan class=\"url\"\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003e/api/v2/:username/integrations/\u003cstrong\u003eair_quality\u003c/strong\u003e\u003c/span\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eSo \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cspan\u003esimplest \u003c/span\u003e\u003cspan\u003eapproach \u003c/span\u003e\u003cspan\u003eis \u003c/span\u003e\u003cspan\u003eto \u003c/span\u003e\u003cstrong\u003e\u003cspan\u003ecopy \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cspan\u003eweather \u003c/span\u003e\u003cspan\u003efunction \u003c/span\u003e\u003cspan\u003eand \u003c/span\u003e\u003cspan\u003eadapt \u003c/span\u003e\u003cspan\u003eit\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e:\u003c/span\u003e\u003c/p\u003e\n      \n\n","metadata":{}},{"element_type":"code","content":"# Adafruit IO Connected Services\n    def receive_weather(self, weather_id: int):\n        \"\"\"\n        Get data from the Adafruit IO Weather Forecast Service\n        NOTE: This service is avaliable to Adafruit IO Plus subscribers only.\n\n        :param int weather_id: ID for retrieving a specified weather record.\n        \"\"\"\n        path = self._compose_path(f\"integrations/weather/{weather_id}\")\n        return self._get(path)","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eSo, the new function should be:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"def receive_aqi(self, aqi_id: int):\n        \"\"\"\n        Get data from the Adafruit IO AQI Service\n        NOTE: This service is avaliable to Adafruit IO Plus subscribers only.\n\n        :param int aqi_id: ID for retrieving a specified AQI record.\n        \"\"\"\n        path = self._compose_path(f\"integrations/air_quality/{aqi_id}\")\n        return self._get(path)","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eA few important notes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eRemove or rename the original \u003ccode\u003e.mpy\u003c/code\u003e file so CircuitPython doesn’t load the wrong version.\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003ePlace your modified \u003ccode\u003e.py\u003c/code\u003e file in the \u003ccode\u003eadafruit_io\u003c/code\u003e folder inside \u003ccode\u003elib\u003c/code\u003e.\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eBe aware that customizing a library \u003cstrong\u003edisconnects you from future updates\u003c/strong\u003e — so only do this when you have a specific need.\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eNow We Build\u003c/h1\u003e\n\u003cp\u003e\u003cspan\u003eWhile \u003c/span\u003e\u003cspan\u003eorganizing \u003c/span\u003e\u003cspan\u003emy \u003c/span\u003e\u003cspan\u003eworkshop, \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003erealized \u003c/span\u003e\u003cspan\u003eI \u003c/span\u003e\u003cspan\u003ehad \u003c/span\u003e\u003cspan\u003eeverything \u003c/span\u003e\u003cspan\u003eneeded \u003c/span\u003e\u003cspan\u003eto \u003c/span\u003e\u003cspan\u003ebuild \u003c/span\u003e\u003cspan\u003ea \u003c/span\u003e\u003cspan\u003esmall \u003c/span\u003e\u003cspan\u003eAQI \u003c/span\u003e\u003cspan\u003edisplay \u003c/span\u003e\u003cspan\u003eusing \u003c/span\u003e\u003cspan\u003ea \u003c/span\u003e\u003cstrong\u003e\u003cspan\u003eRaspberry \u003c/span\u003e\u003cspan\u003ePi \u003c/span\u003e\u003cspan\u003ePico \u003c/span\u003e\u003cspan\u003e2W\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eand \u003c/span\u003e\u003cspan\u003ean \u003c/span\u003e\u003cstrong\u003e\u003cspan\u003eHDMI \u003c/span\u003e\u003cspan\u003edisplay\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003evia \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cstrong\u003e\u003cspan\u003eHSTX \u003c/span\u003e\u003cspan\u003eDVI \u003c/span\u003e\u003cspan\u003ePiCowbell\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e. \u003c/span\u003e\u003cspan\u003ePerfect \u003c/span\u003e\u003cspan\u003eexcuse \u003c/span\u003e\u003cspan\u003eto \u003c/span\u003e\u003cspan\u003etry \u003c/span\u003e\u003cspan\u003ethe \u003c/span\u003e\u003cspan\u003enew \u003c/span\u003e\u003cspan\u003ePower‑Up.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/6087","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/6363","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2197","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2819","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/1667","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003cp\u003eAssembly is straight forward - follow the various Learn Guides.\u0026nbsp;\u003c/p\u003e\n\u003ch2\u003eCoding the Project\u003c/h2\u003e\n\u003cp\u003eFirst, be sure you have a\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003cem\u003esettings.toml\u003c/em\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003efile which includes the following elements:\u003c/p\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"code","content":"CIRCUITPY_WIFI_SSID = \"***Your WiFi SSID Here***\"\nCIRCUITPY_WIFI_PASSWORD = \"***Your WiFi Password Here***\"\naio_username = \"***Your AIO Username Here***\"\naio_key = \"***Your AIO Key Here***\"\nTIMEZONE=\"***Your Timezone Here***\"\nAQIO = xx \nAQIE = xx","metadata":{"language":"auto","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003cp\u003eNote that the\u0026nbsp;\u003cstrong\u003eAQIO\u003c/strong\u003e (Open Meteo) and \u003cstrong\u003eAQIE\u003c/strong\u003e (Airnow) are interger IDs you set up for your location on the AQI Power-Up page.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eThe project uses:\u003c/span\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eA custom \u003ccode\u003esetup_display.py\u003c/code\u003e to initialize the DVI framebuffer\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eA large \u003ccode\u003ecode.py\u003c/code\u003e that:\u003c/span\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eConnects to WiFi\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eRetrieves AQI data from both services\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eDisplays current, forecast, and pollutant details\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eRotates health guidance text\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eUpdates hourly with retry logic\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eShows a countdown to the next update\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cspan\u003eThe display layout includes:\u003c/span\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eLeft column: labels\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eCenter column: Open‑Meteo data\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eRight column: AirNow data\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eColor indicators for category severity\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cspan\u003eRotating health recommendations\u003c/span\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cspan\u003eThe full code is long, but the structure is clean and modular, making it easy to adapt for other displays or data sources.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"code","content":"import supervisor\nimport displayio\nimport picodvi\nimport framebufferio\nimport board\n\n\ndef setup_display():\n    # Skip if the display is already initialized\n    if supervisor.runtime.display is None:\n        displayio.release_displays()\n        fb = picodvi.Framebuffer(320, 240,\n            clk_dp=board.GP14, clk_dn=board.GP15,\n            red_dp=board.GP12, red_dn=board.GP13,\n            green_dp=board.GP18, green_dn=board.GP19,\n            blue_dp=board.GP16, blue_dn=board.GP17,\n            color_depth=8)\n        display = framebufferio.FramebufferDisplay(fb)\n\n        # set the display onto supervisor.runtime,\n        # so it will be available to code.py\n        supervisor.runtime.display = display","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eNext is code.py for the project - as noted in my projects I do use AI a good deal to help with the grunt work:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"import os\nimport gc\nimport wifi\nimport socketpool\nimport ssl\nimport time\nimport rtc\nimport adafruit_requests\nfrom adafruit_io.adafruit_io import IO_HTTP\nimport displayio\nimport board\nimport supervisor\nfrom setup_display import setup_display\nfrom adafruit_display_shapes.rect import Rect\nfrom adafruit_display_shapes.roundrect import RoundRect\nfrom adafruit_display_shapes.line import Line\nfrom adafruit_bitmap_font import bitmap_font\nfrom adafruit_display_text import label, wrap_text_to_pixels\n\n# Debug flag - set to False to reduce memory usage\nDEBUG = False\n\n# Global list to store health texts for rotation (text, color)\nhealth_texts = []\n\ndef format_pollutant(value):\n    \"\"\"Helper function to format pollutant values\"\"\"\n    return \" - - \" if value is None else (f\"{value} ug\" if value != 'N/A' else 'N/A')\n\ndef hex_to_int(color_str):\n    \"\"\"Convert hex color string to integer\"\"\"\n    if color_str and color_str != 'N/A' and color_str.startswith('#'):\n        return int(color_str.replace('#', '0x'), 16)\n    return 0xFF0000  # Default to red if invalid\n\ndef title_case(s):\n    # Replace underscores with spaces first\n    s = s.replace('_', ' ')\n    if len(s) \u003e 3:\n        words = s.split()\n        titled = [word[0].upper() + word[1:].lower() if len(word) \u003e 0 else '' for word in words]\n        return ' '.join(titled)\n    else:\n        return s\n\ndef update_display_with_data(data_aqio, data_aqie):\n    \"\"\"\n    Update display with AQI data from both feeds\n    \"\"\"\n    global health_texts\n    \n    # Update last update time from RTC\n    current_time = time.localtime()\n    \n    # Collect health texts from both sources (alternating between services)\n    health_texts = []\n    \n    # Update AQIO column (center)\n    if data_aqio:\n        line1_c.text = title_case(data_aqio.get('provider', 'N/A'))\n        current = data_aqio.get('current', {})\n        \n        line2_c.text = str(current.get('aqi', 'N/A'))\n        line5_c.text = current.get('category_label', 'N/A')\n        line8_c.text = format_pollutant(current.get('pm2_5'))\n        line9_c.text = format_pollutant(current.get('o3'))\n        line10_c.text = (current.get('primary_pollutant', 'N/A') or 'N/A').upper()\n        line11_c.text = format_pollutant(current.get('pm10'))\n        line12_c.text = format_pollutant(current.get('co'))\n        line13_c.text = format_pollutant(current.get('no2'))\n        line14_c.text = format_pollutant(current.get('so2'))\n        o_color.fill = hex_to_int(current.get('category_color', '#FF0000'))\n        \n        forecast_today = data_aqio.get('forecast_today', {})\n        line3_c.text = str(forecast_today.get('aqi', 'N/A'))\n        line6_c.text = forecast_today.get('category_label', 'N/A')\n        o_color_f.fill = hex_to_int(forecast_today.get('category_color', '#FF0000'))\n        \n        forecast_tomorrow = data_aqio.get('forecast_tomorrow', {})\n        line4_c.text = str(forecast_tomorrow.get('aqi', 'N/A'))\n        line7_c.text = forecast_tomorrow.get('category_label', 'N/A')\n        o_color_t.fill = hex_to_int(forecast_tomorrow.get('category_color', '#FF0000'))\n        \n        # Collect OM health texts with time period prefixes and colors\n        health_texts.append((f\"Now: {current.get('health_description', 'N/A')}\", 0xFFFFFF, 0))  # White\n        health_texts.append((f\"Now: {current.get('health_recommendation', 'N/A')}\", 0xFFFFFF, 1))  # White\n        health_texts.append((f\"Today: {forecast_today.get('health_description', 'N/A')}\", 0xFFFF00, 4))  # Yellow\n        health_texts.append((f\"Today: {forecast_today.get('health_recommendation', 'N/A')}\", 0xFFFF00, 5))  # Yellow\n        health_texts.append((f\"Tomorrow: {forecast_tomorrow.get('health_description', 'N/A')}\", 0x00FFFF, 8))  # Cyan\n        health_texts.append((f\"Tomorrow: {forecast_tomorrow.get('health_recommendation', 'N/A')}\", 0x00FFFF, 9))  # Cyan\n    \n    # Update AQIE column (right)\n    if data_aqie:\n        line1_r.text = title_case(data_aqie.get('provider', 'N/A'))\n        current = data_aqie.get('current', {})\n        \n        line2_r.text = str(current.get('aqi', 'N/A'))\n        line5_r.text = current.get('category_label', 'N/A')\n        line8_r.text = format_pollutant(current.get('pm2_5'))\n        line9_r.text = format_pollutant(current.get('o3'))\n        line10_r.text = (current.get('primary_pollutant', 'N/A') or 'N/A').upper()\n        line11_r.text = format_pollutant(current.get('pm10'))\n        line12_r.text = format_pollutant(current.get('co'))\n        line13_r.text = format_pollutant(current.get('no2'))\n        line14_r.text = format_pollutant(current.get('so2'))\n        e_color.fill = hex_to_int(current.get('category_color', '#FF0000'))\n        \n        forecast_today = data_aqie.get('forecast_today', {})\n        line3_r.text = str(forecast_today.get('aqi', 'N/A'))\n        line6_r.text = forecast_today.get('category_label', 'N/A')\n        e_color_f.fill = hex_to_int(forecast_today.get('category_color', '#FF0000'))\n        \n        forecast_tomorrow = data_aqie.get('forecast_tomorrow', {})\n        line4_r.text = str(forecast_tomorrow.get('aqi', 'N/A'))\n        line7_r.text = forecast_tomorrow.get('category_label', 'N/A')\n        e_color_t.fill = hex_to_int(forecast_tomorrow.get('category_color', '#FF0000'))\n        \n        # Collect Airnow health texts with time period prefixes and colors\n        health_texts.append((f\"Now: {current.get('health_description', 'N/A')}\", 0xFFFFFF, 2))  # White\n        health_texts.append((f\"Now: {current.get('health_recommendation', 'N/A')}\", 0xFFFFFF, 3))  # White\n        health_texts.append((f\"Today: {forecast_today.get('health_description', 'N/A')}\", 0xFFFF00, 6))  # Yellow\n        health_texts.append((f\"Today: {forecast_today.get('health_recommendation', 'N/A')}\", 0xFFFF00, 7))  # Yellow\n        health_texts.append((f\"Tomorrow: {forecast_tomorrow.get('health_description', 'N/A')}\", 0x00FFFF, 10))  # Cyan\n        health_texts.append((f\"Tomorrow: {forecast_tomorrow.get('health_recommendation', 'N/A')}\", 0x00FFFF, 11))  # Cyan\n    \n    # Sort health_texts by the order integer (third element)\n    health_texts.sort(key=lambda x: x[2])\n\ndef get_aqi_data(feed_key, timeout=30):\n    \"\"\"\n    Retrieve AQI data from Adafruit IO feed\n    Args:\n        feed_key: Feed key string ('aquo' or 'aque')\n        timeout: Request timeout in seconds (default: 30)\n    Returns:\n        JSON data from the feed\n    \"\"\"\n    try:\n        print(f\"\\nRetrieving AQI data from feed: {feed_key}\")\n        # Force garbage collection before network request\n        gc.collect()\n        data = io.receive_aqi(feed_key)\n        parse_aqi_data(data)\n        return data\n    except OSError as e:\n        # Network-related errors (ETIMEDOUT, ECONNABORTED, etc.)\n        error_code = getattr(e, 'errno', None)\n        if error_code == 116:\n            print(f\"Connection timeout (ETIMEDOUT) for feed {feed_key}\")\n        elif error_code == 113:\n            print(f\"Connection aborted (ECONNABORTED) for feed {feed_key}\")\n        else:\n            print(f\"Network error retrieving AQI data: {e}\")\n        return None\n    except Exception as e:\n        print(f\"Error retrieving AQI data: {e}\")\n        return None\n\ndef parse_aqi_data(data):\n    \"\"\"\n    Parse and display AQI data from JSON response\n    Args:\n        data: JSON data dictionary from Adafruit IO\n    \"\"\"\n    if not data or not DEBUG:\n        return\n\n    print(\"\\n\" + \"=\" * 60)\n    print(f\"AIR QUALITY DATA - {data.get('name', 'Unknown')}\")\n    print(f\"Provider: {data.get('provider', 'Unknown').upper()}\")\n    print(\"=\" * 60)\n\n    # Parse current conditions\n    if 'current' in data:\n        current = data['current']\n        print(\"\\nCURRENT CONDITIONS:\")\n        print(f\"  AQI: {current.get('aqi', 'N/A')}\")\n        print(f\"  Category: {current.get('category_label', 'N/A')}\")\n        print(f\"  Color: {current.get('category_color', 'N/A')}\")\n        print(f\"  Timestamp: {current.get('timestamp', 'N/A')}\")\n\n        # Show location info if available (EPA/AirNow data)\n        if current.get('reporting_area'):\n            print(f\"  Location: {current.get('reporting_area', '')}, {current.get('state', '')}\")\n\n        print(f\"\\n  POLLUTANTS:\")\n        # Only show pollutants that have values\n        if current.get('pm2_5') is not None:\n            print(f\"    PM2.5: {current.get('pm2_5')} µg/m³\")\n        if current.get('pm10') is not None:\n            print(f\"    PM10:  {current.get('pm10')} µg/m³\")\n        if current.get('co') is not None:\n            print(f\"    CO:    {current.get('co')} µg/m³\")\n        if current.get('o3') is not None:\n            print(f\"    O3:    {current.get('o3')} µg/m³\")\n        if current.get('no2') is not None:\n            print(f\"    NO2:   {current.get('no2')} µg/m³\")\n        if current.get('so2') is not None:\n            print(f\"    SO2:   {current.get('so2')} µg/m³\")\n        if current.get('primary_pollutant'):\n            print(f\"    Primary: {current.get('primary_pollutant').upper()}\")\n\n        print(f\"\\n  HEALTH:\")\n        print(f\"    {current.get('health_recommendation', 'N/A')}\")\n\n    # Parse forecast today\n    if 'forecast_today' in data:\n        forecast = data['forecast_today']\n        print(f\"\\nTODAY'S FORECAST:\")\n        print(f\"  AQI: {forecast.get('aqi', 'N/A')}\")\n        print(f\"  Category: {forecast.get('category_label', 'N/A')}\")\n        if forecast.get('primary_pollutant'):\n            print(f\"  Primary Pollutant: {forecast.get('primary_pollutant').upper()}\")\n        print(f\"  Recommendation: {forecast.get('health_recommendation', 'N/A')}\")\n\n    # Parse forecast tomorrow\n    if 'forecast_tomorrow' in data:\n        forecast = data['forecast_tomorrow']\n        print(f\"\\nTOMORROW'S FORECAST:\")\n        print(f\"  AQI: {forecast.get('aqi', 'N/A')}\")\n        print(f\"  Category: {forecast.get('category_label', 'N/A')}\")\n        if forecast.get('primary_pollutant'):\n            print(f\"  Primary Pollutant: {forecast.get('primary_pollutant').upper()}\")\n        print(f\"  Recommendation: {forecast.get('health_recommendation', 'N/A')}\")\n\n    print(\"\\n\" + \"=\" * 60)\n\nsetup_display()\ndisplay = supervisor.runtime.display\n\n# Load fonts\nfont_11 = bitmap_font.load_font(\"fonts/ArialMT-11.pcf\")\nfont_12 = bitmap_font.load_font(\"fonts/ArialMT-12.pcf\")\nprint(\"Fonts loaded successfully\")\n\n# Create display group and add navy blue background\nmain_group = displayio.Group()\nnavy_blue = 0x000066  # Navy blue color\nbackground = Rect(0, 0, display.width, display.height, fill=navy_blue)\nmain_group.append(background)\n\n# Create text labels - Left column (data point names)\ntext_color = 0xFFFFFF  # White text\n\ncu_time = label.Label(font_11, text=\"Current time:\", color=0x00FFFF, anchored_position = (2, 2), anchor_point=(0.0, 0.0))\nmain_group.append(cu_time)\n\nline2 = label.Label(font_12, text=\"Current AQI:\", color=text_color, anchored_position = (100, 25), anchor_point=(1.0, 0.0))\nmain_group.append(line2)\n\nline3 = label.Label(font_12, text=\"Forecast AQI:\", color=0xFFFF00, anchored_position = (100, 40), anchor_point=(1.0, 0.5))\nmain_group.append(line3)\n\nline4 = label.Label(font_12, text=\"Tomorrow AQI:\", color=0x00FFFF, anchored_position = (100, 55), anchor_point=(1.0, 1.0))\nmain_group.append(line4)\n\nline5 = label.Label(font_12, text=\"Current Cat:\", color=text_color, anchored_position = (100, 68), anchor_point=(1.0, 0.0))\nmain_group.append(line5)\n\nline6 = label.Label(font_12, text=\"Forecast Cat:\", color=0xFFFF00, anchored_position = (100, 83), anchor_point=(1.0, 0.5))\nmain_group.append(line6)\n\nline7 = label.Label(font_12, text=\"Tomorrow Cat:\", color=0x00FFFF, anchored_position = (100, 98), anchor_point=(1.0, 1.0))\nmain_group.append(line7)\n\nline8 = label.Label(font_11, text=\"PM2.5:\", color=text_color, anchored_position = (100, 113), anchor_point=(1.0, 0.0))\nmain_group.append(line8)\n\nline9 = label.Label(font_11, text=\"O3:\", color=text_color, anchored_position = (100, 125), anchor_point=(1.0, 0.0))\nmain_group.append(line9)\n\nline10 = label.Label(font_11, text=\"Primary Pollutant:\", color=0xFFFF00, anchored_position = (100, 137), anchor_point=(1.0, 0.0))\nmain_group.append(line10)\n\nline11 = label.Label(font_11, text=\"PM10:\", color=text_color, anchored_position = (100, 149), anchor_point=(1.0, 0.0))\nmain_group.append(line11)\n\nline12 = label.Label(font_11, text=\"CO:\", color=text_color, anchored_position = (100, 161), anchor_point=(1.0, 0.0))\nmain_group.append(line12)\n\nline13 = label.Label(font_11, text=\"NO2:\", color=text_color, anchored_position = (100, 173), anchor_point=(1.0, 0.0))\nmain_group.append(line13)\n\nline14 = label.Label(font_11, text=\"SO2:\", color=text_color, anchored_position = (100, 185), anchor_point=(1.0, 0.0))\nmain_group.append(line14)\n\nline15 = label.Label(font_11, text=\"text:\", color=text_color, anchored_position = (10, 199), anchor_point=(0.0, 0.0))\nmain_group.append(line15)\n\n# Create center column\nline1_c = label.Label(font_12, text=\"Line 1_c\", color=0x44FF44, anchored_position = (120, 10), anchor_point=(0.0, 0.5))\nmain_group.append(line1_c)\n\nline2_c = label.Label(font_12, text=\"Line 2_c\", color=text_color, anchored_position = (120, 25), anchor_point=(0.0, 0.0))\nmain_group.append(line2_c)\n\nline3_c = label.Label(font_12, text=\"Line 3_c\", color=0xFFFF00, anchored_position = (120, 40), anchor_point=(0.0, 0.5))\nmain_group.append(line3_c)\n\nline4_c = label.Label(font_12, text=\"Line 4_c\", color=0x00FFFF, anchored_position = (120, 55), anchor_point=(0.0, 1.0))\nmain_group.append(line4_c)\n\n\nline5_c = label.Label(font_12, text=\"Line 5_c\", color=text_color, anchored_position = (120, 68), anchor_point=(0.0, 0.0))\nmain_group.append(line5_c)\n\nline6_c = label.Label(font_12, text=\"Line 6_c\", color=0xFFFF00, anchored_position = (120, 83), anchor_point=(0.0, 0.5))\nmain_group.append(line6_c)\n\nline7_c = label.Label(font_12, text=\"Line 7_c\", color=0x00FFFF, anchored_position = (120, 98), anchor_point=(0.0, 1.0))\nmain_group.append(line7_c)\n\nline8_c = label.Label(font_11, text=\"Line 8_c\", color=text_color, anchored_position = (120, 113), anchor_point=(0.0, 0.0))\nmain_group.append(line8_c)\n\nline9_c = label.Label(font_11, text=\"Line 9_c\", color=text_color, anchored_position = (120, 125), anchor_point=(0.0, 0.0))\nmain_group.append(line9_c)\n\nline10_c = label.Label(font_11, text=\"Line 10_c\", color=0xFFFF00, anchored_position = (120, 137), anchor_point=(0.0, 0.0))\nmain_group.append(line10_c)\n\nline11_c = label.Label(font_11, text=\"Line 11_c\", color=text_color, anchored_position = (120, 149), anchor_point=(0.0, 0.0))\nmain_group.append(line11_c)\n\nline12_c = label.Label(font_11, text=\"Line 12_c\", color=text_color, anchored_position = (120, 161), anchor_point=(0.0, 0.0))\nmain_group.append(line12_c)\n\nline13_c = label.Label(font_11, text=\"Line 13_c\", color=text_color, anchored_position = (120, 173), anchor_point=(0.0, 0.0))\nmain_group.append(line13_c)\n\nline14_c = label.Label(font_11, text=\"Line 14_c\", color=text_color, anchored_position = (120, 185), anchor_point=(0.0, 0.0))\nmain_group.append(line14_c)\n\n\nline1_r = label.Label(font_12, text=\"Line 1_r\", color=0x44FF44, anchored_position = (220, 10), anchor_point=(0.0, 0.5))\nmain_group.append(line1_r)\n\nline2_r = label.Label(font_12, text=\"Line 2_r\", color=text_color, anchored_position = (220, 25), anchor_point=(0.0, 0.0))\nmain_group.append(line2_r)\n\nline3_r = label.Label(font_12, text=\"Line 3_r\", color=0xFFFF00, anchored_position = (220, 40), anchor_point=(0.0, 0.5))\nmain_group.append(line3_r)\n\nline4_r = label.Label(font_12, text=\"Line 4_r\", color=0x00FFFF, anchored_position = (220, 55), anchor_point=(0.0, 1.0))\nmain_group.append(line4_r)\n\nline5_r = label.Label(font_12, text=\"Line 5_r\", color=text_color, anchored_position = (220, 68), anchor_point=(0.0, 0.0))\nmain_group.append(line5_r)\n\nline6_r = label.Label(font_12, text=\"Line 6_r\", color=0xFFFF00, anchored_position = (220, 83), anchor_point=(0.0, 0.5))\nmain_group.append(line6_r)\n\nline7_r = label.Label(font_12, text=\"Line 7_r\", color=0x00FFFF, anchored_position = (220, 98), anchor_point=(0.0, 1.0))\nmain_group.append(line7_r)\n\nline8_r = label.Label(font_11, text=\"Line 8_r\", color=text_color, anchored_position = (220, 113), anchor_point=(0.0, 0.0))\nmain_group.append(line8_r)\n\nline9_r = label.Label(font_11, text=\"Line 9_r\", color=text_color, anchored_position = (220, 125), anchor_point=(0.0, 0.0))\nmain_group.append(line9_r)\n\nline10_r = label.Label(font_11, text=\"Line 10_r\", color=0xFFFF00, anchored_position = (220, 137), anchor_point=(0.0, 0.0))\nmain_group.append(line10_r)\n\nline11_r = label.Label(font_11, text=\"Line 11_r\", color=text_color, anchored_position = (220, 149), anchor_point=(0.0, 0.0))\nmain_group.append(line11_r)\n\nline12_r = label.Label(font_11, text=\"Line 12_r\", color=text_color, anchored_position = (220, 161), anchor_point=(0.0, 0.0))\nmain_group.append(line12_r)\n\nline13_r = label.Label(font_11, text=\"Line 13_r\", color=text_color, anchored_position = (220, 173), anchor_point=(0.0, 0.0))\nmain_group.append(line13_r)\n\nline14_r = label.Label(font_11, text=\"Line 14_r\", color=text_color, anchored_position = (220, 185), anchor_point=(0.0, 0.0))\nmain_group.append(line14_r)\n\n# Add horizontal separator lines\nseparator_lineb = Line(0, 62, display.width, 62, color=0x0000FF)\nmain_group.append(separator_lineb)\n\nseparator_linea = Line(0, 20, display.width, 20, color=0x0000FF)\nmain_group.append(separator_linea)\n\nseparator_linec = Line(0, 107, display.width, 107, color=0x0000FF)\nmain_group.append(separator_linec)\n\nseparator_lined = Line(0, 196, display.width, 196, color=0x0000FF)\nmain_group.append(separator_lined)\n\n# Add colored rectangles for category indicators\no_color = RoundRect(160, 27, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(o_color)\n\ne_color = RoundRect(260, 27, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(e_color)\n\no_color_f = RoundRect(160, 37, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(o_color_f)\n\ne_color_f = RoundRect(260, 37, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(e_color_f)\n\no_color_t = RoundRect(160, 47, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(o_color_t)\n\ne_color_t = RoundRect(260, 47, 29, 9, r=3, fill=0xFF0000)\nmain_group.append(e_color_t)\n\ndisplay.root_group = main_group\n\n# Get WiFi credentials from settings.toml\nssid = os.getenv(\"CIRCUITPY_WIFI_SSID\")\npassword = os.getenv(\"CIRCUITPY_WIFI_PASSWORD\")\n\naqio = os.getenv(\"AQIO\")\naqie = os.getenv(\"AQIE\")\n\n# Cache settings\naio_username = os.getenv(\"AIO_USERNAME\")\naio_key = os.getenv(\"AIO_KEY\")\ntimezone = os.getenv(\"TIMEZONE\")\n\nprint(f\"Connecting to WiFi: {ssid}\")\n\n# Connect to WiFi with retry logic\nwifi_connected = False\nmax_retries = 3\nfor attempt in range(max_retries):\n    try:\n        wifi.radio.connect(ssid, password)\n        print(f\"Connected to {ssid}\")\n        print(f\"IP Address: {wifi.radio.ipv4_address}\")\n        wifi_connected = True\n        break\n    except Exception as e:\n        print(f\"WiFi connection attempt {attempt + 1}/{max_retries} failed: {e}\")\n        if attempt \u003c max_retries - 1:\n            time.sleep(5)\n        else:\n            print(\"Failed to connect to WiFi after multiple attempts\")\n            time.sleep(60)\n            supervisor.reload()\n\nif wifi_connected:\n    # Create socket pool for network requests\n    pool = socketpool.SocketPool(wifi.radio)\n    print(\"WiFi connection successful!\")\n\n    # Initialize Adafruit IO with SSL context and timeout\n    ssl_context = ssl.create_default_context()\n    requests = adafruit_requests.Session(pool, ssl_context)\n    \n    io = IO_HTTP(aio_username, aio_key, requests)\n    print(f\"Connected to Adafruit IO as: {aio_username}\")\n\n    # Initialize RTC and set time from Adafruit IO\n    print(f\"\\nSetting RTC time using timezone: {timezone}\")\n    try:\n        aio_time = io.receive_time()\n        if DEBUG:\n            print(f\"Received time from Adafruit IO: {aio_time}\")\n        \n        rtc_instance = rtc.RTC()\n        rtc_instance.datetime = aio_time\n        \n        current_time = time.localtime()\n        print(f\"RTC set to: {current_time.tm_year}-{current_time.tm_mon:02d}-{current_time.tm_mday:02d} \"\n              f\"{current_time.tm_hour:02d}:{current_time.tm_min:02d}:{current_time.tm_sec:02d}\")\n    except Exception as e:\n        print(f\"Error setting RTC: {e}\")\n\n# Retrieve data from both feeds with retry logic\ndata_aqio = None\ndata_aqie = None\n\ndef check_wifi_connection():\n    \"\"\"Check if WiFi is still connected, attempt reconnect if not\"\"\"\n    if not wifi.radio.connected:\n        print(\"WiFi disconnected! Attempting to reconnect...\")\n        try:\n            wifi.radio.connect(ssid, password)\n            print(f\"Reconnected to {ssid}\")\n            return True\n        except Exception as e:\n            print(f\"WiFi reconnection failed: {e}\")\n            return False\n    return True\n\ndef fetch_aqi_with_retry(feed_key, max_attempts=3):\n    \"\"\"Fetch AQI data with exponential backoff retry logic\"\"\"\n    base_delay = 5  # Start with 5 second delay\n    \n    for attempt in range(max_attempts):\n        # Check WiFi connection before attempting request\n        if not check_wifi_connection():\n            print(\"WiFi not connected, skipping attempt\")\n            if attempt \u003c max_attempts - 1:\n                time.sleep(base_delay * (2 ** attempt))\n            continue\n        \n        data = get_aqi_data(feed_key)\n        if data:\n            print(f\"Successfully retrieved data from feed {feed_key}\")\n            return data\n        \n        if attempt \u003c max_attempts - 1:\n            # Exponential backoff: 5s, 10s, 20s...\n            delay = base_delay * (2 ** attempt)\n            print(f\"Retrying feed {feed_key} in {delay} seconds (attempt {attempt + 1}/{max_attempts})...\")\n            time.sleep(delay)\n        else:\n            print(f\"Failed to retrieve data from feed {feed_key} after {max_attempts} attempts\")\n    \n    return None\n\nif wifi_connected:\n    if aqio:\n        data_aqio = fetch_aqi_with_retry(aqio)\n    if aqie:\n        data_aqie = fetch_aqi_with_retry(aqie)\n    \n    # Update the display with the retrieved data\n    update_display_with_data(data_aqio, data_aqie)\n    gc.collect()\n    if DEBUG:\n        print(\"gc.mem_free:\", gc.mem_free())\nprint(gc.mem_free())\nprint(\"-\" * 20)\n\n# Main loop - non-blocking update every hour\nif wifi_connected:\n    UPDATE_INTERVAL = 3600  # Update every hour (3600 seconds)\n    TIME_UPDATE_INTERVAL = 30  # Update current time display every 30 seconds\n    HEALTH_TEXT_INTERVAL = 10  # Rotate health text every 10 seconds\n    last_aqi_update = time.monotonic()\n    last_clock_update = time.monotonic()\n    last_health_update = time.monotonic()\n    health_text_index = 0\n    \n    print(\"\\nEntering main loop...\")\n    \n    while True:\n        now = time.monotonic()\n        \n        # Update current time display every 30 seconds\n        if now - last_clock_update \u003e= TIME_UPDATE_INTERVAL:\n            rtc_time = time.localtime()\n            # Calculate minutes until next data update\n            elapsed = now - last_aqi_update\n            remaining_seconds = UPDATE_INTERVAL - elapsed\n            remaining_minutes = int(remaining_seconds / 60)\n            cu_time.text = f\"{rtc_time.tm_hour:02d}:{rtc_time.tm_min:02d} ({remaining_minutes})\"\n            last_clock_update = now\n        \n        # Rotate health text every 10 seconds\n        if now - last_health_update \u003e= HEALTH_TEXT_INTERVAL and health_texts:\n            # Get current health text, color, and order\n            text, color, order = health_texts[health_text_index]\n            wrapped_text = \"\\n\".join(wrap_text_to_pixels(text, 310, font_11))\n            line15.text = wrapped_text\n            line15.color = color\n            \n            # Move to next text\n            health_text_index = (health_text_index + 1) % len(health_texts)\n            last_health_update = now\n        \n        # Check if it's time to update AQI data (1 hour has passed)\n        if now - last_aqi_update \u003e= UPDATE_INTERVAL:\n            print(\"\\nUpdating AQI data...\")\n            \n            # Verify WiFi is still connected before attempting update\n            if not check_wifi_connection():\n                print(\"WiFi connection lost, skipping this update cycle\")\n                last_aqi_update = now  # Reset timer to try again in next cycle\n            else:\n                # Retrieve updated data with retry\n                if aqio:\n                    data_aqio = fetch_aqi_with_retry(aqio)\n                if aqie:\n                    data_aqie = fetch_aqi_with_retry(aqie)\n                \n                # Update the display\n                update_display_with_data(data_aqio, data_aqie)\n                health_text_index = 0  # Reset to first text after update\n                gc.collect()\n                if DEBUG:\n                    print(\"gc.mem_free:\", gc.mem_free())\n                \n                # Reset the timer\n                last_aqi_update = now\n        \n        # Short sleep to prevent busy-waiting\n        time.sleep(1)\nelse:\n    print(\"WiFi not connected. Reloading system...\")\n    time.sleep(60)\n    supervisor.reload()","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eIn Closing\u003c/h1\u003e\n\u003cp\u003e\u003cspan\u003eEditing helper libraries isn’t something you should do casually — but it’s a powerful skill when you need it. I’ve used this approach before, such as disabling button initialization in a helper library so I could manage them with the \u003ccode\u003ekeypad\u003c/code\u003e module instead.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eThe key is understanding that libraries are just Python. With care and a clear goal, you can extend them to support new features long before official updates arrive.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eAnd in this case, it unlocked a great little AQI dashboard powered by the brand‑new Adafruit IO Air Quality Power‑Up. \u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n","metadata":{}}]