Shelly.Events (Shelly v0.3.0)

Copy Markdown View Source

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/4guard 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(state)

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_device_id(id)

@spec normalize_device_id(String.t() | integer() | term()) :: String.t() | nil

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)

normalize_device_id(id, known)

@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.

start_link(client, opts \\ [])

@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 (see normalize_device_id/2), which is the difference between a Gen1 device's events landing under the right key or being silently unmatchable.