-
Notifications
You must be signed in to change notification settings - Fork 0
HomeKit Command Sequence
How a HomeKit write (e.g. a position change in the iOS Home app) travels through
SomfyShadeController to SomfyShade::sendCommand and the RF transmitter, and how
shade state is reported back to HomeKit.
sequenceDiagram
autonumber
actor User as iOS Home app
participant HAP as HAP stack<br/>(hap-loop task)
participant HK as HomeKit.cpp<br/>shade_write()
participant Ctrl as SomfyShadeController
participant Q as SomfyCommandQueue
participant Main as mainLoop<br/>(app_main task)
participant Shade as SomfyShade
participant Seq as SomfyTargetSequencer
participant Tx as SomfyCommandTransmitter<br/>→ SomfyRemote
participant RF as Transceiver (RMT/CC1101)
Note over User,RF: Command path — HomeKit write → RF frame
User->>HAP: set TargetPosition / HoldPosition
HAP->>HK: shade_write(write_data, count, serv_priv=shade)
activate HK
alt TargetPosition
HK->>HK: undo flipPosition → somfy target (0–100)
HK->>Ctrl: enqueueShadeTargetForced(shadeId, target)
Ctrl->>Q: push(ShadeTargetForced)
else HoldPosition (true)
HK->>Ctrl: enqueueShadeCommand(shadeId, Stop, MOVE_REPEATS)
Ctrl->>Q: push(ShadeCommand)
end
HK->>HAP: hap_char_update_val() + HAP_STATUS_SUCCESS
deactivate HK
HAP-->>User: write ack (immediate, RF not yet sent)
Note over Main,RF: Later, on the main loop task
Main->>Ctrl: loop()
activate Main
Ctrl->>Ctrl: drainCommandQueue()
Ctrl->>Q: empty() / ready() / txBusy()?
alt queue ready && TX idle
Q-->>Ctrl: pop() → queued_cmd_t
alt ShadeTargetForced
Ctrl->>Shade: moveToTargetForced(target)
Shade->>Seq: moveToTargetForced(pos, tilt)
Seq->>Shade: sendCommand(cmd, repeat, stepSize)
else ShadeCommand
Ctrl->>Shade: sendCommand(Stop, MOVE_REPEATS)
end
Shade->>Tx: commandTransmitter.sendCommand(...)
Tx->>RF: emit Somfy RF frame
else not ready / TX busy
Ctrl-->>Main: skip (retry next tick)
end
deactivate Main
Note over Main,User: State feedback path — movement → HomeKit notify
Main->>Shade: checkMovement() (each loop tick)
Shade->>Shade: update currentPos / direction / target
Shade->>HK: emitState() → homekit.notifyShadeState(shade)
HK->>HK: transformPosition() (apply flipPosition)
HK->>HAP: hap_char_update_val(CurrentPosition / TargetPosition / PositionState)
HAP-->>User: characteristic change notifications
A button press in the web UI (or any REST client) issues an HTTP request to
/shadeCommand with command=Up (or a target for a position slider). Unlike the
HomeKit callback, the synchronous WebServer handler already runs on the
app_main task — but it still only enqueues. The command converges on the same
SomfyCommandQueue and is drained by the very next drainCommandQueue() in the loop,
so the execution half of the diagram below is identical to the HomeKit path.
sequenceDiagram
autonumber
actor User as Web UI / REST client
participant Web as WebServer<br/>handleShadeCommand()<br/>(runs on app_main task)
participant Ctrl as SomfyShadeController
participant Q as SomfyCommandQueue
participant Main as mainLoop<br/>(app_main task)
participant Shade as SomfyShade
participant Tx as SomfyCommandTransmitter<br/>→ SomfyRemote
participant RF as Transceiver (RMT/CC1101)
Note over User,RF: Web command path — button press → RF frame
User->>Web: HTTP /shadeCommand?shadeId=N&command=Up
activate Web
Web->>Web: translateSomfyCommand("up") → somfy_commands::Up
alt target <= 100 (position slider)
Web->>Ctrl: enqueueShadeTarget(shadeId, transformPosition(target))
Ctrl->>Q: push(ShadeTarget)
else command button (Up / Down / My / Stop)
Web->>Ctrl: enqueueShadeCommand(shadeId, command, repeat, stepSize)
Ctrl->>Q: push(ShadeCommand)
end
Web-->>User: sendShadeJSON() — HTTP 200 (RF not yet sent)
deactivate Web
Note over Main,RF: Same drain path as HomeKit — next loop tick
Main->>Ctrl: loop() → drainCommandQueue()
activate Main
Ctrl->>Q: empty() / ready() / txBusy()?
alt queue ready && TX idle
Q-->>Ctrl: pop() → queued_cmd_t
alt ShadeCommand
Ctrl->>Shade: sendCommand(Up, repeat, stepSize)
else ShadeTarget
Ctrl->>Shade: moveToTarget(target)
end
Shade->>Tx: commandTransmitter.sendCommand(...)
Tx->>RF: emit Somfy RF frame
else not ready / TX busy
Ctrl-->>Main: skip (retry next tick)
end
deactivate Main
Difference vs. HomeKit: HomeKit's
shade_writeruns on the hap-loop task and usesenqueueShadeTargetForced(boosted/auto-stop move). The web handler runs on the app_main task and uses the plainenqueueShadeTarget/enqueueShadeCommand. Both deposit into the oneSomfyCommandQueue, so RF transmission, throttling, and theemitState→ WebSocket /notifyShadeStatefeedback are shared.
The web UI has a second, distinct endpoint for the press-and-hold / live button
gesture: /repeatCommand (handleRepeatCommand). Unlike /shadeCommand, this path
calls the shade directly and synchronously — it bypasses the command queue
entirely. It is the latency-sensitive fast-path for a button that is being held
down, where the UI fires repeated requests for as long as the button is pressed.
The handler branches on isLastCommand(command):
-
First / different press → send a fresh frame (new rolling code) via
sendCommand. -
Same command repeated (button still held) → re-send the identical frame (same
rolling code) via
repeatFrame— exactly how a physical Somfy remote implements hold.
sequenceDiagram
autonumber
actor User as Web UI (button held)
participant Web as WebServer<br/>handleRepeatCommand()<br/>(runs on app_main task)
participant Shade as SomfyShade
participant Remote as SomfyRemote
participant RF as Transceiver (RMT/CC1101)
Note over User,RF: /repeatCommand — DIRECT call, no queue
loop while button held (UI re-fires)
User->>Web: HTTP /repeatCommand?shadeId=N&command=Up
activate Web
Web->>Shade: isLastCommand(command)?
alt new / different command
Web->>Shade: sendCommand(command, repeat, stepSize)
Shade->>Remote: (new rolling code)
else same command as last (held)
Web->>Shade: repeatFrame(repeat)
Shade->>Remote: repeatFrame() — reuse lastFrame
end
alt TX idle
Remote->>RF: beginTransmit() + emit frame
else txBusy()
Remote-->>Web: skip repeat (non-blocking, no spin-wait)
end
Web-->>User: toJSONRef() — HTTP 200
deactivate Web
end
Why bypassing the queue is safe here: the synchronous
WebServerhandler already runs on theapp_maintask — the same task that drains the queue — andrepeatFrameis non-blocking (it checkstxBusy()and skips rather than spin-waiting, SomfyRemote.cpp:179). The trade-off is deliberate: it gives up the queue's throttling/ordering in exchange for immediacy on the hold gesture.
| Trigger | Endpoint / callback | Task | Mechanism | Reaches shade via |
|---|---|---|---|---|
| HomeKit position write | shade_write |
hap-loop | enqueue (forced) |
enqueueShadeTargetForced → drain |
| Web tap (Up / target) | /shadeCommand |
app_main | enqueue |
enqueueShadeCommand / enqueueShadeTarget → drain |
| Web press-and-hold | /repeatCommand |
app_main | direct |
sendCommand / repeatFrame (no queue) |
| Concern | Where | Why |
|---|---|---|
| Task decoupling |
shade_write enqueues; drainCommandQueue executes |
RF send blocks; keeping it off the HAP loop task and bounded on the WDT-monitored main loop avoids stalls/panics |
| Throttling |
cmdQueue.ready() (CMD_QUEUE_DRAIN_MS) + transceiver.txBusy()
|
One frame drained per tick when the radio is free; prevents flooding the RMT TX queue |
| Position inversion |
flipPosition in shade_write and transformPosition in notifyShadeState
|
HomeKit uses 0 = closed / 100 = open; the inversion is undone on the way in and re-applied on the way out |
| Reliability boost |
MOVE_REPEATS repeat count |
A single HomeKit tap is delivered with enough RF repeats that the app needs no retry |
-
main/HomeKit.cpp:88 —
shade_write(TargetPosition / HoldPosition) -
main/HomeKit.cpp:272 —
notifyShadeState(CurrentPosition / TargetPosition / PositionState) -
main/somfy/SomfyShadeController.cpp:612 —
drainCommandQueue -
main/somfy/SomfyShadeController.cpp:681 —
enqueueShadeTargetForced -
main/somfy/SomfyShadeController.cpp:735 —
loop(drain +checkMovement) -
main/somfy/shade/SomfyShade.cpp:520 —
sendCommand -
main/somfy/shade/SomfyShade.cpp:540 —
moveToTargetForced -
main/somfy/shade/SomfyMQTTPublisher.cpp:147 —
emitState→homekit.notifyShadeState -
main/web/WebShades.cpp:77 —
handleShadeCommand(web Up/Down/My/Stop + position target) -
main/web/Web.cpp:639 —
/shadeCommandroute registration (REST :8081 and HTTP :80) -
main/web/Web.cpp:318 —
handleRepeatCommand(/repeatCommand, direct sendCommand / repeatFrame) -
main/somfy/SomfyRemote.cpp:161 —
isLastCommand/repeatFrame(hold gesture frame reuse)