diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/Readme.md b/Readme.md index 866255e..8eadcc8 100644 --- a/Readme.md +++ b/Readme.md @@ -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 | Sensor | Topic | Unit | diff --git a/main.py b/main.py index b6eed6e..f6cfcf6 100644 --- a/main.py +++ b/main.py @@ -239,10 +239,11 @@ class GadgetbridgeMQTT: logger.debug(f"Heart rate query failed: {e}") # Daily Summary Data (filtered by device) + # Note: XIAOMI_DAILY_SUMMARY_SAMPLE uses MILLISECONDS timestamps try: 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", - (self.device_id, day_midnight) + (self.device_id, day_midnight * 1000) # Convert to milliseconds ) row = cursor.fetchone() if row: @@ -254,8 +255,10 @@ class GadgetbridgeMQTT: logger.debug(f"Daily summary query failed: {e}") # Sleep Data (filtered by device) + # Note: XIAOMI_SLEEP_TIME_SAMPLE uses MILLISECONDS timestamps 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( """ @@ -265,7 +268,7 @@ class GadgetbridgeMQTT: ORDER BY TIMESTAMP DESC LIMIT 1 """, - (self.device_id, day_ago_ts) + (self.device_id, day_ago_ts_ms) ) row = cursor.fetchone() if row: @@ -274,13 +277,18 @@ class GadgetbridgeMQTT: # Convert duration to hours if total_duration is not None: data["sleep_duration"] = round(total_duration / 60.0, 2) - # NULL means "not finalized", treat as False (not awake) - # 0 means explicitly "still asleep" - # 1 means explicitly "woke up" - if is_awake_flag is None: - is_awake = False # No data = not awake + + # Determine if user is awake: + # 1. If IS_AWAKE flag is explicitly set to 1, user is awake + # 2. If WAKEUP_TIME exists and is in the past, user is 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: - is_awake = (is_awake_flag == 1) + is_awake = False data["is_awake"] = is_awake @@ -288,6 +296,7 @@ class GadgetbridgeMQTT: logger.debug(f"Sleep query failed: {e}") # Sleep Stage Data (current stage) + # Note: XIAOMI_SLEEP_STAGE_SAMPLE uses MILLISECONDS timestamps try: cursor.execute( """ @@ -303,9 +312,16 @@ class GadgetbridgeMQTT: if 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 = { - 0: "awake", 1: "light_sleep", 2: "deep_sleep", - 3: "rem_sleep", 4: "deep_sleep_v2", 5: "light_sleep_v2" + 0: "not_sleep", + 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}") diff --git a/setup.py b/setup.py index 75814ad..08d7f80 100644 --- a/setup.py +++ b/setup.py @@ -25,21 +25,23 @@ def print_banner(): def get_input(prompt, default=None, required=True): - """Get user input with optional default value""" - if default: - prompt = f"{prompt} [{default}]: " - else: - prompt = f"{prompt}: " - - value = input(prompt).strip() - - if not value and default: - return default - if not value and required: - print("This field is required!") - return get_input(prompt.replace(f" [{default}]", "").replace(": ", ""), default, required) - - return value + """Get user input with optional default value (loop-based to avoid stack overflow)""" + while True: + if default: + full_prompt = f"{prompt} [{default}]: " + else: + full_prompt = f"{prompt}: " + + value = input(full_prompt).strip() + + if value: + return value + if default: + return default + if required: + print("This field is required!") + continue + return value def setup_mqtt(): @@ -62,13 +64,18 @@ def setup_mqtt(): def save_config(config): - """Save configuration to file""" + """Save configuration to file with restricted permissions""" os.makedirs(CONFIG_DIR, exist_ok=True) with open(CONFIG_FILE, "w") as f: 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(" (Note: MQTT credentials stored in plaintext - file permissions set to 600)") def setup_directories(): @@ -148,12 +155,14 @@ def install_dependencies(): """Install required Python packages""" print("\n=== Installing Dependencies ===\n") - try: - os.system("pip install paho-mqtt") + # Use python -m pip to ensure we're using the correct Python interpreter + exit_code = os.system(f"{sys.executable} -m pip install paho-mqtt") + + if exit_code == 0: print("✓ paho-mqtt installed") - except Exception as e: - print(f"✗ Failed to install paho-mqtt: {e}") - print(" Run manually: pip install paho-mqtt") + else: + print(f"✗ pip install failed (exit code: {exit_code})") + print(f" Run manually: {sys.executable} -m pip install paho-mqtt") def print_gadgetbridge_instructions(export_dir): @@ -224,7 +233,9 @@ def main(): install_dependencies() # 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()