sync
This commit is contained in:
511
fitbitsync.py
511
fitbitsync.py
@@ -22,14 +22,10 @@ except ImportError:
|
||||
FITBIT_LIBRARY = False
|
||||
|
||||
try:
|
||||
from garminconnect import Garmin
|
||||
import garminconnect
|
||||
GARMIN_LIBRARY = "garminconnect"
|
||||
except ImportError:
|
||||
try:
|
||||
import garth
|
||||
GARMIN_LIBRARY = "garth"
|
||||
except ImportError:
|
||||
GARMIN_LIBRARY = None
|
||||
GARMIN_LIBRARY = None
|
||||
|
||||
import schedule
|
||||
|
||||
@@ -86,7 +82,6 @@ class ConfigManager:
|
||||
return self._create_default_config()
|
||||
return self._create_default_config()
|
||||
|
||||
|
||||
def _create_default_config(self) -> Dict:
|
||||
"""Create default configuration"""
|
||||
config = {
|
||||
@@ -540,7 +535,7 @@ class FitbitClient:
|
||||
return records
|
||||
|
||||
class GarminClient:
|
||||
"""Client for Garmin Connect using garminconnect or garth library"""
|
||||
"""Client for Garmin Connect using garminconnect library"""
|
||||
|
||||
def __init__(self, config: ConfigManager):
|
||||
self.config = config
|
||||
@@ -551,8 +546,14 @@ class GarminClient:
|
||||
self.garmin_client = None
|
||||
self.read_only_mode = config.get('sync.read_only_mode', False)
|
||||
|
||||
if not GARMIN_LIBRARY:
|
||||
raise ImportError("Neither 'garminconnect' nor 'garth' library is installed. Please install one of them.")
|
||||
# Check if garminconnect is available
|
||||
try:
|
||||
import garminconnect
|
||||
self.garminconnect = garminconnect
|
||||
logger.info("Using garminconnect library")
|
||||
except ImportError:
|
||||
logger.error("garminconnect library not installed. Install with: pip install garminconnect")
|
||||
raise ImportError("garminconnect library is required but not installed")
|
||||
|
||||
async def authenticate(self) -> bool:
|
||||
"""Authenticate with Garmin Connect"""
|
||||
@@ -569,10 +570,42 @@ class GarminClient:
|
||||
logger.info("No stored Garmin credentials found. Please set them up.")
|
||||
return self._setup_credentials()
|
||||
|
||||
if GARMIN_LIBRARY == "garminconnect":
|
||||
return await self._authenticate_garminconnect()
|
||||
elif GARMIN_LIBRARY == "garth":
|
||||
return await self._authenticate_garth()
|
||||
# Create Garmin Connect client
|
||||
logger.info("Authenticating with Garmin Connect...")
|
||||
|
||||
# Try to load existing session first
|
||||
if os.path.exists(self.session_file):
|
||||
try:
|
||||
self.garmin_client = self.garminconnect.Garmin()
|
||||
self.garmin_client.load(self.session_file)
|
||||
|
||||
# Test the session by trying to get profile
|
||||
profile = self.garmin_client.get_full_name()
|
||||
logger.info(f"Resumed existing session for user: {profile}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Existing session invalid: {e}")
|
||||
# Fall through to fresh login
|
||||
|
||||
# Perform fresh login
|
||||
self.garmin_client = self.garminconnect.Garmin(
|
||||
self.username,
|
||||
self.password,
|
||||
is_cn=self.is_china
|
||||
)
|
||||
|
||||
# Login and save session
|
||||
self.garmin_client.login()
|
||||
|
||||
# Save session data
|
||||
self.garmin_client.save(self.session_file)
|
||||
|
||||
# Test the connection
|
||||
profile = self.garmin_client.get_full_name()
|
||||
logger.info(f"Successfully authenticated for user: {profile}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Garmin authentication error: {e}")
|
||||
@@ -603,103 +636,16 @@ class GarminClient:
|
||||
print("✅ Credentials saved securely")
|
||||
return True
|
||||
|
||||
async def _authenticate_garminconnect(self) -> bool:
|
||||
"""Authenticate using garminconnect library"""
|
||||
try:
|
||||
logger.info("Authenticating with Garmin Connect using garminconnect library...")
|
||||
|
||||
# Create Garmin client (garminconnect doesn't support is_china parameter)
|
||||
self.garmin_client = Garmin(
|
||||
email=self.username,
|
||||
password=self.password
|
||||
)
|
||||
|
||||
# Try to load existing session
|
||||
if os.path.exists(self.session_file):
|
||||
try:
|
||||
with open(self.session_file, 'r') as f:
|
||||
session_data = json.load(f)
|
||||
self.garmin_client.login()
|
||||
logger.info("Loaded existing Garmin session")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load existing session: {e}")
|
||||
|
||||
# Perform fresh login
|
||||
self.garmin_client.login()
|
||||
|
||||
# Save session data for future use (if available)
|
||||
try:
|
||||
# Different versions of garminconnect may have different session handling
|
||||
if hasattr(self.garmin_client, 'session') and self.garmin_client.session:
|
||||
session_data = {
|
||||
"session_data": self.garmin_client.session.cookies.get_dict(),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
with open(self.session_file, 'w') as f:
|
||||
json.dump(session_data, f)
|
||||
logger.info("Saved Garmin session data")
|
||||
else:
|
||||
logger.info("Session data not available for saving")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save session data: {e}")
|
||||
|
||||
# Test the connection
|
||||
user_profile = self.garmin_client.get_user_profile()
|
||||
logger.info(f"Successfully authenticated as {user_profile.get('displayName', 'Unknown')}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"garminconnect authentication failed: {e}")
|
||||
return False
|
||||
|
||||
async def _authenticate_garth(self) -> bool:
|
||||
"""Authenticate using garth library"""
|
||||
try:
|
||||
logger.info("Authenticating with Garmin Connect using garth library...")
|
||||
|
||||
# Configure garth
|
||||
garth.configure(domain="garmin.com" if not self.is_china else "garmin.cn")
|
||||
|
||||
# Try to load existing session
|
||||
if os.path.exists(self.session_file):
|
||||
try:
|
||||
garth.resume(self.session_file)
|
||||
garth.client.username # Test if session is valid
|
||||
logger.info("Resumed existing Garmin session")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to resume existing session: {e}")
|
||||
|
||||
# Perform fresh login
|
||||
garth.login(self.username, self.password)
|
||||
|
||||
# Save session
|
||||
garth.save(self.session_file)
|
||||
logger.info("Successfully authenticated and saved session")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"garth authentication failed: {e}")
|
||||
return False
|
||||
|
||||
async def upload_weight_data(self, records: List[WeightRecord]) -> Tuple[int, int]:
|
||||
"""Upload weight records to Garmin"""
|
||||
"""Upload weight records to Garmin using garminconnect"""
|
||||
if self.read_only_mode:
|
||||
logger.info(f"Read-only mode: Would upload {len(records)} weight records to Garmin")
|
||||
# Simulate successful upload in read-only mode
|
||||
for record in records:
|
||||
logger.info(f"Read-only mode: Would upload {record.weight_kg}kg at {record.timestamp}")
|
||||
return len(records), 0 # All "successful", none failed
|
||||
return len(records), 0
|
||||
|
||||
if not self.garmin_client and GARMIN_LIBRARY == "garminconnect":
|
||||
logger.error("Not authenticated with Garmin (garminconnect)")
|
||||
return 0, len(records)
|
||||
|
||||
if GARMIN_LIBRARY == "garth" and not os.path.exists(self.session_file):
|
||||
logger.error("Not authenticated with Garmin (garth)")
|
||||
if not self.garmin_client:
|
||||
logger.error("Garmin client not authenticated")
|
||||
return 0, len(records)
|
||||
|
||||
success_count = 0
|
||||
@@ -707,12 +653,7 @@ class GarminClient:
|
||||
|
||||
for record in records:
|
||||
try:
|
||||
if GARMIN_LIBRARY == "garminconnect":
|
||||
success = await self._upload_weight_garminconnect(record)
|
||||
elif GARMIN_LIBRARY == "garth":
|
||||
success = await self._upload_weight_garth(record)
|
||||
else:
|
||||
success = False
|
||||
success = await self._upload_weight_garminconnect(record)
|
||||
|
||||
if success:
|
||||
success_count += 1
|
||||
@@ -721,13 +662,154 @@ class GarminClient:
|
||||
logger.error(f"Failed to upload weight record: {record.weight_kg}kg at {record.timestamp}")
|
||||
|
||||
# Rate limiting - wait between requests
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading weight record: {e}")
|
||||
|
||||
return success_count, total_count - success_count
|
||||
|
||||
async def _upload_weight_garminconnect(self, record: WeightRecord) -> bool:
|
||||
"""Upload weight using garminconnect library"""
|
||||
try:
|
||||
# Format date as YYYY-MM-DD string
|
||||
date_str = record.timestamp.strftime("%Y-%m-%d")
|
||||
|
||||
logger.info(f"Uploading weight via garminconnect: {record.weight_kg}kg on {date_str}")
|
||||
|
||||
# Convert datetime to timestamp string format that garminconnect expects
|
||||
# Some versions expect ISO format string, others expect timestamp
|
||||
timestamp_str = record.timestamp.isoformat()
|
||||
|
||||
# Try different methods depending on garminconnect version
|
||||
try:
|
||||
# Method 1: Try add_body_composition with datetime object
|
||||
result = self.garmin_client.add_body_composition(
|
||||
timestamp=record.timestamp,
|
||||
weight=record.weight_kg
|
||||
)
|
||||
|
||||
except Exception as e1:
|
||||
logger.debug(f"Method 1 failed: {e1}")
|
||||
|
||||
try:
|
||||
# Method 2: Try with ISO format string
|
||||
result = self.garmin_client.add_body_composition(
|
||||
timestamp=timestamp_str,
|
||||
weight=record.weight_kg
|
||||
)
|
||||
|
||||
except Exception as e2:
|
||||
logger.debug(f"Method 2 failed: {e2}")
|
||||
|
||||
try:
|
||||
# Method 3: Try with date string only
|
||||
result = self.garmin_client.add_body_composition(
|
||||
timestamp=date_str,
|
||||
weight=record.weight_kg
|
||||
)
|
||||
|
||||
except Exception as e3:
|
||||
logger.debug(f"Method 3 failed: {e3}")
|
||||
|
||||
try:
|
||||
# Method 4: Try set_body_composition if add_body_composition doesn't exist
|
||||
if hasattr(self.garmin_client, 'set_body_composition'):
|
||||
result = self.garmin_client.set_body_composition(
|
||||
timestamp=record.timestamp,
|
||||
weight=record.weight_kg
|
||||
)
|
||||
else:
|
||||
# Method 5: Try legacy weight upload methods
|
||||
if hasattr(self.garmin_client, 'add_weigh_in'):
|
||||
result = self.garmin_client.add_weigh_in(
|
||||
weight=record.weight_kg,
|
||||
date=date_str
|
||||
)
|
||||
else:
|
||||
raise Exception("No suitable weight upload method found")
|
||||
|
||||
except Exception as e4:
|
||||
logger.error(f"All upload methods failed: {e1}, {e2}, {e3}, {e4}")
|
||||
return False
|
||||
|
||||
if result:
|
||||
logger.info(f"garminconnect upload successful")
|
||||
return True
|
||||
else:
|
||||
logger.error("garminconnect upload returned no result")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"garminconnect upload error: {e}")
|
||||
|
||||
# Check if it's an authentication error
|
||||
if "401" in str(e) or "unauthorized" in str(e).lower():
|
||||
logger.error("Authentication failed - session may be expired")
|
||||
# Try to re-authenticate
|
||||
try:
|
||||
logger.info("Attempting to re-authenticate...")
|
||||
self.garmin_client.login()
|
||||
self.garmin_client.save_session(self.session_file)
|
||||
|
||||
# Retry the upload
|
||||
result = self.garmin_client.add_body_composition(
|
||||
timestamp=record.timestamp,
|
||||
weight=record.weight_kg
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Upload successful after re-authentication")
|
||||
return True
|
||||
else:
|
||||
logger.error("Upload failed even after re-authentication")
|
||||
return False
|
||||
|
||||
except Exception as re_auth_error:
|
||||
logger.error(f"Re-authentication failed: {re_auth_error}")
|
||||
return False
|
||||
|
||||
# Check if it's a rate limiting error
|
||||
elif "429" in str(e) or "rate" in str(e).lower():
|
||||
logger.error("Rate limit exceeded")
|
||||
logger.error("Wait at least 1-2 hours before trying again")
|
||||
return False
|
||||
|
||||
# Check if it's a duplicate entry error
|
||||
elif "duplicate" in str(e).lower() or "already exists" in str(e).lower():
|
||||
logger.warning(f"Weight already exists for {date_str}")
|
||||
logger.info("Treating duplicate as successful upload")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_recent_weights(self, days: int = 7) -> List[Dict]:
|
||||
"""Get recent weight data from Garmin (for verification)"""
|
||||
if self.read_only_mode:
|
||||
logger.info("Read-only mode: Cannot fetch Garmin weights")
|
||||
return []
|
||||
|
||||
try:
|
||||
if not self.garmin_client:
|
||||
logger.error("Garmin client not authenticated")
|
||||
return []
|
||||
|
||||
# Get body composition data for the last N days
|
||||
from datetime import timedelta
|
||||
end_date = datetime.now().date()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
weights = self.garmin_client.get_body_composition(
|
||||
startdate=start_date,
|
||||
enddate=end_date
|
||||
)
|
||||
|
||||
return weights if weights else []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent weights: {e}")
|
||||
return []
|
||||
|
||||
def check_garmin_weights(self, days: int = 30):
|
||||
"""Check recent Garmin weights for anomalies"""
|
||||
try:
|
||||
@@ -749,24 +831,30 @@ class GarminClient:
|
||||
|
||||
for weight_entry in recent_weights:
|
||||
try:
|
||||
date = weight_entry.get('calendarDate', 'Unknown')
|
||||
weight_raw = weight_entry.get('weight', 0)
|
||||
# garminconnect returns different format than garth
|
||||
date = weight_entry.get('timestamp', weight_entry.get('date', 'Unknown'))
|
||||
if isinstance(date, datetime):
|
||||
date = date.strftime('%Y-%m-%d')
|
||||
|
||||
# Garmin stores weight in grams, convert to kg
|
||||
weight_kg = weight_raw / 1000 if weight_raw else 0
|
||||
# Weight might be in different fields depending on API version
|
||||
weight_kg = (
|
||||
weight_entry.get('weight') or
|
||||
weight_entry.get('bodyWeight') or
|
||||
weight_entry.get('weightInKilos', 0)
|
||||
)
|
||||
|
||||
# Check for anomalies (weights outside normal human range)
|
||||
if weight_kg > 1000 or weight_kg < 20: # Clearly wrong values
|
||||
anomalies.append((date, weight_kg, weight_raw))
|
||||
if weight_kg > 300 or weight_kg < 30: # Clearly wrong values
|
||||
anomalies.append((date, weight_kg))
|
||||
status = "❌ ANOMALY"
|
||||
elif weight_kg > 300 or weight_kg < 30: # Suspicious values
|
||||
anomalies.append((date, weight_kg, weight_raw))
|
||||
elif weight_kg > 200 or weight_kg < 40: # Suspicious values
|
||||
anomalies.append((date, weight_kg))
|
||||
status = "⚠️ SUSPICIOUS"
|
||||
else:
|
||||
normal_weights.append((date, weight_kg))
|
||||
status = "✅ OK"
|
||||
|
||||
print(f"📅 {date}: {weight_kg:.1f}kg (raw: {weight_raw}) {status}")
|
||||
print(f"📅 {date}: {weight_kg:.1f}kg {status}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error parsing weight entry: {e}")
|
||||
@@ -775,8 +863,8 @@ class GarminClient:
|
||||
print(f"\n🚨 Found {len(anomalies)} anomalous weight entries!")
|
||||
print("These may need to be manually deleted from Garmin Connect.")
|
||||
print("Anomalous entries:")
|
||||
for date, weight_kg, weight_raw in anomalies:
|
||||
print(f" - {date}: {weight_kg:.1f}kg (raw value: {weight_raw})")
|
||||
for date, weight_kg in anomalies:
|
||||
print(f" - {date}: {weight_kg:.1f}kg")
|
||||
|
||||
if normal_weights:
|
||||
print(f"\n✅ {len(normal_weights)} normal weight entries found")
|
||||
@@ -786,182 +874,7 @@ class GarminClient:
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Garmin weights: {e}")
|
||||
print(f"❌ Error checking Garmin weights: {e}")
|
||||
|
||||
|
||||
async def _upload_weight_garminconnect(self, record: WeightRecord) -> bool:
|
||||
"""Upload weight using garminconnect library"""
|
||||
try:
|
||||
# Convert timestamp to various formats we might need
|
||||
date_str = record.timestamp.strftime("%Y-%m-%d")
|
||||
timestamp_ms = int(record.timestamp.timestamp() * 1000)
|
||||
|
||||
# Weight should be in kg for most methods
|
||||
weight_kg = record.weight_kg
|
||||
|
||||
logger.info(f"Uploading weight: {weight_kg}kg on {date_str}")
|
||||
|
||||
# Try methods in order of most likely to work correctly
|
||||
upload_attempts = [
|
||||
# Method 1: add_weigh_in_with_timestamps (if available)
|
||||
{
|
||||
'method': 'add_weigh_in_with_timestamps',
|
||||
'args': [timestamp_ms, weight_kg],
|
||||
'kwargs': {},
|
||||
'description': 'timestamp + weight in kg'
|
||||
},
|
||||
# Method 2: add_weigh_in with date string and weight in kg
|
||||
{
|
||||
'method': 'add_weigh_in',
|
||||
'args': [date_str, weight_kg],
|
||||
'kwargs': {},
|
||||
'description': 'date string + weight in kg'
|
||||
},
|
||||
# Method 3: add_weigh_in with keyword arguments
|
||||
{
|
||||
'method': 'add_weigh_in',
|
||||
'args': [],
|
||||
'kwargs': {'date': date_str, 'weight': weight_kg},
|
||||
'description': 'kwargs: date + weight in kg'
|
||||
},
|
||||
# Method 4: add_body_composition with just date and weight
|
||||
{
|
||||
'method': 'add_body_composition',
|
||||
'args': [date_str],
|
||||
'kwargs': {'weight': weight_kg},
|
||||
'description': 'date + weight in kg as kwarg'
|
||||
},
|
||||
# Method 5: add_body_composition with date and weight as positional args
|
||||
{
|
||||
'method': 'add_body_composition',
|
||||
'args': [date_str, weight_kg],
|
||||
'kwargs': {},
|
||||
'description': 'date + weight in kg as positional args'
|
||||
},
|
||||
# Method 6: Try without any timestamp, just weight
|
||||
{
|
||||
'method': 'add_body_composition',
|
||||
'args': [],
|
||||
'kwargs': {'weight': weight_kg},
|
||||
'description': 'weight only in kg'
|
||||
}
|
||||
]
|
||||
|
||||
for i, attempt in enumerate(upload_attempts, 1):
|
||||
method_name = attempt['method']
|
||||
args = attempt['args']
|
||||
kwargs = attempt['kwargs']
|
||||
description = attempt['description']
|
||||
|
||||
if hasattr(self.garmin_client, method_name):
|
||||
try:
|
||||
logger.info(f"Trying method {i}: {method_name} ({description})")
|
||||
logger.info(f" Args: {args}")
|
||||
logger.info(f" Kwargs: {kwargs}")
|
||||
|
||||
method = getattr(self.garmin_client, method_name)
|
||||
|
||||
# Call method with both positional and keyword arguments
|
||||
if args and kwargs:
|
||||
result = method(*args, **kwargs)
|
||||
elif args:
|
||||
result = method(*args)
|
||||
elif kwargs:
|
||||
result = method(**kwargs)
|
||||
else:
|
||||
result = method()
|
||||
|
||||
logger.info(f"✅ Method {method_name} succeeded. Result: {result}")
|
||||
|
||||
# Verify the upload by checking recent weights
|
||||
await asyncio.sleep(2) # Give Garmin time to process
|
||||
try:
|
||||
recent_weights = self.get_recent_weights(1)
|
||||
if recent_weights:
|
||||
latest_weight = recent_weights[0]
|
||||
garmin_weight_kg = latest_weight.get('weight', 0) / 1000 if latest_weight.get('weight') else 0
|
||||
logger.info(f"Verification: Latest Garmin weight is {garmin_weight_kg}kg")
|
||||
|
||||
# Check if the weight is reasonable (within 10% of what we uploaded)
|
||||
if abs(garmin_weight_kg - weight_kg) / weight_kg < 0.1:
|
||||
logger.info(f"✅ Weight verification passed: {garmin_weight_kg}kg ≈ {weight_kg}kg")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"⚠️ Weight verification failed: uploaded {weight_kg}kg but Garmin shows {garmin_weight_kg}kg")
|
||||
# Continue to try other methods
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not verify upload: {e}")
|
||||
# Assume success if we can't verify
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Method {method_name} failed: {e}")
|
||||
# Log the full error for the first few attempts
|
||||
if i <= 3:
|
||||
import traceback
|
||||
logger.warning(f"Full error for {method_name}: {traceback.format_exc()}")
|
||||
continue
|
||||
else:
|
||||
logger.debug(f"Method {method_name} not available")
|
||||
|
||||
logger.error("All upload methods failed")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"garminconnect upload error: {e}")
|
||||
import traceback
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
async def _upload_weight_garth(self, record: WeightRecord) -> bool:
|
||||
"""Upload weight using garth library"""
|
||||
try:
|
||||
# Garth expects weight in kg and timestamp in specific format
|
||||
timestamp_ms = int(record.timestamp.timestamp() * 1000)
|
||||
|
||||
# Use garth to upload weight
|
||||
response = garth.post(
|
||||
"biometric-service",
|
||||
"/biometric-service/weight",
|
||||
json={
|
||||
"timestampGMT": timestamp_ms,
|
||||
"unitKey": "kg",
|
||||
"value": record.weight_kg
|
||||
}
|
||||
)
|
||||
|
||||
return response.status_code in [200, 201, 204]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"garth upload error: {e}")
|
||||
return False
|
||||
|
||||
def get_recent_weights(self, days: int = 7) -> List[Dict]:
|
||||
"""Get recent weight data from Garmin (for verification)"""
|
||||
if self.read_only_mode:
|
||||
logger.info("Read-only mode: Cannot fetch Garmin weights")
|
||||
return []
|
||||
|
||||
try:
|
||||
if GARMIN_LIBRARY == "garminconnect" and self.garmin_client:
|
||||
end_date = datetime.now().date()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
weights = self.garmin_client.get_body_composition(
|
||||
start_date.isoformat(),
|
||||
end_date.isoformat()
|
||||
)
|
||||
return weights or []
|
||||
|
||||
elif GARMIN_LIBRARY == "garth":
|
||||
# Implementation for garth if needed
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent weights: {e}")
|
||||
return []
|
||||
|
||||
class WeightSyncApp:
|
||||
"""Main application class"""
|
||||
|
||||
@@ -1415,4 +1328,4 @@ async def main():
|
||||
print("\n🔄 Currently in FULL SYNC mode - will upload to Garmin")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user