Balboa Spa Controller App

I've been getting a "Severe Hub CPU Load" message and when I check the app stats it shows that BWA SPA app is running at 50% of my overall CPU load. Is everyone else experiencing this too?

I have installed the App and drivers successfully. I will post to github if I see any issues crop up. But so far it is looking good!

Hello, I am new to Hubitat and am looking to purchase one for controlling my hottub. (I already have the wifi module and the BWA app) . I am just looking to question that if I purchase an elevation C-7 and implement this app and driver, will I be able to make a schedule / timer for the temperature of the hottub? Ie every Friday at 5pm increase hottub to 99f?

1 Like

Following to see if the scheduling is possible. I would expect it to be, but I wasn't aware of this driver until about 10 minutes ago.

@rob2337 and @3rdStng

This app does work for the most part. It appears to be abandoned code at this point, and I wish someone smarter than I am who can code would take it over.

The driver does allow setting a thermostat and pump as well as light using Hubitat. Scheduling is fairly straightforward, you can use "Set Thermostats" within Rule Machine to do this. The response time is amazing, you can set temperature on the dashboard and see the BWA (Spa Control) app respond instantly.

Where it gets a bit convoluted, and I sure wish I could code, is the dashboard in some cases. The pump on/off from a dashboard is not quite right, at least for my 2 speed pump (see my post above). And you don't have control at all over things like ready/rest or high/low temperature range, and filter cycles like you do from the BWA (Spa Control) app. You can see many of these attributes, but not set them from what I have been able to find.

The other thing with dashboard (which I did a workaround with) is that it is nice to have a dashboard button for my favorite temps. So I can push 103 and it just goes there without having to tap up like 20 times on the thermostat tile. I had to write a rule and use a virtual button to create these favorites. I wish the code handled this more directly or cleanly. But that is just a wish, and not completely necessary for everyday use. Using this method, I can say "Alexa hot tub 103" and it sets the temperature. If you would like me to show an example of how this works, I would be glad to post screenshots.

Be sure to go into all the apps and drivers code, and change the log level in the first few lines to either 4 or better yet a 5 to reduce logging. I think they are set to 3 by default, which logs a LOT.

Also, please note that I have the newer BWA spa Wifi interface, the older ones seem to be a bit more buggy apparently. The model listed in my order was BWA WI-FI MODULE Worldwide App 2019 Model" I think the part number is 50350? So if you have a different version, I have not tested how that works.

Glad to answer any more questions about this, or show examples of rules...

Hello, I purchased a Hubitat and installed this code. I am amazed on how well it works. I can set a schedule to control my hottub temperature and also have a notification if the temperature drops below a certain set temperature. Thank you to this thread!

Happy to say I just installed this too and it works great - even with Alexa (which is what I was hoping for, otherwise I would just use the native BWA app) I've tried the Balboa integration on 2 other platforms and could never get it to work beyond just reporting the status.

The only thing that isn't working from Alexa for me is the set temperature. I dont change it much anyway so this isnt really an inconvenience - I supposed if it mattered, I could try the rule/virtual button approach suggested.

Ok. so still a newbie. So I was able to install drivers (1 Parent and 2 children) followed by app. The app tries to configure itself, logs in successfully to the Balboa cloud, finds the right SPA, but it seems to be in an infinite loop. When I reach "Done" button at the end, it starts through the configuration cycle again....any thoughts?

I just installed this and noticed there's no 2 speed pump control. I cloned the repo and started to tinker with the code to figure out how to set the pump speeds. After thinking about it, I don't see a reason why you would want to set the pumps to high using automation. In fact just about the only thing I would want automated is the circ pump, and the lights. The circ pump already runs on a programmable schedule inside the tub controller, and ready / rest mode, well, you can simply set the heating set point to a very low point and leave the tub in ready mode and be done with it.

Literally, I don't have a compelling reason to add any of this functionality at this point. But maybe someone out there does have a compelling use care for it?

1 Like

