π F1 Calendar Script β Google Calendar Integration for Race Automation
F1 Calendar Script is a lightweight personal automation tool that integrates with Google Calendar to detect upcoming Formula 1 events and trigger local scripts β such as launching a Formula 1 streaming app on an Android TV box.
The script runs entirely on your own Windows PC or server, respects your privacy, and requires only read-only access to your Google Calendar. Itβs ideal for fans who want to automate their race-day setup without cloud dependencies.
π§ Features
- Monitors shared or personal Google Calendar events with βF1β in the title
- Automatically runs
.bat
scripts to start and stop apps or systems - Works with Android TV via ADB, smart home hubs, or any local automation
- Logs activity and avoids duplicate triggers
- Runs on Windows (Python 3.x required)
π Privacy & Security
- Calendar access is read-only
- No personal data is stored or shared
- The app runs 100% locally
- Full privacy policy and terms of service available
π₯ Download & Setup
- http://bytesmith17.co.uk/wp-content/uploads/2025/06/F1-Calendar-Script.zip
- Instructions included in the README
- Requires:
- A Google Cloud API project with Calendar API enabled
- OAuth client credentials (JSON)
- One-time authorization via browser (for access token)
- Python 3.8+ with dependencies: nginxCopyEdit
pip install google-auth google-auth-oauthlib google-api-python-client
π§ͺ Verification Status
This app has been submitted to Google for verification to remove unverified app warnings and allow long-term token use. Until approved, you may see a security prompt when authorizing access.
π‘ Want to Contribute or Learn More?
This tool was developed in collaboration with ChatGPT by OpenAI to help F1 fans streamline their home race setup.
import datetime
import os
import re
import json
import sys
import warnings
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
# === CONFIGURATION ===
START_SCRIPT = r"C:\Users\mnorth\Desktop\F1-Start.bat"
STOP_SCRIPT = r"C:\Users\mnorth\Desktop\F1-Stop.bat"
CALENDAR_ID = 'lr1dt7l71u9929odlmfkvuq8u56gso3q@import.calendar.google.com'
KEYWORD = 'F1'
LOG_FILE_PATH = r"C:\Users\mnorth\Desktop\f1_calendar_log.txt"
STATE_FILE = r"C:\Users\mnorth\Desktop\f1_state.json"
LOCK_FILE = r"C:\Users\mnorth\Desktop\f1_calendar.lock"
ENABLE_LOGGING_TO_FILE = True
# === EARLY LOGGING FUNCTION ===
def early_log(msg):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
with open(LOG_FILE_PATH, "a", encoding="utf-8") as f:
f.write(f"{timestamp} - {msg}\n")
except Exception as e:
print(f"[EARLY_LOG ERROR] {e}")
print(msg)
# === LOCK FILE CHECK ===
if os.path.exists(LOCK_FILE):
early_log("[INFO] Lock file exists. Another instance may be running. Exiting.")
sys.exit()
else:
with open(LOCK_FILE, "w") as f:
f.write(str(os.getpid()))
# === Suppress warnings ===
warnings.filterwarnings("ignore", category=DeprecationWarning)
# === Logging with 24-hour retention and max 1000 lines ===
def log_message(msg):
print(msg)
if not ENABLE_LOGGING_TO_FILE:
return
try:
now = datetime.datetime.now()
lines_to_keep = []
if os.path.exists(LOG_FILE_PATH):
with open(LOG_FILE_PATH, "r", encoding="utf-8") as f:
for line in f:
match = re.match(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", line)
if match:
timestamp = datetime.datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
if (now - timestamp).total_seconds() <= 86400:
lines_to_keep.append(line)
elif line.startswith("==="):
lines_to_keep.append(line)
if not any(f"=== {now.strftime('%Y-%m-%d')} ===" in line for line in lines_to_keep):
lines_to_keep.append(f"\n=== {now.strftime('%Y-%m-%d')} ===\n")
new_line = f"{now.strftime('%Y-%m-%d %H:%M:%S')} - {msg}\n"
lines_to_keep.append(new_line)
if len(lines_to_keep) > 1000:
lines_to_keep = lines_to_keep[-1000:]
with open(LOG_FILE_PATH, "w", encoding="utf-8") as f:
f.writelines(lines_to_keep)
except Exception as e:
print(f"[ERROR] Failed to write to log file: {e}")
def load_state():
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f)
log_message(f"[STATE] Loaded state file with {len(state)} entries.")
return state
except Exception as e:
log_message(f"[WARN] Failed to load state file: {e}")
return {}
log_message("[STATE] No existing state file found.")
return {}
def save_state(state):
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f)
log_message("[INFO] Script launched.")
# === Authenticate ===
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
try:
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
service = build('calendar', 'v3', credentials=creds)
except Exception as e:
log_message(f"[ERROR] Failed to authenticate: {e}")
if os.path.exists(LOCK_FILE): os.remove(LOCK_FILE)
sys.exit()
# === Define time window ===
now_utc = datetime.datetime.now(datetime.timezone.utc)
past = (now_utc - datetime.timedelta(hours=4)).isoformat()
future = (now_utc + datetime.timedelta(hours=2)).isoformat()
log_message(f"[INFO] Checking for '{KEYWORD}' events from {past} to {future}")
# === Fetch events ===
try:
events_result = service.events().list(
calendarId=CALENDAR_ID,
timeMin=past,
timeMax=future,
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
except Exception as e:
log_message(f"[ERROR] Failed to retrieve events: {e}")
if os.path.exists(LOCK_FILE): os.remove(LOCK_FILE)
sys.exit()
log_message(f"[DEBUG] Found {len(events)} total upcoming event(s).")
state = load_state()
found_matching = False
for event in events:
title = event.get('summary', '(No Title)')
start_str = event['start'].get('dateTime')
end_str = event['end'].get('dateTime')
event_id = event.get('id')
log_message(f"[EVENT] Title: {title}")
log_message(f" Start: {start_str}")
log_message(f" End: {end_str}")
if KEYWORD not in title:
continue
if not start_str or not end_str:
log_message(f"[WARN] Skipping event with missing times.")
continue
found_matching = True
try:
start_dt = datetime.datetime.fromisoformat(start_str).astimezone(datetime.timezone.utc)
end_dt = datetime.datetime.fromisoformat(end_str).astimezone(datetime.timezone.utc)
except Exception as e:
log_message(f"[ERROR] Failed to parse datetime: {e}")
continue
seconds_to_start = (start_dt - now_utc).total_seconds()
seconds_since_end = (now_utc - end_dt).total_seconds()
if 300 <= seconds_to_start <= 600:
if state.get(event_id) != "started":
log_message(f"[ACTION] 5β10 minutes before event start. Running START script...")
os.system(f'"{START_SCRIPT}"')
state[event_id] = "started"
save_state(state)
else:
log_message(f"[SKIP] START script already run for this event.")
elif 3600 <= seconds_since_end <= 7200:
if state.get(event_id) != "stopped":
log_message(f"[ACTION] 1β2 hours after event end. Running STOP script...")
os.system(f'"{STOP_SCRIPT}"')
state[event_id] = "stopped"
save_state(state)
else:
log_message(f"[SKIP] STOP script already run for this event.")
if not found_matching:
log_message(f"[INFO] No matching '{KEYWORD}' events found in the checked window.")
log_message("[INFO] Script finished.")
if os.path.exists(LOCK_FILE):
os.remove(LOCK_FILE)