mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +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"
|
CONF_COAP_PORT: Final = "coap_port"
|
||||||
FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})")
|
FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})")
|
||||||
|
|
||||||
# max light transition time in milliseconds
|
# max BLOCK light transition time in milliseconds (min=0)
|
||||||
MAX_TRANSITION_TIME: Final = 5000
|
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 = (
|
RGBW_MODELS: Final = (
|
||||||
MODEL_BULB,
|
MODEL_BULB,
|
||||||
|
@ -24,14 +24,15 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
BLOCK_MAX_TRANSITION_TIME_MS,
|
||||||
DUAL_MODE_LIGHT_MODELS,
|
DUAL_MODE_LIGHT_MODELS,
|
||||||
KELVIN_MAX_VALUE,
|
KELVIN_MAX_VALUE,
|
||||||
KELVIN_MIN_VALUE_COLOR,
|
KELVIN_MIN_VALUE_COLOR,
|
||||||
KELVIN_MIN_VALUE_WHITE,
|
KELVIN_MIN_VALUE_WHITE,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
MAX_TRANSITION_TIME,
|
|
||||||
MODELS_SUPPORTING_LIGHT_TRANSITION,
|
MODELS_SUPPORTING_LIGHT_TRANSITION,
|
||||||
RGBW_MODELS,
|
RGBW_MODELS,
|
||||||
|
RPC_MIN_TRANSITION_TIME_SEC,
|
||||||
SHBLB_1_RGB_EFFECTS,
|
SHBLB_1_RGB_EFFECTS,
|
||||||
STANDARD_RGB_EFFECTS,
|
STANDARD_RGB_EFFECTS,
|
||||||
)
|
)
|
||||||
@ -116,9 +117,16 @@ def async_setup_rpc_entry(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
light_key_ids = get_rpc_key_ids(coordinator.device.status, "light")
|
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||||
if light_key_ids:
|
|
||||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
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):
|
class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
||||||
@ -280,7 +288,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
|||||||
|
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
params["transition"] = min(
|
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):
|
if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes):
|
||||||
@ -352,7 +360,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
|||||||
|
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
params["transition"] = min(
|
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)
|
self.control_result = await self.set_state(**params)
|
||||||
@ -366,40 +374,14 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
|
|||||||
super()._update_callback()
|
super()._update_callback()
|
||||||
|
|
||||||
|
|
||||||
class RpcShellySwitchAsLight(ShellyRpcEntity, LightEntity):
|
class RpcShellyLightBase(ShellyRpcEntity, LightEntity):
|
||||||
"""Entity that controls a relay as light on RPC based Shelly devices."""
|
"""Base Entity for RPC based Shelly devices."""
|
||||||
|
|
||||||
_attr_color_mode = ColorMode.ONOFF
|
_component: str = "Light"
|
||||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
|
||||||
|
|
||||||
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
|
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
|
||||||
"""Initialize light."""
|
"""Initialize light."""
|
||||||
super().__init__(coordinator, f"switch:{id_}")
|
super().__init__(coordinator, f"{self._component.lower()}:{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_}")
|
|
||||||
self._id = id_
|
self._id = id_
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -412,6 +394,16 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity):
|
|||||||
"""Return the brightness of this light between 0..255."""
|
"""Return the brightness of this light between 0..255."""
|
||||||
return percentage_to_brightness(self.status["brightness"])
|
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:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn on light."""
|
"""Turn on light."""
|
||||||
params: dict[str, Any] = {"id": self._id, "on": True}
|
params: dict[str, Any] = {"id": self._id, "on": True}
|
||||||
@ -419,8 +411,66 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity):
|
|||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
params["brightness"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS])
|
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:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off light."""
|
"""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:1": {"id": 1, "type": "analog", "enable": True},
|
||||||
"input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True},
|
"input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True},
|
||||||
"light:0": {"name": "test light_0"},
|
"light:0": {"name": "test light_0"},
|
||||||
|
"rgb:0": {"name": "test rgb_0"},
|
||||||
|
"rgbw:0": {"name": "test rgbw_0"},
|
||||||
"switch:0": {"name": "test switch_0"},
|
"switch:0": {"name": "test switch_0"},
|
||||||
"cover:0": {"name": "test cover_0"},
|
"cover:0": {"name": "test cover_0"},
|
||||||
"thermostat:0": {
|
"thermostat:0": {
|
||||||
@ -223,6 +225,8 @@ MOCK_STATUS_RPC = {
|
|||||||
"input:1": {"id": 1, "percent": 89, "xpercent": 8.9},
|
"input:1": {"id": 1, "percent": 89, "xpercent": 8.9},
|
||||||
"input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}},
|
"input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}},
|
||||||
"light:0": {"output": True, "brightness": 53.0},
|
"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},
|
"cloud": {"connected": False},
|
||||||
"cover:0": {
|
"cover:0": {
|
||||||
"state": "stopped",
|
"state": "stopped",
|
||||||
|
@ -539,6 +539,137 @@ async def test_rpc_light(
|
|||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
assert state.attributes[ATTR_BRIGHTNESS] == 33
|
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)
|
entry = entity_registry.async_get(entity_id)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123456789ABC-light:0"
|
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