This text is AI generated!

SolarWBWB2 — Tasmota Berry Controller

A Tasmota Berry script (autoexec.be) that drives a bi-stable three-way valve to switch a domestic hot-water circuit between an electric and a gas waterboiler, based on a DS18B20 temperature reading. The same module also drives two status LEDs and an OLED display.

This document explains how the code evolved from a flat procedural script into the current class-based version with manual/automatic mode, Home Assistant MQTT auto-discovery, and persistent user-configurable thresholds. The full V6 source is included at the end for reference.


1. Hardware & Tasmota setup

Resource Purpose
DS18B20 Temperature sensor on the hot-water buffer
Relay 1 Pulse coil A of the bi-stable valve (→ electric position)
Relay 2 Pulse coil B of the bi-stable valve (→ gas position)
Relay 3 LED “Electric”
Relay 4 LED “Gas”
Relay 5 Display on/off
Relay 6 Virtual relay used as a valve-position indicator

The valve coils need a 25 s pulse, so both relays are configured with PulseTime 125 and made mutually exclusive with InterLock 1,2.

Default switching thresholds (now user-configurable, see §2 V6):

  • T ≥ 40 °C → switch to the electric boiler
  • T < 33 °C → switch to the gas boiler
  • In between → keep the current position (hysteresis)

2. Evolution of the code

V1 — Flat procedural script

The original autoexec.be configured Tasmota at the top of the file and defined a single t2v() (temperature-to-valve) function that read the temperature, decided which boiler to use, and pulsed the right relay plus the LEDs and the virtual indicator. A DS18B20 rule and a 30-minute cron job both called t2v().

This worked, but everything lived in module scope: state (t, oldvalue) was global, and the two switching branches inside t2v() were inline, making them impossible to trigger manually.

V2 — Class refactor + Tasmota commands

Three changes were applied:

  1. Wrap everything in a class (SolarWBWB2). State (t, oldvalue) and constants (thresholds, init sentinel) became class members, so the global namespace stays clean and the controller is easy to reason about.
  2. Split t2v() into two functions: switch_to_electric() and switch_to_gas(). Each one pulses the correct valve coil, sets the virtual relay 6 indicator, and updates the two LEDs. t2v() now just picks which one to call based on the temperature.
  3. Expose both functions as Tasmota commands with tasmota.add_cmd, so they are reachable from the web console and from MQTT (cmnd/<topic>/ValveElec, cmnd/<topic>/ValveGas). Each handler replies with a JSON payload via tasmota.resp_cmnd, which is what MQTT clients expect.

V3 — Emergency switch-back uses the new function

A small but important correction: the “fast emergency switch-back to gas” inside update_temp() was still calling the old inline logic. It was changed to call self.switch_to_gas() so the LEDs and the virtual indicator are kept consistent with the actual valve position. Comments were tightened up at the same time.

V4 — Manual / Automatic mode

A runtime mode lets a human override the temperature-based logic. Design choices:

  • Implicit manual switch — calling ValveElec or ValveGas puts the controller into manual mode automatically. No separate “enable manual” step.
  • Explicit return to auto — a new ValveAuto command goes back to automatic and immediately re-runs t2v() so the valve reflects the current temperature right away (instead of waiting up to 30 minutes for the next cron tick).
  • Mode is reportedValveMode (no payload) returns the current mode as JSON, so MQTT clients can poll it.
  • Safety in manual mode — while in manual, the cron evaluation, the boot-time evaluation, and the <33 °C emergency switch-back are all suspended. The valve stays exactly where the operator put it.
  • No persistence — a reboot always starts in auto. This keeps the script simple and matches the principle that a power cycle should return the system to a known-safe automatic state.

V5 — Home Assistant MQTT auto-discovery

V4 already exposed the controller over MQTT, but Home Assistant had no way to find the custom Berry commands on its own — Tasmota’s native discovery only knows about its built-in features (relays, sensors, etc.), not about arbitrary add_cmd handlers. V5 closes that gap by having the Berry script publish its own retained MQTT discovery payloads under the standard homeassistant/ prefix.

