Repository navigation
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.
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.
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.
Three layered paths, used in this order:
-
Standard Android
SensorManager.TYPE_PROXIMITY: the normal path. Works on most models. -
JNI gpio_keys listener: for models that expose proximity as kernel input events (X2i, etc.). The
libshellyinput.sonative helper opens/dev/input/event*and watches forKEY_F5(near, distance = 0) andKEY_F6(far, distance = max range). -
geteventshell fallback: if the JNI lib failed to load, spawnsgetevent -land 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.
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:
brightnessis 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.
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.
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.
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.
/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>) whenpublishThermalSensorsis 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.
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.
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.
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.
If something is misbehaving, the order of suspects is usually:
- The right sysfs node not existing (different kernel, different model). Check the logs with
adb logcat. - A permission missing (rare after a proper install, but
WRITE_SETTINGSis the usual one). - 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.
Setup
Integration
Features
Reference