1. Home
  2. Scripting
  3. Control Shelly from an NTC sensor

Scripting

Control Shelly from an NTC sensor

Reuse the floor sensor already in the screed instead of chasing a new probe.

Updated August 2026

Why this is worth doing

Every electric underfloor heating installation has a temperature sensor buried in the screed. When the old thermostat is replaced with a Shelly, that sensor usually gets abandoned and a new probe is fitted wherever one will reach.

It does not have to be. A Shelly Plus Add-on can read the existing NTC sensor, whatever brand it is, and a script converts its resistance into a temperature the app can show and act on.

That matters because the buried sensor is in the right place. It measures the floor, not the air near it, and getting a new probe to the same depth means opening the screed.

This example uses a Devireg NTC 15 kΩ. Any NTC works — the different sensor section covers what to change, and it is not optional reading.

What you need

  • A Shelly 1PM Gen3 or another Gen3/Gen4 device with the same form factor
  • A Shelly Plus Add-on
  • An NTC sensor — the one already in the floor, or any other

Update the device to the latest firmware before starting.

The sensor’s own numbers matter. A resistance printed on an NTC — 15 kΩ — is its resistance at 25 °C, not a rating. For the Devireg used here:

Temperature Resistance
0 °C 42 000 Ω
25 °C 15 000 Ω
50 °C 6 000 Ω

Add the sensor

Wire the NTC to the Plus Add-on’s analog input, then tell the device it is there. In Shelly Smart Control:

  1. Open the device and tap the Add-on symbol
  2. Add peripheral
  3. Select Analog
  4. Add peripheral, and allow the reboot if it asks

The same is available in the web interface. After the reboot the device has a voltmeter component — usually id 100 — and that is what the script reads.

Install the script

Create a script on the device and paste this in. See how to add a script, and enable run on startup.

// Brief description:
// - Periodically reads voltage from a thermistor via a voltmeter component
// - Converts voltage to temperature using Steinhart-Hart equation
// - Writes calculated temperature to a virtual component
// - Retrieves desired temperature (either static or from virtual component)
// - Applies hysteresis and optional inversion to control a relay based on temperature
// SH Coefficient Calculator
// https://rusefi.com/Steinhart-Hart.html
//
// Thermistor wiki page
// https://en.wikipedia.org/wiki/Thermistor
/**************** START CHANGE HERE ****************/
let CONFIG = {
  scanInterval: 10, // seconds, the script runs every 10 seconds to fetch the voltage
  voltmeterID: 100, // ID of the voltmeter - when we install the plugin, the device defines this number
  // Desired temperature settings
  useVirtualDesiredTemp: true, // Set to true to use a virtual component for the desired temperature
  desiredTemp: 25,             // Desired temperature in Celsius (used if useVirtualDesiredTemp is false)
  hysteresis: 1,               // Hysteresis band in degrees Celsius
  invertRelay: false,          // Set to true to invert the relay action
  /**
   * Applies math to the voltage and returns the result. This function is called every time the voltage is measured
   * @param {Number} voltage The currently measured voltage
   * @returns The temperature based on the voltage
   */
  calcTemp: function (voltage) {
    const constVoltage = 10;
    const R1 = 10000;
    const A = 0.0010377385695278978;
    const B = 0.00021633455581572119;
    const C = 2.654857502585547e-7;
    const R2 = R1 * (voltage / (constVoltage - voltage));
    const logR2 = Math.log(R2);
    let T = 1.0 / (A + (B + C * logR2 * logR2) * logR2);
    T = T - 273.15; // Celsius
    console.log(
      "Current Temperature: " +
        T.toFixed(2) +
        " °C | Voltage: " +
        voltage +
        "V | Resistance: " +
        R2.toFixed(2) +
        "Ω"
    );
    return T;
  },
  /**
   * This function is called every time a temperature is read
   * @param {Number} temperature The currently calculated temperature
   * @param {Number} desiredTemp The desired temperature
   */
  onTempReading: function (temperature, desiredTemp) {
    // Fetch the current relay status
    let switchStatus = Shelly.getComponentStatus("switch:0");
    if (switchStatus === null || typeof switchStatus.output !== "boolean") {
      console.log("Cannot read relay status");
      return;
    }
    let relayIsOn = switchStatus.output; // true or false
    // Calculate thresholds
    let onThreshold = desiredTemp - CONFIG.hysteresis / 2;
    let offThreshold = desiredTemp + CONFIG.hysteresis / 2;
    let turnRelayOn = false;
    let turnRelayOff = false;
    if (!CONFIG.invertRelay) {
      // Normal operation
      if (!relayIsOn && temperature < onThreshold) {
        turnRelayOn = true;
      } else if (relayIsOn && temperature > offThreshold) {
        turnRelayOff = true;
      }
    } else {
      // Inverted operation
      if (!relayIsOn && temperature > offThreshold) {
        turnRelayOn = true;
      } else if (relayIsOn && temperature < onThreshold) {
        turnRelayOff = true;
      }
    }
    if (turnRelayOn) {
      // Turn relay on
      Shelly.call("Switch.Set", { id: 0, on: true });
      console.log("Turning relay on");
    } else if (turnRelayOff) {
      // Turn relay off
      Shelly.call("Switch.Set", { id: 0, on: false });
      console.log("Turning relay off");
    } else {
      // No relay action needed
      console.log("No relay action needed");
    }
  },
};
/**************** STOP CHANGE HERE ****************/
function fetchVoltage() {
  // Fetch the voltmeter component
  const voltmeter = Shelly.getComponentStatus(
    "voltmeter:" + JSON.stringify(CONFIG.voltmeterID)
  );
  // Exit if component does not exist
  if (typeof voltmeter === "undefined" || voltmeter === null) {
    console.log("Cannot find voltmeter component");
    return;
  }
  const voltage = voltmeter["voltage"];
  // Exit if voltage cannot be read
  if (typeof voltage !== "number") {
    console.log("Cannot read the voltage or it is NaN");
    return;
  }
  // Calculate the temperature based on the voltage
  const temp = CONFIG.calcTemp(voltage);
  // Write temperature to virtual component with id 200
  Shelly.call("Number.Set", { id: 200, value: Number(temp.toFixed(2)) });
  print("Temperature " + temp.toFixed(2) + " °C written to Virtual component (Id:200)");
  // Exit if temperature was not calculated correctly
  if (typeof temp !== "number") {
    console.log("Something went wrong calculating the temperature");
    return;
  }
  // Fetch desired temperature if necessary
  if (CONFIG.useVirtualDesiredTemp) {
    Shelly.call(
      "Number.GetStatus",
      { id: 201 },
      function (result, error_code, error_message) {
        if (error_code === 0 && result && typeof result.value === "number") {
          let desiredTemp = result.value;
          console.log("Desired temperature fetched from virtual component: " + desiredTemp + " °C");
          CONFIG.onTempReading(temp, desiredTemp);
        } else {
          console.log("Error fetching desired temperature: " + error_message);
          // Handle error, maybe use default desired temperature
          CONFIG.onTempReading(temp, CONFIG.desiredTemp);
        }
      }
    );
  } else {
    // Use desired temperature from CONFIG
    CONFIG.onTempReading(temp, CONFIG.desiredTemp);
  }
}
// Initialize the script
function init() {
  // Start the timer
  Timer.set(CONFIG.scanInterval * 1000, true, fetchVoltage);
  // Fetch voltage at startup
  fetchVoltage();
}
init();