Design choices:

  • Hard-coded topictasmota_BA39AC (Tasmota default for this MAC).
  • Default discovery prefixhomeassistant/.
  • Mode UI = sensor + 3 buttonsValveElec, ValveGas, ValveAuto buttons, plus a read-only sensor for the current mode.
  • Valve position is left to Tasmota — Tasmota’s own native discovery already exposes Relay 6 (“Elec / Gas”) in HA, which is enough.
  • One HA device — all entities share the same device.identifiers, so HA groups them under one device card.
  • Availability — every entity uses tele/<topic>/LWT as availability_topic.
  • Mode state topicstat/<topic>/VALVE_MODE, retained.
  • Recovery on broker reconnect — a rule on Mqtt#Connected re-publishes both discovery and current state.
  • One-time Tasmota tweaksSetOption19 0 (modern HA discovery, do not interfere), SetOption59 1 (state telemetry on every power change), SetOption4 0 (responses on cmnd/, not stat/).

V6 — Configurable, persistent thresholds (current)

V1–V5 hard-coded the upper/lower thresholds (40 / 33 °C) as static class members. V6 turns them into runtime-configurable, persistent values that can be changed from the Tasmota console, over MQTT, or directly from Home Assistant.

Design choices:

  • Integers only. Switching at half-degree resolution adds no value here, and an integer command surface is simpler to validate and display. Payloads with a decimal point are rejected.
  • Persistence via Berry’s persist module. Two keys — valve_upper, valve_lower — are saved with persist.save() after every successful change. On boot, init() loads them back if present; otherwise it falls back to the defaults DEFAULT_UPPER = 40 and DEFAULT_LOWER = 33. Static constants DEFAULT_* keep those defaults visible at the top of the class.
  • Per-device instance state. The thresholds moved from static var UPPER_THRESHOLD to instance vars self.upper / self.lower, since they are now mutable per device.
  • Validation.
    • Must parse as an integer.
    • Must lie in the absolute range [THR_MIN, THR_MAX] = [0, 100] °C.
    • Cross-validation: setting ValveUpperThreshold to a value <= self.lower is rejected (resp_cmnd_failed), and vice versa for ValveLowerThreshold. This prevents an inverted range that would break t2v().
    • If the persisted pair is somehow inverted at boot, init() self-heals by reverting to the defaults.
  • Immediate effect. After a successful change, if the controller is in AUTO mode, t2v() is re-evaluated immediately so the valve position can react to the new threshold without waiting for the next cron tick.
  • MQTT state topics. Two retained topics mirror the current values:
    • stat/tasmota_BA39AC/VALVE_UPPER
    • stat/tasmota_BA39AC/VALVE_LOWER Published on every successful change, on boot, and on every Mqtt#Connected.
  • HA auto-discovery as number entities. Two homeassistant/number/ config payloads are published (retained) alongside the existing V5 discovery messages. Both use mode: "box", step: 1, unit: °C, and entity_category: "config", so HA renders them as configuration inputs on the same device card. Each entity binds:
    • command_topiccmnd/tasmota_BA39AC/ValveUpperThreshold (or …/ValveLowerThreshold)
    • state_topic → the matching stat/.../VALVE_UPPER|LOWER topic When the user moves the slider in HA, HA publishes the new integer to the command topic; the Berry handler validates, persists, and echoes back via the state topic — closing the round-trip.

Resulting command surface (V6)

Command Payload Effect Side-effects
ValveElec Pulse to electric mode → manual, publish mode
ValveGas Pulse to gas mode → manual, publish mode
ValveAuto Return to automatic, re-evaluate t2v() immediately mode → auto, publish mode
ValveMode Report current mode as JSON none
ValveUpperThreshold int 0..100 Set upper threshold (must be > lower) persist, publish state, re-eval if AUTO
ValveLowerThreshold int 0..100 Set lower threshold (must be < upper) persist, publish state, re-eval if AUTO
ValveThresholds Report both thresholds as JSON none

Resulting HA entities (V6)

HA entity Component Topic / command
button.solarwbwb2_valve_electric button cmnd/tasmota_BA39AC/ValveElec
button.solarwbwb2_valve_gas button cmnd/tasmota_BA39AC/ValveGas
button.solarwbwb2_valve_auto button cmnd/tasmota_BA39AC/ValveAuto
sensor.solarwbwb2_valve_mode sensor stat/tasmota_BA39AC/VALVE_MODE (auto/manual)
number.solarwbwb2_valve_upper_threshold number cmd ValveUpperThreshold, state …/VALVE_UPPER (°C int)
number.solarwbwb2_valve_lower_threshold number cmd ValveLowerThreshold, state …/VALVE_LOWER (°C int)
(Tasmota native) Relay 6 switch Valve position indicator (“Elec / Gas”)

