Skip to content

HomeKit Command Sequence

Matthias Jobst edited this page May 29, 2026 · 2 revisions

HomeKit → SomfyShade 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.

Sequence diagram

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
Loading

Web button path (e.g. the Up button)

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
Loading

Difference vs. HomeKit: HomeKit's shade_write runs on the hap-loop task and uses enqueueShadeTargetForced (boosted/auto-stop move). The web handler runs on the app_main task and uses the plain enqueueShadeTarget / enqueueShadeCommand. Both deposit into the one SomfyCommandQueue, so RF transmission, throttling, and the emitState → WebSocket / notifyShadeState feedback are shared.

Web press-and-hold path (/repeatCommand)

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
Loading

Why bypassing the queue is safe here: the synchronous WebServer handler already runs on the app_main task — the same task that drains the queue — and repeatFrame is non-blocking (it checks txBusy() 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.

The three web/HomeKit entry points at a glance

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)

Key takeaways

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

Source references