mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +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.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
@ -148,10 +149,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
if self.device.mac_addr == TARGET_ANY:
|
if self.device.mac_addr == TARGET_ANY:
|
||||||
self.device.mac_addr = response.target_addr
|
self.device.mac_addr = response.target_addr
|
||||||
|
|
||||||
# Update model-specific configuration
|
# Update extended multizone devices
|
||||||
if lifx_features(self.device)["multizone"]:
|
if lifx_features(self.device)["extended_multizone"]:
|
||||||
await self.async_update_color_zones()
|
await self.async_get_extended_color_zones()
|
||||||
await self.async_update_multizone_effect()
|
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"]:
|
if lifx_features(self.device)["hev"]:
|
||||||
await self.async_get_hev_cycle()
|
await self.async_get_hev_cycle()
|
||||||
@ -159,7 +164,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
if lifx_features(self.device)["infrared"]:
|
if lifx_features(self.device)["infrared"]:
|
||||||
response = await async_execute_lifx(self.device.get_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."""
|
"""Get updated color information for each zone."""
|
||||||
zone = 0
|
zone = 0
|
||||||
top = 1
|
top = 1
|
||||||
@ -175,6 +180,15 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
if zone == top - 1:
|
if zone == top - 1:
|
||||||
zone -= 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:
|
def async_get_hev_cycle_state(self) -> bool | None:
|
||||||
"""Return the current HEV cycle state."""
|
"""Return the current HEV cycle state."""
|
||||||
if self.device.hev_cycle is None:
|
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."""
|
"""Update the device firmware effect running state."""
|
||||||
await async_execute_lifx(self.device.get_multizone_effect)
|
await async_execute_lifx(self.device.get_multizone_effect)
|
||||||
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
|
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,
|
LIFX_SET_HEV_CYCLE_STATE_SCHEMA,
|
||||||
"set_hev_cycle_state",
|
"set_hev_cycle_state",
|
||||||
)
|
)
|
||||||
if lifx_features(device)["multizone"]:
|
if lifx_features(device)["extended_multizone"]:
|
||||||
entity: LIFXLight = LIFXStrip(coordinator, manager, entry)
|
entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry)
|
||||||
|
elif lifx_features(device)["multizone"]:
|
||||||
|
entity = LIFXMultiZone(coordinator, manager, entry)
|
||||||
elif lifx_features(device)["color"]:
|
elif lifx_features(device)["color"]:
|
||||||
entity = LIFXColor(coordinator, manager, entry)
|
entity = LIFXColor(coordinator, manager, entry)
|
||||||
else:
|
else:
|
||||||
@ -362,8 +364,8 @@ class LIFXColor(LIFXLight):
|
|||||||
return (hue, sat) if sat else None
|
return (hue, sat) if sat else None
|
||||||
|
|
||||||
|
|
||||||
class LIFXStrip(LIFXColor):
|
class LIFXMultiZone(LIFXColor):
|
||||||
"""Representation of a LIFX light strip with multiple zones."""
|
"""Representation of a legacy LIFX multizone device."""
|
||||||
|
|
||||||
_attr_effect_list = [
|
_attr_effect_list = [
|
||||||
SERVICE_EFFECT_COLORLOOP,
|
SERVICE_EFFECT_COLORLOOP,
|
||||||
@ -426,16 +428,53 @@ class LIFXStrip(LIFXColor):
|
|||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
# set_color_zones does not update the
|
# 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()
|
await self.get_color()
|
||||||
|
|
||||||
async def update_color_zones(
|
async def update_color_zones(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a get color zones message to the bulb."""
|
"""Send a get color zones message to the device."""
|
||||||
try:
|
try:
|
||||||
await self.coordinator.async_update_color_zones()
|
await self.coordinator.async_get_color_zones()
|
||||||
except asyncio.TimeoutError as ex:
|
except asyncio.TimeoutError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Timeout setting updating color zones for {self.name}"
|
f"Timeout getting color zones from {self.name}"
|
||||||
) from ex
|
) 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.set_color_zones = MockLifxCommand(bulb)
|
||||||
bulb.get_multizone_effect = MockLifxCommand(bulb)
|
bulb.get_multizone_effect = MockLifxCommand(bulb)
|
||||||
bulb.set_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
|
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:
|
async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
|
||||||
"""Test the firmware move effect on a light strip."""
|
"""Test the firmware move effect on a light strip."""
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user