homeassistant_integration.py 15 KB

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