Balboa Binding

Did anybody out there try to implement something in OpenHAB to integrate into Balboa Hot Tub using the WiFi Module??

Seems like someone dealed with it for Samsung SmartThings.

1 Like

What is your goal?
Mine was to control the time the spa (balboa 601) heat the water. Now that I have solar panels I decided to ensure I only heat the spa during the daytime.

On the Balboa board you will find a jumper J29. When this jumper is closed (wire across the terminals) the spa will stop the elements from heating the water. So you can use a wifi switch to close the contact. You can get even more clever and get the info from your Solar inverter and insure you only heat the water when the PV is above a certain value or use the Astro binding to ensure you only heat the water say 1 hour after sun up.

Hope this helps

Well that is just a small part of the complete story.

The goal is to have a binding available which I can use to monitor the current temperature and to set the target temperature.

Without knowing the current temperature, I cannot decide if to heat the water right now or not.
The idea is to “store” energy in the tub rather than feed it to the grid.
This makes sense only when I can control the target temperature and leave the rest to the Balboa software.
The maximum temperature is at 40°C and I would allow the thing to go down to 36°C
Also setting the system into “REST” mode via OpenHAB would make sense with a “Holiday” feature which controls all temperatures at home (AirCon+Heating).

So what I really would need is a proper Balboa Binding.

Well, that topic seems quite active within the SmartThings community - and I found all information that are required to develop a binding for OpenHab:

Now, here comes the issue:

I have no clue how to develop my own binding for OpenHab. I’m a programmer for more than 30 years, but I have never written one line of java code, nor do I know which tools to use and where to start.

1 Like

To develop your own Binding, you should start reading


and setup an IDE


During setup choose to develop an openHAB2-addon and then have a closer look at the milight Binding, as it uses UDP to communicate with the devices.

Here’s my integration of teh Bulboa controller. You need to know the IP Address of your controller to configure the Python script.

I do not take credit for this script as it was posted in the original ST threads above. I just modified it to be easier to integrate into Openhab by formatting the output as JSON and added some arguments for commands to execute:

(spa.py) Python Script. Out in /etc/openhab2/scripts/
You will need to modify the line “SpaClient.s.connect((‘192.168.2.127’, 4257))” to be the ip address of your controller.

import sys
import json
import crc8
import logging
import socket
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)


class SpaClient:

    def __init__(self, socket):
        self.s = socket
        self.light = False
        self.current_temp = 0
        self.hour = 12
        self.minute = 0
        self.heating_mode = ""
        self.temp_scale = ""
        self.temp_range = ""
        self.pump1 = ""
        self.pump2 = ""
        self.set_temp = 0
        self.read_all_msg()
        self.priming = False
        self.time_scale = "12 Hr"
        self.heating = False
        self.circ_pump = False

    s = None

    @staticmethod
    def get_socket():
        if SpaClient.s is None:
            SpaClient.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            SpaClient.s.connect(('192.168.2.127', 4257))
            SpaClient.s.setblocking(0)
        return SpaClient.s

    def handle_status_update(self, byte_array):
        self.current_temp = 'NA' if (byte_array[2] == 255) else byte_array[2]
        self.priming = byte_array[1] & 0x01 == 1
        self.hour = byte_array[3]
        self.minute = byte_array[4]
        self.heating_mode = \
            ('Ready', 'Rest', 'Ready in Rest')[byte_array[5]]
        flag3 = byte_array[9]
        self.temp_scale = 'Farenheit' if (flag3 & 0x01 == 0) else 'Celcius'
        self.time_scale = '12 Hr' if (flag3 & 0x02 == 0) else '24 Hr'
        flag4 = byte_array[10]
        self.heating = '0' if (flag4 & 0x30 == 0) else '1'
        self.temp_range = 'Low' if (flag4 & 0x04 == 0) else 'High'
        pump_status = byte_array[11]
        #self.pump1 = ('Off', 'Low', 'High')[pump_status & 0x03]
        self.pump2 = ('Off', 'Low', 'High')[pump_status & 0x12]
        self.pump1 = ('Off', 'Low', 'High')[pump_status & 0x03]
        #self.pump2 = byte_array[11]
        self.circ_pump = byte_array[13] & 0x02 == 1
        self.light = '1' if (byte_array[14] == 3) else byte_array[14]
        #self.light = byte_array[14] & 0x03
        self.set_temp = byte_array[20]

    def get_set_temp(self):
        return self.set_temp

    def get_pump1(self):
        return self.pump1

    def get_pump2(self):
        return self.pump2

    def get_temp_range(self):
        return self.temp_range

    def get_current_time(self):
        return "%d:%02d" % (self.hour, self.minute)

    def get_light(self):
        return self.light

    def get_current_temp(self):
        return self.current_temp

    def string_status(self):
        s = ""
        s = s + "{\n"
        s = s + '"TEMP": "%d",\n"SET_TEMP": "%d",\n"TIME": "%d:%02d",\n' % \
            (self.current_temp, self.set_temp, self.hour, self.minute)
        s = s + '"PRIMING": "%s",\n"HEATING_MODE": "%s",\n"TEMP_SCALE": "%s",\n"TIME_SCALE": "%s",\n' % \
            (self.priming, self.heating_mode, self.temp_scale, self.time_scale)
        s = s + '"HEATING": "%s",\n"TEMP_RANGE": "%s",\n"PUMP1": "%s",\n"PUMP2": "%s",\n"CIRC_PUMP": "%s",\n"LIGHTS": "%s"\n' % \
            (self.heating, self.temp_range, self.pump1, self.pump2, self.circ_pump, self.light)
        s = s + "}\n"
        return s

    def compute_checksum(self, len_bytes, bytes):
        hash = crc8.crc8()
        hash._sum = 0x02
        hash.update(len_bytes)
        hash.update(bytes)
        checksum = hash.digest()[0]
        checksum = checksum ^ 0x02
        return checksum

    def read_msg(self):
        chunks = []
        try:
            len_chunk = self.s.recv(2)
        except:
            return False
        if len_chunk == b'' or len(len_chunk) == 0:
            return False
        length = len_chunk[1]
