add sleep sensors
This commit is contained in:
parent
7ba2db2a6f
commit
ffd8493167
@ -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
|
|
||||||
|
|||||||
@ -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
113
main.py
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user