From d55eabef956e83b3db6280959f05a5dfbcefd321 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 15 Dec 2025 17:50:55 +0900 Subject: [PATCH 1/3] add support for ceiling lights --- .../components/switchbot_cloud/__init__.py | 2 ++ .../components/switchbot_cloud/light.py | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index ff1d8b5e12062d..e25763bc89421b 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -276,6 +276,8 @@ async def make_device_data( "Color Bulb", "RGBICWW Floor Lamp", "RGBICWW Strip Light", + "Ceiling Light", + "Ceiling Light Pro", ]: coordinator = await coordinator_for_device( hass, entry, api, device, coordinators_by_id diff --git a/homeassistant/components/switchbot_cloud/light.py b/homeassistant/components/switchbot_cloud/light.py index 77062702831ae4..b1d6d8ead2311b 100644 --- a/homeassistant/components/switchbot_cloud/light.py +++ b/homeassistant/components/switchbot_cloud/light.py @@ -4,6 +4,7 @@ from typing import Any from switchbot_api import ( + CeilingLightCommands, CommonCommands, Device, Remote, @@ -149,11 +150,36 @@ async def _send_rgb_color_command(self, rgb_color: tuple) -> None: ) +class SwitchBotCloudCeilingLight(SwitchBotCloudLight): + """Representation of SwitchBot Ceiling Light.""" + + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2700 + + _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + + async def _send_brightness_command(self, brightness: int) -> None: + """Send a brightness command.""" + await self.send_api_command( + CeilingLightCommands.SET_BRIGHTNESS, + parameters=str(value_map_brightness(brightness)), + ) + + async def _send_color_temperature_command(self, color_temp_kelvin: int) -> None: + """Send a color temperature command.""" + await self.send_api_command( + CeilingLightCommands.SET_COLOR_TEMPERATURE, + parameters=str(color_temp_kelvin), + ) + + @callback def _async_make_entity( api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator -) -> SwitchBotCloudStripLight | SwitchBotCloudRGBWWLight: +) -> SwitchBotCloudStripLight | SwitchBotCloudRGBWWLight | SwitchBotCloudCeilingLight: """Make a SwitchBotCloudLight.""" if device.device_type == "Strip Light": return SwitchBotCloudStripLight(api, device, coordinator) + if device.device_type in ["Ceiling Light", "Ceiling Light Pro"]: + return SwitchBotCloudCeilingLight(api, device, coordinator) return SwitchBotCloudRGBWWLight(api, device, coordinator) From 18fbc0392acf3961e9cc0fbc4ea7e1120a22e935 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 22 Dec 2025 17:42:05 +0000 Subject: [PATCH 2/3] fix a bug where we would apply incorrect color mode to the ceiling lights --- homeassistant/components/switchbot_cloud/light.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/light.py b/homeassistant/components/switchbot_cloud/light.py index b1d6d8ead2311b..55296fd4349e57 100644 --- a/homeassistant/components/switchbot_cloud/light.py +++ b/homeassistant/components/switchbot_cloud/light.py @@ -54,6 +54,16 @@ class SwitchBotCloudLight(SwitchBotCloudEntity, LightEntity): _attr_color_mode = ColorMode.UNKNOWN + def _get_default_color_mode(self) -> ColorMode: + """Return the default color mode.""" + if not self.supported_color_modes: + return ColorMode.UNKNOWN + if ColorMode.RGB in self.supported_color_modes: + return ColorMode.RGB + if ColorMode.COLOR_TEMP in self.supported_color_modes: + return ColorMode.COLOR_TEMP + return ColorMode.UNKNOWN + def _set_attributes(self) -> None: """Set attributes from coordinator data.""" if self.coordinator.data is None: @@ -84,8 +94,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: brightness: int | None = kwargs.get("brightness") rgb_color: tuple[int, int, int] | None = kwargs.get("rgb_color") color_temp_kelvin: int | None = kwargs.get("color_temp_kelvin") + if brightness is not None: - self._attr_color_mode = ColorMode.RGB + self._attr_color_mode = self._get_default_color_mode() await self._send_brightness_command(brightness) elif rgb_color is not None: self._attr_color_mode = ColorMode.RGB @@ -94,7 +105,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: self._attr_color_mode = ColorMode.COLOR_TEMP await self._send_color_temperature_command(color_temp_kelvin) else: - self._attr_color_mode = ColorMode.RGB + self._attr_color_mode = self._get_default_color_mode() await self.send_api_command(CommonCommands.ON) await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() From 77d2a8f4dbb61bc17dd9dc18d91ba9e514e33916 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 22 Dec 2025 17:43:11 +0000 Subject: [PATCH 3/3] add tests for the ceiling light and the mode changes --- .../components/switchbot_cloud/test_light.py | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/components/switchbot_cloud/test_light.py b/tests/components/switchbot_cloud/test_light.py index e4f39c0d5306b1..c8e1b10f32f15e 100644 --- a/tests/components/switchbot_cloud/test_light.py +++ b/tests/components/switchbot_cloud/test_light.py @@ -2,9 +2,14 @@ from unittest.mock import patch -from switchbot_api import Device, SwitchBotAPI +import pytest +from switchbot_api import CeilingLightCommands, CommonCommands, Device, SwitchBotAPI -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + ATTR_COLOR_MODE, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, @@ -298,3 +303,122 @@ async def test_rgbww_light_turn_on( mock_send_command.assert_called() state = hass.states.get(entity_id) assert state.state is STATE_ON + + +@pytest.mark.parametrize("device_type", ["Ceiling Light", "Ceiling Light Pro"]) +async def test_ceiling_light_turn_on( + hass: HomeAssistant, mock_list_devices, mock_get_status, device_type +) -> None: + """Test ceiling light turn on.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType=device_type, + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "off", "brightness": 1, "colorTemperature": 4567}, + {"power": "on", "brightness": 10, "colorTemperature": 5555}, + {"power": "on", "brightness": 10, "colorTemperature": 5555}, + {"power": "on", "brightness": 10, "colorTemperature": 5555}, + {"power": "on", "brightness": 10, "colorTemperature": 5555}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_OFF + + # Test turn on with brightness + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "brightness": 99}, + blocking=True, + ) + mock_send_command.assert_called_with( + "light-id-1", + CeilingLightCommands.SET_BRIGHTNESS, + "command", + "38", + ) + state = hass.states.get(entity_id) + assert state.state is STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + + # Test turn on with color temp + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, "color_temp_kelvin": 3333}, + blocking=True, + ) + mock_send_command.assert_called_with( + "light-id-1", + CeilingLightCommands.SET_COLOR_TEMPERATURE, + "command", + "3333", + ) + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + # Test turn on without arguments + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_send_command.assert_called_with( + "light-id-1", + CommonCommands.ON, + "command", + "default", + ) + state = hass.states.get(entity_id) + assert state.state is STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + + +@pytest.mark.parametrize("device_type", ["Ceiling Light", "Ceiling Light Pro"]) +async def test_ceiling_light_turn_off( + hass: HomeAssistant, mock_list_devices, mock_get_status, device_type +) -> None: + """Test ceiling light turn off.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="light-id-1", + deviceName="light-1", + deviceType=device_type, + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.side_effect = [ + {"power": "on", "brightness": 1, "colorTemperature": 4567}, + {"power": "off", "brightness": 1, "colorTemperature": 4567}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "light.light_1" + state = hass.states.get(entity_id) + assert state.state is STATE_ON + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_with( + "light-id-1", + CommonCommands.OFF, + "command", + "default", + ) + state = hass.states.get(entity_id) + assert state.state is STATE_OFF