Repository: sbidy/pywizlight

Issue:
Add Support for remotes
Buttons

ON: `"src":"wfa1"`
OFF: `"src":"wfa2"`
NIGHT: `"wfa3"`
1: `"wfa16"`
2: `"wfa17"`
3: `"src":"wfa18"`
4: `"wfa19"`
BRIGHT - `"src":"wfa8"`
BRIGHT + `"src":"wfa9"`

Discussion:

Patch:
diff --git a/pywizlight/bulb.py b/pywizlight/bulb.py
index e4efccb..3a04fa1 100755
--- a/pywizlight/bulb.py
+++ b/pywizlight/bulb.py
@@ -47,13 +47,29 @@ RGBWW_COLORS = set(RGBWW_ORDER)
 _IGNORE_STATE_KEYS = {"mqttCd", "ts", "rssi"}
 PIR_SOURCE = "pir"
 
+WIZMOTE_BUTTON_MAP = {
+    "wfa1": "on",
+    "wfa2": "off",
+    "wfa3": "night",
+    "wfa8": "decrease_brightness",
+    "wfa9": "increase_brightness",
+    "wfa16": "1",
+    "wfa17": "2",
+    "wfa18": "3",
+    "wfa19": "4",
+}
+
+ALWAYS_SEND_SRCS = set([PIR_SOURCE, *WIZMOTE_BUTTON_MAP])
+
 
 def states_match(old: Dict[str, Any], new: Dict[str, Any]) -> bool:
     """Check if states match except for keys we do not want to callback on."""
     old_src = old.get("src")
     new_src = new.get("src")
-    # Always send the update when there is a PIR change.
-    if new_src != old_src and PIR_SOURCE in (old_src, new_src):
+    # Always send the update when there is a PIR change or button press.
+    if new_src != old_src and (
+        new_src in ALWAYS_SEND_SRCS or old_src in ALWAYS_SEND_SRCS
+    ):
         return False
     for key, val in new.items():
         if key != "src" and old.get(key) != val and key not in _IGNORE_STATE_KEYS:


Test patch:
diff --git a/pywizlight/tests/test_bulb.py b/pywizlight/tests/test_bulb.py
index a12878e..28b33fb 100644
--- a/pywizlight/tests/test_bulb.py
+++ b/pywizlight/tests/test_bulb.py
@@ -276,7 +276,7 @@ async def test_timeout_PilotBuilder(bad_bulb: wizlight) -> None:
 
 
 @pytest.mark.asyncio
-async def test_states_match() -> None:
+async def test_states_match_with_occupancy() -> None:
     """Test states match always sends pir updates but we ignore mqttCd, rssi, and ts."""
     state_off_ios = {
         "mac": "a8bb50d46a1c",
@@ -354,3 +354,48 @@ async def test_states_match() -> None:
     assert not states_match(
         ios_off, occupancy_not_detected
     )  # source change matters since its a PIR
+
+
+@pytest.mark.asyncio
+async def test_states_match_with_button() -> None:
+    """Test states match always sends button updates but we ignore mqttCd, rssi, and ts."""
+
+    button_on_press = {
+        "mac": "a8bb50d46a1c",
+        "rssi": -48,
+        "src": "wfa1",
+        "state": True,
+    }
+    button_off_press = {
+        "mac": "a8bb50d46a1c",
+        "rssi": -69,
+        "src": "wfa2",
+        "state": False,
+        "sceneId": 0,
+    }
+    button_1_press_state_false = {
+        "mac": "a8bb50d46a1c",
+        "rssi": -69,
+        "src": "wfa16",
+        "state": False,
+        "sceneId": 0,
+    }
+    button_1_press_state_true = {
+        "mac": "a8bb50d46a1c",
+        "rssi": -69,
+        "src": "wfa16",
+        "state": True,
+        "sceneId": 0,
+    }
+    ios_off = {
+        "mac": "a8bb50d46a1c",
+        "rssi": -69,
+        "src": "ios",
+        "state": False,
+        "sceneId": 0,
+    }
+    assert not states_match(button_on_press, button_off_press)
+    assert not states_match(button_1_press_state_false, button_1_press_state_true)
+    assert not states_match(
+        ios_off, button_1_press_state_false
+    )  # source change matters since its a button