瀏覽代碼

Add support for the ceiling light (#96)

J. Nick Koston 2 年之前
父節點
當前提交
0b992fc3d0

+ 2 - 0
switchbot/__init__.py

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

+ 12 - 2
switchbot/adv_parser.py

@@ -1,12 +1,15 @@
 """Library to handle connection with Switchbot."""
 """Library to handle connection with Switchbot."""
 from __future__ import annotations
 from __future__ import annotations
 
 
+import logging
 from collections.abc import Callable
 from collections.abc import Callable
 from typing import TypedDict
 from typing import TypedDict
 
 
 from bleak.backends.device import BLEDevice
 from bleak.backends.device import BLEDevice
 from bleak.backends.scanner import AdvertisementData
 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.bot import process_wohand
 from .adv_parsers.bulb import process_color_bulb
 from .adv_parsers.bulb import process_color_bulb
 from .adv_parsers.contact import process_wocontact
 from .adv_parsers.contact import process_wocontact
@@ -18,6 +21,8 @@ from .adv_parsers.plug import process_woplugmini
 from .const import SwitchbotModel
 from .const import SwitchbotModel
 from .models import SwitchBotAdvertisement
 from .models import SwitchBotAdvertisement
 
 
+_LOGGER = logging.getLogger(__name__)
+
 
 
 class SwitchbotSupportedType(TypedDict):
 class SwitchbotSupportedType(TypedDict):
     """Supported type of Switchbot."""
     """Supported type of Switchbot."""
@@ -73,6 +78,11 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
         "modelFriendlyName": "Color Bulb",
         "modelFriendlyName": "Color Bulb",
         "func": process_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
         "address": device.address,  # MacOS uses UUIDs
         "rawAdvData": list(advertisement_data.service_data.values())[0],
         "rawAdvData": list(advertisement_data.service_data.values())[0],
         "data": {},
         "data": {},
+        "model": _model,
+        "isEncrypted": bool(_service_data[0] & 0b10000000),
     }
     }
 
 
     type_data = SUPPORTED_TYPES.get(_model)
     type_data = SUPPORTED_TYPES.get(_model)
     if type_data:
     if type_data:
         data.update(
         data.update(
             {
             {
-                "isEncrypted": bool(_service_data[0] & 0b10000000),
-                "model": _model,
                 "modelFriendlyName": type_data["modelFriendlyName"],
                 "modelFriendlyName": type_data["modelFriendlyName"],
                 "modelName": type_data["modelName"],
                 "modelName": type_data["modelName"],
                 "data": type_data["func"](_service_data, _mfr_data),
                 "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"
     METER = "WoSensorTH"
     MOTION_SENSOR = "WoPresence"
     MOTION_SENSOR = "WoPresence"
     COLOR_BULB = "WoBulb"
     COLOR_BULB = "WoBulb"
+    CEILING_LIGHT = "WoCeiling"

+ 26 - 2
switchbot/devices/base_light.py

@@ -4,10 +4,15 @@ import logging
 from abc import abstractmethod
 from abc import abstractmethod
 from typing import Any
 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."""
     """Representation of a Switchbot light."""
 
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
     def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -75,3 +80,22 @@ class SwitchbotBaseLight(SwitchbotSequenceDevice):
     @abstractmethod
     @abstractmethod
     async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
     async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
         """Set rgb."""
         """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"
 UP_KEY = "570104"
 
 
 
 
-_LOGGER = logging.getLogger(__name__)
-
-
 class Switchbot(SwitchbotDevice):
 class Switchbot(SwitchbotDevice):
     """Representation of a Switchbot."""
     """Representation of a Switchbot."""
 
 

+ 3 - 7
switchbot/devices/bulb.py

@@ -1,15 +1,11 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-import asyncio
 import logging
 import logging
-from enum import Enum
 from typing import Any
 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_COMMMAND_HEADER = "4701"
 BULB_REQUEST = f"{REQ_HEADER}4801"
 BULB_REQUEST = f"{REQ_HEADER}4801"
 
 
@@ -29,7 +25,7 @@ from .base_light import SwitchbotBaseLight
 from .device import ColorMode
 from .device import ColorMode
 
 
 
 
-class SwitchbotBulb(SwitchbotBaseLight):
+class SwitchbotBulb(SwitchbotSequenceBaseLight):
     """Representation of a Switchbot bulb."""
     """Representation of a Switchbot bulb."""
 
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
     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
 import logging
 from typing import Any
 from typing import Any
 
 
-from .device import SwitchbotDevice
+from .device import REQ_HEADER, SwitchbotDevice
 
 
 # Curtain keys
 # 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__)
 _LOGGER = logging.getLogger(__name__)

+ 3 - 0
switchbot/devices/device.py

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

+ 4 - 6
switchbot/devices/light_strip.py

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

+ 3 - 5
switchbot/devices/plug.py

@@ -1,13 +1,11 @@
 """Library to handle connection with Switchbot."""
 """Library to handle connection with Switchbot."""
 from __future__ import annotations
 from __future__ import annotations
 
 
-from typing import Any
-
-from .device import SwitchbotDevice
+from .device import REQ_HEADER, SwitchbotDevice
 
 
 # Plug Mini keys
 # 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):
 class SwitchbotPlugMini(SwitchbotDevice):