after reading through this thread some more, I understand the problem and the solution. There's no way to directly set the pump to high or low directly, it's about sending button presses, and if you send button presses you either write this code with a state machine pattern, or you keep track of the state somehow, i.e. how many clicks you've sent, and what state the pump is in, as is done here:

I believe Richard's older code had already implemented this solution:

https://github.com/Lcstyle/HBBWASpaManager/blob/master/old/HBBWASpaManager_Device.groovy#L366

https://github.com/garbled1/pybalboa/blob/5f2a5dfc450ba520f55c7486c860b8ecb9e63137/pybalboa/balboa.py#L306

I still don't see a valid use case for this level of control for the jet pumps.

Best use case I can think of would be a broken interface/button on the controller, where otherwise functionality is preserved.

1 Like

Hi There, I did find a use case for a different feature in this integration that wasn't available. So I went ahead and implemented it. In the spirit of giving back here it is:

  1. I keep my tub at 101, but I have an alexa routine called "get tub ready", which flips a virtual switch
  2. that virtual switch is auto off after 5 seconds, but the on is a trigger for a rule machine rule that bumps the temp to 103 for an hour. Plenty of time for it to get real nice and toasty in the winter and accomodate any temperature drops in freezing cold conditions.
  3. Recently, I've been sick, and haven't used the tub in over a week, and probably won't for another week. The last thing on my mind was managing the tub, so it kind of just sat in Ready mode at 101 for about 2 weeks.
  4. I would have really liked to have had the tub set to Rest mode for that time (for energy saving reasons), but who can remember small details like this when life happens.

So that's what I implemented, Rest/Ready mode control. In addition, I added the actuator capability so that I could control ready mode via a rule machine rule.

This is quick and dirty, but to gain the same functionality, modify the HB BPA SPA Parent driver as follows:

  1. Insert the following code under the metadata function under // Additional attributes:
capability "Actuator"
attribute "ReadyMode", "enum", ["Ready", "Rest"]
command "setReadyMode"
  1. add the following code anywhere. For non-experts, here we're declaring a new function call, so make sure you add this outside of any existing function declarations, note the "{" and "}" function delimiter braces.
void setReadyMode() {
    log.debug "In parent setReadyMode..."
    ReadyMode = device.currentValue("ReadyMode")
    log.debug "Device current mode is ${ReadyMode}"
    text_mode = ReadyMode == "Ready" ? "Rest" : "Ready"
    sendEvent(name: "ReadyMode", value: text_mode)
    log.debug "Setting mode to ${text_mode}"
    sendCommand("Button", BUTTON_MAP.HeatMode)
    }
  1. at the end of the parsePanelData function:
right under the existing call:
    sendEvent(name: "spaStatus", value: "${heatMode}\n${isHeating ? "heating to ${targetTemperature}°" : "not heating"}")
add:
    sendEvent(name: "ReadyMode", value: "${heatMode}")
  1. save and enjoy.

If you're curious, my rule machine rule just looks for the aforementioned alexa controlled virtual switch to turn off and stay off for 72 hours before it triggers the rule. In my rule, I check the ReadyMode attribute for the device to make sure it is in Ready state, if it is, I use the actuator to trigger setReadyMode. setReadyMode is a button press, there's no state tracking, if it's in ready mode it will go into rest, and vice versa, just like the button on the tub control panel and in the BWA app.

P.S.
In an earlier comment in this thread I mentioned you could just as well approximate this behavior by setting your temperature to a lower temperature, and that is true. However in ready mode the tub cycles the heater pump every 15 minutes (I believe) to check that it is maintaining the set temperature. If you're going to be away for a period of time, Rest mode is seemingly better. Regardless of your preference here, now you can do both, set your temp to a much lower setting if you'd like, and set the tub to Rest mode. For my own personal use case in this situation, my preference was to keep the tub nearer to my normal target temperature. For that reason I found Rest mode was more appropriate. However, there's no objective answer here, as this is a highly subjective preference.