Removing the discovery from HA

If you ever want HA to forget these entities, publish empty retained payloads to the discovery topics:

mosquitto_pub -t homeassistant/button/solarwbwb2_ba39ac/valve_elec/config   -r -n
mosquitto_pub -t homeassistant/button/solarwbwb2_ba39ac/valve_gas/config    -r -n
mosquitto_pub -t homeassistant/button/solarwbwb2_ba39ac/valve_auto/config   -r -n
mosquitto_pub -t homeassistant/sensor/solarwbwb2_ba39ac/valve_mode/config   -r -n
mosquitto_pub -t homeassistant/number/solarwbwb2_ba39ac/valve_upper/config  -r -n
mosquitto_pub -t homeassistant/number/solarwbwb2_ba39ac/valve_lower/config  -r -n

3. Full source — autoexec.be V6

#----------------------------------------
# autoexec.be  —  SolarWBWB2 - V6
# - Initialises Tasmota configuration
# - Updates the display with the measured temperature
# - Controls the bi-stable three-way valve between
#   electric and gas waterboilers based on temperature
# - V4: Manual / Automatic mode (ValveElec / ValveGas /
#       ValveAuto / ValveMode)
# - V5: Home Assistant MQTT auto-discovery
#          - button.solarwbwb2_valve_elec
#          - button.solarwbwb2_valve_gas
#          - button.solarwbwb2_valve_auto
#          - sensor.solarwbwb2_valve_mode
# - V6 (this file): Configurable, persistent thresholds
#       Two new Tasmota commands (web console + MQTT):
#          ValveUpperThreshold <int 0..100>   (>= -> electric)
#          ValveLowerThreshold <int 0..100>   (<  -> gas)
#          ValveThresholds                    (report both)
#       Stored in Tasmota's `persist` module, so they
#       survive reboots. Auto-discovered in HA as two
#       `number` entities, grouped under the same device.
#       Cross-validation enforces upper > lower.
#       After a successful change in AUTO mode, t2v() is
#       re-evaluated immediately.
#
#   MQTT topic is hard-coded as "tasmota_BA39AC"
#   (Tasmota default for this device's MAC).
#----------------------------------------#

import persist

#----------------------------------------
# One-time Tasmota configuration
#----------------------------------------#
tasmota.cmd('Teleperiod 30')                       # send sensors to MQTT every 30s
tasmota.cmd('SetOption65 1')                       # Disable Fast Power Cycle Device Recovery
tasmota.cmd('SetOption55 1')                       # Enable mDNS service
tasmota.cmd('SetOption19 0')                       # Use modern (native) HA discovery, not legacy
tasmota.cmd('SetOption59 1')                       # Send tele/%topic%/STATE on every power change
tasmota.cmd('SetOption4 0')                        # Return MQTT response on cmnd/ topic, not stat/
tasmota.cmd('Backlog0 Timezone 99; TimeStd 0,0,10,1,3,60; TimeDst 0,0,3,1,2,120') # Brussels
tasmota.cmd('WebButton1 Valve-Elec')
tasmota.cmd('WebButton2 Valve-Gas')
tasmota.cmd('WebButton3 LED-Elec')
tasmota.cmd('WebButton4 LED-Gas')
tasmota.cmd('WebButton5 Display')
tasmota.cmd('PulseTime1 125')                      # 25s pulse on relay1 (valve coil)
tasmota.cmd('PulseTime2 125')                      # 25s pulse on relay2 (valve coil)
tasmota.cmd('InterLock 1,2')
tasmota.cmd('InterLock 1')
tasmota.global.devices_present = 6
tasmota.cmd('WebButton6 Elec / Gas')               # virtual relay6 = valve position


