[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/253/original/Cedar_Grove_Weather_Architecture.001.png?1728485122","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003cp\u003eThe objective of this project is to replace the existing WeatherMatrix project's \u003cem\u003eMatrixPortal M4\u003c/em\u003e display with a newer version that uses weather data provided by \u003cem\u003eAdafruit IO Plus\u003c/em\u003e (\u003cem\u003eAIO+\u003c/em\u003e) instead of the\u0026nbsp;\u003cem\u003eopenweathermap.org\u003c/em\u003e web API. The requirements include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement with \u003cstrong\u003eCircuitPython\u003c/strong\u003e for ease of development and prototyping.\u003c/li\u003e\n\u003cli\u003eDuplicate the existing 64x32 LED matrix display layout including label colors and icons.\u003c/li\u003e\n\u003cli\u003eDevelop an architecture to support multiple autonomous displays and local data feed devices (new).\u003c/li\u003e\n\u003cli\u003eUtilize a weather data source that accurately aggregates station information that closely matches local conditions.\u003c/li\u003e\n\u003cli\u003eWeather data updated approximately 3 times per hour and is reliably available.\u003c/li\u003e\n\u003cli\u003eService subscription is free or reasonably priced.\u003c/li\u003e\n\u003cli\u003eInclude data elements for wind gusts and local workshop temperature (new).\u003c/li\u003e\n\u003cli\u003eDisplay a progress bar to indicate data time since last update (new).\u003c/li\u003e\n\u003cli\u003eIf possible, use the existing \u003cem\u003eMatrixPortal M4\u003c/em\u003e hardware; upgrade to \u003cem\u003eMatrixPortal S3\u003c/em\u003e only if necessary.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3\u003eSpecial Acknowledgements\u003c/h3\u003e\n\u003cp\u003eThank you to \u003ca href=\"https://learn.adafruit.com/u/johnpark\" target=\"_blank\"\u003eJohn Park\u003c/a\u003e for the initial weather display design concept (see \u003ca href=\"https://learn.adafruit.com/weather-display-matrix\" target=\"_blank\"\u003eWeather Display Matrix\u003c/a\u003e) that inspired this project. Also, the work of \u003ca href=\"https://learn.adafruit.com/u/Seekwill\" target=\"_blank\"\u003eTrevor Beaton\u003c/a\u003e was instrumental in creating the updated display design with a clearer and simpler coding approach (see \u003ca href=\"https://learn.adafruit.com/itsasnap-daily-weather-forecast-board/code-with-circuitpython\" target=\"_blank\"\u003eitsaSNAP Daily Weather Forecast Board\u003c/a\u003e).\u003c/p\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/252/original/matrix_weather.jpeg?1728485121","metadata":{"caption":"The original MatrixWeather display - MatrixPortal M4 with 3mm 64x32 matrix display"}},{"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        \u003ch2\u003eChoose a Weather Observation Source\u003c/h2\u003e\n\u003cp\u003eThree options for sourcing weather data were considered. Here are some pros and cons of each.\u003c/p\u003e\n\u003ch3\u003eopenweathermap.org:\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eutilizes the existing or similar web API; minimal code changes will be needed\u003c/li\u003e\n\u003cli\u003eweather station aggregation often doesn't always match local conditions\u003c/li\u003e\n\u003cli\u003edata retrieval throttling is reasonable with a consistent service level\u003c/li\u003e\n\u003cli\u003efree service was phased out; will eventually require paying for the service\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3\u003eweather.gov:\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003ea moderately well-documented web API; moderate code changes will be needed\u003c/li\u003e\n\u003cli\u003eweather station data is not aggregated and closely matches local conditions\u003c/li\u003e\n\u003cli\u003esingle weather station data is not reliably available, sometime for hours at a stretch\u003c/li\u003e\n\u003cli\u003edata retrieval throttling is currently reasonable, but without a guaranteed service level\u003c/li\u003e\n\u003cli\u003ea free service\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3\u003eAIO+ Weather (Apple WeatherKit):\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003emoderately well-documented CircuitPython library approach; major code changes will be needed\u003c/li\u003e\n\u003cli\u003eweather station data is aggregated and matches local and iPhone conditions\u003c/li\u003e\n\u003cli\u003edata retrieval throttling is reasonable with a consistent service level\u003c/li\u003e\n\u003cli\u003eweather data and workshop conditions can be combined and viewed on a web-based IO dashboard\u003c/li\u003e\n\u003cli\u003erequires a paid subscription to \u003cem\u003eAIO+\u003c/em\u003e for access to its \u003cem\u003eWeather Service\u003c/em\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe choice to use \u003cem\u003eAIO+ Weather \u003c/em\u003ewas an easy one since my existing weather tracking and corrosion monitoring projects require a subscription to \u003cem\u003eAIO+\u003c/em\u003e. We'll need a new system design and a major rewrite of the existing MatrixWeather project code to make this happen, providing ample opportunities to improve performance and reliability. The next step was to redesign the overarching weather system architecture.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"https://adafruit-playground.com/u/CGrover/pages/weather-gov-a-truly-free-weather-api\" target=\"_blank\"\u003eweather.gov: A Truly Free Weather API\u003c/a\u003e and \u003ca href=\"https://adafruit-playground.com/u/CGrover/pages/aio-weather-a-premium-alternative-for-local-observations\" target=\"_blank\"\u003eAIO+ Weather: A Premium Alternative for Local Observations\u003c/a\u003e for a detailed discussion of each alternative.\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":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/246/original/Cedar_Grove_Weather_Architecture.001.png?1728485119","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eUpdate the System Design\u003c/h2\u003e\n\u003cp\u003eThe new system architecture initially consists of three local components. The first is a repeater that retrieves \u003cem\u003eAIO+ Weather Service \u003c/em\u003edata \u003cem\u003e(Apple WeatherKit)\u003c/em\u003e \u0026nbsp;and sends observation information to existing \u003cem\u003eAIO\u003c/em\u003e weather feeds. Since it runs separately on an \u003cem\u003eESP32-S3 TFT Feather\u003c/em\u003e, the\u0026nbsp;\u003cem\u003eAIO Weather Repeater\u003c/em\u003e can continuously and asynchronously provide data to the feeds that are ultimately shared with other system components.\u003c/p\u003e\n\u003cp\u003eThe \u003cem\u003eWorkshop Corrosion Monitor\u003c/em\u003e is a device that uploads observations of workshop conditions measured by sensors inside the workshop. The monitor is physically installed in the workshop and displays interior conditions. The monitor detects corrosion events by watching a dew point calculation, a combination of temperature and humidity that creates moisture condensation and causes corrosion. Predicting potential corrosion events is a future capability that will require watching exterior weather trends provided by\u0026nbsp;\u003cem\u003eWeather Repeater\u003c/em\u003e feeds.\u003c/p\u003e\n\u003cp\u003eThe final system component is the \u003cem\u003eMatrixWeather Receiver\u003c/em\u003e, an updated version of the current \u003cem\u003eMatrixWeather\u003c/em\u003e display. The receiver gets its information from the \u003cem\u003eAIO\u003c/em\u003e feeds, buffering it from the weather repeater and workshop monitor data sources. With this design, all three components of the system operate autonomously, tied together by services and feeds in the \u003cem\u003eAIO\u003c/em\u003e cloud.\u003c/p\u003e\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/255/original/AIO_dashboard.png?1728485126","metadata":{"caption":"The AIO Web Dashboard composite view of weather and corrosion feeds"}},{"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        \u003ch2\u003eThe AIO Weather REPEATER\u003c/h2\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/248/original/IMG_2367.jpeg?1728485118","metadata":{"caption":"AIO Weather Repeater - ESP32-S3 TFT Feather"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe AIO Weather Repeater fetches a subset of\u0026nbsp;\u003cem\u003eAIO Weather\u003c/em\u003e data, converts the values, and publishes the revised data to \u003cem\u003eAIO\u003c/em\u003e feeds. A single repeater is needed for the system to create the data that other devices may need. For this project, the repeater is powered from an AC adapter and backup battery. It is permanently mounted within range of a WiFi access point.\u003c/p\u003e\n\u003cp\u003eThe following data fields are fetched by the repeater and forwarded to the feeds.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\"conditionCode\" text published to \"weather-description\"\u003c/li\u003e\n\u003cli\u003e\"temperature\" converted to degrees F and published to \"weather-temperature\"\u003c/li\u003e\n\u003cli\u003e\"humidity\" published to \"weather-humidity\"\u003c/li\u003e\n\u003cli\u003e\"windSpeed\" converted to MPH and published to \"weather-windspeed\"\u003c/li\u003e\n\u003cli\u003e\"windDirection\" converted to compass description text and published to \"weather-winddirection\"\u003c/li\u003e\n\u003cli\u003e\"windGust\" converted to MPH and published to \"weather-windgusts\"\u003c/li\u003e\n\u003cli\u003e\"metadata.readTime\" a data freshness time stamp that is currently not published\u003c/li\u003e\n\u003cli\u003e\"daylight\" a True/False value published to \"weather-daylight\"\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn addition, a \"system-watchdog\" value is published. The watchdog value indicates the number of minutes that have elapsed since the last hard boot of the repeater's \u003cem\u003eESP32-S3 TFT Feather\u003c/em\u003e board.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/CedarGroveStudios/AIO_Weather_Repeater\" target=\"_blank\"\u003eAIO Weather Repeater code repository\u003c/a\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2024 JG for Cedar Grove Maker Studios\n# SPDX-License-Identifier: MIT\n#\n# AIO_weather_repeater_v00.py\n# An AIO+ Weather Repeater for ESP32-S3 Feather\n\nimport board\nimport digitalio\nimport os\nimport time\nimport json\nimport ssl\nimport supervisor\nimport neopixel\nimport adafruit_datetime\nimport adafruit_connection_manager\nimport wifi\nimport adafruit_requests\nimport adafruit_minimqtt.adafruit_minimqtt as mqtt\nfrom adafruit_io.adafruit_io import IO_MQTT\nfrom cedargrove_temperaturetools.unit_converters import celsius_to_fahrenheit\n\nfrom weatherkit_to_weathmap_icon import kit_to_map_icon\n\n# Set default display brightness\nboard.DISPLAY.brightness = 0.025\n\n# Instantiate the LED\nled = digitalio.DigitalInOut(board.LED)\nled.direction = digitalio.Direction.OUTPUT\nled.value = False\n\n# Initialize NeoPixel\npixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.01)\npixel[0] = 0xFFFF00  # Initializing (yellow)\n\n# Get weather observations from the Sea-Tac station (47.45,-122.31)\nW_TOPIC_KEY = 2730  # Integer value\nW_TOPIC_DESC = \"Sea-Tac Airport\"\n\n\ndef subscribe(client, userdata, topic, granted_qos):\n    \"\"\"Callback method for when the client subscribes to a new feed.\"\"\"\n    print(f\"  ... subscribed to '{topic}' with QOS level {granted_qos}\")\n\n\ndef message(client, feed_id, payload):\n    \"\"\"Callback method for when a subscribed feed has a new value.\"\"\"\n    global weather_data\n    # print(payload)\n    if feed_id == \"weather\":\n        weather_data = json.loads(payload)\n\n\ndef wind_direction(heading):\n    if heading is None:\n        return '--'\n    return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][int(((heading + 22.5) % 360) / 45)]\n\n\ndef publish_to_aio(value, feed):\n    \"\"\"Publish a value to an AIO feed at a throttled transaction rate. A\n    blocking method.\n    :param union(integer, float, string) value: The value to publish.\n    :param string feed: The name of the AIO feed.\"\"\"\n    if value is not None:\n        time.sleep(2.5)  # Until throttle method is re-enabled\n        try:\n            aio.publish(feed, value)\n            pixel[0] = 0x00FF00  # Success (green)\n            print(f\"  ... SUCCESS: published '{value}' to AIO '{feed}'\")\n        except Exception as aio_publish_error:\n            pixel[0] = 0xFF0000  # Error (red)\n            print(f\"  FAILED to publish '{value}' to AIO '{feed}'\")\n            print(f\"    publish error: {str(aio_publish_error)}\")\n\n            print(\"attempting to reconnect to AIO...\")\n            try:\n                aio.reconnect()\n                pixel[0] = 0x00FF00  # Success (green)\n            except Exception as pub_reconnect_error:\n                pixel[0] = 0xFF0000  # Error (red)\n                print(f\"  FAILED: publish aio.reconnect {str(pub_reconnect_error)}\")\n                print(\"      MCU will soft reset in 60 seconds.\")\n                time.sleep(60)\n                supervisor.reload()  # soft reset: keeps the terminal session alive\n\n    else:\n        print(f\"  FAILED to publish '{value}' to AIO '{feed}'\")\n\n\ndef busy(delay):\n    \"\"\"An alternative time.sleep function that blinks the LED once per second.\n    A blocking method.\"\"\"\n    pixel[0] = 0x000000  # Normal (black)\n    for blinks in range(int(round(delay, 0))):\n        led.value = True\n        time.sleep(0.05)\n        led.value = False\n        time.sleep(0.95)\n\ntry:\n    # Connect to Wi-Fi access point\n    print(f\"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}\")\n    wifi.radio.connect(\n        os.getenv(\"CIRCUITPY_WIFI_SSID\"), os.getenv(\"CIRCUITPY_WIFI_PASSWORD\")\n    )\n    pixel[0] = 0x00FF00  # Success (green)\n    print(\"  SUCCESS: connected to WiFi access point\")\nexcept Exception as wifi_access_error:\n    pixel[0] = 0xFF0000  # Error (red)\n    print(f\"  FAILED: connect to WiFi. Error: {wifi_access_error}\")\n    print(\"      MCU will soft reset in 60 seconds.\")\n    time.sleep(60)\n    supervisor.reload()  # soft reset: keeps the terminal session alive\n\n# Initialize a requests session\npool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)\nrequests = adafruit_requests.Session(pool, ssl.create_default_context())\n\n# Connect to AIO and MQTT\n# Initialize the MQTT Client object (keep_alive default is 300 sec)\nmqtt_client = mqtt.MQTT(\n    broker=\"io.adafruit.com\",\n    username=os.getenv(\"aio_username\"),\n    password=os.getenv(\"aio_key\"),\n    keep_alive=180,\n    socket_pool=pool,\n    ssl_context=ssl.create_default_context(),\n)\n\n# Initialize AIO MQTT helper and connect to AIO\naio = IO_MQTT(mqtt_client)\nprint(\"  ... IO MQTT helper initialized\")\naio.connect()\nprint(f\"  ... IO MQTT connected: {mqtt_client.is_connected()}\")\n\n# Connect the callback methods to AIO\naio.on_subscribe = subscribe\naio.on_message = message\n\n# Subscribe to the AIO+ weather topic\naio.subscribe_to_weather(W_TOPIC_KEY, \"current\")\n\n# Initialize the weather_data and history variables\nweather_data = None\nweather_data_old = None\n\nwhile True:\n    # Fetch and update the weather conditions from AIO\n    try:\n        pixel[0] = 0xFFFF00  # AIO in progress (yellow)\n        time.sleep(2.5)  # Until throttle method is re-enabled\n        aio.loop(10)\n        pixel[0] = 0x00FF00  # Success (green)\n    except Exception as loop_error:\n        pixel[0] = 0xFF0000  # Error (red)\n        print(f\"  FAILED: aio.loop {str(loop_error)}\")\n        print(\"    attempting to reconnect to AIO\")\n        try:\n            time.sleep(10)\n            aio.connect()\n            pixel[0] = 0x00FF00  # Success (green)\n        except Exception as loop_reconnect_error:\n            pixel[0] = 0xFF0000  # Error (red)\n            print(f\"  FAILED: loop aio.reconnect {str(loop_reconnect_error)}\")\n            print(\"      MCU will soft reset in 60 seconds.\")\n            time.sleep(60)\n            supervisor.reload()  # soft reset: keeps the terminal session alive\n\n    if weather_data:\n        if weather_data != weather_data_old:\n            weatherkit_desc = weather_data[\"conditionCode\"]\n            weatherkit_temp = celsius_to_fahrenheit(weather_data[\"temperature\"])\n            weatherkit_humid = weather_data[\"humidity\"] * 100\n            weatherkit_wind_speed = weather_data[\"windSpeed\"] * 0.6214\n            weatherkit_wind_dir = wind_direction(weather_data[\"windDirection\"])\n            weatherkit_wind_gusts = weather_data[\"windGust\"] * 0.6214\n            weatherkit_timestamp = weather_data[\"metadata\"][\"readTime\"]\n            weatherkit_daylight = weather_data[\"daylight\"]\n            # print(\"daylight\", weatherkit_daylight, type(weatherkit_daylight), dir(weatherkit_daylight))\n\n            print(\"-\" * 13 + \" NEW \" + \"-\" * 13)\n            print(f\"timestamp: {weatherkit_timestamp}\")\n\n            sample_time = adafruit_datetime.datetime.fromisoformat(weatherkit_timestamp[:-1])\n            print(f\"sample_time: {sample_time}\")\n\n            local_time = sample_time - adafruit_datetime.timedelta(hours=7)\n            print(f\"local_time: {local_time}\")\n\n            try:\n                if weatherkit_daylight:\n                    icon = kit_to_map_icon[weatherkit_desc][1] + \"d\"\n                else:\n                    icon = kit_to_map_icon[weatherkit_desc][1] + \"n\"\n                long_desc = kit_to_map_icon[weatherkit_desc][0]\n            except KeyError as desc_error:\n                print(f\"  NO Description: {desc_error}\")\n                icon = \"99d\"\n                long_desc = f\"unknown description: {weatherkit_desc}\"\n            print(f\"icon: {icon}\")\n            print(f\"long_description: {long_desc}\")\n\n            publish_to_aio(int(round(time.monotonic() / 60, 0)), \"system-watchdog\")\n            publish_to_aio(weatherkit_desc, \"weather-description\")\n            publish_to_aio(weatherkit_humid, \"weather-humidity\")\n            publish_to_aio(weatherkit_temp, \"weather-temperature\")\n            publish_to_aio(weatherkit_wind_dir, \"weather-winddirection\")\n            publish_to_aio(weatherkit_wind_gusts, \"weather-windgusts\")\n            publish_to_aio(weatherkit_wind_speed, \"weather-windspeed\")\n            publish_to_aio(str(weatherkit_daylight), \"weather-daylight\")\n\n            weather_data_old = weather_data  # to watch for changes\n        busy(120)  # Delay for two minutes to reduce query rate\n    else:\n        print(f\"  ... waiting for weather observations from {W_TOPIC_DESC}\")\n        busy(10)  # Step up query rate when first starting","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        \u003ch2\u003eThe Workshop Corrosion Monitor\u003c/h2\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/249/original/coorosion_monitor_001.jpeg?1728485119","metadata":{"caption":"Workshop Corrosion Monitor - PyPortal M4 with local temperature//humidity sensor"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe Corrosion Monitor lives in the adjacent workshop building. It is a data sender that uploads five feeds to \u003cem\u003eAIO\u003c/em\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCorrosion Index: a composite value that indicates a corrosion event\u003c/li\u003e\n\u003cli\u003eDew Point: the air temperature when moisture condensation commences\u003c/li\u003e\n\u003cli\u003eHumidity: the relative content of moisture suspended in air\u003c/li\u003e\n\u003cli\u003eTemperature: the workshop interior temperature\u003c/li\u003e\n\u003cli\u003eCPU Temperature: the on-chip temperature of the \u003cem\u003ePyPortal M4\u003c/em\u003e MPU (used to operate the monitor's cooling fan)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eCurrently, the Corrosion Monitor detects when a corrosion event occurs and sends data to \u003cem\u003eAIO\u003c/em\u003e. When upgraded, it will retrieve weather information trends in order to predict near-term future corrosion events.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/CedarGroveStudios/CorrosionMonitor\" target=\"_blank\"\u003eWorkshop Corrosion Monitor code repository\u003c/a\u003e\u003c/p\u003e\n      \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  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eThe MatrixWeather RECEIVER\u003c/h2\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/250/original/IMG_2366.jpeg?1728485121","metadata":{"caption":"MatrixWeather Receiver - MatrixPortal M4 with 3mm 32x64 matrix display"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n        \u003cp\u003ePlaced in the home's living area, the MatrixWeather Receiver's large 64x32 display makes it easy to see the most impactful weather conditions at a glance. At this time, only one display is used, but the system architecture should support others, if the need emerges. For this project the receiver is implemented with a \u003cem\u003eMatrixPortal M4\u003c/em\u003e and is compatible with the \u003cem\u003eMatrixPortal S3\u003c/em\u003e board.\u003c/p\u003e\n\u003cp\u003eSince only five text fields and one bitmap graphic are used for the receiver's display, the new version uses the simpler, abstracted \u003ccode\u003eMatrixPortal\u003c/code\u003e and \u003ccode\u003ePortalBase\u003c/code\u003e CircuitPython libraries that help with \u003cem\u003eAIO\u003c/em\u003e feed communications.\u003c/p\u003e\n\u003cp\u003eThe display consists of five text labels:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTemperature in degrees Fahrenheit\u003c/li\u003e\n\u003cli\u003eWind direction/speed in MPH\u003c/li\u003e\n\u003cli\u003eHumidity %\u003c/li\u003e\n\u003cli\u003eA scrolling label for the description, wind gust speed, and workshop temperature\u003c/li\u003e\n\u003cli\u003eProgress bar\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eInstead of using a graphic object, the progress bar is a text label object containing a single period character (\".\") that moves across the bottom row, indicating when the weather data will be updated. Right after the data is refreshed, a green indicator starts at the left edge of the display and gradually moves to the right. As it gets close to the right side, it turns red. New data will be fetched from the AIO feeds when it reaches the edge. The text label positioning property is not exposed in the \u003ccode\u003eMatrixPortal\u003c/code\u003e class, so the code uses the class' internal variable.\u003c/p\u003e\n\u003cp\u003eThe built-in \u003cem\u003eAIO\u003c/em\u003e fetch capability of the \u003ccode\u003ePortalBase.Network\u003c/code\u003e class retrieves all data in the feed rather than just the last entry. Although it is possible to extract a single entry from the query result with some simple CircuitPython code, the entire table of values would still reside in the \u003cem\u003eMatrixPortal\u003c/em\u003e's memory. The \u003cem\u003eMatrixPortal M4\u003c/em\u003e doesn't have the capacity for storing the entire extract; upgrading to the \u003cem\u003eS3\u003c/em\u003e version with more memory would be required. Instead, a hidden function in the network class can be used to fetch only the last feed value, allowing continued use of the existing \u003cem\u003eMatrixPortal M4\u003c/em\u003e board.\u003c/p\u003e\n\u003cp\u003eTo control display brightness and reduce power consumption, the \u003ccode\u003ecedargrove_palettefader\u003c/code\u003e library was used to alter the graphic icon palette and text label colors. As with the text table position property, the bitmap palette is not exposed in the \u003ccode\u003eMatrixPortal\u003c/code\u003e class, so the internal palette object was used in the code. An issue to expose the class' hidden properties will be submitted.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/CedarGroveStudios/MatrixWeather_Receiver/tree/main\" target=\"_blank\"\u003eMatrixWeather Receiver code repository\u003c/a\u003e\u003c/p\u003e\n      \n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: Modified 2024 JG for Cedar Grove Maker Studios\n# inspired by code developed by Trevor Beaton for Adafruit Industries\n#\n# SPDX-License-Identifier: MIT\n#\n# MatrixWeather_AIO_Receiver\n\nimport time\n\nimport board\nimport terminalio\nfrom adafruit_matrixportal.matrixportal import MatrixPortal\nfrom cedargrove_palettefader.palettefader import PaletteFader\nfrom weatherkit_to_weathmap_icon import kit_to_map_icon\n\n# AIO Weather Receiver Parameters\nUPDATE_DELAY = 1320  # 22 minutes\nBRIGHTNESS = 0.1\n\n# Instantiate matrix display\nmp = MatrixPortal(width=64, height=32, bit_depth=6, debug=False, rotation=90, status_neopixel=board.NEOPIXEL)\n\n# Load splash graphic and dim per BRIGHTNESS\nmp.set_background(\"images/background_sun_clouds.bmp\", (65, 65))\nbkg_normal = PaletteFader(\n    mp.graphics._bg_sprite.pixel_shader,\n    BRIGHTNESS,\n    gamma=0.65,\n    normalize=True,\n)\nmp.graphics._bg_sprite.pixel_shader = bkg_normal.palette\nmp.graphics._bg_sprite.x = 0\nmp.graphics._bg_sprite.y = 0\n\n# Define color list for labels and dim per BRIGHTNESS\nLABEL_COLORS_REF = [\n    0xFFFF00,  # yellow; temperature\n    0x0066FF,  # blue; description\n    0x00FFFF,  # cyan; humidity\n    0xFF00FF,  # purple; wind\n    0x800000,  # red; progress bar\n    0x008000,  # green; progress bar\n]\nlabel_colors = PaletteFader(LABEL_COLORS_REF, BRIGHTNESS, gamma=1.0, normalize=False)\n\n# Define the temperature label\nmp.add_text(\n    text_font=terminalio.FONT,\n    text_color=label_colors.palette[0],\n    text_position=(16, 6),\n    text_anchor_point=(0.5, 0.5),\n    scrolling=False,\n)\n\n# Define the wind speed/direction label\nmp.add_text(\n    text_font=terminalio.FONT,\n    text_color=label_colors.palette[2],\n    text_position=(16, 45),\n    text_anchor_point=(0.5, 0.5),\n    scrolling=False,\n)\n\n# Define the humidity label\nmp.add_text(\n    text_font=terminalio.FONT,\n    text_color=label_colors.palette[3],\n    text_position=(16, 34),\n    text_anchor_point=(0.5, 0.5),\n    scrolling=False,\n)\n\n# Define the scrolling description label\nmp.add_text(\n    text_font=terminalio.FONT,\n    text_color=label_colors.palette[1],\n    text_position=(16, 55),\n    text_anchor_point=(0.5, 0.5),\n    scrolling=True,\n)\n\n# Define the progress bar label\nmp.add_text(\n    text_font=terminalio.FONT,\n    text_color=label_colors.palette[3],\n    text_position=(16, 61),\n    text_anchor_point=(0.5, 0.5),\n    scrolling=False,\n)\nmp.set_text(\".\", 4)\n\n\ndef get_last_value(feed_key):\n    \"\"\"Fetch the latest value of the AIO feed.\n    :param str feed_key: The AIO feed key.\"\"\"\n    try:\n        last_value = mp.network.io_client.receive_data(feed_key)[\"value\"]\n        return last_value\n    except (KeyError, IndexError) as e:\n        print(f\"Error fetching data from feed {feed_key}: {e}\")\n\n\ndef update_display():\n    \"\"\"Fetch last values and update the display.\"\"\"\n    temperature = get_last_value(\"weather-temperature\")\n    temperature = f\"{int(round(float(temperature), 0))} F\"\n\n    humidity = get_last_value(\"weather-humidity\")\n    humidity = f\"{int(round(float(humidity), 0))}%\"\n\n    condition = get_last_value(\"weather-description\")\n    try:\n        current_condition = kit_to_map_icon[condition][0]\n    except:\n        current_condition = \"unknown\"  # If condition is not in translator\n\n    daylight = get_last_value(\"weather-daylight\")\n    if daylight == \"True\":\n        icon_suffix = \"d\"\n    else:\n        icon_suffix = \"n\"\n\n    wind_speed = get_last_value(\"weather-windspeed\")\n    wind_speed = f\"{int(round(float(wind_speed), 0))}\"\n\n    wind_dir = get_last_value(\"weather-winddirection\")\n\n    wind_gusts = get_last_value(\"weather-windgusts\")\n    wind_gusts = f\"{int(round(float(wind_gusts), 0))}\"\n\n    wind = f\"{wind_dir} {wind_speed}\"\n\n    shop_temperature = get_last_value(\"shop.int-temperature\")\n    shop_temperature = f\"{int(round(float(shop_temperature), 0))}\"\n\n    # Get the local time and provide hour-of-day for is_daytime method\n    try:\n        mp.get_local_time(\"America/Los_Angeles\")\n        display_time = f\"{time.localtime().tm_hour:2d}:{time.localtime().tm_min:2d}\"\n        print(f\"Local Time: {display_time}\")\n    except Exception as e:\n        print(f\"Error fetching local time: {e}\")\n\n    # Create icon file name\n    if current_condition != \"unknown\":\n        icon = f\"index_{kit_to_map_icon[condition][2]:02d}{icon_suffix}.bmp\"\n    else:\n        icon = \"index_08d.bmp\"  # Default icon if condition not in translator\n    print(f\"Icon filename: {icon}\")\n\n    mp.set_text(temperature, 0)\n    mp.set_text(humidity, 1)\n    mp.set_text(wind, 2)\n    mp.set_text(f\"{current_condition} Gusts:{wind_gusts} Shop:{shop_temperature}\", 3)\n\n    mp.set_background(f\"images/{icon}\", (65, 65))\n    mp.graphics._bg_sprite.pixel_shader.make_transparent(0)\n    icon_normal = PaletteFader(\n        mp.graphics._bg_sprite.pixel_shader,\n        BRIGHTNESS,\n        gamma=1.0,\n        normalize=True,\n    )\n    mp.graphics._bg_sprite.pixel_shader = icon_normal.palette\n    mp.graphics._bg_sprite.x = 8\n    mp.graphics._bg_sprite.y = 12\n\nold_prog_bar_x = -1\nlast_weather_update = time.monotonic()\nupdate_display()\n\n# Main loop\nwhile True:\n    current_time = time.monotonic()\n\n    # Update weather every UPDATE_DELAY seconds\n    if current_time - last_weather_update \u003e UPDATE_DELAY:\n        update_display()\n        last_weather_update = current_time\n\n    prog_bar_x = int(round(32 * (current_time - last_weather_update) / UPDATE_DELAY, 0))\n    if prog_bar_x != old_prog_bar_x:\n        if prog_bar_x \u003e 25:\n            mp._text[4][\"color\"] = label_colors.palette[4]\n        else:\n            mp._text[4][\"color\"] = label_colors.palette[5]\n        mp._text[4][\"position\"] = ((prog_bar_x, 61))\n        mp.set_text(\".\", 4)\n        old_prog_bar_x = prog_bar_x\n\n    # Sleep for 0.1 second then scroll wind and description text\n    time.sleep(0.10)  # Sleep for 0.1 second\n    mp.scroll()","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eWeather Icon Translator Module\u003c/p\u003e\n\u003cp\u003eThis module is used by the receiver to translate \u003cem\u003eApple WeatherKit\u003c/em\u003e conditions to more descriptive narratives and to map a condition to an\u0026nbsp;\u003cem\u003eopenweathermap\u003c/em\u003e filename or matrix icon index. The receiver code provides the short description and uses the returned index value to build the icon filename. Index icon bitmap files are contained in the \u003ccode\u003eimages\u003c/code\u003e folder.\u003c/p\u003e\n      \n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# Apple WeatherKit to openweathermap icon code translator\n#   WeatherCondition, description, openweathermap icon code, matrix icon index\n\nkit_to_map_icon = {\n    \"BlowingDust\": [\"Blowing dust or sand.\", \"50\", 8],\n    \"Clear\": [\"Clear.\", \"01\", 0],\n    \"Cloudy\": [\"Cloudy, overcast.\", \"04\", 3],\n    \"Foggy\": [\"Fog.\", \"50\", 8],\n    \"Haze\": [\"Haze.\", \"50\", 8],\n    \"MostlyClear\": [\"Mostly clear.\", \"02\", 1],\n    \"MostlyCloudy\": [\"Mostly cloudy.\", \"03\", 2],\n    \"PartlyCloudy\": [\"Partly cloudy.\", \"02\", 1],\n    \"Smoky\": [\"Smoky.\", \"50\", 8],\n    \"Breezy\": [\"Breezy, light wind.\", \"99\", 8],\n    \"Windy\": [\"Windy.\", \"99\", 8],\n    \"Drizzle\": [\"Drizzle or light rain.\", \"10\", 5],\n    \"HeavyRain\": [\"Heavy rain.\", \"09\", 4],\n    \"IsolatedThunderstorms\": [\"Light thunderstorms.\", \"11\", 6],\n    \"Rain\": [\"Rain.\", \"09\", 4],\n    \"SunShowers\": [\"Rain with visible sun.\", \"10\", 5],\n    \"ScatteredThunderstorms\": [\"Scattered thunderstorms.\", \"11\", 6],\n    \"StrongStorms\": [\"Strong thunderstorms.\", \"11\", 6],\n    \"Thunderstorms\": [\"Thunderstorms.\", \"11\", 6],\n    \"Frigid\": [\"Frigid conditions.\", \"99\", 8],\n    \"Hail\": [\"Hail.\", \"13\", 7],\n    \"Hot\": [\"High temperatures.\", \"99\", 0],\n    \"Flurries\": [\"Flurries or light snow.\", \"13\", 7],\n    \"Sleet\": [\"Sleet.\", \"13\", 7],\n    \"Snow\": [\"Snow.\", \"13\", 7],\n    \"SunFlurries\": [\"Snow flurries with visible sun.\", \"13\", 7],\n    \"WintryMix\": [\"Wintry mix.\", \"13\", 7],\n    \"Blizzard\": [\"Blizzard.\", \"13\", 7],\n    \"BlowingSnow\": [\"Blowing or drifting snow.\", \"13\", 7],\n    \"FreezingDrizzle\": [\"Freezing drizzle or light rain.\", \"09\", 4],\n    \"FreezingRain\": [\"Freezing rain.\", \"09\", 4],\n    \"HeavySnow\": [\"Heavy snow.\", \"13\", 7],\n    \"Hurricane\": [\"Hurricane.\", \"99\", 8],\n    \"TropicalStorm\": [\"Tropical storm.\", \"99\", 8],\n}","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        \u003ch1\u003eWish List (in progress)\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eMatrixPortal and PortalBase CircuitPython Libraries\n\u003cul\u003e\n\u003cli\u003eExpose the following functions and properties:\n\u003cul\u003e\n\u003cli\u003e\n\u003ccode\u003enetwork.io_client.receive_data( )\u003c/code\u003e\u0026nbsp; to reduce size of \u003cem\u003eAIO\u003c/em\u003e retrieval data set for \u003cem\u003eM4\u003c/em\u003e processor\u003c/li\u003e\n\u003cli\u003e\n\u003ccode\u003egraphics._bg_sprite.pixel_shader\u003c/code\u003e\u0026nbsp; for software-based display brightness control\u003c/li\u003e\n\u003cli\u003e\n\u003ccode\u003egraphics._bg_sprite.x\u003c/code\u003e \u0026nbsp;for software-based display brightness control\u003c/li\u003e\n\u003cli\u003e\n\u003ccode\u003egraphics._bg_sprite.y\u003c/code\u003e \u0026nbsp;for software-based display brightness control\u003c/li\u003e\n\u003cli\u003e\n\u003ccode\u003e_text[index][\"color\"]\u003c/code\u003e\u0026nbsp; to change color of progress bar label (and any text label)\u003c/li\u003e\n\u003cli\u003e\n\u003ccode\u003e_text[index][\"position\"]\u003c/code\u003e to change position of progress bar label (and any text label)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eAdafruit IO\n\u003cul\u003e\n\u003cli\u003eCollection of selectable server-side feeds for a client's \u003cem\u003eAIO\u003c/em\u003e account with the latest weather observations for a particular latitude/longitude or NWS station, perhaps built on the existing \u003cem\u003eAIO+ Weather\u003c/em\u003e integration. Would be a welcome feature that could create a storm of new project innovation.\u003c/li\u003e\n\u003cli\u003eUse the throttling feature in the \u003ccode\u003eadafruit_io\u003c/code\u003e CircuitPython library to manage to \u003cem\u003eAIO\u003c/em\u003e service levels (free = 30 feeds/min, \u003cem\u003eAIO+\u003c/em\u003e = 60 feeds/min). Would be particularly useful for managing feed rates from multiple \u003cem\u003eAIO\u003c/em\u003e projects such as this one.\u003c/li\u003e\n\u003cli\u003eIt's been difficult to find examples for how to control \u003cem\u003eAIO\u003c/em\u003e feed rates in order to stay under account limits, particularly for projects like this one that share feeds and independently query and publish to the same \u003cem\u003eAIO\u003c/em\u003e account's feeds. Some further research is needed -- knowing that there's a user-facing throttle management scheme in \u003cem\u003eAIO\u003c/em\u003e that's supported by the CircuitPython \u003cem\u003eAIO\u003c/em\u003e libraries.\u003c/li\u003e\n\u003cli\u003eCreate a CircuitPython library of icon bitmap files to match \u003cem\u003eApple WeatherKit\u003c/em\u003e,\u0026nbsp;\u003cem\u003eopenweathermap.org,\u003c/em\u003e and/or \u003ca href=\"https://api.weather.gov/icons/land/night/few?size=large\" target=\"_blank\"\u003e\u003cem\u003eweather.gov\u003c/em\u003e\u003c/a\u003e conditions. Consider adding a function to resize the bitmap for a specified resolution or pixel size.\u003c/li\u003e\n\u003c/ul\u003e\n\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","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5483","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4745","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5778","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2279","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4116","metadata":{}}]