Shelly.Client (Shelly v0.3.0)

Copy Markdown View Source

Everything needed to talk to one Shelly account: where it lives, how to authenticate, how to pace requests.

Every function in this library takes one of these. There are two ways to authenticate and a client can hold both:

  • OAuth (:token) — the "connect your account" path. Authorizes the account API (Shelly.Account) and the realtime websocket (Shelly.Events), and expires after 12 hours.
  • Auth key (:auth_key) — the classic per-account key from the Shelly app. Authorizes Shelly.CloudV2 / Shelly.CloudV1, and does not expire.

Holding both is the useful combination: realtime and discovery run on the token, and control keeps working through its expiry on the key.

client = Shelly.OAuth.exchange_code(code) |> then(fn {:ok, c} -> c end)
client = Shelly.Client.put_auth_key(client, "MWFmZTNm…")

Shelly.Client.token_expired?(client)
#=> false

One server per client

Both credentials address the same :server. Shelly issues an auth key together with its own server URI, and an OAuth token names its server in the JWT — for one account those are the same host, which is why this struct holds one. If you ever meet an account whose key and token live on different endpoints, build two clients rather than expecting put_auth_key/2 to carry a second address.

Rate limiting

Shelly's ~1 request/second budget is per account, shared by both transports. :rate_key is what Shelly.RateGate paces on; it defaults to the credential, which means a token refresh would start a fresh budget and let a burst through. Set it to something stable — your own account id — as soon as you have one:

Shelly.Client.new(server: url, token: token, rate_key: account.id)

Request options

:req_options is merged into every HTTP call this client makes — proxies, timeouts, or a :plug stub in tests.

Summary

Functions

Seconds until the token expires (negative once it has), or nil.

Can this client use the auth-key paths (v2/v1)?

Build a client. :server is required; everything else is optional, so a key-only client is new(server: url, auth_key: key) and an OAuth-only one is new(server: url, token: token).

Can this client use the OAuth paths (account API, websocket)?

Attach (or replace) the never-expiring auth key.

Attach a stable pacing key — see the rate-limiting note above.

Merge extra Req options (proxy, timeouts, a :plug stub).

Is this client's OAuth token unusable — absent or past its expiry?

Types

t()

@type t() :: %Shelly.Client{
  auth_key: String.t() | nil,
  expires_at: DateTime.t() | nil,
  label: String.t() | nil,
  rate_key: term(),
  refresh_token: String.t() | nil,
  req_options: keyword(),
  server: String.t(),
  token: String.t() | nil
}

Functions

expires_in(client)

@spec expires_in(t()) :: integer() | nil

Seconds until the token expires (negative once it has), or nil.

keyed?(client)

@spec keyed?(t()) :: boolean()

Can this client use the auth-key paths (v2/v1)?

new(attrs)

@spec new(keyword() | map()) :: t()

Build a client. :server is required; everything else is optional, so a key-only client is new(server: url, auth_key: key) and an OAuth-only one is new(server: url, token: token).

The server is normalized to https://host.

oauth?(client)

@spec oauth?(t()) :: boolean()

Can this client use the OAuth paths (account API, websocket)?

put_auth_key(client, auth_key)

@spec put_auth_key(t(), String.t() | nil) :: t()

Attach (or replace) the never-expiring auth key.

put_rate_key(client, rate_key)

@spec put_rate_key(t(), term()) :: t()

Attach a stable pacing key — see the rate-limiting note above.

put_req_options(client, options)

@spec put_req_options(
  t(),
  keyword()
) :: t()

Merge extra Req options (proxy, timeouts, a :plug stub).

token_expired?(client)

@spec token_expired?(t()) :: boolean()

Is this client's OAuth token unusable — absent or past its expiry?

Named for the token rather than the client because a key-only client answers true here and is nonetheless fully functional: that is the point. if token_expired?(client), do: CloudV2, else: Account routes both a lapsed token and a key-only client to the path that works.

A client whose expiry was never recorded is assumed live, since the cloud is the real authority. Note the trap: a token whose JWT cannot be parsed leaves :expires_at nil, so this answers false forever — a renewal loop driven only off this will never fire. Record the expiry when you store the token.