Eurotronic Spirit Zigbee Thermostat2

Couldn’t find the guide on how to intergrate Eurotronic SPZB0001 via zigbee2mqtt with OpenHAB so here is my way of doing it.


    Thing mqtt:topic:thermostat4 "Eurotronic Thermostat" @ "Office" {
        Type number : local_temperature           "Local Temperature"           [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.local_temperature" ]
        Type number : current_heating_setpoint    "current_heating_setpoint"    [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.current_heating_setpoint",
                                                                                commandTopic="zigbee2mqtt/thermostat4/set", formatBeforePublish="{ \"current_heating_setpoint\": %s }"]
        Type switch : mirror_display              "Mirror Display"      [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_host_flags.mirror_display",     on="true", off="false",
                                                                        commandTopic="zigbee2mqtt/thermostat4/set",  transformationPatternOut="JS:eurotronic_host_flags-mirror_display.js",  on="true", off="false"]
        Type switch : boost                       "Boost"               [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_host_flags.boost",              on="true", off="false", 
                                                                        commandTopic="zigbee2mqtt/thermostat4/set",  transformationPatternOut="JS:eurotronic_host_flags-boost.js",           on="true", off="false"]
        Type switch : window_open                 "Window Open"         [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_host_flags.window_open",        on="true", off="false", 
                                                                        commandTopic="zigbee2mqtt/thermostat4/set",  transformationPatternOut="JS:eurotronic_host_flags-window_open.js",     on="true", off="false"]
        Type switch : child_protection            "Child Protecton"     [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_host_flags.child_protection",   on="true", off="false",
                                                                        commandTopic="zigbee2mqtt/thermostat4/set",  transformationPatternOut="JS:eurotronic_host_flags-child_protection.js",on="true", off="false"]
        Type string : system_mode                 "System Mode"                 [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.system_mode",
                                                                                commandTopic="zigbee2mqtt/thermostat4/set", formatBeforePublish="{ \"system_mode\": \"%s\" }"]
        Type number : batt                        "Batt"                        [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.battery" ]
        Type number : link                        "Link"                        [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.linkquality" ]
        Type number : lastseen_epoch              "LastSeen"                    [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.last_seen" ]
        Type number : pi_heating_demand           "pi_heating_demand"           [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.pi_heating_demand" ]
        Type number : eurotronic_error_status     "eurotronic_error_status"     [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_error_status" ]
//      Type number : eurotronic_system_mode      "Eurotronic System Mode"      [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.eurotronic_system_mode" ]
//      Type number : occupied_heating_setpoint   "occupied_heating_setpoint"   [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.occupied_heating_setpoint" ]
//      Type number : unoccupied_heating_setpoint "unoccupied_heating_setpoint" [ stateTopic="zigbee2mqtt/thermostat4", transformationPattern="JSONPATH:$.unoccupied_heating_setpoint" ]

There is a problem with using formamtBeforePublish outgoing transformation in OH 2.5.1 with switches, as discussed here HERE. So we have to use JS to form the correct outgoing JSON when setting the extra eurotronic flags.


(function(flag) {
    if (flag == 'true') {
        var data = '{"eurotronic_host_flags": {"boost": true}}';
    } else if (flag == 'false') {
        var data = '{"eurotronic_host_flags": {"boost": false}}';
    return data;

You need to create three more .js transformations, one for each of the flags used.
Substitute the “boost” with “child_protection” “mirror_display” “window_open” and save them naming accordingly.

Here is the error map to give some meanings to the numeric errors. Havent tried myseld, as had no operational errors so far.


0=No errors
4=Valve adaption failed (E1)
8=Valve movement too slow (E2)
16=Valve not moving/blocked (E3)


Number   TS4_local_temperature           "Current Temperature [%.1f°C]"         <temperature>   (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:local_temperature"}
Number   TS4_current_heating_setpoint    "Temperature Setpoint [%.1f°C]"        <heating>       (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:current_heating_setpoint"}
String   TS4_system_mode                 "Mode"                                 <radiator>      (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:system_mode"}
Number   TS4_pi_heating_demand           "Valve open [%d %%]"                   <pressure>      (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:pi_heating_demand"}
Switch   TS4_mirror_display              "Mirror Display"                       <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:mirror_display"}
Switch   TS4_boost                       "Boost"                                <switch>        (gPers_Change_Day, gTS_Boost)   {channel="mqtt:topic:thermostat4:boost"}
Switch   TS4_boost_Timer                                                        <switch>        (gTS_Boost_Timer)               {expire="15m, command=OFF"} // Timer
Switch   TS4_window_open                 "Window Open"                          <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:window_open"}
Switch   TS4_window_open_Timer                                                  <switch>                                        {expire="15m, command=OFF"} // Timer
Switch   TS4_child_protection            "Child Protection"                     <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:child_protection"}
Number   TS4_batt                        "Battery: [%s]"                        <battery>       (gPers_Change_Day, gZB_bat)     {channel="mqtt:topic:thermostat4:batt"}
Number   TS4_link                        "Link: [%s]"                  <qualityofservice>       (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:link"}
Number   TS4_lastseen_epoch              "Last Seen: [%s]"                                      (gZB_lastseen)                  {channel="mqtt:topic:thermostat4:lastseen_epoch"}
DateTime TS4_lastseen_datetime           "Last Seen: [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]"
Number   TS4_eurotronic_error_status     "Error [MAP(]" <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:eurotronic_error_status"}
//Number TS4_eurotronic_system_mode      "eurotronic_system_mode"               <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:eurotronic_system_mode"}
//Number TS4_occupied_heating_setpoint   "occupied_heating_setpoint"            <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:occupied_heating_setpoint"}
//Number TS4_unoccupied_heating_setpoint "unoccupied_heating_setpoint"          <switch>        (gPers_Change_Day)              {channel="mqtt:topic:thermostat4:unoccupied_heating_setpoint"}

Make sure to define all the used groups as well as the items.

The manufacturer claims that there is an inbuilt algorithm that senses the drastic change in temperature and makes a decision regarding status of the window, but since I have contact sensors on all of the windows I have implemented this logic manually. The thermostat would go into window open mode 1 minute after the window is open and will stay in that mode for 15 minutes.

rule "office window timer "
    Item sens_contact9_status changed
        if      ( sens_contact9_status.state == "false" ) {
                postUpdate(sens_contact9_Timer, "ON") // Set timer
        else if ( sens_contact9_status.state ==  "true" ) {
                 postUpdate(sens_contact9_Timer, "OFF")

rule "office window timer expires and window is still open"
    Item sens_contact9_Timer received update "OFF"
        if      ( sens_contact9_status.state == "false" ) {
                sendCommand(TS4_window_open, "ON") // Set Window Open mode
                postUpdate(TS4_window_open_Timer, "ON") // Set Window Open Timer
        else if ( sens_contact9_status.state ==  "true" ) {
                postUpdate(TS4_window_open_Timer, "OFF") // Cancel Window Open Timer

rule "office window timer expires"
    Item TS4_window_open_Timer received update "OFF"
        sendCommand(TS4_window_open, "OFF") // Set Window Open mode to OFF
        if ( sens_contact9_status.state == "false" ) {
                sendPushoverMessage(pushoverBuilder("Office Window is OPEN for 15 minutes. Consider closing"))

By default the boost mode simply sets the temperature setpoint to 30 degrees, I decided to limit the time it heats at full power to 15 minutes. The rule works both, when the boost mode is initiated via mqtt or via a physical press of the button on the thermostat itself. Note that this rule applies to all thermostats that you have in the group, while the “window open” is linked individually to a particurlar contact sensor.

import org.eclipse.smarthome.model.script.ScriptServiceUtil
rule "Thermostat boost enabled"
    Member of gTS_Boost received update "ON"
        val triggeringItem_Timer = ScriptServiceUtil.getItemRegistry.getItem( + "_Timer")
        if      ( triggeringItem.state == ON && triggeringItem_Timer.state != ON ) {
                postUpdate(triggeringItem_Timer, "ON") // Set timer

rule "Thermostat boost timer expires"
    Member of gTS_Boost_Timer received update "OFF"
        val triggeringItem_main = ScriptServiceUtil.getItemRegistry.getItem("_Timer").get(0))
        sendCommand(triggeringItem_main, "OFF") // Set Boost mode to OFF

rule "Thermostat boost manually switched OFF"
    Member of gTS_Boost received update "OFF"
        val triggeringItem_Timer = ScriptServiceUtil.getItemRegistry.getItem( + "_Timer")
        if      ( triggeringItem_Timer.state == ON ) {
                sendCommand(triggeringItem_Timer, "OFF") //  Disable timer
                //postUpdate(triggeringItem_Timer, "OFF") //  Disable timer

Another couple of rules that i use to convert the last report time of the zibee sensors to human readable for and alert in cases the sensors was quiet for more then 2 hrs. The low level sensor battery status is checked and reported as well.


rule "convert lastseen time from epoch to ISO8601"
     Member of gZB_lastseen changed
     // Determine new variable name that will contain DateTime type lastseen value
     val triggeringItem_with_datetime ="_epoch").get(0)+ "_datetime"
     // Convert from epoch to ISO8601 DateTime type lastseen value
     var DateTime DateTimeFromEpoch = new DateTime((triggeringItem.state as Number).longValue)

     logInfo("ZigBeeRules", + ": " + DateTimeFromEpoch.toString)

rule "Check last report time and battery level"
      Time cron "0 0 0/1 * * ? *"
//     Time cron "0 0/1 * * * ? *"
     val current_epoch = now.getMillis()
     //logInfo("ZigBeeRules", current_epoch.toString)

     gZB_lastseen.members.forEach[member |
     logInfo("ZigBeeRules", "'{}' lastseen is " + member.state.toString,
       if ( member.state != UNDEF && member.state != NULL ) {
          // logInfo("ZigBeeRules", "'{}' lastseen is " + member.state.toString,
           val timesincelastcontact = current_epoch - member.state as Number
           if ( timesincelastcontact > 7200000 ) {
              logInfo("ZigBeeRules", "'{}' time since contact is " + timesincelastcontact.toString,
              sendPushoverMessage(pushoverBuilder("ZigBeeRules " + + " time since contact is " + timesincelastcontact.toString))
        else  {
           logInfo("ZigBeeRules", "'{}' is in " + member.state.toString + " state. Please investigate",
           sendPushoverMessage(pushoverBuilder("ZigBeeRules " + + " is in " + member.state.toString + " state. Please investigate"))

     gZB_bat.members.forEach[ member |
         // logInfo("MyRules", "processing member '{}'",
         if ( member.state < 40 ) {
            logInfo("ZigBeeRules", "'{}' battery level is " + member.state.toString,
         if ( member.state < 20 ) {
            logInfo("ZigBeeRules", "'{}' battery level is " + member.state.toString,
           sendPushoverMessage(pushoverBuilder("ZigBeeRules " + + " battery level is " + member.state.toString))

Finally the sitemap part.

                Frame label="Thermostat" {
                        Setpoint item=TS4_current_heating_setpoint minValue=5 maxValue=30 step=1
                        Default  item=TS4_local_temperature labelcolor=[>24="orange",>20="green",>15="blue"] valuecolor=[>24="orange",>20="green",>15="blue"]
                        Switch   item=TS4_system_mode mappings=[off='OFF', heat='ON', auto='AUTO']
                        Text     item=TS4_pi_heating_demand labelcolor=[>75="red",>50="orange",>25="green",>5="blue"] valuecolor=[>75="red",>50="orange",>25="green",>5="blue"]{
                                Frame label="Options" {
                                        Default item=TS4_mirror_display
                                        Default item=TS4_boost
                                        Default item=TS4_window_open
                                        Default item=TS4_child_protection
                                Frame label="Misc" {
                                        Default item=TS4_batt
                                        Default item=TS4_link
                                        Default item=TS4_lastseen_datetime
                                        Default item=TS4_eurotronic_error_status

I guess thats it, hope that someone will find it useful.


Hi Konstantin,

thank you for your great tutorial! It helped me a lot to get this thermostats working.

There is one thing you could simplify:
If you set advanced property last_seen to ISO_8601_local in the configuration.yaml of zigbee2mqtt you’ll get the time stamp directly in the right format. So your additional item TS4_lastseen_datetime and the conversion rule is not needed anymore.

My item now looks like this:

DateTime radiator_store1_lastseen "Last seen: [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" {channel="mqtt:topic:mosquitto:radiator_store1:lastseen"}


Hi Peter,

the reason I prefer to use epoch in all of mine zigbee devices as the last seen reported field is the simplicity of checking which devices have not reported for more then a certain time. in this case its 7200 seconds (2hrs). As far as I remember I had some troubles with comparing the ISO_8601 values in OH more then a year ago when I introduced zignbee into my home network, so I set epoch in Z2M and it remained so from the start. But thanks for the tip anyway.

One more thing I observed since I wrote the initial post, is that the thermostats sometimes do not fully close, indicating the pi_demand_valve open at 2-5%. The radiators remain warm and the temperature in the room starts raising. The only solution I found is to open the valve fully, then close it, then set it back to auto. Here is the script that checks for such conditions i.e. not fully closed valve and the temperature in the room higher then set by 1 degree.

// Set Valve to 0% forcefully
rule “Thermostat valve check closure”
Item TS4_local_temperature changed or
Item TS5_local_temperature changed or
Item TS6_local_temperature changed or
Item TS7_local_temperature changed

    var TSX_local_temperature = ScriptServiceUtil.getItemRegistry.getItem("_local_temperature").get(0) + "_local_temperature")
    var TSX_current_heating_setpoint = ScriptServiceUtil.getItemRegistry.getItem("_local_temperature").get(0) + "_current_heating_setpoint")
    var TSX_pi_heating_demand = ScriptServiceUtil.getItemRegistry.getItem("_local_temperature").get(0) + "_pi_heating_demand")
    var TSX_system_mode = ScriptServiceUtil.getItemRegistry.getItem("_local_temperature").get(0) + "_system_mode")

    logInfo("TS_INFORMATIONAL: ", "Temperature (" + + ") in the room changed to (" + TSX_local_temperature.state + ") , set temperature (" + TSX_current_heating_setpoint.state + ") and valve state (" + TSX_pi_heating_demand.state + ")")

    // If temperature in the room is higher then the set temperature AND valve is not closed
    if      ( ((TSX_local_temperature.state as DecimalType) > (TSX_current_heating_setpoint.state as DecimalType) + 1) && TSX_pi_heating_demand.state > 0 ) {
            logInfo("TS_WARNING: ", "execute FORCED CLOSURE - Temperature (" + + ") in the room (" + + ": " + TSX_local_temperature.state + ") is higher then set temperature (" + TSX_current_heating_setpoint.state + ") and valve is not fully closed (" + TSX_pi_heating_demand.state + ")")

            // Set valve to 100%, wait a bit, turn the heating off, update temp and set to auto. This works.
            sendCommand(TSX_system_mode, "heat")
            sendCommand(TSX_system_mode, "off")
            sendCommand(TSX_current_heating_setpoint, "23" )
            sendCommand(TSX_system_mode, "auto")



This makes sense - thank you.

I will have a look for this. If I observe the same behavior, now I know how to solve it :+1:

Sorry for my off-topic question:

Are you satisfied with your thermostat? I’ve one, too. Unfortunately, it often looses the connection to my zigbee2mqtt stick, especially when the battery is low. There’s no big distance between my zigbee gateway and the thermostat, but the battery already gets low after a few weeks.

I am pretty much satisfied, I have 4 of those units.
The problem with the p_valve stuck at 2% or so, usually occurs when the batteries get low, so I assume thats what causing them. 2x AA batteries roughly last for 6-7 months of active use. As far as I can remember I’ve installed new batteries in October last year and now they are at 40, 20, 20, 30 %.
The connection of those units with Z2M with CC26X2R1 is pretty stable. There were problems with recent z2m versions and controlling of those units, not sure if it was solved.

Thank you for describing your experinces! I’m using CC2531 as Zigbee Stick. Maybe I try to update my zigbee2mqtt version.

I’ve been using cc2531 initially, but when reached around 25 devices the network became unstable.
I would recommend you change to a more powerfull coordinator.