mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add Shelly RGB/RGBW profiles support (#113808)
* Add Shelly RGB/RGBW profiles support * Update homeassistant/components/shelly/light.py Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com> * Use walrus in rgbw_key_ids * Use walrus in light_key_ids --------- Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
parent
8728057b1b
commit
63275d61a5
@ -35,8 +35,11 @@ DATA_CONFIG_ENTRY: Final = "config_entry"
|
||||
CONF_COAP_PORT: Final = "coap_port"
|
||||
FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})")
|
||||
|
||||
# max light transition time in milliseconds
|
||||
MAX_TRANSITION_TIME: Final = 5000
|
||||
# max BLOCK light transition time in milliseconds (min=0)
|
||||
BLOCK_MAX_TRANSITION_TIME_MS: Final = 5000
|
||||
|
||||
# min RPC light transition time in seconds (max=10800, limited by light entity to 6553)
|
||||
RPC_MIN_TRANSITION_TIME_SEC = 0.5
|
||||
|
||||
RGBW_MODELS: Final = (
|
||||
MODEL_BULB,
|
||||
|
@ -24,14 +24,15 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
BLOCK_MAX_TRANSITION_TIME_MS,
|
||||
DUAL_MODE_LIGHT_MODELS,
|
||||
KELVIN_MAX_VALUE,
|
||||
KELVIN_MIN_VALUE_COLOR,
|
||||
KELVIN_MIN_VALUE_WHITE,
|
||||
LOGGER,
|
||||
MAX_TRANSITION_TIME,
|
||||
MODELS_SUPPORTING_LIGHT_TRANSITION,
|
||||
RGBW_MODELS,
|
||||
RPC_MIN_TRANSITION_TIME_SEC,
|
||||
SHBLB_1_RGB_EFFECTS,
|
||||
STANDARD_RGB_EFFECTS,
|
||||
)
|
||||
@ -116,9 +117,16 @@ def async_setup_rpc_entry(
|
||||
)
|
||||
return
|
||||
|
||||
light_key_ids = get_rpc_key_ids(coordinator.device.status, "light")
|
||||
if light_key_ids:
|
||||
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
||||
return
|
||||
|
||||
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
|
||||
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
|
||||
return
|
||||
|
||||
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
|
||||
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
|
||||
|
||||
|
||||
class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
||||
@ -280,7 +288,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
params["transition"] = min(
|
||||
int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME
|
||||
int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS
|
||||
)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes):
|
||||
@ -352,7 +360,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
params["transition"] = min(
|
||||
int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME
|
||||
int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS
|
||||
)
|
||||
|
||||
self.control_result = await self.set_state(**params)
|
||||
@ -366,40 +374,14 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
||||
super()._update_callback()
|
||||
|
||||
|
||||
class RpcShellySwitchAsLight(ShellyRpcEntity, LightEntity):
|
||||
"""Entity that controls a relay as light on RPC based Shelly devices."""
|
||||
class RpcShellyLightBase(ShellyRpcEntity, LightEntity):
|
||||
"""Base Entity for RPC based Shelly devices."""
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_component: str = "Light"
|
||||
|
||||
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
|
||||
"""Initialize light."""
|
||||
super().__init__(coordinator, f"switch:{id_}")
|
||||
self._id = id_
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""If light is on."""
|
||||
return bool(self.status["output"])
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on light."""
|
||||
await self.call_rpc("Switch.Set", {"id": self._id, "on": True})
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off light."""
|
||||
await self.call_rpc("Switch.Set", {"id": self._id, "on": False})
|
||||
|
||||
|
||||
class RpcShellyLight(ShellyRpcEntity, LightEntity):
|
||||
"""Entity that controls a light on RPC based Shelly devices."""
|
||||
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
|
||||
"""Initialize light."""
|
||||
super().__init__(coordinator, f"light:{id_}")
|
||||
super().__init__(coordinator, f"{self._component.lower()}:{id_}")
|
||||
self._id = id_
|
||||
|
||||
@property
|
||||
@ -412,6 +394,16 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return percentage_to_brightness(self.status["brightness"])
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int]:
|
||||
"""Return the rgb color value [int, int, int]."""
|
||||
return cast(tuple, self.status["rgb"])
|
||||
|
||||
@property
|
||||
def rgbw_color(self) -> tuple[int, int, int, int]:
|
||||
"""Return the rgbw color value [int, int, int, int]."""
|
||||
return (*self.status["rgb"], self.status["white"])
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on light."""
|
||||
params: dict[str, Any] = {"id": self._id, "on": True}
|
||||
@ -419,8 +411,66 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity):
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
params["brightness"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
await self.call_rpc("Light.Set", params)
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
params["transition_duration"] = max(
|
||||
kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC
|
||||
)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
params["rgb"] = list(kwargs[ATTR_RGB_COLOR])
|
||||
|
||||
if ATTR_RGBW_COLOR in kwargs:
|
||||
params["rgb"] = list(kwargs[ATTR_RGBW_COLOR][:-1])
|
||||
params["white"] = kwargs[ATTR_RGBW_COLOR][-1]
|
||||
|
||||
await self.call_rpc(f"{self._component}.Set", params)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off light."""
|
||||
await self.call_rpc("Light.Set", {"id": self._id, "on": False})
|
||||
params: dict[str, Any] = {"id": self._id, "on": False}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
params["transition_duration"] = max(
|
||||
kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC
|
||||
)
|
||||
|
||||
await self.call_rpc(f"{self._component}.Set", params)
|
||||
|
||||
|
||||
class RpcShellySwitchAsLight(RpcShellyLightBase):
|
||||
"""Entity that controls a relay as light on RPC based Shelly devices."""
|
||||
|
||||
_component = "Switch"
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
|
||||
class RpcShellyLight(RpcShellyLightBase):
|
||||
"""Entity that controls a light on RPC based Shelly devices."""
|
||||
|
||||
_component = "Light"
|
||||
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION
|
||||
|
||||
|
||||
class RpcShellyRgbLight(RpcShellyLightBase):
|
||||
"""Entity that controls a RGB light on RPC based Shelly devices."""
|
||||
|
||||
_component = "RGB"
|
||||
|
||||
_attr_color_mode = ColorMode.RGB
|
||||
_attr_supported_color_modes = {ColorMode.RGB}
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION
|
||||
|
||||
|
||||
class RpcShellyRgbwLight(RpcShellyLightBase):
|
||||
"""Entity that controls a RGBW light on RPC based Shelly devices."""
|
||||
|
||||
_component = "RGBW"
|
||||
|
||||
_attr_color_mode = ColorMode.RGBW
|
||||
_attr_supported_color_modes = {ColorMode.RGBW}
|
||||
_attr_supported_features = LightEntityFeature.TRANSITION
|
||||
|
@ -169,6 +169,8 @@ MOCK_CONFIG = {
|
||||
"input:1": {"id": 1, "type": "analog", "enable": True},
|
||||
"input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True},
|
||||
"light:0": {"name": "test light_0"},
|
||||
"rgb:0": {"name": "test rgb_0"},
|
||||
"rgbw:0": {"name": "test rgbw_0"},
|
||||
"switch:0": {"name": "test switch_0"},
|
||||
"cover:0": {"name": "test cover_0"},
|
||||
"thermostat:0": {
|
||||
@ -223,6 +225,8 @@ MOCK_STATUS_RPC = {
|
||||
"input:1": {"id": 1, "percent": 89, "xpercent": 8.9},
|
||||
"input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}},
|
||||
"light:0": {"output": True, "brightness": 53.0},
|
||||
"rgb:0": {"output": True, "brightness": 53.0, "rgb": [45, 55, 65]},
|
||||
"rgbw:0": {"output": True, "brightness": 53.0, "rgb": [21, 22, 23], "white": 120},
|
||||
"cloud": {"connected": False},
|
||||
"cover:0": {
|
||||
"state": "stopped",
|
||||
|
@ -539,6 +539,137 @@ async def test_rpc_light(
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 33
|
||||
|
||||
# Turn on, transition = 10.1
|
||||
mock_rpc_device.call_rpc.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 10.1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
mock_rpc_device.call_rpc.assert_called_once_with(
|
||||
"Light.Set", {"id": 0, "on": True, "transition_duration": 10.1}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# Turn off, transition = 0.4, should be limited to 0.5
|
||||
mock_rpc_device.call_rpc.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 0.4},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "light:0", "output", False)
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
mock_rpc_device.call_rpc.assert_called_once_with(
|
||||
"Light.Set", {"id": 0, "on": False, "transition_duration": 0.5}
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-light:0"
|
||||
|
||||
|
||||
async def test_rpc_device_rgb_profile(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test RPC device in RGB profile."""
|
||||
monkeypatch.delitem(mock_rpc_device.status, "light:0")
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgbw:0")
|
||||
entity_id = "light.test_rgb_0"
|
||||
await init_integration(hass, 2)
|
||||
|
||||
# Test initial
|
||||
state = hass.states.get(entity_id)
|
||||
attributes = state.attributes
|
||||
assert state.state == STATE_ON
|
||||
assert attributes[ATTR_RGB_COLOR] == (45, 55, 65)
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGB]
|
||||
assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION
|
||||
|
||||
# Turn on, RGB = [70, 80, 90]
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [70, 80, 90]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgb:0", "rgb", [70, 80, 90])
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
mock_rpc_device.call_rpc.assert_called_once_with(
|
||||
"RGB.Set", {"id": 0, "on": True, "rgb": [70, 80, 90]}
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
attributes = state.attributes
|
||||
assert state.state == STATE_ON
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.RGB
|
||||
assert attributes[ATTR_RGB_COLOR] == (70, 80, 90)
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-rgb:0"
|
||||
|
||||
|
||||
async def test_rpc_device_rgbw_profile(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test RPC device in RGBW profile."""
|
||||
monkeypatch.delitem(mock_rpc_device.status, "light:0")
|
||||
monkeypatch.delitem(mock_rpc_device.status, "rgb:0")
|
||||
entity_id = "light.test_rgbw_0"
|
||||
await init_integration(hass, 2)
|
||||
|
||||
# Test initial
|
||||
state = hass.states.get(entity_id)
|
||||
attributes = state.attributes
|
||||
assert state.state == STATE_ON
|
||||
assert attributes[ATTR_RGBW_COLOR] == (21, 22, 23, 120)
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBW]
|
||||
assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION
|
||||
|
||||
# Turn on, RGBW = [72, 82, 92, 128]
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: [72, 82, 92, 128]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mutate_rpc_device_status(
|
||||
monkeypatch, mock_rpc_device, "rgbw:0", "rgb", [72, 82, 92]
|
||||
)
|
||||
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbw:0", "white", 128)
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
mock_rpc_device.call_rpc.assert_called_once_with(
|
||||
"RGBW.Set", {"id": 0, "on": True, "rgb": [72, 82, 92], "white": 128}
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
attributes = state.attributes
|
||||
assert state.state == STATE_ON
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.RGBW
|
||||
assert attributes[ATTR_RGBW_COLOR] == (72, 82, 92, 128)
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-rgbw:0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user