fix sleep stage

This commit is contained in:
Oliver Großkloß 2025-12-08 14:38:27 +01:00
parent e7c00f6901
commit 592530aac2
4 changed files with 63 additions and 33 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

View File

@ -79,6 +79,8 @@ Config is stored at `~/.config/gadgetbridge_mqtt/config.json`:
} }
``` ```
**Security Note:** MQTT credentials are stored in plaintext. The setup script sets file permissions to `600` (owner read/write only), but be aware of this if sharing your device or backups.
## Published Sensors ## Published Sensors
| Sensor | Topic | Unit | | Sensor | Topic | Unit |

38
main.py
View File

@ -239,10 +239,11 @@ class GadgetbridgeMQTT:
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",
(self.device_id, day_midnight) (self.device_id, day_midnight * 1000) # Convert to milliseconds
) )
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -254,8 +255,10 @@ 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:
day_ago_ts = int(time.time()) - 24 * 3600 day_ago_ts_ms = (int(time.time()) - 24 * 3600) * 1000 # 24h ago in milliseconds
now_ms = int(time.time()) * 1000 # Current time in milliseconds
cursor.execute( cursor.execute(
""" """
@ -265,7 +268,7 @@ class GadgetbridgeMQTT:
ORDER BY TIMESTAMP DESC ORDER BY TIMESTAMP DESC
LIMIT 1 LIMIT 1
""", """,
(self.device_id, day_ago_ts) (self.device_id, day_ago_ts_ms)
) )
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -274,13 +277,18 @@ class GadgetbridgeMQTT:
# Convert duration to hours # Convert duration to hours
if total_duration is not None: if total_duration is not None:
data["sleep_duration"] = round(total_duration / 60.0, 2) data["sleep_duration"] = round(total_duration / 60.0, 2)
# NULL means "not finalized", treat as False (not awake)
# 0 means explicitly "still asleep" # Determine if user is awake:
# 1 means explicitly "woke up" # 1. If IS_AWAKE flag is explicitly set to 1, user is awake
if is_awake_flag is None: # 2. If WAKEUP_TIME exists and is in the past, user is awake
is_awake = False # No data = not awake # 3. Otherwise, assume still sleeping
if is_awake_flag == 1:
is_awake = True
elif wakeup_raw is not None and wakeup_raw <= now_ms:
# WAKEUP_TIME is in the past = user has woken up
is_awake = True
else: else:
is_awake = (is_awake_flag == 1) is_awake = False
data["is_awake"] = is_awake data["is_awake"] = is_awake
@ -288,6 +296,7 @@ class GadgetbridgeMQTT:
logger.debug(f"Sleep query failed: {e}") logger.debug(f"Sleep query failed: {e}")
# Sleep Stage Data (current stage) # Sleep Stage Data (current stage)
# Note: XIAOMI_SLEEP_STAGE_SAMPLE uses MILLISECONDS timestamps
try: try:
cursor.execute( cursor.execute(
""" """
@ -303,9 +312,16 @@ class GadgetbridgeMQTT:
if row: if row:
stage_code, stage_timestamp = row stage_code, stage_timestamp = row
# Sleep stage codes from Gadgetbridge SleepDetailsParser.java:
# 0: NOT_SLEEP, 1: N/A (unknown), 2: DEEP_SLEEP,
# 3: LIGHT_SLEEP, 4: REM_SLEEP, 5: AWAKE
stage_names = { stage_names = {
0: "awake", 1: "light_sleep", 2: "deep_sleep", 0: "not_sleep",
3: "rem_sleep", 4: "deep_sleep_v2", 5: "light_sleep_v2" 1: "unknown",
2: "deep_sleep",
3: "light_sleep",
4: "rem_sleep",
5: "awake"
} }
data["sleep_stage"] = stage_names.get(stage_code, f"unknown_{stage_code}") data["sleep_stage"] = stage_names.get(stage_code, f"unknown_{stage_code}")

View File

@ -25,21 +25,23 @@ def print_banner():
def get_input(prompt, default=None, required=True): def get_input(prompt, default=None, required=True):
"""Get user input with optional default value""" """Get user input with optional default value (loop-based to avoid stack overflow)"""
if default: while True:
prompt = f"{prompt} [{default}]: " if default:
else: full_prompt = f"{prompt} [{default}]: "
prompt = f"{prompt}: " else:
full_prompt = f"{prompt}: "
value = input(prompt).strip()
value = input(full_prompt).strip()
if not value and default:
return default if value:
if not value and required: return value
print("This field is required!") if default:
return get_input(prompt.replace(f" [{default}]", "").replace(": ", ""), default, required) return default
if required:
return value print("This field is required!")
continue
return value
def setup_mqtt(): def setup_mqtt():
@ -62,13 +64,18 @@ def setup_mqtt():
def save_config(config): def save_config(config):
"""Save configuration to file""" """Save configuration to file with restricted permissions"""
os.makedirs(CONFIG_DIR, exist_ok=True) os.makedirs(CONFIG_DIR, exist_ok=True)
with open(CONFIG_FILE, "w") as f: with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
# Restrict file permissions (owner read/write only) for security
# Note: MQTT credentials are stored in plaintext
os.chmod(CONFIG_FILE, 0o600)
print(f"\n✓ Config saved to: {CONFIG_FILE}") print(f"\n✓ Config saved to: {CONFIG_FILE}")
print(" (Note: MQTT credentials stored in plaintext - file permissions set to 600)")
def setup_directories(): def setup_directories():
@ -148,12 +155,14 @@ def install_dependencies():
"""Install required Python packages""" """Install required Python packages"""
print("\n=== Installing Dependencies ===\n") print("\n=== Installing Dependencies ===\n")
try: # Use python -m pip to ensure we're using the correct Python interpreter
os.system("pip install paho-mqtt") exit_code = os.system(f"{sys.executable} -m pip install paho-mqtt")
if exit_code == 0:
print("✓ paho-mqtt installed") print("✓ paho-mqtt installed")
except Exception as e: else:
print(f"Failed to install paho-mqtt: {e}") print(f"pip install failed (exit code: {exit_code})")
print(" Run manually: pip install paho-mqtt") print(f" Run manually: {sys.executable} -m pip install paho-mqtt")
def print_gadgetbridge_instructions(export_dir): def print_gadgetbridge_instructions(export_dir):
@ -224,7 +233,9 @@ def main():
install_dependencies() install_dependencies()
# Download/copy main script # Download/copy main script
download_main_script() if not download_main_script():
print("\n✗ Setup aborted: main script is required.")
sys.exit(1)
# Create autostart # Create autostart
create_autostart() create_autostart()