P.P.S. I wish I could automate the process of water maintenance (checking levels, feeding chemicals) etc. but I think that might be a bit beyond the scope of Hubitat (at least for now, we'll have to wait for the AI version of Hubitat that has arms and legs). :wink:

3 Likes

This is great, very helpful to be able to control things this way.

There is only a couple other things I wish I could get fixed with this.

  • I wish I could set (sync) the time via Hubitat. After a power outage, or testing the GFCI, I would love to be able to automate the time setting. Minor, but would be nice to have.
  • I mentioned above the 2-speed pump doesn't work quite right the way it is implemented currently. From a dashboard, you don't quite know if you are in low or high speed pump mode.

You and me both. I usually use the tub at least once if not twice a week, but if I am not feeling well, or the weather is bad for a period of time, it would be nice to not have to remember to check things.

1 Like

Thanks for the code/tips. I expanded on what you did for those who want to set low/high temperature range... Adding to your code

        attribute "TempRange", "enum", ["low", "high"]
        command "setTempRange"

In def parsePanelData

    sendEvent(name: "TempRange", value: "${heatingMode}")

Add new Function

void setTempRange() {
    log.debug "In parent setTempRange..."
    TempRange = device.currentValue("TempRange")
    log.debug "Device current heating mode is ${TempRange}"
    temp_range = TempRange == "low" ? "high" : "high"
    sendEvent(name: "TempRange", value: temp_range)
    log.debug "Setting hTemp Range to ${temp_range}"
    sendCommand("Button", BUTTON_MAP.TempRange)
}


3 Likes

I added a little more to HB BPA SPA Parent (a "switch" that makes it easy when you want to put the spa in rest more and low temp - aka for a vacation home or a while you're away on a long vacation) and turn it back on when you're headed for a "spa" vacation...:slight_smile: The single switch takes the spa to "high temp" and "Ready mode" when it's on, and "low temp" and "Rest mode" when it's off.

Add the following capability

        capability "Switch"

Next update the following functions

def on() {
    if (device.currentValue("ReadyMode") == "Rest") {
       setReadyMode()   
    }    
    if (device.currentValue("TempRange") == "low") {
       setTempRange() 
    }         
}

def off() {
    if (device.currentValue("ReadyMode") == "Ready") {
       setReadyMode()   
    }    
    if (device.currentValue("TempRange") == "high") {
       setTempRange() 
    }         
}

Finally add this to the def parsePanelData(encodedData) function
if (device.currentValue("ReadyMode") == "Ready" && device.currentValue("TempRange") == "high") {
   sendEvent(name: "switch", value: "on")    
} else {
   sendEvent(name: "switch", value: "off")  
}
2 Likes

would you mind posting your parent driver with the updated code?

1 Like

Apparently this won't work with the ControlMySpa Balboa controller. Bummer. :frowning:

Same here please.

Absolutely... here you go.

/*
 *  Hubitat BWA Spa Manager
 *  -> Parent Device Driver
 *
 *  Copyright 2020 Richard Powell
 *   based on work Copyright 2020 Nathan Spencer that he did for SmartThings
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  CHANGE HISTORY
 *  VERSION     DATE            NOTES
 *  0.9.0       2020-01-30      Initial release with basic access and control of spas
 *  1.0.0       2020-01-31      Updated UI and icons as well as switch functionality that can be controlled with
 *                              Alexa. Added preference for a "Default Temperature When Turned On"
 *  1.1.0       2020-06-03      Additional functionality for aux, temperature range, and heat modes
 *  1.1.1       2020-07-26      Adjusted icons to better match functionality for aux, temperature range and heat modes
 *                              and removed duplicate tile declaration
 *  1.1.2b      2020-09-17      Modified / validated to work on Hubitat
 *  1.1.3       2020-10-11      Major rewrite of this driver to work with Hubitat's Parent-Child device driver model
 *  1.1.4       2020-10-11      Support the remaining device types except Blower, more code clean-up
 *
 */

import groovy.transform.Field
import groovy.time.TimeCategory

@Field static int LOG_LEVEL = 3

@Field static String NAMESPACE = "richardpowellus"

@Field static String DEVICE_NAME_PREFIX = "HB BWA SPA"
@Field static String PARENT_DEVICE_NAME = "HB BPA SPA Parent"
@Field static String THERMOSTAT_CHILD_DEVICE_NAME = "HB BWA SPA Thermostat"
@Field static String SWITCH_CHILD_DEVICE_NAME = "HB BWA SPA Switch"

metadata {
    definition (name: PARENT_DEVICE_NAME, namespace: NAMESPACE, author: "Richard Powell") {
        capability "Configuration"
        capability "Actuator"
        capability "Switch"
        attribute "ReadyMode", "enum", ["Ready", "Rest"]
        attribute "TempRange", "enum", ["low", "high"]
        /* This is a list of attributes sent to us right after we successfully login
         * to Balboa and pull details about Spas linked to the user's account.
         *
         * Hubitat requires attributes to be defined in order for sendEvent(...) to
         * be able to update that attribute.
         */
        attribute "create_user_id", "string"
        attribute "deviceId", "string" // renamed from "device_id"
        attribute "update_user_id", "string"
        attribute "updated_at", "string"
        attribute "__v", "string"
        attribute "active", "string"
        attribute "created_at", "string"
        attribute "_id", "string"
        
        // Additional attributes
        attribute "spaStatus", "string"
        command "setReadyMode"
        command "setTempRange"

    }   
}

@Field static Map PUMP_BUTTON_MAP = [
    1: 4, // Pump 1 maps to Balboa API Button #4
    2: 5, // Pump 2 maps to Balboa API Button #5 etc.
    3: 6,
    4: 7,
    5: 8,
    6: 9]

@Field static Map LIGHT_BUTTON_MAP = [
    1: 17, // Light 1 maps to Balboa API Button #17 etc.
    2: 18]

@Field static Map AUX_BUTTON_MAP = [
    1: 22, // Aux 1 maps to Balboa API Button #22 etc.
    2: 23
    ]

@Field static Map BUTTON_MAP = [
    Blower: 12,
    Mister: 14,
    Aux1: 22,
    Aux2: 23,
    TempRange: 80,
    HeatMode: 81]

def logMessage(level, message) {
    if (level >= LOG_LEVEL) {
        if (level < 3) {
            log.debug message
        } else {
            log.info message
        }
    }
}

def installed() {
}

def updated() {
}

def on() {
    if (device.currentValue("ReadyMode") == "Rest") {
       setReadyMode()   
    }    
    if (device.currentValue("TempRange") == "low") {
       setTempRange() 
    }         
}

def off() {
    if (device.currentValue("ReadyMode") == "Ready") {
       setReadyMode()   
    }    
    if (device.currentValue("TempRange") == "high") {
       setTempRange() 
    }         
}


def sendCommand(action, data) {
    parent.sendCommand(device.currentValue("deviceId"), action, data)
    runIn(2, refresh)
}

def parseDeviceData(Map results) {
    results.each {name, value ->
        sendEvent(name: name, value: value, displayed: true)
    }
}

def createChildDevices(spaConfiguration) {
    // Thermostat
    fetchChild(true, "Thermostat", "Thermostat")
 
    /* The incoming spaConfiguration has a list of all the possible add-on devices like
       pumps, lights, etc. mapped to a boolean indicating whether or not this particular
       hot tub actually has that specific device installed on it.

       Iterate through all the possible add-on devices and if the hot tub we're working
       with actually has that device installed on it then we will go ahead and create a
       child device for it (passing "true" as the first parameter to fetchChild(...) will
       have it go and create a device if it doesn't exist already.
    */
    
    // Pumps
    spaConfiguration.each { k, v ->
        if (k.startsWith("Pump") && v == true) {
            def pumpNumber = k[4].toInteger()
            fetchChild(true, "Switch", "Pump ${pumpNumber}", PUMP_BUTTON_MAP[pumpNumber])
        }
    }
    
    // Lights
    spaConfiguration.each { k, v ->
        if (k.startsWith("Light") && v == true) {
            def lightNumber = k[5].toInteger()
            fetchChild(true, "Switch", "Light ${lightNumber}", LIGHT_BUTTON_MAP[lightNumber])
        }
    }
    
    // Blower
    if (spaConfiguration["Blower"] == true) {
        // TODO: Support Blower properly. It's not a "Switch" device type.
        //fetchChild(true, ???, "Blower", BUTTON_MAP.Blower)
    }
    
    // Aux
    spaConfiguration.each { k, v ->
        if (k.startsWith("Aux") && v == true) {
            def lightNumber = k[3].toInteger()
            fetchChild(true, "Switch", "Aux ${lightNumber}", AUX_BUTTON_MAP[lightNumber])
        }
    }
    
    // Mister
    if (spaConfiguration["Mister"] == true) {
        fetchChild(true, "Switch", "Mister", BUTTON_MAP.Mister)
    }
}

def parsePanelData(encodedData) {
    byte[] decoded = encodedData.decodeBase64()

    def is24HourTime = (decoded[13] & 2) != 0 ? true : false
    def currentTimeHour = decoded[7]
    def currentTimeMinute = decoded[8]
    
    def temperatureScale = (decoded[13] & 1) == 0 ? "F" : "C"
    def actualTemperature = decoded[6]
    
    def targetTemperature = decoded[24]
    def isHeating = (decoded[14] & 48) != 0
    def heatingMode = (decoded[14] & 4) == 4 ? "high" : "low"
    def heatMode
    switch (decoded[9]) {
        case 0:
            heatMode = "Ready"
            break;
        case 1:
            heatMode = "Rest"
            break;
        case 2:
            heatMode = "Ready in Rest"
            break;
        default:
            heatMode = "None"
    }
    
    // Send events to Thermostat child device
    def thermostatChildDevice = fetchChild(false, "Thermostat", "Thermostat")
    if (thermostatChildDevice != null) {
        thermostatChildDevice.sendEventsWithUnits([
            [name: "temperature", value: actualTemperature, unit: temperatureScale],
            [name: "heatingSetpoint", value: targetTemperature, unit: temperatureScale]
        ])
        thermostatChildDevice.sendEvents([
            [name: "thermostatMode", value: isHeating ? "heat" : "off"],
            [name: "thermostatOperatingState", value: isHeating ? "heating" : "idle"],
        ])
    }
      
    def filtermode
    switch (decoded[13] & 12) {
        case 4:
            filterMode = "Filter 1"
            break;
        case 8:
            filterMode = "Filter 2"
            break;
        case 12:
            filterMode = "Filter 1 & 2"
            break;
        case 0:
        default:
            filterMode = "Off"
    }
    
    def accessibilityType
    switch (decoded[13] & 48) {
        case 16:
            accessibilityType = "Pump Light"
            break;
        case 32:
        case 42:
            accessibilityType = "None"
            break;
        default:
            accessibilityType = "All"
    }
    
    // Pumps
    def pumpState = []
    pumpState[0] = null
    def pump1ChildDevice = fetchChild(false, "Switch", "Pump 1")
    if (pump1ChildDevice != null) {
        switch (decoded[15] & 3) { // Pump 1
            case 1:
            	pumpState[1] = "low"
                break
            case 2:
            	pumpState[1] = "high"
                break
            default:
            	pumpState[1] = "off"
        }
        pump1ChildDevice.parse(pumpState[1])
    }
    def pump2ChildDevice = fetchChild(false, "Switch", "Pump 2")
    if (pump2ChildDevice != null) {
        switch (decoded[15] & 12) { // Pump 2
            case 4:
                pumpState[2] = "low"
                break
            case 8:
                pumpState[2] = "high"
                break
            default:
                pumpState[2] = "off"
        }
        pump2ChildDevice.parse(pumpState[2])
    }
    def pump3ChildDevice = fetchChild(false, "Switch", "Pump 3")
    if (pump3ChildDevice != null) {
        switch (decoded[15] & 48) { // Pump 3
            case 16:
            	pumpState[3] = "low"
                break
            case 32:
            	pumpState[3] = "high"
                break
            default:
            	pumpState[3] = "off"
        }
        pump3ChildDevice.parse(pumpState[3])
    }
    def pump4ChildDevice = fetchChild(false, "Switch", "Pump 4")
    if (pump4ChildDevice != null) {
        switch (decoded[15] & 192) {
            case 64:
            	pumpState[4] = "low"
                break
            case 128:
            	pumpState[4] = "high"
                break
            default:
            	pumpState[4] = "off"
        }
        pump4ChildDevice.parse(pumpState[4])
    }
    def pump5ChildDevice = fetchChild(false, "Switch", "Pump 5")
    if (pump5ChildDevice != null) {
        switch (decoded[16] & 3) {
            case 1:
            	pumpState[5] = "low"
                break
            case 2:
            	pumpState[5] = "high"
                break
            default:
            	pumpState[5] = "off"
        }
        pump5ChildDevice.parse(pumpState[5])
    }
    def pump6ChildDevice = fetchChild(false, "Switch", "Pump 6")
    if (pump6ChildDevice != null) {
        switch (decoded[16] & 12) {
            case 4:
            	pumpState[6] = "low"
                break
            case 8:
            	pumpState[6] = "high"
                break
            default:
            	pumpState[6] = "off"
        }
        pump6ChildDevice.parse(pumpState[6])
    }
    
    // TODO: Support Blower properly. It's not a switch device type
    switch (decoded[17] & 12) {
        case 4:
        	blowerState = "low"
            break
        case 8:
        	blowerState = "medium"
            break
        case 12:
        	blowerState = "high"
            break
        default:
        	blowerState = "off"
    }
    
    // Lights
    def lightState = []
    lightState[0] = null
    def light1ChildDevice = fetchChild(false, "Switch", "Light 1")
    if (light1ChildDevice != null) {
        lightState[1] = (decoded[18] & 3) != 0
        light1ChildDevice.parse(lightState[1])
    }
    def light2ChildDevice = fetchChild(false, "Switch", "Light 2")
    if (light2ChildDevice != null) {
        lightState[2] = (decoded[18] & 12) != 0
        light2ChildDevice.parse(lightState[2])
    }
    
    // Mister
    def misterChildDevice = fetchChild(false, "Switch", "Mister")
    def misterState = null
    if (misterChildDevice != null) {
        misterState = (decoded[19] & 1) != 0
        misterChildDevice.parse(misterState)
    }
    
    // Aux
    def auxState = []
    auxState[0] = null
    def aux1ChildDevice = fetchChild(false, "Switch", "Aux 1")
    if (aux1ChildDevice != null) {
        auxState[1] = (decoded[19] & 8) != 0
        aux1ChildDevice.parse(auxState[1])
    }
    def aux2ChildDevice = fetchChild(false, "Switch", "Aux 2")
    if (aux2ChildDevice != null) {
        auxState[2] = (decoded[19] & 16) != 0
        aux2ChildDevice.parse(auxState[2])
    }
    
    def wifiState
    switch (decoded[16] & 240) {
    	case 0:
        	wifiState = "OK"
            break
        case 16:
        	wifiState = "Spa Not Communicating"
            break
        case 32:
        	wifiState = "Startup"
            break
        case 48:
        	wifiState = "Prime"
            break
        case 64:
        	wifiState = "Hold"
            break
        case 80:
        	wifiState = "Panel"
            break
    }
    
    def pumpStateStatus
    if (decoded[15] < 1 && decoded[16] < 1 && (decoded[17] & 3) < 1) {
    	pumpStateStatus = "Off"
    } else {
    	pumpStateStatus = isHeating ? "Low Heat" : "Low"
    }
    
    if (actualTemperature == 255) {
    	actualTemperature = device.currentValue("temperature") * (temperatureScale == "C" ? 2.0F : 1)
    }
    
    if (temperatureScale == "C") {
    	actualTemperature /= 2.0F
    	targetTemperature /= 2.0F
    }
    
    logMessage(2, "Actual Temperature: ${actualTemperature}\n"
                + "Current Time Hour: ${currentTimeHour}\n"
                + "Current Time Minute: ${currentTimeMinute}\n"
                + "Is 24-Hour Time: ${is24HourTime}\n"
                + "Temperature Scale: ${temperatureScale}\n"
                + "Target Temperature: ${targetTemperature}\n"
                + "Filter Mode: ${filterMode}\n"
                + "Accessibility Type: ${accessibilityType}\n"
                + "Heating Mode: ${heatingMode}\n"
                + "lightState[1]: ${lightState[1]}\n"
                + "lightState[2]: ${lightState[2]}\n"
                + "Heat Mode: ${heatMode}\n"
                + "Is Heating: ${isHeating}\n"
                + "pumpState[1]: ${pumpState[1]}\n"
                + "pumpState[2]: ${pumpState[2]}\n"
                + "pumpState[3]: ${pumpState[3]}\n"
                + "pumpState[4]: ${pumpState[4]}\n"
                + "pumpState[5]: ${pumpState[5]}\n"
                + "pumpState[6]: ${pumpState[6]}\n"
                + "blowerState: ${blowerState}\n"
                + "misterState: ${misterState}\n"
                + "auxState[1]: ${auxState[1]}\n"
                + "auxState[2]: ${auxState[2]}\n"
                + "pumpStateStatus: ${pumpStateStatus}\n"
                + "wifiState: ${wifiState}\n"
    )
    
    sendEvent(name: "spaStatus", value: "${heatMode}\n${isHeating ? "heating to ${targetTemperature}°" : "not heating"}")
    sendEvent(name: "ReadyMode", value: "${heatMode}")
    sendEvent(name: "TempRange", value: "${heatingMode}")
    if (device.currentValue("ReadyMode") == "Ready" && device.currentValue("TempRange") == "high") {
       sendEvent(name: "switch", value: "on")    
    } else {
       sendEvent(name: "switch", value: "off")  
    }
    
}

def fetchChild(createIfDoesntExist, String type, String name, Integer balboaApiButtonNumber = 0) {
    String thisId = device.id
    def childDeviceName = "${thisId}-${name}"
    logMessage(2, "childDeviceName: '${childDeviceName}")
    
    def cd = getChildDevice(childDeviceName)
    if (!cd && createIfDoesntExist) {
        def driverName = "${DEVICE_NAME_PREFIX} ${type}"
                
        logMessage(3, "Adding Child Device. Driver: '${driverName}', Name: '${childDeviceName}'")
        cd = addChildDevice(NAMESPACE, driverName, childDeviceName, [name: "${device.displayName} {$name}", isComponent: true])
        
        // Switches will need to know their respective Balboa API Button IDs
        if (type == "Switch" && balboaApiButtonNumber > 0) {
            cd.setBalboaAPIButtonNumber(balboaApiButtonNumber)
        }
    }
    return cd
}



void setReadyMode() {
    log.debug "In parent setReadyMode..."
    ReadyMode = device.currentValue("ReadyMode")
    log.debug "Device current mode is ${ReadyMode}"
    text_mode = ReadyMode == "Ready" ? "Rest" : "Ready"
    sendEvent(name: "ReadyMode", value: text_mode)
    log.debug "Setting mode to ${text_mode}"
    sendCommand("Button", BUTTON_MAP.HeatMode)
}

void setTempRange() {
    log.debug "In parent setTempRange..."
    TempRange = device.currentValue("TempRange")
    log.debug "Device current heating mode is ${TempRange}"
    temp_range = TempRange == "low" ? "high" : "high"
    sendEvent(name: "TempRange", value: temp_range)
    log.debug "Setting hTemp Range to ${temp_range}"
    sendCommand("Button", BUTTON_MAP.TempRange)
}

void refresh() {
    parent.pollChildren()
}
2 Likes

This app and drivers worked perfectly on my Bullfrog A7L model. I want to thank the original developer and also all those involved with adding tweaks from above posts. It is so great to integrate this into HE so I can operate the spa efficiently.

I was also going to add some enhancements that I might make down the road (e.g HE location mode polling intervals, etc). I was wondering:

  1. Why isn't this great app installed via HPM?
  2. Is the Github being maintained (last was 2020) with the latest updates from those enhancements above? If not, I am willing to fork and add to HPM.
1 Like