led_controller.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 LEDController:
  8. def __init__(self, ip_address: Optional[str] = None):
  9. self.ip_address = ip_address
  10. def _get_base_url(self) -> str:
  11. """Get base URL for WLED JSON API"""
  12. if not self.ip_address:
  13. raise ValueError("No WLED IP configured")
  14. return f"http://{self.ip_address}/json"
  15. def set_ip(self, ip_address: str) -> None:
  16. """Update the WLED IP address"""
  17. self.ip_address = ip_address
  18. def _send_command(self, state_params: Dict = None) -> Dict:
  19. """Send command to WLED and return status"""
  20. try:
  21. url = self._get_base_url()
  22. # First check current state
  23. response = requests.get(f"{url}/state", timeout=2)
  24. response.raise_for_status()
  25. current_state = response.json()
  26. preset_id = current_state.get('ps', -1)
  27. # If WLED is off and we're trying to set something, turn it on first
  28. if not current_state.get('on', False) and state_params and 'on' not in state_params:
  29. # Turn on power first
  30. requests.post(f"{url}/state", json={"on": True}, timeout=2)
  31. # Now send the actual command if there are parameters
  32. if state_params:
  33. response = requests.post(f"{url}/state", json=state_params, timeout=2)
  34. response.raise_for_status()
  35. # Only update current_state if we got a non-empty response
  36. if response.text:
  37. current_state = response.json()
  38. # Use True as default since WLED is typically on when responding
  39. is_on = current_state.get('on', True)
  40. return {
  41. "connected": True,
  42. "is_on": is_on,
  43. "preset_id": preset_id,
  44. "brightness": current_state.get('bri', 0),
  45. "message": "WLED is ON" if is_on else "WLED is OFF"
  46. }
  47. except ValueError as e:
  48. return {"connected": False, "message": str(e)}
  49. except requests.RequestException as e:
  50. return {"connected": False, "message": f"Cannot connect to WLED: {str(e)}"}
  51. except json.JSONDecodeError as e:
  52. return {"connected": False, "message": f"Error parsing WLED response: {str(e)}"}
  53. def check_wled_status(self) -> Dict:
  54. """Check WLED connection status and brightness"""
  55. return self._send_command()
  56. def set_brightness(self, value: int) -> Dict:
  57. """Set WLED brightness (0-255)"""
  58. if not 0 <= value <= 255:
  59. return {"connected": False, "message": "Brightness must be between 0 and 255"}
  60. return self._send_command({"bri": value})
  61. def set_power(self, state: int) -> Dict:
  62. """Set WLED power state (0=Off, 1=On, 2=Toggle)"""
  63. if state not in [0, 1, 2]:
  64. return {"connected": False, "message": "Power state must be 0 (Off), 1 (On), or 2 (Toggle)"}
  65. if state == 2:
  66. return self._send_command({"on": "t"}) # Toggle
  67. return self._send_command({"on": bool(state)})
  68. def _hex_to_rgb(self, hex_color: str) -> tuple:
  69. """Convert hex color string to RGB tuple"""
  70. hex_color = hex_color.lstrip('#')
  71. if len(hex_color) != 6:
  72. raise ValueError("Hex color must be 6 characters long (without #)")
  73. return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
  74. def set_color(self, r: int = None, g: int = None, b: int = None, w: int = None, hex: str = None) -> Dict:
  75. """Set WLED color using RGB(W) values or hex color code"""
  76. if hex is not None:
  77. try:
  78. r, g, b = self._hex_to_rgb(hex)
  79. except ValueError as e:
  80. return {"connected": False, "message": str(e)}
  81. # Prepare segment with color
  82. seg = {"col": [[r or 0, g or 0, b or 0]]}
  83. if w is not None:
  84. if not 0 <= w <= 255:
  85. return {"connected": False, "message": "White value must be between 0 and 255"}
  86. seg["col"][0].append(w)
  87. return self._send_command({"seg": [seg]})
  88. def set_effect(self, effect_index: int, speed: int = None, intensity: int = None,
  89. brightness: int = None, palette: int = None,
  90. # Primary color
  91. r: int = None, g: int = None, b: int = None, w: int = None, hex: str = None,
  92. # Secondary color
  93. r2: int = None, g2: int = None, b2: int = None, w2: int = None, hex2: str = None,
  94. # Transition
  95. transition: int = 0) -> Dict:
  96. """
  97. Set WLED effect with optional parameters
  98. Args:
  99. effect_index: Effect index (0-101)
  100. speed: Effect speed (0-255)
  101. intensity: Effect intensity (0-255)
  102. brightness: LED brightness (0-255)
  103. palette: FastLED palette index (0-46)
  104. r, g, b: Primary RGB color values (0-255)
  105. w: Primary White value for RGBW (0-255)
  106. hex: Primary hex color code (e.g., '#ff0000' or 'ff0000')
  107. r2, g2, b2: Secondary RGB color values (0-255)
  108. w2: Secondary White value for RGBW (0-255)
  109. hex2: Secondary hex color code
  110. transition: Duration of crossfade in 100ms units (e.g. 7 = 700ms). Default 0 for instant change.
  111. """
  112. try:
  113. effect_index = int(effect_index)
  114. except (ValueError, TypeError):
  115. return {"connected": False, "message": "Effect index must be a valid integer between 0 and 101"}
  116. if not 0 <= effect_index <= 101:
  117. return {"connected": False, "message": "Effect index must be between 0 and 101"}
  118. # Convert primary hex to RGB if provided
  119. if hex is not None:
  120. try:
  121. r, g, b = self._hex_to_rgb(hex)
  122. except ValueError as e:
  123. return {"connected": False, "message": f"Primary color: {str(e)}"}
  124. # Convert secondary hex to RGB if provided
  125. if hex2 is not None:
  126. try:
  127. r2, g2, b2 = self._hex_to_rgb(hex2)
  128. except ValueError as e:
  129. return {"connected": False, "message": f"Secondary color: {str(e)}"}
  130. # Build segment parameters
  131. seg = {"fx": effect_index}
  132. if speed is not None:
  133. if not 0 <= speed <= 255:
  134. return {"connected": False, "message": "Speed must be between 0 and 255"}
  135. seg["sx"] = speed
  136. if intensity is not None:
  137. if not 0 <= intensity <= 255:
  138. return {"connected": False, "message": "Intensity must be between 0 and 255"}
  139. seg["ix"] = intensity
  140. # Prepare colors array
  141. colors = []
  142. # Add primary color
  143. primary = [r or 0, g or 0, b or 0]
  144. if w is not None:
  145. if not 0 <= w <= 255:
  146. return {"connected": False, "message": "Primary white value must be between 0 and 255"}
  147. primary.append(w)
  148. colors.append(primary)
  149. # Add secondary color if any secondary color parameter is provided
  150. if any(x is not None for x in [r2, g2, b2, w2, hex2]):
  151. secondary = [r2 or 0, g2 or 0, b2 or 0]
  152. if w2 is not None:
  153. if not 0 <= w2 <= 255:
  154. return {"connected": False, "message": "Secondary white value must be between 0 and 255"}
  155. secondary.append(w2)
  156. colors.append(secondary)
  157. if colors:
  158. seg["col"] = colors
  159. if palette is not None:
  160. if not 0 <= palette <= 46:
  161. return {"connected": False, "message": "Palette index must be between 0 and 46"}
  162. seg["pal"] = palette
  163. # Combine with global parameters
  164. state = {"seg": [seg], "transition": transition}
  165. if brightness is not None:
  166. if not 0 <= brightness <= 255:
  167. return {"connected": False, "message": "Brightness must be between 0 and 255"}
  168. state["bri"] = brightness
  169. return self._send_command(state)
  170. def set_preset(self, preset_id: int) -> bool:
  171. preset_id = int(preset_id)
  172. # Send the command and get response
  173. response = self._send_command({"ps": preset_id})
  174. # if response.get('preset_id', -1) != preset_id:
  175. # return False
  176. # else:
  177. # return True
  178. def effect_loading(led_controller: LEDController):
  179. res = led_controller.set_effect(47, hex='#ffa000', hex2='#000000', speed=150, intensity=150)
  180. if res.get('is_on', False):
  181. return True
  182. else:
  183. return False
  184. def effect_idle(led_controller: LEDController):
  185. led_controller.set_preset(1)
  186. # res = led_controller.set_effect(0, hex='#ffe0a0', brightness=255)
  187. # if res.get('is_on', False):
  188. # return True
  189. # else:
  190. # return False
  191. def effect_connected(led_controller: LEDController):
  192. res = led_controller.set_effect(0, hex='#08ff00', brightness=100)
  193. time.sleep(1)
  194. led_controller.set_effect(0, brightness=0) # Turn off
  195. time.sleep(0.5)
  196. res = led_controller.set_effect(0, hex='#08ff00', brightness=100)
  197. time.sleep(1)
  198. effect_idle(led_controller)
  199. if res.get('is_on', False):
  200. return True
  201. else:
  202. return False
  203. def effect_playing(led_controller: LEDController):
  204. led_controller.set_preset(2)
  205. # res = led_controller.set_effect(9, speed=40, intensity=125, brightness=255)
  206. # if res.get('is_on', False):
  207. # return True
  208. # else:
  209. # return False