OH2 Control built-in timers of Tasmota device via openHAB

Introduction

Time-based execution of Tasmota device can be done from openHAB via cron from .rules file. However, cron-based execution requires two things to be functioning mostly at all times. Your network and openHAB server. If one of them isn’t working at the time when device should turn on or off, cron job won’t be applied.

Tasmota has 16 built-in timers, which are stored in device’s memory. Meaning, once you set up timers, you don’t need to rely on your openHAB server or your network to function properly at all times. Unfortunately you have to access timers via device’s IP address.

Preparation

In order to control built-in timers via openHAB, you need to make sure to have enabled timers on device. (Configuration → Configure Timer → Enable Timers checked)

Also, make sure to have these add-ons installed:
Bindings: MQTT Binding
Misc: MQTT Broker Moquette
Persistence: MapDB Persistence
Transformations: JSONPath Transformation, RegEx Transformation

Code

Example below shows how water heater connected to Sonoff smart plug flashed with Tasmota can be turned on and off by two built-in timers. Currently time (hour and minute), action (on or off), save and disable timer can be controlled. Repeat and all days are always checked.

default.things

Bridge mqtt:broker:local_broker "Local broker"  [host="localhost"] {
    Thing topic cellar "Cellar" @ "Cellar" {
        Channels:
            Type switch : water_heater_switch  [stateTopic="stat/tasmota_ABCXYZ/POWER", commandTopic="cmnd/tasmota_ABCXYZ/POWER"]
            Type switch : water_heater_state [stateTopic="stat/tasmota_ABCXYZ/POWER"]
            Type string : water_heater_timer_1 [stateTopic="stat/tasmota_ABCXYZ/RESULT", commandTopic="cmnd/tasmota_ABCXYZ/TIMER1", transformationPattern="REGEX:(.*Timer1.*)∩REGEX:(.*Time.*)∩REGEX:(.*Action.*)"]
            Type string : water_heater_timer_2 [stateTopic="stat/tasmota_ABCXYZ/RESULT", commandTopic="cmnd/tasmota_ABCXYZ/TIMER2", transformationPattern="REGEX:(.*Timer2.*)∩REGEX:(.*Time.*)∩REGEX:(.*Action.*)"]
    }
}

groups.items


Group    gWater_Heater_Timers

water_heater.items

Switch    Water_Heater_Switch              "Switch"          <switch>                               {channel="mqtt:topic:local_broker:cellar:water_heater_switch"}
Switch    Water_Heater_State               "Water Heater"    <water>                                {channel="mqtt:topic:local_broker:cellar:water_heater_state"}
String    Water_Heater_Timer_1                                            (gWater_Heater_Timers)    {channel="mqtt:topic:local_broker:cellar:water_heater_timer_1"}
String    Water_Heater_Timer_1_Overview    "Timer 1 [%s]"    <time>
Number    Water_Heater_Timer_1_Hour        "Hour [%d]"       <time>
Number    Water_Heater_Timer_1_Minute      "Minute [%d]"     <time>
Number    Water_Heater_Timer_1_Action      "Action"          <switch>
Number    Water_Heater_Timer_1_Save        "Save"            <contact>
Number    Water_Heater_Timer_1_Disable     "Disable"         <error>
String    Water_Heater_Timer_2                                            (gWater_Heater_Timers)    {channel="mqtt:topic:local_broker:cellar:water_heater_timer_2"}
String    Water_Heater_Timer_2_Overview    "Timer 2 [%s]"    <time>
Number    Water_Heater_Timer_2_Hour        "Hour [%d]"       <time>
Number    Water_Heater_Timer_2_Minute      "Minute [%d]"     <time>
Number    Water_Heater_Timer_2_Action      "Action"          <switch>
Number    Water_Heater_Timer_2_Save        "Save"            <contact>
Number    Water_Heater_Timer_2_Disable     "Disable"         <error>

water_heater.rules

val String disableTimerJson = "{\"Arm\":0,\"Mode\":0,\"Time\":\"00:00\",\"Window\":0,\"Days\":\"0000000\",\"Repeat\":0,\"Output\":1,\"Action\":0}"

rule "Fetch Water Heater Timers"
when
    // every 5 minutes
    Time cron "0 0/5 * * * ?" 
then
    gWater_Heater_Timers.members.forEach[item | item.sendCommand("")] 

    if (Water_Heater_Timer_1.state == NULL) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_1 is NULL.")

        return
    } else if (Water_Heater_Timer_1.state.toString.equals("")) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_1 is empty.")

        return
    }
    
    // Water_Heater_Timer_1.state has to be stored in variable, otherwise error is thrown
    val String timer1Json = Water_Heater_Timer_1.state.toString
    var timer1TimeJson = transform("JSONPATH", "$.Timer1.Time", timer1Json)
    var timer1ActionJson = transform("JSONPATH", "$.Timer1.Action", timer1Json)

    // remove prefix from single digit number for setpoints
    val Integer timer1Hour = Integer::parseInt(timer1TimeJson.toString.split(":").get(0)) 
    val Integer timer1Minute = Integer::parseInt(timer1TimeJson.toString.split(":").get(1))
    val String timer1Action = if (timer1ActionJson.toString == "0") "OFF" else "ON"

    Water_Heater_Timer_1_Hour.postUpdate(timer1Hour)
    Water_Heater_Timer_1_Minute.postUpdate(timer1Minute)
    Water_Heater_Timer_1_Action.postUpdate(timer1ActionJson)
    Water_Heater_Timer_1_Overview.postUpdate(timer1TimeJson + " - " + timer1Action)

    if (Water_Heater_Timer_2.state == NULL) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_2 is NULL.")

        return
    } else if (Water_Heater_Timer_2.state.toString.equals("")) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_2 is empty.")

        return
    }
    
    // Water_Heater_Timer_2.state has to be stored in variable, otherwise error is thrown
    val String timer2Json = Water_Heater_Timer_2.state.toString
    var timer2TimeJson = transform("JSONPATH", "$.Timer2.Time", timer2Json)
    var timer2ActionJson = transform("JSONPATH", "$.Timer2.Action", timer2Json)

    // remove prefix from single digit number for setpoints
    val Integer timer2Hour = Integer::parseInt(timer2TimeJson.toString.split(":").get(0)) 
    val Integer timer2Minute = Integer::parseInt(timer2TimeJson.toString.split(":").get(1))
    val String timer2Action = if (timer2ActionJson.toString == "0") "OFF" else "ON"

    Water_Heater_Timer_2_Hour.postUpdate(timer2Hour)
    Water_Heater_Timer_2_Minute.postUpdate(timer2Minute)
    Water_Heater_Timer_2_Action.postUpdate(timer2ActionJson)
    Water_Heater_Timer_2_Overview.postUpdate(timer2TimeJson + " - " + timer2Action)
end

rule "Update Water Heater Timer 1"
when
    Item Water_Heater_Timer_1_Save received command
then
    // add prefix for single digit numbers
    var String timer1Hour =  if (Water_Heater_Timer_1_Hour.state.toString.length == 1) "0" + Water_Heater_Timer_1_Hour.state.toString else Water_Heater_Timer_1_Hour.state.toString
    var String timer1Minute = if (Water_Heater_Timer_1_Minute.state.toString.length == 1) "0" + Water_Heater_Timer_1_Minute.state.toString else Water_Heater_Timer_1_Minute.state.toString
    val String timer1Time =  timer1Hour + ":" + timer1Minute
    var String timer1Action = if (Water_Heater_Timer_1_Action.state == 0) "OFF" else "ON"
    var timer1Json = "{\"Arm\":1,\"Time\":\"" + timer1Time + "\",\"Window\":0,\"Days\":\"SMTWTFS\",\"Repeat\":1,\"Output\":1,\"Action\":" + Water_Heater_Timer_1_Action.state.toString + "}"

    Water_Heater_Timer_1_Overview.postUpdate(timer1Time + " - " + timer1Action)
    Water_Heater_Timer_1.sendCommand(timer1Json)
end

rule "Disable Water Heater Timer 1"
when
    Item Water_Heater_Timer_1_Disable received command
then
    Water_Heater_Timer_1_Overview.postUpdate("00:00 - OFF")
    Water_Heater_Timer_1_Hour.postUpdate(0)
    Water_Heater_Timer_1_Minute.postUpdate(0)
    Water_Heater_Timer_1_Action.postUpdate(0)

    Water_Heater_Timer_1.sendCommand(disableTimerJson)
end

rule "Update Water Heater Timer 2"
when
    Item Water_Heater_Timer_2_Save received command
then
    // add prefix for single digit numbers
    var String timer2Hour =  if (Water_Heater_Timer_2_Hour.state.toString.length == 1) "0" + Water_Heater_Timer_2_Hour.state.toString else Water_Heater_Timer_2_Hour.state.toString
    var String timer2Minute = if (Water_Heater_Timer_2_Minute.state.toString.length == 1) "0" + Water_Heater_Timer_2_Minute.state.toString else Water_Heater_Timer_2_Minute.state.toString
    val String timer2Time =  timer2Hour + ":" + timer2Minute
    var String timer2Action = if (Water_Heater_Timer_2_Action.state == 0) "OFF" else "ON"
    var timer2Json = "{\"Arm\":1,\"Time\":\"" + timer2Time + "\",\"Window\":0,\"Days\":\"SMTWTFS\",\"Repeat\":1,\"Output\":1,\"Action\":" + Water_Heater_Timer_2_Action.state.toString + "}"

    Water_Heater_Timer_2_Overview.postUpdate(timer2Time + " - " + timer2Action)
    Water_Heater_Timer_2.sendCommand(timer2Json)
end

rule "Disable Water Heater Timer 2"
when
    Item Water_Heater_Timer_2_Disable received command
then
    Water_Heater_Timer_2_Overview.postUpdate("00:00 - OFF")
    Water_Heater_Timer_2_Hour.postUpdate(0)
    Water_Heater_Timer_2_Minute.postUpdate(0)
    Water_Heater_Timer_2_Action.postUpdate(0)

    Water_Heater_Timer_2.sendCommand(disableTimerJson)
end

default.sitemap

sitemap default label="Home" { 
	Frame label="Cellar" { 
		Group item=Water_Heater_State valuecolor=[=="OFF"="red", =="ON"="green"] { 
			Switch item=Water_Heater_Switch
			Text icon="none"
			Group item=Water_Heater_Timer_1_Overview { 
				Setpoint item=Water_Heater_Timer_1_Hour minValue=0 maxValue=23 step=1 
				Setpoint item=Water_Heater_Timer_1_Minute minValue=0 maxValue=59 step=1 
				Switch item=Water_Heater_Timer_1_Action mappings=[0="OFF", 1="ON"] 
				Switch item=Water_Heater_Timer_1_Save mappings=[0="CONFIRM"] 
				Text icon="none"
				Text icon="none"
				Switch item=Water_Heater_Timer_1_Disable mappings=[0="DISABLE TIMER"]
			} 
			Group item=Water_Heater_Timer_2_Overview { 
				Setpoint item=Water_Heater_Timer_2_Hour minValue=0 maxValue=23 step=1 
				Setpoint item=Water_Heater_Timer_2_Minute minValue=0 maxValue=59 step=1 
				Switch item=Water_Heater_Timer_2_Action mappings=[0="OFF", 1="ON"] 
				Switch item=Water_Heater_Timer_2_Save mappings=[0="CONFIRM"] 
				Text icon="none"
				Text icon="none"
				Switch item=Water_Heater_Timer_2_Disable mappings=[0="DISABLE TIMER"]
			} 
		}  
	} 
}

Screenshots

Since I can post only one image as a new user, all screenshots are available on repo.

Conclusion

At the moment, more timers can be controlled just by copy-pasting previous stuff inside .items, .things, .sitemap and .rules file. There are few things missing to control. Such as mode, window, days, repeat, output, toogle action.

Source code is also available on github. Feel free to post replies here or write an issue or submit a pull request on repository.

5 Likes

Good post! I wrote something similar too - can never have enough tutorials!

2 Likes

thanks but timers not working for me, only ON & OFF

Could you please be more specific?

Make sure to have Enable Timers checked in device settings and all required add-ons installed. Also in .things file make sure your state and command topic matches your device’s mqtt full topic.

I installed all required add-ons.
I copied what you wrote down to the following files:

default.things
groups.items
water_heater.items
water_heater.rules
default.sitemap

And just the right topic I changed to what I have set in tasmota mqtt
What works is turning the device off and on
And what does not work is scheduling
For example: I change the time 05:00 press ON and then CONFIRM but the device does not turn on at the time I set.

This is how I set timers 1 & 2 in tasmota:

solved, I found the issue, I also needed to mark the second enable next to repeat
Now it’s working.

1 Like

How can I turn off the Repeat mode?
That will not repeat itself and will be one-time until I choose another time to turn the timer on or off

Generically, you need to send Repeat:0 to the timer.

If you never want it to repeat, change the rules above to change any instance of:

\"Repeat\":1

To:

\"Repeat\":0
2 Likes

Can it work with OH3?

I haven’t test it with OH3. One thing for sure is that broker Moquette isn’t in version 3 anymore so you will have to use different broker and give it a shot.

Edit: I’ve added OH2 to the thread name and openhab2 tag since guide was written for OH2 so people won’t get confused since openHAB version 3. It might work with OH3 with some modification though.

i check and the items work and i change to diffrent broker but the timers not changes in tasmota device
maybe the setting in tasmota wong?

Thanks for the reply.

Since you have changed broker, have you tried to send any command to the device itself via broker to make sure your broker is working?

Timer settings are correct based on the screenshot.

I would probably check if this line in .things file corresponds with your broker settings since you have different broker:

Bridge mqtt:broker:local_broker "Local broker"  [host="localhost"]

Also, you might post log to see what seems to be the problem.

hi,
the broker is fine just check now


`2021-05-16 18:41:06.125 [WARN ] [b.core.model.script.actions.BusEvent] - Cannot convert '{"Arm":1,"Time":"07:12","Window":0,"Days":"SMTWTFS","Repeat":1,"Output":1,"Action":21}' to a command type which item 'Water_Heater_Timer_1' accepts: [OnOffType, RefreshType].

[ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'water_heater-1' failed: The name 'Water_Heater_Timers' cannot be resolved to an item or type; line 8, column 5, length 19 in water_heater

Hi,

I am trying to get the tasmota timers programmed over the OH3 UI.
I have defined a set of mqtt items to do that - they work as expected.

UID: mqtt:topic:7ac24aa150:d1075773e7
label: RWZ_Timer1
thingTypeUID: mqtt:topic
configuration:
  payloadNotAvailable: Offline
  availabilityTopic: tele/RolloWZ/LWT
  payloadAvailable: Online
bridgeUID: mqtt:broker:7ac24aa150
location: Wohnzimmer
channels:
  - id: RWZ_T1_Enable
    channelTypeUID: mqtt:switch
    label: RolloWZ Timer1 Enable
    description: ""
    configuration:
      postCommand: true
      retained: true
      formatBeforePublish: '{"Enable":%s}'
      commandTopic: cmnd/RolloWZ/TIMER1
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Enable
      off: "0"
      on: "1"
  - id: RWZ_T1_Repeat
    channelTypeUID: mqtt:switch
    label: RolloWz Timer1 Repeat
    description: ""
    configuration:
      postCommand: true
      retained: true
      formatBeforePublish: '{"Repeat":%s}'
      commandTopic: cmnd/RolloWZ/TIMER1
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Repeat
      off: "0"
      on: "1"
  - id: RWZ_T1_Time
    channelTypeUID: mqtt:string
    label: RolloWZ Timer1 Time
    description: ""
    configuration:
      commandTopic: cmnd/RolloWZ/TIMER1
      retained: true
      postCommand: true
      formatBeforePublish: '{"Time":%s}'
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Time
  - id: RWZ_T1_Days
    channelTypeUID: mqtt:string
    label: RolloWZ Timer1 Days
    description: ""
    configuration:
      commandTopic: cmnd/RolloWZ/TIMER1
      retained: true
      postCommand: true
      formatBeforePublish: '{"Days":"%s"}'
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Days
  - id: RWZ_T1_Output
    channelTypeUID: mqtt:number
    label: "RolloWZ Timer1 Output "
    description: ""
    configuration:
      commandTopic: cmnd/RolloWZ/TIMER1
      retained: true
      postCommand: true
      formatBeforePublish: '{"Output":%s}'
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Output
  - id: RWZ_T1_Action
    channelTypeUID: mqtt:switch
    label: RolloWZ Timer1 Action
    description: ""
    configuration:
      retained: true
      postCommand: true
      formatBeforePublish: '{"Action":%s}'
      commandTopic: cmnd/RolloWZ/TIMER1
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Action
      off: "0"
      on: "1"
  - id: RWZ_T1_Mode
    channelTypeUID: mqtt:number
    label: RolloWZ Timer1 Mode
    description: ""
    configuration:
      commandTopic: cmnd/RolloWZ/TIMER1
      retained: true
      postCommand: true
      formatBeforePublish: '{"Mode":%s}'
      stateTopic: stat/RolloWZ/RESULT
      transformationPattern: JSONPATH:$.Timer1.Mode

The time and the days are transferred as string input. For some reason that only works if i additionally add an OH-input-card in the metadata of the corresponding item:

value: oh-input-card
config:
  inputmode: text
  footer: Time "hh:mm"
  name: Switchtime
  action: command
  placeholder: hh:mm
  title: Input Time
  type: text
  sendButton: true

The result is:

If i add the masonary to a page everything works although it looks terrible and consumes lots of space:

Actually my plan was to add the whole group from the model: If i do that i get this input mask which is what i planned but the string inputs are not possible :frowning:
Any Idea how to solve that?

grafik

Generally I could need some help to get a decent User interface…
The actual plan was to have a stepper to select the timer number and then handle all timers over one set of items. Not sure if that is possible though.

I’ve implemented this almost verbatim in the current version of OH and its working almost perfectly.
I am getting “Water_Heater_Timer_X is empty” warnings most times the rule “Fetch Water Heater Timers” runs and cannot figure out why. It alternates between Timer 1 and 2.

Here’s the full rules file:

val String disableTimerJson = "{\"Arm\":0,\"Mode\":0,\"Time\":\"00:00\",\"Window\":0,\"Days\":\"0000000\",\"Repeat\":0,\"Output\":1,\"Action\":0}"

rule "Fetch Water Heater Timers"
when
    // every 5 minutes
    Time cron "0 0/5 * * * ?" or Item StudyLight changed 
then
    gWater_Heater_Timers.members.forEach[item | item.sendCommand("")] 
    logInfo("Fetch Water Heater Timers",Water_Heater_Timer_1.state.toString )
    logInfo("Fetch Water Heater Timers",Water_Heater_Timer_2.state.toString )
    
    if (Water_Heater_Timer_1.state == NULL) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_1 is NULL.")

        return
    } else if (Water_Heater_Timer_1.state.toString.equals("")) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_1 is empty.")

        return
    }
    
    // Water_Heater_Timer_1.state has to be stored in variable, otherwise error is thrown
    val String timer1Json = Water_Heater_Timer_1.state.toString
    var timer1TimeJson = transform("JSONPATH", "$.Timer1.Time", timer1Json)
    var timer1ActionJson = transform("JSONPATH", "$.Timer1.Action", timer1Json)

    // remove prefix from single digit number for setpoints
    val Integer timer1Hour = Integer::parseInt(timer1TimeJson.toString.split(":").get(0)) 
    val Integer timer1Minute = Integer::parseInt(timer1TimeJson.toString.split(":").get(1))
    val String timer1Action = if (timer1ActionJson.toString == "0") "OFF" else "ON"

    Water_Heater_Timer_1_Hour.postUpdate(timer1Hour)
    Water_Heater_Timer_1_Minute.postUpdate(timer1Minute)
    Water_Heater_Timer_1_Action.postUpdate(timer1ActionJson)
    Water_Heater_Timer_1_Overview.postUpdate(timer1TimeJson + " - " + timer1Action)

    if (Water_Heater_Timer_2.state == NULL) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_2 is NULL.")

        return
    } else if (Water_Heater_Timer_2.state.toString.equals("")) {
        logWarn("Fetch Water Heater Timers", "Water_Heater_Timer_2 is empty.")

        return
    }
    
    // Water_Heater_Timer_2.state has to be stored in variable, otherwise error is thrown
    val String timer2Json = Water_Heater_Timer_2.state.toString
    var timer2TimeJson = transform("JSONPATH", "$.Timer2.Time", timer2Json)
    var timer2ActionJson = transform("JSONPATH", "$.Timer2.Action", timer2Json)

    // remove prefix from single digit number for setpoints
    val Integer timer2Hour = Integer::parseInt(timer2TimeJson.toString.split(":").get(0)) 
    val Integer timer2Minute = Integer::parseInt(timer2TimeJson.toString.split(":").get(1))
    val String timer2Action = if (timer2ActionJson.toString == "0") "OFF" else "ON"

    Water_Heater_Timer_2_Hour.postUpdate(timer2Hour)
    Water_Heater_Timer_2_Minute.postUpdate(timer2Minute)
    Water_Heater_Timer_2_Action.postUpdate(timer2ActionJson)
    Water_Heater_Timer_2_Overview.postUpdate(timer2TimeJson + " - " + timer2Action)
end

rule "Update Water Heater Timer 1"
when
    Item Water_Heater_Timer_1_Save received command
then
    // add prefix for single digit numbers
    var String timer1Hour =  if (Water_Heater_Timer_1_Hour.state.toString.length == 1) "0" + Water_Heater_Timer_1_Hour.state.toString else Water_Heater_Timer_1_Hour.state.toString
    var String timer1Minute = if (Water_Heater_Timer_1_Minute.state.toString.length == 1) "0" + Water_Heater_Timer_1_Minute.state.toString else Water_Heater_Timer_1_Minute.state.toString
    val String timer1Time =  timer1Hour + ":" + timer1Minute
    var String timer1Action = if (Water_Heater_Timer_1_Action.state == 0) "OFF" else "ON"
    var timer1Json = "{\"Arm\":1,\"Time\":\"" + timer1Time + "\",\"Window\":0,\"Days\":\"SMTWTFS\",\"Repeat\":1,\"Output\":1,\"Action\":" + Water_Heater_Timer_1_Action.state.toString + "}"

    Water_Heater_Timer_1_Overview.postUpdate(timer1Time + " - " + timer1Action)
    Water_Heater_Timer_1.sendCommand(timer1Json)
end

rule "Disable Water Heater Timer 1"
when
    Item Water_Heater_Timer_1_Disable received command
then
    Water_Heater_Timer_1_Overview.postUpdate("00:00 - OFF")
    Water_Heater_Timer_1_Hour.postUpdate(0)
    Water_Heater_Timer_1_Minute.postUpdate(0)
    Water_Heater_Timer_1_Action.postUpdate(0)

    Water_Heater_Timer_1.sendCommand(disableTimerJson)
end

rule "Update Water Heater Timer 2"
when
    Item Water_Heater_Timer_2_Save received command
then
    // add prefix for single digit numbers
    var String timer2Hour =  if (Water_Heater_Timer_2_Hour.state.toString.length == 1) "0" + Water_Heater_Timer_2_Hour.state.toString else Water_Heater_Timer_2_Hour.state.toString
    var String timer2Minute = if (Water_Heater_Timer_2_Minute.state.toString.length == 1) "0" + Water_Heater_Timer_2_Minute.state.toString else Water_Heater_Timer_2_Minute.state.toString
    val String timer2Time =  timer2Hour + ":" + timer2Minute
    var String timer2Action = if (Water_Heater_Timer_2_Action.state == 0) "OFF" else "ON"
    var timer2Json = "{\"Arm\":1,\"Time\":\"" + timer2Time + "\",\"Window\":0,\"Days\":\"SMTWTFS\",\"Repeat\":1,\"Output\":1,\"Action\":" + Water_Heater_Timer_2_Action.state.toString + "}"

    Water_Heater_Timer_2_Overview.postUpdate(timer2Time + " - " + timer2Action)
    Water_Heater_Timer_2.sendCommand(timer2Json)
end

rule "Disable Water Heater Timer 2"
when
    Item Water_Heater_Timer_2_Disable received command
then
    Water_Heater_Timer_2_Overview.postUpdate("00:00 - OFF")
    Water_Heater_Timer_2_Hour.postUpdate(0)
    Water_Heater_Timer_2_Minute.postUpdate(0)
    Water_Heater_Timer_2_Action.postUpdate(0)

    Water_Heater_Timer_2.sendCommand(disableTimerJson)
end

I stuck the 2 logInfo in for debugging

    logInfo("Fetch Water Heater Timers",Water_Heater_Timer_1.state.toString )
    logInfo("Fetch Water Heater Timers",Water_Heater_Timer_2.state.toString )

and the output looks like this:

2022-10-28 08:16:01.301 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer1":{"Arm":1,"Mode":0,"Time":"06:01","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":1}}
2022-10-28 08:16:01.308 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer2":{"Arm":1,"Mode":0,"Time":"06:59","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":0}}
2022-10-28 08:16:01.316 [WARN ] [del.script.Fetch Water Heater Timers] - Water_Heater_Timer_1 is empty.
2022-10-28 08:20:00.295 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer1":{"Arm":1,"Mode":0,"Time":"06:01","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":1}}
2022-10-28 08:20:00.297 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer2":{"Arm":1,"Mode":0,"Time":"06:59","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":0}}
2022-10-28 08:20:00.310 [WARN ] [del.script.Fetch Water Heater Timers] - Water_Heater_Timer_1 is empty.
2022-10-28 08:25:00.295 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer1":{"Arm":1,"Mode":0,"Time":"06:01","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":1}}
2022-10-28 08:25:00.298 [INFO ] [del.script.Fetch Water Heater Timers] - {"Timer2":{"Arm":1,"Mode":0,"Time":"06:59","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":0}}
2022-10-28 08:25:00.332 [WARN ] [del.script.Fetch Water Heater Timers] - Water_Heater_Timer_2 is empty.

Any thoughts on what might be happening?