homeassistant_integration.py 17 KB

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