Fix fast polling when PollControl is not on first EP - #1817
TheJulianJES wants to merge 3 commits into
Conversation
`_discover` initialised endpoints sequentially and called `begin_fast_polling` after each one, using a `try/except…else` to mark fast polling as initiated. But `begin_fast_polling` also returns silently when no `PollControl` cluster is found yet, so the `else` branch fired even when nothing was bound. On a fresh multi-endpoint device whose `PollControl` lives on a later endpoint, we'd skip every subsequent retry once that first (futile) call returned, leaving fast polling unbound for the rest of the interview. Use `_fast_polling` (only set on a real bind) as the retry signal so we keep trying after each endpoint until `PollControl` actually shows up.
Covers the case where `PollControl` lives on a later non-ZDO endpoint: the first `begin_fast_polling` call (after ep1 init) returns silently because no endpoint has `PollControl` yet, and the retry must still fire once ep2 has been initialised. Fails against the prior `try/except…else` logic and passes against the `_fast_polling`-based retry signal.
…trol cases The earlier regression test only covered PollControl on a later endpoint. Add two more cases to guard the working paths against future regressions: - PollControl on the first endpoint: bind + fast_poll_timeout write must still happen during interview. - No PollControl on any endpoint: no Bind_req should be sent at all. Also strengthen the later-endpoint test to assert the bind and fast_poll_timeout write, and factor the shared mock setup into a helper.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #1817 +/- ##
==========================================
- Coverage 99.55% 99.55% -0.01%
==========================================
Files 64 64
Lines 13212 13210 -2
==========================================
- Hits 13153 13151 -2
Misses 59 59 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Looking at the technical manual for this device, it seems like there's an "auto bind" for any The "Power Configuration" and "Temperature Measurement" may also be bound automatically in "EZ Mode":
This might explain why Z2M doesn't bind them. Though the temperature cluster is on another endpoint anyways, which seems to not share the 4 binding limit with the main EP. |
|
In general, we should also investigate whether binding Z2M also binds the cluster on a lot of devices, but I'm not sure why. |
|
Ah, Z2M does set up attribute reporting for |
|
With the 4 binding slots on EP 35 filled, excluding |
|
These are the Frient devices in our diagnostics that have more than 4 clusters on an endpoint that ZHA likely binds:
In particular, these are the
With this PR, we should ideally remove and reset the |
Proposed change
This fixes an issue where fast polling is not correctly activated during initial pairing of a device (or a re-interview) if the
PollControlcluster is not on the first endpoint. This is an issue with all/many Frient end-devices.Skim the AI description below for more information. Note: Also read the related issue at the bottom.
Impact on Frient devices with limited binding table
However, this breaks Frient devices – at least my Frient Intelligent Smoke Alarm that shipped with v4.0.8.
Currently, we automatically bind 4 clusters on endpoint 35 using ZHA (
BinaryInput,PowerConfiguration,IasWd,IasZone). They all bind properly without the patch. With it,PollControlis also bound now, which then causes aTABLE_FULLerror forIasZone...PollControlcluster and states that only 4 clusters can be bound: https://github.com/Koenkk/zigbee-herdsman-converters/blob/42d3b11e44fbdccf1bf75c0d63f8983e030af622/src/devices/develco.ts#L426-L432PollControlcluster here: https://github.com/Koenkk/zigbee-herdsman/blob/b7bf6264d3f178fcbf53d02c2906b0fe52e941d8/src/controller/model/device.ts#L1096-L1109PollControlbinding during pairing..TemperatureMeasurementbut that's on EP 38, not 35. The 4 bind limit is apparently per EP.BinaryInputbind, as we don't use that cluster, but rely onIasZoneinstead.reliabilityattribute as a fault sensorIasZonebind because the write to "IAS CIE Address" should deal with the change notifications instead?PowerConfigurationcluster on this endpoint.This needs to be checked further.
AI description
Bug
In
Device._discover(), fast polling is supposed to be set up "as soon as we are aware of aPollControlcluster" —begin_fast_pollingis called after each endpoint is initialised, until it succeeds. The retry-stop signal was implemented as theelsebranch of atry/except (TimeoutError, DeliveryError):But
begin_fast_polling()also returns silently (no exception) whenfind_cluster(PollControl.cluster_id)raisesValueError— i.e. when no endpoint has aPollControlcluster yet. Endpoint clusters aren't populated untilep.initialize()runs on that specific endpoint, so on a fresh multi-endpoint device whosePollControllives on something other than the first endpoint innon_zdo_endpoints, the very first call (after ep1 init) returns silently, theelsebranch fires,initiated_fast_pollingis set toTrue, and the retry on subsequent endpoints is suppressed. NoBind_reqfor cluster0x0020is ever sent, nofast_poll_timeoutwrite happens, the device never starts emittingPollControlcheck-ins, and zigpy never reaches it again.Real-world reproduction
A frient WISZB-131 with endpoints
[1, 35, 38]andPollControlon EP35. After OTA + re-interview, the onlyBind_reqs on the wire are ZHA's configure-phase ones (BinaryInput, PowerConfiguration, TempMeas, IAS Zone) — no0x0020. TheDevice does not support fast pollingdebug line fires once after EP1'sSimple_Desc_rspand zigpy never re-checks. Devices that don't persist their binding table across reboot/firmware change then silently stop sending check-ins.Fix
Use
self._fast_polling(only set on a real bind insidebegin_fast_polling()) as the retry-stop signal instead of the absence-of-exception:Entry-state
initiated_fast_polling = self._fast_pollingis preserved at the top of the block, so devices that are already in fast polling mode (e.g. mid-OTA re-init) still skip the loop entirely. The symmetricif self.all_endpoints_init: ...branch is unchanged — that path only callsbegin_fast_polling()once and is unaffected.Test coverage
Three new tests in
tests/test_device.py:test_initialize_fast_polling_pollcontrol_on_later_endpoint— multi-endpoint device withPollControlon ep2. Assertsdev._fast_pollingisTrue, thatbind()was called, and thatfast_poll_timeoutwas written. Verified to fail against pre-fix code (the regression target).test_initialize_fast_polling_pollcontrol_on_first_endpoint— same shape butPollControlon ep1, ensuring the previously-working path still works after the change.test_initialize_fast_polling_no_pollcontrol— device with noPollControlanywhere; asserts noBind_reqis sent and_fast_pollingstaysFalse.A small helper
_make_fast_polling_init_mocksfactors the sharedmockepinit/mock_ep_get_model_infosetup.Full
tests/test_device.py(66 tests) passes.Out of scope
A separate, orthogonal issue exists in
begin_fast_polling()itself:Cluster.bind()returns theBind_reqstatus tuple rather than raising on non-SUCCESS, so aTABLE_FULL(or other) bind failure is silently ignored andself._fast_pollingis set toTrueregardless. Not addressed here — should be tackled separately.