Add auto reconnect logic

This commit is contained in:
Oliver 2025-12-19 13:02:43 +00:00
parent 92ca129510
commit 8514180505

83
main.py
View File

@ -30,6 +30,7 @@ except ImportError:
CONFIG_FILE = os.path.expanduser("~/.config/gadgetbridge_mqtt/config.json")
GB_EXPORT_DIR = "/storage/emulated/0/Documents/GB_Export"
PUBLISH_INTERVAL = 300 # 5 minutes
STALE_THRESHOLD_SECONDS = 1200 # 20 minutes (Force reconnect if no new data for this long)
# --- Logging ---
logging.basicConfig(
@ -76,6 +77,32 @@ def trigger_bluetooth_connect(device_mac):
return send_gadgetbridge_intent(action, extra)
def trigger_bluetooth_reconnect(device_mac):
"""
Force a full disconnect cycle.
Used when data is stale to clear 'zombie' connection states.
Note: We only Disconnect here. The subsequent normal sync cycle
will handle the Connect, effectively completing the reset.
"""
if not device_mac:
logger.warning("No device MAC address available for reconnection")
return False
logger.warning("Initiating Force Disconnect (Zombie Recovery)...")
# 1. Force Disconnect
send_gadgetbridge_intent(
"nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_DISCONNECT",
f"-e EXTRA_DEVICE_ADDRESS '{device_mac}'"
)
# Wait for the stack to clear
logger.info("Waiting 15s for Bluetooth stack to clear...")
time.sleep(15)
return True
def trigger_gadgetbridge_sync(device_mac=None):
"""Trigger Gadgetbridge to sync data from band and export database.
@ -126,6 +153,9 @@ class GadgetbridgeMQTT:
self.last_db_mtime = 0
self.running = True
# New: Track data freshness for watchdog
self.last_data_timestamp = 0
# Register signal handlers for graceful shutdown
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
@ -365,19 +395,20 @@ class GadgetbridgeMQTT:
logger.debug(f"Battery query failed: {e}")
# Latest Heart Rate (filtered by device)
# --- MODIFIED: Capture TIMESTAMP for watchdog ---
try:
cursor.execute(
"SELECT HEART_RATE FROM XIAOMI_ACTIVITY_SAMPLE WHERE DEVICE_ID = ? AND HEART_RATE > 0 AND HEART_RATE < 255 ORDER BY TIMESTAMP DESC LIMIT 1",
"SELECT HEART_RATE, TIMESTAMP FROM XIAOMI_ACTIVITY_SAMPLE WHERE DEVICE_ID = ? AND HEART_RATE > 0 AND HEART_RATE < 255 ORDER BY TIMESTAMP DESC LIMIT 1",
(self.device_id,)
)
row = cursor.fetchone()
if row:
data["heart_rate"] = row[0]
self.last_data_timestamp = row[1] # <--- NEW: Update timestamp
except Exception as e:
logger.debug(f"Heart rate query failed: {e}")
# Daily Summary Data (filtered by device)
# Note: XIAOMI_DAILY_SUMMARY_SAMPLE uses MILLISECONDS timestamps
try:
cursor.execute(
"SELECT HR_RESTING, HR_MAX, HR_AVG, CALORIES FROM XIAOMI_DAILY_SUMMARY_SAMPLE WHERE DEVICE_ID = ? AND TIMESTAMP >= ? ORDER BY TIMESTAMP DESC LIMIT 1",
@ -393,13 +424,8 @@ class GadgetbridgeMQTT:
logger.debug(f"Daily summary query failed: {e}")
# Sleep Data (filtered by device)
# Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps
try:
# SLEEP DURATION: Sum consecutive sleep sessions until there's a 2h+ gap
# This groups the main sleep session with any brief wakeups/naps
# Only resets when a truly new sleep period begins
# Get all recent sessions (last 24h)
day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000
cursor.execute(
"""
@ -412,7 +438,6 @@ class GadgetbridgeMQTT:
(self.device_id, day_ago_ts_ms)
)
# Group sessions that are within 2 hours of each other
sessions = cursor.fetchall()
total_sleep_min = 0
last_wakeup_ms = None
@ -420,13 +445,11 @@ class GadgetbridgeMQTT:
for sess_row in sessions:
sess_start, sess_wake, sess_total, sess_deep, sess_light, sess_rem = sess_row
# If this session ended more than 2h before the next one started, stop
if last_wakeup_ms is not None:
gap_hours = (last_wakeup_ms - sess_wake) / 1000 / 3600 if sess_wake else 999
if gap_hours > 2:
break # This is a separate sleep period, don't include
break
# Calculate session duration
sess_min = 0
if sess_deep or sess_light or sess_rem:
sess_min = (sess_deep or 0) + (sess_light or 0) + (sess_rem or 0)
@ -434,13 +457,12 @@ class GadgetbridgeMQTT:
sess_min = sess_total
total_sleep_min += sess_min
last_wakeup_ms = sess_start # Track when this session started (for gap calc)
last_wakeup_ms = sess_start
if total_sleep_min > 0:
data["sleep_duration"] = round(total_sleep_min / 60.0, 2)
# IS_AWAKE / SLEEP_STAGE: Use the MOST RECENT session (within 24h)
day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000
cursor.execute(
"""
SELECT TOTAL_DURATION, IS_AWAKE, WAKEUP_TIME, TIMESTAMP,
@ -457,8 +479,6 @@ class GadgetbridgeMQTT:
(total_duration, is_awake_flag, wakeup_raw, sleep_start_ms,
deep_dur, light_dur, rem_dur, awake_dur) = row
# Determine if currently in a sleep session
# User is sleeping if: sleep_start <= now < wakeup_time
in_sleep_session = (
sleep_start_ms is not None and
wakeup_raw is not None and
@ -477,8 +497,6 @@ class GadgetbridgeMQTT:
)
data["is_awake"] = is_awake
# Report sleep stage - Xiaomi codes: 2=deep, 3=light, 4=REM, 5=awake
# Stage mapping for Xiaomi devices (verified from database)
stage_names = {
0: "not_sleep",
1: "unknown",
@ -490,20 +508,16 @@ class GadgetbridgeMQTT:
if stage_code is not None and stage_timestamp_ms is not None:
stage_age_minutes = (now_ms - stage_timestamp_ms) / 1000 / 60
# Report stage if in sleep session or stage is recent (within 2 hours)
if in_sleep_session or stage_age_minutes <= 120:
data["sleep_stage"] = stage_names.get(stage_code, f"unknown_{stage_code}")
data["sleep_stage_code"] = stage_code
else:
# Not in sleep session and stage is stale
data["sleep_stage"] = "not_sleep"
data["sleep_stage_code"] = 0
else:
# No stage data
data["sleep_stage"] = "not_sleep" if is_awake else "unknown"
data["sleep_stage_code"] = 0
else:
# No sleep data - user is awake
data["is_awake"] = True
data["in_sleep_session"] = False
data["sleep_stage"] = "not_sleep"
@ -619,6 +633,27 @@ class GadgetbridgeMQTT:
return False
def check_and_recover_connection(self):
"""
WATCHDOG: Checks if data is stale (>20 mins) and forces Bluetooth restart if so.
"""
if self.last_data_timestamp == 0:
return # No data seen yet, skip check
current_ts = int(time.time())
time_diff = current_ts - self.last_data_timestamp
if time_diff > STALE_THRESHOLD_SECONDS:
logger.warning(f"Data stale ({time_diff}s > {STALE_THRESHOLD_SECONDS}s). Triggering recovery...")
if self.device_mac:
# 1. Disconnect and wait
trigger_bluetooth_reconnect(self.device_mac)
# 2. Reset timestamp so we don't loop immediately
self.last_data_timestamp = current_ts
else:
logger.warning("Cannot recover: No device MAC available")
def run(self):
"""Main loop"""
export_dir = self.config.get("export_dir", GB_EXPORT_DIR)
@ -645,8 +680,12 @@ class GadgetbridgeMQTT:
# Check if it's time to trigger sync and publish
if current_time - self.last_publish_time >= interval:
try:
# 1. WATCHDOG (New)
self.check_and_recover_connection()
logger.info("Triggering Gadgetbridge sync...")
# Use device MAC for Bluetooth reconnection if available
# 2. SYNC (Original - this handles the CONNECT logic)
# If watchdog ran, this provides the "Reconnect" part of the cycle
trigger_gadgetbridge_sync(self.device_mac)
# Wait for export to complete