Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/switchbot_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 40 additions & 3 deletions homeassistant/components/switchbot_cloud/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

from switchbot_api import (
CeilingLightCommands,
CommonCommands,
Device,
Remote,
Expand Down Expand Up @@ -53,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:
Expand Down Expand Up @@ -83,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
Expand All @@ -93,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()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole _get_default_color_mode() is here to preserve the existing logic of updating the ColorMode here; but I am not sure why it's here in the first place.

Would just not updating the _attr_color_mode here (and in the brightness is not None case?) at all would be a better move? I don't fullly understand HASS internals to understand what the implications of that are, but it feels weird to always update this, even when only touching the brightness?

await self.send_api_command(CommonCommands.ON)
await asyncio.sleep(AFTER_COMMAND_REFRESH)
await self.coordinator.async_request_refresh()
Expand Down Expand Up @@ -149,11 +161,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,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These, arguably, could be the same commands as RGBWWLightCommands, since they use the same actual commands (here: https://github.com/SeraphicCorp/py-switchbot-api/blob/main/switchbot_api/commands.py#L294C30-L294C49).

But they're separate on the py-switchbot-api side, so I've left them distinct here too. Maybe we should unify that somehow?

parameters=str(color_temp_kelvin),
)

Comment on lines +164 to +185
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new SwitchBotCloudCeilingLight class lacks test coverage. Tests should be added to verify:

  1. Turn on/off functionality
  2. Brightness control using CeilingLightCommands.SET_BRIGHTNESS
  3. Color temperature control using CeilingLightCommands.SET_COLOR_TEMPERATURE
  4. Supported color modes (only COLOR_TEMP)
  5. Temperature range enforcement (2700-6500K)

Other light types in this integration have comprehensive tests (see test_strip_light_turn_on, test_rgbww_light_turn_on). Similar tests should be added for ceiling lights.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that can happen in this PR


Comment on lines +185 to +186
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base class async_turn_on method sets _attr_color_mode = ColorMode.RGB when adjusting brightness (line 88) or when turning on without parameters (line 97). However, SwitchBotCloudCeilingLight only supports ColorMode.COLOR_TEMP. This mismatch could cause the entity to report an incorrect color mode.

Consider overriding async_turn_on in SwitchBotCloudCeilingLight to set the correct color mode, or adjust the base class logic to check the supported color modes before setting the color mode.

Suggested change
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the ceiling light with the correct color mode."""
# Only COLOR_TEMP is supported
if (brightness := kwargs.get("brightness")) is not None:
await self._send_brightness_command(brightness)
if (color_temp := kwargs.get("color_temp_kelvin")) is not None:
await self._send_color_temperature_command(color_temp)
if not kwargs:
await self.send_api_command(CommonCommands.TURN_ON)
self._attr_is_on = True
self._attr_color_mode = ColorMode.COLOR_TEMP
await asyncio.sleep(AFTER_COMMAND_REFRESH)
await self.coordinator.async_request_refresh()

Copilot uses AI. Check for mistakes.
@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)
128 changes: 126 additions & 2 deletions tests/components/switchbot_cloud/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Loading