From e3e68dad36015539a80cd8434a16afc99a0a89ce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 2 Oct 2024 06:48:47 +0200 Subject: [PATCH] Revert "Support Z-Wave JS dimming lights using color intensity (#122639)" (#127256) This reverts commit c7cfd56b720be8212af2686ecfa5b8cad6ee299b. --- .../components/zwave_js/discovery.py | 55 +- homeassistant/components/zwave_js/light.py | 281 +++----- tests/components/zwave_js/test_light.py | 618 +++++------------- 3 files changed, 285 insertions(+), 669 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 63f91d5b83d0a..cff0eb434e0b0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -238,12 +238,6 @@ class ZWaveDiscoverySchema: command_class={CommandClass.SWITCH_BINARY}, property={CURRENT_VALUE_PROPERTY} ) -COLOR_SWITCH_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_COLOR}, - property={CURRENT_COLOR_PROPERTY}, - property_key={None}, -) - SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SOUND_SWITCH}, property={TONE_ID_PROPERTY}, @@ -768,6 +762,33 @@ class ZWaveDiscoverySchema: }, ), ), + # HomeSeer HSM-200 v1 + ZWaveDiscoverySchema( + platform=Platform.LIGHT, + hint="black_is_off", + manufacturer_id={0x001E}, + product_id={0x0001}, + product_type={0x0004}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_COLOR}, + property={CURRENT_COLOR_PROPERTY}, + property_key={None}, + ), + absent_values=[SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA], + ), + # Logic Group ZDB5100 + ZWaveDiscoverySchema( + platform=Platform.LIGHT, + hint="black_is_off", + manufacturer_id={0x0234}, + product_id={0x0121}, + product_type={0x0003}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_COLOR}, + property={CURRENT_COLOR_PROPERTY}, + property_key={None}, + ), + ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks # Door Lock CC @@ -969,11 +990,10 @@ class ZWaveDiscoverySchema: ), entity_category=EntityCategory.CONFIG, ), - # binary switches without color support + # binary switches ZWaveDiscoverySchema( platform=Platform.SWITCH, primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, - absent_values=[COLOR_SWITCH_CURRENT_VALUE_SCHEMA], ), # switch for Indicator CC ZWaveDiscoverySchema( @@ -1067,25 +1087,6 @@ class ZWaveDiscoverySchema: # catch any device with multilevel CC as light # NOTE: keep this at the bottom of the discovery scheme, # to handle all others that need the multilevel CC first - # - # Colored light (legacy device) that can only be controlled through Color Switch CC. - ZWaveDiscoverySchema( - platform=Platform.LIGHT, - hint="color_onoff", - primary_value=COLOR_SWITCH_CURRENT_VALUE_SCHEMA, - absent_values=[ - SWITCH_BINARY_CURRENT_VALUE_SCHEMA, - SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - ], - ), - # Colored light that can be turned on or off with the Binary Switch CC. - ZWaveDiscoverySchema( - platform=Platform.LIGHT, - hint="color_onoff", - primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, - required_values=[COLOR_SWITCH_CURRENT_VALUE_SCHEMA], - ), - # Dimmable light with or without color support. ZWaveDiscoverySchema( platform=Platform.LIGHT, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 4a044ca3f52aa..020f1b66b3d10 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -76,8 +76,8 @@ def async_add_light(info: ZwaveDiscoveryInfo) -> None: driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - if info.platform_hint == "color_onoff": - async_add_entities([ZwaveColorOnOffLight(config_entry, driver, info)]) + if info.platform_hint == "black_is_off": + async_add_entities([ZwaveBlackIsOffLight(config_entry, driver, info)]) else: async_add_entities([ZwaveLight(config_entry, driver, info)]) @@ -111,10 +111,9 @@ def __init__( self._supports_color = False self._supports_rgbw = False self._supports_color_temp = False - self._supports_dimming = False - self._color_mode: str | None = None self._hs_color: tuple[float, float] | None = None self._rgbw_color: tuple[int, int, int, int] | None = None + self._color_mode: str | None = None self._color_temp: int | None = None self._min_mireds = 153 # 6500K as a safe default self._max_mireds = 370 # 2700K as a safe default @@ -130,28 +129,15 @@ def __init__( ) self._supported_color_modes: set[ColorMode] = set() - self._target_brightness: Value | None = None - # get additional (optional) values and set features - if self.info.primary_value.command_class == CommandClass.SWITCH_BINARY: - # This light can not be dimmed separately from the color channels - self._target_brightness = self.get_zwave_value( - TARGET_VALUE_PROPERTY, - CommandClass.SWITCH_BINARY, - add_to_watched_value_ids=False, - ) - self._supports_dimming = False - elif self.info.primary_value.command_class == CommandClass.SWITCH_MULTILEVEL: - # This light can be dimmed separately from the color channels - self._target_brightness = self.get_zwave_value( - TARGET_VALUE_PROPERTY, - CommandClass.SWITCH_MULTILEVEL, - add_to_watched_value_ids=False, - ) - self._supports_dimming = True - elif self.info.primary_value.command_class == CommandClass.BASIC: - # If the command class is Basic, we must generate a name that includes - # the command class name to avoid ambiguity + # If the command class is Basic, we must geenerate a name that includes + # the command class name to avoid ambiguity + self._target_brightness = self.get_zwave_value( + TARGET_VALUE_PROPERTY, + CommandClass.SWITCH_MULTILEVEL, + add_to_watched_value_ids=False, + ) + if self.info.primary_value.command_class == CommandClass.BASIC: self._attr_name = self.generate_name( include_value_name=True, alternate_value_name="Basic" ) @@ -160,13 +146,6 @@ def __init__( CommandClass.BASIC, add_to_watched_value_ids=False, ) - self._supports_dimming = True - - self._current_color = self.get_zwave_value( - CURRENT_COLOR_PROPERTY, - CommandClass.SWITCH_COLOR, - value_property_key=None, - ) self._target_color = self.get_zwave_value( TARGET_COLOR_PROPERTY, CommandClass.SWITCH_COLOR, @@ -237,7 +216,7 @@ def hs_color(self) -> tuple[float, float] | None: @property def rgbw_color(self) -> tuple[int, int, int, int] | None: - """Return the RGBW color.""" + """Return the hs color.""" return self._rgbw_color @property @@ -264,39 +243,11 @@ async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = kwargs.get(ATTR_TRANSITION) - brightness = kwargs.get(ATTR_BRIGHTNESS) - - hs_color = kwargs.get(ATTR_HS_COLOR) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - rgbw = kwargs.get(ATTR_RGBW_COLOR) - - new_colors = self._get_new_colors(hs_color, color_temp, rgbw) - if new_colors is not None: - await self._async_set_colors(new_colors, transition) - - # set brightness (or turn on if dimming is not supported) - await self._async_set_brightness(brightness, transition) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the light off.""" - await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) - - def _get_new_colors( - self, - hs_color: tuple[float, float] | None, - color_temp: int | None, - rgbw: tuple[int, int, int, int] | None, - brightness_scale: float | None = None, - ) -> dict[ColorComponent, int] | None: - """Determine the new color dict to set.""" # RGB/HS color + hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color is not None and self._supports_color: red, green, blue = color_util.color_hs_to_RGB(*hs_color) - if brightness_scale is not None: - red = round(red * brightness_scale) - green = round(green * brightness_scale) - blue = round(blue * brightness_scale) colors = { ColorComponent.RED: red, ColorComponent.GREEN: green, @@ -306,9 +257,10 @@ def _get_new_colors( # turn of white leds when setting rgb colors[ColorComponent.WARM_WHITE] = 0 colors[ColorComponent.COLD_WHITE] = 0 - return colors + await self._async_set_colors(colors, transition) # Color temperature + color_temp = kwargs.get(ATTR_COLOR_TEMP) if color_temp is not None and self._supports_color_temp: # Limit color temp to min/max values cold = max( @@ -323,18 +275,20 @@ def _get_new_colors( ), ) warm = 255 - cold - colors = { - ColorComponent.WARM_WHITE: warm, - ColorComponent.COLD_WHITE: cold, - } - if self._supports_color: - # turn off color leds when setting color temperature - colors[ColorComponent.RED] = 0 - colors[ColorComponent.GREEN] = 0 - colors[ColorComponent.BLUE] = 0 - return colors + await self._async_set_colors( + { + # turn off color leds when setting color temperature + ColorComponent.RED: 0, + ColorComponent.GREEN: 0, + ColorComponent.BLUE: 0, + ColorComponent.WARM_WHITE: warm, + ColorComponent.COLD_WHITE: cold, + }, + transition, + ) # RGBW + rgbw = kwargs.get(ATTR_RGBW_COLOR) if rgbw is not None and self._supports_rgbw: rgbw_channels = { ColorComponent.RED: rgbw[0], @@ -346,15 +300,17 @@ def _get_new_colors( if self._cold_white: rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3] + await self._async_set_colors(rgbw_channels, transition) - return rgbw_channels + # set brightness + await self._async_set_brightness(kwargs.get(ATTR_BRIGHTNESS), transition) - return None + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) async def _async_set_colors( - self, - colors: dict[ColorComponent, int], - transition: float | None = None, + self, colors: dict[ColorComponent, int], transition: float | None = None ) -> None: """Set (multiple) defined colors to given value(s).""" # prefer the (new) combined color property @@ -405,14 +361,9 @@ async def _async_set_brightness( zwave_transition = {TRANSITION_DURATION_OPTION: "default"} # setting a value requires setting targetValue - if self._supports_dimming: - await self._async_set_value( - self._target_brightness, zwave_brightness, zwave_transition - ) - else: - await self._async_set_value( - self._target_brightness, zwave_brightness > 0, zwave_transition - ) + await self._async_set_value( + self._target_brightness, zwave_brightness, zwave_transition + ) # We do an optimistic state update when setting to a previous value # to avoid waiting for the value to be updated from the device which is # typically delayed and causes a confusing UX. @@ -476,8 +427,15 @@ def _calculate_color_values(self) -> None: """Calculate light colors.""" (red_val, green_val, blue_val, ww_val, cw_val) = self._get_color_values() - if self._current_color and isinstance(self._current_color.value, dict): - multi_color = self._current_color.value + # prefer the (new) combined color property + # https://github.com/zwave-js/node-zwave-js/pull/1782 + combined_color_val = self.get_zwave_value( + CURRENT_COLOR_PROPERTY, + CommandClass.SWITCH_COLOR, + value_property_key=None, + ) + if combined_color_val and isinstance(combined_color_val.value, dict): + multi_color = combined_color_val.value else: multi_color = {} @@ -528,10 +486,11 @@ def _calculate_color_values(self) -> None: self._color_mode = ColorMode.RGBW -class ZwaveColorOnOffLight(ZwaveLight): - """Representation of a colored Z-Wave light with an optional binary switch to turn on/off. +class ZwaveBlackIsOffLight(ZwaveLight): + """Representation of a Z-Wave light where setting the color to black turns it off. - Dimming for RGB lights is realized by scaling the color channels. + Currently only supports lights with RGB, no color temperature, and no white + channels. """ def __init__( @@ -540,137 +499,61 @@ def __init__( """Initialize the light.""" super().__init__(config_entry, driver, info) - self._last_on_color: dict[ColorComponent, int] | None = None - self._last_brightness: int | None = None + self._last_color: dict[str, int] | None = None + self._supported_color_modes.discard(ColorMode.BRIGHTNESS) @property - def brightness(self) -> int | None: - """Return the brightness of this light between 0..255. + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return 255 - Z-Wave multilevel switches use a range of [0, 99] to control brightness. - """ + @property + def is_on(self) -> bool | None: + """Return true if device is on (brightness above 0).""" if self.info.primary_value.value is None: return None - if self._target_brightness and self.info.primary_value.value is False: - # Binary switch exists and is turned off - return 0 - - # Brightness is encoded in the color channels by scaling them lower than 255 - color_values = [ - v.value - for v in self._get_color_values() - if v is not None and v.value is not None - ] - return max(color_values) if color_values else 0 + return any(value != 0 for value in self.info.primary_value.value.values()) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - if ( kwargs.get(ATTR_RGBW_COLOR) is not None or kwargs.get(ATTR_COLOR_TEMP) is not None + or kwargs.get(ATTR_HS_COLOR) is not None ): - # RGBW and color temp are not supported in this mode, - # delegate to the parent class await super().async_turn_on(**kwargs) return transition = kwargs.get(ATTR_TRANSITION) - brightness = kwargs.get(ATTR_BRIGHTNESS) - hs_color = kwargs.get(ATTR_HS_COLOR) - new_colors: dict[ColorComponent, int] | None = None - scale: float | None = None - - if brightness is None and hs_color is None: - # Turned on without specifying brightness or color - if self._last_on_color is not None: - if self._target_brightness: - # Color is already set, use the binary switch to turn on - await self._async_set_brightness(None, transition) - return - - # Preserve the previous color - new_colors = self._last_on_color - elif self._supports_color: - # Turned on for the first time. Make it white - new_colors = { + # turn on light to last color if known, otherwise set to white + if self._last_color is not None: + await self._async_set_colors( + { + ColorComponent.RED: self._last_color["red"], + ColorComponent.GREEN: self._last_color["green"], + ColorComponent.BLUE: self._last_color["blue"], + }, + transition, + ) + else: + await self._async_set_colors( + { ColorComponent.RED: 255, ColorComponent.GREEN: 255, ColorComponent.BLUE: 255, - } - elif brightness is not None: - # If brightness gets set, preserve the color and mix it with the new brightness - if self.color_mode == ColorMode.HS: - scale = brightness / 255 - if ( - self._last_on_color is not None - and None not in self._last_on_color.values() - ): - # Changed brightness from 0 to >0 - old_brightness = max(self._last_on_color.values()) - new_scale = brightness / old_brightness - scale = new_scale - new_colors = {} - for color, value in self._last_on_color.items(): - new_colors[color] = round(value * new_scale) - elif hs_color is None and self._color_mode == ColorMode.HS: - hs_color = self._hs_color - elif hs_color is not None and brightness is None: - # Turned on by using the color controls - current_brightness = self.brightness - if current_brightness == 0 and self._last_brightness is not None: - # Use the last brightness value if the light is currently off - scale = self._last_brightness / 255 - elif current_brightness is not None: - scale = current_brightness / 255 - - # Reset last color until turning off again - self._last_on_color = None - - if new_colors is None: - new_colors = self._get_new_colors( - hs_color=hs_color, color_temp=None, rgbw=None, brightness_scale=scale + }, + transition, ) - if new_colors is not None: - await self._async_set_colors(new_colors, transition) - - # Turn the binary switch on if there is one - await self._async_set_brightness(brightness, transition) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - - # Remember last color and brightness to restore it when turning on - self._last_brightness = self.brightness - if self._current_color and isinstance(self._current_color.value, dict): - red = self._current_color.value.get(COLOR_SWITCH_COMBINED_RED) - green = self._current_color.value.get(COLOR_SWITCH_COMBINED_GREEN) - blue = self._current_color.value.get(COLOR_SWITCH_COMBINED_BLUE) - - last_color: dict[ColorComponent, int] = {} - if red is not None: - last_color[ColorComponent.RED] = red - if green is not None: - last_color[ColorComponent.GREEN] = green - if blue is not None: - last_color[ColorComponent.BLUE] = blue - - if last_color: - self._last_on_color = last_color - - if self._target_brightness: - # Turn off the binary switch only - await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) - else: - # turn off all color channels - colors = { + self._last_color = self.info.primary_value.value + await self._async_set_colors( + { ColorComponent.RED: 0, ColorComponent.GREEN: 0, ColorComponent.BLUE: 0, - } - - await self._async_set_colors( - colors, - kwargs.get(ATTR_TRANSITION), - ) + }, + kwargs.get(ATTR_TRANSITION), + ) + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 4c725c6dc291b..376bd700a2a1b 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -8,7 +8,6 @@ ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, @@ -38,8 +37,8 @@ ZEN_31_ENTITY, ) -ZDB5100_ENTITY = "light.matrix_office" HSM200_V1_ENTITY = "light.hsm200" +ZDB5100_ENTITY = "light.matrix_office" async def test_light( @@ -511,388 +510,14 @@ async def test_light_none_color_value( assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] -async def test_light_on_off_color( - hass: HomeAssistant, client, logic_group_zdb5100, integration -) -> None: - """Test the light entity for RGB lights without dimming support.""" - node = logic_group_zdb5100 - state = hass.states.get(ZDB5100_ENTITY) - assert state.state == STATE_OFF - - async def update_color(red: int, green: int, blue: int) -> None: - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 1, - "property": "currentColor", - "propertyKey": 2, # red - "newValue": red, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "red", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 1, - "property": "currentColor", - "propertyKey": 3, # green - "newValue": green, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "green", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 1, - "property": "currentColor", - "propertyKey": 4, # blue - "newValue": blue, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "blue", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 1, - "property": "currentColor", - "newValue": { - "red": red, - "green": green, - "blue": blue, - }, - "prevValue": None, - "propertyName": "currentColor", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - async def update_switch_state(state: bool) -> None: - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Binary Switch", - "commandClass": 37, - "endpoint": 1, - "property": "currentValue", - "newValue": state, - "prevValue": None, - "propertyName": "currentValue", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - # Turn on the light. Since this is the first call, the light should default to white - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ZDB5100_ENTITY}, - blocking=True, - ) - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 51, - "endpoint": 1, - "property": "targetColor", - } - assert args["value"] == { - "red": 255, - "green": 255, - "blue": 255, - } - - args = client.async_send_command.call_args_list[1][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 37, - "endpoint": 1, - "property": "targetValue", - } - assert args["value"] is True - - # Force the light to turn off - await update_switch_state(False) - - state = hass.states.get(ZDB5100_ENTITY) - assert state.state == STATE_OFF - - # Force the light to turn on (green) - await update_color(0, 255, 0) - await update_switch_state(True) - - state = hass.states.get(ZDB5100_ENTITY) - assert state.state == STATE_ON - - client.async_send_command.reset_mock() - - # Set the brightness to 128. This should be encoded in the color value - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ZDB5100_ENTITY, ATTR_BRIGHTNESS: 128}, - blocking=True, - ) - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 51, - "endpoint": 1, - "property": "targetColor", - } - assert args["value"] == { - "red": 0, - "green": 128, - "blue": 0, - } - - args = client.async_send_command.call_args_list[1][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 37, - "endpoint": 1, - "property": "targetValue", - } - assert args["value"] is True - - client.async_send_command.reset_mock() - - # Force the light to turn on (green, 50%) - await update_color(0, 128, 0) - - # Set the color to red. This should preserve the previous brightness value - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ZDB5100_ENTITY, ATTR_HS_COLOR: (0, 100)}, - blocking=True, - ) - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 51, - "endpoint": 1, - "property": "targetColor", - } - assert args["value"] == { - "red": 128, - "green": 0, - "blue": 0, - } - - args = client.async_send_command.call_args_list[1][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 37, - "endpoint": 1, - "property": "targetValue", - } - assert args["value"] is True - - client.async_send_command.reset_mock() - - # Force the light to turn on (red, 50%) - await update_color(128, 0, 0) - - # Turn the device off. This should only affect the binary switch, not the color - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ZDB5100_ENTITY}, - blocking=True, - ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 37, - "endpoint": 1, - "property": "targetValue", - } - assert args["value"] is False - - client.async_send_command.reset_mock() - - # Force the light to turn off - await update_switch_state(False) - - # Turn the device on again. This should only affect the binary switch, not the color - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ZDB5100_ENTITY}, - blocking=True, - ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == node.node_id - assert args["valueId"] == { - "commandClass": 37, - "endpoint": 1, - "property": "targetValue", - } - assert args["value"] is True - - -async def test_light_color_only( +async def test_black_is_off( hass: HomeAssistant, client, express_controls_ezmultipli, integration ) -> None: - """Test the light entity for RGB lights with Color Switch CC only.""" + """Test the black is off light entity.""" node = express_controls_ezmultipli state = hass.states.get(HSM200_V1_ENTITY) assert state.state == STATE_ON - async def update_color(red: int, green: int, blue: int) -> None: - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 0, - "property": "currentColor", - "propertyKey": 2, # red - "newValue": red, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "red", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 0, - "property": "currentColor", - "propertyKey": 3, # green - "newValue": green, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "green", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 0, - "property": "currentColor", - "propertyKey": 4, # blue - "newValue": blue, - "prevValue": None, - "propertyName": "currentColor", - "propertyKeyName": "blue", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": node.node_id, - "args": { - "commandClassName": "Color Switch", - "commandClass": 51, - "endpoint": 0, - "property": "currentColor", - "newValue": { - "red": red, - "green": green, - "blue": blue, - }, - "prevValue": None, - "propertyName": "currentColor", - }, - }, - ) - node.receive_event(event) - await hass.async_block_till_done() - # Attempt to turn on the light and ensure it defaults to white await hass.services.async_call( LIGHT_DOMAIN, @@ -914,14 +539,64 @@ async def update_color(red: int, green: int, blue: int) -> None: client.async_send_command.reset_mock() # Force the light to turn off - await update_color(0, 0, 0) - + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() state = hass.states.get(HSM200_V1_ENTITY) assert state.state == STATE_OFF - # Force the light to turn on (50% green) - await update_color(0, 128, 0) - + # Force the light to turn on + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() state = hass.states.get(HSM200_V1_ENTITY) assert state.state == STATE_ON @@ -944,9 +619,6 @@ async def update_color(red: int, green: int, blue: int) -> None: client.async_send_command.reset_mock() - # Force the light to turn off - await update_color(0, 0, 0) - # Assert that the last color is restored await hass.services.async_call( LIGHT_DOMAIN, @@ -963,23 +635,44 @@ async def update_color(red: int, green: int, blue: int) -> None: "endpoint": 0, "property": "targetColor", } - assert args["value"] == {"red": 0, "green": 128, "blue": 0} + assert args["value"] == {"red": 0, "green": 255, "blue": 0} client.async_send_command.reset_mock() - # Force the light to turn on (50% green) - await update_color(0, 128, 0) - + # Force the light to turn on + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": None, + "prevValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() state = hass.states.get(HSM200_V1_ENTITY) - assert state.state == STATE_ON + assert state.state == STATE_UNKNOWN client.async_send_command.reset_mock() - # Assert that the brightness is preserved when changing colors + # Assert that call fails if attribute is added to service call await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_RGB_COLOR: (255, 0, 0)}, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_RGBW_COLOR: (255, 76, 255, 0)}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -991,21 +684,22 @@ async def update_color(red: int, green: int, blue: int) -> None: "endpoint": 0, "property": "targetColor", } - assert args["value"] == {"red": 128, "green": 0, "blue": 0} - - client.async_send_command.reset_mock() + assert args["value"] == {"red": 255, "green": 76, "blue": 255} - # Force the light to turn on (50% red) - await update_color(128, 0, 0) - state = hass.states.get(HSM200_V1_ENTITY) - assert state.state == STATE_ON +async def test_black_is_off_zdb5100( + hass: HomeAssistant, client, logic_group_zdb5100, integration +) -> None: + """Test the black is off light entity.""" + node = logic_group_zdb5100 + state = hass.states.get(ZDB5100_ENTITY) + assert state.state == STATE_OFF - # Assert that the color is preserved when changing brightness + # Attempt to turn on the light and ensure it defaults to white await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_BRIGHTNESS: 69}, + {ATTR_ENTITY_ID: ZDB5100_ENTITY}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -1014,31 +708,79 @@ async def update_color(red: int, green: int, blue: int) -> None: assert args["nodeId"] == node.node_id assert args["valueId"] == { "commandClass": 51, - "endpoint": 0, + "endpoint": 1, "property": "targetColor", } - assert args["value"] == {"red": 69, "green": 0, "blue": 0} + assert args["value"] == {"red": 255, "green": 255, "blue": 255} client.async_send_command.reset_mock() - await update_color(69, 0, 0) - - # Turn off again - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, - blocking=True, + # Force the light to turn off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 1, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, ) - await update_color(0, 0, 0) + node.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get(ZDB5100_ENTITY) + assert state.state == STATE_OFF - client.async_send_command.reset_mock() + # Force the light to turn on + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 1, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get(ZDB5100_ENTITY) + assert state.state == STATE_ON - # Assert that the color is preserved when turning on with brightness await hass.services.async_call( LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_BRIGHTNESS: 123}, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ZDB5100_ENTITY}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -1047,31 +789,18 @@ async def update_color(red: int, green: int, blue: int) -> None: assert args["nodeId"] == node.node_id assert args["valueId"] == { "commandClass": 51, - "endpoint": 0, + "endpoint": 1, "property": "targetColor", } - assert args["value"] == {"red": 123, "green": 0, "blue": 0} - - client.async_send_command.reset_mock() - - await update_color(123, 0, 0) - - # Turn off again - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, - blocking=True, - ) - await update_color(0, 0, 0) + assert args["value"] == {"red": 0, "green": 0, "blue": 0} client.async_send_command.reset_mock() - # Assert that the brightness is preserved when turning on with color + # Assert that the last color is restored await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_HS_COLOR: (240, 100)}, + {ATTR_ENTITY_ID: ZDB5100_ENTITY}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -1080,14 +809,14 @@ async def update_color(red: int, green: int, blue: int) -> None: assert args["nodeId"] == node.node_id assert args["valueId"] == { "commandClass": 51, - "endpoint": 0, + "endpoint": 1, "property": "targetColor", } - assert args["value"] == {"red": 0, "green": 0, "blue": 123} + assert args["value"] == {"red": 0, "green": 255, "blue": 0} client.async_send_command.reset_mock() - # Clear the color value to trigger an unknown state + # Force the light to turn on event = Event( type="value updated", data={ @@ -1097,18 +826,21 @@ async def update_color(red: int, green: int, blue: int) -> None: "args": { "commandClassName": "Color Switch", "commandClass": 51, - "endpoint": 0, + "endpoint": 1, "property": "currentColor", "newValue": None, - "prevValue": None, + "prevValue": { + "red": 0, + "green": 255, + "blue": 0, + }, "propertyName": "currentColor", }, }, ) node.receive_event(event) await hass.async_block_till_done() - - state = hass.states.get(HSM200_V1_ENTITY) + state = hass.states.get(ZDB5100_ENTITY) assert state.state == STATE_UNKNOWN client.async_send_command.reset_mock() @@ -1117,7 +849,7 @@ async def update_color(red: int, green: int, blue: int) -> None: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_RGBW_COLOR: (255, 76, 255, 0)}, + {ATTR_ENTITY_ID: ZDB5100_ENTITY, ATTR_RGBW_COLOR: (255, 76, 255, 0)}, blocking=True, ) assert len(client.async_send_command.call_args_list) == 1 @@ -1126,7 +858,7 @@ async def update_color(red: int, green: int, blue: int) -> None: assert args["nodeId"] == node.node_id assert args["valueId"] == { "commandClass": 51, - "endpoint": 0, + "endpoint": 1, "property": "targetColor", } assert args["value"] == {"red": 255, "green": 76, "blue": 255}