Skip to content

Hardware Reference

RapierXbox edited this page May 13, 2026 · 1 revision

Hardware Reference

This page is the "what's actually happening under the hood" reference for ShellyElevate. You don't need to read it to use the app. It's here so that when something behaves weirdly, you can figure out which subsystem is involved and what to look at.

If you're trying to add a new device model or debug why a sensor reads wrong, this is where to start.

Temperature & humidity

Both come from a Sensirion SHT3x I²C sensor exposed by the kernel at:

/sys/devices/platform/sht3x-user/sht3x_access

A single read returns humidity:temperature as raw 16-bit values. ShellyElevate converts them with the standard SHT3x formulas:

temp_c   = (raw_t * 175.0 / 65535.0) - 45.0 + offset_t - dynamic_correction
humid_p  = (raw_h * 100.0 / 65535.0) + offset_h

The offset_t and offset_h are per-model. They were measured against reference sensors for each board because the display's own heat throws off the SHT3x by a varying amount. Values are in Supported Devices.

The dynamic correction is the optional thermal-zone-based offset: subtract k * max(0, dev_temp - baseline) from the reading. Off by default.

Both sensors are polled and published every 30 seconds over MQTT.

Light sensor

A regular Android SensorManager.TYPE_LIGHT sensor. The driver fires onSensorChanged callbacks; ShellyElevate emits an internal broadcast on a 1-second cadence with a 15% relative threshold (i.e. don't bother re-broadcasting if the value hasn't changed much).

The auto-brightness logic reads this internal broadcast:

  • Below 30 lux: clamp to minBrightness (default 48).
  • Above 500 lux: clamp to 255.
  • In between: linear ramp.

There's a 3-second hysteresis so a brief flash of light (someone walking past with a phone) doesn't yank the brightness around. The hysteresis is bypassed when transitioning to brightness 0 so the screen turns off fast.

The animator runs at 50 ms per step (~20 fps, the max the sysfs backlight node can keep up with) and only writes when the delta is at least 2 units to avoid micro-adjustments.

Proximity sensor

Three layered paths, used in this order:

  1. Standard Android SensorManager.TYPE_PROXIMITY: the normal path. Works on most models.
  2. JNI gpio_keys listener: for models that expose proximity as kernel input events (X2i, etc.). The libshellyinput.so native helper opens /dev/input/event* and watches for KEY_F5 (near, distance = 0) and KEY_F6 (far, distance = max range).
  3. getevent shell fallback: if the JNI lib failed to load, spawns getevent -l and parses its output for the same key events.

Whichever path delivers a value, it goes through the same internal "proximity updated" broadcast. The rest of the app doesn't care which method produced it.

Brightness

ShellyElevate probes a handful of sysfs backlight nodes at startup, in order:

/sys/devices/platform/leds-mt65xx/leds/lcd-backlight/brightness
/sys/devices/platform/sprd_backlight/backlight/sprd_backlight/brightness
/sys/devices/platform/backlight/backlight/backlight/brightness

The first one that exists wins. Brightness values are 0-255 as written to the node; the animator handles the smoothing.

Two modes:

  • Manual: brightness is written directly.
  • Automatic: light-sensor-driven, see above. The manual slider is hidden in the settings UI.

When the Settings screen is open, brightness is forced to 255 so settings are readable in any lighting, then restored when settings close.

Relays

Two code paths depending on hardware generation.

V1 (Stargate / Atlantis / Pegasus): direct sysfs writes.

/sys/devices/platform/leds/red_enable      relay 0
/sys/devices/platform/leds/green_enable    relay 1

V2 (Blake / Maverick / Jenna / Cally / Dayna): Android system properties plus an init.rc trigger.

setprop shelly.relay.<n>.state 1
setprop ctl.start cloud.shelly.<model>.relay

A fallback to direct sysfs (/sys/class/strelay/relay1, relay2) exists in case the property path fails.

On Blake (XL) and Cally (XLi), the hardware relay logic is inverted: writing 1 turns the relay off. The firmware XORs with an inversion flag transparently, so from the API you don't notice.

