Hue.Resource (Hue v0.2.0)

Copy Markdown View Source

Generic access to every CLIP v2 resource type.

Nothing in this library is a special case of anything else: light, room, scene, behavior_instance, and the thirty-odd others all go through here, so no consumer is ever blocked by a missing wrapper.

{:ok, lights} = Hue.Resource.list(client, :light)
:ok = Hue.Resource.update(client, :light, rid, %{"on" => %{"on" => true}})

Partial success

A bridge response can carry both data and errors — some of what you asked for worked. Pass return: :detailed to receive {:ok, data, errors} instead of having the errors discarded. get/4 does not accept it: it already unwraps to a single resource, and there is no data list left for the errors to sit alongside.

Writes

update/5 and delete/4 return plain :ok in the default :simple mode. The bridge answers a write with only the rid it touched, which carries no information the caller did not already have — the actual state change arrives on the eventstream, not in this response.

Telemetry

Every call is wrapped in :telemetry.span/3 under [:hue, :request], so [:hue, :request, :start] and either [:hue, :request, :stop] or [:hue, :request, :exception] fire around it. Start metadata carries :method and :path; stop metadata adds :result (:ok or :error), reflecting what the caller receives — including a domain-level reinterpretation such as get/4 collapsing an empty result to :not_found, not just the wire-level HTTP outcome.

Trust boundary

type and rid are interpolated straight into the request path with no escaping. A rid from a less-trusted source could contain something like ../ and redirect the request elsewhere on the same bridge. The blast radius is bounded by whatever the application key itself can reach — this is not a privilege escalation — but rids are expected to come from the bridge (a prior list/3, get/4, or create/4 response), not from end-user input passed straight through.

Summary

Functions

Fetches one resource by rid.

Lists every resource of one type.

Lists every resource on the bridge in one request.

Converts a resource type from the wire into an atom, when this library knows it.

Types

return_mode()

@type return_mode() :: :simple | :detailed

rid()

@type rid() :: String.t()

type()

@type type() :: atom()

Functions

create(client, type, body, options \\ [])

@spec create(Hue.Client.t(), type(), map(), keyword()) ::
  {:ok, list()} | {:ok, list(), list()} | {:error, Hue.Error.t()}

Creates a resource.

delete(client, type, rid, options \\ [])

@spec delete(Hue.Client.t(), type(), rid(), keyword()) ::
  :ok | {:ok, list(), list()} | {:error, Hue.Error.t()}

Deletes a resource.

Returns :ok in :simple mode (the default) — see the moduledoc's "Writes" section for why. Pass return: :detailed for {:ok, data, errors}.

get(client, type, rid, options \\ [])

@spec get(Hue.Client.t(), type(), rid(), keyword()) ::
  {:ok, map()} | {:error, Hue.Error.t()}

Fetches one resource by rid.

Returns {:error, %Hue.Error{reason: :not_found, rid: rid}} for a rid that does not exist. Observed firmware 1.78.0 on a BSB002 answers a missing rid, a malformed rid, and an unknown resource type all with HTTP 404 and a JSON errors body (probed 2026-08-06) — that is the path this actually takes. An HTTP 200 with an empty data array is handled too, defensively: it is a shape the CLIP v2 schema permits, even though no probe against real hardware has produced it.

Does not accept return: :detailed — see the moduledoc.

list(client, type, options \\ [])

@spec list(Hue.Client.t(), type(), keyword()) ::
  {:ok, list()} | {:ok, list(), list()} | {:error, Hue.Error.t()}

Lists every resource of one type.

list_all(client, options \\ [])

@spec list_all(
  Hue.Client.t(),
  keyword()
) :: {:ok, list()} | {:ok, list(), list()} | {:error, Hue.Error.t()}

Lists every resource on the bridge in one request.

This is what seeds Hue.Bridge's cache. The reference bridge answers with 178 resources across 22 types in roughly 154 KB, which is why reconnecting refetches everything rather than resuming the eventstream from an id.

type(name)

@spec type(String.t() | nil) :: atom() | String.t() | nil

Converts a resource type from the wire into an atom, when this library knows it.

An unrecognised type is returned as the string it arrived as. A name from the network must never become an atom: the atom table is not garbage collected, and a bridge firmware that invents a type should not be able to grow it.

update(client, type, rid, body, options \\ [])

@spec update(Hue.Client.t(), type(), rid(), map(), keyword()) ::
  :ok | {:ok, list(), list()} | {:error, Hue.Error.t()}

Updates one resource.

Returns :ok in :simple mode (the default) — see the moduledoc's "Writes" section for why. Pass return: :detailed for {:ok, data, errors}.