Balboa Binding

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.0
        self.hour = 12
        self.minute = 0
        self.heating_mode = ""
        self.temp_scale = ""
        self.temp_range = ""
        self.pump1 = ""
        self.pump2 = ""
        self.pump3 = ""
        self.blower = ""
        self.set_temp = 0.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(('10.2.0.160', 4257))
            SpaClient.s.setblocking(0)
        return SpaClient.s

    def handle_status_update(self, byte_array):
        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 'Celsius'

        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 >> 2 & 0x03]
        self.pump3 = ('Off', 'Low', 'High')[pump_status >> 4 & 0x03]
        self.circ_pump = byte_array[13] & 0x02 != 0
        self.blower = ('Off', 'On')[byte_array[13] >> 2 & 0x01]
        self.light = '1' if (byte_array[14] == 3) else byte_array[14]
        #self.light = byte_array[14] & 0x03
        if byte_array[2] == 255:
            self.current_temp = 0.0 
            self.set_temp = 1.0 * byte_array[20]
        elif self.temp_scale == 'Celsius':
            self.current_temp = byte_array[2] / 2.0
            self.set_temp = byte_array[20] / 2.0
        else:
            self.current_temp = 1.0 * byte_array[2]
            self.set_temp = 1.0 * 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": "%s",\n"SET_TEMP": "%s",\n"TIME": "%d:%02d",\n' % \
            (format(self.current_temp, '.1f'), format(self.set_temp, '.1f'), 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"PUMP3": "%s",\n"BLOWER": "%s",\n"CIRC_PUMP": "%s",\n"LIGHTS": "%s"\n' % \
            (self.heating, self.temp_range, self.pump1, self.pump2, self.pump3, self.blower, 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
        # 0x06 - pump 3
        # 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):
        time.sleep(1)                    
        self.read_all_msg() # Read status first to get current temperature unit
        dec = float(temp) * 2.0 if (self.temp_scale == "Celsius") else float(temp)
        self.set_temp = int(dec)
        self.send_message(b'\x0a\xbf\x20', bytes([int(self.set_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

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

    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

import time
c = SpaClient(SpaClient.get_socket())

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(0x11) #lights
if str(sys.argv[1]) == "pump1":
    c.send_toggle_message(0x04) #pump1
if str(sys.argv[1]) == "pump2":
    c.send_toggle_message(0x05) #pump2
if str(sys.argv[1]) == "pump3":
    c.send_toggle_message(0x06) #pump3
if str(sys.argv[1]) == "blower":
    c.send_toggle_message(0x03) #pump2
if str(sys.argv[1]) == "settemp":
    c.set_temperature(sys.argv[2])
if str(sys.argv[1]) == "heatingmode":
    c.send_toggle_message(0x51)
if str(sys.argv[1]) == "temprange":
    c.send_toggle_message(0x50)

And just to give the complete content, here is my OH rule for the tub:

var lastAlert = now.minusMinutes(5) // set to five minutes ago so the Rule can run when OH first starts

var Number LoadingFor = 0 
var Number NotLoadingFor = 0 
var Number SwitchOffTries = 0 
var Number SwitchOnTries = 0 

rule "Init Balboa"
when 
		System started
then
  postUpdate(BALBOA_OPERATIONMODE, "AUTO")
end

rule "HeatUpManually"
when 
  Item BALBOA_MANUAL changed to ON or
  Item BALBOA_OPERATIONMODE received command
then
  logInfo("-18-balboa.R3", "Manual Heating On Received: " + BALBOA_OPERATIONMODE.state)
  if ((BALBOA_OPERATIONMODE.state == "AUTO") || (BALBOA_OPERATIONMODE.state == "ON"))
  {
    logInfo("-18-balboa.R3", "Manual Heating On Received")
    if (BALBOA_TEMP_RANGE.state == "Low")
    {
        executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py temprange")
    }
    if (BALBOA_SET_TEMP.state < 38.5)
    {
      executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 38.5")
    }
  }
end

rule "SwitchOffTubOverNight"
when 
	//Every weekday at 14:00 hours
	Time cron "0 0 22 ? * * *" or 
  Item BALBOA_OPERATIONMODE changed
then
  if((BALBOA_OPERATIONMODE.state == 'AUTO') || (BALBOA_OPERATIONMODE.state == 'OFF'))
  {
    if (BALBOA_MANUAL.state == ON)
    {
      BALBOA_MANUAL.postUpdate(OFF)
    }
  }
  if((BALBOA_OPERATIONMODE.state == 'AUTO') || (BALBOA_OPERATIONMODE.state == 'OFF')) {
    executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 30");
  } 
  else
  {
    executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 38");
  }
end

rule "HeatUpWhenEnoughPower"
when 
	//Every weekday at 14:00 hours
	Time cron "*/30 * 09-15 ? * * *"
	//Item KebaPower changed or
  //Item PV_Meter changed   
then
  if ((BALBOA_OPERATIONMODE.state == 'AUTO') || (BALBOA_OPERATIONMODE.state == 'ON'))
  {
    var Number Available = -(PV_Meter.state as DecimalType)
    logInfo("-18-balboa.R3", "Available: " + Available.toString() )
    if (((Available > 2000) && (KebaPlugLocked.state == OFF)) || (Available > 3000))
    {
			SwitchOnTries += 1;
			SwitchOffTries = 0;
      logInfo("-18-balboa.R3", "SwitchOn: " + SwitchOnTries);
    }
    if (((Available < -1000) && (KebaPlugLocked.state == OFF)) || (Available < -100))
    {
      if (BALBOA_MANUAL.state == ON) // No need to care if ON anyway
      {
        executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 38");
        logInfo("-18-balboa.R3", "SetTemp: 38")
      }
      else
      {
        SwitchOffTries += 1;
        SwitchOnTries = 0;
      logInfo("-18-balboa.R3", "SwitchOff: " + SwitchOffTries);
      }
    } 
    if (SwitchOnTries >= 5) {
      if (BALBOA_SET_TEMP.state < 40)
      {
        executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 40")
        logInfo("-18-balboa.R3", "HeatUp: 40")
      }
    }
    if (SwitchOffTries >= 5) {
      if (BALBOA_MANUAL.state == ON) // No need to care if ON anyway
      {
        if  (BALBOA_SET_TEMP.state > 38)
{
        executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 38");
        logInfo("-18-balboa.R3", "SetTemp: 38")
}
      }
      else
      {
        if  (BALBOA_SET_TEMP.state > 30) {
        executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 30");
        logInfo("-18-balboa.R3", "SetTemp: 30")
        }
      }
    }
  }
	// update item to switch on light here
end

rule "NormalizeTemperature"
when 
	//Every weekday at 14:00 hours
	Time cron "0 10 15-17 ? * * *"
	//Item KebaPower changed or
  //Item PV_Meter changed   
then
  if ((BALBOA_OPERATIONMODE.state == 'AUTO') || (BALBOA_OPERATIONMODE.state == 'ON'))
  {
    if (BALBOA_SET_TEMP.state > 38)
    {
      executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 37")
      logInfo("-18-balboa.R3", "HeatUp: 38")
    }
  }
	// update item to switch on light here
end

rule "Hot Tub: Parsing (SPARaw) JSON output to individual Items"
when
   Item SPARaw changed
then
logInfo("-18-balboa.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('BALBOA_','$.')
      var String type = value.type
      if (type == "Switch")
      {
        var String newValue = transform("JSONPATH", name, json).replaceAll('"','')
        if (newValue != "Off")
        {
          value.postUpdate(ON)
        }
        else
        {
          value.postUpdate(OFF)
          //value.postUpdate(transform("JSONPATH", name, json).replaceAll('"',''))
        }
      } 
      else
      {
        value.postUpdate(transform("JSONPATH", name, json).replaceAll('"',''))
      }
      //logInfo("-18-balboa.R3", " " )

   ]
   SPARaw_LastChanged.postUpdate( new DateTimeType() )
}
end

rule ToggleLights
when
  Item BALBOA_LIGHTS received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py lights")
end 

rule TogglePump1
when
  Item BALBOA_PUMP1 received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump1")
end 

rule TogglePump2
when
  Item BALBOA_PUMP2 received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump2")
end 

rule TogglePump3
when
  Item BALBOA_PUMP3 received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump3")
end 

rule SetTargetTemp
when
  Item BALBOA_SET_TEMP received command
then
    var String commandline = "/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp "  + (BALBOA_SET_TEMP.state)
    executeCommandLine(commandline)
    logInfo("-18-balboa.R3", "Set target temp: " + commandline)
end

rule ToggleTempRange
when
  Item BALBOA_TEMP_RANGE received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py temprange")
end 

rule ToggleHeatingMode
when
  Item BALBOA_HEATING_MODE received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py heatingmode")
end 

I am using some of my PV output to load the tub over day (and prevent heating over night).

Thats awesome, thanks so much


When changing the heating mode, the status updates with either Ready or Rest, and I have now just spotted the temp range high or low, which is perfect.

{
"TEMP": "38.5",
"SET_TEMP": "39.0",
"TIME": "19:07",
"PRIMING": "False",
"HEATING_MODE": "Ready",
"TEMP_SCALE": "Celsius",
"TIME_SCALE": "24 Hr",
"HEATING": "0",
"TEMP_RANGE": "High",
"PUMP1": "Low",
"PUMP2": "Off",
"PUMP3": "Off",
"BLOWER": "Off",
"CIRC_PUMP": "False",
"LIGHTS": "0"
}

Any ideas on forcing temp range high and forcing heating mode to ready rather than toggling?

Cheers

UPDATE: Figured it out using webcore with smart things, just wrote something to poll the status every 5 mins and then store as a variable and act on it at specific times of the day
 thanks so much for your help

Hello,

In the rule HeatUpManually, you should see the test for BALBOA_TEMP_RANGE.state
== “Low” and then call the toggle command

if (BALBOA_TEMP_RANGE.state ==
“Low”)

{

    executeCommandLine( "/usr/bin/python3

/etc/openhab2/scripts/balboa.py temprange")

The rule
“Hot Tub: Parsing (SPARaw) JSON output to individual Items”

Sets all the items to useful values, based on the raw information received from the tub.

This also sets the TempRange and HeatingMode to the current state as received from the tub.

First, thank you all so much for all the work that went into this script, I hope my small contributions help in some way to make this script the best it can. I’ve made some modifications to the script to make it work as I would like. I’m very new to Python, and programming in general.
I have a new Hot tub with Balboa Controller and Wifi. I am able to control nearly every function in OpenHab’s HabPanel thanks to this forum post and it’s contributors. Here are my updates to the Python Script, Rules, and Items. Things have not changed if your python script is called balboa.py.

How to use this version of balboa.py from CLI and Openhab HABPanel:

/usr/bin/python3 /etc/openhab2/scripts/balboa.py status //Prints status of defined variables from hottub. Unused in HABPanel

/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp 000 //Changes the Target Temperature of the hottub. Uses Item Balboa_Set_Temp in HABPanel on slider defined 90°-104°F

/usr/bin/python3 /etc/openhab2/scripts/balboa.py heatingmode //Toggles the heating mode from ‘Ready’ to ‘Rest’, and back. (My hottub only has the 2 options.) Uses Item Balboa_Heating_Mode in HABPanel as button with empty Command value/Action type. Check “Show the item’s state value” in the ‘Display’ Tab

/usr/bin/python3 /etc/openhab2/scripts/balboa.py temprange //Toggles the heating range from 50°-80°F (Low) or 80°-104°F (High). Uses Item Balboa_Temp_Range in HABPanel as button with empty Command value/Action type. Check “Show the item’s state value” in the ‘Display’ Tab

/usr/bin/python3 /etc/openhab2/scripts/balboa.py lights //Toggles the lights on and off. Uses Item Balboa_Lights in HABPanel as button with empty Command value/Action type. Check “Show the item’s state value” in the ‘Display’ Tab

/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump1 (Off,Low,High) //Sets pump1 to the value of Off, Low or High. A null value will just toggle to the next speed in the series. Be aware that the hottub operating system will start pump1 automatically for heating and filtering, and anytime pump2 is activated. This may override your selection. Uses Item Balboa_Pump1 in HABPanel as a selection with a Comma-separated list of Off,Low,High.

/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump2 (Off,Low,High) //Same as pump1
except pump2!!

/usr/bin/python3 /etc/openhab2/scripts/balboa.py settime (hh mm) //Set the time on the hottub controller. Must be sent as 24hr clock in the format hh mm. I don’t yet have a HABPanel widget for this one.

I also use some Dummy HABPanel widgets for Items Balboa_Temp (Current Temperature) and Balboa_Heating (Heating On/Off)

balboa.py (DON’T FORGET TO CHANGE THE IP ADDRESS from 192.168.2.88 to your hottub’s IP address.!!)

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

    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.88', 4257))
            SpaClient.s.setblocking(0)
        return SpaClient.s

    def handle_status_update(self, byte_array):
        self.priming = byte_array[1] & 0x01 == 1
        self.hour = byte_array[3]
        self.minute = byte_array[4]
        flag2 = byte_array[5]
        self.heating_mode = 'Ready' if (flag2 & 0x03 == 0) else 'Rest'
        flag3 = byte_array[9]
        self.temp_scale = 'Farenheit' if (flag3 & 0x01 == 0) else 'Celsius'
        self.time_scale = '12 Hr' if (flag3 & 0x02 == 0) else '24 Hr'
        flag4 = byte_array[10]
        self.heating = 'Off' if (flag4 & 0x30 == 0) else 'On'
        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 >> 2 & 0x03]
        self.light = 'On' if (byte_array[14] == 3) else 'Off'
        if byte_array[2] == 255:
            self.current_temp = 0.0 
            self.set_temp = 1.0 * byte_array[20]
        elif self.temp_scale == 'Celsius':
            self.current_temp = byte_array[2] / 2.0
            self.set_temp = byte_array[20] / 2.0
        else:
            self.current_temp = 1.0 * byte_array[2]
            self.set_temp = 1.0 * 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": "%s",\n"SET_TEMP": "%s",\n"TIME": "%d:%02d",\n' % \
            (format(self.current_temp, '.1f'), format(self.set_temp, '.1f'), 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"LIGHTS": "%s"\n' % \
            (self.heating, self.temp_range, self.pump1, self.pump2, 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]
        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
        # 0x06 - pump 3
        # 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):
        time.sleep(1)                    
        self.read_all_msg() # Read status first to get current temperature unit
        dec = float(temp) * 2.0 if (self.temp_scale == "Celsius") else float(temp)
        self.set_temp = int(dec)
        #print (b'\x0a\xbf\x20', bytes([int(self.set_temp)]))
        self.send_message(b'\x0a\xbf\x20', bytes([int(self.set_temp)]))

    def set_new_time(self, new_hour, new_minute):
        time.sleep(1)                    
        self.new_time = bytes([int(new_hour)]) + bytes([int(new_minute)])
        #print (self.new_time)
        self.send_message(b'\x0a\xbf\x21', (self.new_time))

    def set_pump1(self, value):
        time.sleep(1)                    
        self.read_all_msg() # Read status first to get current pump1 state
        if self.pump1 == value:
            return
        if value == "High" and self.pump1 == "Off":
            self.send_toggle_message(0x04)
            time.sleep(2)
            self.send_toggle_message(0x04)
        elif value == "Off" and self.pump1 == "Low":
            self.send_toggle_message(0x04)
            time.sleep(2)
            self.send_toggle_message(0x04)
        elif value == "Low" and self.pump1 == "High":
            self.send_toggle_message(0x04)
            time.sleep(2)
            self.send_toggle_message(0x04)
        else:
            self.send_toggle_message(0x04)
        self.pump1 = value

    def set_pump2(self, value):
        time.sleep(1)                    
        self.read_all_msg() # Read status first to get current pump2 state
        if self.pump2 == value:
            return
        if value == "High" and self.pump2 == "Off":
            self.send_toggle_message(0x05)
            time.sleep(2)
            self.send_toggle_message(0x05)
        elif value == "Off" and self.pump2 == "Low":
            self.send_toggle_message(0x05)
            time.sleep(2)
            self.send_toggle_message(0x05)
        elif value == "Low" and self.pump2 == "High":
            self.send_toggle_message(0x05)
            time.sleep(2)
            self.send_toggle_message(0x05)
        else:
            self.send_toggle_message(0x05)
        self.pump2 = value
    
import time
c = SpaClient(SpaClient.get_socket())

if str(sys.argv[1]) == "status": #status
    time.sleep(1)                    
    c.read_all_msg()
    print(c.string_status())
if str(sys.argv[1]) == "lights": #lights toggle
    c.send_toggle_message(0x11) 
if str(sys.argv[1]) == "pump1": #pump1 off,low,high
    time.sleep(1)
    c.set_pump1(sys.argv[2])
if str(sys.argv[1]) == "pump2": #pump2 off,low,high
    time.sleep(1)
    c.set_pump2(sys.argv[2])
if str(sys.argv[1]) == "settemp": #temperature in degrees (C or F)
    c.set_temperature(sys.argv[2])
if str(sys.argv[1]) == "settime": #hh mm
    new_hour = (sys.argv[2])
    new_minute = (sys.argv[3])
    c.set_new_time(new_hour, new_minute)
if str(sys.argv[1]) == "heatingmode": #Heat mode toggle for rest or ready
    c.send_toggle_message(0x51)
if str(sys.argv[1]) == "temprange": #temperature range toggle for high or low
    c.send_toggle_message(0x50)

balboa.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 BALBOA_TEMP "Current Temperature   [%s °F]" (gHotTub,gHotTub_Parse_json)
String BALBOA_SET_TEMP "Target Temperature   [%s °F]" (gHotTub,gHotTub_Parse_json)
//String BALBOA_SET_TIME "Target Time    [%s]" (gHotTub, gHotTub_Parse_json) //Work in progress
String BALBOA_PRIMING "Priming    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_HEATING_MODE "Heating Mode    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_TEMP_SCALE "Temp Scale    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_TIME_SCALE "Time Scale    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_HEATING "Heating    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_TEMP_RANGE "Temp Range    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_PUMP1 "Pump 1    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_PUMP2 "Pump 2    [%s]" (gHotTub,gHotTub_Parse_json)
String BALBOA_LIGHTS "Lighting    [%s]"           (gHotTub,gHotTub_Parse_json)
DateTime SPARaw_LastChanged "Last Changed [%1$tm/%1$td %1$tH:%1$tM]" (gHotTub)

balboa.rules

rule "Hot Tub: Parsing (SPARaw) JSON output to individual Items"
when
   Item SPARaw changed
then
logInfo("-18-balboa.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('BALBOA_','$.')
      var String type = value.type
      if (type == "Switch")
      {
        var String newValue = transform("JSONPATH", name, json).replaceAll('"','')
        if (newValue != "Off")
        {
          value.postUpdate(ON)
        }
        else
        {
          value.postUpdate(OFF)
          //value.postUpdate(transform("JSONPATH", name, json).replaceAll('"',''))
        }
      } 
      else
      {
        value.postUpdate(transform("JSONPATH", name, json).replaceAll('"',''))
      }
      //logInfo("-18-balboa.R3", " " )

   ]
   SPARaw_LastChanged.postUpdate( new DateTimeType() )
}
end

rule ToggleLights
when
  Item BALBOA_LIGHTS received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py lights")
end 

rule SetPump1Speed
when
  Item BALBOA_PUMP1 received command
then
    var String commandline = "/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump1 "  + (BALBOA_PUMP1.state)
    executeCommandLine(commandline)
    logInfo("-18-balboa.R3", "Set Pump1 speed: " + commandline)
end 

rule SetPump2Speed
when
  Item BALBOA_PUMP2 received command
then
    var String commandline = "/usr/bin/python3 /etc/openhab2/scripts/balboa.py pump2 "  + (BALBOA_PUMP2.state)
    executeCommandLine(commandline)
    logInfo("-18-balboa.R3", "Set Pump2 speed: " + commandline)
end 

rule SetTargetTemp
when
  Item BALBOA_SET_TEMP received command
then
    var String commandline = "/usr/bin/python3 /etc/openhab2/scripts/balboa.py settemp "  + (BALBOA_SET_TEMP.state)
    executeCommandLine(commandline)
    logInfo("-18-balboa.R3", "Set target temp: " + commandline)
end

rule ToggleTempRange
when
  Item BALBOA_TEMP_RANGE received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py temprange")
end 

rule ToggleHeatingMode
when
  Item BALBOA_HEATING_MODE received command
then
       executeCommandLine("/usr/bin/python3 /etc/openhab2/scripts/balboa.py heatingmode")
end
 
rule SetCorrectTime
when
  Item BALBOA_SET_TIME received command
then
    var String commandline = "/usr/bin/python3 /etc/openhab2/scripts/balboa.py settime "  + (BALBOA_SET_HOUR.state) + (BALBOA_SET_MINUTE.state)
    executeCommandLine(commandline)
    logInfo("-18-balboa.R3", "Set new time: " + commandline)
end

balboa.things

Thing exec:command:spa [command=“/usr/bin/python3 /etc/openhab2/scripts/balboa.py status”, interval=30, timeout=5]

HABPanel- The “Power” widget is a Wemo switch connected to a contactor. Works well to cut power to the whole system.

Super awesome contribution guys! I forgot about this thread till i was googling Balboa to see if there’s been any advancement on API/Integration into Openhab/Smartthings.

CRPerryJr – i just tried your code and now my pumps work correctly, i can control both my pumps at different speeds which i wasn’t able to do before :smiley:

Also – One thing i would suggest as i’ve done this myself. I firewalled off my Balboa controller IP from reaching the internet. While this breaks the phone app to remotely control the unit, it does prevent them from pushing down a firmware update which can potentially break the script we have working locally.

Just a thought.

1 Like

works well.
the only thing I miss is to control the circulator pump and the blower.

thank you very much for the great script.

Well, it looks like this script is broken now. I just lost access to my hot tub when I was updating my WIFI, and didn’t block the WIFI module before the update came down. Balboa now uses an authenticated cloud API. I now have cloud access through Balboa, but my openhab no longer talks to it. I see there are some efforts to reverse engineer the new API, and some success with Smarthings. https://community.smartthings.com/t/balboa-hot-tub-wifi-module-integration/56640/110

I gave up on using my WiFi module because it was flaky even with a perfect Wifi signal, and never seemed to let me connect (I think I had another device my network using the same discovery protocol, and it confused it). I switched to a direct connection with RS-485 (and a NUC; I actually have a communication wire run from my network closet out to my tub; I know others have mounted a Raspberry Pi in their tub). Anyhow, I finally got around to updating my gem to be a full integration with OpenHAB. I think I’m over writing native bindings because the Java is just such a pain. But the gem now supports full bi-directional communication via MQTT. Docs are updated at https://github.com/ccutrer/balboa_worldwide_app.

Hey guys,

I’m happy to join this discussion with a project I’ve just finished: an esp8266 based mqtt Spa controller.
Repo is here https://github.com/cribskip/esp8266_spa

I’m already using this and its working very well. Please let me know what you think.

Best,
Sascha

Hi,

I have started developing a native binding here https://github.com/carlonnheim/openhab-addons/tree/balboa.

I have the basics implemented, much thanks to the above linked information on protocol details, and can control pumps, lights and blowers so far. Still some way to go
 any help is welcome!

Hi again,

I have taken the native balboa binding to a functioning state and packaged a beta release. Hoping to get some feedback from other users on how it works here.

Regards
//Carl

Hi,

thanks Carl for your work on this!
In order to increase the user base, I’ll try to add balboa wifi adapter compatibility to my esp code and then try your binding.

Best,
Sascha

This is what i’m getting with no updates to my Items:

2020-10-13 21:39:02.686 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152700006665000400000000000000000067010002437E

2020-10-13 21:39:02.686 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:39:13.264 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1A0ABF2464DC24004246425032302020022067A5B3010A0400DE7E

2020-10-13 21:39:13.264 [TRACE] [inding.balboa.internal.BalboaMessage] - Information Response received

2020-10-13 21:39:13.485 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152700006665000400000000000000000067010002437E

2020-10-13 21:39:13.485 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:39:34.181 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152700006565000400000000000000000067010002517E

2020-10-13 21:39:34.181 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:39:34.488 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152700006665000400000000000000000067010002437E

2020-10-13 21:39:34.488 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:39:51.875 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152700006565000400000000000000000067010002517E

2020-10-13 21:39:51.875 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:40:02.671 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152800006565000400000000000000000067010002F27E

2020-10-13 21:40:02.671 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:40:13.228 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1A0ABF2464DC24004246425032302020022067A5B3010A0400DE7E

2020-10-13 21:40:13.228 [TRACE] [inding.balboa.internal.BalboaMessage] - Information Response received

2020-10-13 21:40:13.467 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152800006565000400000000000000000067010002F27E

2020-10-13 21:40:13.467 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:40:18.270 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152800006564000400000000000000000067010002F07E

2020-10-13 21:40:18.270 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:40:21.268 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152800006565000400000000000000000067010002F27E

2020-10-13 21:40:21.268 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

2020-10-13 21:40:24.562 [DEBUG] [nding.balboa.internal.BalboaProtocol] - Processing message: 7E1DFFAF13140067152800006564000400000000000000000067010002F07E

2020-10-13 21:40:24.562 [DEBUG] [inding.balboa.internal.BalboaMessage] - Buffer length 31 is not appropriate for a org.openhab.binding.balboa.internal.BalboaMessage$StatusUpdateMessage, expected 34

You seem to have an unsupported version. The length seems to differ between versions https://github.com/ccutrer/balboa_worldwide_app/wiki#status-update

Maybe @carlonnheim can help out.

I for myself gave up on creating a compatible sketch as most users with openhab tend to go the DIY route and then mqtt is not the wrong way

Hi,

Great to see interest in testing this!

Yes, I need to handle earlier versions of messages along with a lot of other things in that wiki page. I was working off this before, the wiki is much more comprehensive.

My wifi module has unfortunately stopped working, so will not make good progress until I have replaced that. Handling the shorter message lengths is trivial though - I have fixed that and published beta 2. Curious to hear how it works for you @Python!

Regards
//Carl

Finally I got the WLAN Module and installed it in my Balboa Spa.
So, basically I just drop the beta2.jar in /srv/openhab2-addons/, right? After I did this, unfortunately the Binding is not imported. Do I miss something?

Edit:
sorry, works. I didn’t find it in PaperUI in “AddOns”, but thing-configuration worked, and now I’m playing around a bit.

Hi, played around quite a bit and it works! Thanks for that! :+1:
Just a few suggestions:

  1. Would be nice to have a “disconnect” switch or something as there “can only be one” connected to the WLAN Module. So if you want to connect via the App, you have first to stop the binding in the console.
  2. In the App I can set the Filter Regeneration Cycles, would be great to see/set them via the binding also.
  3. Is it possible to read/set the time of the Spa? would be great to have in the binding
  4. Perhaps I didn’t get it, but I understood, you could set a “upper” and “lower” target temperature in the display unit and in the App - the binding only offers one target-temperature, would be nice to have both options (or is the target-temperature dependend on the temperature-range bit? would make sense, though
 :wink:

other than that: great work! Thanks a lot!

Hi,

Great to hear it works and thanks for the suggestions! I will have another pass at it during the holidays I think. Some notes:

  1. Yes, I am thinking either that or a proxy service allowing the app to connect simultaneously through the binding (once connected, the binding “discovers” as the tub itself and tunnels messages through). I also have the idea to build a RS485 sw which pretends to be the tub and connect a display unit to it. This way, I could mount a remote control unit inside the house for example. Lots of work though, and the control units are quite expensive


2-3. Yes, just a matter of implementing those message types.

  1. The controller only exposes the selected temperature range as far as I know. I.e. you cannot interact with the low setpoint while it is set to high and vice versa. You can probably achieve what you want using rules though (have an item tracking each and push the setpoint to the tub upon range change)?

Regards
//Carl

1 Like

Thanks Carl! Your proposed stuff for 1. sound spectacular! :wink:
Thanks a bunch!