base of reflex re write. centered clock and weather

This commit is contained in:
Death916 2025-04-21 05:17:44 -07:00
parent aba3a7b641
commit 3bec50f40e
24 changed files with 1563 additions and 6838 deletions

View file

@ -1,4 +0,0 @@
*.png
*.txt
chromedriver
*.pyc

View file

@ -1 +0,0 @@
__version__ = '0.1.0'

View file

@ -1,28 +0,0 @@
#alarm.py
# alarm component for dash app
from dash import html, dcc
import datetime
import time
class Alarm:
def __init__(self):
self.current_time = datetime.datetime.now()
self.alarms = []
self.alarm_times = []
def add_alarm(self, alarm_time,current_time):
self.alarms.append(alarm_time)
self.current_time = current_time
print(f"Alarm set for {alarm_time}")
def check_alarm(self):
current_time = datetime.datetime.now()
if current_time.time() in self.alarm_times:
print("Alarm triggered!")
return True
return False

View file

@ -1,35 +0,0 @@
import datetime
from dash import html, dcc, Input, Output, State
import alarm
class AlarmModule:
def __init__(self, app):
self.app = app
self.alarm_obj = self.get_alarm_object()
self._alarm_time = None
self.setup_callbacks()
def get_alarm_object(self):
return alarm.Alarm()
def setup_callbacks(self):
@self.app.callback(
Output('time-input', 'style'),
Input('clock-display', 'n_clicks'),
State('time-input', 'style')
)
def toggle_time_input(n_clicks, current_style):
if n_clicks and n_clicks % 2 == 1:
return {'display': 'block', 'margin': '0 auto'}
return {'display': 'none', 'margin': '0 auto'}
@self.app.callback(
Output('output-selected-time', 'children'),
Input('time-input', 'value')
)
def process_selected_time(time_value):
if time_value:
self._alarm_time = time_value
self.alarm_obj.add_alarm(time_value, datetime.datetime.now())
return f'Alarm time: {time_value}'
return 'No time selected yet.'

View file

@ -1,57 +0,0 @@
from dash import Dash, html, dcc
from weather_module import WeatherModule
from news_module import NewsModule
from scores_module import ScoresModule
from alarm_module import AlarmModule
from clock_module import ClockModule
def create_app():
app = Dash(__name__)
app.layout = html.Div([
html.H1(id='clock-display', style={'textAlign': 'center', 'cursor': 'pointer'}),
dcc.Input(id='time-input', type='time', style={'display': 'none', 'margin': '0 auto'}),
html.Div(id='output-selected-time', style={'textAlign': 'center', 'marginBottom': '20px'}),
html.Div([
html.Div([
html.H2("NBA Scores"),
html.Div(id='nba-scores-display', className='score-container')
], id='nba-container', style={'flex': '1'}),
html.Div(id='weather-display', style={"display": "flex", "justify-content": "center", "margin-bottom":"20px", 'flex':'1'}),
html.Div([
html.H2("MLB Scores"),
html.Div(id='mlb-scores-display', className='score-container',style={'column-count': '2', 'column-gap': '10px'})
], id='mlb-container', style={'flex': '1'}),
], id='main-content-container', style={"display": "flex", "gap": "20px", 'flex-wrap': 'wrap'}),
html.Div(id='news-ticker'),
dcc.Interval(id='clock-interval', interval=60000, n_intervals=0),
dcc.Interval(id='weather-interval', interval=550000, n_intervals=0),
dcc.Interval(id='news-interval', interval=300000, n_intervals=0),
dcc.Interval(id='nba-interval', interval=300000, n_intervals=0),
dcc.Interval(id='mlb-interval', interval=300000, n_intervals=0)
])
ClockModule(app)
WeatherModule(app)
NewsModule(app)
scores_module = ScoresModule(app)
scores_module.setup_mlb_callbacks()
alarm_module = AlarmModule(app)
def check_alarms():
trigg = alarm_module.alarm_obj.check_alarm()
if trigg:
print("ALARM TRIGGERED!")
check_alarms()
return app
if __name__ == '__main__':
app = create_app()
app.run_server(debug=False, host='0.0.0.0', port=8050)

View file

