fix sleep stage
This commit is contained in:
parent
e7c00f6901
commit
592530aac2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
@ -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
38
main.py
@ -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}")
|
||||||
|
|||||||
51
setup.py
51
setup.py
@ -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:
|
if value:
|
||||||
return default
|
return value
|
||||||
if not value and required:
|
if default:
|
||||||
print("This field is required!")
|
return default
|
||||||
return get_input(prompt.replace(f" [{default}]", "").replace(": ", ""), default, required)
|
if required:
|
||||||
|
print("This field is required!")
|
||||||
return value
|
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()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user