#	print "length is", length
        #if int(length) == 0:
        #    return False

        try:
            chunk = self.s.recv(length)
        except:
            LOGGER.error("Failed to receive: len_chunk: %s, len: %s",
                         len_chunk, length)
            return False
        chunks.append(len_chunk)
        chunks.append(chunk)

        # Status update prefix
        if chunk[0:3] == b'\xff\xaf\x13':
                # print("Status Update")
                self.handle_status_update(chunk[3:])

        return True

    def read_all_msg(self):
        while (self.read_msg()):
            True

    def send_message(self, type, payload):
        length = 5 + len(payload)
        checksum = self.compute_checksum(bytes([length]), type + payload)
        prefix = b'\x7e'
        message = prefix + bytes([length]) + type + payload + \
            bytes([checksum]) + prefix
        #print(message)
        self.s.send(message)

    def send_config_request(self):
        self.send_message(b'\x0a\xbf\x04', bytes([]))

    def send_toggle_message(self, item):
        # 0x04 - pump 1
        # 0x05 - pump 2
        # 0x11 - light 1
        # 0x51 - heating mode
        # 0x50 - temperature range

        self.send_message(b'\x0a\xbf\x11', bytes([item]) + b'\x00')

    def set_temperature(self, temp):
        self.set_temp = int(temp)
        self.send_message(b'\x0a\xbf\x20', bytes([int(temp)]))

    def set_light(self, value):
        if self.light == value:
            return
        self.send_toggle_message(0x11)
        self.light = value

    def set_pump1(self, value):
        if self.pump1 == value:
            return
        if value == "High" and self.pump1 == "Off":
            self.send_toggle_message(0x04)
            self.send_toggle_message(0x04)
        elif value == "Off" and self.pump1 == "Low":
            self.send_toggle_message(0x04)
            self.send_toggle_message(0x04)
        else:
            self.send_toggle_message(0x04)
        self.pump1 = value


import time
c = SpaClient(SpaClient.get_socket())
#time.sleep(2)
#c.read_all_msg()
#print(c.string_status())
#c.send_config_request()
#c.send_toggle_message(0x11)
#c.set_temperature(97)
#time.sleep(2)
#c.read_all_msg()
#print(c.string_status())
#sc.send_toggle_message(0x11) #light
#send_toggle_message(0x04) #pump1
#send_toggle_message(0x05) #light

