mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Refactor LIFX multizone devices to use extended messages (#79444)
This commit is contained in:
parent
f95b8ccc20
commit
205ce2bac5
@ -12,6 +12,7 @@ from aiolifx.connection import LIFXConnection
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@ -148,10 +149,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
if self.device.mac_addr == TARGET_ANY:
|
||||
self.device.mac_addr = response.target_addr
|
||||
|
||||
# Update model-specific configuration
|
||||
if lifx_features(self.device)["multizone"]:
|
||||
await self.async_update_color_zones()
|
||||
await self.async_update_multizone_effect()
|
||||
# Update extended multizone devices
|
||||
if lifx_features(self.device)["extended_multizone"]:
|
||||
await self.async_get_extended_color_zones()
|
||||
await self.async_get_multizone_effect()
|
||||
# use legacy methods for older devices
|
||||
elif lifx_features(self.device)["multizone"]:
|
||||
await self.async_get_color_zones()
|
||||
await self.async_get_multizone_effect()
|
||||
|
||||
if lifx_features(self.device)["hev"]:
|
||||
await self.async_get_hev_cycle()
|
||||
@ -159,7 +164,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
if lifx_features(self.device)["infrared"]:
|
||||
response = await async_execute_lifx(self.device.get_infrared)
|
||||
|
||||
async def async_update_color_zones(self) -> None:
|
||||
async def async_get_color_zones(self) -> None:
|
||||
"""Get updated color information for each zone."""
|
||||
zone = 0
|
||||
top = 1
|
||||
@ -175,6 +180,15 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
if zone == top - 1:
|
||||
zone -= 1
|
||||
|
||||
async def async_get_extended_color_zones(self) -> None:
|
||||
"""Get updated color information for all zones."""
|
||||
try:
|
||||
await async_execute_lifx(self.device.get_extended_color_zones)
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise HomeAssistantError(
|
||||
f"Timeout getting color zones from {self.name}"
|
||||
) from ex
|
||||
|
||||
def async_get_hev_cycle_state(self) -> bool | None:
|
||||
"""Return the current HEV cycle state."""
|
||||
if self.device.hev_cycle is None:
|
||||
@ -232,7 +246,34 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update_multizone_effect(self) -> None:
|
||||
async def async_set_extended_color_zones(
|
||||
self,
|
||||
colors: list[tuple[int | float, int | float, int | float, int | float]],
|
||||
colors_count: int | None = None,
|
||||
duration: int = 0,
|
||||
apply: int = 1,
|
||||
) -> None:
|
||||
"""Send a single set extended color zones message to the device."""
|
||||
|
||||
if colors_count is None:
|
||||
colors_count = len(colors)
|
||||
|
||||
# pad the color list with blanks if necessary
|
||||
if len(colors) < 82:
|
||||
for _ in range(82 - len(colors)):
|
||||
colors.append((0, 0, 0, 0))
|
||||
|
||||
await async_execute_lifx(
|
||||
partial(
|
||||
self.device.set_extended_color_zones,
|
||||
colors=colors,
|
||||
colors_count=colors_count,
|
||||
duration=duration,
|
||||
apply=apply,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_get_multizone_effect(self) -> None:
|
||||
"""Update the device firmware effect running state."""
|
||||
await async_execute_lifx(self.device.get_multizone_effect)
|
||||
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
|
||||
|
@ -95,8 +95,10 @@ async def async_setup_entry(
|
||||
LIFX_SET_HEV_CYCLE_STATE_SCHEMA,
|
||||
"set_hev_cycle_state",
|
||||
)
|
||||
if lifx_features(device)["multizone"]:
|
||||
entity: LIFXLight = LIFXStrip(coordinator, manager, entry)
|
||||
if lifx_features(device)["extended_multizone"]:
|
||||
entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry)
|
||||
elif lifx_features(device)["multizone"]:
|
||||
entity = LIFXMultiZone(coordinator, manager, entry)
|
||||
elif lifx_features(device)["color"]:
|
||||
entity = LIFXColor(coordinator, manager, entry)
|
||||
else:
|
||||
@ -362,8 +364,8 @@ class LIFXColor(LIFXLight):
|
||||
return (hue, sat) if sat else None
|
||||
|
||||
|
||||
class LIFXStrip(LIFXColor):
|
||||
"""Representation of a LIFX light strip with multiple zones."""
|
||||
class LIFXMultiZone(LIFXColor):
|
||||
"""Representation of a legacy LIFX multizone device."""
|
||||
|
||||
_attr_effect_list = [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
@ -426,16 +428,53 @@ class LIFXStrip(LIFXColor):
|
||||
) from ex
|
||||
|
||||
# set_color_zones does not update the
|
||||
# state of the bulb, so we need to do that
|
||||
# state of the device, so we need to do that
|
||||
await self.get_color()
|
||||
|
||||
async def update_color_zones(
|
||||
self,
|
||||
) -> None:
|
||||
"""Send a get color zones message to the bulb."""
|
||||
"""Send a get color zones message to the device."""
|
||||
try:
|
||||
await self.coordinator.async_update_color_zones()
|
||||
await self.coordinator.async_get_color_zones()
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise HomeAssistantError(
|
||||
f"Timeout setting updating color zones for {self.name}"
|
||||
f"Timeout getting color zones from {self.name}"
|
||||
) from ex
|
||||
|
||||
|
||||
class LIFXExtendedMultiZone(LIFXMultiZone):
|
||||
"""Representation of a LIFX device that supports extended multizone messages."""
|
||||
|
||||
async def set_color(
|
||||
self, hsbk: list[float | int | None], kwargs: dict[str, Any], duration: int = 0
|
||||
) -> None:
|
||||
"""Set colors on all zones of the device."""
|
||||
|
||||
# trigger an update of all zone values before merging new values
|
||||
await self.coordinator.async_get_extended_color_zones()
|
||||
|
||||
color_zones = self.bulb.color_zones
|
||||
if (zones := kwargs.get(ATTR_ZONES)) is None:
|
||||
# merge the incoming hsbk across all zones
|
||||
for index, zone in enumerate(color_zones):
|
||||
color_zones[index] = merge_hsbk(zone, hsbk)
|
||||
else:
|
||||
# merge the incoming HSBK with only the specified zones
|
||||
for index, zone in enumerate(color_zones):
|
||||
if index in zones:
|
||||
color_zones[index] = merge_hsbk(zone, hsbk)
|
||||
|
||||
# send the updated color zones list to the device
|
||||
try:
|
||||
await self.coordinator.async_set_extended_color_zones(
|
||||
color_zones, duration=duration
|
||||
)
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise HomeAssistantError(
|
||||
f"Timeout setting color zones on {self.name}"
|
||||
) from ex
|
||||
|
||||
# set_extended_color_zones does not update the
|
||||
# state of the device, so we need to do that
|
||||
await self.get_color()
|
||||
|
@ -151,7 +151,8 @@ def _mocked_light_strip() -> Light:
|
||||
bulb.set_color_zones = MockLifxCommand(bulb)
|
||||
bulb.get_multizone_effect = MockLifxCommand(bulb)
|
||||
bulb.set_multizone_effect = MockLifxCommand(bulb)
|
||||
|
||||
bulb.get_extended_color_zones = MockLifxCommand(bulb)
|
||||
bulb.set_extended_color_zones = MockLifxCommand(bulb)
|
||||
return bulb
|
||||
|
||||
|
||||
|
@ -412,6 +412,243 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_extended_multizone_messages(hass: HomeAssistant) -> None:
|
||||
"""Test a light strip that supports extended multizone."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_light_strip()
|
||||
bulb.product = 38 # LIFX Beam
|
||||
bulb.power_level = 65535
|
||||
bulb.color = [65535, 65535, 65535, 3500]
|
||||
bulb.color_zones = [(65535, 65535, 65535, 3500)] * 8
|
||||
bulb.zones_count = 8
|
||||
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||
device=bulb
|
||||
), _patch_device(device=bulb):
|
||||
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.HS
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [
|
||||
ColorMode.COLOR_TEMP,
|
||||
ColorMode.HS,
|
||||
]
|
||||
assert attributes[ATTR_HS_COLOR] == (360.0, 100.0)
|
||||
assert attributes[ATTR_RGB_COLOR] == (255, 0, 0)
|
||||
assert attributes[ATTR_XY_COLOR] == (0.701, 0.299)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert bulb.set_power.calls[0][0][0] is False
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert bulb.set_power.calls[0][0][0] is True
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
(0, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
(0, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
# always use a set_extended_color_zones
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
(0, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.3, 0.7)},
|
||||
blocking=True,
|
||||
)
|
||||
# Single color uses the fast path
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
(0, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# always use set_extended_color_zones
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_RGB_COLOR: (255, 255, 255),
|
||||
ATTR_ZONES: [0, 2],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
# set a two zones
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||
bulb.set_color.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.power_level = 0
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 255, 255), ATTR_ZONES: [3]},
|
||||
blocking=True,
|
||||
)
|
||||
# set a one zone
|
||||
assert len(bulb.set_power.calls) == 2
|
||||
assert len(bulb.get_color_zones.calls) == 0
|
||||
assert len(bulb.set_color.calls) == 0
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
|
||||
bulb.get_color_zones.reset_mock()
|
||||
bulb.set_power.reset_mock()
|
||||
bulb.set_color_zones.reset_mock()
|
||||
|
||||
bulb.set_extended_color_zones = MockFailingLifxCommand(bulb)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_RGB_COLOR: (255, 255, 255),
|
||||
ATTR_ZONES: [3],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
bulb.set_extended_color_zones = MockLifxCommand(bulb)
|
||||
bulb.get_extended_color_zones = MockFailingLifxCommand(bulb)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_state",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_RGB_COLOR: (255, 255, 255),
|
||||
ATTR_ZONES: [3],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
|
||||
"""Test the firmware move effect on a light strip."""
|
||||
config_entry = MockConfigEntry(
|
||||
|
Loading…
x
Reference in New Issue
Block a user