From 2c35c39a1cc07ac50ff9ddaeeadab21a17679cc2 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart" <git@edgarpierre.fr>
Date: Sun, 9 Mar 2025 23:10:45 +0100
Subject: [PATCH] Add uptime sensor and state topics for system and user
 clients

---
 hasspy/__init__.py |  1 +
 hasspy/mqtt.py     | 27 +++++++++++++++++++++++++++
 2 files changed, 28 insertions(+)

diff --git a/hasspy/__init__.py b/hasspy/__init__.py
index 7807b61..565cd4e 100644
--- a/hasspy/__init__.py
+++ b/hasspy/__init__.py
@@ -66,5 +66,6 @@ def main() -> int:
 
     log.info("Shutting down")
 
+    ha.timer.cancel()
     ha.loop_stop()
     return 0
diff --git a/hasspy/mqtt.py b/hasspy/mqtt.py
index 5f60c67..43e5e02 100644
--- a/hasspy/mqtt.py
+++ b/hasspy/mqtt.py
@@ -2,6 +2,7 @@ import io
 import json
 import logging
 import re
+from datetime import datetime, timezone
 from pathlib import Path
 from subprocess import run
 from threading import Thread, Timer
@@ -150,6 +151,10 @@ class HassSystemClient(HassClient):
             if code != 0:
                 log.error(f"Failed to execute command: {cmd}")
 
+    @property
+    def state_topic(self) -> str:
+        return f"{self.node_id}/system/state"
+
     @property
     def components(self) -> dict[str, dict[str, Any]]:
         return {
@@ -176,6 +181,14 @@ class HassSystemClient(HassClient):
                 "icon": "mdi:sleep",
                 "payload_press": "SUSPEND",
             },
+            "uptime": {
+                "unique_id": f"{self.node_id}_uptime",
+                "p": "sensor",
+                "name": "Uptime",
+                "icon": "mdi:clock",
+                "device_class": "timestamp",
+                "value_template": "{{ value_json.uptime|timestamp_utc }}",
+            },
         }
 
     @property
@@ -184,8 +197,18 @@ class HassSystemClient(HassClient):
             "power": "POWER_OFF"
             if Path("/run/systemd/shutdown/scheduled").exists()
             else "POWER_ON",
+            "uptime": self.uptime_value,
         }
 
+    @property
+    def uptime_value(self) -> float | None:
+        code, out = run_command(["uptime", "--since"])
+        if code != 0:
+            log.error("Failed to get uptime")
+            return None
+
+        return datetime.fromisoformat(out.strip()).astimezone(timezone.utc).timestamp()
+
 
 class HassUserClient(HassClient):
     commands = {
@@ -224,6 +247,10 @@ class HassUserClient(HassClient):
     def availability_topic(self) -> str:
         return f"{self.node_id}/user/availability"
 
+    @property
+    def state_topic(self) -> str:
+        return f"{self.node_id}/user/state"
+
     @property
     def components(self) -> dict[str, dict[str, Any]]:
         return {