Fix Z-Wave device class endpoint discovery (#142171)

* Add test fixture and test for Glass 9 shutter

* Fix zwave_js device class discovery matcher

* Fall back to node device class

* Fix test_special_meters modifying node state

* Handle value added after node ready
This commit is contained in:
Martin Hjelmare 2025-06-20 07:56:20 +02:00 committed by GitHub
parent 341d9f15f0
commit 11564e3df5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 8547 additions and 9 deletions

View File

@ -1334,21 +1334,49 @@ def async_discover_single_value(
continue
# check device_class_generic
# If the value has an endpoint but it is missing on the node
# we can't match the endpoint device class to the schema device class.
# This could happen if the value is discovered after the node is ready.
if schema.device_class_generic and (
not value.node.device_class
or not any(
value.node.device_class.generic.label == val
for val in schema.device_class_generic
(
(endpoint := value.endpoint) is None
or (node_endpoint := value.node.endpoints.get(endpoint)) is None
or (device_class := node_endpoint.device_class) is None
or not any(
device_class.generic.label == val
for val in schema.device_class_generic
)
)
and (
(device_class := value.node.device_class) is None
or not any(
device_class.generic.label == val
for val in schema.device_class_generic
)
)
):
continue
# check device_class_specific
# If the value has an endpoint but it is missing on the node
# we can't match the endpoint device class to the schema device class.
# This could happen if the value is discovered after the node is ready.
if schema.device_class_specific and (
not value.node.device_class
or not any(
value.node.device_class.specific.label == val
for val in schema.device_class_specific
(
(endpoint := value.endpoint) is None
or (node_endpoint := value.node.endpoints.get(endpoint)) is None
or (device_class := node_endpoint.device_class) is None
or not any(
device_class.specific.label == val
for val in schema.device_class_specific
)
)
and (
(device_class := value.node.device_class) is None
or not any(
device_class.specific.label == val
for val in schema.device_class_specific
)
)
):
continue

View File

@ -301,6 +301,12 @@ def shelly_europe_ltd_qnsh_001p10_state_fixture() -> dict[str, Any]:
return load_json_object_fixture("shelly_europe_ltd_qnsh_001p10_state.json", DOMAIN)
@pytest.fixture(name="touchwand_glass9_state", scope="package")
def touchwand_glass9_state_fixture() -> dict[str, Any]:
"""Load the Touchwand Glass 9 shutter node state fixture data."""
return load_json_object_fixture("touchwand_glass9_state.json", DOMAIN)
@pytest.fixture(name="merten_507801_state", scope="package")
def merten_507801_state_fixture() -> dict[str, Any]:
"""Load the Merten 507801 Shutter node state fixture data."""
@ -1040,6 +1046,14 @@ def shelly_qnsh_001P10_cover_shutter_fixture(
return node
@pytest.fixture(name="touchwand_glass9")
def touchwand_glass9_fixture(client, touchwand_glass9_state) -> Node:
"""Mock a Touchwand glass9 node."""
node = Node(client, copy.deepcopy(touchwand_glass9_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="merten_507801")
def merten_507801_cover_fixture(client, merten_507801_state) -> Node:
"""Mock a Merten 507801 Shutter node."""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,24 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration)
assert state
async def test_touchwand_glass9(
hass: HomeAssistant,
client: MagicMock,
touchwand_glass9: Node,
integration: MockConfigEntry,
) -> None:
"""Test a touchwand_glass9 is discovered as a cover."""
node = touchwand_glass9
node_device_class = node.device_class
assert node_device_class
assert node_device_class.specific.label == "Unused"
assert not hass.states.async_entity_ids_count("light")
assert hass.states.async_entity_ids_count("cover") == 3
state = hass.states.get("cover.gp9")
assert state
async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None:
"""Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover."""
node = zvidar

View File

@ -27,7 +27,7 @@ from homeassistant.components.persistent_notification import async_dismiss
from homeassistant.components.zwave_js import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id, get_device_id_ext
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import (
area_registry as ar,
@ -366,6 +366,7 @@ async def test_listen_done_after_setup(
@pytest.mark.usefixtures("client")
@pytest.mark.parametrize("platforms", [[Platform.SENSOR]])
async def test_new_entity_on_value_added(
hass: HomeAssistant,
multisensor_6: Node,

View File

@ -655,6 +655,17 @@ async def test_special_meters(
"value": 659.813,
},
)
node_data["endpoints"].append(
{
"nodeId": 102,
"index": 10,
"installerIcon": 1792,
"userIcon": 1792,
"commandClasses": [
{"id": 50, "name": "Meter", "version": 3, "isSecure": False}
],
}
)
# Add an ElectricScale.KILOVOLT_AMPERE_REACTIVE value to the state so we can test that
# it is handled differently (no device class)
node_data["values"].append(
@ -678,6 +689,17 @@ async def test_special_meters(
"value": 659.813,
},
)
node_data["endpoints"].append(
{
"nodeId": 102,
"index": 11,
"installerIcon": 1792,
"userIcon": 1792,
"commandClasses": [
{"id": 50, "name": "Meter", "version": 3, "isSecure": False}
],
}
)
node = Node(client, node_data)
event = {"node": node}
client.driver.controller.emit("node added", event)