add sleep sensors

This commit is contained in:
Oliver Großkloß 2025-07-17 13:17:20 +02:00
parent 7ba2db2a6f
commit ffd8493167
3 changed files with 118 additions and 13 deletions

View File

@ -4,15 +4,10 @@ This is a Gadgetbridge MQTT bridge for TrueNAS Scale, which allows you to connec
## Setup ## Setup
- edit [```compose.yaml```](./compose.yaml) and set - copy [```compose.yaml```](./compose.yaml) your TrueNAS Scale Custom App and set
- mount points for your data - mount points for your data
- mount point for your Gadgetbridge database - mount point for your Gadgetbridge database
- your Timezone - your Timezone
- environment variables for your MQTT broker - 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" - 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]```
- start the app - start the app
- find your DEVICE_NAME in the App Log and set it in the compose.yaml, then restart

View File

@ -15,7 +15,6 @@ services:
- MQTT_USERNAME=***** - MQTT_USERNAME=*****
- MQTT_PASSWORD=***** - MQTT_PASSWORD=*****
- GADGETBRIDGE_DB_PATH=/data/Gadgetbridge.db - GADGETBRIDGE_DB_PATH=/data/Gadgetbridge.db
- DEVICE_NAME="unknown"
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
- PUBLISH_INTERVAL_SECONDS=300 - PUBLISH_INTERVAL_SECONDS=300
- DAY_END_TIME=5 # 5 AM - DAY_END_TIME=5 # 5 AM

113
main.py
View File

@ -25,7 +25,6 @@ class GadgetbridgeMQTTPublisher:
self.load_config() self.load_config()
self.mqtt_client = None self.mqtt_client = None
self.publish_interval = int(os.getenv("PUBLISH_INTERVAL_SECONDS", "300")) self.publish_interval = int(os.getenv("PUBLISH_INTERVAL_SECONDS", "300"))
# Define sensors here for easy extension
self.sensors = [ self.sensors = [
{ {
"name": "Daily Steps", "name": "Daily Steps",
@ -89,6 +88,67 @@ class GadgetbridgeMQTTPublisher:
"state_class": "measurement", "state_class": "measurement",
"query": self.query_latest_heart_rate, "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): def setup_logging(self):
@ -212,6 +272,57 @@ class GadgetbridgeMQTTPublisher:
row = cursor.fetchone() row = cursor.fetchone()
return row[0] if row else None 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]: def get_sensor_data(self) -> Dict[str, Any]:
"""Query all sensors and return their values as a dict""" """Query all sensors and return their values as a dict"""
if not os.path.exists(self.db_path): if not os.path.exists(self.db_path):