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

* Z-Wave JS: support non-dimmable color lights

* remove black_is_off light, support on/off/color

* fix: tests for on/off light

* fix: typo

* remove commented out old test code

* add test for off and on

* support colored lights without separate brightness control

* add test for color-only light

* refactor: extract color only light

* fix: preserve color when changing brightness

* extend tests

* refactor again

* refactor scale check

* refactor: remove impossible check

* review feedback

* review feedback

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
AlCalzone
2024-08-29 00:01:53 +02:00
committed by GitHub
co-authored by Martin Hjelmare
parent ada6b7875c
commit c7cfd56b72
3 changed files with 736 additions and 352 deletions
+510 -242
View File
@@ -8,6 +8,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
@@ -37,8 +38,8 @@ from .common import (
ZEN_31_ENTITY,
)
HSM200_V1_ENTITY = "light.hsm200"
ZDB5100_ENTITY = "light.matrix_office"
HSM200_V1_ENTITY = "light.hsm200"
async def test_light(
@@ -510,14 +511,388 @@ async def test_light_none_color_value(
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"]
async def test_black_is_off(
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(
hass: HomeAssistant, client, express_controls_ezmultipli, integration
) -> None:
"""Test the black is off light entity."""
"""Test the light entity for RGB lights with Color Switch CC only."""
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,
@@ -539,64 +914,14 @@ async def test_black_is_off(
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": 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()
await update_color(0, 0, 0)
state = hass.states.get(HSM200_V1_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": 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()
# Force the light to turn on (50% green)
await update_color(0, 128, 0)
state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_ON
@@ -619,6 +944,9 @@ async def test_black_is_off(
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,
@@ -635,11 +963,131 @@ async def test_black_is_off(
"endpoint": 0,
"property": "targetColor",
}
assert args["value"] == {"red": 0, "green": 255, "blue": 0}
assert args["value"] == {"red": 0, "green": 128, "blue": 0}
client.async_send_command.reset_mock()
# Force the light to turn on
# Force the light to turn on (50% green)
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(
type="value updated",
data={
@@ -652,17 +1100,14 @@ async def test_black_is_off(
"endpoint": 0,
"property": "currentColor",
"newValue": None,
"prevValue": {
"red": 0,
"green": 255,
"blue": 0,
},
"prevValue": None,
"propertyName": "currentColor",
},
},
)
node.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(HSM200_V1_ENTITY)
assert state.state == STATE_UNKNOWN
@@ -687,183 +1132,6 @@ async def test_black_is_off(
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(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,