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
persistmodule, so they survive reboots and OTA updates. - Editable at runtime through Tasmota commands (console, MQTT, web UI,
rules) and through Home Assistant
numberentities. - Validated: must be integers in
[0..100], andLower < Upperis 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()
load_cfg()— reads thresholds frompersist.subscribe()— registers the SOC MQTT subscription. Tasmota’smqttmodule accepts subscriptions before the broker is connected and re-applies them on every (re)connect, so noMqtt#Connectedhandler is needed for subscriptions.- Registers two console commands (
HeaterUpperThreshold,HeaterLowerThreshold). - Schedules HA discovery: on every
Mqtt#Connectedevent, 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 callspersist.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|OFFonly 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
HeaterUpperThresholdon the console would fall through to_set_thresholdwith 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 pernumberentity), grouped under one HA device.publish_state()posts one shared retained JSON state message that both entities read with avalue_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 tosliderif preferred). - Unique IDs:
tongou_0A38A0_upper,tongou_0A38A0_lower. - Availability: both entities advertise
avty_t=tele/tongou_0A38A0/LWTwithpl_avail=Online/pl_not_avail=Offline. This is Tasmota’s built-in Last-Will topic: Tasmota publishesOnline(retained) on connect, and the broker publishesOffline(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#Connectedevent (and after the 10 s boot timer). To permanently un-discover, first remove or disable the script (e.g. deleteautoexec.beand reboot), then clear the retained topics as above.
6. Tasmota / Berry operating cheatsheet
Files & deployment
- The script must be uploaded as
autoexec.bein the Tasmota file system (Consoles → Manage File system). - After upload:
Restart 1to 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
- 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_timercheckingnow - last_soc_ts. - Expose the heater state to HA – add a discovery message for a
switchorbinary_sensormirroringPOWER1, so HA can also command/observe the relay through the same device card. - Expose SOC to HA – add a
sensordiscovery for the last seen SOC, so the dashboard shows the value driving the controller. - Configurable topics & device id – move
DEVICE_TOPIC,SOC_TOPIC, broker prefixes topersistso they can be changed via commands without editing the script. - Min on/off time – add minimum dwell time after each transition to reduce contactor wear (e.g. ignore opposite transitions for 60 s).
- Schedule / time-of-day override – disable heating at night or during peak tariff hours.
HA availability topic– ✅ implemented in V8: discovery payloads includeavty_t=tele/tongou_0A38A0/LWTwithpl_avail=Online/pl_not_avail=Offline, so HA marks the entities as unavailable when the device is offline.- Slider vs box – switch
"mode":"box"to"mode":"slider"if a draggable UI is preferred in HA. - 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. - Remove HA discovery on shutdown – publish empty retained discovery
messages from a dedicated
Heater Forgetcommand, to make decommissioning clean without manualmosquitto_pub.