diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 08013287..7736580c 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -2,7 +2,7 @@ name: Create and publish Docker image on: push: - branches: ['main', 'release', 'project_structure'] + branches: ['develop', 'release'] env: REGISTRY: ghcr.io diff --git a/bitbot.dockerfile b/bitbot.dockerfile index 9a6e6b81..7a70d1ac 100644 --- a/bitbot.dockerfile +++ b/bitbot.dockerfile @@ -1,35 +1,45 @@ -FROM python:3.11-slim-bookworm +# Stage 1: Build stage +FROM python:3.11-slim-bookworm AS builder -# avoid bytecode baggage +# Avoid bytecode baggage ENV PYTHONDONTWRITEBYTECODE=1 -# install OS reqs -RUN apt-get update -y && \ - apt-get install -y \ - --no-install-recommends \ - libopenblas-dev libopenjp2-7 libtiff6 libxcb1 libfreetype6-dev \ - && rm -rf /var/lib/apt/lists/* - -# venv to keep python happy +# Create a virtual environment RUN python3 -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" -# update pip +# Update pip RUN python3 -m pip install --upgrade pip --no-cache-dir -# download python reqs +# Copy and install Python dependencies COPY requirements.txt . -RUN python3 -m pip download \ - -r requirements.txt \ - --extra-index-url https://www.piwheels.org/simple -# install python reqs RUN python3 -m pip install -v \ --prefer-binary \ -r requirements.txt \ - --extra-index-url https://www.piwheels.org/simple + --extra-index-url https://www.piwheels.org/simple \ + --no-cache-dir -# app code +# Stage 2: Final stage +FROM python:3.11-slim-bookworm + +# Avoid bytecode baggage +ENV PYTHONDONTWRITEBYTECODE=1 + +# Install only the necessary runtime dependencies +RUN apt-get update -y && \ + apt-get install -y \ + --no-install-recommends \ + libopenblas-dev libopenjp2-7 libtiff6 libxcb1 libfreetype6-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy only the necessary application code WORKDIR /code COPY . . + +# Set the default command CMD ["python", "run.py"] \ No newline at end of file diff --git a/config/config.ini b/config/config.ini index 2667eee5..80845995 100644 --- a/config/config.ini +++ b/config/config.ini @@ -32,7 +32,7 @@ enabled = false [tide_times] enabled = false -location_id = 9414290 +location_id = 0184 [comments] up = moon,yolo,pump it,gentlemen diff --git a/docs/development.md b/docs/development.md index 4ec9d0c8..7cccb4da 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,7 @@ UML diagram of broad [package interactions](http://www.plantuml.com/plantuml/svg - [`Pillow`](https://github.com/python-pillow/Pillow) draws **drawing overlay** text onto the graph ## 🐳 Docker -**Github actions** builds and tests and publishes a **container image** on each commit to `main` and `release` +**Github actions** builds and tests and publishes a **container image** on each commit to `develop` and `release` 🐳 `x86` faster than the Pi. ```sh diff --git a/index.html b/index.html deleted file mode 100644 index 6e7fdfe8..00000000 --- a/index.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - 🤖 Bitbot - - - - - - - - - - - - - - - -

🤖 BitBot Config

-
-

🖼️ Last refresh

- -
-
-

⚙️ Application Config

-
-

Edit 'config.ini' to set the exchange, currency, screen refresh, overlay and layout options

- -
-
-
-
-

📸 Picture Mode

-
-
-

- - -

-

- - -

- - -
-
-
-

📈 Chart Styles

-
-

These files manage the colours and layout of the chart plot, they are matplolib style files and follow the example defined here

- -
-
-

🌳 Logs

-
-

Check here if something isn't working!

