Hue.Bridge.Graph (Hue v0.2.0)

Copy Markdown View Source

Walks the resource graph to turn a target a human wrote into a resource this library can act on.

Targets are names or rids, interchangeably

Names are what a person has in mind; rids are what survives someone renaming a room in the Hue app. Both are accepted everywhere a target is, and rid is tried first — an exact identity match should never lose to a coincidental name collision.

Rooms and zones are not writable

A room has no on state. What responds to a write is the grouped_light service the room owns, reached through room.services. That indirection is why "dim the living room" is several lookups rather than one, and why doing it per call over layer 1 costs several round trips.

Two rooms on the reference bridge are empty and expose no grouped_light service at all, so :no_grouped_light is a case that fires in real use rather than a defensive branch. It is deliberately distinct from :not_found: the room exists, and telling the caller it does not would send them looking for the wrong problem.

Summary

Functions

Resolves a room or zone target to the grouped_light that acts for it.

Resolves a name-or-rid to the resource itself.

Functions

grouped_light(table, type, target)

@spec grouped_light(Hue.Bridge.Cache.table(), :room | :zone, String.t()) ::
  {:ok, map()} | {:error, Hue.Error.t()}

Resolves a room or zone target to the grouped_light that acts for it.

Returns :no_grouped_light when the room or zone exists but owns no such service — an empty room, which is a real state on real bridges.

resolve(table, type, target)

@spec resolve(Hue.Bridge.Cache.table(), atom(), String.t()) ::
  {:ok, map()} | {:error, Hue.Error.t()}

Resolves a name-or-rid to the resource itself.

Tries the rid first; falls back to the name index. Cache.fetch/3 can also fail with :not_synced or :not_started — those are not "no such rid" and must propagate unchanged rather than send this to the name index, which would only fail the same way a second time.

The exact match on :not_found is deliberate, and currently untestable

This matches {:error, %Error{reason: :not_found}} specifically, rather than a catch-all {:error, _}, on purpose: a :not_synced or :not_started result is a fact about the cache, not about the target, and should be returned as-is instead of paying for a second lookup that asks the same cache the same question a different way.

But today, Cache.fetch/3 and Cache.fetch_by_name/3 share the exact same readable/1 gate, so for :not_synced and :not_started both paths produce the byte-identical error — falling through to fetch_by_name unconditionally would be observably wrong only if the two functions' gating ever diverged. No test in this suite can currently tell "propagated directly" apart from "fell through and independently derived the same answer" without instrumenting Cache with a call counter, which was judged not worth building. Do not simplify this to a catch-all on the strength of the test suite staying green if you do — the suite cannot see the difference, but a future change to either function's gating could make it observable again, silently.