hyperion_controller.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import requests
  2. import json
  3. from typing import Dict, Optional
  4. import time
  5. import logging
  6. logger = logging.getLogger(__name__)
  7. class HyperionController:
  8. """Controller for Hyperion LED system using JSON-RPC API"""
  9. def __init__(self, ip_address: Optional[str] = None, port: int = 8090):
  10. self.ip_address = ip_address
  11. self.port = port
  12. # Priority for Dune Weaver effects (lower = higher priority)
  13. # Using 100 to allow user to override with lower priorities if needed
  14. self.priority = 100
  15. def _get_base_url(self) -> str:
  16. """Get base URL for Hyperion JSON-RPC API"""
  17. if not self.ip_address:
  18. raise ValueError("No Hyperion IP configured")
  19. return f"http://{self.ip_address}:{self.port}/json-rpc"
  20. def set_ip(self, ip_address: str, port: int = 8090) -> None:
  21. """Update the Hyperion IP address and port"""
  22. self.ip_address = ip_address
  23. self.port = port
  24. def _send_command(self, command: str, **params) -> Dict:
  25. """Send JSON-RPC command to Hyperion and return response"""
  26. try:
  27. url = self._get_base_url()
  28. payload = {
  29. "command": command,
  30. **params
  31. }
  32. response = requests.post(url, json=payload, timeout=2)
  33. response.raise_for_status()
  34. result = response.json()
  35. if not result.get("success", False):
  36. error_msg = result.get("error", "Unknown error")
  37. return {
  38. "connected": False,
  39. "message": f"Hyperion command failed: {error_msg}"
  40. }
  41. return {
  42. "connected": True,
  43. "message": "Command successful",
  44. "response": result
  45. }
  46. except ValueError as e:
  47. return {"connected": False, "message": str(e)}
  48. except requests.RequestException as e:
  49. return {"connected": False, "message": f"Cannot connect to Hyperion: {str(e)}"}
  50. except json.JSONDecodeError as e:
  51. return {"connected": False, "message": f"Error parsing Hyperion response: {str(e)}"}
  52. def check_hyperion_status(self) -> Dict:
  53. """Check Hyperion connection status and component state"""
  54. result = self._send_command("serverinfo")
  55. if result.get("connected"):
  56. response = result.get("response", {})
  57. info = response.get("info", {})
  58. components = {c["name"]: c["enabled"] for c in info.get("components", [])}
  59. return {
  60. "connected": True,
  61. "is_on": components.get("ALL", False),
  62. "ledstream_on": components.get("LEDDEVICE", False),
  63. "hostname": info.get("hostname", "unknown"),
  64. "version": info.get("version", "unknown"),
  65. "message": "Hyperion is ON" if components.get("ALL", False) else "Hyperion is OFF"
  66. }
  67. return result
  68. def set_power(self, state: int) -> Dict:
  69. """
  70. Set Hyperion power state (component control)
  71. Args:
  72. state: 0=Off, 1=On, 2=Toggle
  73. """
  74. if state not in [0, 1, 2]:
  75. return {"connected": False, "message": "Power state must be 0 (Off), 1 (On), or 2 (Toggle)"}
  76. if state == 2:
  77. # Get current state and toggle
  78. status = self.check_hyperion_status()
  79. if not status.get("connected"):
  80. return status
  81. state = 0 if status.get("is_on", False) else 1
  82. result = self._send_command(
  83. "componentstate",
  84. componentstate={
  85. "component": "ALL",
  86. "state": bool(state)
  87. }
  88. )
  89. return result
  90. def set_color(self, r: int = 0, g: int = 0, b: int = 0, duration: int = 86400000) -> Dict:
  91. """
  92. Set solid color on Hyperion
  93. Args:
  94. r, g, b: RGB values (0-255)
  95. duration: Duration in milliseconds (default = 86400000ms = 24 hours)
  96. Note: Some Hyperion instances don't support duration=0 for infinite
  97. """
  98. if not all(0 <= val <= 255 for val in [r, g, b]):
  99. return {"connected": False, "message": "RGB values must be between 0 and 255"}
  100. result = self._send_command(
  101. "color",
  102. priority=self.priority,
  103. color=[r, g, b],
  104. duration=duration
  105. )
  106. return result
  107. def set_effect(self, effect_name: str, args: Optional[Dict] = None, duration: int = 86400000) -> Dict:
  108. """
  109. Set Hyperion effect
  110. Args:
  111. effect_name: Name of the effect (e.g., 'Rainbow swirl', 'Warm mood blobs')
  112. args: Optional effect arguments
  113. duration: Duration in milliseconds (default = 86400000ms = 24 hours)
  114. """
  115. params = {
  116. "priority": self.priority,
  117. "effect": {"name": effect_name},
  118. "duration": duration
  119. }
  120. if args:
  121. params["effect"]["args"] = args
  122. result = self._send_command("effect", **params)
  123. return result
  124. def clear_priority(self, priority: Optional[int] = None) -> Dict:
  125. """
  126. Clear a specific priority or Dune Weaver's priority
  127. Args:
  128. priority: Priority to clear (defaults to self.priority)
  129. """
  130. if priority is None:
  131. priority = self.priority
  132. result = self._send_command("clear", priority=priority)
  133. return result
  134. def clear_all(self) -> Dict:
  135. """Clear all priorities (return to default state)"""
  136. result = self._send_command("clear", priority=-1)
  137. return result
  138. def set_brightness(self, value: int) -> Dict:
  139. """
  140. Set Hyperion brightness
  141. Args:
  142. value: Brightness (0-100)
  143. """
  144. if not 0 <= value <= 100:
  145. return {"connected": False, "message": "Brightness must be between 0 and 100"}
  146. result = self._send_command(
  147. "adjustment",
  148. adjustment={
  149. "brightness": value
  150. }
  151. )
  152. return result
  153. def effect_loading(hyperion_controller: HyperionController) -> bool:
  154. """Show loading effect - orange color (24 hour duration)"""
  155. # Turn on Hyperion first
  156. hyperion_controller.set_power(1)
  157. res = hyperion_controller.set_color(r=255, g=160, b=0, duration=86400000)
  158. return res.get('connected', False)
  159. def effect_idle(hyperion_controller: HyperionController, effect_name: str = None) -> bool:
  160. """Show idle effect - use configured effect or clear priority to return to default"""
  161. # Turn on Hyperion first
  162. hyperion_controller.set_power(1)
  163. if effect_name:
  164. res = hyperion_controller.set_effect(effect_name)
  165. else:
  166. res = hyperion_controller.clear_priority()
  167. return res.get('connected', False)
  168. def effect_connected(hyperion_controller: HyperionController) -> bool:
  169. """Show connected effect - green flash"""
  170. # Turn on Hyperion first
  171. hyperion_controller.set_power(1)
  172. # Flash green twice with explicit 1 second durations
  173. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  174. time.sleep(1.2) # Wait for flash to complete
  175. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  176. time.sleep(1.2) # Wait for flash to complete
  177. effect_idle(hyperion_controller)
  178. return res.get('connected', False)
  179. def effect_playing(hyperion_controller: HyperionController, effect_name: str = None) -> bool:
  180. """Show playing effect - use configured effect or clear to show default"""
  181. # Turn on Hyperion first
  182. hyperion_controller.set_power(1)
  183. if effect_name:
  184. res = hyperion_controller.set_effect(effect_name)
  185. else:
  186. # Clear priority to show the user's configured effect/color
  187. res = hyperion_controller.clear_priority()
  188. return res.get('connected', False)