- -
- - - \ No newline at end of file diff --git a/src/bitbot.py b/src/bitbot.py index 5f799178..120338b1 100644 --- a/src/bitbot.py +++ b/src/bitbot.py @@ -2,7 +2,6 @@ from os.path import exists import io -from matplotlib import font_manager from src.drawing.market_charts.mpf_plotted_chart import MplFinanceChart from src.exchanges import crypto_exchanges, stock_exchanges from src.configuration.log_decorator import info_log @@ -15,10 +14,6 @@ class BitBot(): - def __init__(self, config, display): - self.config = config - self.display = display - def __init__(self, config, files): self.config = config self.files = files @@ -58,9 +53,9 @@ def display_chart(self): @info_log def display_message(self, message): - img = Image.new("P", self.display.size()) + img = Image.new("RGBA", self.display.size(), (255, 0, 0, 0)) draw = ImageDraw.Draw(img) - centered_text(draw, message, self.display.title_font, self.display.size(), border=True) + centered_text(draw, message, self.display.title_font, img.size, border=True, pos="centre") self.display.show(img) return img @@ -89,13 +84,13 @@ def display_tide_times(self): @info_log def display_connection_error(self): self.display_message(""" - NO INTERNET CONNECTION - ---------------------------- - Please check your WIFI - ---------------------------- - To configure WiFi access, - connect to 'bitbot-' WiFi AP - and follow the instructions""") +NO INTERNET CONNECTION +---------------------------- +Please check your WIFI +---------------------------- +To configure WiFi access, +connect to 'comitup-' WiFi AP +and follow the instructions""") def __repr__(self): return f'' diff --git a/src/configuration/bitbot_config.py b/src/configuration/bitbot_config.py index 655b75f9..740884bc 100644 --- a/src/configuration/bitbot_config.py +++ b/src/configuration/bitbot_config.py @@ -220,9 +220,10 @@ def tide_times_enabled(self): def tide_location_id(self): return self.config['tide_times']["location_id"] - # timed meaages + # timed messages def today_has_special_message(self, datetime): return self.special_message(datetime) != None def special_message(self, datetime): return self.config.get('special_messages', datetime.strftime("%YYYY-%MM-%DD"), fallback=None) + diff --git a/src/drawing/image_utils/Align.py b/src/drawing/image_utils/Align.py index 6f7c1793..b9312732 100644 --- a/src/drawing/image_utils/Align.py +++ b/src/drawing/image_utils/Align.py @@ -2,21 +2,24 @@ class Align: - def TopRight(display, message_size): - return (display.size[0] - message_size[0] - padding - 1, padding) + def TopRight(display_size, message_size): + return (display_size[0] - message_size[0] - padding - 1, padding) - def BottomRight(display, message_size): - return (display.size[0] - message_size[0], display.size[1] - message_size[1]) + def BottomRight(display_size, message_size): + display_width_minus_text_width = display_size[0] - message_size[0] + display_height_minus_text_height = display_size[1] - message_size[1] + + return (display_width_minus_text_width, display_height_minus_text_height) - def BottomLeft(display, message_size): - return (0, display.size[1] - message_size[1]) + def BottomLeft(display_size, message_size): + return (0, display_size[1] - message_size[1]) - def TopLeft(display, message_size): + def TopLeft(display_size, message_size): return (0 + padding + 1, 0 + padding + 1) - def Centre(display, message_size): - message_y = (display.size[1][1] - message_size[1]) / 2 - message_x = (display.size[1][0] - message_size[0]) / 2 + def Centre(display_size, message_size): + message_y = (display_size[0] - message_size[0]) / 2 + message_x = (display_size[1] - message_size[1]) / 2 return (message_y, message_x) # 🏳️ select image area with the most white pixels diff --git a/src/drawing/image_utils/CenteredText.py b/src/drawing/image_utils/CenteredText.py index fc8479b1..59a24a9e 100644 --- a/src/drawing/image_utils/CenteredText.py +++ b/src/drawing/image_utils/CenteredText.py @@ -8,15 +8,15 @@ def centered_text(draw, text, font, container_size, pos='centre', border=False): # 📏 where to position the message if pos == 'centre': - message_x, message_y = Align.Centre(container_size, message_size) + message_y, message_x = Align.Centre(container_size, message_size) elif pos == 'topright': - message_x, message_y = Align.TopRight(container_size, message_size) + message_y, message_x = Align.TopRight(container_size, message_size) elif pos == 'topleft': - message_x, message_y = Align.TopLeft(container_size, message_size) + message_y, message_x = Align.TopLeft(container_size, message_size) # 🖊️ draw the message at position draw.multiline_text( - (message_x, message_y), + (message_y, message_x), text, fill='black', font=font, @@ -24,8 +24,8 @@ def centered_text(draw, text, font, container_size, pos='centre', border=False): # 📏 measure border box if border: - x0, y0 = (message_x - padding, message_y - padding) - x1 = message_x + message_size[0] + padding - y1 = message_y + message_size[1] + padding + y0, x0 = (message_y - padding, message_x - padding) + y1 = message_y + message_size[0] + padding + x1 = message_x + message_size[1] + padding # 🖊️ draw box at position - draw.rectangle([(x0, y0), (x1, y1)], outline='red') + draw.rectangle([(y0, x0), (y1, x1)], outline='red') diff --git a/src/drawing/image_utils/DrawText.py b/src/drawing/image_utils/DrawText.py index 1fa98df6..b7080408 100644 --- a/src/drawing/image_utils/DrawText.py +++ b/src/drawing/image_utils/DrawText.py @@ -59,5 +59,5 @@ def __init__(self, text, font, colour='black', align=None): self.align = align def draw_on(self, draw, pos=(0, 0)): - pos = self.align(draw.im, self.size) if self.align else pos + pos = self.align(draw.im.size, self.size) if self.align else pos draw.text(pos, self.text, self.colour, self.font) \ No newline at end of file diff --git a/src/drawing/image_utils/TextBlock.py b/src/drawing/image_utils/TextBlock.py index f0d2eb2a..8e0be54d 100644 --- a/src/drawing/image_utils/TextBlock.py +++ b/src/drawing/image_utils/TextBlock.py @@ -1,6 +1,6 @@ from .DrawText import DrawText - +# texts is array of drawtext class TextBlock: def __init__(self, texts, align=None): self.texts = texts @@ -26,5 +26,5 @@ def draw_on(self, draw, pos=(0, 0)): def draw_text_row(self, draw, x_pos, y_pos, row): for text in row: - text.draw_on(draw, (x_pos, y_pos)) + text.draw_on(draw, pos=(x_pos, y_pos)) x_pos += DrawText.width(text) \ No newline at end of file diff --git a/src/drawing/intro.py b/src/drawing/intro.py index 1d1b1524..1f246735 100644 --- a/src/drawing/intro.py +++ b/src/drawing/intro.py @@ -2,6 +2,9 @@ import time from .image_utils.CenteredText import centered_text from ..configuration.network_utils import wait_for_internet_connection + +from src.configuration.network_utils import get_ip + page1 = '''I'm Bitbot. I can chart crypto and stock markets. @@ -20,10 +23,10 @@ Once I have internet access, I will load the next page...''' -page3 = '''Good job! I'm connected :D +page3 = f'''Good job! I'm connected :D To change my config, -visit 'http://bitbot:8080' +visit 'http://{get_ip()}:8080' from any device in your network. In 30 seconds time, diff --git a/src/drawing/market_charts/chart_overlay.py b/src/drawing/market_charts/chart_overlay.py index 18f251bd..f81c9801 100644 --- a/src/drawing/market_charts/chart_overlay.py +++ b/src/drawing/market_charts/chart_overlay.py @@ -14,12 +14,11 @@ class ChartOverlay(): def __init__(self, config, display, chart_data): self.config = config - self.display = display self.chart_data = chart_data - self.title_font = self.display.title_font - self.price_font = self.display.price_font - self.medium_font = self.display.medium_font - self.tiny_font = self.display.tiny_font + self.title_font = display.title_font + self.price_font = display.price_font + self.medium_font = display.medium_font + self.tiny_font = display.tiny_font @info_log def draw_on(self, chart_image): @@ -104,7 +103,7 @@ def price_increasing(self, chartdata): return chartdata.start_price() < chartdata.last_close() def format_time(self): - return datetime.now().strftime("%-H:%M%b%-d") + return datetime.now().strftime("%-H:%M %b%-d") def ai_comments(self): return self.config.get_price_action_comments() diff --git a/src/drawing/tide_times/tidal_graph.py b/src/drawing/tide_times/tidal_graph.py index fe9433f7..7436cd5f 100644 --- a/src/drawing/tide_times/tidal_graph.py +++ b/src/drawing/tide_times/tidal_graph.py @@ -2,42 +2,52 @@ import matplotlib.pyplot as plt from datetime import datetime from PIL import Image +import matplotlib as mpl -def get_noaa_tide_data(station_id): -# date = datetime.date.today().strftime("%Y-%m-%d") +easytide_base_url = "https://easytide.admiralty.co.uk/Home/GetPredictionData" -# # Using NOAA Tides & Currents API -# # station_id = '9414290' # Example station ID for San Francisco -# api_url = f"https://environment.data.gov.uk/flood-monitoring/id/stations/{station_id}/readings?_sorted&date={date}" -# response = requests.get(api_url) -# response.raise_for_status() # Raise an exception for bad status codes -# data = response.json() +def get_tide_data(station_id): + params = { + 'stationId': station_id, + } - url = f'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?product=predictions&application=NOS.COOPS.TAC.WL&begin_date={datetime.now().strftime("%Y%m%d")}&range=168&datum=MLLW&station={station_id}&time_zone=lst_ldt&units=metric&interval=h&format=json' + response = requests.get(easytide_base_url, params=params) + if response.status_code != 200: + raise Exception(f"Failed to fetch data: HTTP {response.status_code}") + + data = response.json() + + if not data["tidalEventList"]: + raise Exception("No tide data found in the response") - try: - response = requests.get(url) - response.raise_for_status() - data = response.json() - - tide_data = [] - for prediction in data['predictions']: - tide_data.append({ - 'date': prediction['t'], - 'height': float(prediction['v']) - }) - return tide_data - except Exception as e: - print(f"Error fetching tide data: {e}") - return [] + tide_data = [] + for entry in data["tidalEventList"]: + date = datetime.strptime(entry['dateTime'], "%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M') + height = entry['height'] + tide_data.append({'date': date, 'height': height}) + + return tide_data def render_tide_chart(location_id, img_buf): - tide_data = get_noaa_tide_data(location_id) + tide_data = get_tide_data(location_id) from datetime import datetime dates = [datetime.strptime(d['date'], '%Y-%m-%d %H:%M') for d in tide_data] heights = [d['height'] for d in tide_data] + mpl.rcParams["text.hinting_factor"] = "1" + mpl.rcParams["text.hinting"] = "native" + mpl.rcParams["text.antialiased"] = "False" + mpl.rcParams["patch.antialiased"] = "False" + mpl.rcParams["lines.antialiased"] = "False" + mpl.rcParams["font.family"] = "sans-serif" + mpl.rcParams["font.sans-serif"] = "basis33" + mpl.rcParams["font.size"] = "11" + mpl.rcParams["axes.linewidth"] = "0.5" + mpl.rcParams["grid.linestyle"] = "--" + mpl.rcParams["grid.linewidth"] = "0.5" + mpl.rcParams["grid.color"] = "red" + plt.figure(figsize=(4, 3)) plt.plot(dates, heights) plt.title('Next 7 Days') @@ -62,14 +72,13 @@ def render_tide_chart(location_id, img_buf): for date, height in zip(dates, heights): day = date.strftime('%Y-%m-%d') - if day_counts[day] == 24: # Only consider days with 24 data points - if day not in daily_min_max: - daily_min_max[day] = {'min': (date, height), 'max': (date, height)} - else: - if height < daily_min_max[day]['min'][1]: - daily_min_max[day]['min'] = (date, height) - if height > daily_min_max[day]['max'][1]: - daily_min_max[day]['max'] = (date, height) + if day not in daily_min_max: + daily_min_max[day] = {'min': (date, height), 'max': (date, height)} + else: + if height < daily_min_max[day]['min'][1]: + daily_min_max[day]['min'] = (date, height) + if height > daily_min_max[day]['max'][1]: + daily_min_max[day]['max'] = (date, height) # Add labels for min and max for day, values in daily_min_max.items(): @@ -77,20 +86,20 @@ def render_tide_chart(location_id, img_buf): plt.annotate( f"{values['min'][0].strftime('%-I:%p')}", xy=(values['min'][0], values['min'][1]), - xytext=(-15, 0), + xytext=(-15, -5), textcoords='offset points', ha='center', - fontsize=8, + fontsize=11, color='red' ) # Label for maximum plt.annotate( f"{values['max'][0].strftime('%-I:%p')}", xy=(values['max'][0], values['max'][1]), - xytext=(14, 0), + xytext=(14, 5), textcoords='offset points', ha='center', - fontsize=8, + fontsize=11, color='blue' ) diff --git a/src/drawing/youtube_stats/subscriber_counter.py b/src/drawing/youtube_stats/subscriber_counter.py index c724b36a..2baa344e 100644 --- a/src/drawing/youtube_stats/subscriber_counter.py +++ b/src/drawing/youtube_stats/subscriber_counter.py @@ -23,5 +23,5 @@ def play(self): text_to_draw = f"{subscriber_count} Subscribers" img = Image.new("RGBA", self.display_size.size, transparent) draw = ImageDraw.Draw(img) - centered_text(draw, text_to_draw, self.font, self.display_size, 'centre') + centered_text(draw, text_to_draw, self.font, self.display_size.size, 'centre') return img \ No newline at end of file diff --git a/test.dockerfile b/test.dockerfile deleted file mode 100644 index 7a70d1ac..00000000 --- a/test.dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# Stage 1: Build stage -FROM python:3.11-slim-bookworm AS builder - -# Avoid bytecode baggage -ENV PYTHONDONTWRITEBYTECODE=1 - -# Create a virtual environment -RUN python3 -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" - -# Update pip -RUN python3 -m pip install --upgrade pip --no-cache-dir - -# Copy and install Python dependencies -COPY requirements.txt . - -RUN python3 -m pip install -v \ - --prefer-binary \ - -r requirements.txt \ - --extra-index-url https://www.piwheels.org/simple \ - --no-cache-dir - -# Stage 2: Final stage -FROM python:3.11-slim-bookworm - -# Avoid bytecode baggage -ENV PYTHONDONTWRITEBYTECODE=1 - -# Install only the necessary runtime dependencies -RUN apt-get update -y && \ - apt-get install -y \ - --no-install-recommends \ - libopenblas-dev libopenjp2-7 libtiff6 libxcb1 libfreetype6-dev \ - && rm -rf /var/lib/apt/lists/* - -# Copy the virtual environment from the builder stage -COPY --from=builder /opt/venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" - -# Copy only the necessary application code -WORKDIR /code -COPY . . - -# Set the default command -CMD ["python", "run.py"] \ No newline at end of file diff --git a/tests/data/whitby_tides.json b/tests/data/whitby_tides.json new file mode 100644 index 00000000..11570c9e --- /dev/null +++ b/tests/data/whitby_tides.json @@ -0,0 +1 @@ +{"tidalEventList":[{"eventType":1,"dateTime":"2025-02-08T06:18:00","height":2.250927077638894,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-08T00:00:00"},{"eventType":0,"dateTime":"2025-02-08T12:33:00","height":4.50918972731683,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-08T00:00:00"},{"eventType":1,"dateTime":"2025-02-08T19:01:00","height":2.0853662323155526,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-08T00:00:00"},{"eventType":0,"dateTime":"2025-02-09T01:15:00","height":4.6573249943865145,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-09T00:00:00"},{"eventType":1,"dateTime":"2025-02-09T07:41:00","height":2.1723152205092107,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-09T00:00:00"},{"eventType":0,"dateTime":"2025-02-09T13:46:00","height":4.730628059134607,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-09T00:00:00"},{"eventType":1,"dateTime":"2025-02-09T20:15:00","height":1.763520214165164,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-09T00:00:00"},{"eventType":0,"dateTime":"2025-02-10T02:24:00","height":4.858152287952238,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-10T00:00:00"},{"eventType":1,"dateTime":"2025-02-10T08:41:00","height":1.9814674061220303,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-10T00:00:00"},{"eventType":0,"dateTime":"2025-02-10T14:42:00","height":5.004950966474468,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-10T00:00:00"},{"eventType":1,"dateTime":"2025-02-10T21:11:00","height":1.4278918078855554,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-10T00:00:00"},{"eventType":0,"dateTime":"2025-02-11T03:16:00","height":5.059063440048395,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-11T00:00:00"},{"eventType":1,"dateTime":"2025-02-11T09:28:00","height":1.779085496410402,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-11T00:00:00"},{"eventType":0,"dateTime":"2025-02-11T15:27:00","height":5.247440375380359,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-11T00:00:00"},{"eventType":1,"dateTime":"2025-02-11T21:56:00","height":1.16184957015816,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-11T00:00:00"},{"eventType":0,"dateTime":"2025-02-12T03:59:00","height":5.200586351290798,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-12T00:00:00"},{"eventType":1,"dateTime":"2025-02-12T10:07:00","height":1.6071006495793392,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-12T00:00:00"},{"eventType":0,"dateTime":"2025-02-12T16:07:00","height":5.425011191464042,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-12T00:00:00"},{"eventType":1,"dateTime":"2025-02-12T22:36:00","height":0.9928605605025923,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-12T00:00:00"},{"eventType":0,"dateTime":"2025-02-13T04:36:00","height":5.270786648463542,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-13T00:00:00"},{"eventType":1,"dateTime":"2025-02-13T10:42:00","height":1.484266893296131,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-13T00:00:00"},{"eventType":0,"dateTime":"2025-02-13T16:43:00","height":5.528284041729102,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-13T00:00:00"},{"eventType":1,"dateTime":"2025-02-13T23:11:00","height":0.9233890962693343,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-13T00:00:00"},{"eventType":0,"dateTime":"2025-02-14T05:10:00","height":5.275528669494031,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-14T00:00:00"},{"eventType":1,"dateTime":"2025-02-14T11:14:00","height":1.4205558364075426,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-14T00:00:00"},{"eventType":0,"dateTime":"2025-02-14T17:16:00","height":5.557999002562121,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-14T00:00:00"},{"eventType":1,"dateTime":"2025-02-14T23:42:00","height":0.944651202343493,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-14T00:00:00"},{"eventType":0,"dateTime":"2025-02-15T05:42:00","height":5.226291012611799,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-15T00:00:00"},{"eventType":1,"dateTime":"2025-02-15T11:43:00","height":1.419590908938737,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-15T00:00:00"},{"eventType":0,"dateTime":"2025-02-15T17:47:00","height":5.519299591512786,"isApproximateTime":null,"isApproximateHeight":null,"date":"2025-02-15T00:00:00"}],"tidalHeightOccurrenceList":[{"dateTime":"2025-02-08T23:00:00Z","height":3.916645},{"dateTime":"2025-02-08T23:30:00Z","height":4.185804},{"dateTime":"2025-02-09T00:00:00Z","height":4.404631},{"dateTime":"2025-02-09T00:30:00Z","height":4.562275},{"dateTime":"2025-02-09T01:00:00Z","height":4.645841},{"dateTime":"2025-02-09T01:30:00Z","height":4.648231},{"dateTime":"2025-02-09T02:00:00Z","height":4.570677},{"dateTime":"2025-02-09T02:30:00Z","height":4.422385},{"dateTime":"2025-02-09T03:00:00Z","height":4.21686},{"dateTime":"2025-02-09T03:30:00Z","height":3.969473},{"dateTime":"2025-02-09T04:00:00Z","height":3.695627},{"dateTime":"2025-02-09T04:30:00Z","height":3.40792},{"dateTime":"2025-02-09T05:00:00Z","height":3.116596},{"dateTime":"2025-02-09T05:30:00Z","height":2.835423},{"dateTime":"2025-02-09T06:00:00Z","height":2.584642},{"dateTime":"2025-02-09T06:30:00Z","height":2.384243},{"dateTime":"2025-02-09T07:00:00Z","height":2.246322},{"dateTime":"2025-02-09T07:30:00Z","height":2.178079},{"dateTime":"2025-02-09T08:00:00Z","height":2.189037},{"dateTime":"2025-02-09T08:30:00Z","height":2.287581},{"dateTime":"2025-02-09T09:00:00Z","height":2.469514},{"dateTime":"2025-02-09T09:30:00Z","height":2.716126},{"dateTime":"2025-02-09T10:00:00Z","height":3.005216},{"dateTime":"2025-02-09T10:30:00Z","height":3.319045},{"dateTime":"2025-02-09T11:00:00Z","height":3.640694},{"dateTime":"2025-02-09T11:30:00Z","height":3.949819},{"dateTime":"2025-02-09T12:00:00Z","height":4.22665},{"dateTime":"2025-02-09T12:30:00Z","height":4.4562},{"dateTime":"2025-02-09T13:00:00Z","height":4.624896},{"dateTime":"2025-02-09T13:30:00Z","height":4.717224},{"dateTime":"2025-02-09T14:00:00Z","height":4.72096},{"dateTime":"2025-02-09T14:30:00Z","height":4.634716},{"dateTime":"2025-02-09T15:00:00Z","height":4.467853},{"dateTime":"2025-02-09T15:30:00Z","height":4.234976},{"dateTime":"2025-02-09T16:00:00Z","height":3.952256},{"dateTime":"2025-02-09T16:30:00Z","height":3.63539},{"dateTime":"2025-02-09T17:00:00Z","height":3.297398},{"dateTime":"2025-02-09T17:30:00Z","height":2.95044},{"dateTime":"2025-02-09T18:00:00Z","height":2.612124},{"dateTime":"2025-02-09T18:30:00Z","height":2.3066},{"dateTime":"2025-02-09T19:00:00Z","height":2.056223},{"dateTime":"2025-02-09T19:30:00Z","height":1.875637},{"dateTime":"2025-02-09T20:00:00Z","height":1.777164},{"dateTime":"2025-02-09T20:30:00Z","height":1.776093},{"dateTime":"2025-02-09T21:00:00Z","height":1.882673},{"dateTime":"2025-02-09T21:30:00Z","height":2.090288},{"dateTime":"2025-02-09T22:00:00Z","height":2.37758},{"dateTime":"2025-02-09T22:30:00Z","height":2.720788},{"dateTime":"2025-02-09T23:00:00Z","height":3.098193},{"dateTime":"2025-02-09T23:30:00Z","height":3.485305},{"dateTime":"2025-02-10T00:00:00Z","height":3.855635},{"dateTime":"2025-02-10T00:30:00Z","height":4.188873},{"dateTime":"2025-02-10T01:00:00Z","height":4.471927},{"dateTime":"2025-02-10T01:30:00Z","height":4.689947},{"dateTime":"2025-02-10T02:00:00Z","height":4.82313},{"dateTime":"2025-02-10T02:30:00Z","height":4.856639},{"dateTime":"2025-02-10T03:00:00Z","height":4.790144},{"dateTime":"2025-02-10T03:30:00Z","height":4.635666},{"dateTime":"2025-02-10T04:00:00Z","height":4.410172},{"dateTime":"2025-02-10T04:30:00Z","height":4.131722},{"dateTime":"2025-02-10T05:00:00Z","height":3.817236},{"dateTime":"2025-02-10T05:30:00Z","height":3.480236},{"dateTime":"2025-02-10T06:00:00Z","height":3.13385},{"dateTime":"2025-02-10T06:30:00Z","height":2.79753},{"dateTime":"2025-02-10T07:00:00Z","height":2.496298},{"dateTime":"2025-02-10T07:30:00Z","height":2.25152},{"dateTime":"2025-02-10T08:00:00Z","height":2.077567},{"dateTime":"2025-02-10T08:30:00Z","height":1.989033},{"dateTime":"2025-02-10T09:00:00Z","height":2.002977},{"dateTime":"2025-02-10T09:30:00Z","height":2.127473},{"dateTime":"2025-02-10T10:00:00Z","height":2.351999},{"dateTime":"2025-02-10T10:30:00Z","height":2.653826},{"dateTime":"2025-02-10T11:00:00Z","height":3.009054},{"dateTime":"2025-02-10T11:30:00Z","height":3.392711},{"dateTime":"2025-02-10T12:00:00Z","height":3.775395},{"dateTime":"2025-02-10T12:30:00Z","height":4.13041},{"dateTime":"2025-02-10T13:00:00Z","height":4.44282},{"dateTime":"2025-02-10T13:30:00Z","height":4.703554},{"dateTime":"2025-02-10T14:00:00Z","height":4.895762},{"dateTime":"2025-02-10T14:30:00Z","height":4.995301},{"dateTime":"2025-02-10T15:00:00Z","height":4.985936},{"dateTime":"2025-02-10T15:30:00Z","height":4.86869},{"dateTime":"2025-02-10T16:00:00Z","height":4.656865},{"dateTime":"2025-02-10T16:30:00Z","height":4.36836},{"dateTime":"2025-02-10T17:00:00Z","height":4.022693},{"dateTime":"2025-02-10T17:30:00Z","height":3.638112},{"dateTime":"2025-02-10T18:00:00Z","height":3.229167},{"dateTime":"2025-02-10T18:30:00Z","height":2.811358},{"dateTime":"2025-02-10T19:00:00Z","height":2.40807},{"dateTime":"2025-02-10T19:30:00Z","height":2.047507},{"dateTime":"2025-02-10T20:00:00Z","height":1.75306},{"dateTime":"2025-02-10T20:30:00Z","height":1.542703},{"dateTime":"2025-02-10T21:00:00Z","height":1.436488},{"dateTime":"2025-02-10T21:30:00Z","height":1.454511},{"dateTime":"2025-02-10T22:00:00Z","height":1.603301},{"dateTime":"2025-02-10T22:30:00Z","height":1.869819},{"dateTime":"2025-02-10T23:00:00Z","height":2.230223},{"dateTime":"2025-02-10T23:30:00Z","height":2.657026},{"dateTime":"2025-02-11T00:00:00Z","height":3.116361},{"dateTime":"2025-02-11T00:30:00Z","height":3.570096},{"dateTime":"2025-02-11T01:00:00Z","height":3.989617},{"dateTime":"2025-02-11T01:30:00Z","height":4.362221},{"dateTime":"2025-02-11T02:00:00Z","height":4.677421},{"dateTime":"2025-02-11T02:30:00Z","height":4.912627},{"dateTime":"2025-02-11T03:00:00Z","height":5.04041},{"dateTime":"2025-02-11T03:30:00Z","height":5.047109},{"dateTime":"2025-02-11T04:00:00Z","height":4.938651},{"dateTime":"2025-02-11T04:30:00Z","height":4.732241},{"dateTime":"2025-02-11T05:00:00Z","height":4.449186},{"dateTime":"2025-02-11T05:30:00Z","height":4.112058},{"dateTime":"2025-02-11T06:00:00Z","height":3.740195},{"dateTime":"2025-02-11T06:30:00Z","height":3.34784},{"dateTime":"2025-02-11T07:00:00Z","height":2.951386},{"dateTime":"2025-02-11T07:30:00Z","height":2.5761},{"dateTime":"2025-02-11T08:00:00Z","height":2.250409},{"dateTime":"2025-02-11T08:30:00Z","height":1.996583},{"dateTime":"2025-02-11T09:00:00Z","height":1.832776},{"dateTime":"2025-02-11T09:30:00Z","height":1.779357},{"dateTime":"2025-02-11T10:00:00Z","height":1.852867},{"dateTime":"2025-02-11T10:30:00Z","height":2.05306},{"dateTime":"2025-02-11T11:00:00Z","height":2.361905},{"dateTime":"2025-02-11T11:30:00Z","height":2.752959},{"dateTime":"2025-02-11T12:00:00Z","height":3.194082},{"dateTime":"2025-02-11T12:30:00Z","height":3.645204},{"dateTime":"2025-02-11T13:00:00Z","height":4.067942},{"dateTime":"2025-02-11T13:30:00Z","height":4.441997},{"dateTime":"2025-02-11T14:00:00Z","height":4.763241},{"dateTime":"2025-02-11T14:30:00Z","height":5.02215},{"dateTime":"2025-02-11T15:00:00Z","height":5.192696},{"dateTime":"2025-02-11T15:30:00Z","height":5.247205},{"dateTime":"2025-02-11T16:00:00Z","height":5.175469},{"dateTime":"2025-02-11T16:30:00Z","height":4.986029},{"dateTime":"2025-02-11T17:00:00Z","height":4.696961},{"dateTime":"2025-02-11T17:30:00Z","height":4.330944},{"dateTime":"2025-02-11T18:00:00Z","height":3.91202},{"dateTime":"2025-02-11T18:30:00Z","height":3.459476},{"dateTime":"2025-02-11T19:00:00Z","height":2.987861},{"dateTime":"2025-02-11T19:30:00Z","height":2.516825},{"dateTime":"2025-02-11T20:00:00Z","height":2.076074},{"dateTime":"2025-02-11T20:30:00Z","height":1.696682},{"dateTime":"2025-02-11T21:00:00Z","height":1.403069},{"dateTime":"2025-02-11T21:30:00Z","height":1.217291},{"dateTime":"2025-02-11T22:00:00Z","height":1.162925},{"dateTime":"2025-02-11T22:30:00Z","height":1.255717},{"dateTime":"2025-02-11T23:00:00Z","height":1.492775},{"dateTime":"2025-02-11T23:30:00Z","height":1.854879},{"dateTime":"2025-02-12T00:00:00Z","height":2.312951},{"dateTime":"2025-02-12T00:30:00Z","height":2.826648},{"dateTime":"2025-02-12T01:00:00Z","height":3.346474},{"dateTime":"2025-02-12T01:30:00Z","height":3.831488},{"dateTime":"2025-02-12T02:00:00Z","height":4.26437},{"dateTime":"2025-02-12T02:30:00Z","height":4.639998},{"dateTime":"2025-02-12T03:00:00Z","height":4.941285},{"dateTime":"2025-02-12T03:30:00Z","height":5.136496},{"dateTime":"2025-02-12T04:00:00Z","height":5.200514},{"dateTime":"2025-02-12T04:30:00Z","height":5.130329},{"dateTime":"2025-02-12T05:00:00Z","height":4.940622},{"dateTime":"2025-02-12T05:30:00Z","height":4.654385},{"dateTime":"2025-02-12T06:00:00Z","height":4.298944},{"dateTime":"2025-02-12T06:30:00Z","height":3.900402},{"dateTime":"2025-02-12T07:00:00Z","height":3.476757},{"dateTime":"2025-02-12T07:30:00Z","height":3.04179},{"dateTime":"2025-02-12T08:00:00Z","height":2.61727},{"dateTime":"2025-02-12T08:30:00Z","height":2.23485},{"dateTime":"2025-02-12T09:00:00Z","height":1.92481},{"dateTime":"2025-02-12T09:30:00Z","height":1.710017},{"dateTime":"2025-02-12T10:00:00Z","height":1.611236},{"dateTime":"2025-02-12T10:30:00Z","height":1.647735},{"dateTime":"2025-02-12T11:00:00Z","height":1.826699},{"dateTime":"2025-02-12T11:30:00Z","height":2.136713},{"dateTime":"2025-02-12T12:00:00Z","height":2.552635},{"dateTime":"2025-02-12T12:30:00Z","height":3.038908},{"dateTime":"2025-02-12T13:00:00Z","height":3.547795},{"dateTime":"2025-02-12T13:30:00Z","height":4.028984},{"dateTime":"2025-02-12T14:00:00Z","height":4.451726},{"dateTime":"2025-02-12T14:30:00Z","height":4.811598},{"dateTime":"2025-02-12T15:00:00Z","height":5.108093},{"dateTime":"2025-02-12T15:30:00Z","height":5.321733},{"dateTime":"2025-02-12T16:00:00Z","height":5.420922},{"dateTime":"2025-02-12T16:30:00Z","height":5.385852},{"dateTime":"2025-02-12T17:00:00Z","height":5.218269},{"dateTime":"2025-02-12T17:30:00Z","height":4.934187},{"dateTime":"2025-02-12T18:00:00Z","height":4.557319},{"dateTime":"2025-02-12T18:30:00Z","height":4.11595},{"dateTime":"2025-02-12T19:00:00Z","height":3.635074},{"dateTime":"2025-02-12T19:30:00Z","height":3.130761},{"dateTime":"2025-02-12T20:00:00Z","height":2.618574},{"dateTime":"2025-02-12T20:30:00Z","height":2.12573},{"dateTime":"2025-02-12T21:00:00Z","height":1.688176},{"dateTime":"2025-02-12T21:30:00Z","height":1.337798},{"dateTime":"2025-02-12T22:00:00Z","height":1.099268},{"dateTime":"2025-02-12T22:30:00Z","height":0.99557},{"dateTime":"2025-02-12T23:00:00Z","height":1.045485},{"dateTime":"2025-02-12T23:30:00Z","height":1.253304},{"dateTime":"2025-02-13T00:00:00Z","height":1.605684},{"dateTime":"2025-02-13T00:30:00Z","height":2.075385},{"dateTime":"2025-02-13T01:00:00Z","height":2.620353},{"dateTime":"2025-02-13T01:30:00Z","height":3.184525},{"dateTime":"2025-02-13T02:00:00Z","height":3.715564},{"dateTime":"2025-02-13T02:30:00Z","height":4.187116},{"dateTime":"2025-02-13T03:00:00Z","height":4.595242},{"dateTime":"2025-02-13T03:30:00Z","height":4.930825},{"dateTime":"2025-02-13T04:00:00Z","height":5.165338},{"dateTime":"2025-02-13T04:30:00Z","height":5.267649},{"dateTime":"2025-02-13T05:00:00Z","height":5.22614},{"dateTime":"2025-02-13T05:30:00Z","height":5.050913},{"dateTime":"2025-02-13T06:00:00Z","height":4.764492},{"dateTime":"2025-02-13T06:30:00Z","height":4.396677},{"dateTime":"2025-02-13T07:00:00Z","height":3.979223},{"dateTime":"2025-02-13T07:30:00Z","height":3.535709},{"dateTime":"2025-02-13T08:00:00Z","height":3.07973},{"dateTime":"2025-02-13T08:30:00Z","height":2.628179},{"dateTime":"2025-02-13T09:00:00Z","height":2.211477},{"dateTime":"2025-02-13T09:30:00Z","height":1.865727},{"dateTime":"2025-02-13T10:00:00Z","height":1.619983},{"dateTime":"2025-02-13T10:30:00Z","height":1.495726},{"dateTime":"2025-02-13T11:00:00Z","height":1.511069},{"dateTime":"2025-02-13T11:30:00Z","height":1.675811},{"dateTime":"2025-02-13T12:00:00Z","height":1.983533},{"dateTime":"2025-02-13T12:30:00Z","height":2.411912},{"dateTime":"2025-02-13T13:00:00Z","height":2.925269},{"dateTime":"2025-02-13T13:30:00Z","height":3.47313},{"dateTime":"2025-02-13T14:00:00Z","height":3.997753},{"dateTime":"2025-02-13T14:30:00Z","height":4.457571},{"dateTime":"2025-02-13T15:00:00Z","height":4.842366},{"dateTime":"2025-02-13T15:30:00Z","height":5.156649},{"dateTime":"2025-02-13T16:00:00Z","height":5.390425},{"dateTime":"2025-02-13T16:30:00Z","height":5.515104},{"dateTime":"2025-02-13T17:00:00Z","height":5.505734},{"dateTime":"2025-02-13T17:30:00Z","height":5.357577},{"dateTime":"2025-02-13T18:00:00Z","height":5.083133},{"dateTime":"2025-02-13T18:30:00Z","height":4.705009},{"dateTime":"2025-02-13T19:00:00Z","height":4.253072},{"dateTime":"2025-02-13T19:30:00Z","height":3.757084},{"dateTime":"2025-02-13T20:00:00Z","height":3.236836},{"dateTime":"2025-02-13T20:30:00Z","height":2.705785},{"dateTime":"2025-02-13T21:00:00Z","height":2.186551},{"dateTime":"2025-02-13T21:30:00Z","height":1.715845},{"dateTime":"2025-02-13T22:00:00Z","height":1.331941},{"dateTime":"2025-02-13T22:30:00Z","height":1.063799},{"dateTime":"2025-02-13T23:00:00Z","height":0.93318},{"dateTime":"2025-02-13T23:30:00Z","height":0.957303},{"dateTime":"2025-02-14T00:00:00Z","height":1.142843},{"dateTime":"2025-02-14T00:30:00Z","height":1.480485},{"dateTime":"2025-02-14T01:00:00Z","height":1.945671},{"dateTime":"2025-02-14T01:30:00Z","height":2.497936},{"dateTime":"2025-02-14T02:00:00Z","height":3.080584},{"dateTime":"2025-02-14T02:30:00Z","height":3.635272},{"dateTime":"2025-02-14T03:00:00Z","height":4.126637},{"dateTime":"2025-02-14T03:30:00Z","height":4.54732},{"dateTime":"2025-02-14T04:00:00Z","height":4.893714},{"dateTime":"2025-02-14T04:30:00Z","height":5.144017},{"dateTime":"2025-02-14T05:00:00Z","height":5.266717},{"dateTime":"2025-02-14T05:30:00Z","height":5.244201},{"dateTime":"2025-02-14T06:00:00Z","height":5.081393},{"dateTime":"2025-02-14T06:30:00Z","height":4.798646},{"dateTime":"2025-02-14T07:00:00Z","height":4.425803},{"dateTime":"2025-02-14T07:30:00Z","height":3.997786},{"dateTime":"2025-02-14T08:00:00Z","height":3.54384},{"dateTime":"2025-02-14T08:30:00Z","height":3.080312},{"dateTime":"2025-02-14T09:00:00Z","height":2.620638},{"dateTime":"2025-02-14T09:30:00Z","height":2.190947},{"dateTime":"2025-02-14T10:00:00Z","height":1.829},{"dateTime":"2025-02-14T10:30:00Z","height":1.569301},{"dateTime":"2025-02-14T11:00:00Z","height":1.435354},{"dateTime":"2025-02-14T11:30:00Z","height":1.443015},{"dateTime":"2025-02-14T12:00:00Z","height":1.600909},{"dateTime":"2025-02-14T12:30:00Z","height":1.904661},{"dateTime":"2025-02-14T13:00:00Z","height":2.334357},{"dateTime":"2025-02-14T13:30:00Z","height":2.855702},{"dateTime":"2025-02-14T14:00:00Z","height":3.419277},{"dateTime":"2025-02-14T14:30:00Z","height":3.965878},{"dateTime":"2025-02-14T15:00:00Z","height":4.447281},{"dateTime":"2025-02-14T15:30:00Z","height":4.845658},{"dateTime":"2025-02-14T16:00:00Z","height":5.165169},{"dateTime":"2025-02-14T16:30:00Z","height":5.403238},{"dateTime":"2025-02-14T17:00:00Z","height":5.537823},{"dateTime":"2025-02-14T17:30:00Z","height":5.543587},{"dateTime":"2025-02-14T18:00:00Z","height":5.411343},{"dateTime":"2025-02-14T18:30:00Z","height":5.149827},{"dateTime":"2025-02-14T19:00:00Z","height":4.779215},{"dateTime":"2025-02-14T19:30:00Z","height":4.328419},{"dateTime":"2025-02-14T20:00:00Z","height":3.829596},{"dateTime":"2025-02-14T20:30:00Z","height":3.306886},{"dateTime":"2025-02-14T21:00:00Z","height":2.774556},{"dateTime":"2025-02-14T21:30:00Z","height":2.25121},{"dateTime":"2025-02-14T22:00:00Z","height":1.770913},{"dateTime":"2025-02-14T22:30:00Z","height":1.37501},{"dateTime":"2025-02-14T23:00:00Z","height":1.096842},{"dateTime":"2025-02-14T23:30:00Z","height":0.957915},{"dateTime":"2025-02-15T00:00:00Z","height":0.972001},{"dateTime":"2025-02-15T00:30:00Z","height":1.144268},{"dateTime":"2025-02-15T01:00:00Z","height":1.466656},{"dateTime":"2025-02-15T01:30:00Z","height":1.916511},{"dateTime":"2025-02-15T02:00:00Z","height":2.456002},{"dateTime":"2025-02-15T02:30:00Z","height":3.031801},{"dateTime":"2025-02-15T03:00:00Z","height":3.585913},{"dateTime":"2025-02-15T03:30:00Z","height":4.077999},{"dateTime":"2025-02-15T04:00:00Z","height":4.495594},{"dateTime":"2025-02-15T04:30:00Z","height":4.836565},{"dateTime":"2025-02-15T05:00:00Z","height":5.085383},{"dateTime":"2025-02-15T05:30:00Z","height":5.213825},{"dateTime":"2025-02-15T06:00:00Z","height":5.201706},{"dateTime":"2025-02-15T06:30:00Z","height":5.049507},{"dateTime":"2025-02-15T07:00:00Z","height":4.774529},{"dateTime":"2025-02-15T07:30:00Z","height":4.40486},{"dateTime":"2025-02-15T08:00:00Z","height":3.975799},{"dateTime":"2025-02-15T08:30:00Z","height":3.520497},{"dateTime":"2025-02-15T09:00:00Z","height":3.059675},{"dateTime":"2025-02-15T09:30:00Z","height":2.606321},{"dateTime":"2025-02-15T10:00:00Z","height":2.182011},{"dateTime":"2025-02-15T10:30:00Z","height":1.822306},{"dateTime":"2025-02-15T11:00:00Z","height":1.564142},{"dateTime":"2025-02-15T11:30:00Z","height":1.433103},{"dateTime":"2025-02-15T12:00:00Z","height":1.443041},{"dateTime":"2025-02-15T12:30:00Z","height":1.599433},{"dateTime":"2025-02-15T13:00:00Z","height":1.897374},{"dateTime":"2025-02-15T13:30:00Z","height":2.318429},{"dateTime":"2025-02-15T14:00:00Z","height":2.830258},{"dateTime":"2025-02-15T14:30:00Z","height":3.386507},{"dateTime":"2025-02-15T15:00:00Z","height":3.931085},{"dateTime":"2025-02-15T15:30:00Z","height":4.414811},{"dateTime":"2025-02-15T16:00:00Z","height":4.814346},{"dateTime":"2025-02-15T16:30:00Z","height":5.130128},{"dateTime":"2025-02-15T17:00:00Z","height":5.36268},{"dateTime":"2025-02-15T17:30:00Z","height":5.496328},{"dateTime":"2025-02-15T18:00:00Z","height":5.508614},{"dateTime":"2025-02-15T18:30:00Z","height":5.388634},{"dateTime":"2025-02-15T19:00:00Z","height":5.142147},{"dateTime":"2025-02-15T19:30:00Z","height":4.786613},{"dateTime":"2025-02-15T20:00:00Z","height":4.348474},{"dateTime":"2025-02-15T20:30:00Z","height":3.859572},{"dateTime":"2025-02-15T21:00:00Z","height":3.34699},{"dateTime":"2025-02-15T21:30:00Z","height":2.827535},{"dateTime":"2025-02-15T22:00:00Z","height":2.317969},{"dateTime":"2025-02-15T22:30:00Z","height":1.848494},{"dateTime":"2025-02-15T23:00:00Z","height":1.459909},{"dateTime":"2025-02-15T23:30:00Z","height":1.188069}],"lunarPhaseList":[{"lunarPhaseType":3,"dateTime":"2025-02-12T13:53:00"}],"footerNote":""} \ No newline at end of file diff --git a/tests/images/intro0.png b/tests/images/intro0.png new file mode 100644 index 00000000..5a688aba Binary files /dev/null and b/tests/images/intro0.png differ diff --git a/tests/images/intro1.png b/tests/images/intro1.png new file mode 100644 index 00000000..c569d965 Binary files /dev/null and b/tests/images/intro1.png differ diff --git a/tests/images/intro2.png b/tests/images/intro2.png new file mode 100644 index 00000000..4f30e372 Binary files /dev/null and b/tests/images/intro2.png differ diff --git a/tests/images/test_centered.png b/tests/images/test_centered.png new file mode 100644 index 00000000..0b3209b0 Binary files /dev/null and b/tests/images/test_centered.png differ diff --git a/tests/images/tide_times.png b/tests/images/tide_times.png new file mode 100644 index 00000000..a1fa898b Binary files /dev/null and b/tests/images/tide_times.png differ diff --git a/tests/test_chart_rendering.py b/tests/test_chart_rendering.py index c2af6c2e..bf52bf2c 100644 --- a/tests/test_chart_rendering.py +++ b/tests/test_chart_rendering.py @@ -6,6 +6,7 @@ import os import pathlib import unittest +import glob from src.drawing.market_charts.mpf_plotted_chart import MplFinanceChart from src.exchanges.CandleData import CandleData @@ -59,7 +60,8 @@ def test(self): return test - for file_name in os.listdir(test_data_path): + for file_path in glob.glob(f"{test_data_path}/*.pkl"): + file_name = os.path.basename(file_path) test_name = f"test_{file_name}".replace(".pkl","") this_tests_images_path=os.path.join(test_images_path, chart_size) os.makedirs(this_tests_images_path, exist_ok=True) diff --git a/tests/test_overlay_drawing.py b/tests/test_overlay_drawing.py index 5561cbf1..20f3cf18 100644 --- a/tests/test_overlay_drawing.py +++ b/tests/test_overlay_drawing.py @@ -1,17 +1,17 @@ from PIL import ImageFont, Image, ImageDraw, ImageChops from src.drawing.image_utils.DrawText import DrawText from src.drawing.image_utils.TextBlock import TextBlock +from src.drawing.image_utils.CenteredText import centered_text from src.configuration.bitbot_files import use_config_dir import unittest import os import pathlib -curdir = pathlib.Path(__file__).parent.resolve() -files = use_config_dir(os.path.join(curdir, "../")) +current_directory = pathlib.Path(__file__).parent.resolve() +files = use_config_dir(os.path.join(current_directory, "../")) transparent = (0, 0, 0, 0) white = (255, 255, 255) -image_file_name = 'tests/images/title_block.png' class TestTextBlocks(unittest.TestCase): @@ -20,6 +20,7 @@ class TestTextBlocks(unittest.TestCase): price_font = ImageFont.truetype(fontPath, 32) def test_text_block(self): + image_file_name = 'tests/images/title_block.png' block = TextBlock([ [ DrawText('GBP' + ' (' + 'Sterling' + ') ', self.title_font, colour=white), @@ -32,18 +33,35 @@ def test_text_block(self): image = Image.new('RGBA', block.size(), transparent) image_drawing = ImageDraw.Draw(image) - block.draw_on(image_drawing) - image.save(image_file_name) previous_image = Image.open(image_file_name) if os.path.isfile(image_file_name) else None if previous_image is None: assert False, f"New image result, re-run the test to accept: '{image_file_name}'" + image.save(image_file_name) diff = ImageChops.difference(image, previous_image) - if diff.getbbox(): file_name = f'tests/images/overlay_diff.png' diff.save(file_name) assert False, f"images were different, see '{file_name}'" # os.system(f"code 'diff.png'") + + + def test_centered(self): + image_file_name = 'tests/images/test_centered.png' + image = Image.new("RGBA", (400, 300), transparent) + draw = ImageDraw.Draw(image) + centered_text(draw, "test", self.price_font, image.size, 'centre', border=True) + + previous_image = Image.open(image_file_name) if os.path.isfile(image_file_name) else None + if previous_image is None: + assert False, f"New image result, re-run the test to accept: '{image_file_name}'" + + image.save(image_file_name) + diff = ImageChops.difference(image, previous_image) + if diff.getbbox(): + file_name = f'tests/images/test_centered_diff.png' + diff.save(file_name) + assert False, f"images were different, see '{image_file_name}'" + # os.system(f"code '{image_file_name}'") diff --git a/tests/test_tides.py b/tests/test_tides.py new file mode 100644 index 00000000..d4b6cbb6 --- /dev/null +++ b/tests/test_tides.py @@ -0,0 +1,10 @@ +import io +import unittest +from src.drawing.tide_times import tidal_graph +from PIL import Image, ImageDraw + +class TestTidalGraph(unittest.TestCase): + def test_data_fetch_and_render(self): + with io.BytesIO() as img_buf: + image = tidal_graph.render_tide_chart("0184", img_buf) + image.save(f'tests/images/tide_times.png')