Skip to content

How a client renders any device

A client renders an unfamiliar thing from the thing’s own descriptor, not from a hard-coded device type. The server describes each thing as a set of capabilities, and each control in those capabilities maps to one of the client’s closed control primitives: toggle, lock, slider, stepper, color, color_temp, setpoint, select, momentary, readout, or text. The same model covers devices and endpoints, so the client can render a room list, a property overview, or a detail page from the same shape. If a thing reports a feature the control needs, the control appears; if it does not, the control is filtered out. If the client has never seen a primitive, icon token, or enum value, it still shows a readable fallback instead of breaking.

The catalogue is a set of capability descriptors. Each descriptor names the capability, the icon, the controls, and any room actions. Each control carries a key, label, primitive, binding, group, and optional bounds, options, icon token, or feature requirement. A settings-bound control belongs to the device settings document; a state-bound control belongs to live state and can be changed through commands.

This matters because the client does not guess where a value comes from. A settings control uses the device’s settings schema and PATCHes the settings document. A state control reflects current state and changes when a command succeeds and the device or connector reports the new value back.

Where it livesWritten throughControl binding
SettingsDeviceSettings.byCapabilityPATCH /devices/:id/settingssettings
StateDeviceStatusReport.payload, Endpoint.statePOST …/commands changes it; a connector or the device reports itstate

The same rule applies across devices and endpoints. A thing is rendered with the same shape whichever table it comes from: id, kind, category, icon, capabilities, state, liveness, and settings. For a client, that means a speaker in one table and a hub in the other still look like the same sort of thing. It also means a room tile and a property overview can share logic without special cases.

The client shows controls only when the thing can actually carry them. A capability can require a feature flag, and if the thing does not report that feature, the control is removed. That keeps the screen honest: no fake dimmer on a bulb that does not dim, no warmth control without color_temp, and no camera privacy mode unless the service declares it.

The client also degrades safely when it receives something newer than it knows:

  • an unknown primitive becomes a labelled read-only row;
  • an unknown icon token falls back to the category icon;
  • an unknown enum value is dropped rather than thrown away with the whole thing;
  • a settings control with a bad key is rejected by validation instead of appearing as a broken write.

When the client sends a command, it does not assume the requested value is already true. What is stored after a command is what the connector read back, not what was asked. The client must treat the response as a receipt and preserve the relationship between the request and the reported result.

The public shape includes a top-level optional Thing.commandDelivery field. It tells the client whether the command path is hub or reported. The nested optional Thing.command object contains commandId, status, detail, issuedAt, and optional reportedAt. The status can be pending, held, confirmed, rejected, or unproven.

Initial reads expose Thing.commandDelivery before any write. A client must not flatten these fields into the rest of the thing, and it must not clear optimistic state too early. In particular, a client must suppress optimistic reported-state changes for hub delivery and for unresolved commands. pending, held, and unproven mean the user has not yet gotten a final readback. confirmed means the device report matched. rejected means the request was refused. reportedAt appears only when the report exists.

Why the same descriptor works in different views

Section titled “Why the same descriptor works in different views”

The same capability description drives several layouts:

  • a room tile shows the important controls for that room’s members;
  • a thing row gives a compact summary and one inline control when useful;
  • a detail page can show the full hero control and the rest of the capability cards.

Because the control is described rather than hand-authored, the client can keep those layouts consistent. A control with optionsFrom uses the thing’s own state to build its select options at render time, so if the list changes, both clients see it at the next poll without a release.

Rely on the descriptor, the binding, and the feature flags. Do not rely on one-off device names or assumptions about a category. If the thing reports the capability, the client can render it. If the thing does not report the feature, the control stays hidden. If the client does not know the primitive or token, it still renders something readable.

That is the practical limit of the model: a new device category can ship through catalogue data; a new primitive requires an app update. The client is built to render what it has never seen, as long as the server describes it in the supported shape.

Last updated