Skip to content

Conversation

@TheJulianJES
Copy link
Contributor

@TheJulianJES TheJulianJES commented Jan 29, 2026

#615 removed handle_on_off_output_cluster_exception, so the OnOffClusterHandler is no longer used as a cluster handler for client/output devices. However, the cluster_command logic for on_with_timed_off was only present there.

This PR adds it to the OnOffClientClusterHandler to fix the issue of motion binary sensors not updating on cluster commands. The logic is also removed from the (server) OnOffClusterHandler, as that should (hopefully) not be used anymore.

@codecov
Copy link

codecov bot commented Jan 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.32%. Comparing base (33176de) to head (538d715).
⚠️ Report is 1 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #635      +/-   ##
==========================================
+ Coverage   97.10%   97.32%   +0.21%     
==========================================
  Files          62       62              
  Lines       10710    10716       +6     
==========================================
+ Hits        10400    10429      +29     
+ Misses        310      287      -23     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TheJulianJES TheJulianJES added the bugfix This PR fixes a bug label Jan 29, 2026
@TheJulianJES
Copy link
Contributor Author

We may need a similar fix for alarm control panels, though they were already broken somewhat before these changes.

Comment on lines 321 to 325
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
)
Copy link
Contributor Author

@TheJulianJES TheJulianJES Jan 29, 2026

Choose a reason for hiding this comment

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

We probably shouldn't directly call the cluster_command method on the client cluster handler.
Instead, we can use listener_event with "cluster_command" or use make_zcl_header:

# Option 1: call `listener_event` with `"cluster_command"`, similar to existing `async_test_iaszone_on_off` test
cluster.listener_event(
    "cluster_command", 1, OnOff.ServerCommandDefs.on_with_timed_off.id, [0, 1800, 0]
)

# Option 2: Use `make_zcl_header`
hdr = make_zcl_header(
    OnOff.ServerCommandDefs.on_with_timed_off.id, global_command=False
)
cluster.handle_message(hdr, [0, 1800, 0])

What do we prefer? handle_message with make_zcl_header, so we're not relying on listener_event cluster_command staying the same over time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've opted to use handle_message with make_zcl_header in the tests now. It's different to the async_test_iaszone_on_off test, which also checks cluster handler functionality, but that can be changed in the future. This also tests more of the "whole stack".

Comment on lines -597 to -635
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the entire cluster_command handling for the server OnOff cluster handler. Unfortunately, it was never covered by tests, hence we didn't catch this.

I don't think it can be used at all for the server handler, so should be fine to remove.

@TheJulianJES TheJulianJES marked this pull request as ready for review January 29, 2026 16:35
@TheJulianJES
Copy link
Contributor Author

Tested these changes with an IKEA TRADFRI motion sensor and it works properly again.

Copy link
Contributor

@puddly puddly left a comment

Choose a reason for hiding this comment

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

Thanks!

@puddly puddly merged commit be4aa43 into zigpy:dev Jan 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix This PR fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants