Skip to content

Commit 0b992fc

Browse files
authored
Add support for the ceiling light (#96)
1 parent 28692b7 commit 0b992fc

File tree

12 files changed

+164
-33
lines changed

12 files changed

+164
-33
lines changed

switchbot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .devices.base_light import SwitchbotBaseLight
77
from .devices.bot import Switchbot
88
from .devices.bulb import SwitchbotBulb
9+
from .devices.ceiling_light import SwitchbotCeilingLight
910
from .devices.curtain import SwitchbotCurtain
1011
from .devices.device import ColorMode, SwitchbotDevice
1112
from .devices.light_strip import SwitchbotLightStrip
@@ -20,6 +21,7 @@
2021
"ColorMode",
2122
"SwitchbotBaseLight",
2223
"SwitchbotBulb",
24+
"SwitchbotCeilingLight",
2325
"SwitchbotDevice",
2426
"SwitchbotCurtain",
2527
"SwitchbotLightStrip",

switchbot/adv_parser.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Library to handle connection with Switchbot."""
22
from __future__ import annotations
33

4+
import logging
45
from collections.abc import Callable
56
from typing import TypedDict
67

78
from bleak.backends.device import BLEDevice
89
from bleak.backends.scanner import AdvertisementData
910

11+
from switchbot.adv_parsers.ceiling_light import process_woceiling
12+
1013
from .adv_parsers.bot import process_wohand
1114
from .adv_parsers.bulb import process_color_bulb
1215
from .adv_parsers.contact import process_wocontact
@@ -18,6 +21,8 @@
1821
from .const import SwitchbotModel
1922
from .models import SwitchBotAdvertisement
2023

24+
_LOGGER = logging.getLogger(__name__)
25+
2126

2227
class SwitchbotSupportedType(TypedDict):
2328
"""Supported type of Switchbot."""
@@ -73,6 +78,11 @@ class SwitchbotSupportedType(TypedDict):
7378
"modelFriendlyName": "Color Bulb",
7479
"func": process_color_bulb,
7580
},
81+
"q": {
82+
"modelName": SwitchbotModel.CEILING_LIGHT,
83+
"modelFriendlyName": "Ceiling Light",
84+
"func": process_woceiling,
85+
},
7686
}
7787

7888

@@ -95,14 +105,14 @@ def parse_advertisement_data(
95105
"address": device.address, # MacOS uses UUIDs
96106
"rawAdvData": list(advertisement_data.service_data.values())[0],
97107
"data": {},
108+
"model": _model,
109+
"isEncrypted": bool(_service_data[0] & 0b10000000),
98110
}
99111

100112
type_data = SUPPORTED_TYPES.get(_model)
101113
if type_data:
102114
data.update(
103115
{
104-
"isEncrypted": bool(_service_data[0] & 0b10000000),
105-
"model": _model,
106116
"modelFriendlyName": type_data["modelFriendlyName"],
107117
"modelName": type_data["modelName"],
108118
"data": type_data["func"](_service_data, _mfr_data),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Ceiling Light adv parser."""
2+
from __future__ import annotations
3+
4+
import logging
5+
6+
_LOGGER = logging.getLogger(__name__)
7+
8+
# Off d94b2d012b3c4864106124
9+
# on d94b2d012b3c4a641061a4
10+
# Off d94b2d012b3c4b64106124
11+
# on d94b2d012b3c4d641061a4
12+
# 00112233445566778899AA
13+
14+
15+
def process_woceiling(data: bytes, mfr_data: bytes | None) -> dict[str, bool | int]:
16+
"""Process WoCeiling services data."""
17+
assert mfr_data is not None
18+
return {
19+
"sequence_number": mfr_data[6],
20+
"isOn": bool(mfr_data[10] & 0b10000000),
21+
"brightness": mfr_data[7] & 0b01111111,
22+
"cw": int(mfr_data[8:10].hex(), 16),
23+
"color_mode": 1,
24+
}

switchbot/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ class SwitchbotModel(StrEnum):
1818
METER = "WoSensorTH"
1919
MOTION_SENSOR = "WoPresence"
2020
COLOR_BULB = "WoBulb"
21+
CEILING_LIGHT = "WoCeiling"

switchbot/devices/base_light.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
from abc import abstractmethod
55
from typing import Any
66

7-
from .device import ColorMode, SwitchbotSequenceDevice
7+
from .device import ColorMode, SwitchbotDevice
88

9+
_LOGGER = logging.getLogger(__name__)
10+
import asyncio
911

10-
class SwitchbotBaseLight(SwitchbotSequenceDevice):
12+
from ..models import SwitchBotAdvertisement
13+
14+
15+
class SwitchbotBaseLight(SwitchbotDevice):
1116
"""Representation of a Switchbot light."""
1217

1318
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -75,3 +80,22 @@ async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
7580
@abstractmethod
7681
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
7782
"""Set rgb."""
83+
84+
85+
class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
86+
"""Representation of a Switchbot light."""
87+
88+
def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
89+
"""Update device data from advertisement."""
90+
current_state = self._get_adv_value("sequence_number")
91+
super().update_from_advertisement(advertisement)
92+
new_state = self._get_adv_value("sequence_number")
93+
_LOGGER.debug(
94+
"%s: update advertisement: %s (seq before: %s) (seq after: %s)",
95+
self.name,
96+
advertisement,
97+
current_state,
98+
new_state,
99+
)
100+
if current_state != new_state:
101+
asyncio.ensure_future(self.update())

switchbot/devices/bot.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
UP_KEY = "570104"
1515

1616

17-
_LOGGER = logging.getLogger(__name__)
18-
19-
2017
class Switchbot(SwitchbotDevice):
2118
"""Representation of a Switchbot."""
2219

switchbot/devices/bulb.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
from __future__ import annotations
22

3-
import asyncio
43
import logging
5-
from enum import Enum
64
from typing import Any
75

8-
from switchbot.models import SwitchBotAdvertisement
6+
from .base_light import SwitchbotSequenceBaseLight
7+
from .device import REQ_HEADER, ColorMode
98

10-
from .device import SwitchbotDevice, SwitchbotSequenceDevice
11-
12-
REQ_HEADER = "570f"
139
BULB_COMMMAND_HEADER = "4701"
1410
BULB_REQUEST = f"{REQ_HEADER}4801"
1511

@@ -29,7 +25,7 @@
2925
from .device import ColorMode
3026

3127

32-
class SwitchbotBulb(SwitchbotBaseLight):
28+
class SwitchbotBulb(SwitchbotSequenceBaseLight):
3329
"""Representation of a Switchbot bulb."""
3430

3531
def __init__(self, *args: Any, **kwargs: Any) -> None:

switchbot/devices/ceiling_light.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Any
5+
6+
from .base_light import SwitchbotBaseLight
7+
from .device import REQ_HEADER, ColorMode
8+
9+
CEILING_LIGHT_COMMMAND_HEADER = "5401"
10+
CEILING_LIGHT_REQUEST = f"{REQ_HEADER}5501"
11+
12+
CEILING_LIGHT_COMMAND = f"{REQ_HEADER}{CEILING_LIGHT_COMMMAND_HEADER}"
13+
CEILING_LIGHT_ON_KEY = f"{CEILING_LIGHT_COMMAND}01FF01FFFF"
14+
CEILING_LIGHT_OFF_KEY = f"{CEILING_LIGHT_COMMAND}02FF01FFFF"
15+
CW_BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}010001"
16+
BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}01FF01"
17+
18+
19+
_LOGGER = logging.getLogger(__name__)
20+
21+
22+
class SwitchbotCeilingLight(SwitchbotBaseLight):
23+
"""Representation of a Switchbot bulb."""
24+
25+
def __init__(self, *args: Any, **kwargs: Any) -> None:
26+
"""Switchbot bulb constructor."""
27+
super().__init__(*args, **kwargs)
28+
self._state: dict[str, Any] = {}
29+
30+
@property
31+
def color_modes(self) -> set[ColorMode]:
32+
"""Return the supported color modes."""
33+
return {ColorMode.COLOR_TEMP}
34+
35+
async def update(self) -> None:
36+
"""Update state of device."""
37+
38+
async def turn_on(self) -> bool:
39+
"""Turn device on."""
40+
result = await self._send_command(CEILING_LIGHT_ON_KEY)
41+
ret = self._check_command_result(result, 0, {0x01})
42+
self._override_adv_data = {"isOn": True}
43+
self._fire_callbacks()
44+
return ret
45+
46+
async def turn_off(self) -> bool:
47+
"""Turn device off."""
48+
result = await self._send_command(CEILING_LIGHT_OFF_KEY)
49+
ret = self._check_command_result(result, 0, {0x01})
50+
self._override_adv_data = {"isOn": False}
51+
self._fire_callbacks()
52+
return ret
53+
54+
async def set_brightness(self, brightness: int) -> bool:
55+
"""Set brightness."""
56+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
57+
result = await self._send_command(f"{BRIGHTNESS_KEY}{brightness:02X}0FA1")
58+
ret = self._check_command_result(result, 0, {0x01})
59+
self._override_adv_data = {"brightness": brightness, "isOn": True}
60+
self._fire_callbacks()
61+
return ret
62+
63+
async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
64+
"""Set color temp."""
65+
assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
66+
assert 2700 <= color_temp <= 6500, "Color Temp must be between 0 and 100"
67+
result = await self._send_command(
68+
f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
69+
)
70+
ret = self._check_command_result(result, 0, {0x01})
71+
self._state["cw"] = color_temp
72+
self._override_adv_data = {"brightness": brightness, "isOn": True}
73+
self._fire_callbacks()
74+
return ret
75+
76+
async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
77+
"""Set rgb."""
78+
# Not supported on this device