Hardware buttons

Only the XL (Blake) and XLi (Cally) have the four front-bezel buttons. Detection sits in ButtonPressDetector and classifies presses by hold time and click count:

Type Rule
short release within 500 ms; actually held until a 400 ms multi-click timeout to see if more clicks follow
long held >= 1000 ms; fires immediately on release
double two short presses within 400 ms
triple three short presses within 400 ms

The power_button (kernel keycode 140) follows the same logic. Long-press fires reboot when powerButtonAutoReboot is on.

If you map a button to a relay (via buttonRelayEnabled plus buttonRelayMap0..3), a short press toggles the relay. Long/double/triple still emit MQTT events but don't touch the relay.

Swipe gestures

A swipe over the WebView triggers a relay 0 toggle if switchOnSwipe is on. Thresholds:

  • Velocity > 2.5 px/ms and distance > 250 px.
  • Vertical only. Horizontal swipes are ignored.
  • Hard-coded to relay 0.

A swipe_event MQTT message goes out for every detected swipe, so dashboards or HA can react to it independently of the relay toggle.

Thermal zones

/sys/class/thermal/thermal_zone*/temp (Linux's standard thermal interface). At startup ShellyElevate walks the directory and snapshots every zone's type (a sanitized version, non-alphanumerics get replaced with _).

Used in two places:

  • As the reference temperature for the optional dynamic temperature offset.
  • As MQTT topics (thermal/<zone_type>) when publishThermalSensors is on. Useful for graphing SoC temperatures or sanity-checking the offset.

The available zones are hardware-dependent and not stable across kernels. Look at MQTT.md for the topic shape, or just enable publishThermalSensors once and see what shows up in HA.

Media playback

Two independent MediaPlayer instances:

  • Music player: for playMusic(). Loops. Can be paused/resumed.
  • Effects player: for playEffect(). One-shot. Pauses music for the duration of the effect, then resumes it.

Both use STREAM_MUSIC so the system volume slider affects them. Volume on the API is 0.0 to 1.0 and is applied to both players.

The whole subsystem is gated behind mediaEnabled. Until that's on, the players are never initialized and every /media/* API call returns 400.

UART (dimmer)

If your wall display is wired to a Shelly Dimmer backplate, the link is a UART at:

/dev/ttyS5      /dev/ttyMT1      /dev/ttyS1

Probed in that order. 57600 baud, 8N1.

The protocol is Shelly's STES: a small frame format with a magic header (0xAA 0x55), length, command byte, payload, and a one-byte checksum. The full command set is in the source (StesCommand); the ones you see from outside are mostly:

  • GET_STATUS: read brightness, on/off, error flags.
  • SET_DIMMER: set brightness (0-1000 scale, gamma-corrected).
  • POWER_METER: read W/V/A.

ShellyElevate probes the bus on startup with a GET_VERSION. If the backplate doesn't respond, the dimmer is reported as not attached and the /device/dimmer API returns 404.

The protocol also supports OTA firmware updates of the dimmer's STM32, using the standard ST UART bootloader (AN3155). That's wired in but normally only used during a firmware push.

InputMonitor (JNI)

A native helper (libshellyinput.so) that opens /dev/input/event* directly and exposes hardware key events to the Java side. Used for:

  • Proximity gpio_keys on models that don't have a regular Android proximity sensor.
  • Anything else where the Linux input event is more reliable than going through Android's IME stack.

If the library fails to load, the dependent features fall back to spawning getevent and parsing its text output. Slower but works.

In short

If something is misbehaving, the order of suspects is usually:

  1. The right sysfs node not existing (different kernel, different model). Check the logs with adb logcat.
  2. A permission missing (rare after a proper install, but WRITE_SETTINGS is the usual one).
  3. The fallback paths kicking in and being slower than expected. The JNI input library failing to load is the most common.

Most user-facing issues land in Troubleshooting. This page is for when troubleshooting points you here.

Clone this wiki locally