Shelly.Status (Shelly v0.3.0)

Copy Markdown View Source

Parses Shelly device status payloads into a flat status map — one parser for every transport (legacy v1 API, v2 API, OAuth account API, websocket events) and every hardware shape:

  • Gen2/3/4 RPC components, per channel, in resolution order: switch:N (relays, with or without power metering — Plus 1PM meters, Shelly 1 Gen4 and Pro 3 don't), cover:N (roller shutters), light:N (dimmers), pm1:N (metering-only PM Mini), em1:N / em:0 (Pro 3EM class energy meters — measure, can't switch).
  • Gen1 arrays: relays/meters (Watt-minute counters!), lights, rollers, emeters.

Besides the live values, the parse reports what it found: :component ("switch", "cover", "light", "pm", "em", "relay", …, "unknown") and :metered (the payload actually carries a power measurement — a 0 W reading from an unmetered relay is not data).

has_component?/2 guards partial payloads: websocket events may carry only the piece that changed (sys, an input), and parsing those as a full status would wrongly report the device off.

Known limitations on two Gen1 devices

  • Shelly 2.5 in roller mode reports relays and rollers, and relays win — so an open blind reads as a closed relay. The device gives no in-payload signal of its mode, and reordering would break the far more common relay mode, which also carries a rollers array. Pass extra: %{component: "cover"} when you know the mode.
  • Pro 3EM channel 0 resolves to em1:0 (phase A) rather than the em:0 three-phase aggregate, since a per-channel component is the more useful answer for a caller asking about channel 0. Read status["em:0"] directly for the total.

Summary

Types

t()

A parsed status.

Functions

Which component the payload carries for this channel ("switch", "cover", "light", "pm", "em", "relay", "flood", "smoke", "presence", "sensor") or nil when none — use it to guard event deltas against a device's known component before applying parse/4.

Normalize the v2 API's gen strings ("G1".."G4") to integers.

Does this payload carry actual data for the device's channel? Partial payloads (websocket deltas with only sys or an input update) must be skipped, not parsed into a false "off".

Parse a device_status map for one channel. online is supplied by the caller (it lives outside device_status in every transport). extra merges transport-level fields (model/gen) over the parsed defaults.

Types

t()

@type t() :: %{
  on: boolean() | nil,
  online: boolean(),
  watts: float(),
  voltage: float() | nil,
  current: float() | nil,
  energy_wh: float() | nil,
  temp_c: float() | nil,
  humidity: float() | nil,
  battery: integer() | nil,
  rssi: integer() | nil,
  input_state: boolean() | nil,
  source: String.t() | nil,
  model: String.t() | nil,
  gen: integer() | nil,
  metered: boolean(),
  component: String.t(),
  raw: map()
}

A parsed status.

Every parse emits all of these keys, so pattern-matching is safe. It is deliberately a plain map rather than a struct because parse/4 merges a caller-supplied extra map into the result — a documented extension point for transport-level fields the parser doesn't know about. Merging arbitrary keys into a struct produces a malformed struct instead of an error, which would turn that extension point into silent corruption.

:on is nil when the payload named the component but carried no state for it (a partial websocket delta) — unknown, not off.

Functions

component_of(status, channel)

@spec component_of(map() | term(), non_neg_integer()) :: String.t() | nil

Which component the payload carries for this channel ("switch", "cover", "light", "pm", "em", "relay", "flood", "smoke", "presence", "sensor") or nil when none — use it to guard event deltas against a device's known component before applying parse/4.

gen_to_int(value)

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

Normalize the v2 API's gen strings ("G1".."G4") to integers.

has_component?(status, channel)

@spec has_component?(map() | term(), non_neg_integer()) :: boolean()

Does this payload carry actual data for the device's channel? Partial payloads (websocket deltas with only sys or an input update) must be skipped, not parsed into a false "off".

parse(status, channel, online, extra \\ %{})

@spec parse(map() | term(), non_neg_integer(), boolean(), map()) :: t()

Parse a device_status map for one channel. online is supplied by the caller (it lives outside device_status in every transport). extra merges transport-level fields (model/gen) over the parsed defaults.