switchbot/devices/curtain.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
import logging
55
from typing import Any
66

7-
from .device import SwitchbotDevice
7+
from .device import REQ_HEADER, SwitchbotDevice
88

99
# Curtain keys
10-
OPEN_KEY = "570f450105ff00" # 570F4501010100
11-
CLOSE_KEY = "570f450105ff64" # 570F4501010164
12-
POSITION_KEY = "570F450105ff" # +actual_position ex: 570F450105ff32 for 50%
13-
STOP_KEY = "570F450100ff"
14-
CURTAIN_EXT_SUM_KEY = "570f460401"
15-
CURTAIN_EXT_ADV_KEY = "570f460402"
16-
CURTAIN_EXT_CHAIN_INFO_KEY = "570f468101"
10+
OPEN_KEY = f"{REQ_HEADER}450105ff00" # 570F4501010100
11+
CLOSE_KEY = f"{REQ_HEADER}450105ff64" # 570F4501010164
12+
POSITION_KEY = f"{REQ_HEADER}450105ff" # +actual_position ex: 570F450105ff32 for 50%
13+
STOP_KEY = f"{REQ_HEADER}450100ff"
14+
CURTAIN_EXT_SUM_KEY = f"{REQ_HEADER}460401"
15+
CURTAIN_EXT_ADV_KEY = f"{REQ_HEADER}460402"
16+
CURTAIN_EXT_CHAIN_INFO_KEY = f"{REQ_HEADER}468101"
1717

1818

1919
_LOGGER = logging.getLogger(__name__)

switchbot/devices/device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626

2727
_LOGGER = logging.getLogger(__name__)
2828

29+
REQ_HEADER = "570f"
30+
31+
2932
# Keys common to all device types
3033
DEVICE_GET_BASIC_SETTINGS_KEY = "5702"
3134
DEVICE_SET_MODE_KEY = "5703"

0 commit comments

Comments
 (0)