1
0

test_strip_light.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. from unittest.mock import AsyncMock, MagicMock
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import SwitchBotAdvertisement, SwitchbotModel
  5. from switchbot.const.light import ColorMode
  6. from switchbot.devices import light_strip
  7. from switchbot.devices.base_light import SwitchbotBaseLight
  8. from switchbot.devices.device import SwitchbotOperationError
  9. from . import (
  10. CANDLE_WARMER_LAMP_INFO,
  11. FLOOR_LAMP_INFO,
  12. PERMANENT_OUTDOOR_LIGHT_INFO,
  13. RGBIC_NEON_LIGHT_INFO,
  14. RGBICWW_CEILING_LIGHT_INFO,
  15. RGBICWW_FLOOR_LAMP_INFO,
  16. RGBICWW_STRIP_LIGHT_INFO,
  17. STRIP_LIGHT_3_INFO,
  18. )
  19. from .test_adv_parser import AdvTestCase, generate_ble_device
  20. ALL_LIGHT_CASES = [
  21. (STRIP_LIGHT_3_INFO, light_strip.SwitchbotStripLight3),
  22. (FLOOR_LAMP_INFO, light_strip.SwitchbotStripLight3),
  23. (CANDLE_WARMER_LAMP_INFO, light_strip.SwitchbotCandleWarmerLamp),
  24. (RGBICWW_STRIP_LIGHT_INFO, light_strip.SwitchbotRgbicLight),
  25. (RGBICWW_FLOOR_LAMP_INFO, light_strip.SwitchbotRgbicLight),
  26. (PERMANENT_OUTDOOR_LIGHT_INFO, light_strip.SwitchbotPermanentOutdoorLight),
  27. (RGBIC_NEON_LIGHT_INFO, light_strip.SwitchbotRgbicNeonLight),
  28. (RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight),
  29. ]
  30. # RGB + color-temp devices. Excludes brightness-only lights (CWL) and
  31. # RGB-only lights (RGBIC Neon) whose color_modes differ from the rest.
  32. RGB_LIGHT_CASES = [
  33. case
  34. for case in ALL_LIGHT_CASES
  35. if case[1]
  36. not in (light_strip.SwitchbotCandleWarmerLamp, light_strip.SwitchbotRgbicNeonLight)
  37. ]
  38. @pytest.fixture(params=RGB_LIGHT_CASES)
  39. def device_case(request):
  40. return request.param
  41. @pytest.fixture
  42. def expected_effects(device_case):
  43. adv_info, _dev_cls = device_case
  44. EXPECTED = {
  45. SwitchbotModel.STRIP_LIGHT_3: ("christmas", "halloween", "sunset"),
  46. SwitchbotModel.FLOOR_LAMP: ("christmas", "halloween", "sunset"),
  47. SwitchbotModel.RGBICWW_STRIP_LIGHT: ("romance", "energy", "heartbeat"),
  48. SwitchbotModel.RGBICWW_FLOOR_LAMP: ("romance", "energy", "heartbeat"),
  49. SwitchbotModel.RGBICWW_CEILING_LIGHT: ("romance", "energy", "heartbeat"),
  50. SwitchbotModel.PERMANENT_OUTDOOR_LIGHT: ("romance", "energy", "heartbeat"),
  51. }
  52. return EXPECTED[adv_info.modelName]
  53. def create_device_for_command_testing(
  54. adv_info: AdvTestCase,
  55. dev_cls: type[SwitchbotBaseLight],
  56. init_data: dict | None = None,
  57. ):
  58. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  59. device = dev_cls(
  60. ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=adv_info.modelName
  61. )
  62. device.update_from_advertisement(
  63. make_advertisement_data(ble_device, adv_info, init_data)
  64. )
  65. device._send_command = AsyncMock()
  66. device._check_command_result = MagicMock()
  67. device.update = AsyncMock()
  68. return device
  69. def make_advertisement_data(
  70. ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None
  71. ):
  72. """Set advertisement data with defaults."""
  73. if init_data is None:
  74. init_data = {}
  75. return SwitchBotAdvertisement(
  76. address="aa:bb:cc:dd:ee:ff",
  77. data={
  78. "rawAdvData": adv_info.service_data,
  79. "data": adv_info.data | init_data,
  80. "isEncrypted": False,
  81. "model": adv_info.model,
  82. "modelFriendlyName": adv_info.modelFriendlyName,
  83. "modelName": adv_info.modelName,
  84. },
  85. device=ble_device,
  86. rssi=-80,
  87. active=True,
  88. )
  89. @pytest.mark.asyncio
  90. async def test_default_info(device_case, expected_effects):
  91. """Test default initialization of the strip light."""
  92. adv_info, dev_cls = device_case
  93. device = create_device_for_command_testing(adv_info, dev_cls)
  94. assert device.rgb is None
  95. device._state = {"r": 30, "g": 0, "b": 0, "cw": 3200}
  96. assert device.is_on() is True
  97. assert device.on is True
  98. assert device.color_mode == ColorMode.RGB
  99. assert device.color_modes == {ColorMode.RGB, ColorMode.COLOR_TEMP}
  100. assert device.rgb == (30, 0, 0)
  101. assert device.color_temp == 3200
  102. assert device.brightness == adv_info.data["brightness"]
  103. assert device.min_temp == 2700
  104. assert device.max_temp == 6500
  105. # Check that effect list contains expected lowercase effect names
  106. effect_list = device.get_effect_list
  107. assert effect_list is not None
  108. assert all(effect.islower() for effect in effect_list)
  109. # Verify some known effects are present
  110. for effect in expected_effects:
  111. assert effect in effect_list
  112. @pytest.mark.asyncio
  113. async def test_rgbic_neon_light_info() -> None:
  114. """Test color_mode / color_modes on SwitchbotRgbicNeonLight (RGB only)."""
  115. adv_info, dev_cls = RGBIC_NEON_LIGHT_INFO, light_strip.SwitchbotRgbicNeonLight
  116. device = create_device_for_command_testing(adv_info, dev_cls)
  117. assert device.color_mode == ColorMode.RGB
  118. assert device.color_modes == {ColorMode.RGB}
  119. @pytest.mark.asyncio
  120. async def test_candle_warmer_lamp_info() -> None:
  121. """Test default initialization of the candle warmer lamp."""
  122. adv_info, dev_cls = CANDLE_WARMER_LAMP_INFO, light_strip.SwitchbotCandleWarmerLamp
  123. device = create_device_for_command_testing(adv_info, dev_cls)
  124. assert device.rgb is None
  125. assert device.is_on() is True
  126. assert device.on is True
  127. assert device.color_mode == ColorMode.BRIGHTNESS
  128. assert device.color_modes == {
  129. ColorMode.BRIGHTNESS,
  130. }
  131. assert device.brightness == adv_info.data["brightness"]
  132. assert device.get_effect_list is None
  133. @pytest.mark.asyncio
  134. async def test_candle_warmer_lamp_unsupported_operations() -> None:
  135. """Test that RGB/color-temp/effect operations are not supported on CWL."""
  136. device = create_device_for_command_testing(
  137. CANDLE_WARMER_LAMP_INFO, light_strip.SwitchbotCandleWarmerLamp
  138. )
  139. with pytest.raises(
  140. SwitchbotOperationError,
  141. match="does not support this functionality",
  142. ):
  143. await device.set_rgb(100, 255, 128, 64)
  144. with pytest.raises(
  145. SwitchbotOperationError,
  146. match="does not support this functionality",
  147. ):
  148. await device.set_color_temp(100, 4000)
  149. with pytest.raises(SwitchbotOperationError, match="not supported"):
  150. await device.set_effect("sunset")
  151. @pytest.mark.asyncio
  152. @pytest.mark.parametrize(
  153. ("basic_info", "version_info"), [(True, False), (False, True), (False, False)]
  154. )
  155. @pytest.mark.parametrize(("adv_info", "dev_cls"), ALL_LIGHT_CASES)
  156. async def test_get_basic_info_returns_none(
  157. basic_info, version_info, adv_info, dev_cls
  158. ) -> None:
  159. """Test that get_basic_info returns None if no data is available."""
  160. device = create_device_for_command_testing(adv_info, dev_cls)
  161. device._send_command = AsyncMock(side_effect=[version_info, basic_info])
  162. device._check_command_result = MagicMock(
  163. side_effect=[bool(version_info), bool(basic_info)]
  164. )
  165. assert await device.get_basic_info() is None
  166. @pytest.mark.asyncio
  167. @pytest.mark.parametrize(
  168. "device_case",
  169. [
  170. case
  171. for case in RGB_LIGHT_CASES
  172. if case[1] is not light_strip.SwitchbotRgbicwwCeilingLight
  173. ],
  174. )
  175. @pytest.mark.parametrize(
  176. ("info_data", "result"),
  177. [
  178. (
  179. {
  180. "basic_info": b"\x01\x00<\xff\x00\xd8\x00\x19d\x00\x03",
  181. "version_info": b"\x01\x01\n",
  182. },
  183. [False, 60, 255, 0, 216, 6500, 3, 1.0],
  184. ),
  185. (
  186. {
  187. "basic_info": b"\x01\x80NK\xff:\x00\x19d\xff\x02",
  188. "version_info": b"\x01\x01\n",
  189. },
  190. [True, 78, 75, 255, 58, 6500, 2, 1.0],
  191. ),
  192. (
  193. {
  194. "basic_info": b"\x01\x80$K\xff:\x00\x13\xf9\xff\x06",
  195. "version_info": b"\x01\x01\n",
  196. },
  197. [True, 36, 75, 255, 58, 5113, 6, 1.0],
  198. ),
  199. ],
  200. )
  201. async def test_strip_light_get_basic_info(info_data, result, device_case):
  202. """Test getting basic info from the strip light."""
  203. adv_info, dev_cls = device_case
  204. device = create_device_for_command_testing(adv_info, dev_cls)
  205. device._send_command = AsyncMock(
  206. side_effect=[info_data["version_info"], info_data["basic_info"]]
  207. )
  208. device._check_command_result = MagicMock(side_effect=[True, True])
  209. info = await device.get_basic_info()
  210. assert info["isOn"] is result[0]
  211. assert info["brightness"] == result[1]
  212. assert info["r"] == result[2]
  213. assert info["g"] == result[3]
  214. assert info["b"] == result[4]
  215. assert info["cw"] == result[5]
  216. assert info["color_mode"] == result[6]
  217. assert info["firmware"] == result[7]
  218. @pytest.mark.asyncio
  219. async def test_set_color_temp(device_case):
  220. """Test setting color temperature."""
  221. adv_info, dev_cls = device_case
  222. device = create_device_for_command_testing(adv_info, dev_cls)
  223. await device.set_color_temp(50, 3000)
  224. device._send_command.assert_called_with(
  225. device._set_color_temp_command.format("320BB8")
  226. )
  227. @pytest.mark.asyncio
  228. async def test_turn_on(device_case):
  229. """Test turning on the strip light."""
  230. init_data = {"isOn": True}
  231. adv_info, dev_cls = device_case
  232. device = create_device_for_command_testing(adv_info, dev_cls, init_data)
  233. await device.turn_on()
  234. device._send_command.assert_called_with(device._turn_on_command)
  235. assert device.is_on() is True
  236. @pytest.mark.asyncio
  237. async def test_turn_off(device_case):
  238. """Test turning off the strip light."""
  239. init_data = {"isOn": False}
  240. adv_info, dev_cls = device_case
  241. device = create_device_for_command_testing(adv_info, dev_cls, init_data)
  242. await device.turn_off()
  243. device._send_command.assert_called_with(device._turn_off_command)
  244. assert device.is_on() is False
  245. @pytest.mark.asyncio
  246. async def test_set_brightness(device_case):
  247. """Test setting brightness."""
  248. adv_info, dev_cls = device_case
  249. device = create_device_for_command_testing(adv_info, dev_cls)
  250. await device.set_brightness(75)
  251. device._send_command.assert_called_with(device._set_brightness_command.format("4B"))
  252. @pytest.mark.asyncio
  253. async def test_set_rgb(device_case):
  254. """Test setting RGB values."""
  255. adv_info, dev_cls = device_case
  256. device = create_device_for_command_testing(adv_info, dev_cls)
  257. await device.set_rgb(100, 255, 128, 64)
  258. device._send_command.assert_called_with(device._set_rgb_command.format("64FF8040"))
  259. @pytest.mark.asyncio
  260. async def test_set_effect_with_invalid_effect(device_case):
  261. """Test setting an invalid effect."""
  262. adv_info, dev_cls = device_case
  263. device = create_device_for_command_testing(adv_info, dev_cls)
  264. with pytest.raises(
  265. SwitchbotOperationError, match="Effect invalid_effect not supported"
  266. ):
  267. await device.set_effect("invalid_effect")
  268. @pytest.mark.asyncio
  269. async def test_set_effect_with_valid_effect(device_case):
  270. """Test setting a valid effect."""
  271. adv_info, dev_cls = device_case
  272. device = create_device_for_command_testing(adv_info, dev_cls)
  273. device._send_multiple_commands = AsyncMock()
  274. await device.set_effect("christmas")
  275. device._send_multiple_commands.assert_called_with(device._effect_dict["christmas"])
  276. assert device.get_effect() == "christmas"
  277. @pytest.mark.asyncio
  278. async def test_effect_list_contains_lowercase_names(device_case, expected_effects):
  279. """Test that all effect names in get_effect_list are lowercase."""
  280. adv_info, dev_cls = device_case
  281. device = create_device_for_command_testing(adv_info, dev_cls)
  282. effect_list = device.get_effect_list
  283. assert effect_list is not None, "Effect list should not be None"
  284. # All effect names should be lowercase
  285. for effect_name in effect_list:
  286. assert effect_name.islower(), f"Effect name '{effect_name}' is not lowercase"
  287. # Verify some known effects are present
  288. for expected_effect in expected_effects:
  289. assert expected_effect in effect_list, (
  290. f"Expected effect '{expected_effect}' not found"
  291. )
  292. @pytest.mark.asyncio
  293. async def test_set_effect_normalizes_case(device_case):
  294. """Test that set_effect normalizes effect names to lowercase."""
  295. adv_info, dev_cls = device_case
  296. device = create_device_for_command_testing(adv_info, dev_cls)
  297. device._send_multiple_commands = AsyncMock()
  298. # Test various case combinations
  299. test_cases = ["CHRISTMAS", "Christmas", "ChRiStMaS", "christmas"]
  300. for test_effect in test_cases:
  301. await device.set_effect(test_effect)
  302. # Should always work regardless of case
  303. device._send_multiple_commands.assert_called()
  304. assert device.get_effect() == test_effect # Stored as provided
  305. @pytest.mark.parametrize(
  306. ("dev_cls", "expected_model"),
  307. [
  308. (light_strip.SwitchbotStripLight3, SwitchbotModel.STRIP_LIGHT_3),
  309. (light_strip.SwitchbotRgbicLight, SwitchbotModel.RGBICWW_STRIP_LIGHT),
  310. (
  311. light_strip.SwitchbotPermanentOutdoorLight,
  312. SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
  313. ),
  314. (light_strip.SwitchbotRgbicNeonLight, SwitchbotModel.RGBIC_NEON_ROPE_LIGHT),
  315. (light_strip.SwitchbotCandleWarmerLamp, SwitchbotModel.CANDLE_WARMER_LAMP),
  316. (
  317. light_strip.SwitchbotRgbicwwCeilingLight,
  318. SwitchbotModel.RGBICWW_CEILING_LIGHT,
  319. ),
  320. ],
  321. )
  322. def test_default_model_classvar(dev_cls, expected_model):
  323. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  324. device = dev_cls(ble_device, "ff", "ffffffffffffffffffffffffffffffff")
  325. assert device._model == expected_model
  326. def create_strip_light_device(init_data: dict | None = None):
  327. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  328. return light_strip.SwitchbotLightStrip(ble_device)
  329. @pytest.mark.asyncio
  330. async def test_strip_light_supported_color_modes():
  331. """Test that the strip light supports the expected color modes."""
  332. device = create_strip_light_device()
  333. assert device.color_modes == {
  334. ColorMode.RGB,
  335. }
  336. @pytest.mark.asyncio
  337. @pytest.mark.parametrize(
  338. ("commands", "results", "final_result"),
  339. [
  340. (("command1", "command2"), [(b"\x01", False), (None, False)], False),
  341. (("command1", "command2"), [(None, False), (b"\x01", True)], True),
  342. (("command1", "command2"), [(b"\x01", True), (b"\x01", False)], True),
  343. ],
  344. )
  345. async def test_send_multiple_commands(commands, results, final_result, device_case):
  346. """Test sending multiple commands."""
  347. adv_info, dev_cls = device_case
  348. device = create_device_for_command_testing(adv_info, dev_cls)
  349. device._send_command = AsyncMock(side_effect=[r[0] for r in results])
  350. device._check_command_result = MagicMock(side_effect=[r[1] for r in results])
  351. result = await device._send_multiple_commands(list(commands))
  352. assert result is final_result
  353. @pytest.mark.asyncio
  354. async def test_unimplemented_color_mode():
  355. class TestDevice(SwitchbotBaseLight):
  356. pass
  357. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  358. device = TestDevice(ble_device)
  359. with pytest.raises(NotImplementedError):
  360. _ = device.color_mode
  361. @pytest.mark.asyncio
  362. async def test_exception_with_wrong_model():
  363. class TestDevice(SwitchbotBaseLight):
  364. def __init__(self, device: BLEDevice, model: str = "unknown") -> None:
  365. super().__init__(device, model=model)
  366. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  367. device = TestDevice(ble_device)
  368. with pytest.raises(
  369. SwitchbotOperationError,
  370. match="Current device aa:bb:cc:dd:ee:ff does not support this functionality",
  371. ):
  372. await device.set_rgb(100, 255, 128, 64)
  373. @pytest.mark.asyncio
  374. @pytest.mark.parametrize(
  375. ("color_mode_value", "expected_color_mode"),
  376. [
  377. (1, ColorMode.EFFECT), # SEGMENTED
  378. (2, ColorMode.RGB),
  379. (3, ColorMode.EFFECT), # SCENE
  380. (4, ColorMode.EFFECT), # MUSIC
  381. (5, ColorMode.EFFECT), # CONTROLLER
  382. (6, ColorMode.COLOR_TEMP),
  383. (7, ColorMode.EFFECT), # EFFECT (RGBIC-specific)
  384. (10, ColorMode.OFF), # UNKNOWN
  385. ],
  386. )
  387. async def test_permanent_outdoor_light_color_mode(
  388. color_mode_value, expected_color_mode
  389. ):
  390. """Test that POL correctly handles all RGBICStripLightColorMode values including EFFECT (7)."""
  391. device = create_device_for_command_testing(
  392. PERMANENT_OUTDOOR_LIGHT_INFO,
  393. light_strip.SwitchbotPermanentOutdoorLight,
  394. init_data={"color_mode": color_mode_value},
  395. )
  396. assert device.color_mode == expected_color_mode
  397. @pytest.mark.asyncio
  398. async def test_rgbicww_ceiling_light_main_sub_light_state() -> None:
  399. """Test the main (warm-white) sub-light state surface."""
  400. device = create_device_for_command_testing(
  401. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  402. )
  403. assert device.is_main_on is True
  404. assert device.main_brightness == 32
  405. @pytest.mark.asyncio
  406. @pytest.mark.parametrize(
  407. ("method", "expected_command_attr"),
  408. [
  409. ("turn_on_main", "_turn_on_main_command"),
  410. ("turn_off_main", "_turn_off_main_command"),
  411. ("turn_on_color", "_turn_on_color_command"),
  412. ("turn_off_color", "_turn_off_color_command"),
  413. ],
  414. )
  415. async def test_rgbicww_ceiling_light_sub_light_power(
  416. method: str, expected_command_attr: str
  417. ) -> None:
  418. """Test the per-sub-light power commands."""
  419. device = create_device_for_command_testing(
  420. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  421. )
  422. await getattr(device, method)()
  423. device._send_command.assert_called_with(getattr(device, expected_command_attr))
  424. @pytest.mark.asyncio
  425. async def test_rgbicww_ceiling_light_set_main_brightness() -> None:
  426. """Test setting main (warm-white) sub-light brightness."""
  427. device = create_device_for_command_testing(
  428. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  429. )
  430. await device.set_main_brightness(75)
  431. device._send_command.assert_called_with(
  432. device._set_main_brightness_command.format("4B")
  433. )
  434. @pytest.mark.asyncio
  435. async def test_rgbicww_ceiling_light_set_main_color_temp() -> None:
  436. """Test setting main (warm-white) sub-light color temperature."""
  437. device = create_device_for_command_testing(
  438. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  439. )
  440. await device.set_main_color_temp(3000)
  441. device._send_command.assert_called_with(
  442. device._set_main_color_temp_command.format("0BB8")
  443. )
  444. @pytest.mark.asyncio
  445. async def test_rgbicww_ceiling_light_color_modes() -> None:
  446. """Test the supported color modes set."""
  447. device = create_device_for_command_testing(
  448. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  449. )
  450. assert device.color_modes == {ColorMode.RGB, ColorMode.COLOR_TEMP}
  451. @pytest.mark.asyncio
  452. @pytest.mark.parametrize(
  453. ("color_mode_value", "expected_color_mode"),
  454. [
  455. (1, ColorMode.EFFECT), # SEGMENTED
  456. (2, ColorMode.RGB), # COLOR
  457. (3, ColorMode.EFFECT), # SCENE
  458. (4, ColorMode.EFFECT), # MUSIC
  459. (5, ColorMode.EFFECT), # CONTROLLER
  460. (6, ColorMode.COLOR_TEMP), # WARMWHITE
  461. (7, ColorMode.EFFECT), # EFFECT
  462. (10, ColorMode.OFF), # UNKNOWN
  463. ],
  464. )
  465. async def test_rgbicww_ceiling_light_color_mode(
  466. color_mode_value, expected_color_mode
  467. ) -> None:
  468. """Test color_mode mapping for the ceiling light."""
  469. device = create_device_for_command_testing(
  470. RGBICWW_CEILING_LIGHT_INFO,
  471. light_strip.SwitchbotRgbicwwCeilingLight,
  472. init_data={"color_mode": color_mode_value},
  473. )
  474. assert device.color_mode == expected_color_mode
  475. @pytest.mark.asyncio
  476. async def test_rgbicww_ceiling_light_get_basic_info_with_main_state() -> None:
  477. """
  478. Test that the ceiling light's get_basic_info reads RGB over GATT.
  479. Power/brightness/mode/color-temp come from the advertisement, not GATT,
  480. so they are not part of the get_basic_info result.
  481. """
  482. device = create_device_for_command_testing(
  483. RGBICWW_CEILING_LIGHT_INFO, light_strip.SwitchbotRgbicwwCeilingLight
  484. )
  485. device._send_command = AsyncMock(
  486. side_effect=[b"\x01\x01\n", b"\x01\x80NK\xff:\xa0\x19d\xff\x02"]
  487. )
  488. device._check_command_result = MagicMock(side_effect=[True, True])
  489. info = await device.get_basic_info()
  490. assert info["r"] == 75
  491. assert info["g"] == 255
  492. assert info["b"] == 58
  493. assert info["firmware"] == 1.0
  494. # Power state must not be sourced from GATT (advertisement is authoritative).
  495. assert "isOn" not in info
  496. assert "main_isOn" not in info