Skip to content

JavaScript Interface

RapierXbox edited this page May 13, 2026 · 1 revision

JavaScript Interface

If the page running inside the WebView is something you control (a custom dashboard, a Lovelace card you wrote, a static HTML page), you can talk to the hardware directly from JavaScript instead of going over HTTP or MQTT. ShellyElevate injects an object called ShellyElevate into every loaded page.

A small read-only subset is always available. The more interesting stuff (events, screensaver control) needs the Extended JavaScript Interface turned on in settings (extendedJavascriptInterface). It's off by default because exposing relay control to anything that ends up in the WebView is the kind of decision you want to make on purpose.

Detecting whether it's available

The page might also be opened in a normal browser for development, so a defensive check is reasonable:

if (typeof ShellyElevate === 'undefined') {
  console.log("Not running inside ShellyElevate");
  return;
}

console.log("Device:", ShellyElevate.getDevice());
console.log("Extended:", ShellyElevate.getExtendedJavascriptInterfaceEnabled());

Sensors (read-only)

ShellyElevate.getTemperature()  // float, °C
ShellyElevate.getHumidity()     // float, %
ShellyElevate.getLux()          // int, lux
ShellyElevate.getProximity()    // float, cm  (last reported)

These return the same calibrated values that go out over MQTT and HTTP. Re-reading them in a tight loop is fine, they're cached.

Relays

ShellyElevate.getRelay(0)         // returns true / false
ShellyElevate.setRelay(0, true)   // turn relay 0 on

Relay indices depend on the model (see Supported Devices). Setting a relay on a model that doesn't have one is a no-op rather than an error.

Screen / brightness

ShellyElevate.getScreenBrightness()         // 0-255 (the setting value)
ShellyElevate.getCurrentScreenBrightness()  // 0-255 (whatever is on the hardware right now)
ShellyElevate.setScreenBrightness(128)

The "current" version is useful while a brightness animation is mid-fade. It gives you the actual value being written to the backlight node.

Screensaver

ShellyElevate.sleep()    // start screensaver now
ShellyElevate.wake()     // stop it

ShellyElevate.getScreenSaverRunning()    // bool: is it active right now
ShellyElevate.getScreenSaverEnabled()    // bool: is the feature on
ShellyElevate.getScreenSaverId()         // int: 0/1/2, see Screensavers page
ShellyElevate.setScreenSaverEnabled(true)
ShellyElevate.setScreenSaverId(2)        // 2 = clock + date

ShellyElevate.keepScreenAlive(true)      // suppress idle timer
ShellyElevate.keepScreenAlive(false)     // resume it

keepScreenAlive(true) is what you want during a video playback or any kind of "user is watching this, don't dim" situation. Don't forget to flip it back off.

Device info

ShellyElevate.getDevice()                              // model SKU
ShellyElevate.getExtendedJavascriptInterfaceEnabled()  // bool
ShellyElevate.isInForeground()                         // bool

isInForeground is the combined "app is in foreground AND screen is on". A useful gate for animations or polling.

Events (extended interface only)

You wire up a JavaScript function and tell ShellyElevate to call it when something happens.

function handleScreenOn()  { console.log("woke up"); }
function handleScreenOff() { console.log("going to sleep"); }
function handleButton(id)  { console.log("button", id); }

ShellyElevate.bind("onScreenOn",   "handleScreenOn");
ShellyElevate.bind("onScreenOff",  "handleScreenOff");
ShellyElevate.bind("onButtonPressed", "handleButton");

The string is the name of a global function. ShellyElevate.bind doesn't take a function reference, it takes the name to look up at call time. Define your handlers on window and you're fine.

Available events:

Event Args Fires when
onScreenOn none display wakes
onScreenOff none display sleeps
onScreensaverOn none screensaver started
onScreensaverOff none screensaver stopped
onMotion none proximity detected (throttled)
onButtonPressed buttonId (int) physical button on the front of the device

Without the extended interface enabled, bind() silently does nothing. That's intentional so a page can call it without checking, and gracefully degrade to no events.

A worked example

A dashboard widget that toggles the kitchen relay on button 1, ramps up the brightness on button 2, and pauses while a video is playing:

(function () {
  if (typeof ShellyElevate === 'undefined') return;

  window.handleButtonPress = function (id) {
    switch (id) {
      case 1:
        const s = ShellyElevate.getRelay(0);
        ShellyElevate.setRelay(0, !s);
        break;
      case 2:
        const b = ShellyElevate.getScreenBrightness();
        ShellyElevate.setScreenBrightness(Math.min(255, b + 50));
        break;
    }
  };

  ShellyElevate.bind("onButtonPressed", "handleButtonPress");

  const video = document.querySelector("video");
  if (video) {
    video.addEventListener("play",  () => ShellyElevate.keepScreenAlive(true));
    video.addEventListener("pause", () => ShellyElevate.keepScreenAlive(false));
    video.addEventListener("ended", () => ShellyElevate.keepScreenAlive(false));
  }

  setInterval(() => {
    const t = ShellyElevate.getTemperature();
    const h = ShellyElevate.getHumidity();
    document.getElementById("temp").textContent = t.toFixed(1) + "°C";
    document.getElementById("hum").textContent  = h.toFixed(1) + "%";
  }, 5000);
})();

Trying it out

The repo ships with a test page at app/src/main/assets/jsinterfacetest.html that exercises every method and event. If you're building a dashboard, that's a useful reference to skim. It's intentionally ugly, but it shows the shape of each call.

Pushing JS from outside

You can also inject JavaScript from outside the WebView using HTTP API, at POST /webview/inject. Same context as in-page JS, so the ShellyElevate object is available there too. Handy for prototypes.

Clone this wiki locally