Refactor LIFX multizone devices to use extended messages (#79444)

This commit is contained in:
Avi Miller 2022-10-02 15:21:48 +11:00 committed by GitHub
parent f95b8ccc20
commit 205ce2bac5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 333 additions and 15 deletions

View File

@ -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")]

View File

@ -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()

View File

@ -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

View File

@ -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(