update calculation for sleep sensors
This commit is contained in:
parent
6a41d7675d
commit
92ca129510
120
main.py
120
main.py
@ -140,24 +140,27 @@ class GadgetbridgeMQTT:
|
|||||||
"""Decide awake state using most reliable signals first.
|
"""Decide awake state using most reliable signals first.
|
||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. If past wakeup time -> awake (session ended)
|
1. If past wakeup time -> awake (session has ended)
|
||||||
2. If is_awake_flag explicitly set to 1 -> awake
|
2. If within sleep session (before wakeup) -> check stage data
|
||||||
3. If recent sleep stage shows awake (code 5) -> awake
|
3. If recent sleep stage shows awake (code 5) -> awake
|
||||||
4. If recent sleep stage shows sleep (2=deep, 3=light, 4=REM) -> sleeping
|
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
|
6. Use HR as fallback: HR < (resting_hr + 10) suggests sleeping
|
||||||
7. Default: awake
|
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
|
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:
|
if wakeup_raw is not None and wakeup_raw <= now_ms:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if is_awake_flag == 1:
|
# Priority 2-5: Within a sleep session (wakeup time in the future)
|
||||||
return True
|
in_session = wakeup_raw is not None and wakeup_raw > now_ms
|
||||||
|
|
||||||
# Check if stage data is recent (within 2 hours for better coverage)
|
# 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
|
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 recent_stage and stage_code is not None:
|
||||||
@ -166,16 +169,16 @@ class GadgetbridgeMQTT:
|
|||||||
if stage_code in (2, 3, 4): # Deep(2), Light(3), REM(4)
|
if stage_code in (2, 3, 4): # Deep(2), Light(3), REM(4)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If within a sleep session (wakeup time is in the future), likely sleeping
|
# Priority 5: If within session but no recent stage data -> assume sleeping
|
||||||
if wakeup_raw is not None and wakeup_raw > now_ms:
|
if in_session:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Use HR as fallback indicator - low HR suggests sleeping
|
# Priority 6: Use HR as fallback indicator (outside sessions)
|
||||||
# Use resting HR + 10 as threshold, or default to 65 if no resting HR available
|
|
||||||
hr_threshold = (resting_hr + 10) if resting_hr else 65
|
hr_threshold = (resting_hr + 10) if resting_hr else 65
|
||||||
if avg_recent_hr is not None and avg_recent_hr < hr_threshold:
|
if avg_recent_hr is not None and avg_recent_hr < hr_threshold:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Priority 7: Default to awake
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def connect_mqtt(self):
|
def connect_mqtt(self):
|
||||||
@ -392,8 +395,52 @@ class GadgetbridgeMQTT:
|
|||||||
# Sleep Data (filtered by device)
|
# Sleep Data (filtered by device)
|
||||||
# Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps
|
# Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps
|
||||||
try:
|
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(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT TOTAL_DURATION, IS_AWAKE, WAKEUP_TIME, TIMESTAMP,
|
SELECT TOTAL_DURATION, IS_AWAKE, WAKEUP_TIME, TIMESTAMP,
|
||||||
@ -403,60 +450,13 @@ class GadgetbridgeMQTT:
|
|||||||
ORDER BY TIMESTAMP DESC
|
ORDER BY TIMESTAMP DESC
|
||||||
LIMIT 1
|
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()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
(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
|
||||||
|
|
||||||
# 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
|
# Determine if currently in a sleep session
|
||||||
# User is sleeping if: sleep_start <= now < wakeup_time
|
# User is sleeping if: sleep_start <= now < wakeup_time
|
||||||
in_sleep_session = (
|
in_sleep_session = (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user