While debugging a MINI-D (UIID 138) used via cloud (LAN unavailable in my setup), I identified three distinct bugs in v12.14.0. They are technically independent but cascade into each other in practice. Bug 1 is the root cause; bug 2 is its visible consequence; bug 3 is unrelated but uncovered in the same session.
Setup
- Plugin: v12.14.0
- Homebridge: v2.0.2 (HAP v2.1.6)
- Node: v24.16.0
- OS: Linux (Homebridge OS)
- Devices: SONOFF MINI-D (UIID 138) and MINI (UIID 1), eWeLink DW2 contact sensor (UIID 154)
Bug 1 — Cloud no longer echoes client sequence on action: update responses
The eWeLink cloud now responds to update commands with a server-generated sequence, different from the one sent by the client. Since wsp matches request/response by sequence, the promise returned by wsClient.sendRequest never resolves and always times out at 20s.
The device executes the command correctly. The status update arrives via a separate broadcast message. Only the formal ACK matching is broken.
Evidence (debug log, with unpackMessage/extractRequestId patched to log raw traffic):
WS SEND seq=1779924561777 payload={…deviceid:"1000a4837b","params":{"switch":"on"},"action":"update"…}
WS RECV {"error":0,"deviceid":"1000a4837b","apikey":"…","sequence":"1779924571781","params":{…,"switch":"on",…}}
WS RECV PARSED sequence="1779924571781" type=string
WS EXTRACT REQID sequence="1779924571781" type=string
[Task timed out after 10000ms — p-queue]
WS FAIL seq=1779924561777 err=WebSocket request was rejected by timeout (20000 ms)
Note: userOnline login responses do echo back the original sequence correctly (verified in same log). The breakage is specific to action: update.
Affected: every device controlled via cloud. Not visible to users on LAN mode.
File: lib/connection/ws.js, around line 349.
Possible fixes:
- Short-term: send
action: update as fire-and-forget (drop sendRequest, use raw send); rely on the broadcast update messages already handled by the receive path for state confirmation. Works perfectly in my testing, eliminates all timeouts.
- Long-term: maintain a
deviceid + timestamp map and resolve pending commands when a matching update broadcast arrives (proper async correlation).
Bug 2 — sendUpdate retries on timeout cause duplicate physical impulses on momentary/inching devices
lib/connection/ws.js:373 retries up to 3 times when the error message contains 504, timeout, or Timeout. The cloud timeout from Bug 1 matches this condition ("WebSocket request was rejected by timeout (20000 ms)"), so every command is silently retried up to 3 times.
For idempotent writes (lights, regular switches) this is harmless — at worst it re-applies the same state. For inching/momentary devices (MINI-D, MINI-R3, garage door triggers, etc.), each retry produces an additional physical pulse, sending the actuator on/off/on/off uncontrollably.
Symptom observed: user issues "open" from HomeKit → shutter opens, then closes by itself ~20s later, then opens again ~20s after that.
Evidence: retries are silent in normal logs. Added a log.warn before sleep(1000) at line 374, retries appeared as expected:
WS SEND seq=… payload={switches:[{switch:"on",outlet:0}]}
[…20s later…]
*** RETRY *** attempt 1 for 10027a108f after error: WebSocket request was rejected by timeout (20000 ms)
[shutter receives a second pulse and reverses direction]
*** RETRY *** attempt 2 for 10027a108f after error: …
Fix: retrying non-idempotent writes on opaque timeouts is unsafe regardless. Either:
- Remove retry logic for writes (it doesn't truly know if the command was applied), or
- Make it opt-in per UIID via a new
noRetryOnTimeout array in constants.js populated with inching-capable devices.
Bug 2 is a direct symptom of Bug 1, but the underlying design problem (retrying non-idempotent writes) should be fixed regardless of how Bug 1 is addressed.
Bug 3 — Garage Door simulation sends invalid dual-outlet payload on SCM single-channel devices
lib/device/simulation/garage-one.js:123 maps switchSCM devices to setup = 'switchMulti'. The switchMulti payload builder (line ~213) generates:
params.switches = [
{ switch: 'on'/'off', outlet: 0 },
{ switch: 'on'/'off', outlet: 1 },
]
This works for true dual-channel garage controllers (e.g. DUALR3 with separate open/close relays), but switchSCM devices (UIID 77, 78, 81, 107, 112, 138, 160, 191, 209, 264) only have outlet 0 — they just happen to use the switches[] format for one channel.
Observed effect on MINI-D (UIID 138):
- "Open" command: payload
{outlet:0 on, outlet:1 off} → device sees outlet:0 on → fires pulse correctly (outlet:1 is silently ignored).
- "Close" command: payload
{outlet:0 off, outlet:1 on} → device sees outlet:0 off (already off after the previous inching auto-off) → no physical action. Outlet 1 ignored. Command silently dropped.
Result: garage door opens fine, but closing always requires a second attempt.
Fix: detect SCM devices in garage-one.js and emit a single-pulse payload identical to switch-single-inched.js:
const isScm = platformConsts.devices.switchSCM.includes(this.accessory.context.eweUIID)
|| platformConsts.devices.switchSCMPower.includes(this.accessory.context.eweUIID)
case 'switchMulti':
if (isScm) {
params.switches = [{ switch: 'on', outlet: 0 }]
} else {
params.switches = [
{ switch: newPos === 0 ? 'on' : 'off', outlet: 0 },
{ switch: newPos === 1 ? 'on' : 'off', outlet: 1 },
]
}
break
Verified locally: with this change the close command works reliably on every attempt.
I am running with all three fixes applied locally and everything works as expected. Happy to open PRs if desired — let me know your preferred approach for Bug 1 (fire-and-forget vs. proper correlation) before I prepare anything.
While debugging a MINI-D (UIID 138) used via cloud (LAN unavailable in my setup), I identified three distinct bugs in v12.14.0. They are technically independent but cascade into each other in practice. Bug 1 is the root cause; bug 2 is its visible consequence; bug 3 is unrelated but uncovered in the same session.
Setup
Bug 1 — Cloud no longer echoes client
sequenceonaction: updateresponsesThe eWeLink cloud now responds to update commands with a server-generated
sequence, different from the one sent by the client. Sincewspmatches request/response bysequence, the promise returned bywsClient.sendRequestnever resolves and always times out at 20s.The device executes the command correctly. The status update arrives via a separate broadcast message. Only the formal ACK matching is broken.
Evidence (debug log, with
unpackMessage/extractRequestIdpatched to log raw traffic):Note:
userOnlinelogin responses do echo back the original sequence correctly (verified in same log). The breakage is specific toaction: update.Affected: every device controlled via cloud. Not visible to users on LAN mode.
File:
lib/connection/ws.js, around line 349.Possible fixes:
action: updateas fire-and-forget (dropsendRequest, use rawsend); rely on the broadcastupdatemessages already handled by the receive path for state confirmation. Works perfectly in my testing, eliminates all timeouts.deviceid + timestampmap and resolve pending commands when a matchingupdatebroadcast arrives (proper async correlation).Bug 2 —
sendUpdateretries on timeout cause duplicate physical impulses on momentary/inching deviceslib/connection/ws.js:373retries up to 3 times when the error message contains504,timeout, orTimeout. The cloud timeout from Bug 1 matches this condition ("WebSocket request was rejected by timeout (20000 ms)"), so every command is silently retried up to 3 times.For idempotent writes (lights, regular switches) this is harmless — at worst it re-applies the same state. For inching/momentary devices (MINI-D, MINI-R3, garage door triggers, etc.), each retry produces an additional physical pulse, sending the actuator on/off/on/off uncontrollably.
Symptom observed: user issues "open" from HomeKit → shutter opens, then closes by itself ~20s later, then opens again ~20s after that.
Evidence: retries are silent in normal logs. Added a
log.warnbeforesleep(1000)at line 374, retries appeared as expected:Fix: retrying non-idempotent writes on opaque timeouts is unsafe regardless. Either:
noRetryOnTimeoutarray inconstants.jspopulated with inching-capable devices.Bug 2 is a direct symptom of Bug 1, but the underlying design problem (retrying non-idempotent writes) should be fixed regardless of how Bug 1 is addressed.
Bug 3 — Garage Door simulation sends invalid dual-outlet payload on SCM single-channel devices
lib/device/simulation/garage-one.js:123mapsswitchSCMdevices tosetup = 'switchMulti'. TheswitchMultipayload builder (line ~213) generates:This works for true dual-channel garage controllers (e.g. DUALR3 with separate open/close relays), but
switchSCMdevices (UIID 77, 78, 81, 107, 112, 138, 160, 191, 209, 264) only have outlet 0 — they just happen to use theswitches[]format for one channel.Observed effect on MINI-D (UIID 138):
{outlet:0 on, outlet:1 off}→ device seesoutlet:0 on→ fires pulse correctly (outlet:1 is silently ignored).{outlet:0 off, outlet:1 on}→ device seesoutlet:0 off(already off after the previous inching auto-off) → no physical action. Outlet 1 ignored. Command silently dropped.Result: garage door opens fine, but closing always requires a second attempt.
Fix: detect SCM devices in
garage-one.jsand emit a single-pulse payload identical toswitch-single-inched.js:Verified locally: with this change the close command works reliably on every attempt.
I am running with all three fixes applied locally and everything works as expected. Happy to open PRs if desired — let me know your preferred approach for Bug 1 (fire-and-forget vs. proper correlation) before I prepare anything.