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
relaysandrollers, 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 arollersarray. Passextra: %{component: "cover"}when you know the mode. - Pro 3EM channel 0 resolves to
em1:0(phase A) rather than theem:0three-phase aggregate, since a per-channel component is the more useful answer for a caller asking about channel 0. Readstatus["em:0"]directly for the total.
Summary
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
@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
@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.
Normalize the v2 API's gen strings ("G1".."G4") to integers.
@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 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.