Browse Source

Add support for the ceiling light (#96)

J. Nick Koston 1 year ago
parent
commit
0b992fc3d0

+ 2 - 0
switchbot/__init__.py

@@ -6,6 +6,7 @@ from .const import SwitchbotModel
 from .devices.base_light import SwitchbotBaseLight
 from .devices.bot import Switchbot
 from .devices.bulb import SwitchbotBulb
+from .devices.ceiling_light import SwitchbotCeilingLight
 from .devices.curtain import SwitchbotCurtain
 from .devices.device import ColorMode, SwitchbotDevice
 from .devices.light_strip import SwitchbotLightStrip
@@ -20,6 +21,7 @@ __all__ = [
     "ColorMode",
     "SwitchbotBaseLight",
     "SwitchbotBulb",
+    "SwitchbotCeilingLight",
     "SwitchbotDevice",
     "SwitchbotCurtain",
     "SwitchbotLightStrip",

+ 12 - 2
switchbot/adv_parser.py

@@ -1,12 +1,15 @@
 """Library to handle connection with Switchbot."""
 from __future__ import annotations
 
+import logging
 from collections.abc import Callable
 from typing import TypedDict
 
 from bleak.backends.device import BLEDevice
 from bleak.backends.scanner import AdvertisementData
 
+from switchbot.adv_parsers.ceiling_light import process_woceiling
+
 from .adv_parsers.bot import process_wohand
 from .adv_parsers.bulb import process_color_bulb
 from .adv_parsers.contact import process_wocontact
@@ -18,6 +21,8 @@ from .adv_parsers.plug import process_woplugmini
 from .const import SwitchbotModel
 from .models import SwitchBotAdvertisement
 
+_LOGGER = logging.getLogger(__name__)
+
 
 class SwitchbotSupportedType(TypedDict):
     """Supported type of Switchbot."""
@@ -73,6 +78,11 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
         "modelFriendlyName": "Color Bulb",
         "func": process_color_bulb,
     },
+    "q": {
+        "modelName": SwitchbotModel.CEILING_LIGHT,
+        "modelFriendlyName": "Ceiling Light",
+        "func": process_woceiling,
+    },
 }
 
 