@ -1,224 +0,0 @@
body {
background-color: #1a1a2e;
color: #e6e6fa;
font-family: 'Arial', sans-serif;
margin: 0;
padding: 10px;
font-size: 16px;
overflow-x: hidden;
min-width: 360px;
}
h1 {
font-size: 2rem;
margin-bottom: 10px;
}
h2 {
font-size: 1.5rem;
margin-bottom: 10px;
}
/* Main Content Container (Scores and Weather) */
#main-content-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
width: 100%;
justify-content: center;
}
#nba-container,
#mlb-container {
flex: 1;
min-width: 300px;
}
/* Individual Score Containers */
.score-container {
width: 100%;
padding: 0; /* Remove padding from score container */
margin: 0; /* remove margin from score container */
}
#mlb-scores-display {
column-count: 1;
}
.score-box {
background-color: #232338;
padding: 2px 5px; /* Reduced padding on score boxes */
margin-bottom: 2px; /* Reduced margin on score boxes */
border-radius: 6px;
border: 1px solid #4a4a82;
display: flex; /* Use flexbox for inner alignment */
flex-direction: column; /* Stack the game-score and game-period vertically */
justify-content: center; /* Center vertically */
}
.game-score {
font-size: 0.9em;
color: #e6e6fa;
text-align: center;
margin: 2px 0; /* Add a small top/bottom margin */
}
.game-period {
font-size: 0.8em;
color: #b39ddb;
text-align: center;
margin: 2px 0; /* Add a small top/bottom margin */
}
/* Weather Styles */
#weather-display {
background-color: #232338;
border-radius: 10px;
padding: 10px;
margin: 10px auto;
max-width: 100%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid #4a4a82;
transition: all 0.3s ease;
flex: 1;
}
#weather-display:hover {
box-shadow: 0 6px 8px rgba(179, 157, 219, 0.2);
transform: translateY(-2px);
}
#weather-display img {
width: 100%;
border-radius: 8px;
margin-top: 5px;
}
/* News Ticker Styles */
.ticker {
position: fixed;
bottom: 0;
width: 100%;
overflow: hidden;
white-space: nowrap;
background-color: #232338;
padding: 5px;
border-radius: 8px;
border: 1px solid #4a4a82;
}
.news-item {
display: inline-block;
padding-right: 30px;
color: #d1c4e9;
animation: ticker linear infinite;
}
@keyframes ticker {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
/* Media Queries */
@media (min-width: 750px) {
/* Landscape layout */
body {
padding: 20px;
font-size: 18px;
max-width: 1500px;
margin: 0 auto;
}
#main-content-container {
flex-wrap: nowrap;
justify-content: space-around;
}
#mlb-scores-display {
column-count: 2;
}
.score-container {
padding: 0; /* Remove padding from score container */
margin: 0; /* remove margin from score container */
}
.score-box {
padding: 2px 5px; /* Reduced padding on score boxes */
margin-bottom: 2px; /* Reduced margin on score boxes */
}
#weather-display {
padding: 20px;
margin: 0;
max-width: 400px;
}
.ticker {
padding: 15px;
}
.news-item {
padding-right: 50px;
}
}
@media (max-width: 480px) {
/* Phone screens */
body {
font-size: 14px;
padding: 0;
transform-origin: top left;
transform: scale(0.6);
width: 166.66%;
margin-left: -33.33%;
min-width: 0;
}
h1 {
font-size: 1.5rem;
}
h2 {
font-size: 1.2rem;
}
#main-content-container {
gap: 1px;
}
#nba-container,
#mlb-container {
min-width: 0;
}
.score-container {
padding: 0; /* Remove padding from score container */
margin: 0; /* remove margin from score container */
}
.score-box {
padding: 2px 5px; /* Reduced padding on score boxes */
margin-bottom: 2px; /* Reduced margin on score boxes */
}
.game-score {
font-size: 0.8em;
margin: 2px 0; /* Add a small top/bottom margin */
}
.game-period {
font-size: 0.7em;
margin: 2px 0; /* Add a small top/bottom margin */
}
.ticker {
padding: 5px;
}
.news-item {
padding-right: 15px;
}
}

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1,15 +0,0 @@
from time import strftime, localtime
from dash import Input, Output
class ClockModule:
def __init__(self, app):
self.app = app
self.setup_callbacks()
def setup_callbacks(self):
@self.app.callback(
Output('clock-display', 'children'),
Input('clock-interval', 'n_intervals')
)
def update_time(n):
return strftime("%B %d, %I:%M %p", localtime())

View file

