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. AuthorizesShelly.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)
#=> falseOne 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
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).
The server is normalized to https://host.
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?
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.