From a2f22f481ec6963f47b49fdbc62896bf7de95725 Mon Sep 17 00:00:00 2001 From: Death916 Date: Tue, 22 Apr 2025 03:21:00 -0700 Subject: [PATCH] weather updates fixed --- deathclock/deathclock.py | 214 +++++++++++++++++++++++++++++---------- utils/weather.py | 109 ++++++++++++++------ 2 files changed, 234 insertions(+), 89 deletions(-) diff --git a/deathclock/deathclock.py b/deathclock/deathclock.py index 583c066..c9ee587 100644 --- a/deathclock/deathclock.py +++ b/deathclock/deathclock.py @@ -1,93 +1,195 @@ -"""Welcome to Reflex! This file outlines the steps to create a basic app.""" +# deathclock.py (or your main app file name) import reflex as rx from datetime import datetime, timezone import asyncio - -from rxconfig import config +import time +# Remove rxconfig import if not used directly +# from rxconfig import config +# --- Import your Weather utility --- from utils.weather import Weather -from utils.scores import NBAScores, mlbScores -from utils.news import News -from utils.alarm import Alarm + +# from utils.scores import NBAScores, mlbScores +# from utils.news import News +# from utils.alarm import Alarm +import logging + +# --- Constants --- +WEATHER_IMAGE_PATH = "/weather.jpg" # Web path in assets folder +WEATHER_FETCH_INTERVAL = 360 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class State(rx.State): - def __init__(self): - self.weather = Weather() - self.nbaScores = NBAScores() - self.mlbScores = mlbScores() - self.news = News() - # Define the state variables - current_time: str = "" - last_weather_update: str = "" + # --- Original state variables (kept as requested) --- + current_time: str = "" # Note: rx.moment replaces the need for this if used for display alarm_time: str = "" alarms: list = [] - weather_img: str = "" - news: list = [] - nba_scores: str = "" - mlb_scores: str = "" + news: list = [] # Placeholder + nba_scores: str = "" # Placeholder + mlb_scores: str = "" # Placeholder - def get_weather(self): - - @rx.background - async def fetch_weather(): - while True: - async with self: - weather_img = self.weather.get_weather_screenshot() - self.weather_img = weather_img - print(f"Weather image updated: {weather_img}") + # --- Weather-specific state variables --- + last_weather_update: str = "Never" + # Initialize with the base path, it will be updated with cache-buster + weather_img: str = WEATHER_IMAGE_PATH + # Placeholder for the weather client + _weather_client: Weather | None = None # This will be set in the constructor + + # --- Initialize Utility Client --- + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Initialize the weather client + try: + + self._weather_client = Weather() + logging.info("Weather client initialized successfully.") + except Exception as e: + logging.error(f"Failed to initialize Weather client: {e}", exc_info=True) + self._weather_client = None # Mark as unusable + # Set error state if needed + self.weather_img = "/error_placeholder.png" # Provide a placeholder error image + self.last_weather_update = "Client Init Error" + + + # --- on_load Handler --- + async def start_background_tasks(self): + """Starts the weather background task when the page loads.""" + logging.info("Triggering background tasks: Weather") + # *** FIX: Return a list containing the handler reference *** + return [State.fetch_weather] + + # --- Weather Background Task --- + @rx.event(background=True) + async def fetch_weather(self): + """Fetches the weather screenshot periodically.""" + # Check if the client initialized correctly + if not hasattr(self, '_weather_client') or self._weather_client is None: + logging.warning("Weather client not initialized. Stopping fetch_weather task.") + # Optionally update state to reflect this persistent error + async with self: + self.last_weather_update = "Error: Weather client unavailable" + yield + return # Exit the task permanently if client init failed + + while True: + try: + logging.info("Attempting to fetch weather screenshot...") + # Call the method from the initialized client + img_web_path = self._weather_client.get_weather_screenshot() # This now comes from utils/weather.py + + if img_web_path: + async with self: + timestamp = int(time.time()) + # Update state with cache-busting query param + # Ensure img_web_path is like "/weather.jpg" + self.weather_img = f"{img_web_path}" + self.last_weather_update = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') + logging.info(f"State.weather_img updated to: {self.weather_img}") + yield # Update frontend + else: + logging.warning("get_weather_screenshot returned None. State not updated.") + # Optionally update status + async with self: + self.last_weather_update = f"Failed fetch @ {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}" + yield + + except Exception as e: + logging.error(f"Error in fetch_weather background task: {e}", exc_info=True) + async with self: # Update state to show error + self.last_weather_update = f"Error @ {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}" + # Optionally reset image to placeholder on error + # self.weather_img = WEATHER_IMAGE_PATH yield - await asyncio.sleep(180) # Fetch every 60 seconds - - + await asyncio.sleep(WEATHER_FETCH_INTERVAL) + def index() -> rx.Component: - + """ Main UI definition, preserving original structure. """ return rx.container( rx.center( rx.vstack( + # Original Clock Button rx.button( rx.moment(interval=1000, format="HH:mm:ss"), font_size="4xl", font_weight="bold", color="white", - background_color="#6f42c1", + background_color="#6f42c1", # Original color ), - + # Original Flex Layout for Cards rx.flex( rx.card( rx.box( - rx.text("SPORTS GO HERE"), + rx.text("SPORTS GO HERE"), # Placeholder ), - ), - rx.card( - rx.image( - src="/weather.jpg", # Use relative path to static asset - alt="Weather", - width="100%", - height="auto", - background_color="white", - - ), - ), rx.card( - rx.box( - rx.text("Other sports"), - ), + rx.vstack( # Added vstack for title/image/status + rx.heading("Weather", size="4"), + rx.image( + + src=State.weather_img, + alt="Current weather conditions for Sacramento", + width="100%", # Keep original width setting + height="auto", # Keep original height setting + + object_fit="contain", # Adjust fit as needed + border_radius="var(--radius-3)", # Use theme radius + ), + rx.text( + f"Last Update: {State.last_weather_update}", + size="1", + color_scheme="gray", + padding_top="0.5em" # Add some space + ), + align="center", # Center heading/image/text + spacing="2", + ) ), - ) - + rx.card( + rx.box( + rx.text("Other sports"), # Placeholder + ), + ), + # Original flex settings + spacing="3", # Add spacing between cards + width="100%", + justify="center", # Center cards horizontally + align="stretch", # Stretch cards vertically if needed + ), + # Original vstack settings + align="center", + spacing="4", # Spacing between clock and flex container ), - ), - ), - + ), + # Original container settings + padding="2rem", # Add some padding + max_width="1200px", # Limit width + margin="0 auto", # Center container + ), - -app = rx.App() -app.add_page(index) +# --- App Setup --- +app = rx.App( + theme=rx.theme( + appearance="dark", # Use dark theme + accent_color="purple", + radius="medium", # Apply consistent border radius + ) +) + + +app.add_page( + index, + title="DeathClock", # Example title + on_load=State.start_background_tasks # Trigger tasks when this page loads +) + +# The original TypeError is resolved by returning [State.fetch_weather] +# from State.start_background_tasks. +# The image display is fixed by binding rx.image src to State.weather_img. \ No newline at end of file diff --git a/utils/weather.py b/utils/weather.py index f1fa26b..6dd6ae7 100644 --- a/utils/weather.py +++ b/utils/weather.py @@ -1,55 +1,98 @@ +# /home/death916/code/python/deathclock/utils/weather.py import os import subprocess import datetime +import reflex as rx +import logging # Optional: Use logging for better error messages -class Weather: - def __init__(self): +# Configure logging (optional but recommended) +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# Define the target filename consistently +WEATHER_FILENAME = "weather.jpg" +# Define the web path expected by the frontend +WEATHER_WEB_PATH = f"/{WEATHER_FILENAME}" # This should be relative to the assets dir + +class Weather(rx.Base): + # No __init__ needed here for Pydantic compatibility + + def _get_assets_dir(self) -> str: + """Calculates and ensures the assets directory exists within the project.""" # Get the directory where this script (weather.py) is located - self.script_dir = os.path.dirname(os.path.abspath(__file__)) + # e.g., /home/death916/code/python/deathclock/utils + script_dir = os.path.dirname(os.path.abspath(__file__)) - # Construct the absolute path to the 'assets' directory in the project root - self.assets_dir = os.path.join(os.path.dirname(os.path.dirname(self.script_dir)), 'assets') + # Construct the absolute path to the project root directory + # This should be the parent directory of 'utils' + # e.g., /home/death916/code/python/deathclock + # --- FIX IS HERE --- + project_root = os.path.dirname(script_dir) + # ----------------- + + # Construct the absolute path to the 'assets' directory within the project root + # e.g., /home/death916/code/python/deathclock/assets + assets_dir = os.path.join(project_root, 'assets') # Ensure the assets directory exists - if not os.path.exists(self.assets_dir): - os.makedirs(self.assets_dir) - - def delete_old_screenshots(self): - """ - Deletes all PNG files in the 'assets' directory that start with 'sacramento_weather_'. - """ - for filename in os.listdir(self.assets_dir): - if filename.startswith("sacramento_weather_"): - os.remove(os.path.join(self.assets_dir, filename)) + if not os.path.exists(assets_dir): + try: + os.makedirs(assets_dir) + logging.info(f"Created assets directory: {assets_dir}") + except OSError as e: + logging.error(f"Failed to create assets directory {assets_dir}: {e}") + # If directory creation fails, saving will also likely fail. + # Consider raising an exception or returning None early. + return assets_dir - def get_weather_screenshot(self): + def delete_old_screenshots(self, assets_dir: str): + """Deletes the specific weather file in the given 'assets' directory.""" + target_file = os.path.join(assets_dir, WEATHER_FILENAME) + if os.path.exists(target_file): + try: + os.remove(target_file) + logging.info(f"Deleted old weather file: {target_file}") + except OSError as e: + logging.error(f"Failed to delete old weather file {target_file}: {e}") + + def get_weather_screenshot(self) -> str | None: """ - Fetches weather information for Sacramento from wttr.in using curl and saves it as a PNG. - Returns the path to the saved image. + Fetches weather info using curl, saves it to the project's assets dir. + Returns the web path (e.g., '/weather.jpg') or None on failure. """ + assets_dir = self._get_assets_dir() + # If _get_assets_dir failed (e.g., couldn't create dir), it might be None or invalid. + # Adding a check here could be useful, though currently it returns the path anyway. + # if not assets_dir or not os.path.isdir(assets_dir): + # logging.error("Assets directory path is invalid or missing.") + # return None + + # Full path to save the file, e.g., /home/death916/code/python/deathclock/assets/weather.jpg + screenshot_path = os.path.join(assets_dir, WEATHER_FILENAME) + try: - # Create a timestamp for the filename - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - screenshot_filename = "weather.png" - screenshot_path = os.path.join(self.assets_dir, screenshot_filename) # save to the proper location - - # Use curl to get the weather data from wttr.in and save it as a PNG. - # add the scale #2 to make the png larger curl_command = [ "curl", "-s", # Silent mode - "v2.wttr.in/Sacramento.png?0pq&scale=.5", # Fetch weather data for Sacramento + "v2.wttr.in/Sacramento.png?0T", # Fetch PNG, no border, no terminal escapes "-o", - screenshot_path, + screenshot_path, # Save to the correct assets path ] - self.delete_old_screenshots() - subprocess.run(curl_command, check=True) - return screenshot_path + # Delete the old file before creating the new one + self.delete_old_screenshots(assets_dir) + + logging.info(f"Running curl command to fetch weather: {' '.join(curl_command)}") + process = subprocess.run(curl_command, check=True, capture_output=True, text=True) + logging.info(f"Curl command successful. Weather image saved to: {screenshot_path}") # Log correct save path + + # *** Return the WEB PATH, which is relative to the assets dir *** + # This part was already correct. Reflex serves the 'assets' folder at the root URL. + return WEATHER_WEB_PATH # e.g., "/weather.jpg" except subprocess.CalledProcessError as e: - print(f"Error fetching weather data: {e}") + logging.error(f"Curl command failed for path {screenshot_path}: {e}") + logging.error(f"Curl stderr: {e.stderr}") return None except Exception as e: - print(f"An unexpected error occurred: {e}") - return None + logging.error(f"An unexpected error occurred saving to {screenshot_path}: {e}") + return None \ No newline at end of file