Files
GarminSync/garminsync/daemon.py

161 lines
5.6 KiB
Python

import signal
import sys
import time
import threading
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from .database import get_session, Activity, DaemonConfig, SyncLog
from .garmin import GarminClient
from .utils import logger
class GarminSyncDaemon:
def __init__(self):
self.scheduler = BackgroundScheduler()
self.running = False
self.web_server = None
def start(self, web_port=8080):
"""Start daemon with scheduler and web UI"""
try:
# Load configuration from database
config = self.load_config()
# Setup scheduled job
if config.enabled:
self.scheduler.add_job(
func=self.sync_and_download,
trigger=CronTrigger.from_crontab(config.schedule_cron),
id='sync_job',
replace_existing=True
)
# Start scheduler
self.scheduler.start()
self.running = True
# Start web UI in separate thread
self.start_web_ui(web_port)
# Setup signal handlers for graceful shutdown
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
logger.info(f"Daemon started. Web UI available at http://localhost:{web_port}")
# Keep daemon running
while self.running:
time.sleep(1)
except Exception as e:
logger.error(f"Failed to start daemon: {str(e)}")
self.stop()
def sync_and_download(self):
"""Scheduled job function"""
try:
self.log_operation("sync", "started")
# Import here to avoid circular imports
from .garmin import GarminClient
from .database import sync_database
# Perform sync and download
client = GarminClient()
# Sync database first
sync_database(client)
# Download missing activities
downloaded_count = 0
session = get_session()
missing_activities = session.query(Activity).filter_by(downloaded=False).all()
for activity in missing_activities:
try:
# Use the correct method name
fit_data = client.download_activity_fit(activity.activity_id)
# Save the file
import os
from pathlib import Path
data_dir = Path(os.getenv("DATA_DIR", "data"))
data_dir.mkdir(parents=True, exist_ok=True)
timestamp = activity.start_time.replace(":", "-").replace(" ", "_")
filename = f"activity_{activity.activity_id}_{timestamp}.fit"
filepath = data_dir / filename
with open(filepath, "wb") as f:
f.write(fit_data)
activity.filename = str(filepath)
activity.downloaded = True
activity.last_sync = datetime.now().isoformat()
downloaded_count += 1
session.commit()
except Exception as e:
logger.error(f"Failed to download activity {activity.activity_id}: {e}")
session.rollback()
session.close()
self.log_operation("sync", "success",
f"Downloaded {downloaded_count} new activities")
except Exception as e:
logger.error(f"Sync failed: {e}")
self.log_operation("sync", "error", str(e))
def load_config(self):
"""Load daemon configuration from database"""
session = get_session()
config = session.query(DaemonConfig).first()
if not config:
# Create default configuration
config = DaemonConfig()
session.add(config)
session.commit()
return config
def start_web_ui(self, port):
"""Start FastAPI web server in a separate thread"""
from .web.app import app
import uvicorn
def run_server():
uvicorn.run(app, host="0.0.0.0", port=port)
web_thread = threading.Thread(target=run_server, daemon=True)
web_thread.start()
self.web_server = web_thread
def signal_handler(self, signum, frame):
"""Handle shutdown signals"""
logger.info("Received shutdown signal, stopping daemon...")
self.stop()
def stop(self):
"""Stop daemon and clean up resources"""
if self.scheduler.running:
self.scheduler.shutdown()
self.running = False
logger.info("Daemon stopped")
def log_operation(self, operation, status, message=None):
"""Log sync operation to database"""
session = get_session()
log = SyncLog(
timestamp=datetime.now().isoformat(),
operation=operation,
status=status,
message=message
)
session.add(log)
session.commit()
def count_missing(self):
"""Count missing activities"""
session = get_session()
return session.query(Activity).filter_by(downloaded=False).count()