[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/604/original/IMG_2628_4.jpeg?1739207028","metadata":{}},{"element_type":"text","content":"\n        \u003ch1\u003eOverview\u003c/h1\u003e\n\u003cp\u003eThis desktop local weather monitor is the latest in a series of updates to John Park’s original \u003ca href=\"https://learn.adafruit.com/pyportal-weather-station\" target=\"_blank\"\u003e\u003cspan\u003eopenweathermap-based PyPortal weather station project\u003c/span\u003e\u003c/a\u003e. It’s a free weather information service that doesn’t require a credit card paywall account. Thanks to some investigative work by \u003ca href=\"https://adafruit-playground.com/u/DJDevon3/pages\" target=\"_blank\"\u003e\u003cspan\u003e@DJDevon3\u003c/span\u003e\u003c/a\u003e, this latest version uses the free non-commercial \u003ca href=\"https://open-meteo.com/\" target=\"_blank\"\u003e\u003cspan\u003eOpen-Meteo\u003c/span\u003e\u003c/a\u003e (OM) URL API service. The best part? You don’t need an account or key to access the API if you’re using it for non-commercial projects.\u003c/p\u003e\n      ","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  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe OM Weather Display periodically updates and displays the following local weather conditions:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDay, date, and time (AM/PM)\u003c/li\u003e\n\u003cli\u003eTomorrow's sunrise and sunset times\u003c/li\u003e\n\u003cli\u003eTemperature\u003c/li\u003e\n\u003cli\u003eRelative Humidity\u003c/li\u003e\n\u003cli\u003eWind speed and direction\u003c/li\u003e\n\u003cli\u003eWind gust speed\u003c/li\u003e\n\u003cli\u003eCondition description and graphic icon\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLocation, time zone, and measurement units settings are stored in the \u003ccode\u003esettings.toml\u003c/code\u003e file. Either \"METRIC\" or \"IMPERIAL\" measurement units can be specified.\u003c/p\u003e\n\u003cp\u003eWeather condition query and internet clock refresh interval rates are set by parameters in the \u003ccode\u003ecode.py\u003c/code\u003e module. The default interval for updating weather conditions is 5 minutes. The local time is updated from the Adafruit Network Time Protocol (NTP) server hourly.\u003c/p\u003e\n\u003cp\u003eThe CircuitPython code runs on an\u0026nbsp;\u003ca href=\"https://www.adafruit.com/product/5477\" target=\"_blank\"\u003eESP32-S3 4MB/2MB Feather\u003c/a\u003e attached to a \u003ca href=\"https://www.adafruit.com/product/3315\" target=\"_blank\"\u003e2.4-inch TFT FeatherWing\u003c/a\u003e. An optional \u003ca href=\"https://www.adafruit.com/product/2748\" target=\"_blank\"\u003eALS-PT19 ambient light sensor\u003c/a\u003e can be used to automatically adjust display brightness.\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/606/original/IMG_2629.jpeg?1739207196","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  \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  \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  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eCircuitPython Code\u003c/h1\u003e\n\u003cp\u003eThe Weather Display's CIRCUITPY root directory contains the following files and folders.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003efiles:\n\u003cul\u003e\n\u003cli\u003esettings.toml -- Wi-Fi and location parameters\u003c/li\u003e\n\u003cli\u003eom_query.py -- OM API URL query string builder\u003c/li\u003e\n\u003cli\u003ewho_to_map_icon.py -- parses the WMO weather code for descriptions and icons\u003c/li\u003e\n\u003cli\u003ecode.py -- the primary code module\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003efolders:\n\u003cul\u003e\n\u003cli\u003efonts -- contains the font files\u003c/li\u003e\n\u003cli\u003eicons_160x160 -- the weather icon bitmap graphics files\u003c/li\u003e\n\u003cli\u003elib -- the CircuitPython library modules\u003c/li\u003e\n\u003cli\u003esd -- a placeholder for the unused SD storage drive\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA downloadable bundle of code files and folders can be found in the\u0026nbsp;\u003ca href=\"https://github.com/CedarGroveStudios/OM_Weather_Display_2.4-inch\" target=\"_blank\"\u003eOM Weather Display GitHub repository\u003c/a\u003e.\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\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\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\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/607/original/OM_WeatherDisplay_root-directory.png?1739207485","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  \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  \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  \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  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eWi-Fi Connect and Local Information (settings.toml)\u003c/h2\u003e\n\u003cp\u003eBesides the WiFi AP SSID and password, the\u0026nbsp;\u003ccode\u003esettings.toml\u003c/code\u003e file contains the location, latitude, longitude, time zone, time zone offset from GMT (hours), and measurement units parameter settings needed to build the OM query.\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\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\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\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\n","metadata":{}},{"element_type":"code","content":"CIRCUITPY_WIFI_SSID = \"wi-fi_ssid\"\nCIRCUITPY_WIFI_PASSWORD = \"averysecurepassword\"\n\nLOCATION = \"Seattle, WA, US\"\nLATITUDE = \"47.6061\"\nLONGITUDE = \"-122.3328\"\nTIMEZONE = \"US/Pacific\"\nTIMEZONE_OFFSET = -8\nMEASUREMENT_UNITS = \"IMPERIAL\"","metadata":{"language":"auto","linenums":false}},{"element_type":"alert","content":"\u003cp\u003eLongitude degrees West of Greenwich are entered as negative values (0 to -180 degrees). Positive values are used for East of Greenwich (0 to 180 degrees).\u003c/p\u003e","metadata":{"class":"element alert-element build-alert alert-info","markdown":"Longitude degrees West of Greenwich are entered as negative values (0 to -180 degrees). Positive values are used for East of Greenwich (0 to 180 degrees).","alert_type":"info"}},{"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  \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  \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  \n  \n  \n  \n        \u003ch2\u003eBuild the Open-Meteo Web API Query (om_query.py)\u003c/h2\u003e\n\u003cp\u003eThis module builds the Open-Meteo (OM) API URL query string from a list of key words and variables. After retrieving local settings from \u003ccode\u003esettings.toml\u003c/code\u003e, the \u003ccode\u003eDATA_SOURCE\u003c/code\u003e string is built by concatenating API keywords to form a URL query. The \u003ccode\u003eDATA_SOURCE\u003c/code\u003e variable is then used by \u003ccode\u003ecode.py\u003c/code\u003e to retrieve the resultant data payload from OM.\u003c/p\u003e\n\u003cp\u003eSome of the OM keywords are not used by this version of the Weather Display. The unused keywords are commented-out but left in the \u003ccode\u003eoil_query.py\u003c/code\u003e file in case they are needed in a future version.\u003c/p\u003e\n\u003cp\u003eThanks to \u003ca href=\"https://adafruit-playground.com/u/DJDevon3/pages\" target=\"_blank\"\u003e@DJDevon3\u003c/a\u003e\u0026nbsp;for the initial URL query builder code that became the foundation for this version.\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\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\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\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios\n# SPDX-License-Identifier: MIT\n\"\"\"\nom_query.py\nQuery builder for Open_Meteo weather conditions Web API.\nLocation information is extracted from the settings.toml file.\nInspired by DJDevon3's Open-Meteo query builder.\n\"\"\"\nimport os\n\nLATITUDE = os.getenv(\"LATITUDE\")\nLONGITUDE = os.getenv(\"LONGITUDE\")\nTIMEZONE = os.getenv(\"TIMEZONE\")\nTIMEZONE_OFFSET = os.getenv(\"TIMEZONE_OFFSET\")\nMEASUREMENT_UNITS = os.getenv(\"MEASUREMENT_UNITS\")\n\nif MEASUREMENT_UNITS == \"METRIC\":\n    UNITS = [\"mm\", \"celsius\", \"kmh\"]\nelse:\n    UNITS = [\"inch\", \"fahrenheit\", \"mph\"]\n\n# Open-Meteo Free API\nDATA_SOURCE = \"http://api.open-meteo.com/v1/forecast?\"\nDATA_SOURCE += \"latitude=\" + LATITUDE\nDATA_SOURCE += \"\u0026longitude=\" + LONGITUDE\nDATA_SOURCE += \"¤t=temperature_2m,\"\nDATA_SOURCE += \"relative_humidity_2m,\"\n# DATA_SOURCE += \"apparent_temperature,\"\n# DATA_SOURCE += \"dew_point_2m,\"\nDATA_SOURCE += \"is_day,\"\n# DATA_SOURCE += \"precipitation,\"\n# DATA_SOURCE += \"rain,\"\n# DATA_SOURCE += \"showers,\"\n# DATA_SOURCE += \"snowfall,\"\nDATA_SOURCE += \"weather_code,\"\n# DATA_SOURCE += \"cloud_cover,\"\n# DATA_SOURCE += \"pressure_msl,\"\n# DATA_SOURCE += \"surface_pressure,\"\nDATA_SOURCE += \"wind_speed_10m,\"\nDATA_SOURCE += \"wind_direction_10m,\"\nDATA_SOURCE += \"wind_gusts_10m\"\nDATA_SOURCE += \"\u0026daily=sunrise,sunset\"\nDATA_SOURCE += \"\u0026temperature_unit=\" + UNITS[1]\nDATA_SOURCE += \"\u0026wind_speed_unit=\" + UNITS[2]\n# DATA_SOURCE += \"\u0026precipitation_unit=\" + UNITS[0]\nDATA_SOURCE += \"\u0026timeformat=unixtime\"\nDATA_SOURCE += \"\u0026timezone=\" + TIMEZONE","metadata":{"language":"python","linenums":false}},{"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  \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  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eTranslate WMO Condition Codes (wmo_to_map_icon.py)\u003c/h2\u003e\n\u003cp\u003eWhen the Open-Meteo data payload is received, it contains a \u003ca href=\"https://wmo.int\" target=\"_blank\"\u003eWorld Meteorological Organization (WMO)\u003c/a\u003e Weather Interpretation Code that is used to report weather conditions. This module consists of a dictionary that provides a short and long condition description as well as a two-digit bitmap file prefix used to display the associated weather icon.\u003c/p\u003e\n\u003cp\u003eWeather icon bitmaps are contained in the \u003ccode\u003eicon_160x160\u003c/code\u003e folder.\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\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\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios\n# SPDX-License-Identifier: MIT\n\"\"\"\nwmo_to_map_icon.py\nDecoder for WMO Weather interpretation codes (WW) commonly used by OpenMateo.\nProvides a short description, long description, and the openweathermap.org\n(\"map\") icon designation.\n\"\"\"\n\n# WMO code: [description, long_description, map_icon_designation]\nwmo_to_map_icon = {\n    \"0\": [\"Clear\", \"Clear sky.\", \"01\"],\n    \"1\": [\"MainlyClear\", \"Mainly clear sky.\", \"02\"],\n    \"2\": [\"PartlyCloudy\", \"Partly cloudy sky.\", \"02\"],\n    \"3\": [\"Overcast\", \"Overcast sky.\", \"03\"],\n    \"45\": [\"Fog\", \"Foggy.\", \"50\"],\n    \"48\": [\"Fog\", \"Depositing rime fog.\", \"50\"],\n    \"51\": [\"Drizzle\", \"Light drizzle.\", \"10\"],\n    \"53\": [\"Drizzle\", \"Moderate drizzle.\", \"09\"],\n    \"55\": [\"Drizzle\", \"Dense drizzle.\", \"09\"],\n    \"56\": [\"FreezingDrizzle\", \"Light freezing drizzle.\", \"09\"],\n    \"57\": [\"FreezingDrizzle\", \"Dense freezing drizzle.\", \"09\"],\n    \"61\": [\"Rain\", \"Slight rain.\", \"10\"],\n    \"63\": [\"Rain\", \"Moderate rain.\", \"09\"],\n    \"65\": [\"Rain\", \"Heavy rain.\", \"09\"],\n    \"66\": [\"FreezingRain\", \"Light freezing rain.\", \"09\"],\n    \"67\": [\"FreezingRain\", \"Heavy freezing rain.\", \"09\"],\n    \"71\": [\"Snow\", \"Slight snow fall.\", \"13\"],\n    \"73\": [\"Snow\", \"Moderate snow fall.\", \"13\"],\n    \"75\": [\"Snow\", \"Heavy snow fall.\", \"13\"],\n    \"77\": [\"Snow\", \"Snow grains falling.\", \"13\"],\n    \"80\": [\"Rain\", \"Slight rain showers.\", \"10\"],\n    \"81\": [\"Rain\", \"Moderate rain showers.\", \"09\"],\n    \"82\": [\"Rain\", \"Violent rain showers.\", \"09\"],\n    \"85\": [\"Snow\", \"Slight snow showers.\", \"13\"],\n    \"86\": [\"Snow\", \"Heavy snow showers.\", \"13\"],\n    \"95\": [\"Thunderstorm\", \"Thunderstorm.\", \"07\"],\n    \"96\": [\"Thunderstorm\", \"Thunderstorm with slight hail.\", \"07\"],\n    \"99\": [\"Thunderstorm\", \"Thunderstorm with heavy hail.\", \"07\"],\n}","metadata":{"language":"python","linenums":false}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/608/original/combined_weather_icons.png?1739207524","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  \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  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eThe Primary Code Module (code.py)\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003ecode.py\u003c/code\u003e module queries Open-Meteo and fills the display with weather condition values and a graphic icon. The module is also used to control the screen update rate, refreshes the local time, and optionally controls display screen brightness.\u003c/p\u003e\n\u003cp\u003eThe OM query rate in seconds is set by the \u003ccode\u003eSAMPLE_INTERVAL\u003c/code\u003e parameter. Since OM updates weather conditions a few times each hour, setting this parameter to 10 or 20 minutes should suffice. If you need a shorter interval rate, consider signing up for one of OM's paid API plans.\u003c/p\u003e\n\u003cp\u003eThe local clock is updated from the Adafruit NTP server each hour. The clock refresh rate is set by the \u003ccode\u003eNTP_INTERVAL\u003c/code\u003e parameter.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eBRIGHTNESS\u003c/code\u003e parameter is used to control the TFT display backlight and NeoPixel intensity. It can be set with a value between 0.0 (completely dark) and 1.0 (brightest setting). When the\u0026nbsp;\u003ccode\u003eLIGHT_SENSOR\u003c/code\u003e is enabled, \u003ccode\u003eBRIGHTNESS\u003c/code\u003e represents the maximum brightness value.\u003c/p\u003e\n\u003cp\u003eIf automatic display brightness is enabled (\u003ccode\u003eLIGHT_SENSOR = True\u003c/code\u003e), the code will periodically read the analog value of pin A3 to calculate a target brightness for the display and NeoPixel. Of course that means that a light sensor should be connected. See the Optional Display Brightness Control section for how to attach the sensor.\u0026nbsp;\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\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\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios\n# SPDX-License-Identifier: MIT\n\"\"\"\nom_weather_display_code.py\nReceives Open-Meteo local weather conditions.\nDesigned for the Adafruit ESP32-S3 4MB/2MB Feather (#5477) and\n2.4\" TFT FeatherWing (#3315).\n\"\"\"\n\nimport time\nimport board\nimport os\nimport gc\nimport rtc\nimport displayio\nimport fourwire\nimport wifi\nimport ssl\nfrom digitalio import DigitalInOut, Direction\nimport pwmio\nimport analogio\nimport supervisor\nfrom simpleio import map_range\nimport neopixel\nimport adafruit_ntp\nimport adafruit_connection_manager\nimport adafruit_requests\nimport adafruit_ili9341\nfrom adafruit_display_text.label import Label\nfrom adafruit_bitmap_font import bitmap_font\nfrom adafruit_display_shapes.roundrect import RoundRect\n\nfrom wmo_to_map_icon import wmo_to_map_icon\nfrom om_query import DATA_SOURCE\n\n# Weather Display Parameters\nSAMPLE_INTERVAL = 600  # Check conditions (sec): typically 600 to 1200\nNTP_INTERVAL = 3600  # Update local time from NTP server (sec): typically 3600\nBRIGHTNESS = 0.75  # TFT and NeoPixel brightness setting\nLIGHT_SENSOR = False  # True when ALS-PT19 sensor is connected to board.A3\n\n# fmt: off\n# Month and weekday lookup tables\nWEEKDAY = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\nMONTH = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n# fmt: on\n\n# Default colors\nBLACK = 0x000000\nRED = 0xFF0000\nGREEN = 0x00FF00\nORANGE = 0xFF8811\nYELLOW = 0xFFFF00\nVIOLET = 0x9900FF\nPURPLE = 0xFF00FF\nWHITE = 0xFFFFFF\nDK_BLUE = 0x000080\n\n# Define a few state/mode pixel colors\nSTARTUP = PURPLE\nNORMAL = DK_BLUE\nFETCH = YELLOW\nERROR = RED\n\n# Set start-up values\nold_brightness = BRIGHTNESS\nclock_tick = False\n\n# Instantiate the display and darken during startup process\n#   Adafruit 2.4\" TFT FeatherWing with LITE connected to TX\nlite = pwmio.PWMOut(board.TX, frequency=500)\nlite.duty_cycle = 0  # Reduce display brightness during startup\ndisplayio.release_displays()  # Release display resources\ndisplay_bus = fourwire.FourWire(\n    board.SPI(), command=board.D10, chip_select=board.D9, reset=None\n)\ndisplay = adafruit_ili9341.ILI9341(display_bus, width=320, height=240)\ndisplay.rotation = 0\n\n# Instantiate the red LED\nled = DigitalInOut(board.LED)\nled.direction = Direction.OUTPUT\nled.value = False\n\n# Instantiate the NeoPixel\npixel = neopixel.NeoPixel(board.NEOPIXEL, 1)\npixel[0] = STARTUP\npixel.brightness = BRIGHTNESS\n\n# Instantiate the ALS-PT19 light sensor\nlight_sensor = analogio.AnalogIn(board.A3)\n\n# ### Helper Methods ###\n\n\ndef adjust_brightness():\n    \"\"\"Acquire the ALS-PT19 light sensor value and gradually adjust display\n    brightness based on ambient light. The display brightness ranges from 0.05\n    to BRIGHTNESS when the ambient light level falls between 5 to 200 lux.\n    Full-scale raw light sensor value (65535) is approximately 1500 Lux.\"\"\"\n    global old_brightness\n    if not LIGHT_SENSOR:\n        return\n    raw = 0\n    for i in range(2000):\n        raw = raw + light_sensor.value\n    target_brightness = round(\n        map_range(raw / 2000 / 65535 * 1500, 5, 200, 0.05, BRIGHTNESS), 3\n    )\n    new_brightness = round(\n        old_brightness + ((target_brightness - old_brightness) / 5), 3\n    )\n    disp_brightness(new_brightness)\n    pixel.brightness = new_brightness\n    old_brightness = new_brightness\n\n\ndef alert(text=\"\"):\n    \"\"\"Flash text in the message area and print to REPL.\n    :param str text: Message text limited to 20 characters. Default is\n    an empty string (no message).\"\"\"\n    if not text or text == \"\":\n        return\n    text = text[:20]\n    print(\"ALERT: \" + text)\n    display_message.color = RED\n    display_message.text = text\n    time.sleep(0.1)\n    display_message.color = YELLOW\n    time.sleep(0.1)\n    display_message.color = RED\n    time.sleep(0.1)\n    display_message.color = YELLOW\n    time.sleep(0.5)\n    display_message.color = None\n\n\ndef am_pm(hour):\n    \"\"\"Provide an adjusted hour and AM/PM string to create to a\n    12-hour time string.\n    :param int hour: The clock hour. No default.\"\"\"\n    if hour \u003c 12:\n        if hour == 0:\n            hour = 12\n        return hour, \"AM\"\n    if hour == 12:\n        return 12, \"PM\"\n    if hour \u003e 12:\n        hour = hour - 12\n    return hour, \"PM\"\n\n\ndef disp_brightness(new_brightness):\n    \"\"\"Set the TFT display brightness.\n    :param float new_brightness: The display brightness 0.0 to 1.0. No default.\"\"\"\n    new_brightness = min(max(new_brightness, 0), 1.0)\n    lite.duty_cycle = int(new_brightness * 0xFFFF)\n\n\ndef display_local_time(repl=True):\n    \"\"\"Show the local time on-screen and in the REPL.\n    :param bool repl: Print time string in the REPL. Default is True (print\n    in the REPL).\"\"\"\n    hour, suffix = am_pm(time.localtime().tm_hour)\n    display_time = f\"{hour:2d}:{time.localtime().tm_min:02d} {suffix}\"\n    clock_digits.text = display_time\n    if repl:\n        print(f\"  Local Time: {display_time}\")\n\n\ndef get_local_time():\n    \"\"\"Update the local time from the NTP server.\"\"\"\n    pixel[0] = FETCH\n    alert(\"UPDATE TIME\")\n    try:\n        ntp = adafruit_ntp.NTP(pool, tz_offset=os.getenv(\"TIMEZONE_OFFSET\"))\n        rtc.RTC().datetime = ntp.datetime\n    except Exception as time_err:\n        print(f\"  ERROR: Fetch local time: {time_err}\")\n    alert(\"  READY\")\n    pixel[0] = NORMAL\n\n\ndef toggle_clock_tick():\n    \"\"\"Toggle the clock tick indicator and red LED.\"\"\"\n    global clock_tick\n    if clock_tick:\n        clock_tick_mask.fill = RED\n        led.value = True\n    else:\n        clock_tick_mask.fill = None\n        led.value = False\n    clock_tick = not clock_tick\n\n\ndef update_display():\n    \"\"\"Fetch latest weather condition values from Open-Mateo and\n    update the display.\"\"\"\n    pixel[0] = FETCH\n    alert(\"UPDATE CONDITIONS\")\n\n    # Update day and date\n    wday = time.localtime().tm_wday\n    month = time.localtime().tm_mon\n    day = time.localtime().tm_mday\n    year = time.localtime().tm_year\n    clock_day_mon_yr.text = f\"{WEEKDAY[wday]}  {MONTH[month - 1]} {day:02d}, {year:04d}\"\n\n    # Get weather conditions from Open-Meteo free API\n    gc.collect()  # Prepare to use memory for query result\n    # print(f\"  mem_free() before fetch: {gc.mem_free() / 1000:.3f}kB\")\n\n    try:\n        with requests.get(DATA_SOURCE) as payload:\n            payload = requests.get(DATA_SOURCE)\n            om_json = payload.json()\n            gc.collect()  # Cleans up 10kB of json payload rubbish\n            pixel[0] = NORMAL\n    except Exception as data_source_err:\n        pixel[0] = ERROR\n        print(f\"ERROR: Fetch data from data source: {data_source_err}\")\n        print(\"  MCU will soft reset in 30 seconds.\")\n        time.sleep(30)\n        supervisor.reload()  # Soft reset: keeps the terminal session alive\n\n    # print(f\"  mem_free()  after fetch: {gc.mem_free() / 1000:.3f}kB\")\n\n    # Calculate and update sunrise/sunset\n    sset = time.localtime(om_json[\"daily\"][\"sunset\"][0] + om_json[\"utc_offset_seconds\"])\n    srise = time.localtime(\n        om_json[\"daily\"][\"sunrise\"][0] + om_json[\"utc_offset_seconds\"]\n    )\n    sunrise.text = f\"rise {am_pm(srise.tm_hour)[0]:2d}:{srise.tm_min:02d} {am_pm(srise.tm_hour)[1]}\"\n    sunset.text = (\n        f\"set {am_pm(sset.tm_hour)[0]:2d}:{sset.tm_min:02d} {am_pm(sset.tm_hour)[1]}\"\n    )\n\n    wind_dir = f\"{wind_direction(om_json['current']['wind_direction_10m'])}\"\n    if om_json[\"current_units\"][\"wind_speed_10m\"] == \"mp/h\":\n        wind_units = \"MPH\"\n    else:\n        wind_units = \"km/H\"\n    windspeed.text = (\n        f\"{wind_dir} {om_json['current']['wind_speed_10m']:.0f} {wind_units}\"\n    )\n    windgust.text = f\"gusts {om_json['current']['wind_gusts_10m']:.0f} {wind_units}\"\n\n    # Update weather description\n    description.text = wmo_to_map_icon[f\"{om_json['current']['weather_code']}\"][0]\n    long_desc.text = wmo_to_map_icon[f\"{om_json['current']['weather_code']}\"][1]\n\n    # Create icon filename\n    if om_json[\"current\"][\"is_day\"]:\n        icon_suffix = \"d\"  # Day\n    else:\n        icon_suffix = \"n\"  # Night\n    icon = wmo_to_map_icon[f\"{om_json['current']['weather_code']}\"][2]\n    icon_file = f\"/icons_160x160/{icon}{icon_suffix}.bmp\"\n    # print(f\"Icon filename: {icon_file}\")\n\n    # Update icon graphic\n    image_group.pop(0)\n    icon_image = displayio.OnDiskBitmap(icon_file)\n    icon_bg = displayio.TileGrid(\n        icon_image,\n        pixel_shader=icon_image.pixel_shader,\n        x=(WIDTH // 2) - 80,\n        y=(HEIGHT // 2) - 80,\n    )\n    image_group.insert(0, icon_bg)\n\n    # Update temperature and humidity\n    temperature.text = f\"{om_json['current']['temperature_2m']:.0f}{om_json['current_units']['temperature_2m']}\"\n    humidity.text = f\"{om_json['current']['relative_humidity_2m']}{om_json['current_units']['relative_humidity_2m']} RH\"\n\n    gc.collect()  # Clean up displayio rendering rubbish\n\n    alert(\"  READY\")\n    pixel[0] = NORMAL\n\n\ndef wind_direction(heading):\n    \"\"\"Provide a one or two character string representation of the compass\n    heading value. Returns '--' if heading is None.\n    :param int heading: The compass heading. No default.\"\"\"\n    if heading is None:\n        return \"--\"\n    return [\"N\", \"NE\", \"E\", \"SE\", \"S\", \"SW\", \"W\", \"NW\"][\n        int(((heading + 22.5) % 360) / 45)\n    ]\n\n\n# ### Primary Initialization Process ###\n# Connect to the Wi-Fi AP specified in settings.toml\npixel[0] = FETCH\nprint(\"Connecting to Wi-Fi...\")\nwhile not wifi.radio.ipv4_address:\n    try:\n        wifi.radio.connect(\n            os.getenv(\"CIRCUITPY_WIFI_SSID\"), os.getenv(\"CIRCUITPY_WIFI_PASSWORD\")\n        )\n    except ConnectionError as connect_err:\n        pixel[0] = ERROR\n        print(\"ERROR: Wi-Fi Connection Error:\", connect_err)\n        print(\"    retrying in 10 seconds\")\n        time.sleep(10)\n        continue\nprint(\"  Wi-Fi Connected\")\npixel[0] = NORMAL\n\npool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)\nrequests = adafruit_requests.Session(pool, ssl.create_default_context())\n\npixel[0] = STARTUP\n\n# Load the text fonts from the fonts folder\nSMALL_FONT = bitmap_font.load_font(\"/fonts/Arial-12.bdf\")\nMEDIUM_FONT = bitmap_font.load_font(\"/fonts/Arial-16.bdf\")\nLARGE_FONT = bitmap_font.load_font(\"/fonts/Arial-Bold-24.bdf\")\n\n# Define the TFT's display size\nWIDTH = display.width\nHEIGHT = display.height\n\n# Define the display group for text and graphic icon\nimage_group = displayio.Group()\ndisplay.root_group = image_group  # Select the group\ndisp_brightness(BRIGHTNESS)  # Watch it build (for the fun of it)\n\n# ### Define display graphic icon, label, and value areas ###\n# Create a replaceable icon background layer as image_group[0]\nicon_image = displayio.OnDiskBitmap(\"/icons_160x160/01d.bmp\")\nicon_bg = displayio.TileGrid(\n    icon_image,\n    pixel_shader=icon_image.pixel_shader,\n    x=(WIDTH // 2) - 80,\n    y=(HEIGHT // 2) - 80,\n)\nimage_group.append(icon_bg)\n\n# Define the project messaging label\ndisplay_message = Label(SMALL_FONT, text=\" \", color=YELLOW)\ndisplay_message.anchor_point = (0.5, 0.5)\ndisplay_message.anchored_position = (display.width // 2, 231)\nimage_group.append(display_message)\nalert(\"STARTUP\")\n\n# Define the day/date and clock labels; update and display local time\nclock_day_mon_yr = Label(MEDIUM_FONT, text=\" \")\nclock_day_mon_yr.anchor_point = (0, 0)\nclock_day_mon_yr.anchored_position = (10, 15)\nclock_day_mon_yr.color = PURPLE\nimage_group.append(clock_day_mon_yr)\n\nclock_digits = Label(MEDIUM_FONT, text=\" \")\nclock_digits.anchor_point = (1.0, 0)\nclock_digits.anchored_position = (display.width - 10, 15)\nclock_digits.color = WHITE\nimage_group.append(clock_digits)\n\nget_local_time()\ndisplay_local_time()\n\n# Define wind speed and gust labels\nwindspeed = Label(MEDIUM_FONT, text=\" \")\nwindspeed.anchor_point = (0, 0)\nwindspeed.anchored_position = (10, 44)\nwindspeed.color = WHITE\nimage_group.append(windspeed)\n\nwindgust = Label(SMALL_FONT, text=\" \")\nwindgust.anchor_point = (0, 0)\nwindgust.anchored_position = (10, 65)\nwindgust.color = RED\nimage_group.append(windgust)\n\n# Define sunrise and sunset labels\nsunrise = Label(SMALL_FONT, text=\" \")\nsunrise.anchor_point = (1.0, 0.0)\nsunrise.anchored_position = (display.width - 10, 44)\nsunrise.color = YELLOW\nimage_group.append(sunrise)\n\nsunset = Label(SMALL_FONT, text=\" \")\nsunset.anchor_point = (1.0, 0.0)\nsunset.anchored_position = (display.width - 10, 60)\nsunset.color = ORANGE\nimage_group.append(sunset)\n\n# Define the short and long description labels\ndescription = Label(LARGE_FONT, text=\" \")\ndescription.anchor_point = (0, 0)\ndescription.anchored_position = (10, 178)\ndescription.color = WHITE\nimage_group.append(description)\n\nlong_desc = Label(SMALL_FONT, text=\" \")\nlong_desc.anchor_point = (0, 0)\nlong_desc.anchored_position = (10, 210)\nlong_desc.color = PURPLE\nimage_group.append(long_desc)\n\n# Define the temperature and humidity labels\ntemperature = Label(LARGE_FONT, text=\" \")\ntemperature.anchor_point = (1.0, 0)\ntemperature.anchored_position = (display.width - 10, 178)\ntemperature.color = WHITE\nimage_group.append(temperature)\n\nhumidity = Label(SMALL_FONT, text=\" \")\nhumidity.anchor_point = (1.0, 0)\nhumidity.anchored_position = (display.width - 10, 210)\nhumidity.color = PURPLE\nimage_group.append(humidity)\n\n# Define the clock tick indicator shap\nclock_tick_mask = RoundRect(310, 4, 7, 8, 1, fill=VIOLET, outline=None, stroke=0)\nimage_group.append(clock_tick_mask)\n\ngc.collect()  # Clean up displayio rendering rubbish\n\n# Initialize interval timing variables\nlast_weather_update = time.monotonic()\nlast_time_update = time.monotonic()\n\n# Initially display the conditions\nupdate_display()  # Fetch initial data from Open-Mateo\n\n# ### Main Loop ###\nwhile True:\n    current_time = time.monotonic()\n\n    # Update weather every SAMPLE_INTERVAL seconds\n    if current_time - last_weather_update \u003e SAMPLE_INTERVAL:\n        update_display()\n        display_local_time()\n        last_weather_update = current_time\n\n    # Update network time every NTP_INTERVAL seconds\n    if current_time - last_time_update \u003e NTP_INTERVAL:\n        get_local_time()\n        display_local_time()\n        last_time_update = current_time\n\n    # Update time display every second\n    toggle_clock_tick()\n    display_local_time(repl=False)\n\n    # Watch for and adjust to ambient light changes\n    adjust_brightness()\n\n    # Adjust wait time to as close to 1 sec as possible\n    time.sleep(max(min(1.0 - (time.monotonic() - current_time), 1.0), 0))","metadata":{"language":"python","linenums":false}},{"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  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eOptional Display Brightness Control\u003c/h1\u003e\n\u003cp\u003e(Some soldering required.)\u003c/p\u003e\n\u003cp\u003eIf TFT display brightness control is desired, the TFT FeatherWing will require a modification to permit the ESP32-S3 Feather's \u003ccode\u003eTX\u003c/code\u003e pin to adjust the TFT backlight intensity. Also, the Weather Display code is designed to use a light sensor to automatically adjust brightness proportional to ambient light near the display.\u003c/p\u003e\n\u003cp\u003eWith the power cable unplugged and any batteries removed, solder a short length of wire between the TFT FeatherWing's\u0026nbsp;\u003ccode\u003eLITE\u003c/code\u003e jumper pad and the pad next to the Feather socket's\u0026nbsp;\u003ccode\u003eTX\u003c/code\u003e pin. The wire provides a path to control the TFT backlight brightness using PWM from the \u003ccode\u003eTX\u003c/code\u003e pin. This modification is required for setting brightness even if a light sensor is not attached for automatic control.\u003c/p\u003e\n\u003cp\u003eFor automatic brightness control, the \u003ca href=\"https://www.adafruit.com/product/2748\" target=\"_blank\"\u003eALS-PT19 ambient light sensor\u003c/a\u003e is attached to the FeatherWing's \u003ccode\u003eGND\u003c/code\u003e, +\u003ccode\u003e3V\u003c/code\u003e, and \u003ccode\u003eA3\u003c/code\u003e. The easiest method to make the sensor \"connectable\" is to solder three wires with header pins attached to the \u003ccode\u003e-\u003c/code\u003e, \u003ccode\u003e+\u003c/code\u003e, and \u003ccode\u003esig\u003c/code\u003e pads on the sensor breakout board. I used three \u003ca href=\"https://www.adafruit.com/product/4482\" target=\"_blank\"\u003eSoft Silicon Wires with pin connections\u003c/a\u003e, cut to length but leaving one of the wire's pin connectors intact. It's recommended that a red wire be used for the +\u003ccode\u003e3V\u003c/code\u003e power, black for \u003ccode\u003eGND\u003c/code\u003e, and some other color (yellow is nice) for the signal connection to \u003ccode\u003eA3\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003ePlug the red wire into the \u003ccode\u003e3V\u003c/code\u003e pin of the Feather socket (the third pin when counting from the left), black into the \u003ccode\u003eGND\u003c/code\u003e pin (the fourth pin), and the yellow signal wire into \u003ccode\u003eA3\u003c/code\u003e (the eighth pin).\u003c/p\u003e\n\u003cp\u003eThe final step for automatic control is to edit the \u003ccode\u003eLIGHT_SENSOR\u003c/code\u003e parameter in \u003ccode\u003ecode.py\u003c/code\u003e (line 39) so that it's set to \u003ccode\u003eTrue\u003c/code\u003e.\u0026nbsp;Save\u0026nbsp;\u003ccode\u003ecode.py\u003c/code\u003e, position the sensor to detect ambient light, and the weather display will be ready to go.\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\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/609/original/light_sensor_composite.png?1739207559","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003ch1\u003ePlanned Enhancements and Wish List\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eIncrease the number of icons for weather conditions. (planned)\u003c/li\u003e\n\u003cli\u003eUse a 3.5-inch TFT FeatherWing to simultaneously display more data categories such as barometric pressure and precipitation totals. (planned)\u003c/li\u003e\n\u003cli\u003eAdd touch-sensitive selection of forecasts and graphical data trends.\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eWould like future TFT FeatherWing and breakout display board designs to provide mounting screw holes that are equidistant from the center of the TFT viewing area rather than the exterior of the TFT module. Would make the display look more centered in an enclosure where the mounting screws are exposed.\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003eI'll be patiently wishing for an Adafruit ESP32-S3 8MB/2MB (or 16MB/8MB) PyPortal family with all the bells and whistles of the original PyPortal --connectors, on-board temperature sensor, light sensor, audio speaker and in all three sizes. Plug and play. Fingers are crossed that a revised PyPortal will also have an integrated on-off power switch.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n","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  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eResources and Adafruit Products\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eThe free \u003ca href=\"https://open-meteo.com\" target=\"_blank\"\u003eOpen-Meteo\u003c/a\u003e (OM) API service web site.\u003c/li\u003e\n\u003cli\u003eA NOAA archive listing of\u0026nbsp;\u003ca href=\"https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM\" target=\"_blank\"\u003eWMO Interpretation Codes\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eFor some helpful hints for attaching header pins to the ESP32-S3 Feather, see \u003ca href=\"https://learn.adafruit.com/how-to-solder-headers\"\u003eHow to Solder Headers\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e\n\u003ca href=\"https://www.tapplastics.com/product/plastics/cut_to_size_plastic/polycarbonate_sheets/516\" target=\"_blank\"\u003eTap Plastics polycarbonate\u003c/a\u003e 1/16-inch and 1/8-inch sheets.\u003c/li\u003e\n\u003c/ul\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\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5477","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3315","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2748","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4482","metadata":{}}]