Hue.Pairing (Hue v0.2.0)

Copy Markdown View Source

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/2 routes on the shape of the body, not the status code, and hands the error to Hue.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

Functions

Builds the devicetype string a pairing request sends, in the form "hue_ex##{app}".

Presses through to a bridge and exchanges the link-button press for an application key and a clientkey.

Calls pair/2 repeatedly until it succeeds or timeout milliseconds pass.

Types

keys()

@type keys() :: %{application_key: String.t(), clientkey: String.t()}

Functions

device_type(app)

@spec device_type(String.t()) :: String.t()

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.

pair(bridge, options \\ [])

@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 in device_type/1, e.g. the name of the application asking. Defaults to "elixir". A non-binary raises FunctionClauseError from device_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.

pair_when_pressed(bridge, options \\ [])

@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.