Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/test_discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ async def test_device_override_picks_highest_priority(
"""Test that a device override selects only the highest-priority match."""

# A Philips light matches both Light (priority 0) and HueLight (priority 1) in the
# LIGHT_OR_SWITCH_OR_SHADE feature group. With a SWITCH override, only one Switch
# GENERIC_OPEN_CLOSE feature group. With a SWITCH override, only one Switch
# entity should be created, not duplicates from collecting all priority levels.
zigpy_device = await zigpy_device_from_json(
zha_gateway.application_controller,
Expand Down
122 changes: 122 additions & 0 deletions tests/test_valve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Test zha valve."""

from unittest.mock import AsyncMock, call, patch

import pytest
import zigpy.zcl.foundation as zcl_f

from tests.common import get_entity, join_zigpy_device, zigpy_device_from_json
from zha.application import Platform
from zha.application.gateway import Gateway
from zha.application.helpers import DeviceOverridesConfiguration
from zha.application.platforms.valve import Valve
from zha.application.platforms.valve.const import ValveEntityFeature


async def test_valve_not_discovered_by_default(zha_gateway: Gateway) -> None:
"""Test that valves are not discovered by default."""
zigpy_device = await zigpy_device_from_json(
zha_gateway.application_controller,
"tests/data/devices/sonoff-swv.json",
)
zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

with pytest.raises(KeyError):
get_entity(zha_device, platform=Platform.VALVE)

get_entity(
zha_device,
platform=Platform.SWITCH,
qualifier_func=lambda entity: (
entity.cluster_handlers.get("on_off")
and entity.cluster_handlers["on_off"].cluster
== zigpy_device.endpoints[1].on_off
),
)


async def test_valve_discovered_via_platform_override(zha_gateway: Gateway) -> None:
"""Test valve discovery when overridden via device configuration."""
zigpy_device = await zigpy_device_from_json(
zha_gateway.application_controller,
"tests/data/devices/sonoff-swv.json",
)

zha_gateway.config.config.device_overrides = {
f"{zigpy_device.ieee}-1": DeviceOverridesConfiguration(type=Platform.VALVE)
}

zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

valve = get_entity(
zha_device,
platform=Platform.VALVE,
exact_entity_type=Valve,
qualifier_func=lambda entity: (
entity.cluster_handlers.get("on_off")
and entity.cluster_handlers["on_off"].cluster
== zigpy_device.endpoints[1].on_off
),
)

assert valve.supported_features == (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
)
assert valve.reports_position is False

with pytest.raises(KeyError):
get_entity(
zha_device,
platform=Platform.SWITCH,
qualifier_func=lambda entity: (
entity.cluster_handlers.get("on_off")
and entity.cluster_handlers["on_off"].cluster
== zigpy_device.endpoints[1].on_off
),
)


async def test_valve_level_control_commands(zha_gateway: Gateway) -> None:
"""Test level-based valve set_position and stop with platform override."""
zigpy_device = await zigpy_device_from_json(
zha_gateway.application_controller,
"tests/data/devices/sinope-technologies-va4220zb.json",
)

zha_gateway.config.config.device_overrides = {
f"{zigpy_device.ieee}-1": DeviceOverridesConfiguration(type=Platform.VALVE)
}

zha_device = await join_zigpy_device(zha_gateway, zigpy_device)
valve = get_entity(
zha_device,
platform=Platform.VALVE,
exact_entity_type=Valve,
qualifier_func=lambda entity: (
entity.cluster_handlers.get("on_off")
and entity.cluster_handlers["on_off"].cluster
== zigpy_device.endpoints[1].on_off
),
)

assert valve.reports_position is True
assert valve.supported_features & ValveEntityFeature.SET_POSITION
assert valve.supported_features & ValveEntityFeature.STOP

level_cluster = zigpy_device.endpoints[1].level

with patch.object(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are sort of auto-generated and will be rewritten later.

level_cluster,
"move_to_level_with_on_off",
AsyncMock(return_value=[0, zcl_f.Status.SUCCESS]),
) as move_to_level_with_on_off:
await valve.async_set_valve_position(position=47)
await zha_gateway.async_block_till_done()
assert move_to_level_with_on_off.mock_calls == [call(120, 1)]

with patch.object(
level_cluster, "stop", AsyncMock(return_value=[0, zcl_f.Status.SUCCESS])
) as stop:
await valve.async_stop_valve()
await zha_gateway.async_block_till_done()
assert stop.mock_calls == [call()]
2 changes: 2 additions & 0 deletions zha/application/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
siren,
switch,
update,
valve,
)

# importing cluster handlers updates registries
Expand Down Expand Up @@ -90,6 +91,7 @@
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
Platform.VALVE,
Platform.UPDATE,
)

Expand Down
6 changes: 4 additions & 2 deletions zha/application/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
class PlatformFeatureGroup(StrEnum):
"""Feature groups for platform entities."""

# OnOff server clusters can be turned into lights, shades, or switches (fallback)
LIGHT_OR_SWITCH_OR_SHADE = "light_or_switch_or_shade"
# OnOff server clusters can be turned into lights, shades, switches (fallback).
# Valves are also allowed via overrides but have no device types so are not auto
# discovered.
GENERIC_OPEN_CLOSE = "generic_open_close"

# OnOff client clusters can be turned into manufacturer-specific motion sensors or
# fall back to generic binary sensors
Expand Down
4 changes: 2 additions & 2 deletions zha/application/platforms/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ class Shade(BaseCover):
(512, zha.DeviceType.SHADE), # TODO: remove this Tuya hack
}
),
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 0),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 0),
)

def __init__(
Expand Down Expand Up @@ -912,7 +912,7 @@ class KeenVent(Shade):
_cluster_handler_match = ClusterHandlerMatch(
cluster_handlers=frozenset({CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_ON_OFF}),
manufacturers=frozenset({"Keen Home Inc"}),
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 1),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 1),
)

async def async_open_cover(self, **kwargs: Any) -> None: # pylint: disable=unused-argument
Expand Down
8 changes: 4 additions & 4 deletions zha/application/platforms/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ class Light(BaseClusterHandlerLight, PlatformEntity):
{CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}
),
profile_device_types=LIGHT_PROFILE_DEVICE_TYPES,
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 0),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 0),
)

def __init__(
Expand Down Expand Up @@ -1050,7 +1050,7 @@ class HueLight(Light):
manufacturers=frozenset({"Philips", "Signify Netherlands B.V."}),
profile_device_types=LIGHT_PROFILE_DEVICE_TYPES,
# We want this entity to be preferred over the base light
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 1),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 1),
)


Expand All @@ -1076,7 +1076,7 @@ class ForceOnLight(Light):
),
profile_device_types=LIGHT_PROFILE_DEVICE_TYPES,
# We want this entity to be preferred over the base light
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 1),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 1),
)


Expand All @@ -1095,7 +1095,7 @@ class MinTransitionLight(Light):
manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS,
profile_device_types=LIGHT_PROFILE_DEVICE_TYPES,
# We want this entity to be preferred over the base light
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, 1),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, 1),
)


Expand Down
2 changes: 1 addition & 1 deletion zha/application/platforms/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class Switch(PlatformEntity, BaseSwitch):
_cluster_handler_match = ClusterHandlerMatch(
cluster_handlers=frozenset({CLUSTER_HANDLER_ON_OFF}),
# Switch entities have the lowest priority
feature_priority=(PlatformFeatureGroup.LIGHT_OR_SWITCH_OR_SHADE, -1),
feature_priority=(PlatformFeatureGroup.GENERIC_OPEN_CLOSE, -1),
)

def __init__(
Expand Down
Loading
Loading