@ -0,0 +1,57 @@
"""Welcome to Reflex! This file outlines the steps to create a basic app."""
import reflex as rx
from datetime import datetime, timezone
import asyncio
from rxconfig import config
from utils.weather import Weather
from utils.scores import NBAScores, mlbScores
from utils.news import News
from utils.alarm import Alarm
class State(rx.State):
"""The app state."""
def index() -> rx.Component:
return rx.container(
rx.center(
rx.vstack(
rx.button(
rx.moment(interval=1000, format="HH:mm:ss"),
font_size="4xl",
font_weight="bold",
color="white",
background_color="#6f42c1",
),
rx.box(
rx.image(
src=Weather().get_weather_screenshot(),
alt="Weather",
width="100%",
height="auto",
background_color="white",
),
),
),
),
),
app = rx.App()
app.add_page(index)

View file

@ -1,107 +0,0 @@
#deathclock
import datetime
import time
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QTimer, QObject, Signal, Slot, Property
from time import strftime, localtime
import requests
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import os
import news
import weather
class clock():
def update_time(self):
curr_time = strftime("%B %d, %I:%M %p", localtime())
engine.rootObjects()[0].setProperty('currTime', curr_time)
return curr_time
def time_and_date():
print(time.asctime())
def alarm():
alarm_time = input("what time should the alarm be set?")
def ring():
pass
"""
class gui():
def handleTouchAreaPressed(self, signal):
# Implement your desired behavior when the left area is pressed
print("here touch area")
leftTouchAreaMouse = engine.rootObjects()[0].findChild("leftTouchAreaMouse")
leftTouchAreaMouse.connect(b"touchAreaPressed", self.handleTouchAreaPressed)
print("Left area pressed!")
"""
def main():
#gui_obj = gui()
app = QGuiApplication(sys.argv)
global engine
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load( 'main.qml')
# create instance of weather class
weather_obj = weather.Weather()
weather_obj.weatherUpdated.connect(lambda weather_map_path: engine.rootContext().setContextProperty("weatherMapPath", weather_map_path))
# set timer for weather map
weatherTimer = QTimer()
weatherTimer.setInterval(300000) # 10 minutes
weatherTimer.timeout.connect(weather_obj.download_sacramento_weather_map(engine))
weather_obj.download_sacramento_weather_map(engine)
weatherTimer.start()
# create instance of clock class
timeupdate = clock()
# start timer for clock
timer = QTimer()
timer.setInterval(100) # msecs 100 = 1/10th sec
timer.timeout.connect(timeupdate.update_time)
timer.start()
# create instance of news class
news_obj = news.news()
news_ticker = news_obj.get_news()
#print(news_obj._news_dict)
#print(news_ticker)
news_context = engine.rootContext()
news_context.setContextProperty("news", str(news_ticker))
#start timer for news
news_timer = QTimer()
news_timer.timeout.connect(news_obj.get_news)
news_timer.setInterval(300000) # 10 minutes #changed to 5 mins
news_timer.start()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

View file

@ -1,107 +0,0 @@
import feedparser
import asyncio
import aiofiles
import random
from time import localtime, strftime
import socket
import aiohttp
def print_time():
print(strftime("%B %d, %I:%M %p", localtime()))
class News:
def __init__(self):
self._news_dict = {}
self._news_dict_length = 0
socket.setdefaulttimeout(10) # Set default timeout for socket operations
async def _fetch_feed(self, session, feed):
"""Fetches and parses a single feed asynchronously."""
max_entries = 10 # Maximum number of entries to fetch from each feed
try:
# Add timeout to the request
timeout = aiohttp.ClientTimeout(total=5)
async with session.get(feed, timeout=timeout) as response:
if response.status != 200:
print(f"Skip feed {feed}: status {response.status}")
return []
text = await response.text()
d = feedparser.parse(text)
if hasattr(d, 'status') and d.status != 200:
print(f"Skip feed {feed}: status {d.status}")
return []
feed_entries = []
# Limit the number of entries parsed
for i, post in enumerate(d.entries):
if i >= max_entries:
break # Stop parsing if we've reached the limit
feed_entries.append({
'title': post.title,
'source': d.feed.title if hasattr(d.feed, 'title') else 'Unknown',
'publish_date': post.published if hasattr(post, 'published') else '',
'summary': post.summary if hasattr(post, 'summary') else ''
})
print(f"Added {len(feed_entries)} entries from {feed}")
return feed_entries
except aiohttp.ClientError as e:
print(f"Error processing feed {feed}: {e}")
return []
except Exception as e:
print(f"Error processing feed {feed}: {e}")
return []
async def get_news(self):
print_time()
feeds = []
self._news_dict = {}
self._news_dict_length = 0
try:
async with aiofiles.open("feeds.txt", "r") as f:
async for line in f:
feeds.append(line.strip())
except Exception as e:
print(f"Error reading feeds.txt: {e}")
return {}
# Limit the number of feeds to process at once
if len(feeds) > 10:
feeds = random.sample(feeds, 10)
print("Getting news entries...")
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = [self._fetch_feed(session, feed) for feed in feeds]
all_feed_entries_list = await asyncio.gather(*tasks, return_exceptions=True)
all_entries = []
for result in all_feed_entries_list:
if isinstance(result, list) and result:
all_entries.extend(result)
if not all_entries:
print("No entries collected")
return {}
if len(all_entries) > 30:
all_entries = random.sample(all_entries, 30)
for entry in all_entries:
self._news_dict[entry['title']] = entry
try:
async with aiofiles.open("news.txt", "w") as f:
print("Writing news to file...")
for entry in self._news_dict.values():
await f.write(f"[{entry['publish_date']}] {entry['source']}: {entry['title']}\n")
except Exception as e:
print(f"Error writing to news.txt: {e}")
return self._news_dict