if str(sys.argv[1]) == "status":
    time.sleep(1)                    
    c.read_all_msg()
    print(c.string_status())
if str(sys.argv[1]) == "lights":
#    c.send_toggle_message(0x04) #pump1
    #c.send_toggle_message(0x05) #pump2
    c.send_toggle_message(0x11) #lights
if str(sys.argv[1]) == "jets":
    c.send_toggle_message(0x05) #pump2
    #c.send_toggle_message(0x11) #lights


spa.items

Group gHotTub
Group gHotTub_Parse_json //used to parse values out into each item using .foreachmember

String SPARaw "SPA: [%s]"  (gHotTub)     {channel="exec:command:spa:output"}  //defined in spa.things
String BULLFROG_TEMP "Current Temperature   [%s °F]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_SET_TEMP "Target Temperature   [%s °F]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_PRIMING "Priming    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_HEATING_MODE "Heating Mode    [%s]" 
String BULLFROG_TEMP_SCALE "Temp Scale    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_TIME_SCALE "Time Scale    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_HEATING "Heating    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_TEMP_RANGE "Temp Range    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_PUMP1 "Jets 1    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_PUMP2 "Jets 2    [%s]" (gHotTub,gHotTub_Parse_json)
String BULLFROG_CIRC_PUMP                           (gHotTub,gHotTub_Parse_json)
String BULLFROG_LIGHTS "Lighting    [%s]"           (gHotTub,gHotTub_Parse_json)
DateTime SPARaw_LastChanged "Last Changed [%1$tm/%1$td %1$tH:%1$tM]" (gHotTub)
//heat runtime items:
DateTime HotTub_Heater_LastOnState "Heater Last On: [%1$tm/%1$td %1$tH:%1$tM]" (gHotTub)        //Timestamp when heater turns on. Use this to calculate duration when it turns off
DateTime HotTub_Heater_LastOFFState "Heater Last OFF: [%1$tm/%1$td %1$tH:%1$tM]" (gHotTub)        //Timestamp when heater turns OFF. Use this to calculate how long it's been since it was last on,
Number HotTub_Heater_Last_Runtime_Sec  "Current Runtime [%d sec] seconds" (gHotTub)             //Daily total of Heating Duration in Seconds
Number HotTub_Heater_Last_Runtime_Min  "Current Runtime [%d min] Minutes" (gHotTub)             //Daily total of Heating Duration in Minutes

//switch when activated will turn the following items on; Hot Tub Jets, Hot Tub Lights and Deck Lights
Switch vReady_Hottub_For_Use  "Turn Jets and Lights On Hottub" (gHotTub) ["Lighting"]

spa.things:

Thing exec:command:spa [command="/usr/bin/python3 /etc/openhab2/scripts/spa.py status", interval=10, timeout=5]

spa.rules

rule "Hot Tub: Parsing (SPARaw) JSON output to individual Items"
when
   Item SPARaw changed
then
logInfo("-18-bullfrog.R2", "### Hot Tub: Parsing (SPARaw) JSON output to individual Items ###")
{
   val String json = (SPARaw.state as StringType).toString
   gHotTub_Parse_json.members.forEach [ value |
      var String name = value.name.replace('BULLFROG_','$.')
      value.postUpdate(transform("JSONPATH", name, json).replaceAll('"',''))
   ]
   SPARaw_LastChanged.postUpdate( new DateTimeType() )
}
end


rule "Hot Tub: Heater ON. Starting Duration Recording"
when
	Item BULLFROG_HEATING changed from 0 to 1
then
logInfo("-18-bullfrog.R4", "### Hot Tub: Heater ON. Starting Duration Recording ###")
	{
	    postUpdate(HotTub_Heater_LastOnState, new DateTimeType())
        //calculate how many minutes its been since the heater was last on:
        var DateTimeType prevOFFState = HotTub_Heater_LastOFFState.state as DateTimeType
        var Number execDuration = Seconds::secondsBetween(new DateTime(prevOFFState.zonedDateTime.toInstant.toEpochMilli), now ).getSeconds()
        var Number heaterLastON_Minutes = execDuration / 60
        //Round Last ON Duration to whole number:
        var Number heaterLastON_Minutes_Rounded = Math::round(heaterLastON_Minutes.floatValue())
        sendPushoverMessage(pushoverBuilder("Heater Last on: "+heaterLastON_Minutes_Rounded+" mins ago\n\nToday: "+Math::round((HotTub_Heater_Last_Runtime_Min.state as DecimalType).floatValue())+" mins.\nTarget Temp: "+ BULLFROG_SET_TEMP.state +"°F\n\nOutside Temp: "+ Temperature.state +"°C").withTitle("Heater Turned ON ("+ BULLFROG_TEMP.state +"°F)").withPriority(0).withApiKey(pushover_api))

    }
