This text is AI generated!

Heater Controller — autoexec.be - V8

A Tasmota Berry script that switches a waterboiler relay (POWER1) based on the State-Of-Charge (SOC) of a Victron battery received over MQTT, with two user-configurable thresholds, hysteresis, persistence, native Tasmota commands, and Home Assistant MQTT auto-discovery.

  • Tasmota module: TongouSY2
  • Device topic: tongou_0A38A0
  • SOC source topic: victron/N/c0619ab74a5f/battery/512/Soc (payload {"value": 65.0})

1. Operating principle

The script implements a bang-bang controller with hysteresis:

SOC > Heater_Upper_Threshold  →  POWER ON   (battery has surplus → dump into water)
SOC < Heater_Lower_Threshold  →  POWER OFF  (battery low → protect)
Heater_Lower ≤ SOC ≤ Heater_Upper  →  keep current state (deadband)

The deadband prevents the relay from chattering around a single setpoint.

Both thresholds are:

  • Persisted in flash via the Berry persist module, so they survive reboots and OTA updates.
  • Editable at runtime through Tasmota commands (console, MQTT, web UI, rules) and through Home Assistant number entities.
  • Validated: must be integers in [0..100], and Lower < Upper is enforced at set-time.

The script is fully event-driven after init(); there is no polling loop.


2. Code walkthrough

Imports & constants

  • persist, string, json, mqtt — all stdlib Berry modules shipped with Tasmota.
  • DEVICE_TOPIC, SOC_TOPIC, DEFAULT_UPPER (80 %), DEFAULT_LOWER (50 %), HA_DISCOVERY_PREFIX (homeassistant), HA_DEVICE_ID.

class HeaterCtl

Holds three pieces of state:

Field Meaning
upper upper threshold, % (persisted)
lower lower threshold, % (persisted)
last_soc most recent SOC seen (or nil)

init()

  1. load_cfg() — reads thresholds from persist.
  2. subscribe() — registers the SOC MQTT subscription. Tasmota’s mqtt module accepts subscriptions before the broker is connected and re-applies them on every (re)connect, so no Mqtt#Connected handler is needed for subscriptions.
  3. Registers two console commands (HeaterUpperThreshold, HeaterLowerThreshold).
  4. Schedules HA discovery: on every Mqtt#Connected event, plus a one-shot 10-second boot timer (in case the broker is already connected when autoexec runs).

load_cfg() / save_cfg()

  • Uses persist.find("key", default) for compact one-line loads.
  • save_cfg() writes both keys and calls persist.save().

subscribe()

Calls mqtt.subscribe(SOC_TOPIC, callback). The lambda forwards (topic, payload_s) into on_mqtt_soc.

on_mqtt_soc(topic, payload_s)

Parses the JSON payload, extracts value, rounds to the nearest integer, and hands off to on_soc(soc). Wrapped in try/except so a malformed payload can never crash the controller.

on_soc(soc) — the control law

  • Reads current relay state via power_state().
  • Decides ON / OFF / nothing using the hysteresis rule above.
  • Logs a single line per SOC update.
  • Issues Power ON|OFF only if a transition is actually needed.

power_state()

Returns the boolean state of relay 1 (POWER1) using tasmota.get_power().

_parse_percent(payload) — strict validator

  • Strips whitespace.
  • Rejects empty / longer than 3 chars.
  • Rejects anything that is not pure digits (no sign, no decimal, no exponent).
  • Rejects out-of-range values.
  • Returns the int or nil.

_set_threshold(which, payload)

  • Validates with _parse_percent.
  • Enforces Lower < Upper.
  • Persists, re-evaluates with last_soc, republishes HA state, and answers the command with a JSON payload of both thresholds.

cmd_upper(payload) / cmd_lower(payload)

Each command supports two roles:

  • Query (no payload): returns the current value as JSON. The first 4 lines of each handler implement this: without them a bare HeaterUpperThreshold on the console would fall through to _set_threshold with empty payload and be rejected as invalid.
  • Set (with payload): delegates to _set_threshold.

publish_discovery() / publish_state()

  • publish_discovery() posts two retained HA MQTT discovery configs (one per number entity), grouped under one HA device.
  • publish_state() posts one shared retained JSON state message that both entities read with a value_template.

Bootstrap

A single global instance is created at module load:

heater_ctl = HeaterCtl()

3. Resulting command surface

All commands work from the Tasmota web console, serial, MQTT, and rules.

Command Effect Response
HeaterUpperThreshold query {"HeaterUpperThreshold":80}
HeaterUpperThreshold 75 set upper to 75 % {"HeaterUpperThreshold":75,"HeaterLowerThreshold":50}
HeaterLowerThreshold query {"HeaterLowerThreshold":50}
HeaterLowerThreshold 40 set lower to 40 % {"HeaterUpperThreshold":75,"HeaterLowerThreshold":40}
HeaterUpperThreshold 30 (with lower=50) rejected {"HeaterUpperThreshold":"Upper must be > Lower"}
HeaterLowerThreshold 105 rejected {"HeaterLowerThreshold":"Invalid: integer 0..100 only"}

Via MQTT:

cmnd/tongou_0A38A0/HeaterUpperThreshold       → query
cmnd/tongou_0A38A0/HeaterUpperThreshold  75   → set

The shared state topic (retained) is:

