update calculation for sleep sensors

This commit is contained in:
Oliver 2025-12-18 11:48:21 +00:00
parent 6a41d7675d
commit 92ca129510

130
main.py
View File

@ -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 = (