1
0
mirror of https://github.com/elisspace/weatherdisplay.git synced 2026-08-29 15:44:11 +00:00
Files
weatherdisplay/TideTracker.py
Eli 3351142e8b Update TideTracker.py
Handle empty rows with pandas `dropna`
2025-03-05 17:10:46 -05:00

690 lines
23 KiB
Python

'''
****************************************************************
****************************************************************
TideTracker for E-Ink Display
based on work by Sam Baker
****************************************************************
****************************************************************
'''
from datetime import datetime
from PIL import ImageFont
import sys
import os
import time
import traceback
import requests, json
from io import BytesIO
import noaa_coops as nc
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
import datetime as dt
import pandas as pd
sys.path.append('lib')
from waveshare_epd import epd4in26
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, timedelta
picdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'images')
icondir = os.path.join(picdir, 'icon')
fontdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'font')
'''
****************************************************************
Location specific info required
****************************************************************
'''
# Optional, displayed on top left
LOCATION = 'New York City'
# NOAA Station Code for tide data
StationID = 12345 # get station ID from NOAA
# For weather data
# Create Account on openweathermap.com and get API key
API_KEY = '<insert API key here>'
# Get LATITUDE and LONGITUDE of location
LATITUDE = '<insert latitude here>'
LONGITUDE = '<insert long here>'
UNITS = 'imperial'
# Create URL for API call
BASE_URL = 'http://api.openweathermap.org/data/3.0/onecall?'
URL = BASE_URL + 'lat=' + LATITUDE + '&lon=' + LONGITUDE + '&units=' + UNITS +'&appid=' + API_KEY
'''
****************************************************************
Functions and defined variables
****************************************************************
'''
from PIL import ImageFont
def get_text_dimensions(text_string, font):
# https://stackoverflow.com/a/46220683/9263761
ascent, descent = font.getmetrics()
text_width = font.getmask(text_string).getbbox()[2]
text_height = font.getmask(text_string).getbbox()[3] + descent
return (text_width, text_height)
# define funciton for writing image and sleeping for specified time
def write_to_screen(image, sleep_seconds):
print('Writing to screen.') # for debugging
# Create new blank image template matching screen resolution
h_image = Image.new('1', (epd.width, epd.height), 255)
# Open the template
screen_output_file = Image.open(os.path.join(picdir, image))
# Initialize the drawing context with template as background
h_image.paste(screen_output_file, (0, 0))
epd.display(epd.getbuffer(h_image))
# Sleep
epd.sleep() # Put screen to sleep to prevent damage
print('Sleeping for ' + str(sleep_seconds) +'.')
time.sleep(sleep_seconds) # Determines refresh rate on data
epd.init() # Re-Initialize screen
import requests
def test_noaa_api(station_id):
url = f"https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?begin_date=20240720&end_date=20240721&station={station_id}&product=water_level&datum=MLLW&time_zone=lst_ldt&units=english&format=json"
response = requests.get(url)
print(f"NOAA API Test Response Status: {response.status_code}")
print(f"NOAA API Test Response Content: {response.text[:200]}...") # Print first 200 characters
# define function for displaying error
def display_error(error_source):
# Display an error
print('Error in the', error_source, 'request.')
# Initialize drawing
error_image = Image.new('1', (epd.width, epd.height), 255)
# Initialize the drawing
draw = ImageDraw.Draw(error_image)
draw.text((100, 150), error_source +' ERROR', font=font50, fill=black)
draw.text((100, 300), 'Retrying in 30 seconds', font=font22, fill=black)
current_time = datetime.now().strftime('%H:%M')
draw.text((300, 365), 'Last Refresh: ' + str(current_time), font = font50, fill=black)
# Save the error image
error_image_file = 'error.png'
error_image.save(os.path.join(picdir, error_image_file))
# Close error image
error_image.close()
# Write error to screen
write_to_screen(error_image_file, 30)
# define function for getting weather data
def getWeather(URL):
# Ensure there are no errors with connection
error_connect = True
while error_connect == True:
try:
# HTTP request
print('Attempting to connect to OWM.')
response = requests.get(URL)
print('Connection to OWM successful.')
error_connect = None
except:
# Call function to display connection error
print('Connection error.')
display_error('CONNECTION')
# Check status of code request
if response.status_code == 200:
print('Connection to Open Weather successful.')
# get data in jason format
data = response.json()
with open('data.txt', 'w') as outfile:
json.dump(data, outfile)
return data
else:
# Call function to display HTTP error
display_error('HTTP')
def generate_summary(wind_speed, temp_current, precip):
# Define thresholds
WIND_THRESHOLD_HIGH = 15 # example threshold for too windy (in MPH)
TIDE_THRESHOLD_HIGH = 5.0 # example threshold for high tide (in feet)
TEMP_THRESHOLD_LOW = 70 # example for too cold (in Fahrenheit)
TEMP_THRESHOLD_HIGH = 90 # example for too hot (in Fahrenheit)
PRECIP_THRESHOLD = 15 # example for high precipitation (in percentage)
# Logic for generating the summary
summary = []
if wind_speed > WIND_THRESHOLD_HIGH:
summary.append("Too windy")
#if tide_level > TIDE_THRESHOLD_HIGH:
# summary.append("Tide too high")
if temp_current < TEMP_THRESHOLD_LOW:
summary.append("Too cold")
elif temp_current > TEMP_THRESHOLD_HIGH:
summary.append("Too hot")
if daily_precip_percent > PRECIP_THRESHOLD:
summary.append("Too much rain")
# Check for "just right" conditions
if not summary:
summary.append("Just right")
return ", ".join(summary)
# last 24 hour data, add argument for start/end_date
def past24(StationID):
try:
# Create Station Object
stationdata = nc.Station(StationID)
# Get today date string
today = dt.datetime.now()
todaystr = today.strftime("%Y%m%d %H:%M")
# Get yesterday date string
yesterday = today - dt.timedelta(days=1)
yesterdaystr = yesterday.strftime("%Y%m%d %H:%M")
print(f"Requesting tide data from {yesterdaystr} to {todaystr}")
# Get water level data
WaterLevel = stationdata.get_data(
begin_date=yesterdaystr,
end_date=todaystr,
product="water_level",
datum="MLLW",
time_zone="lst_ldt")
print("Raw API response:")
print(WaterLevel)
if isinstance(WaterLevel, str):
print("API returned a string instead of JSON. Content:")
print(WaterLevel)
raise ValueError("Invalid API response")
WaterLevel['v'] = WaterLevel['v'].astype(float)
print("WaterLevel data structure:")
print(WaterLevel.columns)
print(WaterLevel.head())
return WaterLevel
except Exception as e:
print(f"Error in past24: {str(e)}")
print(f"Error type: {type(e).__name__}")
print(f"Error args: {e.args}")
raise
def get_tide_data(station_id):
try:
# Get today and yesterday's date
today = datetime.now()
yesterday = today - timedelta(days=1)
# Format the URL
url = f"https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?begin_date={yesterday.strftime('%Y%m%d')}&end_date={today.strftime('%Y%m%d')}&station={station_id}&product=water_level&datum=MLLW&time_zone=lst_ldt&units=english&format=json"
# Make the request
response = requests.get(url)
response.raise_for_status()
# Parse the JSON response
data = response.json()
# Convert to DataFrame
df = pd.DataFrame(data['data'])
# Convert 't' to datetime and set as index
df['t'] = pd.to_datetime(df['t'])
df.set_index('t', inplace=True)
# Convert 'v' to float
df['v'] = pd.to_numeric(df['v'], errors='coerce')
df.dropna(subset=['v'], inplace=True) #double check spacing here, weird copy/paste issue
print("WaterLevel data structure:")
print(df.head())
print(df.dtypes)
return df
except Exception as e:
print(f"Error in get_tide_data: {str(e)}")
raise
def test_noaa_api_direct(station_id):
today = dt.datetime.now()
yesterday = today - dt.timedelta(days=1)
url = f"https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?begin_date={yesterday.strftime('%Y%m%d')}&end_date={today.strftime('%Y%m%d')}&station={station_id}&product=water_level&datum=MLLW&time_zone=lst_ldt&units=english&format=json"
try:
response = requests.get(url)
print(f"Direct NOAA API Response Status: {response.status_code}")
print(f"Direct NOAA API Response Content: {response.text[:500]}...") # Print first 500 characters
except Exception as e:
print(f"Error in direct NOAA API request: {str(e)}")
# Plot last 24 hours of tide
def plotTide(TideData):
water_level_column = 'v'
# Ensure the index is datetime
if not isinstance(TideData.index, pd.DatetimeIndex):
TideData.index = pd.to_datetime(TideData.index)
# Filter data to include only the last 12 hours
end_time = TideData.index.max()
start_time = end_time - timedelta(hours=12)
TideData = TideData.loc[start_time:end_time]
# Adjust data for negative values
minlevel = TideData[water_level_column].min()
TideData[water_level_column] = TideData[water_level_column].astype(float) - minlevel
# Create Plot - adjust figure size to match your e-ink display dimensions
fig, axs = plt.subplots(figsize=(8, 3)) # Adjust these values as needed
# Adjust subplot parameters
plt.subplots_adjust(left=0.00, right=0.95, top=0.9, bottom=0.2)
# Convert datetime to matplotlib date numbers
dates = mdates.date2num(TideData.index.to_pydatetime())
# Plot using matplotlib's plot function
axs.fill_between(dates, 0, TideData[water_level_column], color='black', alpha=0.1)
axs.plot(dates, TideData[water_level_column], color='black', linewidth=2)
# Add vertical line for current time
current_time = datetime.now()
axs.axvline(x=mdates.date2num(current_time), color='black', linestyle='--', linewidth=4)
# Format x-axis to show only hours
axs.xaxis.set_major_formatter(mdates.DateFormatter('%H'))
axs.xaxis.set_major_locator(mdates.HourLocator(interval=6))
# Remove top and right spines
axs.spines['top'].set_visible(False)
axs.spines['right'].set_visible(False)
# Add labels
#plt.ylabel('Tide (ft)', fontsize=8)
#plt.xlabel('Hour', fontsize=8)
# Increase tick label font size
axs.tick_params(axis='both', which='major', labelsize=8)
# Tight layout
plt.tight_layout()
# Save and close
plt.savefig('images/TideLevel.png', dpi=80, bbox_inches='tight', pad_inches=0.0)
plt.close(fig) # Close the figure to free up memory
def get_hilo_data(station_id):
try:
# Get today and tomorrow's date
today = dt.datetime.now()
tomorrow = today + dt.timedelta(days=1)
# Format the URL
url = f"https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?begin_date={today.strftime('%Y%m%d')}&end_date={tomorrow.strftime('%Y%m%d')}&station={station_id}&product=predictions&datum=MLLW&interval=hilo&time_zone=lst_ldt&units=english&format=json"
# Make the request
response = requests.get(url)
response.raise_for_status() # Raise an exception for bad status codes
# Parse the JSON response
data = response.json()
# Convert to DataFrame
df = pd.DataFrame(data['predictions'])
# Convert 't' to datetime and set as index
df['t'] = pd.to_datetime(df['t'])
df.set_index('t', inplace=True)
print("HiLo data structure:")
print(df.columns)
print(df.head())
return df
except Exception as e:
print(f"Error in get_hilo_data: {str(e)}")
print(f"Error type: {type(e).__name__}")
print(f"Error args: {e.args}")
raise
def HiLo(StationID): # trying to replace this with get_hilo_data
try:
# Create Station Object
stationdata = nc.Station(StationID)
# Get today date string
today = dt.datetime.now()
todaystr = today.strftime("%Y%m%d")
# Get tomorrow date string
tomorrow = today + dt.timedelta(days=1)
tomorrowstr = tomorrow.strftime("%Y%m%d")
print(f"Requesting tide prediction data from {todaystr} to {tomorrowstr}")
# Get Hi and Lo Tide info
TideHiLo = stationdata.get_data(
begin_date=todaystr,
end_date=tomorrowstr,
product="predictions",
datum="MLLW",
interval="hilo",
time_zone="lst_ldt")
print("TideHiLo data structure:")
print(TideHiLo.columns)
print(TideHiLo.head())
# Print the first few rows of data
print("First few rows of TideHiLo data:")
print(TideHiLo.head().to_string())
print("Data types of TideHiLo columns:")
print(TideHiLo.dtypes)
return TideHiLo
except Exception as e:
print(f"Error in HiLo: {str(e)}")
print(f"Error type: {type(e).__name__}")
print(f"Error args: {e.args}")
raise
# Set the font sizes
font15 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 15)
font20 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 20)
font22 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 22)
font30 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 30)
font35 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 35)
font50 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 50)
font60 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 60)
font100 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 100)
font160 = ImageFont.truetype(os.path.join(fontdir, 'Font.ttc'), 160)
# Set the colors
black = 'rgb(0,0,0)'
white = 'rgb(255,255,255)'
grey = 'rgb(235,235,235)'
'''
****************************************************************
Main Loop
****************************************************************
'''
# Initialize and clear screen
print('Initializing and clearing screen.')
epd = epd4in26.EPD() # Create object for display functions
epd.init()
epd.Clear()
while True:
# test_noaa_api(StationID) #testing for Claude
# try:
# WaterLevel = get_tide_data(StationID)
# print("Water Level Data:")
# print(WaterLevel.head())
# print(WaterLevel.dtypes)
# plotTide(WaterLevel)
# except Exception as e:
# print(f"Error: {str(e)}")
# print(f"Error type: {type(e).__name__}")
# print(f"Error args: {e.args}")
# Get weather data
data = getWeather(URL)
# get current dict block
current = data['current']
# get current
temp_current = current['temp']
# get feels like
feels_like = current['feels_like']
# get wind speed
wind_speed = current['wind_speed']
# get humidity
humidity = current['humidity']
# get pressure
wind = current['wind_speed']
# get description
weather = current['weather']
report = weather[0]['description']
# get icon url
icon_code = weather[0]['icon']
# get daily dict block
daily = data['daily']
# get daily precip
daily_precip_float = daily[0]['pop']
#format daily precip
daily_precip_percent = daily_precip_float * 100
# get min and max temp
daily_temp = daily[0]['temp']
temp_max = daily_temp['max']
temp_min = daily_temp['min']
# Generate simple text summary
summary = generate_summary(wind_speed, temp_current, daily_precip_percent)
print(summary)
# Set strings to be printed to screen
string_location = LOCATION
string_temp_current = format(temp_current, '.0f') + u'\N{DEGREE SIGN}F'
string_feels_like = 'Feels like: ' + format(feels_like, '.0f') + u'\N{DEGREE SIGN}F'
string_humidity = 'Humidity: ' + str(humidity) + '%'
string_wind = 'Wind: ' + format(wind, '.1f') + ' MPH'
string_report = 'Now: ' + report.title()
string_temp_max = 'High: ' + format(temp_max, '>.0f') + u'\N{DEGREE SIGN}F'
string_temp_min = 'Low: ' + format(temp_min, '>.0f') + u'\N{DEGREE SIGN}F'
string_precip_percent = 'Precip: ' + str(format(daily_precip_percent, '.0f')) + '%'
# get min and max temp
nx_daily_temp = daily[1]['temp']
nx_temp_max = nx_daily_temp['max']
nx_temp_min = nx_daily_temp['min']
# get daily precip
nx_daily_precip_float = daily[1]['pop']
#format daily precip
nx_daily_precip_percent = nx_daily_precip_float * 100
# get min and max temp
nx_nx_daily_temp = daily[2]['temp']
nx_nx_temp_max = nx_nx_daily_temp['max']
nx_nx_temp_min = nx_nx_daily_temp['min']
# get daily precip
nx_nx_daily_precip_float = daily[2]['pop']
#format daily precip
nx_nx_daily_precip_percent = nx_nx_daily_precip_float * 100
# Tomorrow Forcast Strings
nx_day_high = 'High: ' + format(nx_temp_max, '>.0f') + u'\N{DEGREE SIGN}F'
nx_day_low = 'Low: ' + format(nx_temp_min, '>.0f') + u'\N{DEGREE SIGN}F'
nx_precip_percent = 'Precip: ' + str(format(nx_daily_precip_percent, '.0f')) + '%'
nx_weather_icon = daily[1]['weather']
nx_icon = nx_weather_icon[0]['icon']
# Overmorrow Forcast Strings
nx_nx_day_high = 'High: ' + format(nx_nx_temp_max, '>.0f') + u'\N{DEGREE SIGN}F'
nx_nx_day_low = 'Low: ' + format(nx_nx_temp_min, '>.0f') + u'\N{DEGREE SIGN}F'
nx_nx_precip_percent = 'Precip: ' + str(format(nx_nx_daily_precip_percent, '.0f')) + '%'
nx_nx_weather_icon = daily[2]['weather']
nx_nx_icon = nx_nx_weather_icon[0]['icon']
# Last updated time
now = dt.datetime.now()
current_time = now.strftime("%H:%M")
last_update_string = 'Last Updated: ' + current_time
test_noaa_api_direct(StationID) #testing
# Tide Data
# Get water level
wl_error = True
while wl_error == True:
try:
WaterLevel = get_tide_data(StationID) #trying to replace past24
#WaterLevel = past24(StationID)
wl_error = False
except Exception as e:
print(f"Error retrieving tide data: {str(e)}")
display_error('Tide Data')
time.sleep(30) # Wait for 30 seconds before retrying
try:
plotTide(WaterLevel)
except Exception as e:
print(f"Error in plotTide: {str(e)}")
print(f"Error type: {type(e).__name__}")
print(f"Error args: {e.args}")
# Open template file
template = Image.open(os.path.join(picdir, 'template.png'))
# Initialize the drawing context with template as background
draw = ImageDraw.Draw(template)
# Current weather
## Open icon file
icon_file = icon_code + '.png'
icon_image = Image.open(os.path.join(icondir, icon_file))
icon_image = icon_image.resize((130,130))
template.paste(icon_image, (50, 50))
draw.text((125,10), LOCATION, font=font35, fill=black)
# Center current weather report
w, h = get_text_dimensions(string_report, font20)
#print(w)
if w > 250:
string_report = 'Now:\n' + report.title()
center = int(120-(w/2))
draw.text((center,175), string_report, font=font20, fill=black)
# Data
draw.text((250,55), string_temp_current, font=font35, fill=black)
y = 100
draw.text((250,y), string_feels_like, font=font15, fill=black)
draw.text((250,y+20), string_wind, font=font15, fill=black)
draw.text((250,y+40), string_precip_percent, font=font15, fill=black)
draw.text((250,y+60), string_temp_max, font=font15, fill=black)
draw.text((250,y+80), string_temp_min, font=font15, fill=black)
draw.text((125,218), last_update_string, font=font15, fill=black)
# Weather Forcast
# Tomorrow
icon_file = nx_icon + '.png'
icon_image = Image.open(os.path.join(icondir, icon_file))
icon_image = icon_image.resize((130,130))
template.paste(icon_image, (435, 50))
draw.text((450,20), 'Tomorrow', font=font22, fill=black)
draw.text((415,180), nx_day_high, font=font15, fill=black)
draw.text((515,180), nx_day_low, font=font15, fill=black)
draw.text((460,200), nx_precip_percent, font=font15, fill=black)
# Next Next Day Forcast
icon_file = nx_nx_icon + '.png'
icon_image = Image.open(os.path.join(icondir, icon_file))
icon_image = icon_image.resize((130,130))
template.paste(icon_image, (635, 50))
draw.text((625,20), 'Next-Next Day', font=font22, fill=black)
draw.text((615,180), nx_nx_day_high, font=font15, fill=black)
draw.text((715,180), nx_nx_day_low, font=font15, fill=black)
draw.text((660,200), nx_nx_precip_percent, font=font15, fill=black)
## Dividing lines
draw.line((400,10,400,220), fill='black', width=3)
draw.line((600,20,600,210), fill='black', width=2)
# Tide Info
# Graph
tidegraph = Image.open('images/TideLevel.png')
# template.paste(tidegraph, (125, 240)) Original Numbers
template.paste(tidegraph, (160, 250))
# Large horizontal dividing line
h = 240
draw.line((25, h, 775, h), fill='black', width=3)
# Daily tide times
draw.text((30,260), "Today's Tide", font=font22, fill=black)
# Get tide time predictions
hilo_error = True
while hilo_error == True:
try:
hilo_daily = get_hilo_data(StationID)
# hilo_daily = HiLo(StationID) #replaced with new function get_hilo_data
print("HiLo function completed successfully") # Claude troubleshooting
hilo_error = False
except:
print(f"Error in HiLo: {str(e)}")
display_error('Tide Prediction')
time.sleep(30) # Wait for 30 seconds before retrying
# Display tide preditions
y_loc = 300 # starting location of list
if 'type' in hilo_daily.columns:
# Iterate over predictions
current_time = datetime.now()
future_tides = hilo_daily[hilo_daily.index > current_time]
for index, row in future_tides.head(4).iterrows():
# For high tide
if row['type'] == 'H':
tide_time = index.strftime("%H:%M")
tidestr = "High: " + tide_time
# For low tide
elif row['type'] == 'L':
tide_time = index.strftime("%H:%M")
tidestr = "Low: " + tide_time
# Draw to display image
draw.text((40,y_loc), tidestr, font=font15, fill=black)
y_loc += 25 # This bumps the next prediction down a line
else:
print("'type' column not found in tide prediction data")
print("Available columns:", hilo_daily.columns)
# You might want to add some error handling or alternative display here
draw.text((40,y_loc), "Tide data unavailable", font=font15, fill=black)
# Save the image for display as PNG
screen_output_file = os.path.join(picdir, 'screen_output.png')
template.save(screen_output_file)
# Close the template file
template.close()
write_to_screen(screen_output_file, 600)
#epd.Clear()