diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 32c54c2b290..69a3d05539b 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -16,6 +16,7 @@ from zwave_js_server.const import ( from zwave_js_server.const.command_class.barrier_operator import ( SIGNALING_STATE_PROPERTY, ) +from zwave_js_server.const.command_class.color_switch import CURRENT_COLOR_PROPERTY from zwave_js_server.const.command_class.humidity_control import ( HUMIDITY_CONTROL_MODE_PROPERTY, ) @@ -125,9 +126,9 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne): # [optional] the value's property name must match ANY of these values property_name: set[str] | None = None # [optional] the value's property key must match ANY of these values - property_key: set[str | int] | None = None + property_key: set[str | int | None] | None = None # [optional] the value's property key name must match ANY of these values - property_key_name: set[str] | None = None + property_key_name: set[str | None] | None = None # [optional] the value's metadata_type must match ANY of these values type: set[str] | None = None @@ -180,8 +181,8 @@ class ZWaveDiscoverySchema: def get_config_parameter_discovery_schema( property_: set[str | int] | None = None, property_name: set[str] | None = None, - property_key: set[str | int] | None = None, - property_key_name: set[str] | None = None, + property_key: set[str | int | None] | None = None, + property_key_name: set[str | None] | None = None, **kwargs: Any, ) -> ZWaveDiscoverySchema: """ @@ -462,6 +463,20 @@ DISCOVERY_SCHEMAS = [ }, ), ), + # HomeSeer HSM-200 v1 + ZWaveDiscoverySchema( + 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], + ), # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= # Door lock mode config parameter. Functionality equivalent to Notification CC # list sensors. diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index caba0f5de36..0f3f17df41a 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -74,8 +74,10 @@ async def async_setup_entry( def async_add_light(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Light.""" - light = ZwaveLight(config_entry, client, info) - async_add_entities([light]) + if info.platform_hint == "black_is_off": + async_add_entities([ZwaveBlackIsOffLight(config_entry, client, info)]) + else: + async_add_entities([ZwaveLight(config_entry, client, info)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -127,7 +129,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # get additional (optional) values and set features self._target_brightness = self.get_zwave_value( - TARGET_VALUE_PROPERTY, add_to_watched_value_ids=False + TARGET_VALUE_PROPERTY, + CommandClass.SWITCH_MULTILEVEL, + add_to_watched_value_ids=False, ) self._target_color = self.get_zwave_value( TARGET_COLOR_PROPERTY, @@ -167,14 +171,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._calculate_color_values() @property - def brightness(self) -> int: + def brightness(self) -> int | None: """Return the brightness of this light between 0..255. Z-Wave multilevel switches use a range of [0, 99] to control brightness. """ - if self.info.primary_value.value is not None: - return round((self.info.primary_value.value / 99) * 255) - return 0 + if self.info.primary_value.value is None: + return None + return round((self.info.primary_value.value / 99) * 255) @property def color_mode(self) -> str | None: @@ -182,9 +186,12 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self._color_mode @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if device is on (brightness above 0).""" - return self.brightness > 0 + brightness = self.brightness + if brightness is None: + return None + return brightness > 0 @property def hs_color(self) -> tuple[float, float] | None: @@ -318,6 +325,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self, brightness: int | None, transition: float | None = None ) -> None: """Set new brightness to light.""" + # If we have no target brightness value, there is nothing to do + if not self._target_brightness: + return if brightness is None: # Level 255 means to set it to previous value. zwave_brightness = 255 @@ -426,3 +436,77 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._rgbw_color = (red, green, blue, white) # Light supports rgbw, set color mode to rgbw self._color_mode = COLOR_MODE_RGBW + + +class ZwaveBlackIsOffLight(ZwaveLight): + """ + Representation of a Z-Wave light where setting the color to black turns it off. + + Currently only supports lights with RGB, no color temperature, and no white channels. + """ + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the light.""" + super().__init__(config_entry, client, info) + + self._last_color: dict[str, int] | None = None + self._supported_color_modes.discard(COLOR_MODE_BRIGHTNESS) + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return 255 + + @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 + 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.""" + await super().async_turn_on(**kwargs) + + 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 + ): + return + + transition = kwargs.get(ATTR_TRANSITION) + # 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, + }, + transition, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + self._last_color = self.info.primary_value.value + await self._async_set_colors( + { + ColorComponent.RED: 0, + ColorComponent.GREEN: 0, + ColorComponent.BLUE: 0, + }, + kwargs.get(ATTR_TRANSITION), + ) + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 318dc99a1df..2535daaf114 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -497,6 +497,12 @@ def zp3111_state_fixture(): return json.loads(load_fixture("zwave_js/zp3111-5_state.json")) +@pytest.fixture(name="express_controls_ezmultipli_state", scope="session") +def light_express_controls_ezmultipli_state_fixture(): + """Load the Express Controls EZMultiPli node state fixture data.""" + return json.loads(load_fixture("zwave_js/express_controls_ezmultipli_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -981,3 +987,11 @@ def zp3111_fixture(client, zp3111_state): node = Node(client, copy.deepcopy(zp3111_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="express_controls_ezmultipli") +def express_controls_ezmultipli_fixture(client, express_controls_ezmultipli_state): + """Mock a Express Controls EZMultiPli node.""" + node = Node(client, copy.deepcopy(express_controls_ezmultipli_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json new file mode 100644 index 00000000000..ea267d86b8c --- /dev/null +++ b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json @@ -0,0 +1,673 @@ +{ + "nodeId": 96, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 30, + "productId": 1, + "productType": 4, + "firmwareVersion": "1.8", + "zwavePlusVersion": 1, + "name": "HSM200", + "location": "Basement", + "deviceConfig": { + "filename": "/data/db/devices/0x001e/ezmultipli.json", + "isEmbedded": true, + "manufacturer": "Express Controls", + "manufacturerId": 30, + "label": "EZMultiPli", + "description": "Multi Sensor", + "devices": [ + { + "productType": 4, + "productId": 1 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "EZMultiPli", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 96, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "7": "Motion detection (location provided)" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 6, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 16.8 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Illuminance", + "propertyName": "Illuminance", + "ccVersion": 6, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 0 + }, + "unit": "%" + }, + "value": 61 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Remaining duration" + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Red color.", + "label": "Current value (Red)", + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Green color.", + "label": "Current value (Green)", + "min": 0, + "max": 255 + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Blue color.", + "label": "Current value (Blue)", + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyName": "currentColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Current Color" + }, + "value": { + "red": 0, + "green": 255, + "blue": 0 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "hexColor", + "propertyName": "hexColor", + "ccVersion": 1, + "metadata": { + "type": "color", + "readable": true, + "writeable": true, + "label": "RGB Color", + "minLength": 6, + "maxLength": 7 + }, + "value": "00ff00" + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Red color.", + "label": "Target value (Red)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Green color.", + "label": "Target value (Green)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Blue color.", + "label": "Target value (Blue)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Target Color", + "valueChangeOptions": ["transitionDuration"] + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.8"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "OnTime", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "OnTime", + "default": 10, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "OnLevel", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0-99, 255", + "label": "OnLevel", + "default": 255, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "LiteMin", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LiteMin", + "default": 60, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "TempMin", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "A Temperature report is sent to the controller every TempMin minutes.", + "label": "TempMin", + "default": 60, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "TempAdj", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "TempAdj", + "default": 0, + "min": -128, + "max": 127, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": -40 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 6, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 2, + "isSecure": false + }, + { + "id": 119, + "name": "Node Naming and Location", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001e:0x0004:0x0001:1.8", + "statistics": { + "commandsTX": 147, + "commandsRX": 322, + "commandsDroppedRX": 0, + "commandsDroppedTX": 3, + "timeoutResponse": 0 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 373ca2525ac..01de5a70692 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -13,9 +13,17 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, SUPPORT_TRANSITION, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) from .common import ( AEON_SMART_SWITCH_LIGHT_ENTITY, @@ -24,6 +32,8 @@ from .common import ( ZEN_31_ENTITY, ) +HSM200_V1_ENTITY = "light.hsm200" + async def test_light(hass, client, bulb_6_multi_color, integration): """Test the light entity.""" @@ -62,7 +72,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -95,7 +104,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -168,7 +176,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -206,7 +213,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -444,7 +450,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -469,7 +474,6 @@ async def test_optional_light(hass, client, aeon_smart_switch_6, integration): async def test_rgbw_light(hass, client, zen_31, integration): """Test the light entity.""" - zen_31 state = hass.states.get(ZEN_31_ENTITY) assert state @@ -523,7 +527,6 @@ async def test_rgbw_light(hass, client, zen_31, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, "value": 59, @@ -542,3 +545,161 @@ async def test_light_none_color_value(hass, light_color_null_values, integration assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + + +async def test_black_is_off(hass, client, express_controls_ezmultipli, integration): + """Test the black is off light entity.""" + node = express_controls_ezmultipli + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_ON + + # 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}, + 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"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + 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": 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 + 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 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_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"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + 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: HSM200_V1_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"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == {"red": 0, "green": 255, "blue": 0} + + client.async_send_command.reset_mock()