@@ -95,14 +105,14 @@ def parse_advertisement_data(
         "address": device.address,  # MacOS uses UUIDs
         "rawAdvData": list(advertisement_data.service_data.values())[0],
         "data": {},
+        "model": _model,
+        "isEncrypted": bool(_service_data[0] & 0b10000000),
     }
 
     type_data = SUPPORTED_TYPES.get(_model)
     if type_data:
         data.update(
             {
-                "isEncrypted": bool(_service_data[0] & 0b10000000),
-                "model": _model,
                 "modelFriendlyName": type_data["modelFriendlyName"],
                 "modelName": type_data["modelName"],
                 "data": type_data["func"](_service_data, _mfr_data),

+ 24 - 0
switchbot/adv_parsers/ceiling_light.py

@@ -0,0 +1,24 @@
+"""Ceiling Light adv parser."""
+from __future__ import annotations
+
+import logging
+
+_LOGGER = logging.getLogger(__name__)
+
+# Off d94b2d012b3c4864106124
+# on  d94b2d012b3c4a641061a4
+# Off d94b2d012b3c4b64106124
+# on  d94b2d012b3c4d641061a4
+#     00112233445566778899AA
+
+
+def process_woceiling(data: bytes, mfr_data: bytes | None) -> dict[str, bool | int]:
+    """Process WoCeiling services data."""
+    assert mfr_data is not None
+    return {
+        "sequence_number": mfr_data[6],
+        "isOn": bool(mfr_data[10] & 0b10000000),
+        "brightness": mfr_data[7] & 0b01111111,
+        "cw": int(mfr_data[8:10].hex(), 16),
+        "color_mode": 1,
+    }

+ 1 - 0
switchbot/const.py

@@ -18,3 +18,4 @@ class SwitchbotModel(StrEnum):
     METER = "WoSensorTH"
     MOTION_SENSOR = "WoPresence"
     COLOR_BULB = "WoBulb"
+    CEILING_LIGHT = "WoCeiling"

+ 26 - 2
switchbot/devices/base_light.py

@@ -4,10 +4,15 @@ import logging
 from abc import abstractmethod
 from typing import Any
 
-from .device import ColorMode, SwitchbotSequenceDevice
+from .device import ColorMode, SwitchbotDevice
 
+_LOGGER = logging.getLogger(__name__)
+import asyncio
 
-class SwitchbotBaseLight(SwitchbotSequenceDevice):
+from ..models import SwitchBotAdvertisement
+
+
+class SwitchbotBaseLight(SwitchbotDevice):
     """Representation of a Switchbot light."""
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -75,3 +80,22 @@ class SwitchbotBaseLight(SwitchbotSequenceDevice):
     @abstractmethod
     async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
         """Set rgb."""
+
+
+class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
+    """Representation of a Switchbot light."""
+
+    def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
+        """Update device data from advertisement."""
+        current_state = self._get_adv_value("sequence_number")
+        super().update_from_advertisement(advertisement)
+        new_state = self._get_adv_value("sequence_number")
+        _LOGGER.debug(
+            "%s: update advertisement: %s (seq before: %s) (seq after: %s)",
+            self.name,
+            advertisement,
+            current_state,
+            new_state,
+        )
+        if current_state != new_state:
+            asyncio.ensure_future(self.update())

+ 0 - 3
switchbot/devices/bot.py

@@ -14,9 +14,6 @@ DOWN_KEY = "570103"
 UP_KEY = "570104"
 
 
-_LOGGER = logging.getLogger(__name__)
-
-
 class Switchbot(SwitchbotDevice):
     """Representation of a Switchbot."""
 

+ 3 - 7
switchbot/devices/bulb.py

@@ -1,15 +1,11 @@
 from __future__ import annotations
 
-import asyncio
 import logging
-from enum import Enum
 from typing import Any
 
-from switchbot.models import SwitchBotAdvertisement
+from .base_light import SwitchbotSequenceBaseLight
+from .device import REQ_HEADER, ColorMode
 
-from .device import SwitchbotDevice, SwitchbotSequenceDevice
-
-REQ_HEADER = "570f"
 BULB_COMMMAND_HEADER = "4701"
 BULB_REQUEST = f"{REQ_HEADER}4801"
 
@@ -29,7 +25,7 @@ from .base_light import SwitchbotBaseLight
 from .device import ColorMode
 
 
-class SwitchbotBulb(SwitchbotBaseLight):
+class SwitchbotBulb(SwitchbotSequenceBaseLight):
     """Representation of a Switchbot bulb."""
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:

+ 78 - 0
switchbot/devices/ceiling_light.py

@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from .base_light import SwitchbotBaseLight
+from .device import REQ_HEADER, ColorMode
+
+CEILING_LIGHT_COMMMAND_HEADER = "5401"
+CEILING_LIGHT_REQUEST = f"{REQ_HEADER}5501"
+
+CEILING_LIGHT_COMMAND = f"{REQ_HEADER}{CEILING_LIGHT_COMMMAND_HEADER}"
+CEILING_LIGHT_ON_KEY = f"{CEILING_LIGHT_COMMAND}01FF01FFFF"
+CEILING_LIGHT_OFF_KEY = f"{CEILING_LIGHT_COMMAND}02FF01FFFF"
+CW_BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}010001"
+BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}01FF01"
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SwitchbotCeilingLight(SwitchbotBaseLight):
+    """Representation of a Switchbot bulb."""
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        """Switchbot bulb constructor."""
+        super().__init__(*args, **kwargs)
+        self._state: dict[str, Any] = {}
+
+    @property
+    def color_modes(self) -> set[ColorMode]:
+        """Return the supported color modes."""
+        return {ColorMode.COLOR_TEMP}
+
+    async def update(self) -> None:
+        """Update state of device."""
+
+    async def turn_on(self) -> bool:
+        """Turn device on."""
+        result = await self._send_command(CEILING_LIGHT_ON_KEY)
+        ret = self._check_command_result(result, 0, {0x01})
+        self._override_adv_data = {"isOn": True}
+        self._fire_callbacks()
+        return ret
+
+    async def turn_off(self) -> bool:
+        """Turn device off."""
+        result = await self._send_command(CEILING_LIGHT_OFF_KEY)
+        ret = self._check_command_result(result, 0, {0x01})
+        self._override_adv_data = {"isOn": False}
+        self._fire_callbacks()
+        return ret
+
+    async def set_brightness(self, brightness: int) -> bool:
+        """Set brightness."""
+        assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
+        result = await self._send_command(f"{BRIGHTNESS_KEY}{brightness:02X}0FA1")
+        ret = self._check_command_result(result, 0, {0x01})
+        self._override_adv_data = {"brightness": brightness, "isOn": True}
+        self._fire_callbacks()
+        return ret
+
+    async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
+        """Set color temp."""
+        assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
+        assert 2700 <= color_temp <= 6500, "Color Temp must be between 0 and 100"
+        result = await self._send_command(
+            f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
+        )
+        ret = self._check_command_result(result, 0, {0x01})
+        self._state["cw"] = color_temp
+        self._override_adv_data = {"brightness": brightness, "isOn": True}
+        self._fire_callbacks()
+        return ret
+
+    async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
+        """Set rgb."""
+        # Not supported on this device