stat/tongou_0A38A0/HEATER_THRESHOLDS
{"HeaterUpperThreshold":75,"HeaterLowerThreshold":40}

4. Resulting Home Assistant entities

Two number entities are auto-discovered, both attached to one device (tongou_0A38A0):

Entity ID (default) Name Range Step Unit Icon
number.tongou_0a38a0_upper Heater Upper Threshold 0–100 1 % mdi:thermometer-chevron-up
number.tongou_0a38a0_lower Heater Lower Threshold 0–100 1 % mdi:thermometer-chevron-down
  • Mode: box (numeric input field; switch to slider if preferred).
  • Unique IDs: tongou_0A38A0_upper, tongou_0A38A0_lower.
  • Availability: both entities advertise avty_t = tele/tongou_0A38A0/LWT with pl_avail=Online / pl_not_avail=Offline. This is Tasmota’s built-in Last-Will topic: Tasmota publishes Online (retained) on connect, and the broker publishes Offline (retained) via LWT when the device drops off. As a result the HA entities automatically show as unavailable whenever the Tasmota device is offline (no power, no Wi-Fi, broker unreachable, …) and become available again on reconnect — no extra code required on the device side.
  • Changes from HA publish to the command topic, hit cmd_upper / cmd_lower, are validated and persisted, then echoed back through the state topic so HA reflects the new value (and rejects out-of-range edits cleanly).

5. Un-discovering HA entities

HA discovery is driven by retained messages on the discovery config topics. To make HA forget the entities, publish an empty retained payload to the same topics:

mosquitto_pub -h <broker> -t 'homeassistant/number/tongou_0A38A0/upper/config' -r -n
mosquitto_pub -h <broker> -t 'homeassistant/number/tongou_0A38A0/lower/config' -r -n

-r = retained, -n = empty payload. HA will remove the entities within a few seconds.

You should also clear the retained state topic if you want a clean slate:

mosquitto_pub -h <broker> -t 'stat/tongou_0A38A0/HEATER_THRESHOLDS' -r -n

⚠️ If the Tasmota device is still running this script, it will simply re-publish the discovery config on the next Mqtt#Connected event (and after the 10 s boot timer). To permanently un-discover, first remove or disable the script (e.g. delete autoexec.be and reboot), then clear the retained topics as above.


6. Tasmota / Berry operating cheatsheet

Files & deployment

  • The script must be uploaded as autoexec.be in the Tasmota file system (Consoles → Manage File system).
  • After upload: Restart 1 to execute.

Useful console commands

Command Purpose
Restart 1 reboot the device
Status 0 full status dump
Power / Power ON / Power OFF / Power TOGGLE manual relay control
MqttHost, MqttPort, MqttUser, MqttPassword broker config
Topic tongou_0A38A0 set device topic
SetOption series toggle Tasmota behaviors

Berry REPL (Consoles → Berry Scripting console)

Snippet Purpose
heater_ctl.upper / heater_ctl.lower inspect live values
heater_ctl.last_soc last SOC received
heater_ctl.on_soc(72) inject a fake SOC for testing
heater_ctl.publish_discovery() force HA re-discovery now
heater_ctl.publish_state() force HA state refresh
import persist; persist.heater_upper inspect persisted value
import persist; persist.remove("heater_upper"); persist.save() reset to default on next boot

Watching MQTT

mosquitto_sub -h <broker> -v -t 'stat/tongou_0A38A0/#' \
                              -t 'cmnd/tongou_0A38A0/#' \
                              -t 'homeassistant/number/tongou_0A38A0/#' \
                              -t 'victron/N/c0619ab74a5f/battery/512/Soc'

Logs

Console messages prefixed with [Heater] are emitted by this script:

  • on init: [Heater] init: upper=80% lower=50%
  • on every SOC: [Heater] SOC=65% upper=80 lower=50 power=OFF
  • on parse error: [Heater] mqtt parse error: ...

7. Possible improvements

  1. Fail-safe on stale SOC – if no SOC message arrives for N minutes, force POWER OFF (or fall back to a safe default). Easy to add with a periodic tasmota.set_timer checking now - last_soc_ts.
  2. Expose the heater state to HA – add a discovery message for a switch or binary_sensor mirroring POWER1, so HA can also command/observe the relay through the same device card.
  3. Expose SOC to HA – add a sensor discovery for the last seen SOC, so the dashboard shows the value driving the controller.
  4. Configurable topics & device id – move DEVICE_TOPIC, SOC_TOPIC, broker prefixes to persist so they can be changed via commands without editing the script.
  5. Min on/off time – add minimum dwell time after each transition to reduce contactor wear (e.g. ignore opposite transitions for 60 s).
  6. Schedule / time-of-day override – disable heating at night or during peak tariff hours.
  7. HA availability topic – ✅ implemented in V8: discovery payloads include avty_t=tele/tongou_0A38A0/LWT with pl_avail=Online / pl_not_avail=Offline, so HA marks the entities as unavailable when the device is offline.
  8. Slider vs box – switch "mode":"box" to "mode":"slider" if a draggable UI is preferred in HA.
  9. Unit tests in Berry – add a small self-test driven from the REPL (on_soc(81), on_soc(49), etc.) to validate state machine after changes.
  10. Remove HA discovery on shutdown – publish empty retained discovery messages from a dedicated Heater Forget command, to make decommissioning clean without manual mosquitto_pub.
Comments: