Obtains an application key from a bridge.
This is the one place the library still speaks the legacy v1 API, because pairing was never moved to CLIP v2. It carries v1 semantics with it:
- The endpoint is
POST /api— not under/clip/v2. - Application errors arrive at HTTP 200 too, wrapped as
[{"error":{"type":101, …}}]rather than signalled by status.pair/2routes on the shape of the body, not the status code, and hands the error toHue.Error.from_pairing/1. - The round link button on the bridge must have been pressed within roughly the last thirty seconds.
# Press the round link button on the bridge first.
{:ok, keys} = Hue.Pairing.pair(bridge)generateclientkey: true is always sent, so the response also carries the
key the Entertainment streaming API needs. This library does not implement
Entertainment, but the key is requested now so pairing never has to be
repeated later to get it.
First contact has no pin yet
Pairing typically happens before any certificate has been trusted, so
bridge.fingerprint is usually nil. pair/2 builds its request with
Hue.new/2, which already treats a nil fingerprint as "verification off,
this is first contact" — the same rule Hue.Transport.ssl_options/1
documents. Routing through Hue.new/2 also means a caller-supplied
:connect_options is merged (and a colliding :transport_opts rejected)
by the same guard Hue.Client documents, rather than this module
reimplementing it and getting it wrong.
Telemetry
pair/2 wraps its request in :telemetry.span/3 under [:hue, :pairing],
mirroring Hue.Resource. Start metadata carries :method and :path; stop
metadata adds :result (:ok or :error). The returned application key
and clientkey are never included — an application key is exactly as
sensitive as a password, and telemetry handlers are commonly attached
loggers.
Summary
Types
Functions
Builds the devicetype string a pairing request sends, in the form
"hue_ex##{app}".
Hue's v1 API limits devicetype to 40 bytes, so a
long app is truncated to fit. The truncation is byte-safe: it never
splits a multi-byte grapheme, so the result is always valid UTF-8 and
always at most 40 bytes — not 40
characters, which is not the same thing for any app outside ASCII.
@spec pair( Hue.Bridge.Info.t(), keyword() ) :: {:ok, keys()} | {:error, Hue.Error.t()}
Presses through to a bridge and exchanges the link-button press for an application key and a clientkey.
Returns {:error, %Hue.Error{reason: :link_button_not_pressed}} if the
button was not pressed recently enough — that is the expected outcome of
calling this before pressing it, not a bug.
Options
:app— a binary identifying this pairing indevice_type/1, e.g. the name of the application asking. Defaults to"elixir". A non-binary raisesFunctionClauseErrorfromdevice_type/1— that is a caller bug, not a bridge response, so it is not caught.
Every other option is forwarded to Hue.new/2 (and from there to
Req.new/1), so :plug, :connect_options, :receive_timeout, :retry,
and so on all work exactly as they do everywhere else in this library.
:port and :fingerprint default to the values on bridge, but can be
overridden the same way Hue.new/2 allows.
@spec pair_when_pressed( Hue.Bridge.Info.t(), keyword() ) :: {:ok, keys()} | {:error, Hue.Error.t()}
Calls pair/2 repeatedly until it succeeds or timeout milliseconds pass.
This blocks the calling process for up to timeout (default
60000ms) via Process.sleep/1 between attempts, waiting
:poll_interval (default 2000ms) between them. It
is meant for a script, an IEx session, or a one-off Task — never call
it from inside a GenServer callback, where blocking the process for up to
a minute would also block everything else that process is responsible for.
Only :link_button_not_pressed is retried. Any other error — a transport
failure, a bad host, a malformed response — returns immediately, since
waiting and asking again would not change the outcome.
Accepts :timeout and :poll_interval in addition to every option
pair/2 takes; both are milliseconds.