From cc5cbcca681a4d0224db754c7a34f73b361f62f5 Mon Sep 17 00:00:00 2001 From: caternuson Date: Mon, 7 Oct 2024 10:09:57 -0700 Subject: [PATCH 1/2] openmeteo magtag weather update --- .../openmeteo/bmps/weather_bg.bmp | Bin 0 -> 5250 bytes .../openmeteo/bmps/weather_icons_20px.bmp | Bin 0 -> 2058 bytes .../openmeteo/bmps/weather_icons_70px.bmp | Bin 0 -> 22818 bytes MagTag/MagTag_Weather/openmeteo/code.py | 319 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100755 MagTag/MagTag_Weather/openmeteo/bmps/weather_bg.bmp create mode 100755 MagTag/MagTag_Weather/openmeteo/bmps/weather_icons_20px.bmp create mode 100755 MagTag/MagTag_Weather/openmeteo/bmps/weather_icons_70px.bmp create mode 100644 MagTag/MagTag_Weather/openmeteo/code.py diff --git a/MagTag/MagTag_Weather/openmeteo/bmps/weather_bg.bmp b/MagTag/MagTag_Weather/openmeteo/bmps/weather_bg.bmp new file mode 100755 index 0000000000000000000000000000000000000000..1fa88ebe765aa948b7d93bf5e20bac26becb413e GIT binary patch literal 5250 zcmeI0zi-n(6vq>S6SlOa&JlDAQ9%$}ES|m}2atIaI38N&!sJp9ApaTn@(l2U31F zr2?1K$EIIWOBC&Ie^S3THP_DJ#jWTYMr_4Ubyle(A{u;n6pt^NB;2g3A z+!U?{Fl_}8wzd9{`-t2C!aG_H;~FsC?82<2<!nmb%EhB!YxA@nPVX1 z=&egdJ;IL~)=|{S@Qia!;%>hdTmXJ(SldTg&T%5sBE)KTT*~SoKKK*A->hUgVn^l) zAt%i}kJnWu;EzW3MwY`mO=Q}H*v)+pSe+)MzCW@1Tk}c;pD5OSB7<{=yUpr8aZPaJ zRNV;8SV!8BT#MC_pvO4-$>k!4b*Y5g_eQ`t;M`5u%p?Yq`E@+3hH-A>+l}fOUE*r? xjjG+;b1CQI%EN(0di_;gO>tX>MSFF!!k?kIXQT@rS>jUn@%g3ttK=VWzX1#v=$ill literal 0 HcmV?d00001 diff --git a/MagTag/MagTag_Weather/openmeteo/bmps/weather_icons_20px.bmp b/MagTag/MagTag_Weather/openmeteo/bmps/weather_icons_20px.bmp new file mode 100755 index 0000000000000000000000000000000000000000..aa7faae9c8710fd8460345b28d02ceacfebf006c GIT binary patch literal 2058 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 literal 0 HcmV?d00001 diff --git a/MagTag/MagTag_Weather/openmeteo/code.py b/MagTag/MagTag_Weather/openmeteo/code.py new file mode 100644 index 000000000..597d2e2a6 --- /dev/null +++ b/MagTag/MagTag_Weather/openmeteo/code.py @@ -0,0 +1,319 @@ +# SPDX-FileCopyrightText: 2024 Carter Nelson for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=redefined-outer-name, eval-used, wrong-import-order + +import time +import terminalio +import displayio +import adafruit_imageload +from adafruit_display_text import label +from adafruit_magtag.magtag import MagTag +# needed for NTP +import wifi +import socketpool +import adafruit_ntp + +# --| USER CONFIG |-------------------------- +LAT = 47.6 # latitude +LON = -122.3 # longitude +TMZ = "America/Los_Angeles" # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +METRIC = False # set to True for metric units +# ------------------------------------------- + +# ---------------------------- +# Define various assets +# ---------------------------- +BACKGROUND_BMP = "/bmps/weather_bg.bmp" +ICONS_LARGE_FILE = "/bmps/weather_icons_70px.bmp" +ICONS_SMALL_FILE = "/bmps/weather_icons_20px.bmp" +DAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") +MONTHS = ( + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +) + +# Weather Code Information from https://open-meteo.com/en/docs +# Code Description +# 0 Clear sky +# 1, 2, 3 Mainly clear, partly cloudy, and overcast +# 45, 48 Fog and depositing rime fog +# 51, 53, 55 Drizzle: Light, moderate, and dense intensity +# 56, 57 Freezing Drizzle: Light and dense intensity +# 61, 63, 65 Rain: Slight, moderate and heavy intensity +# 66, 67 Freezing Rain: Light and heavy intensity +# 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity +# 77 Snow grains +# 80, 81, 82 Rain showers: Slight, moderate, and violent +# 85, 86 Snow showers slight and heavy +# 95 * Thunderstorm: Slight or moderate +# 96, 99 * Thunderstorm with slight and heavy hail + +# Map the above WMO codes to index of icon in 3x3 spritesheet +WMO_CODE_TO_ICON = ( + (0,), # 0 = sunny + (1,), # 1 = partly sunny/cloudy + (2,), # 2 = cloudy + (3,), # 3 = very cloudy + (61, 63, 65), # 4 = rain + (51, 53, 55, 80, 81, 82), # 5 = showers + (95, 96, 99), # 6 = storms + (56, 57, 66, 67, 71, 73, 75, 77, 85, 86), # 7 = snow + (45, 48), # 8 = fog and stuff +) + +magtag = MagTag() + +# ---------------------------- +# Backgrounnd bitmap +# ---------------------------- +magtag.graphics.set_background(BACKGROUND_BMP) + +# ---------------------------- +# Weather icons sprite sheet +# ---------------------------- +icons_large_bmp, icons_large_pal = adafruit_imageload.load(ICONS_LARGE_FILE) +icons_small_bmp, icons_small_pal = adafruit_imageload.load(ICONS_SMALL_FILE) + +# ///////////////////////////////////////////////////////////////////////// +# helper functions + +def get_forecast(): + URL = f"https://api.open-meteo.com/v1/forecast?latitude={LAT}&longitude={LON}&" + URL += "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_direction_10m_dominant" + URL += "&timeformat=unixtime" + URL += f"&timezone={TMZ}" + resp = magtag.network.fetch(URL) + return resp.json() + + +def make_banner(x=0, y=0): + """Make a single future forecast info banner group.""" + day_of_week = label.Label(terminalio.FONT, text="DAY", color=0x000000) + day_of_week.anchor_point = (0, 0.5) + day_of_week.anchored_position = (0, 10) + + icon = displayio.TileGrid( + icons_small_bmp, + pixel_shader=icons_small_pal, + x=25, + y=0, + width=1, + height=1, + tile_width=20, + tile_height=20, + ) + + day_temp = label.Label(terminalio.FONT, text="+100F", color=0x000000) + day_temp.anchor_point = (0, 0.5) + day_temp.anchored_position = (50, 10) + + group = displayio.Group(x=x, y=y) + group.append(day_of_week) + group.append(icon) + group.append(day_temp) + + return group + + +def temperature_text(tempC): + if METRIC: + return "{:3.0f}C".format(tempC) + else: + return "{:3.0f}F".format(32.0 + 1.8 * tempC) + + +def wind_text(speedkmh, direction): + wind_dir = "N" + if direction < 337: + wind_dir = "NW" + if direction < 293: + wind_dir = "W" + if direction < 247: + wind_dir = "SW" + if direction < 202: + wind_dir = "S" + if direction < 157: + wind_dir = "SE" + if direction < 112: + wind_dir = "E" + if direction < 67: + wind_dir = "NE" + if direction < 22: + wind_dir = "N" + + wtext = f"from {wind_dir} " + + if METRIC: + wtext += "{:2.0f}kmh".format(speedkmh) + else: + wtext += "{:2.0f}mph".format(0.621371 * speedkmh) + + return wtext + + +def update_today(data): + """Update today weather info.""" + # date text + s = data["daily"]["time"][0] + data["utc_offset_seconds"] + t = time.localtime(s) + today_date.text = "{} {} {}, {}".format( + DAYS[t.tm_wday].upper(), + MONTHS[t.tm_mon - 1].upper(), + t.tm_mday, + t.tm_year + ) + # weather icon + w = data["daily"]["weather_code"][0] + today_icon[0] = next(i for i, t in enumerate(WMO_CODE_TO_ICON) if w in t) + # temperatures + today_low_temp.text = temperature_text(data["daily"]["temperature_2m_min"][0]) + today_high_temp.text = temperature_text(data["daily"]["temperature_2m_max"][0]) + # wind + s = data["daily"]["wind_speed_10m_max"][0] + d = data["daily"]["wind_direction_10m_dominant"][0] + today_wind.text = wind_text(s, d) + # sunrise/set + sr = time.localtime(data["daily"]["sunrise"][0] + data["utc_offset_seconds"]) + ss = time.localtime(data["daily"]["sunset"][0] + data["utc_offset_seconds"]) + today_sunrise.text = "{:2d}:{:02d} AM".format(sr.tm_hour, sr.tm_min) + today_sunset.text = "{:2d}:{:02d} PM".format(ss.tm_hour - 12, ss.tm_min) + + +def update_future(data): + """Update the future forecast info.""" + for i, banner in enumerate(future_banners): + # day of week + s = data["daily"]["time"][i+1] + data["utc_offset_seconds"] + t = time.localtime(s) + banner[0].text = DAYS[t.tm_wday][:3].upper() + # weather icon + w = data["daily"]["weather_code"][i+1] + banner[1][0] = next(x for x, t in enumerate(WMO_CODE_TO_ICON) if w in t) + # temperature + t = data["daily"]["temperature_2m_max"][i+1] + banner[2].text = temperature_text(t) + + +def get_ntp_time(offset): + """Use NTP to get current local time.""" + pool = socketpool.SocketPool(wifi.radio) + ntp = adafruit_ntp.NTP(pool, tz_offset=offset) + if ntp: + return ntp.datetime + else: + return None + + +def go_to_sleep(current_time): + """Enter deep sleep for time needed.""" + # compute current time offset in seconds + hour = current_time.tm_hour + minutes = current_time.tm_min + seconds = current_time.tm_sec + seconds_since_midnight = 60 * (hour * 60 + minutes) + seconds + three_fifteen = (3 * 60 + 15) * 60 + # wake up 15 minutes after 3am + seconds_to_sleep = (24 * 60 * 60 - seconds_since_midnight) + three_fifteen + print( + "Sleeping for {} hours, {} minutes".format( + seconds_to_sleep // 3600, (seconds_to_sleep // 60) % 60 + ) + ) + magtag.exit_and_deep_sleep(seconds_to_sleep) + + +# =========== +# U I +# =========== +today_date = label.Label(terminalio.FONT, text="?" * 30, color=0x000000) +today_date.anchor_point = (0, 0) +today_date.anchored_position = (15, 14) + +location_name = label.Label(terminalio.FONT, text=f"({LAT},{LON})", color=0x000000) +location_name.anchor_point = (0, 0) +location_name.anchored_position = (15, 25) + +today_icon = displayio.TileGrid( + icons_large_bmp, + pixel_shader=icons_small_pal, + x=10, + y=40, + width=1, + height=1, + tile_width=70, + tile_height=70, +) + +today_low_temp = label.Label(terminalio.FONT, text="+100F", color=0x000000) +today_low_temp.anchor_point = (0.5, 0) +today_low_temp.anchored_position = (122, 60) + +today_high_temp = label.Label(terminalio.FONT, text="+100F", color=0x000000) +today_high_temp.anchor_point = (0.5, 0) +today_high_temp.anchored_position = (162, 60) + +today_wind = label.Label(terminalio.FONT, text="99m/s", color=0x000000) +today_wind.anchor_point = (0, 0.5) +today_wind.anchored_position = (110, 95) + +today_sunrise = label.Label(terminalio.FONT, text="12:12 PM", color=0x000000) +today_sunrise.anchor_point = (0, 0.5) +today_sunrise.anchored_position = (45, 117) + +today_sunset = label.Label(terminalio.FONT, text="12:12 PM", color=0x000000) +today_sunset.anchor_point = (0, 0.5) +today_sunset.anchored_position = (130, 117) + +today_banner = displayio.Group() +today_banner.append(today_date) +today_banner.append(location_name) +today_banner.append(today_icon) +today_banner.append(today_low_temp) +today_banner.append(today_high_temp) +today_banner.append(today_wind) +today_banner.append(today_sunrise) +today_banner.append(today_sunset) + +future_banners = [ + make_banner(x=210, y=18), + make_banner(x=210, y=39), + make_banner(x=210, y=60), + make_banner(x=210, y=81), + make_banner(x=210, y=102), +] + +magtag.splash.append(today_banner) +for future_banner in future_banners: + magtag.splash.append(future_banner) + +# =========== +# M A I N +# =========== +print("Fetching forecast...") +forecast_data = get_forecast() + +print("Updating...") +update_today(forecast_data) +update_future(forecast_data) + +print("Refreshing...") +time.sleep(magtag.display.time_to_refresh + 1) +magtag.display.refresh() +time.sleep(magtag.display.time_to_refresh + 1) + +print("Sleeping...") +go_to_sleep(get_ntp_time(forecast_data["utc_offset_seconds"]/3600)) +# entire code will run again after deep sleep cycle +# similar to hitting the reset button + From e5f204213cf6bcf60977be9027331f28edfacda3 Mon Sep 17 00:00:00 2001 From: caternuson Date: Mon, 7 Oct 2024 10:18:51 -0700 Subject: [PATCH 2/2] lint --- MagTag/MagTag_Weather/openmeteo/code.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MagTag/MagTag_Weather/openmeteo/code.py b/MagTag/MagTag_Weather/openmeteo/code.py index 597d2e2a6..6acb1213e 100644 --- a/MagTag/MagTag_Weather/openmeteo/code.py +++ b/MagTag/MagTag_Weather/openmeteo/code.py @@ -90,7 +90,8 @@ def get_forecast(): URL = f"https://api.open-meteo.com/v1/forecast?latitude={LAT}&longitude={LON}&" - URL += "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_direction_10m_dominant" + URL += "daily=weather_code,temperature_2m_max,temperature_2m_min" + URL += ",sunrise,sunset,wind_speed_10m_max,wind_direction_10m_dominant" URL += "&timeformat=unixtime" URL += f"&timezone={TMZ}" resp = magtag.network.fetch(URL) @@ -158,7 +159,6 @@ def wind_text(speedkmh, direction): wtext += "{:2.0f}kmh".format(speedkmh) else: wtext += "{:2.0f}mph".format(0.621371 * speedkmh) - return wtext @@ -316,4 +316,3 @@ def go_to_sleep(current_time): go_to_sleep(get_ntp_time(forecast_data["utc_offset_seconds"]/3600)) # entire code will run again after deep sleep cycle # similar to hitting the reset button -