hyperion_controller.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. # Turn on Hyperion first
  101. self.set_power(1)
  102. # Clear priority before setting new color
  103. self.clear_priority()
  104. result = self._send_command(
  105. "color",
  106. priority=self.priority,
  107. color=[r, g, b],
  108. duration=duration
  109. )
  110. return result
  111. def set_effect(self, effect_name: str, args: Optional[Dict] = None, duration: int = 86400000) -> Dict:
  112. """
  113. Set Hyperion effect
  114. Args:
  115. effect_name: Name of the effect (e.g., 'Rainbow swirl', 'Warm mood blobs')
  116. args: Optional effect arguments
  117. duration: Duration in milliseconds (default = 86400000ms = 24 hours)
  118. """
  119. # Turn on Hyperion first
  120. self.set_power(1)
  121. # Clear priority before setting new effect
  122. self.clear_priority()
  123. params = {
  124. "priority": self.priority,
  125. "effect": {"name": effect_name},
  126. "duration": duration
  127. }
  128. if args:
  129. params["effect"]["args"] = args
  130. result = self._send_command("effect", **params)
  131. return result
  132. def clear_priority(self, priority: Optional[int] = None) -> Dict:
  133. """
  134. Clear a specific priority or Dune Weaver's priority
  135. Args:
  136. priority: Priority to clear (defaults to self.priority)
  137. """
  138. if priority is None:
  139. priority = self.priority
  140. result = self._send_command("clear", priority=priority)
  141. return result
  142. def clear_all(self) -> Dict:
  143. """Clear all priorities (return to default state)"""
  144. result = self._send_command("clear", priority=-1)
  145. return result
  146. def set_brightness(self, value: int) -> Dict:
  147. """
  148. Set Hyperion brightness
  149. Args:
  150. value: Brightness (0-100)
  151. """
  152. if not 0 <= value <= 100:
  153. return {"connected": False, "message": "Brightness must be between 0 and 100"}
  154. result = self._send_command(
  155. "adjustment",
  156. adjustment={
  157. "brightness": value
  158. }
  159. )
  160. return result
  161. def effect_loading(hyperion_controller: HyperionController) -> bool:
  162. """Show loading effect - Atomic swirl effect"""
  163. # Turn on Hyperion first
  164. hyperion_controller.set_power(1)
  165. time.sleep(0.2) # Give Hyperion time to power on
  166. # Clear priority before setting new effect
  167. hyperion_controller.clear_priority()
  168. time.sleep(0.1) # Give Hyperion time to clear
  169. res = hyperion_controller.set_effect("Atomic swirl")
  170. return res.get('connected', False)
  171. def effect_idle(hyperion_controller: HyperionController, effect_name: str = "off") -> bool:
  172. """Show idle effect - use configured effect or clear priority to return to default
  173. Args:
  174. effect_name: Effect name to show, "off" to clear priority (default), or None for off
  175. """
  176. # Turn on Hyperion first
  177. hyperion_controller.set_power(1)
  178. time.sleep(0.2) # Give Hyperion time to power on
  179. # Clear priority before setting new effect
  180. hyperion_controller.clear_priority()
  181. if effect_name and effect_name != "off":
  182. time.sleep(0.1) # Give Hyperion time to clear
  183. res = hyperion_controller.set_effect(effect_name)
  184. else:
  185. # "off" or None - already cleared above, return to default state
  186. res = {"connected": True}
  187. return res.get('connected', False)
  188. def effect_connected(hyperion_controller: HyperionController) -> bool:
  189. """Show connected effect - green flash
  190. Note: This function only shows the connection flash. The calling code
  191. should explicitly set the idle effect afterwards to ensure the user's
  192. configured idle effect is used.
  193. """
  194. # Turn on Hyperion first
  195. hyperion_controller.set_power(1)
  196. time.sleep(0.2) # Give Hyperion time to power on
  197. # Clear priority before setting new effect
  198. hyperion_controller.clear_priority()
  199. time.sleep(0.1) # Give Hyperion time to clear
  200. # Flash green twice with explicit 1 second durations
  201. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  202. time.sleep(1.2) # Wait for flash to complete
  203. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  204. time.sleep(1.2) # Wait for flash to complete
  205. # Don't call effect_idle here - let the caller set the configured idle effect
  206. return res.get('connected', False)
  207. def effect_playing(hyperion_controller: HyperionController, effect_name: str = "off") -> bool:
  208. """Show playing effect - use configured effect or clear to show default
  209. Args:
  210. effect_name: Effect name to show, "off" to clear priority (default), or None for off
  211. """
  212. # Turn on Hyperion first
  213. hyperion_controller.set_power(1)
  214. time.sleep(0.2) # Give Hyperion time to power on
  215. # Clear priority before setting new effect
  216. hyperion_controller.clear_priority()
  217. if effect_name and effect_name != "off":
  218. time.sleep(0.1) # Give Hyperion time to clear
  219. res = hyperion_controller.set_effect(effect_name)
  220. else:
  221. # "off" or None - already cleared above, show user's configured effect/color
  222. res = {"connected": True}
  223. return res.get('connected', False)