+ 8 - 8
switchbot/devices/curtain.py

@@ -4,16 +4,16 @@ from __future__ import annotations
 import logging
 from typing import Any
 
-from .device import SwitchbotDevice
+from .device import REQ_HEADER, SwitchbotDevice
 
 # Curtain keys
-OPEN_KEY = "570f450105ff00"  # 570F4501010100
-CLOSE_KEY = "570f450105ff64"  # 570F4501010164
-POSITION_KEY = "570F450105ff"  # +actual_position ex: 570F450105ff32 for 50%
-STOP_KEY = "570F450100ff"
-CURTAIN_EXT_SUM_KEY = "570f460401"
-CURTAIN_EXT_ADV_KEY = "570f460402"
-CURTAIN_EXT_CHAIN_INFO_KEY = "570f468101"
+OPEN_KEY = f"{REQ_HEADER}450105ff00"  # 570F4501010100
+CLOSE_KEY = f"{REQ_HEADER}450105ff64"  # 570F4501010164
+POSITION_KEY = f"{REQ_HEADER}450105ff"  # +actual_position ex: 570F450105ff32 for 50%
+STOP_KEY = f"{REQ_HEADER}450100ff"
+CURTAIN_EXT_SUM_KEY = f"{REQ_HEADER}460401"
+CURTAIN_EXT_ADV_KEY = f"{REQ_HEADER}460402"
+CURTAIN_EXT_CHAIN_INFO_KEY = f"{REQ_HEADER}468101"
 
 
 _LOGGER = logging.getLogger(__name__)

+ 3 - 0
switchbot/devices/device.py

@@ -26,6 +26,9 @@ from ..models import SwitchBotAdvertisement
 
 _LOGGER = logging.getLogger(__name__)
 
+REQ_HEADER = "570f"
+
+
 # Keys common to all device types
 DEVICE_GET_BASIC_SETTINGS_KEY = "5702"
 DEVICE_SET_MODE_KEY = "5703"

+ 4 - 6
switchbot/devices/light_strip.py

@@ -3,7 +3,9 @@ from __future__ import annotations
 import logging
 from typing import Any
 
-REQ_HEADER = "570f"
+from .base_light import SwitchbotSequenceBaseLight
+from .device import REQ_HEADER, ColorMode
+
 STRIP_COMMMAND_HEADER = "4901"
 STRIP_REQUEST = f"{REQ_HEADER}4A01"
 
@@ -17,11 +19,7 @@ BRIGHTNESS_KEY = f"{STRIP_COMMAND}14"
 _LOGGER = logging.getLogger(__name__)
 
 
-from .base_light import SwitchbotBaseLight
-from .device import ColorMode
-
-
-class SwitchbotLightStrip(SwitchbotBaseLight):
+class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
     """Representation of a Switchbot light strip."""
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:

+ 3 - 5
switchbot/devices/plug.py

@@ -1,13 +1,11 @@
 """Library to handle connection with Switchbot."""
 from __future__ import annotations
 
-from typing import Any
-
-from .device import SwitchbotDevice
+from .device import REQ_HEADER, SwitchbotDevice
 
 # Plug Mini keys
-PLUG_ON_KEY = "570f50010180"
-PLUG_OFF_KEY = "570f50010100"
+PLUG_ON_KEY = f"{REQ_HEADER}50010180"
+PLUG_OFF_KEY = f"{REQ_HEADER}50010100"
 
 
 class SwitchbotPlugMini(SwitchbotDevice):