diff --git a/Readme.md b/Readme.md index 4ae76b7..81e87cd 100644 --- a/Readme.md +++ b/Readme.md @@ -4,15 +4,10 @@ This is a Gadgetbridge MQTT bridge for TrueNAS Scale, which allows you to connec ## Setup -- edit [```compose.yaml```](./compose.yaml) and set - - mount points for your data - - mount point for your Gadgetbridge database - - your Timezone - - environment variables for your MQTT broker - - your DEVICE_NAME. (Can be skipped first time: If you start the app with "unknown" as DEVICE_NAME, you will see all devices from the Gadgetbridge Database in your App Log.) - - create ```Config``` Dataset in TrueNAS Scale with App Preset "Generic" - - write your compose.yaml into the Config Dataset, e.g. ```sudo nano /mnt/Data/Apps/GadgetbridgeMqtt/Config/compose.yaml``` - - use the Console give admin access (instead of root) e.g:```sudo chown -R 950:950 /mnt/Data/Apps/GadgetbridgeMqtt/App``` (this should prevent apps to modify the config?) -- include it in your TrueNAS Scale Custom Apps e.g. ```include: [/mnt/Data/Apps/GadgetbridgeMqtt/Config/compose.yaml]``` +- copy [```compose.yaml```](./compose.yaml) your TrueNAS Scale Custom App and set +- mount points for your data +- mount point for your Gadgetbridge database +- your Timezone +- environment variables for your MQTT broker +- create ```Config``` Dataset in TrueNAS Scale with App Preset "Generic" - start the app - - find your DEVICE_NAME in the App Log and set it in the compose.yaml, then restart diff --git a/compose.yaml b/compose.yaml index 1b437f9..43662df 100644 --- a/compose.yaml +++ b/compose.yaml @@ -15,7 +15,6 @@ services: - MQTT_USERNAME=***** - MQTT_PASSWORD=***** - GADGETBRIDGE_DB_PATH=/data/Gadgetbridge.db - - DEVICE_NAME="unknown" - PYTHONUNBUFFERED=1 - PUBLISH_INTERVAL_SECONDS=300 - DAY_END_TIME=5 # 5 AM diff --git a/main.py b/main.py index 2c92ed5..a1e88c5 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,6 @@ class GadgetbridgeMQTTPublisher: self.load_config() self.mqtt_client = None self.publish_interval = int(os.getenv("PUBLISH_INTERVAL_SECONDS", "300")) - # Define sensors here for easy extension self.sensors = [ { "name": "Daily Steps", @@ -89,6 +88,67 @@ class GadgetbridgeMQTTPublisher: "state_class": "measurement", "query": self.query_latest_heart_rate, }, + { + "name": "Resting Heart Rate", + "unique_id": "hr_resting", + "state_topic": f"gadgetbridge/{self.device_name}/hr_resting", + "unit_of_measurement": "bpm", + "icon": "mdi:heart-pulse", + "state_class": "measurement", + "query": self.query_hr_resting, + }, + { + "name": "Max Heart Rate", + "unique_id": "hr_max", + "state_topic": f"gadgetbridge/{self.device_name}/hr_max", + "unit_of_measurement": "bpm", + "icon": "mdi:heart-pulse", + "state_class": "measurement", + "query": self.query_hr_max, + }, + { + "name": "Average Heart Rate", + "unique_id": "hr_avg", + "state_topic": f"gadgetbridge/{self.device_name}/hr_avg", + "unit_of_measurement": "bpm", + "icon": "mdi:heart-pulse", + "state_class": "measurement", + "query": self.query_hr_avg, + }, + { + "name": "Calories", + "unique_id": "calories", + "state_topic": f"gadgetbridge/{self.device_name}/calories", + "unit_of_measurement": "kcal", + "icon": "mdi:fire", + "state_class": "total_increasing", + "query": self.query_calories, + }, + { + "name": "Wakeup Time", + "unique_id": "wakeup_time", + "state_topic": f"gadgetbridge/{self.device_name}/wakeup_time", + "icon": "mdi:weather-sunset-up", + "device_class": "timestamp", + "query": self.query_wakeup_time, + }, + { + "name": "Is Awake", + "unique_id": "is_awake", + "state_topic": f"gadgetbridge/{self.device_name}/is_awake", + "icon": "mdi:power-sleep", + "device_class": "enum", + "query": self.query_is_awake, + }, + { + "name": "Total Sleep Duration", + "unique_id": "total_sleep_duration", + "state_topic": f"gadgetbridge/{self.device_name}/total_sleep_duration", + "unit_of_measurement": "h", + "icon": "mdi:sleep", + "state_class": "measurement", + "query": self.query_total_sleep_duration, + }, ] def setup_logging(self): @@ -212,6 +272,57 @@ class GadgetbridgeMQTTPublisher: row = cursor.fetchone() return row[0] if row else None + def query_hr_resting(self, cursor) -> Any: + cursor.execute( + "SELECT HR_RESTING FROM XIAOMI_DAILY_SUMMARY_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + return row[0] if row else None + + def query_hr_max(self, cursor) -> Any: + cursor.execute( + "SELECT HR_MAX FROM XIAOMI_DAILY_SUMMARY_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + return row[0] if row else None + + def query_hr_avg(self, cursor) -> Any: + cursor.execute( + "SELECT HR_AVG FROM XIAOMI_DAILY_SUMMARY_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + return row[0] if row else None + + def query_calories(self, cursor) -> Any: + cursor.execute( + "SELECT CALORIES FROM XIAOMI_DAILY_SUMMARY_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + return row[0] if row else None + + def query_wakeup_time(self, cursor) -> Any: + cursor.execute( + "SELECT WAKEUP_TIME FROM XIAOMI_SLEEP_TIME_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + return datetime.fromtimestamp(row[0]).isoformat() if row and row[0] else None + + def query_is_awake(self, cursor) -> Any: + cursor.execute( + "SELECT IS_AWAKE FROM XIAOMI_SLEEP_TIME_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + # Return as boolean or string for Home Assistant + return bool(row[0]) if row else None + + def query_total_sleep_duration(self, cursor) -> Any: + cursor.execute( + "SELECT TOTAL_DURATION FROM XIAOMI_SLEEP_TIME_SAMPLE ORDER BY TIMESTAMP DESC LIMIT 1" + ) + row = cursor.fetchone() + # Convert minutes to hours, round to 2 decimals + return round(row[0] / 60, 2) if row and row[0] is not None else None + def get_sensor_data(self) -> Dict[str, Any]: """Query all sensors and return their values as a dict""" if not os.path.exists(self.db_path):