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
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.
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!
Just a few suggestions:
- 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.
- In the App I can set the Filter Regeneration Cycles, would be great to see/set them via the binding also.
- Is it possible to read/set the time of the Spa? would be great to have in the binding
- 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 thetarget-temperature
dependend on thetemperature-range
bit? would make sense, thoughâŠ
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:
- 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.
- 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
Thanks Carl! Your proposed stuff for 1. sound spectacular!
Thanks a bunch!