From 4076f8b94e742da743b52d40e443e3a5be11dfa2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:10:24 -0400 Subject: [PATCH] Fix ZHA lighting initial hue/saturation attribute read (#77727) * Handle the case of `current_hue` being `None` * WIP unit tests --- homeassistant/components/zha/light.py | 18 +++--- tests/components/zha/common.py | 21 +++++- tests/components/zha/test_light.py | 93 ++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index ea46c0c49f1..528833608b3 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -612,16 +612,18 @@ class Light(BaseLight, ZhaEntity): and self._color_channel.enhanced_current_hue is not None ): curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 - else: + elif self._color_channel.current_hue is not None: curr_hue = self._color_channel.current_hue * 254 / 360 - curr_saturation = self._color_channel.current_saturation - if curr_hue is not None and curr_saturation is not None: - self._attr_hs_color = ( - int(curr_hue), - int(curr_saturation * 2.54), - ) else: - self._attr_hs_color = (0, 0) + curr_hue = 0 + + if (curr_saturation := self._color_channel.current_saturation) is None: + curr_saturation = 0 + + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), + ) if self._color_channel.color_loop_supported: self._attr_supported_features |= light.LightEntityFeature.EFFECT diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index cad8020267f..56197fa39ec 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -2,12 +2,14 @@ import asyncio from datetime import timedelta import math -from unittest.mock import AsyncMock, Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const +from homeassistant.components.zha.core.helpers import async_get_zha_config_value from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util @@ -243,3 +245,20 @@ async def async_shift_time(hass): next_update = dt_util.utcnow() + timedelta(seconds=11) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): + """Patch the ZHA custom configuration defaults.""" + + def new_get_config(config_entry, section, config_key, default): + if (section, config_key) in overrides: + return overrides[section, config_key] + else: + return async_get_zha_config_value( + config_entry, section, config_key, default + ) + + return patch( + f"homeassistant.components.zha.{component}.async_get_zha_config_value", + side_effect=new_get_config, + ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 5f5e7ab2e38..018ac68a25f 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,6 +14,10 @@ from homeassistant.components.light import ( FLASH_SHORT, ColorMode, ) +from homeassistant.components.zha.core.const import ( + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + ZHA_OPTIONS, +) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -26,6 +30,7 @@ from .common import ( async_test_rejoin, find_entity_id, get_zha_gateway, + patch_zha_config, send_attributes_report, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -340,7 +345,11 @@ async def test_light( if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_SHORT) - # test turning the lights on and off from the HA + # test long flashing the lights from the HA + if cluster_identify: + await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + + # test dimming the lights on and off from the HA if cluster_level: await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id @@ -355,16 +364,82 @@ async def test_light( # test rejoin await async_test_off_from_hass(hass, cluster_on_off, entity_id) - clusters = [cluster_on_off] - if cluster_level: - clusters.append(cluster_level) - if cluster_color: - clusters.append(cluster_color) + clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] await async_test_rejoin(hass, zigpy_device, clusters, reporting) - # test long flashing the lights from the HA - if cluster_identify: - await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + +@pytest.mark.parametrize( + "plugged_attr_reads, config_override, expected_state", + [ + # HS light without cached hue or saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached hue + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with both + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + ], +) +async def test_light_initialization( + hass, + zigpy_device_mock, + zha_device_joined_restored, + plugged_attr_reads, + config_override, + expected_state, +): + """Test zha light initialization with cached attributes and color modes.""" + + # create zigpy devices + zigpy_device = zigpy_device_mock(LIGHT_COLOR) + + # mock attribute reads + zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads + + with patch_zha_config("light", config_override): + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) + + assert entity_id is not None + + # TODO ensure hue and saturation are properly set on startup @patch(