Skip to content

Commit a72fe28

Browse files
authored
Support Shelly RGBCCT lights (home-assistant#155197)
1 parent fa7ff1d commit a72fe28

File tree

3 files changed

+185
-3
lines changed

3 files changed

+185
-3
lines changed

homeassistant/components/shelly/light.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,11 @@ def rgbw_color(self) -> tuple[int, int, int, int]:
399399
"""Return the rgbw color value [int, int, int, int]."""
400400
return (*self.status["rgb"], self.status["white"])
401401

402+
@property
403+
def color_temp_kelvin(self) -> int:
404+
"""Return the CT color value in Kelvin."""
405+
return cast(int, self.status["ct"])
406+
402407
async def async_turn_on(self, **kwargs: Any) -> None:
403408
"""Turn on light."""
404409
params: dict[str, Any] = {"id": self._id, "on": True}
@@ -421,6 +426,12 @@ async def async_turn_on(self, **kwargs: Any) -> None:
421426
params["rgb"] = list(kwargs[ATTR_RGBW_COLOR][:-1])
422427
params["white"] = kwargs[ATTR_RGBW_COLOR][-1]
423428

429+
if self.status.get("mode") is not None:
430+
if ATTR_COLOR_TEMP_KELVIN in kwargs:
431+
params["mode"] = "cct"
432+
elif ATTR_RGB_COLOR in kwargs:
433+
params["mode"] = "rgb"
434+
424435
await self.call_rpc(f"{self._component}.Set", params)
425436

426437
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -479,10 +490,24 @@ def __init__(
479490
self._attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_WHITE
480491
self._attr_max_color_temp_kelvin = KELVIN_MAX_VALUE
481492

493+
494+
class RpcShellyRgbCctLight(RpcShellyLightBase):
495+
"""Entity that controls a RGBCCT light on RPC based Shelly devices."""
496+
497+
_component = "RGBCCT"
498+
499+
_attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.RGB}
500+
_attr_supported_features = LightEntityFeature.TRANSITION
501+
_attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_WHITE
502+
_attr_max_color_temp_kelvin = KELVIN_MAX_VALUE
503+
482504
@property
483-
def color_temp_kelvin(self) -> int:
484-
"""Return the CT color value in Kelvin."""
485-
return cast(int, self.status["ct"])
505+
def color_mode(self) -> ColorMode:
506+
"""Return the color mode."""
507+
if self.status["mode"] == "cct":
508+
return ColorMode.COLOR_TEMP
509+
510+
return ColorMode.RGB
486511

487512

488513
class RpcShellyRgbLight(RpcShellyLightBase):
@@ -529,6 +554,11 @@ class RpcShellyRgbwLight(RpcShellyLightBase):
529554
sub_key="output",
530555
entity_class=RpcShellyRgbLight,
531556
),
557+
"rgbcct": RpcEntityDescription(
558+
key="rgbcct",
559+
sub_key="output",
560+
entity_class=RpcShellyRgbCctLight,
561+
),
532562
"rgbw": RpcEntityDescription(
533563
key="rgbw",
534564
sub_key="output",

homeassistant/components/shelly/sensor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,14 @@ def __init__(
553553
device_class=SensorDeviceClass.POWER,
554554
state_class=SensorStateClass.MEASUREMENT,
555555
),
556+
"power_rgbcct": RpcSensorDescription(
557+
key="rgbcct",
558+
sub_key="apower",
559+
name="Power",
560+
native_unit_of_measurement=UnitOfPower.WATT,
561+
device_class=SensorDeviceClass.POWER,
562+
state_class=SensorStateClass.MEASUREMENT,
563+
),
556564
"a_act_power": RpcSensorDescription(
557565
key="em",
558566
sub_key="a_act_power",
@@ -1023,6 +1031,17 @@ def __init__(
10231031
device_class=SensorDeviceClass.ENERGY,
10241032
state_class=SensorStateClass.TOTAL_INCREASING,
10251033
),
1034+
"energy_rgbcct": RpcSensorDescription(
1035+
key="rgbcct",
1036+
sub_key="aenergy",
1037+
name="Energy",
1038+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
1039+
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
1040+
value=lambda status, _: status["total"],
1041+
suggested_display_precision=2,
1042+
device_class=SensorDeviceClass.ENERGY,
1043+
state_class=SensorStateClass.TOTAL_INCREASING,
1044+
),
10261045
"total_act": RpcSensorDescription(
10271046
key="emdata",
10281047
sub_key="total_act",

tests/components/shelly/test_light.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
MODEL_DIMMER,
1010
MODEL_DIMMER_2,
1111
MODEL_DUO,
12+
MODEL_MULTICOLOR_BULB_G3,
1213
MODEL_RGBW2,
1314
MODEL_VINTAGE_V2,
1415
)
@@ -962,3 +963,135 @@ async def test_rpc_cct_light_without_ct_range(
962963
# default values from constants are 2700 and 6500
963964
assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700
964965
assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500
966+
967+
968+
async def test_rpc_rgbcct_light(
969+
hass: HomeAssistant,
970+
mock_rpc_device: Mock,
971+
entity_registry: EntityRegistry,
972+
monkeypatch: pytest.MonkeyPatch,
973+
) -> None:
974+
"""Test RPC RGBCCT light."""
975+
entity_id = f"{LIGHT_DOMAIN}.test_name"
976+
977+
config = deepcopy(mock_rpc_device.config)
978+
config["rgbcct:0"] = {"id": 0, "name": None}
979+
monkeypatch.setattr(mock_rpc_device, "config", config)
980+
981+
status = deepcopy(mock_rpc_device.status)
982+
status["rgbcct:0"] = {
983+
"id": 0,
984+
"output": False,
985+
"brightness": 44,
986+
"ct": 3349,
987+
"rgb": [76, 140, 255],
988+
"mode": "cct",
989+
}
990+
monkeypatch.setattr(mock_rpc_device, "status", status)
991+
992+
await init_integration(hass, 3, MODEL_MULTICOLOR_BULB_G3)
993+
994+
assert (entry := entity_registry.async_get(entity_id))
995+
assert entry.unique_id == "123456789ABC-rgbcct:0"
996+
997+
# Turn off
998+
await hass.services.async_call(
999+
LIGHT_DOMAIN,
1000+
SERVICE_TURN_OFF,
1001+
{ATTR_ENTITY_ID: entity_id},
1002+
blocking=True,
1003+
)
1004+
1005+
mock_rpc_device.call_rpc.assert_called_once_with(
1006+
"RGBCCT.Set", {"id": 0, "on": False}
1007+
)
1008+
1009+
assert (state := hass.states.get(entity_id))
1010+
assert state.state == STATE_OFF
1011+
1012+
# Turn on
1013+
mock_rpc_device.call_rpc.reset_mock()
1014+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbcct:0", "output", True)
1015+
await hass.services.async_call(
1016+
LIGHT_DOMAIN,
1017+
SERVICE_TURN_ON,
1018+
{ATTR_ENTITY_ID: entity_id},
1019+
blocking=True,
1020+
)
1021+
1022+
mock_rpc_device.call_rpc.assert_called_once_with(
1023+
"RGBCCT.Set", {"id": 0, "on": True}
1024+
)
1025+
mock_rpc_device.mock_update()
1026+
1027+
assert (state := hass.states.get(entity_id))
1028+
assert state.state == STATE_ON
1029+
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
1030+
assert state.attributes[ATTR_BRIGHTNESS] == 112
1031+
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3349
1032+
assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700
1033+
assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500
1034+
1035+
# Turn on, brightness = 88
1036+
mock_rpc_device.call_rpc.reset_mock()
1037+
await hass.services.async_call(
1038+
LIGHT_DOMAIN,
1039+
SERVICE_TURN_ON,
1040+
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 88},
1041+
blocking=True,
1042+
)
1043+
1044+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbcct:0", "brightness", 88)
1045+
mock_rpc_device.mock_update()
1046+
1047+
mock_rpc_device.call_rpc.assert_called_once_with(
1048+
"RGBCCT.Set", {"id": 0, "on": True, "brightness": 88}
1049+
)
1050+
1051+
assert (state := hass.states.get(entity_id))
1052+
assert state.state == STATE_ON
1053+
assert state.attributes[ATTR_BRIGHTNESS] == 224 # 88% of 255
1054+
1055+
# Turn on, color temp = 4444 K
1056+
mock_rpc_device.call_rpc.reset_mock()
1057+
await hass.services.async_call(
1058+
LIGHT_DOMAIN,
1059+
SERVICE_TURN_ON,
1060+
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4444},
1061+
blocking=True,
1062+
)
1063+
1064+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbcct:0", "ct", 4444)
1065+
mock_rpc_device.mock_update()
1066+
1067+
mock_rpc_device.call_rpc.assert_called_once_with(
1068+
"RGBCCT.Set", {"id": 0, "on": True, "ct": 4444, "mode": "cct"}
1069+
)
1070+
1071+
assert (state := hass.states.get(entity_id))
1072+
assert state.state == STATE_ON
1073+
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 4444
1074+
1075+
# Turn on, color 100, 150, 200
1076+
mock_rpc_device.call_rpc.reset_mock()
1077+
await hass.services.async_call(
1078+
LIGHT_DOMAIN,
1079+
SERVICE_TURN_ON,
1080+
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [100, 150, 200]},
1081+
blocking=True,
1082+
)
1083+
1084+
mutate_rpc_device_status(
1085+
monkeypatch, mock_rpc_device, "rgbcct:0", "rgb", [100, 150, 200]
1086+
)
1087+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbcct:0", "mode", "rgb")
1088+
mock_rpc_device.mock_update()
1089+
1090+
mock_rpc_device.call_rpc.assert_called_once_with(
1091+
"RGBCCT.Set", {"id": 0, "on": True, "rgb": [100, 150, 200], "mode": "rgb"}
1092+
)
1093+
1094+
assert (state := hass.states.get(entity_id))
1095+
assert state.state == STATE_ON
1096+
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.RGB
1097+
assert state.attributes[ATTR_RGB_COLOR] == (100, 150, 200)

0 commit comments

Comments
 (0)