Everything you need to change is in the CONFIG block at the top:

Setting Default What it does
scanInterval 10 Seconds between readings
voltmeterID 100 The voltmeter component the Add-on created
useVirtualDesiredTemp true Take the setpoint from a virtual component. False uses desiredTemp instead
desiredTemp 25 Setpoint in °C, used only when the above is false
hysteresis 1 Degrees between switching off and on again
invertRelay false True for cooling: the relay runs above the setpoint rather than below

Below CONFIG sits calcTemp, which converts resistance to temperature using the Steinhart–Hart equation. Its three coefficients A, B and C describe one specific thermistor — the ones here are calculated for the Devireg 15 kΩ above.

constVoltage and R1 describe the Add-on’s voltage divider, not the sensor. Leave them alone.

Virtual components

To see and set the temperature from the app, the device needs two Number virtual components. Open the device, tap the Virtual components icon, and create them under Components.

Create them in this order — the id comes from the order of creation, and the script expects exactly these two:

  • Current temperature, id 200 — where the script writes the reading
  • Desired temperature, id 201 — where you set the target

Give the second a slider range that matches the room. For a floor, 15 to 30 °C is a useful span; above 27 °C is worth thinking twice about on wood.

After about ten seconds the temperature appears, and the slider controls the heating.

Group them. Under the Group tab, create a group with both components and switch on Extract virtual group as device. It then behaves like a device on the home screen, and a scene can trigger on its value. A free account can extract one group.

A different sensor

Everything above works unchanged for a Devireg 15 kΩ. For any other NTC, three numbers in the script have to be recalculated — and this is the part to get right.

Find three resistance values from the sensor’s datasheet: at its minimum temperature, at 25 °C, and at its maximum. For a typical NTC 10 kΩ:

Point Temperature Resistance
Minimum −40 °C 323 839 Ω
Nominal 25 °C 10 000 Ω
Maximum 90 °C 1 034 Ω

Feed them to a Steinhart–Hart calculator to get the coefficients:

Open the tool Steinhart–Hart coefficient calculator Enter three temperature and resistance pairs to get A, B and C. Note that it takes temperatures in Fahrenheit. Opens on rusefi.com — opens in a new window

Paste the three values into calcTemp, replacing A, B and C.

Then check it against something you can measure. Leave the sensor in the room, read a thermometer, and compare. If the script says 21 °C and the room is 21 °C, the coefficients are right. If it says 31, they are not — and that is a five-second test that catches the whole class of error.

Where this script comes from

The voltage-to-temperature conversion is based on Shelly’s own ntc-conversion example, licensed under Apache 2.0. The relay control and the coefficients are by Ronni Nielsen.

What has been changed:

  • The relay control is new. The original leaves its switching commented out, as a place for you to write your own — this version implements it, with an on and off threshold either side of the setpoint.
  • Hysteresis and inversion are added, so the same script drives heating or cooling.
  • Virtual components are read and written, so the temperature and the setpoint live in the app rather than in the code.
  • The coefficients are recalculated for a Devireg 15 kΩ. The original’s are for a 10 kΩ thermistor.
  • The coefficients are verified. Solving Steinhart–Hart for the Devireg’s three points reproduces the values in the script exactly — the reading is correct at 0, 25 and 50 °C.

Tips & best practices

  • Measure the old sensor before trusting it. A multimeter across the leads at a known room temperature tells you both that it is alive and roughly which curve it is on.
  • Widen the hysteresis on a floor. One degree suits air; a screed responds slowly enough that a narrow band just cycles the relay.
  • Cap the floor temperature in your automation. 27 °C is the usual limit for wood, and the script will happily go past it.
  • Write the coefficients down. If the script is ever reinstalled, three numbers stand between the device and a correct reading, and they cannot be recovered from the sensor.
  • The setpoint survives a reboot because it lives in the virtual component rather than the script. That is the main reason to prefer it over desiredTemp.