Real-time events socket for an OAuth-connected account
(wss://<server>:6113/shelly/wss/hk_sock?t=<token>).
Start one per account with a handler; the handler receives normalized events in its own right — persistence, PubSub, whatever — while this process owns the connection and reconnects with backoff:
Shelly.Events.start_link(client,
handler: fn
{:status, device_id, status} -> MyApp.handle_status(device_id, status)
{:online, device_id, online?} -> MyApp.handle_online(device_id, online?)
{:other, _message} -> :ok
end
)device_id is normalized to lowercase hex — ids arrive as hex, as
integers, or as decimal strings, and all three resolve to one form
(see normalize_device_id/1).
status is the raw payload for Shelly.Status.parse/4 — guard
before parsing: events may be partial deltas (only sys, an input
or battery changed), and parsing those as a full status reports a
false "off". Check Shelly.Status.has_component?/2, and when you
know the device's component, prefer comparing
Shelly.Status.component_of/2 against it.
Reconnection uses in-process exponential backoff with jitter (a
WebSockex constraint — the process is unresponsive while it waits)
and gives up after @max_attempts consecutive failures, letting your
supervisor's restart policy take over. Note what that means in practice:
WebSockex exits :normal for a clean server-side close, and a
:transient child is not restarted on a normal exit — so a socket
the cloud closed politely stays closed. Use restart: :permanent (pass
it in the child options) if realtime must come back regardless of how
the connection ended. Note the access token rides
the websocket URL (Shelly's protocol); this module never logs the
URL and redacts disconnect reasons.
Summary
Functions
Child spec for a supervision tree
Normalize a device id to the lowercase hex form devices are addressed by, whatever shape the event carried.
Normalize against a set of ids you already know.
Open the realtime socket for a client.
Functions
Child spec for a supervision tree:
{Shelly.Events, {client, handler: &MyApp.handle_event/1}}use WebSockex injects a spec shaped for its own start_link/2, which
does not match this module's (client, opts) signature — starting one
from a plain {Shelly.Events, [client, opts]} tuple raised at boot.
Normalize a device id to the lowercase hex form devices are addressed by, whatever shape the event carried.
Shelly sends the same id three ways: as hex ("485519999340"), as an
integer, and — on the live websocket — as the decimal rendering of
that number in a string ("79530338915136"). The decimal string is
the trap: it looks like an id, so a consumer matching it against its
device list finds nothing, every event is dropped, and realtime
silently degrades to whatever polling exists.
Shelly's spec pins the hex form to 6 characters (Gen1) or 12,
zero-padded. That is what disambiguates: an all-digit string of any
other length cannot be a valid hex id, so it is decimal. "12133370"
(8 digits) is decimal for the Gen1 id "b923fa", while the 12-digit
"485519999340" is itself hex and passes through.
Converted ids are padded back to the same 6-or-12 width, so an event
carrying the integer and one carrying the string resolve to one key.
Ids that aren't hex at all (the X-prefixed BLE/Z-Wave form) are
downcased and returned as-is.
The one case length cannot settle
A decimal rendering that happens to be exactly 6 or 12 digits is
indistinguishable from a hex id, because such a string is both. This
reaches roughly 5% of Gen1 ids — those whose hex form has a leading
zero, e.g. 0x062fa0, which renders as "405408". Gen2+ ids are
unaffected: no Shelly OUI produces a 12-digit decimal.
When you have the device list — and a consumer matching events against its own devices always does — pass it and the ambiguity resolves by lookup:
Shelly.Events.normalize_device_id("405408", known_ids)
#=> "062fa0" (when "062fa0" is in known_ids and "405408" is not)
@spec normalize_device_id(String.t() | integer() | term(), Enumerable.t()) :: String.t() | nil
Normalize against a set of ids you already know.
Resolves the one case normalize_device_id/1 cannot: when a digits-only
string is both a valid hex id and a valid decimal rendering, the
interpretation present in known_ids wins. Falls back to
normalize_device_id/1 when neither or both are known.
@spec start_link( Shelly.Client.t(), keyword() ) :: {:ok, pid()} | {:error, :no_token | :invalid_server_url | term()}
Open the realtime socket for a client.
Requires an OAuth token — the websocket has no auth-key equivalent.
Options:
:handler— required, arity-1, receives normalized events:name— optional process name:known_ids— optional; the device ids you already know. Pass them and ambiguous ids resolve by lookup (seenormalize_device_id/2), which is the difference between a Gen1 device's events landing under the right key or being silently unmatchable.