Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 70 additions & 68 deletions badge/apps/christmas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
from badgeware import screen, PixelFont, shapes, brushes, io, run, Matrix

try:
from urllib.urequest import urlopen
import json
import network
import ntptime
NETWORK_AVAILABLE = True
except ImportError:
NETWORK_AVAILABLE = False
Expand Down Expand Up @@ -68,12 +67,11 @@ def draw(self):
MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

# Cache for fetched time data
_cached_time = None
_last_fetch_attempt = 0
_fetch_success = False
FETCH_INTERVAL = 60 * 60 * 1000 # Try to fetch once per hour (in milliseconds) when successful
RETRY_INTERVAL = 5 * 1000 # Retry every 5 seconds (in milliseconds) when failed
# NTP sync state
_ntp_synced = False
_ntp_sync_attempt = 0
_last_sync_attempt = 0
SYNC_RETRY_INTERVAL = 5 * 1000 # Retry NTP sync every 5 seconds (in milliseconds) when failed

# Network connection state
WIFI_TIMEOUT = 60
Expand Down Expand Up @@ -180,67 +178,68 @@ def wlan_start():
pass
return False

def fetch_current_date():
def sync_time_via_ntp():
"""
Fetch current date from worldtimeapi.org
Returns (year, month, day) tuple or None if fetch fails
Sync system time using NTP
Returns True if sync was successful, False otherwise
"""
global _cached_time, _last_fetch_attempt, _fetch_success
global _ntp_synced, _ntp_sync_attempt, _last_sync_attempt

if not NETWORK_AVAILABLE:
return None
return False

if not connected:
return None
return False

# Return cached time if still valid
current_ticks = io.ticks
if _cached_time and _fetch_success and (current_ticks - _last_fetch_attempt < FETCH_INTERVAL):
return _cached_time
# If already synced, don't sync again
if _ntp_synced:
return True
Comment on lines +195 to +196
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once NTP sync succeeds, the function returns early and never attempts to re-sync. If the system time drifts significantly or the device runs for an extended period, the time could become inaccurate. Consider implementing periodic re-synchronization (e.g., once per day) to maintain accuracy over longer runtime periods.

Copilot uses AI. Check for mistakes.

# Check if we should retry (use shorter interval when failed)
if not _fetch_success and _last_fetch_attempt > 0:
if current_ticks - _last_fetch_attempt < RETRY_INTERVAL:
return None # Return None to indicate we're still waiting to retry
# Check if we should retry
current_ticks = io.ticks
if _ntp_sync_attempt > 0:
if current_ticks - _last_sync_attempt < SYNC_RETRY_INTERVAL:
return False # Still waiting to retry

# Update last fetch attempt timestamp before trying
_last_fetch_attempt = current_ticks
# Update last sync attempt timestamp
_last_sync_attempt = current_ticks
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variable '_last_sync_attempt' is not used.

Copilot uses AI. Check for mistakes.
_ntp_sync_attempt += 1
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variable _ntp_sync_attempt is incremented without thread-safety mechanisms. If sync_time_via_ntp() could be called from multiple contexts concurrently, this could lead to race conditions. Consider adding synchronization or documenting that this function must only be called from a single thread.

Copilot uses AI. Check for mistakes.

# Attempt to fetch current date from the API
try:
# Use worldtimeapi.org - a free API that doesn't require authentication
# Reduced timeout to 3 seconds to keep badge responsive
response = urlopen("https://worldtimeapi.org/api/timezone/Etc/UTC", timeout=3)
try:
data = response.read()
time_data = json.loads(data)
# datetime format: "2025-10-30T01:23:45.123456+00:00"
datetime_str = time_data.get("datetime", "")

if datetime_str:
# Parse the date part (YYYY-MM-DD) with validation
try:
if "T" not in datetime_str:
raise ValueError("datetime string missing 'T' separator")
date_part = datetime_str.split("T")[0]
parts = date_part.split("-")
if len(parts) != 3:
raise ValueError("date part does not have three components")
year, month, day = parts
_cached_time = (int(year), int(month), int(day))
_fetch_success = True
return _cached_time
except (ValueError, TypeError) as parse_err:
print(f"Failed to parse date from API response: {parse_err}")
_fetch_success = False
finally:
response.close()
# Sync time via NTP
ntptime.settime()
_ntp_synced = True
print("NTP time sync successful")
return True
except Exception as e:
# Network request failed, will retry on next attempt
print(f"Failed to fetch time from internet: {e}")
_fetch_success = False
print(f"NTP sync failed (attempt {_ntp_sync_attempt}): {e}")
return False

def get_current_date():
"""
Get current date from system time (after NTP sync)
Returns (year, month, day) tuple or None if time is not synced
"""
if not _ntp_synced:
return None

return None
try:
# Get current time from system
# time.localtime() returns: (year, month, day, hour, minute, second, weekday, yearday)
current_time = time.localtime()
year = current_time[0]
month = current_time[1]
day = current_time[2]

# Sanity check: if year is unreasonable, NTP didn't actually work
# Valid range: 2025-2100 (the badge was created in 2025)
if year < 2025 or year > 2100:
Comment on lines +235 to +236
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-coded year range 2025-2100 will cause this code to fail in 2101. Consider making the lower bound more flexible (e.g., checking year >= 2024 to allow for some margin) or using a constant that can be easily updated. The upper bound check may be unnecessary unless there's a specific reason to distrust years beyond 2100.

Suggested change
# Valid range: 2025-2100 (the badge was created in 2025)
if year < 2025 or year > 2100:
# Valid lower bound: badge was created in 2025, but allow margin
MIN_VALID_YEAR = 2024
if year < MIN_VALID_YEAR:

Copilot uses AI. Check for mistakes.
return None

return (year, month, day)
except Exception as e:
print(f"Failed to get current date from system time: {e}")
return None

def format_date(year, month, day):
"""
Expand All @@ -254,30 +253,30 @@ def get_current_date_string():
Get current date formatted as DD MMM YYYY
Returns formatted string or "thinking..." if date cannot be determined
"""
# Try to fetch current date from internet
fetched_date = fetch_current_date()
# Get current date from NTP-synced system time
current_date = get_current_date()

if fetched_date:
# Use internet time
year, month, day = fetched_date
if current_date:
# Use synced time
year, month, day = current_date
return format_date(year, month, day)

# Return "thinking..." if we don't have a date yet (no fallback to local time)
# Return "thinking..." if we don't have a synced time yet
return "thinking..."

def get_days_until_christmas():
"""
Calculate days until next Christmas (Dec 25)
Returns None if date cannot be determined from network
Returns None if date cannot be determined from NTP-synced time
"""
# Try to fetch current date from internet
fetched_date = fetch_current_date()
# Get current date from NTP-synced system time
current_date = get_current_date()

if not fetched_date:
# Cannot calculate without valid date from network
if not current_date:
# Cannot calculate without valid synced date
return None

current_year, current_month, current_day = fetched_date
current_year, current_month, current_day = current_date

# Determine which Christmas to count down to
christmas_year = current_year
Expand Down Expand Up @@ -328,6 +327,9 @@ def update():
if NETWORK_AVAILABLE:
if get_connection_details():
wlan_start()
# Once connected, attempt NTP sync
if connected:
sync_time_via_ntp()

# Calculate days until Christmas
days = get_days_until_christmas()
Expand Down