From 92ca129510f6b04b11f09023fc5b6f024b5bd897 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 18 Dec 2025 11:48:21 +0000 Subject: [PATCH] update calculation for sleep sensors --- main.py | 130 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/main.py b/main.py index c7e5c1a..74e5424 100644 --- a/main.py +++ b/main.py @@ -140,42 +140,45 @@ class GadgetbridgeMQTT: """Decide awake state using most reliable signals first. Priority: - 1. If past wakeup time -> awake (session ended) - 2. If is_awake_flag explicitly set to 1 -> awake + 1. If past wakeup time -> awake (session has ended) + 2. If within sleep session (before wakeup) -> check stage data 3. If recent sleep stage shows awake (code 5) -> awake 4. If recent sleep stage shows sleep (2=deep, 3=light, 4=REM) -> sleeping - 5. If we're within a sleep session (before wakeup time) -> sleeping + 5. If within sleep session but no recent stage -> sleeping 6. Use HR as fallback: HR < (resting_hr + 10) suggests sleeping 7. Default: awake + Note: is_awake_flag indicates session has finished (!isSleepFinish in Gadgetbridge) + so we only use it as confirmation when past wakeup time + Stage codes for Xiaomi: 2=deep, 3=light, 4=REM, 5=awake """ - # If we're past the wakeup time, the session is over -> awake + # Priority 1: If we're past the wakeup time, the session is over -> awake if wakeup_raw is not None and wakeup_raw <= now_ms: return True - - if is_awake_flag == 1: - return True - - # Check if stage data is recent (within 2 hours for better coverage) + + # Priority 2-5: Within a sleep session (wakeup time in the future) + in_session = wakeup_raw is not None and wakeup_raw > now_ms + + # Check if stage data is recent (within 2 hours) recent_stage = stage_timestamp_ms is not None and now_ms - stage_timestamp_ms <= 2 * 60 * 60 * 1000 - + if recent_stage and stage_code is not None: if stage_code == 5: # AWAKE stage return True if stage_code in (2, 3, 4): # Deep(2), Light(3), REM(4) return False - - # If within a sleep session (wakeup time is in the future), likely sleeping - if wakeup_raw is not None and wakeup_raw > now_ms: + + # Priority 5: If within session but no recent stage data -> assume sleeping + if in_session: return False - # Use HR as fallback indicator - low HR suggests sleeping - # Use resting HR + 10 as threshold, or default to 65 if no resting HR available + # Priority 6: Use HR as fallback indicator (outside sessions) hr_threshold = (resting_hr + 10) if resting_hr else 65 if avg_recent_hr is not None and avg_recent_hr < hr_threshold: return False + # Priority 7: Default to awake return True def connect_mqtt(self): @@ -392,8 +395,52 @@ class GadgetbridgeMQTT: # Sleep Data (filtered by device) # Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps try: - day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000 # 24h ago in milliseconds - + # 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( + """ + SELECT TIMESTAMP, WAKEUP_TIME, TOTAL_DURATION, + DEEP_SLEEP_DURATION, LIGHT_SLEEP_DURATION, REM_SLEEP_DURATION + FROM XIAOMI_SLEEP_TIME_SAMPLE + WHERE DEVICE_ID = ? AND TIMESTAMP >= ? + ORDER BY TIMESTAMP DESC + """, + (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 + + 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 + + # 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) + elif sess_total: + sess_min = sess_total + + total_sleep_min += sess_min + last_wakeup_ms = sess_start # Track when this session started (for gap calc) + + 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, @@ -403,60 +450,13 @@ class GadgetbridgeMQTT: ORDER BY TIMESTAMP DESC LIMIT 1 """, - (self.device_id, day_ago_ts_ms, now_ms) # Only sessions that have started + (self.device_id, day_ago_ts_ms, now_ms) ) row = cursor.fetchone() if row: (total_duration, is_awake_flag, wakeup_raw, sleep_start_ms, deep_dur, light_dur, rem_dur, awake_dur) = row - # Calculate sleep duration using multiple strategies - actual_sleep_min = 0 - - # Strategy 1: Use breakdown sum if available (most accurate) - if deep_dur or light_dur or rem_dur: - actual_sleep_min = (deep_dur or 0) + (light_dur or 0) + (rem_dur or 0) - - # Strategy 2: Calculate from stage data (count sleep stage entries) - # This works even when TOTAL_DURATION is incomplete - if actual_sleep_min == 0 and sleep_start_ms and wakeup_raw: - try: - cursor.execute( - """ - SELECT - MIN(TIMESTAMP) as first_stage, - MAX(TIMESTAMP) as last_stage, - COUNT(*) as stage_count, - SUM(CASE WHEN STAGE IN (2,3,4) THEN 1 ELSE 0 END) as sleep_count - FROM XIAOMI_SLEEP_STAGE_SAMPLE - WHERE DEVICE_ID = ? AND TIMESTAMP >= ? AND TIMESTAMP <= ? - """, - (self.device_id, sleep_start_ms, wakeup_raw) - ) - stage_row = cursor.fetchone() - if stage_row and stage_row[0] and stage_row[1]: - # Calculate duration from stage time span - stage_span_min = (stage_row[1] - stage_row[0]) / 1000 / 60 - if stage_span_min > 30: # At least 30 min of stage data - actual_sleep_min = stage_span_min - except Exception as e: - logger.debug(f"Stage duration calculation failed: {e}") - - # Strategy 3: Use TOTAL_DURATION if available - if actual_sleep_min == 0 and total_duration: - actual_sleep_min = total_duration - - # Strategy 4: Calculate from session time range (least accurate) - if actual_sleep_min < 30 and sleep_start_ms and wakeup_raw: - session_minutes = (wakeup_raw - sleep_start_ms) / 1000 / 60 - # Only use if session looks reasonable (1-14 hours) - if 60 <= session_minutes <= 840: - # Estimate ~10% awake time - actual_sleep_min = session_minutes * 0.9 - - if actual_sleep_min > 0: - data["sleep_duration"] = round(actual_sleep_min / 60.0, 2) - # Determine if currently in a sleep session # User is sleeping if: sleep_start <= now < wakeup_time in_sleep_session = (