Add auto reconnect logic
This commit is contained in:
parent
92ca129510
commit
8514180505
83
main.py
83
main.py
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user