#----------------------------------------
# Main controller class
#----------------------------------------#
class SolarWBWB2
    static var DEFAULT_UPPER  = 40                 # >= 40 C -> electric
    static var DEFAULT_LOWER  = 33                 # <  33 C -> gas
    static var THR_MIN        = 0                  # sane absolute bounds
    static var THR_MAX        = 100
    static var INIT_SENTINEL  = 5000               # "no value yet" marker

    static var MODE_AUTO   = "auto"
    static var MODE_MANUAL = "manual"

    # MQTT / HA discovery constants
    static var TOPIC      = "tasmota_BA39AC"
    static var HA_PREFIX  = "homeassistant"
    static var DEV_ID     = "solarwbwb2_ba39ac"
    static var DEV_NAME   = "SolarWBWB2"

    var t          # last measured temperature (DS18B20)
    var oldvalue   # previous measurement, for change detection / boot
    var mode       # "auto" | "manual"
    var upper      # configurable upper threshold (int, persisted)
    var lower      # configurable lower threshold (int, persisted)

    def init()
        self.t        = nil
        self.oldvalue = self.INIT_SENTINEL
        self.mode     = self.MODE_AUTO

        # Load persisted thresholds, fall back to defaults.
        self.upper = persist.has("valve_upper") ? int(persist.valve_upper) : self.DEFAULT_UPPER
        self.lower = persist.has("valve_lower") ? int(persist.valve_lower) : self.DEFAULT_LOWER
        if self.upper <= self.lower
            self.upper = self.DEFAULT_UPPER
            self.lower = self.DEFAULT_LOWER
        end

        tasmota.add_rule("DS18B20#Temperature",
                         / value -> self.update_temp(value))
        tasmota.add_cron("0 */30 * * * *",
                         / -> self.t2v(), "every_30m")
        tasmota.add_rule("Mqtt#Connected",
                         / -> self.on_mqtt_connected())

        tasmota.add_cmd("ValveElec",
                        / cmd, idx, payload -> self.cmd_valve_elec(cmd, idx, payload))
        tasmota.add_cmd("ValveGas",
                        / cmd, idx, payload -> self.cmd_valve_gas(cmd, idx, payload))
        tasmota.add_cmd("ValveAuto",
                        / cmd, idx, payload -> self.cmd_valve_auto(cmd, idx, payload))
        tasmota.add_cmd("ValveMode",
                        / cmd, idx, payload -> self.cmd_valve_mode(cmd, idx, payload))
        tasmota.add_cmd("ValveUpperThreshold",
                        / cmd, idx, payload -> self.cmd_valve_upper(cmd, idx, payload))
        tasmota.add_cmd("ValveLowerThreshold",
                        / cmd, idx, payload -> self.cmd_valve_lower(cmd, idx, payload))
        tasmota.add_cmd("ValveThresholds",
                        / cmd, idx, payload -> self.cmd_valve_thresholds(cmd, idx, payload))

        tasmota.set_timer(5000, / -> self.publish_discovery())
        tasmota.set_timer(6000, / -> self.publish_mode_state())
        tasmota.set_timer(6500, / -> self.publish_threshold_states())
    end

    def switch_to_electric()
        tasmota.cmd('power1 1')
        print('Switching to the electric waterboiler')
        tasmota.cmd('power6 1')
        tasmota.cmd('power3 1')
        tasmota.cmd('power4 0')
    end

    def switch_to_gas()
        tasmota.cmd('power2 1')
        print('Switching to the gas waterboiler')
        tasmota.cmd('power6 0')
        tasmota.cmd('power3 0')
        tasmota.cmd('power4 1')
    end

    def t2v()
        if self.mode != self.MODE_AUTO  return  end
        if self.t == nil                return  end
        if   self.t >= self.upper
            self.switch_to_electric()
        elif self.t < self.lower
            self.switch_to_gas()
        end
    end

    def update_temp(value)
        if value != self.oldvalue
            tasmota.cmd("DisplayText _T=" .. value .. " C")
        end
        self.t = value
        if self.oldvalue == self.INIT_SENTINEL && self.mode == self.MODE_AUTO
            self.t2v()
        end
        self.oldvalue = value
        if self.mode == self.MODE_AUTO
            if (self.t < self.lower) && tasmota.get_power(2)
                self.switch_to_gas()
            end
        end
    end

    # ---- Mode commands -------------------------------------------------
    def cmd_valve_elec(cmd, idx, payload)
        self.mode = self.MODE_MANUAL
        self.switch_to_electric()
        self.publish_mode_state()
        tasmota.resp_cmnd('{"ValveElec":"Done","Mode":"manual"}')
    end

    def cmd_valve_gas(cmd, idx, payload)
        self.mode = self.MODE_MANUAL
        self.switch_to_gas()
        self.publish_mode_state()
        tasmota.resp_cmnd('{"ValveGas":"Done","Mode":"manual"}')
    end

    def cmd_valve_auto(cmd, idx, payload)
        self.mode = self.MODE_AUTO
        print('Valve control returned to AUTO mode')
        self.t2v()
        self.publish_mode_state()
        tasmota.resp_cmnd('{"ValveAuto":"Done","Mode":"auto"}')
    end

    def cmd_valve_mode(cmd, idx, payload)
        tasmota.resp_cmnd('{"ValveMode":"' .. self.mode .. '"}')
    end

    # ---- Threshold commands (V6) --------------------------------------
    def parse_int(payload)
        if type(payload) == 'int'  return payload  end
        if type(payload) == 'string'
            var s = string.tr(payload, " \t", "")
            if size(s) == 0  return nil  end
            if string.find(s, ".") >= 0  return nil  end
            try
                return int(s)
            except ..
                return nil
            end
        end
        return nil
    end

    def cmd_valve_upper(cmd, idx, payload)
        var v = self.parse_int(payload)
        if v == nil
            tasmota.resp_cmnd_error()
            return
        end
        if v < self.THR_MIN || v > self.THR_MAX
            tasmota.resp_cmnd_error()
            return
        end
        if v <= self.lower
            tasmota.resp_cmnd_failed()
            return
        end
        self.upper = v
        persist.valve_upper = v
        persist.save()
        self.publish_upper_state()
        if self.mode == self.MODE_AUTO  self.t2v()  end
        tasmota.resp_cmnd('{"ValveUpperThreshold":' .. v .. '}')
    end

    def cmd_valve_lower(cmd, idx, payload)
        var v = self.parse_int(payload)
        if v == nil
            tasmota.resp_cmnd_error()
            return
        end
        if v < self.THR_MIN || v > self.THR_MAX
            tasmota.resp_cmnd_error()
            return
        end
        if v >= self.upper
            tasmota.resp_cmnd_failed()
            return
        end
        self.lower = v
        persist.valve_lower = v
        persist.save()
        self.publish_lower_state()
        if self.mode == self.MODE_AUTO  self.t2v()  end
        tasmota.resp_cmnd('{"ValveLowerThreshold":' .. v .. '}')
    end

    def cmd_valve_thresholds(cmd, idx, payload)
        tasmota.resp_cmnd('{"ValveUpperThreshold":' .. self.upper ..
                          ',"ValveLowerThreshold":' .. self.lower .. '}')
    end

    # ---- HA discovery --------------------------------------------------
    def device_block()
        return '"dev":{"ids":["' .. self.DEV_ID .. '"],' ..
               '"name":"' .. self.DEV_NAME .. '",' ..
               '"mf":"Tasmota","mdl":"SolarWBWB2 Berry"}'
    end

    def publish_button(object_id, name, command)
        var topic = self.HA_PREFIX .. "/button/" .. self.DEV_ID .. "/" .. object_id .. "/config"
        var payload = '{' ..
            '"name":"' .. name .. '",' ..
            '"uniq_id":"' .. self.DEV_ID .. '_' .. object_id .. '",' ..
            '"cmd_t":"cmnd/' .. self.TOPIC .. '/' .. command .. '",' ..
            '"avty_t":"tele/' .. self.TOPIC .. '/LWT",' ..
            '"pl_avail":"Online","pl_not_avail":"Offline",' ..
            self.device_block() ..
            '}'
        tasmota.publish(topic, payload, true)
    end

    def publish_mode_sensor()
        var topic = self.HA_PREFIX .. "/sensor/" .. self.DEV_ID .. "/valve_mode/config"
        var payload = '{' ..
            '"name":"Valve Mode",' ..
            '"uniq_id":"' .. self.DEV_ID .. '_valve_mode",' ..
            '"stat_t":"stat/' .. self.TOPIC .. '/VALVE_MODE",' ..
            '"avty_t":"tele/' .. self.TOPIC .. '/LWT",' ..
            '"pl_avail":"Online","pl_not_avail":"Offline",' ..
            '"icon":"mdi:auto-mode",' ..
            self.device_block() ..
            '}'
        tasmota.publish(topic, payload, true)
    end

    def publish_number(object_id, name, command, stat_topic, icon)
        var topic = self.HA_PREFIX .. "/number/" .. self.DEV_ID .. "/" .. object_id .. "/config"
        var payload = '{' ..
            '"name":"' .. name .. '",' ..
            '"uniq_id":"' .. self.DEV_ID .. '_' .. object_id .. '",' ..
            '"cmd_t":"cmnd/' .. self.TOPIC .. '/' .. command .. '",' ..
            '"stat_t":"' .. stat_topic .. '",' ..
            '"min":' .. self.THR_MIN .. ',"max":' .. self.THR_MAX .. ',"step":1,' ..
            '"mode":"box",' ..
            '"unit_of_meas":"°C",' ..
            '"entity_category":"config",' ..
            '"icon":"' .. icon .. '",' ..
            '"avty_t":"tele/' .. self.TOPIC .. '/LWT",' ..
            '"pl_avail":"Online","pl_not_avail":"Offline",' ..
            self.device_block() ..
            '}'
        tasmota.publish(topic, payload, true)
    end

    def publish_discovery()
        self.publish_button("valve_elec", "Valve Electric", "ValveElec")
        self.publish_button("valve_gas",  "Valve Gas",      "ValveGas")
        self.publish_button("valve_auto", "Valve Auto",     "ValveAuto")
        self.publish_mode_sensor()
        self.publish_number("valve_upper", "Valve Upper Threshold",
                            "ValveUpperThreshold",
                            "stat/" .. self.TOPIC .. "/VALVE_UPPER",
                            "mdi:thermometer-chevron-up")
        self.publish_number("valve_lower", "Valve Lower Threshold",
                            "ValveLowerThreshold",
                            "stat/" .. self.TOPIC .. "/VALVE_LOWER",
                            "mdi:thermometer-chevron-down")
        print('HA discovery published for ' .. self.DEV_ID)
    end

    # ---- Retained state publishers ------------------------------------
    def publish_mode_state()
        tasmota.publish("stat/" .. self.TOPIC .. "/VALVE_MODE", self.mode, true)
    end

    def publish_upper_state()
        tasmota.publish("stat/" .. self.TOPIC .. "/VALVE_UPPER", str(self.upper), true)
    end

    def publish_lower_state()
        tasmota.publish("stat/" .. self.TOPIC .. "/VALVE_LOWER", str(self.lower), true)
    end

    def publish_threshold_states()
        self.publish_upper_state()
        self.publish_lower_state()
    end

    def on_mqtt_connected()
        self.publish_discovery()
        self.publish_mode_state()
        self.publish_threshold_states()
    end
