mirror of
https://github.com/Death916/deathclock.git
synced 2026-04-10 03:04:40 -07:00
weather updates fixed
This commit is contained in:
parent
8a162c0057
commit
a2f22f481e
2 changed files with 234 additions and 89 deletions
|
|
@ -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.
|
||||
109
utils/weather.py
109
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue