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")
|
CONFIG_FILE = os.path.expanduser("~/.config/gadgetbridge_mqtt/config.json")
|
||||||
GB_EXPORT_DIR = "/storage/emulated/0/Documents/GB_Export"
|
GB_EXPORT_DIR = "/storage/emulated/0/Documents/GB_Export"
|
||||||
PUBLISH_INTERVAL = 300 # 5 minutes
|
PUBLISH_INTERVAL = 300 # 5 minutes
|
||||||
|
STALE_THRESHOLD_SECONDS = 1200 # 20 minutes (Force reconnect if no new data for this long)
|
||||||
|
|
||||||
# --- Logging ---
|
# --- Logging ---
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -76,6 +77,32 @@ def trigger_bluetooth_connect(device_mac):
|
|||||||
return send_gadgetbridge_intent(action, extra)
|
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):
|
def trigger_gadgetbridge_sync(device_mac=None):
|
||||||
"""Trigger Gadgetbridge to sync data from band and export database.
|
"""Trigger Gadgetbridge to sync data from band and export database.
|
||||||
|
|
||||||
@ -126,6 +153,9 @@ class GadgetbridgeMQTT:
|
|||||||
self.last_db_mtime = 0
|
self.last_db_mtime = 0
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
|
# New: Track data freshness for watchdog
|
||||||
|
self.last_data_timestamp = 0
|
||||||
|
|
||||||
# Register signal handlers for graceful shutdown
|
# Register signal handlers for graceful shutdown
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
signal.signal(signal.SIGINT, self._signal_handler)
|
||||||
@ -365,19 +395,20 @@ class GadgetbridgeMQTT:
|
|||||||
logger.debug(f"Battery query failed: {e}")
|
logger.debug(f"Battery query failed: {e}")
|
||||||
|
|
||||||
# Latest Heart Rate (filtered by device)
|
# Latest Heart Rate (filtered by device)
|
||||||
|
# --- MODIFIED: Capture TIMESTAMP for watchdog ---
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
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,)
|
(self.device_id,)
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
data["heart_rate"] = row[0]
|
data["heart_rate"] = row[0]
|
||||||
|
self.last_data_timestamp = row[1] # <--- NEW: Update timestamp
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Heart rate query failed: {e}")
|
logger.debug(f"Heart rate query failed: {e}")
|
||||||
|
|
||||||
# Daily Summary Data (filtered by device)
|
# Daily Summary Data (filtered by device)
|
||||||
# Note: XIAOMI_DAILY_SUMMARY_SAMPLE uses MILLISECONDS timestamps
|
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
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",
|
"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}")
|
logger.debug(f"Daily summary query failed: {e}")
|
||||||
|
|
||||||
# Sleep Data (filtered by device)
|
# Sleep Data (filtered by device)
|
||||||
# Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps
|
|
||||||
try:
|
try:
|
||||||
# SLEEP DURATION: Sum consecutive sleep sessions until there's a 2h+ gap
|
# 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
|
day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
@ -412,7 +438,6 @@ class GadgetbridgeMQTT:
|
|||||||
(self.device_id, day_ago_ts_ms)
|
(self.device_id, day_ago_ts_ms)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Group sessions that are within 2 hours of each other
|
|
||||||
sessions = cursor.fetchall()
|
sessions = cursor.fetchall()
|
||||||
total_sleep_min = 0
|
total_sleep_min = 0
|
||||||
last_wakeup_ms = None
|
last_wakeup_ms = None
|
||||||
@ -420,13 +445,11 @@ class GadgetbridgeMQTT:
|
|||||||
for sess_row in sessions:
|
for sess_row in sessions:
|
||||||
sess_start, sess_wake, sess_total, sess_deep, sess_light, sess_rem = sess_row
|
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:
|
if last_wakeup_ms is not None:
|
||||||
gap_hours = (last_wakeup_ms - sess_wake) / 1000 / 3600 if sess_wake else 999
|
gap_hours = (last_wakeup_ms - sess_wake) / 1000 / 3600 if sess_wake else 999
|
||||||
if gap_hours > 2:
|
if gap_hours > 2:
|
||||||
break # This is a separate sleep period, don't include
|
break
|
||||||
|
|
||||||
# Calculate session duration
|
|
||||||
sess_min = 0
|
sess_min = 0
|
||||||
if sess_deep or sess_light or sess_rem:
|
if sess_deep or sess_light or sess_rem:
|
||||||
sess_min = (sess_deep or 0) + (sess_light or 0) + (sess_rem or 0)
|
sess_min = (sess_deep or 0) + (sess_light or 0) + (sess_rem or 0)
|
||||||
@ -434,13 +457,12 @@ class GadgetbridgeMQTT:
|
|||||||
sess_min = sess_total
|
sess_min = sess_total
|
||||||
|
|
||||||
total_sleep_min += sess_min
|
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:
|
if total_sleep_min > 0:
|
||||||
data["sleep_duration"] = round(total_sleep_min / 60.0, 2)
|
data["sleep_duration"] = round(total_sleep_min / 60.0, 2)
|
||||||
|
|
||||||
# IS_AWAKE / SLEEP_STAGE: Use the MOST RECENT session (within 24h)
|
# IS_AWAKE / SLEEP_STAGE: Use the MOST RECENT session (within 24h)
|
||||||
day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT TOTAL_DURATION, IS_AWAKE, WAKEUP_TIME, TIMESTAMP,
|
SELECT TOTAL_DURATION, IS_AWAKE, WAKEUP_TIME, TIMESTAMP,
|
||||||
@ -457,8 +479,6 @@ class GadgetbridgeMQTT:
|
|||||||
(total_duration, is_awake_flag, wakeup_raw, sleep_start_ms,
|
(total_duration, is_awake_flag, wakeup_raw, sleep_start_ms,
|
||||||
deep_dur, light_dur, rem_dur, awake_dur) = row
|
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 = (
|
in_sleep_session = (
|
||||||
sleep_start_ms is not None and
|
sleep_start_ms is not None and
|
||||||
wakeup_raw is not None and
|
wakeup_raw is not None and
|
||||||
@ -477,8 +497,6 @@ class GadgetbridgeMQTT:
|
|||||||
)
|
)
|
||||||
data["is_awake"] = is_awake
|
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 = {
|
stage_names = {
|
||||||
0: "not_sleep",
|
0: "not_sleep",
|
||||||
1: "unknown",
|
1: "unknown",
|
||||||
@ -490,20 +508,16 @@ class GadgetbridgeMQTT:
|
|||||||
|
|
||||||
if stage_code is not None and stage_timestamp_ms is not None:
|
if stage_code is not None and stage_timestamp_ms is not None:
|
||||||
stage_age_minutes = (now_ms - stage_timestamp_ms) / 1000 / 60
|
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:
|
if in_sleep_session or stage_age_minutes <= 120:
|
||||||
data["sleep_stage"] = stage_names.get(stage_code, f"unknown_{stage_code}")
|
data["sleep_stage"] = stage_names.get(stage_code, f"unknown_{stage_code}")
|
||||||
data["sleep_stage_code"] = stage_code
|
data["sleep_stage_code"] = stage_code
|
||||||
else:
|
else:
|
||||||
# Not in sleep session and stage is stale
|
|
||||||
data["sleep_stage"] = "not_sleep"
|
data["sleep_stage"] = "not_sleep"
|
||||||
data["sleep_stage_code"] = 0
|
data["sleep_stage_code"] = 0
|
||||||
else:
|
else:
|
||||||
# No stage data
|
|
||||||
data["sleep_stage"] = "not_sleep" if is_awake else "unknown"
|
data["sleep_stage"] = "not_sleep" if is_awake else "unknown"
|
||||||
data["sleep_stage_code"] = 0
|
data["sleep_stage_code"] = 0
|
||||||
else:
|
else:
|
||||||
# No sleep data - user is awake
|
|
||||||
data["is_awake"] = True
|
data["is_awake"] = True
|
||||||
data["in_sleep_session"] = False
|
data["in_sleep_session"] = False
|
||||||
data["sleep_stage"] = "not_sleep"
|
data["sleep_stage"] = "not_sleep"
|
||||||
@ -619,6 +633,27 @@ class GadgetbridgeMQTT:
|
|||||||
|
|
||||||
return False
|
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):
|
def run(self):
|
||||||
"""Main loop"""
|
"""Main loop"""
|
||||||
export_dir = self.config.get("export_dir", GB_EXPORT_DIR)
|
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
|
# Check if it's time to trigger sync and publish
|
||||||
if current_time - self.last_publish_time >= interval:
|
if current_time - self.last_publish_time >= interval:
|
||||||
try:
|
try:
|
||||||
|
# 1. WATCHDOG (New)
|
||||||
|
self.check_and_recover_connection()
|
||||||
|
|
||||||
logger.info("Triggering Gadgetbridge sync...")
|
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)
|
trigger_gadgetbridge_sync(self.device_mac)
|
||||||
|
|
||||||
# Wait for export to complete
|
# Wait for export to complete
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user