diff --git a/docs/configuration.md b/docs/configuration.md index df3ac27..7b0cc86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1491,6 +1491,20 @@ gcode: state=True, preset=preset)} +[gcode_macro WLED_CONTROL] +description: Control effect values and brightness +gcode: + {% set strip = params.STRIP|default('lights')|string %} + {% set brightness = params.BRIGHTNESS|default(-1)|int %} + {% set intensity = params.INTENSITY|default(-1)|int %} + {% set speed = params.SPEED|default(-1)|int %} + + {action_call_remote_method("set_wled_state", + strip=strip, + brightness=brightness, + intensity=intensity, + speed=speed)} + [gcode_macro WLED_OFF] description: Turn WLED strip off gcode: diff --git a/docs/web_api.md b/docs/web_api.md index 2bbd470..9086b97 100644 --- a/docs/web_api.md +++ b/docs/web_api.md @@ -3466,7 +3466,7 @@ JSON-RPC request: "jsonrpc": "2.0", "method": "machine.device_power.off", "params": { - "dev_one":null, + "dev_one": null, "dev_two": null }, "id": 4564 @@ -3479,6 +3479,340 @@ An object containing power state for each requested device: "printer": "off" } ``` +### WLED APIs +The APIs for WLED are available when the `[wled]` component has been configured. For lower-level control of wled consider using the WLED [JOSN API](https://kno.wled.ge/interfaces/json-api/) directly. + +#### Get strips +HTTP request: +```http +GET /machine/wled/strips +``` +JSON-RPC request: +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.strips", + "id": 7123 +} +``` +Returns: + +Strip information for all wled strips. +```json +{ + "result": { + "strips": { + "lights": { + "strip": "lights", + "status": "on", + "chain_count": 79, + "preset": -1, + "brightness": 255, + "intensity": -1, + "speed": -1, + "error": null + }, + "desk": { + "strip": "desk", + "status": "on", + "chain_count": 60, + "preset": 8, + "brightness": -1, + "intensity": -1, + "speed": -1, + "error": null + } + } + } +} +``` + +#### Get strip status +HTTP request: +```http +GET /machine/wled/status?strip1&strip2 +``` +JSON-RPC request: +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.status", + "params": { + "lights": null, + "desk": null + }, + "id": 7124 +} +``` +Returns: + +Strip information for requested strips. +```json +{ + "result": { + "lights": { + "strip": "lights", + "status": "on", + "chain_count": 79, + "preset": -1, + "brightness": 255, + "intensity": -1, + "speed": -1, + "error": null + }, + "desk": { + "strip": "desk", + "status": "on", + "chain_count": 60, + "preset": 8, + "brightness": -1, + "intensity": -1, + "speed": -1, + "error": null + } + } +} +``` + +#### Turn strip on +Turns the specified strips on to the initial colors or intial preset. + +HTTP request: +```http +POST /machine/wled/on?strip1&strip2 +``` +JSON-RPC request: +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.on", + "params": { + "lights": null, + "desk": null + }, + "id": 7125 +} +``` +Returns: + +Strip information for requested strips. +```json +{ + "result": { + "lights": { + "strip": "lights", + "status": "on", + "chain_count": 79, + "preset": -1, + "brightness": 255, + "intensity": -1, + "speed": -1, + "error": null + }, + "desk": { + "strip": "desk", + "status": "on", + "chain_count": 60, + "preset": 8, + "brightness": -1, + "intensity": -1, + "speed": -1, + "error": null + } + } +} +``` + +#### Turn strip off +Turns off all specified strips. + +HTTP request: +```http +POST /machine/wled/off?strip1&strip2 +``` +JSON-RPC request: +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.off", + "params": { + "lights": null, + "desk": null + }, + "id": 7126 +} +``` +Returns: + +The new state of the specified strips. +```json +{ + "result": { + "lights": { + "strip": "lights", + "status": "off", + "chain_count": 79, + "preset": -1, + "brightness": 255, + "intensity": -1, + "speed": -1, + "error": null + }, + "desk": { + "strip": "desk", + "status": "off", + "chain_count": 60, + "preset": 8, + "brightness": -1, + "intensity": -1, + "speed": -1, + "error": null + } + } +} +``` + +#### Toggle strip on/off state +Turns each strip off if it is on and on if it is off. + +HTTP request: +```http +POST /machine/wled/off?strip1&strip2 +``` +JSON-RPC request: +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.toggle", + "params": { + "lights": null, + "desk": null + }, + "id": 7127 +} +``` +Returns: + +The new state of the specified strips. +```json +{ + "result": { + "lights": { + "strip": "lights", + "status": "on", + "chain_count": 79, + "preset": -1, + "brightness": 255, + "intensity": -1, + "speed": -1, + "error": null + }, + "desk": { + "strip": "desk", + "status": "off", + "chain_count": 60, + "preset": 8, + "brightness": -1, + "intensity": -1, + "speed": -1, + "error": null + } + } +} +``` + +#### Control individual strip state +Toggle, turn on, turn off, turn on with preset, turn on with brightness, or +turn on preset will some of brightness, intensity, and speed. Or simply set +some of brightness, intensity, and speed. + +HTTP requests: + +Turn strip `lights` off +```http +POST /machine/wled/strip?strip=lights&action=off +``` + +Turn strip `lights` on to the initial colors or intial preset. +```http +POST /machine/wled/strip?strip=lights&action=on +``` + +Turn strip `lights` on activating preset 3. +```http +POST /machine/wled/strip?strip=lights&action=on&preset=3 +``` + +Turn strip `lights` on activating preset 3 while specifying speed and +intensity. +```http +POST /machine/wled/strip?strip=lights&action=on&preset=3&intensity=50&speed=255 +``` + +Change strip `lights` brightness (if on) and speed (if a preset is active). +```http +POST /machine/wled/strip?strip=lights&action=control&brightness=99&speed=50 +``` + +JSON-RPC request: + +Returns information for the specified strip. +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.get_strip", + "params": { + "strip": "lights", + }, + "id": 7128 +} +``` + +Calls the action with the arguments for the specified strip. +```json +{ + "jsonrpc": "2.0", + "method":"machine.wled.post_strip", + "params": { + "strip": "lights", + "action": "on", + "preset": 1, + "brightness": 255, + "intensity": 255, + "speed": 255 + }, + "id": 7129 +} +``` +!!! note + The `action` argument may be `on`, `off`, `toggle` or `control`. Any + other value will result in an error. + +The `intensity` and `speed` arguments are only used if a preset is active. +Permitted ranges are 1-255 for `brightness` and 0-255 for `intensity` and +`speed`. When action is `on` a `preset` with some or all of `brightness`, +`intensity` and `speed` may also be specified. If the action `control` is used +one or all of `brightness`, `intensity`, and `speed` must be specified. + +Returns: + +State of the strip. +```json +{ + "result": { + "lights": { + "strip": "lights", + "status": "on", + "chain_count": 79, + "preset": 1, + "brightness": 50, + "intensity": 255, + "speed": 255, + "error": null + } + } +} +``` ### OctoPrint API emulation Partial support of OctoPrint API is implemented with the purpose of diff --git a/moonraker/components/wled.py b/moonraker/components/wled.py index a4f4d8c..b3d6baa 100644 --- a/moonraker/components/wled.py +++ b/moonraker/components/wled.py @@ -65,9 +65,12 @@ class Strip(): def get_strip_info(self: Strip) -> Dict[str, Any]: return { "strip": self.name, - "status": self.onoff, + "status": self.onoff.value, "chain_count": self.chain_count, "preset": self.preset, + "brightness": self.brightness, + "intensity": self.intensity, + "speed": self.speed, "error": self.error_state } @@ -75,6 +78,9 @@ class Strip(): self.send_full_chain_data = True self.onoff = OnOff.on self.preset = self.initial_preset + self.brightness = 255 + self.intensity = -1 + self.speed = -1 if self.initial_preset >= 0: self._update_color_data(self.initial_red, self.initial_green, @@ -133,6 +139,10 @@ class Strip(): else: self.send_full_chain_data = True self.preset = preset + # Without reading the data back from wled we don't know the values + self.brightness = -1 + self.intensity = -1 + self.speed = -1 await self._send_wled_command({"on": True, "ps": preset}) async def wled_off(self: Strip) -> None: @@ -143,6 +153,60 @@ class Strip(): self.send_full_chain_data = True await self._send_wled_command({"on": False}) + async def wled_control(self: Strip, brightness: int, intensity: int, + speed: int) -> None: + logging.debug( + f"WLED: {self.name} control {self.onoff} BRIGHTNESS={brightness} " + f"INTENSITY={intensity} SPEED={speed} CURRENTPRESET={self.preset}") + + if self.onoff == OnOff.off: + logging.info("wled control only permitted when strip is on") + return + + # Even if a preset is not activated sending seg {} information will + # turn it back on + control: Dict[str, Any] + if self.preset != -1: + control = {"tt": 0, "seg": {}} + else: + control = {"tt": 0} + + shouldSend: bool = False + # Using 0 is not recommended in wled docs + if brightness > 0: + if brightness > 255: + logging.info("BRIGHTNESS should be between 1 and 255") + else: + shouldSend = True + self.brightness = brightness + control["bri"] = self.brightness + # Brightness in seg {} - only if a preset is on + if self.preset != -1: + control["seg"]["bri"] = self.brightness + + # Intensity - only if a preset is on + if intensity > -1 and self.preset != -1: + if intensity > 255: + logging.info("INTENSITY should be between 0 and 255") + else: + shouldSend = True + self.intensity = intensity + control["seg"]["ix"] = self.intensity + + # Speed - only if a preset is on + if speed > -1 and self.preset != -1: + if speed > 255: + logging.info("SPEED should be between 0 and 255") + else: + shouldSend = True + self.speed = speed + control["seg"]["sx"] = self.speed + + # Control brightness, intensity, and speed for segment + # This will allow full control for effects such as "Percent" + if shouldSend: + await self._send_wled_command(control) + def _wled_pixel(self: Strip, index: int) -> List[int]: led_color_data: List[int] = [] for p in self._chain_data[(index-1)*self._COLORSIZE: @@ -158,13 +222,21 @@ class Strip(): f"INDEX={index} TRANSMIT={transmit}") self._update_color_data(red, green, blue, white, index) if transmit: + # Clear preset (issues with sending seg{} will revert to preset) + self.preset = -1 + + # If we are coming from a preset without a wled_control + # we don't know a brightness, this will also ensure + # behaviour is consistent prior to introduction of wled_control + if self.brightness == -1: + self.brightness = 255 # Base command for setting an led (for all active segments) # See https://kno.wled.ge/interfaces/json-api/ state: Dict[str, Any] = {"on": True, "tt": 0, - "bri": 255, - "seg": {"bri": 255, "i": []}} + "bri": self.brightness, + "seg": {"bri": self.brightness, "i": []}} if index is None: # All pixels same color only send range command of first color self.send_full_chain_data = False @@ -329,6 +401,9 @@ class WLED: self.server.register_endpoint( "/machine/wled/off", ["POST"], self._handle_batch_wled_request) + self.server.register_endpoint( + "/machine/wled/toggle", ["POST"], + self._handle_batch_wled_request) self.server.register_endpoint( "/machine/wled/strip", ["GET", "POST"], self._handle_single_wled_request) @@ -372,8 +447,9 @@ class WLED: # Full control of wled # state: True, False, "on", "off" # preset: wled preset (int) to use (ignored if state False or "Off") - async def set_wled_state(self: WLED, strip: str, state: str, - preset: int = -1) -> None: + async def set_wled_state(self: WLED, strip: str, state: str = None, + preset: int = -1, brightness: int = -1, + intensity: int = -1, speed: int = -1) -> None: status = None if isinstance(state, bool): @@ -383,21 +459,28 @@ class WLED: if status in ["true", "false"]: status = OnOff.on if status == "true" else OnOff.off - if status is None and preset == -1: + if status is None and preset == -1 and brightness == -1 and \ + intensity == -1 and speed == -1: logging.info( - f"Invalid state received but no preset passed: {state}") + f"Invalid state received but no control or preset data passed") return if strip not in self.strips: logging.info(f"Unknown WLED strip: {strip}") return + # All other arguments are ignored if status == OnOff.off: - # All other arguments are ignored await self.strips[strip].wled_off() - else: + + # Turn on if on or a preset is specified + if status == OnOff.on or preset != -1: await self.strips[strip].wled_on(preset) + # Control + if brightness != -1 or intensity != -1 or speed != -1: + await self.strips[strip].wled_control(brightness, intensity, speed) + # Individual pixel control, for compatibility with SET_LED async def set_wled(self: WLED, strip: str, @@ -429,6 +512,9 @@ class WLED: ) -> Dict[str, Any]: strip_name: str = web_request.get_str('strip') preset: int = web_request.get_int('preset', -1) + brightness: int = web_request.get_int('brightness', -1) + intensity: int = web_request.get_int('intensity', -1) + speed: int = web_request.get_int('speed', -1) req_action = web_request.get_action() if strip_name not in self.strips: @@ -438,10 +524,11 @@ class WLED: return {strip_name: strip.get_strip_info()} elif req_action == "POST": action = web_request.get_str('action').lower() - if action not in ["on", "off", "toggle"]: + if action not in ["on", "off", "toggle", "control"]: raise self.server.error( f"Invalid requested action '{action}'") - result = await self._process_request(strip, action, preset) + result = await self._process_request(strip, action, preset, + brightness, intensity, speed) return {strip_name: result} async def _handle_batch_wled_request(self: WLED, @@ -456,7 +543,8 @@ class WLED: req = ep.split("/")[-1] for name, strip in requested_strips.items(): if strip is not None: - result[name] = await self._process_request(strip, req, -1) + result[name] = await self._process_request(strip, req, -1, + -1, -1, -1) else: result[name] = {"error": "strip_not_found"} return result @@ -464,7 +552,10 @@ class WLED: async def _process_request(self: WLED, strip: Strip, req: str, - preset: int + preset: int, + brightness: int, + intensity: int, + speed: int ) -> Dict[str, Any]: strip_onoff = strip.onoff @@ -472,15 +563,21 @@ class WLED: return strip.get_strip_info() if req == "toggle": req = "on" if strip_onoff == OnOff.off else "off" - if req in ["on", "off"]: + + if req in ["on", "off", "control"]: # Always do something, could be turning off colors, or changing # preset, easier not to have to worry - if req == "on": - strip_onoff = OnOff.on - await strip.wled_on(preset) + if req == "on" or req == "control": + if req == "on": + strip_onoff = OnOff.on + await strip.wled_on(preset) + + if brightness != -1 or intensity != -1 or speed != -1: + await strip.wled_control(brightness, intensity, speed) else: strip_onoff = OnOff.off await strip.wled_off() + return strip.get_strip_info() raise self.server.error(f"Unsupported wled request: {req}")