end

rule "Hot Tub: Heater OFF. Pausing Duration Recording"
when
	Item BULLFROG_HEATING changed from 1 to 0
then
logInfo("-18-bullfrog.R5", "### Hot Tub: Heater OFF. Pausing Duration Recording ###")
	{
        //Timestamp used to calculate how long ago the heater has been off (Previously heated xxx Minutes ago)
        postUpdate(HotTub_Heater_LastOFFState, new DateTimeType())

        //Calculate how many minutes the heater has been on for:
		var DateTimeType prevOnState = HotTub_Heater_LastOnState.state as DateTimeType
		var Number execDuration = Seconds::secondsBetween(new DateTime(prevOnState.zonedDateTime.toInstant.toEpochMilli), now ).getSeconds()
		logInfo("-18-bullfrog.R5","--> Hot Tub HEATER is now OFF after runing for " + execDuration + " secs.")
		//postUpdate(HotTub_Heater_Last_Runtime_Sec, execDuration)
		var Number dailyTotalSeconds = execDuration + HotTub_Heater_Last_Runtime_Sec.state as Number 
		postUpdate(HotTub_Heater_Last_Runtime_Sec, dailyTotalSeconds)
		var Number dailyTotalMinutes = dailyTotalSeconds / 60
		postUpdate(HotTub_Heater_Last_Runtime_Min, dailyTotalMinutes)

       /* if(execDuration < 60){
            var runtime_units = "Sec"
        } else {
            var runtime_units = "Min"
        }*/
        var Number execDurationInMinutes = execDuration / 60
        var Number minutesNow = Math::round(execDurationInMinutes.floatValue())
        var Number minutesToday = Math::round(dailyTotalMinutes.floatValue())
        sendPushoverMessage(pushoverBuilder("Duration: "+ minutesNow +" mins.\n\nToday: "+minutesToday+" mins.\nOutside Temp: "+ Temperature.state +"°C").withTitle("Heater Turned OFF ("+ BULLFROG_TEMP.state +"°F)").withPriority(0).withApiKey(pushover_api))
    }
end


rule "Hot Tub: Heater Duration Counter Reset"
	when
		Time cron "0 0 0 * * ?"
	then
