v0.1.12
The Observe relation lifecycle gets its own vocabulary this release: confirmed relations (#68), targeted refresh and unsubscribe (#69), two-phase shutdown (#67), and probationary delivery with paced teardown (#71). Alongside it, a CoAP-over-TCP wire codec (#70).
Two-phase DTLS session shutdown (#67)
close() now flushes an authenticated close-notify record before closing the socket. RT-OCF frees its DTLS peer on close-notify, so a session that ends this way does not leave the appliance holding state a quick reconnect then collides with.
Hosts that stop network work before their blocking executor drains can split the teardown. quiesce_for_close() is terminal: it interrupts an in-progress handshake, wakes pending requests and notification refetches, and rejects new work while keeping the established socket. A later close() finishes the job.
Confirmed Observe relations (#68)
An RFC 7641 relation is confirmed only by a valid Observe response option, and duplicate or stale 24-bit sequence values are no longer delivered. Some older Samsung firmware answers a registration once and omits that option entirely. For those devices a plain initial 2.05 is probationary until a later packet arrives on the same token with a different Message ID, which keeps a one-off answer from being mistaken for a lasting subscription. on_observe_pending, on_legacy_notification, and on_observe_error let a consumer keep that compatibility path separate from confirmed notifications and from ordinary polling.
Targeted Observe operations (#69)
refresh_observes(paths) renews only the relations that need it and leaves unrelated observations alone, returning the hrefs that succeeded and a failure count. unsubscribe(href) retires one relation. An operation lock serialises relation mutation, so a periodic refresh can no longer interleave re-subscribes onto a session that is closing.
The deregistration sweep in close() is now paced at the session rate limit, one request per interval.
Probationary delivery and paced quiesced teardown (#71)
Two follow-ups from the three above, both found after merge.
A probationary relation withheld both its payload and its status. The complete representation now reaches on_notification while the relation itself stays probationary, so the two decisions are separate: the state is useful immediately, the subscription still has to prove itself. Same-MID retransmissions stay suppressed, and a blockwise probationary response is re-read in full before delivery, so a caller receives the whole representation.
The two-phase path sent no deregistrations at all. quiesce_for_close() set the lifecycle cancel that the sweep checked, so the sweep was skipped and only close-notify went out. Relation metadata now survives quiescence, and the later close() sends exact paced deregistrations through a teardown-only send path before flushing close-notify. Ordinary requests stay rejected after quiescence; only the closing thread is admitted, and only for that sweep.
CoAP-over-TCP framing codec (#70)
Adds smartthings_local.protocol.coap_tcp: a dependency-free encoder and strict parser for RFC 8323 reliable-transport framing, a CSM builder for Max-Message-Size and Block-Wise-Transfer, GET/POST/DELETE helpers with repeated query options and bounded payloads, and an incremental decoder that buffers at most one incomplete frame and rejects an oversized declaration as soon as the length prefix completes. Decoded messages keep tokens, options, and payload bytes out of repr.
The motivation is Block2. Over UDP an RT-OCF appliance fits one block in one datagram and expects a Block2 continuation its request handler drops, so a large resource can only ever be read as block 0. The same read over CoAP-over-TCP returns complete in a single frame. This release ships the wire codec only: it chooses no carrier, opens no socket, and performs no setup or ownership operation.
Boundary
coap_tcp has no caller yet and no carrier. Note that CoAP-over-TCP carries no Message ID, so the token is the only thing matching a response to its request; the request builders currently default to an empty token, and anything that pipelines needs to allocate tokens and demultiplex on them.
#71's quiesced teardown is covered by tests alone, since the bridge in this repo calls the single-phase close(). What hardware did cover: two container recreates against a dryer and an oven, both reconnecting cleanly, and a graceful shutdown measured at 4.05 s from signal to exit with 11 relations per appliance, which matches the paced sweep and sits well inside a 10 s stop grace. That figure comes from process log timestamps. A wire capture would be the stronger evidence, and remains open.