Driving Electric Heaters with OpenHAB, Pilot Wire and Wi-Fi Sensors
Until recently, my heating control system relied on a combination of old “dumb” electric heaters, Oregon temperature sensors, and Chacon on/off modules. Everything communicated over 433 MHz using homemade RFLink gateways running my RFLink2MQTT software on my home made box.
On the software side, I had implemented a custom control system based on target temperatures (Comfort, Eco and Away modes), current room temperature, and a small hysteresis of 0.2°C. The logic was simple: switch heaters on and off to maintain the desired temperature. It worked surprisingly well and served me reliably for many years.
While the software side has proven to be robust, the hardware started to show its age:
- Oregon temperature sensor readings are not always received reliably.
- The mechanical relays inside Chacon DIO modules tend to fail after three to four years of continuous use.
- My heaters are now more than 15 years old. Besides their poor cosmetic condition, they lack the comfort and thermal inertia offered by modern electric heaters.
As I recently had to replace several heaters, I decided it was the perfect opportunity to redesign the whole system. The new version combines modern Wi-Fi temperature sensors, pilot-wire capable heaters, and OpenHAB automation while keeping the same philosophy: simple, reliable and fully automated heating control.
This is the story of my refreshed heating system…
Approach
Problem to solve
Unfortunately, replacing the heaters also meant redesigning the control system in OpenHAB.
Modern electric heaters contain much more electronics than older models, and it is becoming increasingly difficult to find units that behave correctly when their power supply is simply switched off and back on. Most of them require user interaction before resuming operation, such as setting the date and time. Some even emit an annoying “beep” every time power is restored.
As a result, cutting the mains power was no longer a viable control method. I needed a way to drive modern heaters while keeping them permanently powered, without relying on any cloud-based solution.
Another issue is that I have never found a heater with an integrated thermostat capable of maintaining a truly accurate room temperature. When the weather becomes very cold, the actual temperature often ends up below the configured target. For this reason, I wanted to keep my OpenHAB control logic based on external temperature sensors and a hysteresis algorithm.
At the same time, I wanted to move away from my aging 433 MHz infrastructure. While it served me well for years, a Wi-Fi based solution would provide better reliability, easier integration, and one less custom hardware platform.
The concept
My goal was to keep the same temperature + hysteresis approach that had worked well for years, but replace the crude on/off power switching with a more subtle control mechanism based on the pilot wire available on modern electric heaters.
In my setup, I use three temperature profiles:
- Eco / Low: when nobody is using the room or during the night
- Comfort / High: when the room is occupied during normal daytime hours
- Away: when the house is unoccupied for more than one day
The idea is to continue using external temperature sensors while driving the heater through its pilot wire. Depending on the measured temperature and the selected target profile, OpenHAB switches the heater between the following states:
- Stop: when the room temperature exceeds the target temperature plus the hysteresis value
- Comfort: when the room temperature is outside the hysteresis range and the selected profile is Comfort / High
- Eco: same logic as Comfort, but for the Eco / Low profile
This approach combines the best of both worlds. OpenHAB remains responsible for room temperature regulation using accurate external sensors, while the heater itself can still use its built-in control logic and optimized heating modes.
The heaters are never completely powered off, and with properly configured Comfort and Eco temperatures, pilot-wire mode changes occur relatively infrequently. This results in a more elegant and reliable system while preserving the temperature control accuracy of the original setup.
Implementation
Temperature Sensor: Shelly Plus H&T
To replace my aging 433 MHz Oregon sensors, I chose Shelly Plus H&T devices. They provide temperature and humidity measurements over Wi-Fi and provide a simple yet nice display.
My initial goal was to run them on batteries, just like the Oregon sensors. Unfortunately, when battery-powered, the Shelly Plus H&T only reports new values every two hours, or when the temperature changes by more than 0.5°C. This is perfectly acceptable for monitoring, but not for heating control.
The problem appears when the temperature changes slowly. For example, if the temperature changes by less than 0.5°C every few minutes, the sensor may wait nearly two hours before reporting a new value. The resulting gap between the actual and reported temperatures is simply too large for accurate heater control.
I therefore tried the USB-powered trick described here.
The idea is to force the sensor to report itself as externally powered, resulting in much more frequent updates. It worked very well, but battery life dropped to only three or four weeks, far from the six to eight months I used to get with my Oregon sensors.
In the end, I decided to power all Shelly sensors from USB.
|
1 2 3 4 5 6 7 |
Thing mqtt:topic:shellyplusht-1 "shellyplusht-1" (mqtt:broker:mybroker) [ availabilityTopic="shellyplusht-1/online", payloadAvailable="true", payloadNotAvailable="false" ] { Channels: Type number : temperature "temperature" [ stateTopic="shellyplusht-1/status/temperature:0", transformationPattern="JSONPATH($.tC)"] Type number : humidity "humidity" [ stateTopic="shellyplusht-1/status/humidity:0" , transformationPattern="JSONPATH($.rh)"] Type number : batteryLevel "batteryLevel" [ stateTopic="shellyplusht-1/status/devicepower:0", transformationPattern="JSONPATH($.battery.percent)"] Type number : wifiSignal "wifiSignal" [ stateTopic="shellyplusht-1/status/wifi:0" , transformationPattern="JSONPATH($.rssi)∩JS(rssi2signalstrengh.js)"] |
|
1 2 3 4 5 6 7 8 9 10 |
(function(i) { rssiValue = parseInt(i) if(rssiValue >= -30) return 4; if(rssiValue >= -67) return 3; if(rssiValue >= -70) return 2; if(rssiValue >= -80) return 1; return 0 })(input) |
Finally, here is an example of the corresponding OpenHAB items. Note that the Sensor_Updt item is used as a timestamp indicating when the last update was received from the device:
|
1 2 3 4 5 |
Number Sensor_Temp "Temperature [%.1f °C]" <temperature> ["Temperature"] { channel="mqtt:topic:shellyplusht-1:temperature" } Number Sensor_Hum "Humidity [%d %%]" <humidity> ["Humidity"] { channel="mqtt:topic:shellyplusht-1:humidity" } Number Sensor_Batt "Battery [%d %%]" <batterylevel> ["Level"] { channel="mqtt:topic:shellyplusht-1:batteryLevel" } Number Sensor_Wifi "Signal wifi [%d]" <qualityofservice> ["Level"] { channel="mqtt:topic:shellyplusht-1:wifiSignal" } DateTime Sensor_Updt "Last update [%1$ta %1$tR]" <time> ["Timestamp"] { channel="mqtt:topic:shellyplusht-1:temperature" [profile="system:timestamp-update"] } |
Heater Driving with Shelly PM Mini and Shelly Plus 2PM
The pilot-wire control concept is well known in Europe and extensively documented on the Internet. I simply never had a reason to look into it before, as my previous on/off based solution worked well enough for many years.
The idea is to use a pair of diodes together with a Shelly Plus 2PM to generate the four standard pilot-wire modes supported by most “modern” electric heaters:
- Comfort
- Eco
- Stop
- Frost Protection (Anti-Freeze)
To monitor the actual heater power consumption, I also installed a Shelly PM Mini Gen3. This is necessary because the power reported by the Shelly Plus 2PM only reflects the consumption of the pilot-wire control circuit, not the heater itself.
| Mode | SW1 | SW2 |
|---|---|---|
| Comfort | Off | Off |
| Eco | On | On |
| Frost Protection | On | Off |
| Stop | Off | On |
While implementing this, I discovered an interesting inconsistency in the Shelly MQTT API. According to the official documentation the status and command topics do not use exactly the same format.
The status of an output is published on: <device-name>/status/switch:0
The payload is a JSON document containing an output key whose value is either true or false.
Commands, on the other hand, are sent to: <device-name>/command/switch:0 using the lowercase strings on and off.
As a result, retrieving the current state requires a combination of a JSONPath extraction and a transformation map. Likewise, commands must be converted to lowercase because OpenHAB sends ON/OFF commands in uppercase by default.
Below are examples of the Thing and Channel definitions:
|
1 2 3 4 5 6 7 |
Thing mqtt:topic:shellyplus2pm-1 "shellyplus2pm-4" (mqtt:broker:mosquitto) [ availabilityTopic="shellyplus2pm-1/online", payloadAvailable="true", payloadNotAvailable="false" ] { Channels: Type string : update "update" [ stateTopic="shellyplus2pm-1/status/sys", transformationPattern="JSONPATH($.available_updates)"] Type switch : switch0 "switch 0" [ stateTopic="shellyplus2pm-1/status/switch:0", transformationPattern="JSONPATH($.output)∩MAP(true-false-on-off.map)", on="on", off="off", commandTopic="shellyplus2pm-1/command/switch:0"] Type switch : switch1 "switch 1" [ stateTopic="shellyplus2pm-1/status/switch:1", transformationPattern="JSONPATH($.output)∩MAP(true-false-on-off.map)", on="on", off="off", commandTopic="shellyplus2pm-1/command/switch:1"] } |
The true-false-on-off.map transformation file looks like this:
|
1 2 3 4 |
true=on false=off on=true off=false |
And here is an example of the corresponding OpenHAB items:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Group Heater_Parents "Chauffage Parents" <radiator> (lChParents,gHeater) ["HVAC"] Switch Heater_Parents_State "Contrôle" <switch> (Heater_Parents,gHeaterSwitchPilot) ["RadiatorControl"] Switch Heater_Parents_Conf "Température" <switch> (Heater_Parents,gHeaterConf) ["RadiatorControl"] Number Heater_Parents_TempEco "Température ECO [%.1f °C]" <temperature> (Heater_Parents,gHeaterTempEco) ["Control"] {widget="oh-stepper"[step="0.5",min="10",max="25",enableInput="true",autorepeat="true"]} Number Heater_Parents_TempConf "Température CONF [%.1f °C]" <temperature> (Heater_Parents,gHeaterTempConf) ["Control"] {widget="oh-stepper"[step="0.5",min="10",max="25",enableInput="true",autorepeat="true"]} Number Heater_Parents_TempTarg "Température Cible [%.1f °C]" <temperature> (Heater_Parents,gHeaterTempTarg) ["Control"] {widget="oh-stepper"[step="0.5",min="10",max="25",enableInput="true",autorepeat="true"]} Number Heater_Parents_Power "Conso actuelle [%.3f Wh]" <energy> (Heater_Parents) ["Measurement"] { channel="mqtt:topic:shellypmminig3-4:currentpower" } Number Heater_Parents_DayPower "Conso cumulée [%.3f Wh]" <energy> (Heater_Parents,gHeaterDayPower) ["Measurement"] { channel="mqtt:topic:shellypmminig3-4:totalpower" } Number Heater_Parents_HiPower "Conso heures pleines [%.3f Wh]" <energy> (Heater_Parents,gHeaterPower ) ["Measurement"] Number Heater_Parents_LoPower "Conso heures creuses [%.3f Wh]" <energy> (Heater_Parents,gHeaterPower ) ["Measurement"] Switch Heater_Parents_Reset "RaZ conso" <switch> (Heater_Parents,gHeaterPowerReset) ["RadiatorControl"] { channel="mqtt:topic:shellypmminig3-4:resetcounter" } Number Heater_Parents_PilotCtrl "Mode radiateur" <switch> (Heater_Parents,gHeaterPilotWire) ["RadiatorControl"] Switch Heater_Parents_Pilot0 "Pilote 1" <switch> (Heater_Parents) ["RadiatorControl"] { channel="mqtt:topic:shellyplus2pm-4:switch0" } Switch Heater_Parents_Pilot1 "Pilote 2" <switch> (Heater_Parents) ["RadiatorControl"] { channel="mqtt:topic:shellyplus2pm-4:switch1" } |
Finally, the heater control logic itself is implemented using the following OpenHAB rules:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
import org.openhab.core.model.script.ScriptServiceUtil // *************************************************** // SETTINGS // *************************************************** val HEATER_PILOTWIRE_CONF = 0 val HEATER_PILOTWIRE_ECO = 1 val HEATER_PILOTWIRE_FRP = 2 val HEATER_PILOTWIRE_STOP = 3 val HEATER_PILOTWIRE_TEMP = 19 val HEATER_PREFIX="Heater" val HEATER_STATE_SUFFIX="State" val HEATER_PILOT0_SUFFIX="Pilot0" val HEATER_PILOT1_SUFFIX="Pilot1" val HEATER_TARGETTEMP_SUFFIX="TempTarg" val HEATER_PILOTWIRE_SUFFIX="PilotCtrl" val HEATER_HIPOWER_SUFFIX="HiPower" val HEATER_LOPOWER_SUFFIX="LoPower" // *************************************************** // FUNCTIONS // *************************************************** val resetHeaterPower = [ | gHeaterPowerReset.members.forEach[ GenericItem item | item.sendCommand(ON) ] ] // *************************************************** // RULES // *************************************************** rule "Chauffage - gestion fil pilote" when Member of gHeaterPilotWire changed then // get pilot wire order : must be within 0..3, else nothing will happen val int orderValue = (triggeringItem.state as Number).intValue // extract: prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split('_').get(1) try { val pilot0Item = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_PILOT0_SUFFIX) val pilot1Item = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_PILOT1_SUFFIX) switch (orderValue) { case HEATER_PILOTWIRE_CONF: { pilot0Item.sendCommand(OFF) pilot1Item.sendCommand(OFF) } case HEATER_PILOTWIRE_ECO: { pilot0Item.sendCommand(ON) pilot1Item.sendCommand(ON) } case HEATER_PILOTWIRE_FRP: { pilot0Item.sendCommand(ON) pilot1Item.sendCommand(OFF) } case HEATER_PILOTWIRE_STOP: { pilot0Item.sendCommand(OFF) pilot1Item.sendCommand(ON) } } } catch(ItemNotFoundException e) { logError("rules", "Heaters - function setPilotWire - could not get pilot item for: "+ roomName) } end rule "Chauffage - Arret ou mode ECO / CONF selon la temperature cible" when Member of gHeaterSwitchPilot changed then // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split('_').get(1) try{ val pilotWireItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_PILOTWIRE_SUFFIX) if (triggeringItem.state == OFF) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_STOP) } else { val targetTempItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_TARGETTEMP_SUFFIX) val targetTemp = (targetTempItem.state as Number).floatValue // Set heater mode according to target temp if(targetTemp < HEATER_PILOTWIRE_TEMP) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_ECO) } else { pilotWireItem.postUpdate(HEATER_PILOTWIRE_CONF) } } } catch(ItemNotFoundException e) { logError("rules", "Heaters - rules ECO/CONF - could not get item for: "+ roomName) } end rule "Chauffage - Adaptation file pilote eco / conf selon le changement de temperature cible" when Member of gHeaterTempTarg changed then // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split('_').get(1) try { val heaterStateItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_STATE_SUFFIX) if(heaterStateItem.state == ON && heaterStateItem.getGroupNames.contains("gHeaterSwitchPilot")) { val pilotWireItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_PILOTWIRE_SUFFIX) val targetTempItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_TARGETTEMP_SUFFIX) val targetTemp = (targetTempItem.state as Number).floatValue // Set heater mode according to target temp if(targetTemp < HEATER_PILOTWIRE_TEMP) { pilotWireItem.postUpdate(HEATER_PILOTWIRE_ECO) } else { pilotWireItem.postUpdate(HEATER_PILOTWIRE_CONF) } } } catch(ItemNotFoundException e) { logError("rules", "Heaters - rules auto adapt pilot wire uppon targettemp changed - could not get item for: "+ roomName) } end rule "Chauffage - RaZ conso en cours" when Time cron "0 0 7 * * ? *" or Time cron "0 0 23 * * ? *" or Time cron "0 0 0 * * ? *" then resetHeaterPower.apply() end rule "Chauffage - RaZ conso heures pleines / creuses" when Time cron "0 0 0 * * ? *" then gHeaterPower.members.forEach[ GenericItem item | item.postUpdate(0) ] end rule "Chauffage - Dispatching conso heures pleines / creuses " when Member of gHeaterDayPower changed then // ignore in case it has been reset if( (triggeringItem.state as Number).intValue == 0 ) { return } // extract prefix (0), name (1), suffix (3) val roomName = triggeringItemName.split('_').get(1) try { var GenericItem powerItem // LoPower between 23h and 7h, else HiPower if(now.getHour() > 23 || now.getHour() < 7) { powerItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_LOPOWER_SUFFIX) } else { powerItem = ScriptServiceUtil.getItemRegistry.getItem(HEATER_PREFIX + "_"+ roomName +"_" + HEATER_HIPOWER_SUFFIX) } powerItem.state = triggeringItem.state } catch(ItemNotFoundException e) { logError("rules", "Heaters - rules Hi/Lo power dispatching - could not get item for: "+ roomName) } end |
I’m not putting the temperature control logic here as it is too long and contains some room detection logic that would not help for the purpose of this article. But basicaly, it checks the temperature every 2 minutes and change the value of Heater_RoomName_PilotCtrl
Window sensors
In addition to temperature-based control, I also use window sensors as part of the heating automation logic. While most modern heaters provide some form of “open window” detection, I have found these built-in mechanisms to be rather unreliable. They usually react too slowly or fail to detect certain situations altogether.
As with my old Oregon temperature sensors, my previous window sensors relied on a 433 MHz infrastructure. Since one of the goals of this project was to retire all of that aging hardware, I replaced them with inexpensive Wi-Fi sensors based on the CB3S module (BK7231N).
One of the advantages of these devices is that it can be reflashed with a custom firmware through a simple serial connection with BK731Flasher
The pinout is documented here. The four pins required for flashing are:
- 15 = rx
- 16 = tx
- 8 = vcc
- 9 = gnd
- 3 = CEN
I simply soldered a few Dupont wires to the module, connected it to a USB-to-Serial adapter, and flashed the firmware using the appropriate flashing tool.
Once flashed, the sensors can be configured to publish their state directly over MQTT.
Below is the configuration I use on my devices:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "vendor": "Tuya", "bDetailed": "0", "name": "Full Device Name Here", "model": "enter short model name here", "chip": "BK7231N", "board": "TODO", "flags": "1024", "keywords": [ "TODO", "TODO", "TODO" ], "pins": { "7": "Btn;0", "8": "DoorSnsrWSleep_nPup;0", "14": "BAT_Relay;0", "23": "BAT_ADC;0", "26": "WifiLED_n;0" }, "command": "DSEdge 1", "image": "https://obrazki.elektroda.pl/YOUR_IMAGE.jpg", "wiki": "https://www.elektroda.com/rtvforum/topic_YOUR_TOPIC.html" } |
OpenHAB Thing definition is shown below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Thing mqtt:topic:window_sensor1 "window sensor 1" (mqtt:broker:mosquitto) [ availabilityTopic="window_sensor1/connected", payloadAvailable="online", payloadNotAvailable="offline" ] { Channels: Type number : state "State" [ stateTopic="window_sensor1/0/get", on="1", off="0" ] Type number : voltage "Voltage" [ stateTopic="window_sensor1/voltage/get" ] Type number : battery "Battery" [ stateTopic="window_sensor1/battery/get" ] Type string : ip "IP" [ stateTopic="window_sensor1/ip" ] Type number : uptime "Uptime" [ stateTopic="window_sensor1/uptime" ] Type number : freeheap "Freeheap" [ stateTopic="window_sensor1/freeheap" ] Type number : rssi "Rssi" [ stateTopic="window_sensor1/rssi", transformationPattern="SCALE(rssi.scale)"] Type number : sockets "Sockets" [ stateTopic="window_sensor1/sockets" ] Type string : ssid "SSID" [ stateTopic="window_sensor1/ssid" ] Type number : Temperature "Temperature" [ stateTopic="window_sensor1/temp" ] Type string : mac "MAC addr" [ stateTopic="window_sensor1/mac" ] Type string : build "Build" [ stateTopic="window_sensor1/build" ] Type string : host "Hostname" [ stateTopic="window_sensor1/host" ] } |
And finally, here is an example Item definition:
|
1 2 3 4 |
Group:Switch Window_Parents "Fenêtre parents" <mywindow> (lChParents,gWindow) ["Window"] Switch Window_Parents_State "Etat" <mywindow> (Window_Parents,gWindowState) ["OpenState"] { channel="mqtt:topic:window_sensor1:state" [profile="transform:MAP", function="on-off-1-0.map"]} Number Window_Parents_Batt "Batterie [%d %%]" <batterylevel> (Window_Parents,gWindowState,gWindowBatt) ["Level"] { channel="mqtt:topic:window_sensor1:battery" } Number Window_Parents_Wifi "Signal wifi [%d]" <qualityofservice> (Window_Parents,gWindowWifi) ["Level"] { channel="mqtt:topic:window_sensor1:rssi" } |
Using dedicated window sensors allows OpenHAB to immediately stop heating when a window is opened and restore the appropriate heating mode when it is closed again. This approach has proven to be significantly more reliable than relying on the heater’s built-in open-window detection logic.
Sitemap
With all sensors and heater controls integrated into OpenHAB, creating a user-friendly interface becomes straightforward.
Below is an example sitemap representing a single room. It provides quick access to the most relevant information and controls:
- Current temperature and humidity
- Heater operating mode
- Target temperature profile
- Window status
- Heater power consumption
- Sensor health and connectivity information
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
Text item=Sensor_Garage_Temp label="Garage [%.1f °C]" { Text item=Sensor_Garage_Hum Switch item=Window_Garage_State label="Fenêtre" mappings=[ON=" Ouverte " , OFF=" Fermée "] Frame label="Radiateur" { Switch item=Heater_Garage_State mappings=[ON=" Marche " , OFF=" Arret "] Switch item=Heater_Garage_Auto mappings=[ON=" Auto " , OFF=" Manuel " ] Switch item=Heater_Garage_Conf mappings=[ON=" Confort " , OFF="Economie" ] } Frame label="Réglages températures" { Setpoint item=Heater_Garage_TempEco minValue=14 maxValue=23 step=0.5 Setpoint item=Heater_Garage_TempConf minValue=14 maxValue=23 step=0.5 Setpoint item=Heater_Garage_TempTarg minValue=14 maxValue=23 step=0.5 } Frame label="Consommation chauffage" { Text item=Heater_Garage_Power Text item=Heater_Garage_DayPower } Frame label="Sonde température" { Text item=Sensor_Garage_Wifi Text item=Sensor_Garage_Updt } Frame label="Sonde fenêtre" { Text item=Window_Garage_Wifi Text item=Window_Garage_Batt } } |
The resulting interface looks like this:
One of the advantages of this approach is that all heating-related information is available in a single view. It becomes easy to understand why a heater is currently running (or not), verify that sensors are reporting correctly, and quickly identify any issue with the automation logic.
While the interface itself remains deliberately simple, it provides all the information required to monitor and troubleshoot the heating system without having to dig into logs or OpenHAB internals.