View file

@ -1,66 +0,0 @@
import datetime
import asyncio
from dash import html, Input, Output, callback, no_update
from dash.exceptions import PreventUpdate
from news import News
class NewsModule:
def __init__(self, app):
self.app = app
self.news_obj = self.get_news_object()
self._last_news_update = datetime.datetime(2000, 1, 1)
self._cached_news = self.create_loading_message() # Initial loading message
self._initial_run = True
self.setup_callbacks()
def get_news_object(self):
return News()
def create_loading_message(self):
return html.Div("Loading...")
def setup_callbacks(self):
@self.app.callback(
Output('news-ticker', 'children'),
Input('news-interval', 'n_intervals')
)
def update_news(n):
if n is None:
return self._cached_news
current_time = datetime.datetime.now()
time_since_update = (current_time - self._last_news_update).total_seconds()
# Only update if it's been more than 5 minutes or it's the initial run
if time_since_update < 300 and not self._initial_run:
return self._cached_news
try:
print("UPDATING NEWS...")
# Create a new event loop for this request
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
headlines_dict = loop.run_until_complete(self.news_obj.get_news())
loop.close()
if not headlines_dict:
return html.Div("No news available at this time.", className="ticker")
combined_items = " | ".join([f"{data['source']}: {headline}"
for headline, data in headlines_dict.items()])
text_px = len(combined_items) * 8
scroll_speed = 75
duration = max(text_px / scroll_speed, 20)
ticker_style = {"animationDuration": f"{duration}s"}
self._cached_news = html.Div(
html.Span(combined_items, className="news-item", style=ticker_style),
className='ticker'
)
self._last_news_update = current_time
self._initial_run = False
return self._cached_news
except Exception as e:
print(f"Error updating news: {e}")
return html.Div("No news available.")

View file

@ -1,96 +0,0 @@
from nba_api.live.nba.endpoints import scoreboard
from datetime import datetime, timedelta
import statsapi
class NBAScores:
def __init__(self):
self._scores = []
def get_scores(self):
try:
# Get scoreboard data
board = scoreboard.ScoreBoard()
data = board.get_dict()
# Check if we have a valid scoreboard response
if 'scoreboard' not in data:
print("No scoreboard data found")
return []
games = data['scoreboard'].get('games', [])
if not games:
print("No games found in scoreboard")
return []
scores_list = []
for game in games:
try:
game_data = {
'home_team': game['homeTeam']['teamTricode'],
'home_score': game['homeTeam']['score'],
'away_team': game['awayTeam']['teamTricode'],
'away_score': game['awayTeam']['score'],
'period': game['period'],
'status': game['gameStatusText']
}
scores_list.append(game_data)
except KeyError as e:
print(f"Error processing game data: {e}")
continue
self._scores = scores_list
return self._scores
except Exception as e:
print(f"Error fetching scores: {e}")
return []
class mlbScores:
def __init__(self):
self._scores = []
self._games = []
def get_games(self):
try:
# Get MLB games data
games = statsapi.schedule()
self._games = games
return self._games
except Exception as e:
print(f"Error fetching MLB games: {e}")
return []
def get_scores(self):
games = self.get_games()
scores_list = []
if not games:
print("No mlb games found")
return []
for game in games:
try:
game_data = {
'home_team': game['home_name'],
'home_score': game['home_score'],
'away_team': game['away_name'],
'away_score': game['away_score'],
'status': game['status']
}
scores_list.append(game_data)
except KeyError as e:
print(f"Error processing game data: {e}")
continue
return scores_list # RETURN THE LIST
if __name__ == "__main__":
scores = NBAScores()
results = scores.get_scores()
print("\nNBA Scores:")
if results:
for game in results:
print(f"{game['away_team']} {game['away_score']} @ "
f"{game['home_team']} {game['home_score']} "
f"(Status: {game['status']})")
else:
print("No games available")

