|
| 1 | +# SPDX-FileCopyrightText: 2026 Liz Clark for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +# pylint: disable=redefined-outer-name, eval-used, wrong-import-order, unsubscriptable-object |
| 5 | + |
| 6 | +""" |
| 7 | +Xteink X4 Weather Display Demo |
| 8 | +Based on MagTag Weather by Carter Nelson |
| 9 | +""" |
| 10 | + |
| 11 | +import time |
| 12 | +import os |
| 13 | +import board |
| 14 | +import alarm |
| 15 | +import displayio |
| 16 | +import adafruit_imageload |
| 17 | +import ssl |
| 18 | +import wifi |
| 19 | +import socketpool |
| 20 | +import adafruit_requests |
| 21 | +from adafruit_bitmap_font import bitmap_font |
| 22 | +from adafruit_display_text import label |
| 23 | +import gc |
| 24 | + |
| 25 | +gc.collect() |
| 26 | + |
| 27 | +display = board.DISPLAY |
| 28 | +display.rotation = 270 |
| 29 | +DISPLAY_WIDTH = display.width |
| 30 | + |
| 31 | +try: |
| 32 | + wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) |
| 33 | +except TypeError: |
| 34 | + print("Could not find WiFi info. Check your settings.toml file!") |
| 35 | + raise |
| 36 | + |
| 37 | +# --| USER CONFIG |-------------------------- |
| 38 | +LAT = 40.7128 # latitude |
| 39 | +LON = -74.0060 # longitude |
| 40 | +TMZ = "America/New_York" # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones |
| 41 | +METRIC = False # set to True for metric units |
| 42 | +CITY = "New York, NY" # optional |
| 43 | +# ------------------------------------------- |
| 44 | + |
| 45 | +pool = socketpool.SocketPool(wifi.radio) |
| 46 | +requests = adafruit_requests.Session(pool, ssl.create_default_context()) |
| 47 | +URL = f"https://api.open-meteo.com/v1/forecast?latitude={LAT}&longitude={LON}&" |
| 48 | +URL += "daily=weather_code,temperature_2m_max,temperature_2m_min" |
| 49 | +URL += ",sunrise,sunset" |
| 50 | +URL += "&timeformat=unixtime" |
| 51 | +URL += f"&timezone={TMZ}" |
| 52 | +gc.collect() |
| 53 | +resp_data = requests.get(URL) |
| 54 | +#resp_data = get_forecast() |
| 55 | +print("got url") |
| 56 | +forecast_data = resp_data.json() |
| 57 | + |
| 58 | +# ---------------------------- |
| 59 | +# Define various assets |
| 60 | +# ---------------------------- |
| 61 | +gc.collect() |
| 62 | +font_file = "/fonts/Arial-Bold-24.bdf" |
| 63 | +font = bitmap_font.load_font(font_file) |
| 64 | +BACKGROUND_BMP = "/bmps/weather_bg_vert.bmp" |
| 65 | +ICONS_LARGE_FILE = "/bmps/weather-icons.bmp" |
| 66 | +DAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") |
| 67 | +MONTHS = ( |
| 68 | + "January", |
| 69 | + "February", |
| 70 | + "March", |
| 71 | + "April", |
| 72 | + "May", |
| 73 | + "June", |
| 74 | + "July", |
| 75 | + "August", |
| 76 | + "September", |
| 77 | + "October", |
| 78 | + "November", |
| 79 | + "December", |
| 80 | +) |
| 81 | + |
| 82 | +# Weather Code Information from https://open-meteo.com/en/docs |
| 83 | +# Code Description |
| 84 | +# 0 Clear sky |
| 85 | +# 1, 2, 3 Mainly clear, partly cloudy, and overcast |
| 86 | +# 45, 48 Fog and depositing rime fog |
| 87 | +# 51, 53, 55 Drizzle: Light, moderate, and dense intensity |
| 88 | +# 56, 57 Freezing Drizzle: Light and dense intensity |
| 89 | +# 61, 63, 65 Rain: Slight, moderate and heavy intensity |
| 90 | +# 66, 67 Freezing Rain: Light and heavy intensity |
| 91 | +# 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity |
| 92 | +# 77 Snow grains |
| 93 | +# 80, 81, 82 Rain showers: Slight, moderate, and violent |
| 94 | +# 85, 86 Snow showers slight and heavy |
| 95 | +# 95 * Thunderstorm: Slight or moderate |
| 96 | +# 96, 99 * Thunderstorm with slight and heavy hail |
| 97 | + |
| 98 | +# Map the above WMO codes to index of icon in 3x3 spritesheet |
| 99 | +WMO_CODE_TO_ICON = ( |
| 100 | + (0,), # 0 = sunny |
| 101 | + (1,), # 1 = partly sunny/cloudy |
| 102 | + (2, 3, 45, 48,), # 2 = cloudy/very cloudy/fog |
| 103 | + (61, 63, 65, 51, 53, 55, 80, 81, 82), # 4 = rain/showers |
| 104 | + (95, 96, 99), # 6 = storms |
| 105 | + (56, 57, 66, 67, 71, 73, 75, 77, 85, 86), # 7 = snow |
| 106 | +) |
| 107 | + |
| 108 | +# ---------------------------- |
| 109 | +# Backgrounnd bitmap |
| 110 | +# ---------------------------- |
| 111 | +gc.collect() |
| 112 | +splash = displayio.Group() |
| 113 | +bitmap = displayio.OnDiskBitmap(BACKGROUND_BMP) |
| 114 | +tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) |
| 115 | +splash.append(tile_grid) |
| 116 | +display.root_group = splash |
| 117 | +print("got background") |
| 118 | + |
| 119 | +# ---------------------------- |
| 120 | +# Weather icons sprite sheet |
| 121 | +# ---------------------------- |
| 122 | +gc.collect() |
| 123 | +icons_large_bmp, icons_large_pal = adafruit_imageload.load(ICONS_LARGE_FILE) |
| 124 | +print("got icon sheet") |
| 125 | + |
| 126 | +# ///////////////////////////////////////////////////////////////////////// |
| 127 | +# helper functions |
| 128 | + |
| 129 | +def temperature_text(tempC): |
| 130 | + if METRIC: |
| 131 | + return "{:3.0f}C".format(tempC) |
| 132 | + else: |
| 133 | + return "{:3.0f}F".format(32.0 + 1.8 * tempC) |
| 134 | + |
| 135 | + |
| 136 | +def update_today(data): |
| 137 | + """Update today weather info.""" |
| 138 | + # date text |
| 139 | + s = data["daily"]["time"][0] + data["utc_offset_seconds"] |
| 140 | + t = time.localtime(s) |
| 141 | + today_day.text = "{}".format( |
| 142 | + DAYS[t.tm_wday].upper()) |
| 143 | + print(today_day.text) |
| 144 | + today_date.text = "{} {}, {}".format( |
| 145 | + MONTHS[t.tm_mon - 1].upper(), t.tm_mday, t.tm_year |
| 146 | + ) |
| 147 | + # weather icon |
| 148 | + w = data["daily"]["weather_code"][0] |
| 149 | + today_icon[0] = next(i for i, t in enumerate(WMO_CODE_TO_ICON) if w in t) |
| 150 | + # temperatures |
| 151 | + today_temp.text = f"H: {temperature_text(data['daily']['temperature_2m_max'][0])} " |
| 152 | + today_temp.text += f"L: {temperature_text(data['daily']['temperature_2m_min'][0])}" |
| 153 | + # sunrise/set |
| 154 | + sr = time.localtime(data["daily"]["sunrise"][0] + data["utc_offset_seconds"]) |
| 155 | + ss = time.localtime(data["daily"]["sunset"][0] + data["utc_offset_seconds"]) |
| 156 | + today_sunrise.text = "{:2d}:{:02d} AM".format(sr.tm_hour, sr.tm_min) |
| 157 | + today_sunset.text = "{:2d}:{:02d} PM".format(ss.tm_hour - 12, ss.tm_min) |
| 158 | + |
| 159 | +# =========== |
| 160 | +# U I |
| 161 | +# =========== |
| 162 | +print("making ui") |
| 163 | +today_day = label.Label(font, text="?" * 30, color=0x000000) |
| 164 | +today_day.anchor_point = (0.5, 0) |
| 165 | +today_day.anchored_position = (DISPLAY_WIDTH / 2, 106) |
| 166 | +today_date = label.Label(font, text="?" * 30, color=0x000000) |
| 167 | +today_date.anchor_point = (0.5, 0) |
| 168 | +today_date.anchored_position = (DISPLAY_WIDTH / 2, 140) |
| 169 | + |
| 170 | +location_name = label.Label(font, color=0x000000) |
| 171 | +if CITY: |
| 172 | + location_name.text = f"{CITY[:16]}" |
| 173 | +else: |
| 174 | + location_name.text = f"({LAT},{LON})" |
| 175 | +location_name.anchor_point = (0.5, 0) |
| 176 | +location_name.anchored_position = (DISPLAY_WIDTH / 2, 210) |
| 177 | + |
| 178 | +today_icon = displayio.TileGrid( |
| 179 | + icons_large_bmp, |
| 180 | + pixel_shader=icons_large_pal, |
| 181 | + x=203, |
| 182 | + y=275, |
| 183 | + width=1, |
| 184 | + height=1, |
| 185 | + tile_width=74, |
| 186 | + tile_height=74, |
| 187 | +) |
| 188 | +today_icon.x = int(DISPLAY_WIDTH / 2 - today_icon.tile_width / 2) |
| 189 | + |
| 190 | +today_temp = label.Label(font, text="H: +100F", color=0x000000) |
| 191 | +today_temp.anchor_point = (0, 0) |
| 192 | +today_temp.anchored_position = (163, 415) |
| 193 | + |
| 194 | +today_sunrise = label.Label(font, text="12:12 PM", color=0x000000) |
| 195 | +today_sunrise.anchor_point = (0, 0) |
| 196 | +today_sunrise.anchored_position = (202, 520) |
| 197 | + |
| 198 | +today_sunset = label.Label(font, text="12:12 PM", color=0x000000) |
| 199 | +today_sunset.anchor_point = (0, 0) |
| 200 | +today_sunset.anchored_position = (202, 614) |
| 201 | +today_banner = displayio.Group() |
| 202 | +today_banner.append(today_day) |
| 203 | +today_banner.append(today_date) |
| 204 | +today_banner.append(location_name) |
| 205 | +today_banner.append(today_icon) |
| 206 | +today_banner.append(today_temp) |
| 207 | +today_banner.append(today_sunrise) |
| 208 | +today_banner.append(today_sunset) |
| 209 | + |
| 210 | +display.root_group.append(today_banner) |
| 211 | + |
| 212 | +# =========== |
| 213 | +# M A I N |
| 214 | +# =========== |
| 215 | +gc.collect() |
| 216 | +print("Updating...") |
| 217 | +update_today(forecast_data) |
| 218 | + |
| 219 | +print("Refreshing...") |
| 220 | +time.sleep(display.time_to_refresh + 1) |
| 221 | +display.refresh() |
| 222 | +time.sleep(display.time_to_refresh + 1) |
| 223 | + |
| 224 | +print("Sleeping...") |
| 225 | +wake_alarm = alarm.wake_alarm |
| 226 | +pin_alarm = alarm.pin.PinAlarm(pin=board.BUTTON, value=False, pull=True) |
| 227 | +alarm.exit_and_deep_sleep_until_alarms(pin_alarm) |
| 228 | +# entire code will run again |
0 commit comments