homeassistant_integration.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. #!/usr/bin/env python3
  2. """
  3. Home Assistant Integration for YouTube Predownloader
  4. Publishes real-time statistics via MQTT for Home Assistant monitoring
  5. """
  6. import json
  7. import time
  8. import threading
  9. import psutil
  10. import os
  11. from datetime import datetime
  12. from typing import Dict, Any, Optional
  13. import paho.mqtt.client as mqtt
  14. # MQTT Configuration
  15. MQTT_BROKER = "server.local.lan" # Change to your Home Assistant IP
  16. MQTT_PORT = 1883
  17. MQTT_USERNAME = "" # Set if authentication required
  18. MQTT_PASSWORD = "" # Set if authentication required
  19. MQTT_BASE_TOPIC = "youtube_predownloader"
  20. # Home Assistant Discovery Topics
  21. HA_DISCOVERY_PREFIX = "homeassistant"
  22. class HomeAssistantIntegration:
  23. def __init__(self, mqtt_broker=MQTT_BROKER, mqtt_port=MQTT_PORT):
  24. self.mqtt_broker = mqtt_broker
  25. self.mqtt_port = mqtt_port
  26. self.mqtt_client = None
  27. self.process = psutil.Process()
  28. self.stats = {
  29. 'download_queue_length': 0,
  30. 'conversion_queue_length': 0,
  31. 'download_progress': 0,
  32. 'conversion_progress': 0,
  33. 'download_speed': 0,
  34. 'cpu_usage': 0,
  35. 'memory_usage': 0,
  36. 'memory_usage_mb': 0,
  37. 'active_downloads': 0,
  38. 'active_conversions': 0,
  39. 'total_downloads_today': 0,
  40. 'total_conversions_today': 0,
  41. 'service_status': 'waiting', # waiting, running, completed, error
  42. 'next_run_time': None,
  43. 'current_run_start': None,
  44. 'last_run_completed': None,
  45. 'last_updated': None
  46. }
  47. self.is_connected = False
  48. self.update_thread = None
  49. self.running = False
  50. def connect_mqtt(self):
  51. """Connect to MQTT broker"""
  52. try:
  53. self.mqtt_client = mqtt.Client()
  54. if MQTT_USERNAME and MQTT_PASSWORD:
  55. self.mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
  56. self.mqtt_client.on_connect = self._on_mqtt_connect
  57. self.mqtt_client.on_disconnect = self._on_mqtt_disconnect
  58. self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, 60)
  59. self.mqtt_client.loop_start()
  60. print(f"🏠 Connecting to Home Assistant MQTT broker at {self.mqtt_broker}:{self.mqtt_port}")
  61. return True
  62. except Exception as e:
  63. print(f"❌ Failed to connect to MQTT broker: {e}")
  64. return False
  65. def _on_mqtt_connect(self, client, userdata, flags, rc):
  66. """MQTT connection callback"""
  67. if rc == 0:
  68. self.is_connected = True
  69. print("✅ Connected to Home Assistant MQTT broker")
  70. self._publish_discovery_configs()
  71. else:
  72. print(f"❌ Failed to connect to MQTT broker, return code {rc}")
  73. def _on_mqtt_disconnect(self, client, userdata, rc):
  74. """MQTT disconnection callback"""
  75. self.is_connected = False
  76. print("⚠️ Disconnected from MQTT broker")
  77. def _publish_discovery_configs(self):
  78. """Publish Home Assistant MQTT Discovery configurations"""
  79. sensors = [
  80. {
  81. 'name': 'YouTube Download Queue',
  82. 'key': 'download_queue_length',
  83. 'unit': 'videos',
  84. 'icon': 'mdi:download-multiple',
  85. 'device_class': None
  86. },
  87. {
  88. 'name': 'YouTube Conversion Queue',
  89. 'key': 'conversion_queue_length',
  90. 'unit': 'videos',
  91. 'icon': 'mdi:video-vintage',
  92. 'device_class': None
  93. },
  94. {
  95. 'name': 'YouTube Download Progress',
  96. 'key': 'download_progress',
  97. 'unit': '%',
  98. 'icon': 'mdi:progress-download',
  99. 'device_class': None
  100. },
  101. {
  102. 'name': 'YouTube Conversion Progress',
  103. 'key': 'conversion_progress',
  104. 'unit': '%',
  105. 'icon': 'mdi:progress-wrench',
  106. 'device_class': None
  107. },
  108. {
  109. 'name': 'YouTube Download Speed',
  110. 'key': 'download_speed',
  111. 'unit': 'MB/s',
  112. 'icon': 'mdi:speedometer',
  113. 'device_class': None
  114. },
  115. {
  116. 'name': 'YouTube CPU Usage',
  117. 'key': 'cpu_usage',
  118. 'unit': '%',
  119. 'icon': 'mdi:cpu-64-bit',
  120. 'device_class': None
  121. },
  122. {
  123. 'name': 'YouTube Memory Usage',
  124. 'key': 'memory_usage',
  125. 'unit': '%',
  126. 'icon': 'mdi:memory',
  127. 'device_class': None
  128. },
  129. {
  130. 'name': 'YouTube Memory Usage MB',
  131. 'key': 'memory_usage_mb',
  132. 'unit': 'MB',
  133. 'icon': 'mdi:memory',
  134. 'device_class': None
  135. },
  136. {
  137. 'name': 'YouTube Active Downloads',
  138. 'key': 'active_downloads',
  139. 'unit': 'downloads',
  140. 'icon': 'mdi:download',
  141. 'device_class': None
  142. },
  143. {
  144. 'name': 'YouTube Active Conversions',
  145. 'key': 'active_conversions',
  146. 'unit': 'conversions',
  147. 'icon': 'mdi:cog',
  148. 'device_class': None
  149. },
  150. {
  151. 'name': 'YouTube Service Status',
  152. 'key': 'service_status',
  153. 'unit': None,
  154. 'icon': 'mdi:play-circle-outline',
  155. 'device_class': None
  156. },
  157. {
  158. 'name': 'YouTube Next Run Time',
  159. 'key': 'next_run_time',
  160. 'unit': None,
  161. 'icon': 'mdi:clock-outline',
  162. 'device_class': 'timestamp'
  163. },
  164. {
  165. 'name': 'YouTube Current Run Start',
  166. 'key': 'current_run_start',
  167. 'unit': None,
  168. 'icon': 'mdi:clock-start',
  169. 'device_class': 'timestamp'
  170. },
  171. {
  172. 'name': 'YouTube Last Run Completed',
  173. 'key': 'last_run_completed',
  174. 'unit': None,
  175. 'icon': 'mdi:clock-check-outline',
  176. 'device_class': 'timestamp'
  177. }
  178. ]
  179. for sensor in sensors:
  180. config = {
  181. 'name': sensor['name'],
  182. 'unique_id': f"{MQTT_BASE_TOPIC}_{sensor['key']}",
  183. 'state_topic': f"{MQTT_BASE_TOPIC}/sensor/{sensor['key']}/state",
  184. 'icon': sensor['icon'],
  185. 'device': {
  186. 'identifiers': [MQTT_BASE_TOPIC],
  187. 'name': 'YouTube Predownloader',
  188. 'model': 'YouTube Predownloader v1.0',
  189. 'manufacturer': 'Custom',
  190. 'sw_version': '1.0.0'
  191. }
  192. }
  193. if sensor['unit']:
  194. config['unit_of_measurement'] = sensor['unit']
  195. if sensor['device_class']:
  196. config['device_class'] = sensor['device_class']
  197. discovery_topic = f"{HA_DISCOVERY_PREFIX}/sensor/{MQTT_BASE_TOPIC}_{sensor['key']}/config"
  198. self.mqtt_client.publish(discovery_topic, json.dumps(config), retain=True)
  199. print("📡 Published Home Assistant discovery configurations")
  200. def update_stats(self, download_queue=0, conversion_queue=0, download_progress=0,
  201. conversion_progress=0, download_speed=0, active_downloads=0, active_conversions=0,
  202. service_status=None, next_run_time=None, current_run_start=None, last_run_completed=None):
  203. """Update statistics"""
  204. # Update provided stats
  205. update_dict = {
  206. 'download_queue_length': download_queue,
  207. 'conversion_queue_length': conversion_queue,
  208. 'download_progress': round(download_progress, 1),
  209. 'conversion_progress': round(conversion_progress, 1),
  210. 'download_speed': round(download_speed, 2),
  211. 'active_downloads': active_downloads,
  212. 'active_conversions': active_conversions,
  213. 'last_updated': datetime.now().isoformat()
  214. }
  215. # Add optional scheduling stats
  216. if service_status is not None:
  217. update_dict['service_status'] = service_status
  218. if next_run_time is not None:
  219. update_dict['next_run_time'] = next_run_time
  220. if current_run_start is not None:
  221. update_dict['current_run_start'] = current_run_start
  222. if last_run_completed is not None:
  223. update_dict['last_run_completed'] = last_run_completed
  224. self.stats.update(update_dict)
  225. # Update system stats
  226. try:
  227. self.stats['cpu_usage'] = round(self.process.cpu_percent(), 1)
  228. memory_info = self.process.memory_info()
  229. memory_percent = self.process.memory_percent()
  230. self.stats['memory_usage'] = round(memory_percent, 1)
  231. self.stats['memory_usage_mb'] = round(memory_info.rss / 1024 / 1024, 1)
  232. except (psutil.NoSuchProcess, psutil.AccessDenied):
  233. pass
  234. # Publish to MQTT
  235. if self.is_connected:
  236. self._publish_stats()
  237. def _publish_stats(self):
  238. """Publish statistics to MQTT"""
  239. for key, value in self.stats.items():
  240. if key != 'last_updated':
  241. topic = f"{MQTT_BASE_TOPIC}/sensor/{key}/state"
  242. self.mqtt_client.publish(topic, str(value))
  243. # Publish combined status
  244. status_topic = f"{MQTT_BASE_TOPIC}/status"
  245. self.mqtt_client.publish(status_topic, json.dumps(self.stats))
  246. def start_monitoring(self, update_interval=10):
  247. """Start monitoring and publishing stats"""
  248. self.running = True
  249. self.update_thread = threading.Thread(target=self._monitoring_loop, args=(update_interval,), daemon=True)
  250. self.update_thread.start()
  251. print(f"📊 Started Home Assistant monitoring (update interval: {update_interval}s)")
  252. def stop_monitoring(self):
  253. """Stop monitoring"""
  254. self.running = False
  255. if self.mqtt_client:
  256. self.mqtt_client.loop_stop()
  257. self.mqtt_client.disconnect()
  258. print("🛑 Stopped Home Assistant monitoring")
  259. def _monitoring_loop(self, interval):
  260. """Background monitoring loop"""
  261. while self.running:
  262. try:
  263. # Update system stats only (other stats updated via update_stats method)
  264. self.update_stats(
  265. self.stats['download_queue_length'],
  266. self.stats['conversion_queue_length'],
  267. self.stats['download_progress'],
  268. self.stats['conversion_progress'],
  269. self.stats['download_speed'],
  270. self.stats['active_downloads'],
  271. self.stats['active_conversions']
  272. )
  273. time.sleep(interval)
  274. except Exception as e:
  275. print(f"❌ Monitoring error: {e}")
  276. time.sleep(interval)
  277. # Global instance
  278. ha_integration = None
  279. def init_homeassistant_integration(mqtt_broker=MQTT_BROKER, mqtt_port=MQTT_PORT):
  280. """Initialize Home Assistant integration"""
  281. """Initialize Home Assistant integration"""
  282. global ha_integration
  283. if ha_integration is None:
  284. ha_integration = HomeAssistantIntegration(mqtt_broker, mqtt_port)
  285. if ha_integration.connect_mqtt():
  286. ha_integration.start_monitoring()
  287. return ha_integration
  288. else:
  289. ha_integration = None
  290. return None
  291. return ha_integration
  292. def update_homeassistant_stats(**kwargs):
  293. """Update Home Assistant statistics"""
  294. global ha_integration
  295. if ha_integration and ha_integration.is_connected:
  296. ha_integration.update_stats(**kwargs)
  297. def update_service_status(status, next_run=None, current_run_start=None, last_completed=None):
  298. """Update service scheduling status"""
  299. global ha_integration
  300. if ha_integration and ha_integration.is_connected:
  301. ha_integration.update_stats(
  302. service_status=status,
  303. next_run_time=next_run,
  304. current_run_start=current_run_start,
  305. last_run_completed=last_completed
  306. )
  307. def set_service_waiting(next_run_time):
  308. """Set service status to waiting with next run time"""
  309. update_service_status('waiting', next_run=next_run_time)
  310. def set_service_running():
  311. """Set service status to running"""
  312. update_service_status('running', current_run_start=datetime.now().isoformat())
  313. def set_service_completed():
  314. """Set service status to completed"""
  315. update_service_status('completed', last_completed=datetime.now().isoformat())
  316. def set_service_error():
  317. """Set service status to error"""
  318. update_service_status('error')
  319. def stop_homeassistant_integration():
  320. """Stop Home Assistant integration"""
  321. global ha_integration
  322. if ha_integration:
  323. ha_integration.stop_monitoring()
  324. ha_integration = None
  325. if __name__ == "__main__":
  326. # Test the integration
  327. print("🧪 Testing Home Assistant integration...")
  328. integration = init_homeassistant_integration()
  329. if integration:
  330. print("✅ Integration started successfully")
  331. # Simulate some data
  332. for i in range(5):
  333. update_homeassistant_stats(
  334. download_queue=i,
  335. conversion_queue=i*2,
  336. download_progress=i*20,
  337. conversion_progress=i*15,
  338. download_speed=i*2.5,
  339. active_downloads=min(i, 2),
  340. active_conversions=min(i, 2)
  341. )
  342. time.sleep(2)
  343. stop_homeassistant_integration()
  344. print("✅ Test completed")
  345. else:
  346. print("❌ Failed to start integration")