end

solar_wbwb2 = SolarWBWB2()

4. Operating cheatsheet

From the Tasmota web console:

ValveElec                   -> valve to electric, mode becomes manual
ValveGas                    -> valve to gas, mode becomes manual
ValveAuto                   -> back to automatic, re-evaluates immediately
ValveMode                   -> {"ValveMode":"auto"} or {"ValveMode":"manual"}
ValveUpperThreshold 42       -> set upper threshold to 42 °C (must be > lower)
ValveLowerThreshold 30       -> set lower threshold to 30 °C (must be < upper)
ValveThresholds              -> {"ValveUpperThreshold":42,"ValveLowerThreshold":30}

Over MQTT (replace <topic> with the device’s Tasmota topic, here tasmota_BA39AC):

cmnd/<topic>/ValveElec
cmnd/<topic>/ValveGas
cmnd/<topic>/ValveAuto
cmnd/<topic>/ValveMode
cmnd/<topic>/ValveUpperThreshold   42
cmnd/<topic>/ValveLowerThreshold   30
cmnd/<topic>/ValveThresholds

Each command publishes a JSON reply on stat/<topic>/RESULT. Threshold changes additionally publish their new value (retained) on stat/<topic>/VALVE_UPPER and stat/<topic>/VALVE_LOWER, which is what Home Assistant reads back into the corresponding number entity.


5. Possible next steps

  • Status telemetry — publish {mode, valve_position, temperature, thresholds} to tele/<topic>/SOLARWBWB2 every Teleperiod.
  • Manual-mode timeout — auto-revert to auto after N minutes so a forgotten manual override doesn’t stay forever, exposed as a number entity in HA.
  • Hysteresis as a single setting — derive lower from upper minus a configurable hysteresis band, instead of two independent thresholds.
Comments: