Browse Source

Add support for Rgbic Neon Rope Light (#452)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Samuel Xiao 1 day ago
parent
commit
5f68e99a51

+ 2 - 0
switchbot/__init__.py

@@ -51,6 +51,7 @@ from .devices.light_strip import (
     SwitchbotCandleWarmerLamp,
     SwitchbotLightStrip,
     SwitchbotRgbicLight,
+    SwitchbotRgbicNeonLight,
     SwitchbotStripLight3,
 )
 from .devices.lock import SwitchbotLock
@@ -115,6 +116,7 @@ __all__ = [
     "SwitchbotRelaySwitch",
     "SwitchbotRelaySwitch2PM",
     "SwitchbotRgbicLight",
+    "SwitchbotRgbicNeonLight",
     "SwitchbotRollerShade",
     "SwitchbotSmartThermostatRadiator",
     "SwitchbotStripLight3",

+ 24 - 0
switchbot/adv_parser.py

@@ -677,6 +677,30 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "func": process_rgbic_light,
         "manufacturer_id": 2409,
     },
+    b"\x00\x10\xd0\xb5": {
+        "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
+        "modelFriendlyName": "RGBIC Neon Wire Rope Light",
+        "func": process_wostrip,
+        "manufacturer_id": 2409,
+    },
+    b"\x00\x10\xd0\xb6": {
+        "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+        "modelFriendlyName": "RGBIC Neon Rope Light",
+        "func": process_wostrip,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x10\xd0\xb5": {
+        "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
+        "modelFriendlyName": "RGBIC Neon Wire Rope Light",
+        "func": process_wostrip,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x10\xd0\xb6": {
+        "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+        "modelFriendlyName": "RGBIC Neon Rope Light",
+        "func": process_wostrip,
+        "manufacturer_id": 2409,
+    },
     b"\x00\x10\xfb\xa8": {
         "modelName": SwitchbotModel.K11_VACUUM,
         "modelFriendlyName": "K11+ Vacuum",

+ 2 - 0
switchbot/const/__init__.py

@@ -100,6 +100,8 @@ class SwitchbotModel(StrEnum):
     PLUG_MINI_EU = "Plug Mini (EU)"
     RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
     RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
+    RGBIC_NEON_ROPE_LIGHT = "RGBIC Neon Rope Light"
+    RGBIC_NEON_WIRE_ROPE_LIGHT = "RGBIC Neon Wire Rope Light"
     K11_VACUUM = "K11+ Vacuum"
     CLIMATE_PANEL = "Climate Panel"
     SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator"

+ 17 - 0
switchbot/devices/light_strip.py

@@ -328,3 +328,20 @@ class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
         """Return the current color mode."""
         device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
         return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
+
+
+class SwitchbotRgbicNeonLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
+    """Support for Switchbot RGBIC Neon lights."""
+
+    _model = SwitchbotModel.RGBIC_NEON_ROPE_LIGHT
+    _effect_dict = RGBIC_EFFECTS
+
+    @property
+    def color_modes(self) -> set[ColorMode]:
+        """Return the supported color modes."""
+        return {ColorMode.RGB}
+
+    @property
+    def color_mode(self) -> ColorMode:
+        """Return the current color mode."""
+        return ColorMode.RGB

+ 16 - 0
tests/__init__.py

@@ -96,6 +96,22 @@ RGBICWW_FLOOR_LAMP_INFO = AdvTestCase(
     SwitchbotModel.RGBICWW_FLOOR_LAMP,
 )
 
+RGBIC_NEON_LIGHT_INFO = AdvTestCase(
+    b'\xdc\x06u\xa6\xfb\xb2y\x9e"\x00\x11\xb8\x00',
+    b"\x00\x00\x00\x00\x10\xd0\xb6",
+    {
+        "sequence_number": 121,
+        "isOn": True,
+        "brightness": 30,
+        "delay": False,
+        "network_state": 2,
+        "color_mode": 2,
+    },
+    b"\x00\x10\xd0\xb6",
+    "Rgbic Neon Rope Light",
+    SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+)
+
 
 SMART_THERMOSTAT_RADIATOR_INFO = AdvTestCase(
     b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00",

+ 68 - 0
tests/test_adv_parser.py

@@ -3591,6 +3591,36 @@ def test_humidifer_with_empty_data() -> None:
             "RGBICWW Strip Light",
             SwitchbotModel.RGBICWW_STRIP_LIGHT,
         ),
+        AdvTestCase(
+            b"@L\xca!pz/\x8b'\x00\x11:\x00",
+            b"\x00\x00\x00\x00\x10\xd0\xb6",
+            {
+                "sequence_number": 47,
+                "isOn": True,
+                "brightness": 11,
+                "delay": False,
+                "network_state": 2,
+                "color_mode": 7,
+            },
+            b"\x00\x10\xd0\xb6",
+            "RGBIC Neon Rope Light",
+            SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+        ),
+        AdvTestCase(
+            b"@L\xca!pz/\x8b'\x00\x11:\x00",
+            b"\x00\x00\x00\x00\x10\xd0\xb5",
+            {
+                "sequence_number": 47,
+                "isOn": True,
+                "brightness": 11,
+                "delay": False,
+                "network_state": 2,
+                "color_mode": 7,
+            },
+            b"\x00\x10\xd0\xb5",
+            "RGBIC Neon Wire Rope Light",
+            SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
+        ),
         AdvTestCase(
             b"\xb0\xe9\xfe\xe4\xbf\xd8\x0b\x01\x11f\x00\x16M\x15",
             b"\x00\x00M\x00\x10\xfb\xa8",
@@ -3979,6 +4009,36 @@ def test_adv_active(test_case: AdvTestCase) -> None:
             "RGBICWW Strip Light",
             SwitchbotModel.RGBICWW_STRIP_LIGHT,
         ),
+        AdvTestCase(
+            b"@L\xca!pz/\x8b'\x00\x11:\x00",
+            None,
+            {
+                "sequence_number": 47,
+                "isOn": True,
+                "brightness": 11,
+                "delay": False,
+                "network_state": 2,
+                "color_mode": 7,
+            },
+            b"\x00\x10\xd0\xb6",
+            "RGBIC Neon Rope Light",
+            SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+        ),
+        AdvTestCase(
+            b"@L\xca!pz/\x8b'\x00\x11:\x00",
+            None,
+            {
+                "sequence_number": 47,
+                "isOn": True,
+                "brightness": 11,
+                "delay": False,
+                "network_state": 2,
+                "color_mode": 7,
+            },
+            b"\x00\x10\xd0\xb5",
+            "RGBIC Neon Wire Rope Light",
+            SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
+        ),
         AdvTestCase(
             b"\xb0\xe9\xfe\xe4\xbf\xd8\x0b\x01\x11f\x00\x16M\x15",
             None,
@@ -4342,6 +4402,14 @@ def test_adv_passive(test_case: AdvTestCase) -> None:
             "Keypad Vision",
             SwitchbotModel.KEYPAD_VISION,
         ),
+        AdvTestCase(
+            None,
+            b"\x00\x00\x00\x00\x10\xd0\xb6",
+            {},
+            b"\x00\x10\xd0\xb6",
+            "RGBIC Neon Rope Light",
+            SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
+        ),
         AdvTestCase(
             None,
             b"\x00\x00`\x01\x11Q\x98",

+ 17 - 6
tests/test_strip_light.py

@@ -12,6 +12,7 @@ from switchbot.devices.device import SwitchbotOperationError
 from . import (
     CANDLE_WARMER_LAMP_INFO,
     FLOOR_LAMP_INFO,
+    RGBIC_NEON_LIGHT_INFO,
     RGBICWW_FLOOR_LAMP_INFO,
     RGBICWW_STRIP_LIGHT_INFO,
     STRIP_LIGHT_3_INFO,
@@ -24,13 +25,16 @@ ALL_LIGHT_CASES = [
     (CANDLE_WARMER_LAMP_INFO, light_strip.SwitchbotCandleWarmerLamp),
     (RGBICWW_STRIP_LIGHT_INFO, light_strip.SwitchbotRgbicLight),
     (RGBICWW_FLOOR_LAMP_INFO, light_strip.SwitchbotRgbicLight),
+    (RGBIC_NEON_LIGHT_INFO, light_strip.SwitchbotRgbicNeonLight),
 ]
 
-# RGB/effect-capable devices only; excludes brightness-only lights like CWL.
+# RGB + color-temp devices. Excludes brightness-only lights (CWL) and
+# RGB-only lights (RGBIC Neon) whose color_modes differ from the rest.
 RGB_LIGHT_CASES = [
     case
     for case in ALL_LIGHT_CASES
-    if case[1] is not light_strip.SwitchbotCandleWarmerLamp
+    if case[1]
+    not in (light_strip.SwitchbotCandleWarmerLamp, light_strip.SwitchbotRgbicNeonLight)
 ]
 
 
@@ -105,10 +109,7 @@ async def test_default_info(device_case, expected_effects):
     assert device.is_on() is True
     assert device.on is True
     assert device.color_mode == ColorMode.RGB
-    assert device.color_modes == {
-        ColorMode.RGB,
-        ColorMode.COLOR_TEMP,
-    }
+    assert device.color_modes == {ColorMode.RGB, ColorMode.COLOR_TEMP}
     assert device.rgb == (30, 0, 0)
     assert device.color_temp == 3200
     assert device.brightness == adv_info.data["brightness"]
@@ -123,6 +124,15 @@ async def test_default_info(device_case, expected_effects):
         assert effect in effect_list
 
 
+@pytest.mark.asyncio
+async def test_rgbic_neon_light_info() -> None:
+    """Test color_mode / color_modes on SwitchbotRgbicNeonLight (RGB only)."""
+    adv_info, dev_cls = RGBIC_NEON_LIGHT_INFO, light_strip.SwitchbotRgbicNeonLight
+    device = create_device_for_command_testing(adv_info, dev_cls)
+    assert device.color_mode == ColorMode.RGB
+    assert device.color_modes == {ColorMode.RGB}
+
+
 @pytest.mark.asyncio
 async def test_candle_warmer_lamp_info() -> None:
     """Test default initialization of the candle warmer lamp."""
@@ -356,6 +366,7 @@ async def test_set_effect_normalizes_case(device_case):
     [
         (light_strip.SwitchbotStripLight3, SwitchbotModel.STRIP_LIGHT_3),
         (light_strip.SwitchbotRgbicLight, SwitchbotModel.RGBICWW_STRIP_LIGHT),
+        (light_strip.SwitchbotRgbicNeonLight, SwitchbotModel.RGBIC_NEON_ROPE_LIGHT),
         (light_strip.SwitchbotCandleWarmerLamp, SwitchbotModel.CANDLE_WARMER_LAMP),
     ],
 )