hyperion_controller.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. # Turn on Hyperion first
  191. hyperion_controller.set_power(1)
  192. time.sleep(0.2) # Give Hyperion time to power on
  193. # Clear priority before setting new effect
  194. hyperion_controller.clear_priority()
  195. time.sleep(0.1) # Give Hyperion time to clear
  196. # Flash green twice with explicit 1 second durations
  197. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  198. time.sleep(1.2) # Wait for flash to complete
  199. res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
  200. time.sleep(1.2) # Wait for flash to complete
  201. effect_idle(hyperion_controller)
  202. return res.get('connected', False)
  203. def effect_playing(hyperion_controller: HyperionController, effect_name: str = "off") -> bool:
  204. """Show playing effect - use configured effect or clear to show default
  205. Args:
  206. effect_name: Effect name to show, "off" to clear priority (default), or None for off
  207. """
  208. # Turn on Hyperion first
  209. hyperion_controller.set_power(1)
  210. time.sleep(0.2) # Give Hyperion time to power on
  211. # Clear priority before setting new effect
  212. hyperion_controller.clear_priority()
  213. if effect_name and effect_name != "off":
  214. time.sleep(0.1) # Give Hyperion time to clear
  215. res = hyperion_controller.set_effect(effect_name)
  216. else:
  217. # "off" or None - already cleared above, show user's configured effect/color
  218. res = {"connected": True}
  219. return res.get('connected', False)