From 585bbadee6d322570858790dcc1eb4887ef09f6f Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:19:15 +0100 Subject: [PATCH 1/9] Copy `OnOff` `cluster_command` logic to `OnOffClientClusterHandler` --- zha/zigbee/cluster_handlers/general.py | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/zha/zigbee/cluster_handlers/general.py b/zha/zigbee/cluster_handlers/general.py index 51dcf7162..f959d31ff 100644 --- a/zha/zigbee/cluster_handlers/general.py +++ b/zha/zigbee/cluster_handlers/general.py @@ -540,6 +540,59 @@ class MultistateValueClusterHandler(ClusterHandler): class OnOffClientClusterHandler(ClientClusterHandler): """OnOff client cluster handler.""" + def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: + """Initialize OnOffClientClusterHandler.""" + super().__init__(cluster, endpoint) + self._off_listener: asyncio.TimerHandle | None = None + + @property + def on_off(self) -> bool | None: + """Return cached value of on/off attribute.""" + return self.cluster.get(OnOff.AttributeDefs.on_off.name) + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + # for emitting ZHA event + super().cluster_command(tsn, command_id, args) + + cmd = parse_and_log_command(self, tsn, command_id, args) + + if cmd in ( + OnOff.ServerCommandDefs.off.name, + OnOff.ServerCommandDefs.off_with_effect.name, + ): + self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) + elif cmd in ( + OnOff.ServerCommandDefs.on.name, + OnOff.ServerCommandDefs.on_with_recall_global_scene.name, + ): + self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.true) + elif cmd == OnOff.ServerCommandDefs.on_with_timed_off.name: + should_accept = args[0] + on_time = args[1] + # 0 is always accept 1 is only accept when already on + if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): + if self._off_listener is not None: + self._off_listener.cancel() + self._off_listener = None + self.cluster.update_attribute( + OnOff.AttributeDefs.on_off.id, t.Bool.true + ) + if on_time > 0: + self._off_listener = asyncio.get_running_loop().call_later( + (on_time / 10), # value is in 10ths of a second + self.set_to_off, + ) + elif cmd == "toggle": + self.cluster.update_attribute( + OnOff.AttributeDefs.on_off.id, not bool(self.on_off) + ) + + def set_to_off(self, *_): + """Set the state to off.""" + self._off_listener = None + self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) + @registries.BINDABLE_CLUSTERS.register(OnOff.cluster_id) @registries.CLUSTER_HANDLER_REGISTRY.register(OnOff.cluster_id) From f58991cdc529847d8f8623f9b01f95ba48506d0e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:19:50 +0100 Subject: [PATCH 2/9] Modify remote test in test_cluster_handlers.py to also expect attribute update --- tests/test_cluster_handlers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_cluster_handlers.py b/tests/test_cluster_handlers.py index b1996274d..ebad65a41 100644 --- a/tests/test_cluster_handlers.py +++ b/tests/test_cluster_handlers.py @@ -1241,8 +1241,19 @@ async def test_zha_send_event_from_quirk(zha_gateway: Gateway): on_off_ch.cluster_command(1, OnOff.ServerCommandDefs.on.id, []) - assert on_off_ch.emit_zha_event.call_count == 1 - assert on_off_ch.emit_zha_event.mock_calls == [call("on", [])] + assert on_off_ch.emit_zha_event.call_count == 2 + assert on_off_ch.emit_zha_event.mock_calls == [ + call("on", []), + call( + "attribute_updated", + { + "attribute_id": 0, + "attribute_name": "on_off", + "attribute_value": t.Bool.true, + "value": t.Bool.true, + }, + ), + ] on_off_ch.emit_zha_event.reset_mock() await send_attributes_report( From 17a1bf33ce5837a6d2fb10c378794928500b9f33 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:20:39 +0100 Subject: [PATCH 3/9] Add test for new behavior to test_cluster_handlers.py --- tests/test_cluster_handlers.py | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/test_cluster_handlers.py b/tests/test_cluster_handlers.py index ebad65a41..cf4ad9f1a 100644 --- a/tests/test_cluster_handlers.py +++ b/tests/test_cluster_handlers.py @@ -4,6 +4,7 @@ from __future__ import annotations +import asyncio import logging import math from typing import TYPE_CHECKING, Any @@ -1325,3 +1326,111 @@ async def test_zdo_cluster_handler(zha_gateway: Gateway): zha_device.zdo_cluster_handler.unique_id == f"{str(zha_device.ieee)}:{zha_device.name}_ZDO" ) + + +async def test_onoff_client_cluster_handler_on_with_timed_off(zha_gateway: Gateway): + """Test that OnOffClientClusterHandler handles on_with_timed_off command. + + This test verifies that motion sensors with output/client OnOff clusters + properly update the on_off attribute when sending on_with_timed_off commands, + and that the attribute is automatically set to off after the timer expires. + """ + zigpy_device = create_mock_zigpy_device( + zha_gateway, + { + 1: { + SIG_EP_INPUT: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ], + SIG_EP_OUTPUT: [ + OnOff.cluster_id, + ], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + model="Motion Sensor", + manufacturer="Test", + ieee="00:15:8e:01:01:02:03:05", + ) + + zha_device = await join_zigpy_device(zha_gateway, zigpy_device) + + on_off_ch = zha_device.endpoints[1].client_cluster_handlers["1:0x0006_client"] + assert on_off_ch is not None + + # Verify initial state - on_off should be None (not set) + assert on_off_ch.on_off is None + + # Simulate motion sensor sending on_with_timed_off command + # on_off_control=0 means always accept, on_time=1800 (180 seconds in 10ths) + on_off_ch.cluster_command( + 106, # tsn + OnOff.ServerCommandDefs.on_with_timed_off.id, + [0, 1800, 0], # on_off_control, on_time, off_wait_time + ) + + # Verify on_off is now True + assert on_off_ch.on_off == t.Bool.true + + # Advance time by less than the timeout - should still be on + await asyncio.sleep(100) + assert on_off_ch.on_off == t.Bool.true + + # Advance time past the timeout (180 seconds total) + await asyncio.sleep(100) + assert on_off_ch.on_off == t.Bool.false + + # Test that a new on_with_timed_off resets the timer + on_off_ch.cluster_command( + 107, + OnOff.ServerCommandDefs.on_with_timed_off.id, + [0, 500, 0], # 50 seconds + ) + assert on_off_ch.on_off == t.Bool.true + + # Send another command before timer expires - should reset + await asyncio.sleep(30) + on_off_ch.cluster_command( + 108, + OnOff.ServerCommandDefs.on_with_timed_off.id, + [0, 500, 0], # 50 seconds from now + ) + assert on_off_ch.on_off == t.Bool.true + + # Wait 40 seconds - old timer would have fired, but new one shouldn't + await asyncio.sleep(40) + assert on_off_ch.on_off == t.Bool.true + + # Wait for new timer to expire + await asyncio.sleep(20) + assert on_off_ch.on_off == t.Bool.false + + # Test on_off_control=1 (only accept when already on) + # When off, should not accept + on_off_ch.cluster_command( + 109, + OnOff.ServerCommandDefs.on_with_timed_off.id, + [1, 500, 0], # on_off_control=1 + ) + assert on_off_ch.on_off == t.Bool.false + + # Turn on first, then on_off_control=1 should work + on_off_ch.cluster_command( + 110, + OnOff.ServerCommandDefs.on.id, + [], + ) + assert on_off_ch.on_off == t.Bool.true + + on_off_ch.cluster_command( + 111, + OnOff.ServerCommandDefs.on_with_timed_off.id, + [1, 300, 0], # on_off_control=1, 30 seconds + ) + assert on_off_ch.on_off == t.Bool.true + + await asyncio.sleep(35) + assert on_off_ch.on_off == t.Bool.false From d92fbcf934ff4f205e6217d2885e56c82203deac Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:21:43 +0100 Subject: [PATCH 4/9] Move test to test_binary_sensor.py --- tests/test_binary_sensor.py | 60 ++++++++++++++++++ tests/test_cluster_handlers.py | 109 --------------------------------- 2 files changed, 60 insertions(+), 109 deletions(-) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 793a270cc..2c49f76e1 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -1,5 +1,6 @@ """Test zhaws binary sensor.""" +import asyncio from collections.abc import Awaitable, Callable from unittest.mock import MagicMock, call @@ -287,3 +288,62 @@ async def test_quirks_binary_sensor_attr_converter(zha_gateway: Gateway) -> None await send_attributes_report(zha_gateway, cluster, {"on_off": 0}) assert entity.is_on is True + + +async def test_onoff_client_binary_sensor_on_with_timed_off( + zha_gateway: Gateway, +) -> None: + """Test binary sensor with client OnOff cluster handles on_with_timed_off. + + This tests motion sensors that use output/client OnOff clusters and send + on_with_timed_off commands when motion is detected. + """ + zigpy_device = create_mock_zigpy_device( + zha_gateway, + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + ], + SIG_EP_OUTPUT: [ + general.OnOff.cluster_id, + ], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SENSOR, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + model="Motion Sensor", + manufacturer="Test", + ) + + zha_device = await join_zigpy_device(zha_gateway, zigpy_device) + entity = find_entity(zha_device, Platform.BINARY_SENSOR) + assert entity is not None + assert isinstance(entity, BinarySensor) + + # Initial state should be off + assert entity.is_on is False + + # Get the client cluster handler + on_off_ch = zha_device.endpoints[1].client_cluster_handlers["1:0x0006_client"] + assert on_off_ch is not None + + # Simulate motion sensor sending on_with_timed_off command + # on_off_control=0 means always accept, on_time=1800 (180 seconds in 10ths) + on_off_ch.cluster_command( + 106, # tsn + OnOff.ServerCommandDefs.on_with_timed_off.id, + [0, 1800, 0], # on_off_control, on_time, off_wait_time + ) + await zha_gateway.async_block_till_done() + + # Binary sensor should now be on + assert entity.is_on is True + + # Advance time past the timeout (180 seconds) + await asyncio.sleep(200) + await zha_gateway.async_block_till_done() + + # Binary sensor should now be off + assert entity.is_on is False diff --git a/tests/test_cluster_handlers.py b/tests/test_cluster_handlers.py index cf4ad9f1a..ebad65a41 100644 --- a/tests/test_cluster_handlers.py +++ b/tests/test_cluster_handlers.py @@ -4,7 +4,6 @@ from __future__ import annotations -import asyncio import logging import math from typing import TYPE_CHECKING, Any @@ -1326,111 +1325,3 @@ async def test_zdo_cluster_handler(zha_gateway: Gateway): zha_device.zdo_cluster_handler.unique_id == f"{str(zha_device.ieee)}:{zha_device.name}_ZDO" ) - - -async def test_onoff_client_cluster_handler_on_with_timed_off(zha_gateway: Gateway): - """Test that OnOffClientClusterHandler handles on_with_timed_off command. - - This test verifies that motion sensors with output/client OnOff clusters - properly update the on_off attribute when sending on_with_timed_off commands, - and that the attribute is automatically set to off after the timer expires. - """ - zigpy_device = create_mock_zigpy_device( - zha_gateway, - { - 1: { - SIG_EP_INPUT: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - ], - SIG_EP_OUTPUT: [ - OnOff.cluster_id, - ], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - }, - model="Motion Sensor", - manufacturer="Test", - ieee="00:15:8e:01:01:02:03:05", - ) - - zha_device = await join_zigpy_device(zha_gateway, zigpy_device) - - on_off_ch = zha_device.endpoints[1].client_cluster_handlers["1:0x0006_client"] - assert on_off_ch is not None - - # Verify initial state - on_off should be None (not set) - assert on_off_ch.on_off is None - - # Simulate motion sensor sending on_with_timed_off command - # on_off_control=0 means always accept, on_time=1800 (180 seconds in 10ths) - on_off_ch.cluster_command( - 106, # tsn - OnOff.ServerCommandDefs.on_with_timed_off.id, - [0, 1800, 0], # on_off_control, on_time, off_wait_time - ) - - # Verify on_off is now True - assert on_off_ch.on_off == t.Bool.true - - # Advance time by less than the timeout - should still be on - await asyncio.sleep(100) - assert on_off_ch.on_off == t.Bool.true - - # Advance time past the timeout (180 seconds total) - await asyncio.sleep(100) - assert on_off_ch.on_off == t.Bool.false - - # Test that a new on_with_timed_off resets the timer - on_off_ch.cluster_command( - 107, - OnOff.ServerCommandDefs.on_with_timed_off.id, - [0, 500, 0], # 50 seconds - ) - assert on_off_ch.on_off == t.Bool.true - - # Send another command before timer expires - should reset - await asyncio.sleep(30) - on_off_ch.cluster_command( - 108, - OnOff.ServerCommandDefs.on_with_timed_off.id, - [0, 500, 0], # 50 seconds from now - ) - assert on_off_ch.on_off == t.Bool.true - - # Wait 40 seconds - old timer would have fired, but new one shouldn't - await asyncio.sleep(40) - assert on_off_ch.on_off == t.Bool.true - - # Wait for new timer to expire - await asyncio.sleep(20) - assert on_off_ch.on_off == t.Bool.false - - # Test on_off_control=1 (only accept when already on) - # When off, should not accept - on_off_ch.cluster_command( - 109, - OnOff.ServerCommandDefs.on_with_timed_off.id, - [1, 500, 0], # on_off_control=1 - ) - assert on_off_ch.on_off == t.Bool.false - - # Turn on first, then on_off_control=1 should work - on_off_ch.cluster_command( - 110, - OnOff.ServerCommandDefs.on.id, - [], - ) - assert on_off_ch.on_off == t.Bool.true - - on_off_ch.cluster_command( - 111, - OnOff.ServerCommandDefs.on_with_timed_off.id, - [1, 300, 0], # on_off_control=1, 30 seconds - ) - assert on_off_ch.on_off == t.Bool.true - - await asyncio.sleep(35) - assert on_off_ch.on_off == t.Bool.false From 3443f945b6e7c114f222319a0c211138cb9ed327 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:30:05 +0100 Subject: [PATCH 5/9] Remove `cluster_command` logic from (server) `OnOffClusterHandler` --- zha/zigbee/cluster_handlers/general.py | 41 -------------------------- 1 file changed, 41 deletions(-) diff --git a/zha/zigbee/cluster_handlers/general.py b/zha/zigbee/cluster_handlers/general.py index f959d31ff..a6f36cbbe 100644 --- a/zha/zigbee/cluster_handlers/general.py +++ b/zha/zigbee/cluster_handlers/general.py @@ -611,7 +611,6 @@ class OnOffClusterHandler(ClusterHandler): def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: """Initialize OnOffClusterHandler.""" super().__init__(cluster, endpoint) - self._off_listener: asyncio.TimerHandle | None = None if TUYA_PLUG_ONOFF in endpoint.device.exposes_features: self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() @@ -647,46 +646,6 @@ async def turn_off(self) -> None: raise ZHAException(f"Failed to turn off: {result[1]}") self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd in ( - OnOff.ServerCommandDefs.off.name, - OnOff.ServerCommandDefs.off_with_effect.name, - ): - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - elif cmd in ( - OnOff.ServerCommandDefs.on.name, - OnOff.ServerCommandDefs.on_with_recall_global_scene.name, - ): - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.true) - elif cmd == OnOff.ServerCommandDefs.on_with_timed_off.name: - should_accept = args[0] - on_time = args[1] - # 0 is always accept 1 is only accept when already on - if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): - if self._off_listener is not None: - self._off_listener.cancel() - self._off_listener = None - self.cluster.update_attribute( - OnOff.AttributeDefs.on_off.id, t.Bool.true - ) - if on_time > 0: - self._off_listener = asyncio.get_running_loop().call_later( - (on_time / 10), # value is in 10ths of a second - self.set_to_off, - ) - elif cmd == "toggle": - self.cluster.update_attribute( - OnOff.AttributeDefs.on_off.id, not bool(self.on_off) - ) - - def set_to_off(self, *_): - """Set the state to off.""" - self._off_listener = None - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - async def async_update(self): """Initialize cluster handler.""" if self.cluster.is_client: From e4ce5c3f2baa86d643961b05ab2659387ca01e51 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:34:49 +0100 Subject: [PATCH 6/9] Also test toggle command --- tests/test_binary_sensor.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 2c49f76e1..1da4eec65 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -347,3 +347,37 @@ async def test_onoff_client_binary_sensor_on_with_timed_off( # Binary sensor should now be off assert entity.is_on is False + + # Test toggle command + on_off_ch.cluster_command( + 108, + OnOff.ServerCommandDefs.toggle.id, + [], + ) + await zha_gateway.async_block_till_done() + assert entity.is_on is True + + on_off_ch.cluster_command( + 109, + OnOff.ServerCommandDefs.toggle.id, + [], + ) + await zha_gateway.async_block_till_done() + assert entity.is_on is False + + # Test off command + on_off_ch.cluster_command( + 110, + OnOff.ServerCommandDefs.on.id, + [], + ) + await zha_gateway.async_block_till_done() + assert entity.is_on is True + + on_off_ch.cluster_command( + 111, + OnOff.ServerCommandDefs.off.id, + [], + ) + await zha_gateway.async_block_till_done() + assert entity.is_on is False From b9cf1918efe6f0b07ba7ba8e77728683279eea0c Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 13:34:59 +0100 Subject: [PATCH 7/9] Also test `on_with_timed_off` cancelling existing timer --- tests/test_binary_sensor.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 1da4eec65..381340752 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -341,8 +341,17 @@ async def test_onoff_client_binary_sensor_on_with_timed_off( # Binary sensor should now be on assert entity.is_on is True - # Advance time past the timeout (180 seconds) - await asyncio.sleep(200) + # Send another on_with_timed_off while timer is active (covers timer cancel logic) + on_off_ch.cluster_command( + 107, + OnOff.ServerCommandDefs.on_with_timed_off.id, + [0, 500, 0], # 50 seconds + ) + await zha_gateway.async_block_till_done() + assert entity.is_on is True + + # Advance time past the new timeout (50 seconds) + await asyncio.sleep(60) await zha_gateway.async_block_till_done() # Binary sensor should now be off From 05e557984cfe981791493a58d376770f6c7b2bdd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 16:01:21 +0100 Subject: [PATCH 8/9] Replace `create_mock_zigpy_device` with device from IKEA motion sensor diagnostics --- tests/test_binary_sensor.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 381340752..c031938bc 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -23,6 +23,7 @@ join_zigpy_device, send_attributes_report, update_attribute_cache, + zigpy_device_from_json, ) from zha.application import Platform from zha.application.gateway import Gateway @@ -298,23 +299,9 @@ async def test_onoff_client_binary_sensor_on_with_timed_off( This tests motion sensors that use output/client OnOff clusters and send on_with_timed_off commands when motion is detected. """ - zigpy_device = create_mock_zigpy_device( - zha_gateway, - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.PowerConfiguration.cluster_id, - ], - SIG_EP_OUTPUT: [ - general.OnOff.cluster_id, - ], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SENSOR, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - }, - model="Motion Sensor", - manufacturer="Test", + zigpy_device = await zigpy_device_from_json( + zha_gateway.application_controller, + "tests/data/devices/ikea-of-sweden-tradfri-motion-sensor.json", ) zha_device = await join_zigpy_device(zha_gateway, zigpy_device) From 538d715b43c96f4039923da57697e9d68fada2fa Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Jan 2026 16:28:29 +0100 Subject: [PATCH 9/9] Use `make_zcl_header` --- tests/test_binary_sensor.py | 50 +++++++++++++------------------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index c031938bc..abe604e37 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -21,6 +21,7 @@ find_entity, get_entity, join_zigpy_device, + make_zcl_header, send_attributes_report, update_attribute_cache, zigpy_device_from_json, @@ -312,28 +313,25 @@ async def test_onoff_client_binary_sensor_on_with_timed_off( # Initial state should be off assert entity.is_on is False - # Get the client cluster handler - on_off_ch = zha_device.endpoints[1].client_cluster_handlers["1:0x0006_client"] - assert on_off_ch is not None + # Get the client/output cluster + cluster = zigpy_device.endpoints[1].out_clusters[OnOff.cluster_id] # Simulate motion sensor sending on_with_timed_off command # on_off_control=0 means always accept, on_time=1800 (180 seconds in 10ths) - on_off_ch.cluster_command( - 106, # tsn - OnOff.ServerCommandDefs.on_with_timed_off.id, - [0, 1800, 0], # on_off_control, on_time, off_wait_time + hdr = make_zcl_header( + OnOff.ServerCommandDefs.on_with_timed_off.id, global_command=False ) + cluster.handle_message(hdr, [0, 1800, 0]) await zha_gateway.async_block_till_done() # Binary sensor should now be on assert entity.is_on is True # Send another on_with_timed_off while timer is active (covers timer cancel logic) - on_off_ch.cluster_command( - 107, - OnOff.ServerCommandDefs.on_with_timed_off.id, - [0, 500, 0], # 50 seconds + hdr = make_zcl_header( + OnOff.ServerCommandDefs.on_with_timed_off.id, global_command=False ) + cluster.handle_message(hdr, [0, 500, 0]) # 50 seconds await zha_gateway.async_block_till_done() assert entity.is_on is True @@ -345,35 +343,23 @@ async def test_onoff_client_binary_sensor_on_with_timed_off( assert entity.is_on is False # Test toggle command - on_off_ch.cluster_command( - 108, - OnOff.ServerCommandDefs.toggle.id, - [], - ) + hdr = make_zcl_header(OnOff.ServerCommandDefs.toggle.id, global_command=False) + cluster.handle_message(hdr, []) await zha_gateway.async_block_till_done() assert entity.is_on is True - on_off_ch.cluster_command( - 109, - OnOff.ServerCommandDefs.toggle.id, - [], - ) + hdr = make_zcl_header(OnOff.ServerCommandDefs.toggle.id, global_command=False) + cluster.handle_message(hdr, []) await zha_gateway.async_block_till_done() assert entity.is_on is False - # Test off command - on_off_ch.cluster_command( - 110, - OnOff.ServerCommandDefs.on.id, - [], - ) + # Test on/off command + hdr = make_zcl_header(OnOff.ServerCommandDefs.on.id, global_command=False) + cluster.handle_message(hdr, []) await zha_gateway.async_block_till_done() assert entity.is_on is True - on_off_ch.cluster_command( - 111, - OnOff.ServerCommandDefs.off.id, - [], - ) + hdr = make_zcl_header(OnOff.ServerCommandDefs.off.id, global_command=False) + cluster.handle_message(hdr, []) await zha_gateway.async_block_till_done() assert entity.is_on is False