Revert "Support Z-Wave JS dimming lights using color intensity (#122639)" (#127256)

This reverts commit c7cfd56b720be8212af2686ecfa5b8cad6ee299b.
This commit is contained in:
Martin Hjelmare 2024-10-02 06:48:47 +02:00 committed by Franck Nijhof
parent fcf91954ff
commit b8fd921c81
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
3 changed files with 352 additions and 736 deletions

View File

@ -238,12 +238,6 @@ SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
command_class={CommandClass.SWITCH_BINARY}, property={CURRENT_VALUE_PROPERTY} 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( SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema(
command_class={CommandClass.SOUND_SWITCH}, command_class={CommandClass.SOUND_SWITCH},
property={TONE_ID_PROPERTY}, property={TONE_ID_PROPERTY},
@ -768,6 +762,33 @@ DISCOVERY_SCHEMAS = [
}, },
), ),
), ),
# 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 ======= # ====== START OF GENERIC MAPPING SCHEMAS =======
# locks # locks
# Door Lock CC # Door Lock CC
@ -969,11 +990,10 @@ DISCOVERY_SCHEMAS = [
), ),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
), ),
# binary switches without color support # binary switches
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
platform=Platform.SWITCH, platform=Platform.SWITCH,
primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
absent_values=[COLOR_SWITCH_CURRENT_VALUE_SCHEMA],
), ),
# switch for Indicator CC # switch for Indicator CC
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
@ -1067,25 +1087,6 @@ DISCOVERY_SCHEMAS = [
# catch any device with multilevel CC as light # catch any device with multilevel CC as light
# NOTE: keep this at the bottom of the discovery scheme, # NOTE: keep this at the bottom of the discovery scheme,
# to handle all others that need the multilevel CC first # 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( ZWaveDiscoverySchema(
platform=Platform.LIGHT, platform=Platform.LIGHT,
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,

View File

@ -76,8 +76,8 @@ async def async_setup_entry(
driver = client.driver driver = client.driver
assert driver is not None # Driver is ready before platforms are loaded. assert driver is not None # Driver is ready before platforms are loaded.
if info.platform_hint == "color_onoff": if info.platform_hint == "black_is_off":
async_add_entities([ZwaveColorOnOffLight(config_entry, driver, info)]) async_add_entities([ZwaveBlackIsOffLight(config_entry, driver, info)])
else: else:
async_add_entities([ZwaveLight(config_entry, driver, info)]) async_add_entities([ZwaveLight(config_entry, driver, info)])
@ -111,10 +111,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
self._supports_color = False self._supports_color = False
self._supports_rgbw = False self._supports_rgbw = False
self._supports_color_temp = 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._hs_color: tuple[float, float] | None = None
self._rgbw_color: tuple[int, int, int, int] | 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._color_temp: int | None = None
self._min_mireds = 153 # 6500K as a safe default self._min_mireds = 153 # 6500K as a safe default
self._max_mireds = 370 # 2700K as a safe default self._max_mireds = 370 # 2700K as a safe default
@ -130,28 +129,15 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
) )
self._supported_color_modes: set[ColorMode] = set() self._supported_color_modes: set[ColorMode] = set()
self._target_brightness: Value | None = None
# get additional (optional) values and set features # get additional (optional) values and set features
if self.info.primary_value.command_class == CommandClass.SWITCH_BINARY: # If the command class is Basic, we must geenerate a name that includes
# This light can not be dimmed separately from the color channels # the command class name to avoid ambiguity
self._target_brightness = self.get_zwave_value( self._target_brightness = self.get_zwave_value(
TARGET_VALUE_PROPERTY, TARGET_VALUE_PROPERTY,
CommandClass.SWITCH_BINARY, CommandClass.SWITCH_MULTILEVEL,
add_to_watched_value_ids=False, add_to_watched_value_ids=False,
) )
self._supports_dimming = False if self.info.primary_value.command_class == CommandClass.BASIC:
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
self._attr_name = self.generate_name( self._attr_name = self.generate_name(
include_value_name=True, alternate_value_name="Basic" include_value_name=True, alternate_value_name="Basic"
) )
@ -160,13 +146,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
CommandClass.BASIC, CommandClass.BASIC,
add_to_watched_value_ids=False, 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( self._target_color = self.get_zwave_value(
TARGET_COLOR_PROPERTY, TARGET_COLOR_PROPERTY,
CommandClass.SWITCH_COLOR, CommandClass.SWITCH_COLOR,
@ -237,7 +216,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
@property @property
def rgbw_color(self) -> tuple[int, int, int, int] | None: def rgbw_color(self) -> tuple[int, int, int, int] | None:
"""Return the RGBW color.""" """Return the hs color."""
return self._rgbw_color return self._rgbw_color
@property @property
@ -264,39 +243,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"""Turn the device on.""" """Turn the device on."""
transition = kwargs.get(ATTR_TRANSITION) 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 # RGB/HS color
hs_color = kwargs.get(ATTR_HS_COLOR)
if hs_color is not None and self._supports_color: if hs_color is not None and self._supports_color:
red, green, blue = color_util.color_hs_to_RGB(*hs_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 = { colors = {
ColorComponent.RED: red, ColorComponent.RED: red,
ColorComponent.GREEN: green, ColorComponent.GREEN: green,
@ -306,9 +257,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
# turn of white leds when setting rgb # turn of white leds when setting rgb
colors[ColorComponent.WARM_WHITE] = 0 colors[ColorComponent.WARM_WHITE] = 0
colors[ColorComponent.COLD_WHITE] = 0 colors[ColorComponent.COLD_WHITE] = 0
return colors await self._async_set_colors(colors, transition)
# Color temperature # Color temperature
color_temp = kwargs.get(ATTR_COLOR_TEMP)
if color_temp is not None and self._supports_color_temp: if color_temp is not None and self._supports_color_temp:
# Limit color temp to min/max values # Limit color temp to min/max values
cold = max( cold = max(
@ -323,18 +275,20 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
), ),
) )
warm = 255 - cold warm = 255 - cold
colors = { await self._async_set_colors(
ColorComponent.WARM_WHITE: warm, {
ColorComponent.COLD_WHITE: cold, # turn off color leds when setting color temperature
} ColorComponent.RED: 0,
if self._supports_color: ColorComponent.GREEN: 0,
# turn off color leds when setting color temperature ColorComponent.BLUE: 0,
colors[ColorComponent.RED] = 0 ColorComponent.WARM_WHITE: warm,
colors[ColorComponent.GREEN] = 0 ColorComponent.COLD_WHITE: cold,
colors[ColorComponent.BLUE] = 0 },
return colors transition,
)
# RGBW # RGBW
rgbw = kwargs.get(ATTR_RGBW_COLOR)
if rgbw is not None and self._supports_rgbw: if rgbw is not None and self._supports_rgbw:
rgbw_channels = { rgbw_channels = {
ColorComponent.RED: rgbw[0], ColorComponent.RED: rgbw[0],
@ -346,15 +300,17 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
if self._cold_white: if self._cold_white:
rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3] 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( async def _async_set_colors(
self, self, colors: dict[ColorComponent, int], transition: float | None = None
colors: dict[ColorComponent, int],
transition: float | None = None,
) -> None: ) -> None:
"""Set (multiple) defined colors to given value(s).""" """Set (multiple) defined colors to given value(s)."""
# prefer the (new) combined color property # prefer the (new) combined color property
@ -405,14 +361,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
zwave_transition = {TRANSITION_DURATION_OPTION: "default"} zwave_transition = {TRANSITION_DURATION_OPTION: "default"}
# setting a value requires setting targetValue # setting a value requires setting targetValue
if self._supports_dimming: await self._async_set_value(
await self._async_set_value( self._target_brightness, zwave_brightness, zwave_transition
self._target_brightness, zwave_brightness, zwave_transition )
)
else:
await self._async_set_value(
self._target_brightness, zwave_brightness > 0, zwave_transition
)
# We do an optimistic state update when setting to a previous value # 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 # to avoid waiting for the value to be updated from the device which is
# typically delayed and causes a confusing UX. # typically delayed and causes a confusing UX.
@ -476,8 +427,15 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"""Calculate light colors.""" """Calculate light colors."""
(red_val, green_val, blue_val, ww_val, cw_val) = self._get_color_values() (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): # prefer the (new) combined color property
multi_color = self._current_color.value # 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: else:
multi_color = {} multi_color = {}
@ -528,10 +486,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
self._color_mode = ColorMode.RGBW self._color_mode = ColorMode.RGBW
class ZwaveColorOnOffLight(ZwaveLight): class ZwaveBlackIsOffLight(ZwaveLight):
"""Representation of a colored Z-Wave light with an optional binary switch to turn on/off. """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__( def __init__(
@ -540,137 +499,61 @@ class ZwaveColorOnOffLight(ZwaveLight):
"""Initialize the light.""" """Initialize the light."""
super().__init__(config_entry, driver, info) super().__init__(config_entry, driver, info)
self._last_on_color: dict[ColorComponent, int] | None = None self._last_color: dict[str, int] | None = None
self._last_brightness: int | None = None self._supported_color_modes.discard(ColorMode.BRIGHTNESS)
@property @property
def brightness(self) -> int | None: def brightness(self) -> int:
"""Return the brightness of this light between 0..255. """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: if self.info.primary_value.value is None:
return None return None
if self._target_brightness and self.info.primary_value.value is False: return any(value != 0 for value in self.info.primary_value.value.values())
# 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
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
if ( if (
kwargs.get(ATTR_RGBW_COLOR) is not None kwargs.get(ATTR_RGBW_COLOR) is not None
or kwargs.get(ATTR_COLOR_TEMP) 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) await super().async_turn_on(**kwargs)
return return
transition = kwargs.get(ATTR_TRANSITION) transition = kwargs.get(ATTR_TRANSITION)
brightness = kwargs.get(ATTR_BRIGHTNESS) # turn on light to last color if known, otherwise set to white
hs_color = kwargs.get(ATTR_HS_COLOR) if self._last_color is not None:
new_colors: dict[ColorComponent, int] | None = None await self._async_set_colors(
scale: float | None = None {
ColorComponent.RED: self._last_color["red"],
if brightness is None and hs_color is None: ColorComponent.GREEN: self._last_color["green"],
# Turned on without specifying brightness or color ColorComponent.BLUE: self._last_color["blue"],
if self._last_on_color is not None: },
if self._target_brightness: transition,
# Color is already set, use the binary switch to turn on )
await self._async_set_brightness(None, transition) else:
return await self._async_set_colors(
{
# 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 = {
ColorComponent.RED: 255, ColorComponent.RED: 255,
ColorComponent.GREEN: 255, ColorComponent.GREEN: 255,
ColorComponent.BLUE: 255, ColorComponent.BLUE: 255,
} },
elif brightness is not None: transition,
# 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
) )
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: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off.""" """Turn the light off."""
self._last_color = self.info.primary_value.value
# Remember last color and brightness to restore it when turning on await self._async_set_colors(
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 = {
ColorComponent.RED: 0, ColorComponent.RED: 0,
ColorComponent.GREEN: 0, ColorComponent.GREEN: 0,
ColorComponent.BLUE: 0, ColorComponent.BLUE: 0,
} },
kwargs.get(ATTR_TRANSITION),
await self._async_set_colors( )
colors, await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
kwargs.get(ATTR_TRANSITION),
)

View File

@ -8,7 +8,6 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_MODE, ATTR_COLOR_MODE,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS, ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS, ATTR_MIN_MIREDS,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
@ -38,8 +37,8 @@ from .common import (
ZEN_31_ENTITY, ZEN_31_ENTITY,
) )
ZDB5100_ENTITY = "light.matrix_office"
HSM200_V1_ENTITY = "light.hsm200" HSM200_V1_ENTITY = "light.hsm200"
ZDB5100_ENTITY = "light.matrix_office"
async def test_light( async def test_light(
@ -511,388 +510,14 @@ async def test_light_none_color_value(
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"]
async def test_light_on_off_color( async def test_black_is_off(
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(
hass: HomeAssistant, client, express_controls_ezmultipli, integration hass: HomeAssistant, client, express_controls_ezmultipli, integration
) -> None: ) -> None:
"""Test the light entity for RGB lights with Color Switch CC only.""" """Test the black is off light entity."""
node = express_controls_ezmultipli node = express_controls_ezmultipli
state = hass.states.get(HSM200_V1_ENTITY) state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_ON 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 # Attempt to turn on the light and ensure it defaults to white
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -914,14 +539,64 @@ async def test_light_color_only(
client.async_send_command.reset_mock() client.async_send_command.reset_mock()
# Force the light to turn off # 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) state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_OFF assert state.state == STATE_OFF
# Force the light to turn on (50% green) # Force the light to turn on
await update_color(0, 128, 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": 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) state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_ON assert state.state == STATE_ON
@ -944,9 +619,6 @@ async def test_light_color_only(
client.async_send_command.reset_mock() 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 # Assert that the last color is restored
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -963,131 +635,11 @@ async def test_light_color_only(
"endpoint": 0, "endpoint": 0,
"property": "targetColor", "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() client.async_send_command.reset_mock()
# Force the light to turn on (50% green) # Force the light to turn on
await update_color(0, 128, 0)
state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_ON
client.async_send_command.reset_mock()
# Assert that the brightness is preserved when changing colors
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_RGB_COLOR: (255, 0, 0)},
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": 51,
"endpoint": 0,
"property": "targetColor",
}
assert args["value"] == {"red": 128, "green": 0, "blue": 0}
client.async_send_command.reset_mock()
# 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
# Assert that the color is preserved when changing brightness
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_BRIGHTNESS: 69},
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": 51,
"endpoint": 0,
"property": "targetColor",
}
assert args["value"] == {"red": 69, "green": 0, "blue": 0}
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,
)
await update_color(0, 0, 0)
client.async_send_command.reset_mock()
# 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},
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": 51,
"endpoint": 0,
"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)
client.async_send_command.reset_mock()
# Assert that the brightness is preserved when turning on with color
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_HS_COLOR: (240, 100)},
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": 51,
"endpoint": 0,
"property": "targetColor",
}
assert args["value"] == {"red": 0, "green": 0, "blue": 123}
client.async_send_command.reset_mock()
# Clear the color value to trigger an unknown state
event = Event( event = Event(
type="value updated", type="value updated",
data={ data={
@ -1100,14 +652,17 @@ async def test_light_color_only(
"endpoint": 0, "endpoint": 0,
"property": "currentColor", "property": "currentColor",
"newValue": None, "newValue": None,
"prevValue": None, "prevValue": {
"red": 0,
"green": 255,
"blue": 0,
},
"propertyName": "currentColor", "propertyName": "currentColor",
}, },
}, },
) )
node.receive_event(event) node.receive_event(event)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(HSM200_V1_ENTITY) state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
@ -1132,6 +687,183 @@ async def test_light_color_only(
assert args["value"] == {"red": 255, "green": 76, "blue": 255} assert args["value"] == {"red": 255, "green": 76, "blue": 255}
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
# 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: 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": 51,
"endpoint": 1,
"property": "targetColor",
}
assert args["value"] == {"red": 255, "green": 255, "blue": 255}
client.async_send_command.reset_mock()
# 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",
},
},
)
node.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(ZDB5100_ENTITY)
assert state.state == STATE_OFF
# 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
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": 51,
"endpoint": 1,
"property": "targetColor",
}
assert args["value"] == {"red": 0, "green": 0, "blue": 0}
client.async_send_command.reset_mock()
# Assert that the last color is restored
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": 51,
"endpoint": 1,
"property": "targetColor",
}
assert args["value"] == {"red": 0, "green": 255, "blue": 0}
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": None,
"prevValue": {
"red": 0,
"green": 255,
"blue": 0,
},
"propertyName": "currentColor",
},
},
)
node.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(ZDB5100_ENTITY)
assert state.state == STATE_UNKNOWN
client.async_send_command.reset_mock()
# Assert that call fails if attribute is added to service call
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ZDB5100_ENTITY, ATTR_RGBW_COLOR: (255, 76, 255, 0)},
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": 51,
"endpoint": 1,
"property": "targetColor",
}
assert args["value"] == {"red": 255, "green": 76, "blue": 255}
async def test_basic_cc_light( async def test_basic_cc_light(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,