View file

@ -1,69 +0,0 @@
from dash import html, Input, Output
from scores import NBAScores # Import NBAScores class
from scores import mlbScores # Import mlbScores class
class ScoresModule:
def __init__(self, app):
self.app = app
self.scores_obj = self.get_scores_object()
self.mlb_scores_obj = self.get_mlb_scores_object()
self.setup_callbacks()
def get_scores_object(self):
return NBAScores()
def setup_callbacks(self):
@self.app.callback(
Output('nba-scores-display', 'children'),
Input('nba-interval', 'n_intervals')
)
def update_scores(n):
try:
games = self.scores_obj.get_scores()
if not games:
print("no nba games found")
return html.Div("No games available", className='text-center')
return html.Div([
html.Div([
html.Div([
html.Span(f"{game['away_team']} {game['away_score']}"),
html.Span(" @ "),
html.Span(f"{game['home_team']} {game['home_score']}")
], className='game-score'),
html.Div(f"Period: {game['period']}", className='game-period')
], className='score-box')
for game in games
], className='score-container')
except Exception as e:
return html.Div("Scores unavailable")
def get_mlb_scores_object(self):
return mlbScores()
def setup_mlb_callbacks(self):
@self.app.callback(
Output('mlb-scores-display', 'children'),
Input('mlb-interval', 'n_intervals')
)
def update_mlb_scores(n):
try:
games = self.mlb_scores_obj.get_scores()
print("Updating MLB Scores")
if not games:
print("no mlb games found")
return html.Div("No games available", className='text-center')
return html.Div([
html.Div([
html.Div([
html.Span(f"{game['away_team']} {game['away_score']}"),
html.Span(" @ "),
html.Span(f"{game['home_team']} {game['home_score']}")
], className='game-score'),
html.Div(f"Status: {game['status']}", className='game-period')
], className='score-box')
for game in games
], className='score-container')
except Exception as e:
print(f"Error updating MLB Scores: {e}")
return html.Div("Scores unavailable")

View file

@ -1,55 +0,0 @@
import os
import subprocess
import datetime
class Weather:
def __init__(self):
# Get the directory where this script (weather.py) is located
self.script_dir = os.path.dirname(os.path.abspath(__file__))
# Construct the absolute path to the 'assets' directory in the same directory as the script
self.assets_dir = os.path.join(self.script_dir, '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))
def get_weather_screenshot(self):
"""
Fetches weather information for Sacramento from wttr.in using curl and saves it as a PNG.
Returns the path to the saved image.
"""
try:
# Create a timestamp for the filename
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
screenshot_filename = f"sacramento_weather_{timestamp}.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
"-o",
screenshot_path,
]
self.delete_old_screenshots()
subprocess.run(curl_command, check=True)
return screenshot_path
except subprocess.CalledProcessError as e:
print(f"Error fetching weather data: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred: {e}")
return None

View file

@ -1,36 +0,0 @@
import datetime
import os
from dash import html, Input, Output
from weather import Weather # Import Weather class
class WeatherModule:
def __init__(self, app):
self.app = app
self.weather_obj = self.get_weather_object()
self.setup_callbacks()
def get_weather_object(self):
return Weather()
def setup_callbacks(self):
@self.app.callback(
Output('weather-display', 'children'),
Input('weather-interval', 'n_intervals')
)
def update_weather(n):
try:
print("UPDATING WEATHER...")
screenshot_path = self.weather_obj.get_weather_screenshot()
image_name = os.path.basename(screenshot_path)
return html.Div(
[
html.H2("Sacramento Weather"),
html.Img(
src=self.app.get_asset_url(image_name + f"?v={datetime.datetime.now().timestamp()}"),
style={"width": "100%", "display": "block", "image-rendering": "crisp-edges"}
),
],
style={"width": "600px", "margin": "0 auto", "border": "1px solid black"}
)
except Exception as e:
return html.Div(f"Weather update error: {str(e)}")