logInfo("-18-bullfrog.R6", "### Hot Tub: Heater Duration Counter Reset ###")
	{
         sendPushoverMessage(pushoverBuilder("
         Power Used: "+Power_MTU3_TDY.state+" watts\n
         Heating Time: "+HotTub_Heater_Last_Runtime_Min.state+" mins.\n\n
         Temp Low/High: "+ Temp_MinMax_Today.state+"°C").withTitle("Daily Report").withPriority(0).withApiKey(pushover_api))

		postUpdate(HotTub_Heater_Last_Runtime_Sec, 0)
		postUpdate(HotTub_Heater_Last_Runtime_Min, 0)
		logInfo("-18-bullfrog.R5", "--> CRON @ 12:00AM: Sending Pushover Daily Report & Zeroing out Heating Duration for the day.")
    }
end

rule "Readying Hottub For Use"
when
   Item vReady_Hottub_For_Use changed from OFF to ON
then
logInfo("-18-bullfrog.R3", "### Readying Hottub For Use ###")
{
    val StringBuilder BuildMessage = new StringBuilder
    if(gDeck_RGBW_Power.state == OFF && NightState.state == ON) {
       gDeck_RGBW_Power.sendCommand(ON)
       logInfo("-18-bullfrog.R3", "--> Turning on Deck Lighting")
       BuildMessage.append("- Deck Lights\n")
   }
   if(BULLFROG_LIGHTS.state == "0" && NightState.state == ON) {
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/spa.py lights")
       logInfo("-18-bullfrog.R3", "--> Turning on Hottub Lights")
       BuildMessage.append("- Hot Tub Lights\n")
   }
   if(BULLFROG_PUMP1.state == "Off") {
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/spa.py jets")
       logInfo("-18-bullfrog.R3", "--> Turning on Hottub Jets")
       BuildMessage.append("- Hot Tub Jets\n")
   }
   postUpdate(vReady_Hottub_For_Use,OFF)
   sendPushoverMessage(pushoverBuilder("Turning on the following:\n\n"+BuildMessage).withTitle("Readying Hot Tub").withPriority(0).withApiKey(pushover_api))

}
end

Sitemap:

		Text label="Hot Tub [%s °F]" icon="bath" item=BULLFROG_TEMP {				
			Frame label="Python Script"
				{
				Text item=SPARaw_LastChanged icon="calendar" valuecolor=[>6000="red",>600="orange",<=600="green"]
				Switch item=vReady_Hottub_For_Use label="We Be Tubbin'" icon="party" mappings=[ON="Turn on Lights & Jets"]
				Text item=BULLFROG_PUMP1 icon="flow"
				Text item=BULLFROG_PUMP2 icon="flow"
				Text item=BULLFROG_HEATING icon="fire" label="Heating Active" visibility=[BULLFROG_HEATING!="0"] 
				Text item=BULLFROG_TEMP icon="temperature"
				Text item=BULLFROG_SET_TEMP icon="heating"
				Text item=BULLFROG_HEATING_MODE icon="heating"
				Text item=BULLFROG_LIGHTS icon="rgb"
				}
			Frame label="Heating Duration" {
				Text item=HotTub_Heater_Last_Runtime_Min icon="fire" label="Today"
				Text item=HotTub_Heater_Last_Runtime_Sec icon="fire" label="Yesterday"
				Chart item=HotTub_Heater_Last_Runtime_Min refresh=60000 period=24h
			}
			Frame label="Power Use" {
				Text item=Power_Now_MTU3 icon="energy" label="Right Now"
				Text item=Power_MTU3_TDY icon="energy" label="Total Today"
			}
		}

This is a direct copy of all my items so you can get an idea. I’m in the middle of coding the Heater On duration as i want to be able to graph how long the heater is on based on outside temperature.

Attached is a screenshot of my sitemap as well as the Pushover messages (as i use that for notifications/debugging).

Hope this helps you :slight_smile:


My whole deck is RGBW controlled… the next addition to my project will be an ESP8266 / RGB Light sensor to detect the color of the hot tub so i can automatically sync my deck lights to the hot tub light color.

There’s no way to get the Lighting color from the Bulboa controller as its controlled at the circuit board based on on/off sequences so this is the only way i can achieve knowing the color. When i get around to that i’ll post my code for it as well :slight_smile:

deck

1 Like

Absolutely amazing that you got it working,

I started to develop a proper 2.0 binding for the Balboa, but I got stuck with the pure java code to communicate with the network device as I do not have any experience with java.
I do have a working skeleton and all the properties in the paper UI.
I do also get some result from a TcpClient (or so) after connecting. It even seems to be a proper answer from the Balboa device as I get the ~ several times in proper intervals.

Unfortunately, I cannot make any use of your code because of two reasons:
1st: I have no idea how the python code could run and
2nd: I do not have site maps anymore (since the charts flash all the time and cause a complete reload of the pages).

Anyway - I will try to play with it.

Copy the spa.py code to your Openhab directory /etc/openhab2/scripts

and make sure you have python3 installed on your server (apt-get install python3)

Once python3 (it has to be Python3, regular Python doesn’t execute the script correctly and you get errors) is installed the spa.things executes it every 10seconds and will update your items if you copy my rules to your server…

This is what executes/runs the python code:

spa.things:

Thing exec:command:spa [command="/usr/bin/python3 /etc/openhab2/scripts/spa.py status", interval=10, timeout=5]

You don’t need to use the sitemap, its just a visual of the values if anyone wants to see. Once you have the BULLFROG_ vales parsed from the json output you will know the status of all the values it reads:

I’m getting closer!!!
Currently, all the items report: import crc9 ImportError: No module named ‘crc8’.

I guess I need to install a bot more than just python3 (which was installed already)

put this file in your /etc/openhab2/scripts directory:

https://raw.githubusercontent.com/niccokunzmann/crc8/master/crc8.py

Yeaaaah!!!
I’m getting some answers back!
Thank you sooo much.

How good are you with Java?

Awesome! I don’t know Java well…
You can run the script manually as well if you’re ssh’d into your server:

To get Status in JSON output:

python3 spa.py status

Will turn on the 2nd pump (assuming you have two pumps in your tub):

python3 spa.py jets

Toggle Lights:

python3 spa.py lights

You can see in my rule “Ready Hot Tub for Use” that you can execute these commands via:

executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/spa.py jets")

@ThomasBrodkorb if you come up with any good rules for your hot tub let me know :slight_smile:

I hope i never get this one, saying the hot tub temp is 6 degrees below my set temp (possible heater failure, not good in the winter!)

rule "WARNING: Hot Tub Temperature is below set point. Potential Freezing Issue"
	when
		Item BULLFROG_TEMP changed
	then
logInfo("-18-bullfrog.R7", "### WARNING: Hot Tub Temperature is below set point. Potential Freezing Issue ###")
	{
        val TempWarning_Threshold = ((BULLFROG_SET_TEMP.state as DecimalType).intValue - 6)
        logInfo("-18-bullfrog.R7", "<-- Checking Temperature. Current Temp: "+BULLFROG_TEMP.state+"°F // Warning Threshold: "+TempWarning_Threshold+"°F")
        if(BULLFROG_TEMP.state <= TempWarning_Threshold ) {
            logInfo("-18-bullfrog.R7", "<-- WARNING: Hot Tub is set to "+BULLFROG_SET_TEMP.state+"°F but is currently "+BULLFROG_TEMP.state+"°F. Sending Alerts as its below the warning threshold of "+TempWarning_Threshold+"°F")
            sendPushoverMessage(pushoverBuilder("The hot tub is currently set to "+BULLFROG_SET_TEMP.state+"°F\nOperating 6°F Below Threshold.\n\nIts currently "+Temperature.state+"°C Outside").withTitle("TEMPERATURE WARNING").withPriority(2).withApiKey(pushover_api))
        }
    }
end

Oh hey, good work everyone! I’m the one that originally started decoding the Balboa protocol, and here I finally am actually running OpenHAB now. My Spa is still disconnected after a move, but come this summer (or fall, or winter… you know how things are) I may attempt a native OpenHAB binding.

Good to hear you are back.

I would love to see a native binding. I played a lot with your code and now able to use my PV power to heat the tub when sun is shining over the day and reduce the temperature over night to save the costs for buying electricity.

First I charge my car, then the tub if still enough

left or car is full.

On/Off/Auto is also possible for the tub.

Thanks so much for this python code etc, got ‘jets’ and ‘status’ working just fine now.

Has anyone worked out how to set ready or rest?
I have a holiday home and often people press the temp and light and somehow get to the rest mode, the next day they try have a spa and its cold because its in rest mode.

I’d like to run a cron job to every morning at 2am, send something like…
python3 spa.py modeready
python3 spa.py temp39

By doing the above, I need it to ensure the spa mode is in ready and them temp is 39 degrees Celsius.

I can edit the python for the new commands, but not sure what message to send, i.e. 0x11 for lights and 0x05 for jets

Any help would be greatly appreciated.

Many thnaks
Barry

0x50 and 0x51 are the ones you are looking for.

    def set_heatingmode(self, value):
    if self.heating_mode == value:
        return
    if value == "Rest" and self.heating_mode == "Ready":
        self.send_toggle_message(0x51)
        self.send_toggle_message(0x51)
    elif value == "Ready" and self.heating_mode == "Rest":
        self.send_toggle_message(0x51)
        self.send_toggle_message(0x51)
    else:
        self.send_toggle_message(0x51)
    self.heating_mode = value

It works for me.

I can also (if required) post the complete script.

If you wouldn’t mind sharing the whole script, im not using it in OpenHab only in CLI.

Is there only a toggle option or is there a way to just send a command to make it ready?
I guess if there is only toggle, I need to check the status and find out what its on then toggle if not on Ready.