Hue.Bridge.Writes (Hue v0.2.0)

Copy Markdown View Source

The pending-write queue: coalescing, and Hue's per-type rate limits.

This is the reason writes go through Hue.Bridge's process while reads bypass it. Nothing about a read needs serialising. Coalescing and pacing are both statements about all writes together, and there is nowhere else in the design to make them.

Coalescing merges, it does not replace

Twenty slider drags on one light collapse to one request carrying the last value. Two writes setting different things — on: true, then brightness: 40 — collapse to one request carrying both. Deep-merging the new body over the pending one gives both behaviours from a single rule: last-wins per leaf, union across keys.

Pacing is per type

Roughly 10 writes/second for lights and 1/second for grouped_lights, per Hue's own guidance. One request leaves per interval per type, not per pending item: three lights queued at once go out over 300 ms, and a queued grouped_light does not delay them.

A scene recall is paced at the grouped_light rate rather than the light rate, even though it is one HTTP request like a light write. Hue does not document a scene-recall limit, so this is inferred rather than cited — see the comment on @scene_interval.

No clock

Every function that cares about time takes now as a monotonic millisecond argument. The queue holds no timers and reads no clock, so its behaviour under a 1-second grouped_light interval is tested by passing 1_000, not by sleeping for a second.

Summary

Functions

Milliseconds until a write of type may be sent, 0 if now, or :never if nothing of that type is pending.

Queues a write, merging it into whatever is already pending for that target.

An empty queue.

Every type with something pending.

Takes the oldest pending write of type and records the send at now.

Types

key()

@type key() :: {atom(), String.t()}

t()

@type t() :: %Hue.Bridge.Writes{
  collapsed: %{required(key()) => non_neg_integer()},
  last_sent_at: %{required(atom()) => integer()},
  order: [key()],
  pending: %{required(key()) => map()}
}

Functions

due_in(writes, type, now)

@spec due_in(t(), atom(), integer()) :: non_neg_integer() | :never

Milliseconds until a write of type may be sent, 0 if now, or :never if nothing of that type is pending.

enqueue(writes, key, body)

@spec enqueue(t(), key(), map()) :: {t(), non_neg_integer()}

Queues a write, merging it into whatever is already pending for that target.

Returns the queue and the number of writes that have now been absorbed into this one pending body — 0 the first time, 1 the second, and so on. The server reports that as [:hue, :write, :coalesced].

new()

@spec new() :: t()

An empty queue.

pending_types(writes)

@spec pending_types(t()) :: [atom()]

Every type with something pending.

take(writes, type, now)

@spec take(t(), atom(), integer()) :: {:ok, key(), map(), t()} | :empty

Takes the oldest pending write of type and records the send at now.

Returns :empty when nothing of that type is pending. Does not check whether the write is due — that is due_in/3's question, and the server asks it before calling this.