From 6d362d02f7f15237e8b8b838d04eb35fc4057f54 Mon Sep 17 00:00:00 2001 From: Nathaniel McAuliffe Date: Wed, 10 Jun 2026 21:01:32 -0400 Subject: [PATCH] feat: Add support for the AudioReactive usermod --- src/wled/__init__.py | 2 ++ src/wled/models.py | 21 +++++++++++++++++++++ src/wled/wled.py | 17 +++++++++++++++++ tests/test_models.py | 14 ++++++++++++++ tests/test_wled.py | 19 +++++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/src/wled/__init__.py b/src/wled/__init__.py index d9ff9387..486dbdaa 100644 --- a/src/wled/__init__.py +++ b/src/wled/__init__.py @@ -18,6 +18,7 @@ WLEDUpgradeError, ) from .models import ( + AudioReactive, Color, Device, Effect, @@ -39,6 +40,7 @@ __all__ = [ "WLED", + "AudioReactive", "Color", "Device", "Effect", diff --git a/src/wled/models.py b/src/wled/models.py index c068c7d8..dcfa9b56 100644 --- a/src/wled/models.py +++ b/src/wled/models.py @@ -149,6 +149,19 @@ class Nightlight(BaseModel): """Target brightness of nightlight feature.""" +@dataclass(kw_only=True) +class AudioReactive(BaseModel): + """Object holding the AudioReactive usermod state in WLED. + + The AudioReactive usermod reports its state in the `AudioReactive` + key of the state object. Note that the state is reported in the `on` + field, while changing the state is done using the `enabled` field. + """ + + on: bool = field(default=False) + """AudioReactive currently enabled.""" + + @dataclass(kw_only=True) class UDPSync(BaseModel): """Object holding UDP sync state in WLED. @@ -571,6 +584,14 @@ def __post_deserialize__(cls, obj: Info) -> Info: class State(BaseModel): """Object holding the state of WLED.""" + audio_reactive: AudioReactive | None = field( + default=None, metadata=field_options(alias="AudioReactive") + ) + """AudioReactive usermod state. + + `None` if the AudioReactive usermod is not installed on the device. + """ + brightness: int = field(default=1, metadata=field_options(alias="bri")) """Brightness of the light. diff --git a/src/wled/wled.py b/src/wled/wled.py index 65b88ef6..a6d15907 100644 --- a/src/wled/wled.py +++ b/src/wled/wled.py @@ -632,6 +632,23 @@ async def nightlight( nightlight = {k: v for k, v in nightlight.items() if v is not None} await self.request("/json/state", method="POST", data={"nl": nightlight}) + async def audio_reactive(self, *, on: bool) -> None: + """Control the AudioReactive usermod of a WLED device. + + Args: + ---- + on: A boolean, true to enable the AudioReactive usermod, + false otherwise. + + """ + # The AudioReactive usermod reports its state in the `on` field, + # but accepts state changes in the `enabled` field. + await self.request( + "/json/state", + method="POST", + data={"AudioReactive": {"enabled": on}}, + ) + async def upgrade( # noqa: PLR0912 self, *, diff --git a/tests/test_models.py b/tests/test_models.py index 4ec6397d..68cc7e74 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -393,6 +393,20 @@ def test_state_preset_id_positive() -> None: assert state.preset_id == 3 +def test_state_audio_reactive_absent() -> None: + """Test audio_reactive is None when the usermod is not installed.""" + state = State.from_dict(_base_state()) + assert state.audio_reactive is None + + +@pytest.mark.parametrize("on", [True, False]) +def test_state_audio_reactive_present(on: bool) -> None: + """Test audio_reactive state is deserialized when the usermod is present.""" + state = State.from_dict(_base_state(AudioReactive={"on": on})) + assert state.audio_reactive is not None + assert state.audio_reactive.on is on + + # ========================================================================= # Preset model # ========================================================================= diff --git a/tests/test_wled.py b/tests/test_wled.py index 1a4649fc..07e4f6fa 100644 --- a/tests/test_wled.py +++ b/tests/test_wled.py @@ -896,6 +896,25 @@ async def test_nightlight( ) +@pytest.mark.parametrize("on", [True, False]) +async def test_audio_reactive(responses: aioresponses, wled: WLED, on: bool) -> None: + """Test setting AudioReactive usermod state.""" + responses.post( + "http://example.com/json/state", + status=200, + body="{}", + content_type="application/json", + ) + + await wled.audio_reactive(on=on) + + assert_post_payload( + responses, + "http://example.com/json/state", + {"AudioReactive": {"enabled": on}, "v": True}, + ) + + # ========================================================================= # Section 14: WLED client - reset, close, context manager, connected # =========================================================================