Balboa-Binding: port to OH5 and implement improvements

I’m running a Balboa-based hot tub and using the Balboa-Binding as uploaded AddOn org.openhab.binding.balboa-3.2.0-SNAPSHOT.jar works - in theory.
What I’m experiencing are outages of the binding not able to retrieve information or send them. I’m not sure, if this is due to the balboa wifi-module being instable (and from what I read quite hastily and buggy written) or if it’s to do with something the binding sends, which breaks the Balboa controller.

Either way, there is some interesting discussion (Issue #110, Issue #109) around this on the python module. Especially in environments with range extenders, WiFi Mesh,… (which I do have):

So my question is this: anyone around with enough skills to both bump the binding to OH5 architecture and implementing the improvements found on the python module? If I understand their progress correctly, there’s three main parts of improvement, which I think aren’t already in the current 3.2 binding:

Is there a chance to get this on track? :wink:
I can’t code unfortunately…

It’s more or less trivial to get this up and running as a HABApp Rule.
If you can’t do it yourself you can always ask some KI to help and you should be able to produce something working.
I know it’s not what your are asking for but at least that way you can continue using your appliance together with openHAB.
I had to do the same when interfacing my solar inverters since modbus was broken.

Yes, I could run pybalboa in parallel in integrate it, but I’d be happy to be testing a OH5-port of the Balboa-binding - to avoid having all that clutterish architecture to maintain! :wink:

@binderth: I gave it a try with claude and to my surprise it seems to work.

Caution: It’s AI generated, the code was not manually reviewed, only the function has been tested. If you have problems with AI generated code please don’t use it.

  • port to OH 5.2.1
  • Auto-discovery: the binding can now find Balboa Wi-Fi modules on the local network (Inbox → Scan). Things can still be added manually
  • Smarter reconnect: reconnect attempts now back off exponentially (instead of retrying at a fixed interval forever) after a disconnect.
  • Dead-connection detection: if the Wi-Fi module disappears without properly closing the connection (e.g. loses power), the binding notices and reconnects automatically instead of showing ONLINE indefinitely.
  • More accurate status reporting: a temporarily unreachable unit is now shown as OFFLINE with a proper reason, instead of a misleading configuration-error state.

Installation Download jar and drop it into your openHAB `addons` folder.

Here you can find the current beta2:

Some feedback if it works for you (or not) would be welcome.
@clinophobic : Maybe you’re still interessted too.

HI I have ported the python code to js to work with my ethernet to rs485 adapters no wifi connection, this code wait for complete data meassages and throw the others. Reacts super fast and works good for all of this summer anyhow. A little bit updated, maybe this could give som inspiration.

/* global rules, triggers, items, actions, time, Java */

const SPA_CONFIG = {
    ip: "x.x.x.x",
    port: 4257
};

const POOL_MAP = {
    "Pool_pump1": { status: "Pool_PUMP1", id: 0x04 },
    "pool_pump2": { status: "Pool_PUMP2", id: 0x05 },
    "pool_pump3": { status: "Pool_PUMP3", id: 0x06 },
    "pool_lights": { status: "Pool_LIGHTS", id: 0x11 },
    "pool_temprange": { status: "Pool_TEMP_RANGE", id: 0x50 }
};

var spaTimers = {};

function sendSpaCommand(targetId) {
    const Socket = Java.type("java.net.Socket");
    const DataOutputStream = Java.type("java.io.DataOutputStream");
    const InetSocketAddress = Java.type("java.net.InetSocketAddress");

    let socket = new Socket();
    try {
        socket.connect(new InetSocketAddress(SPA_CONFIG.ip, SPA_CONFIG.port), 3000);
        let dOut = new DataOutputStream(socket.getOutputStream());

        function calculateCRC(data) {
            let crc = 0x02;
            for (let i = 0; i < data.length; i++) {
                crc ^= data[i];
                for (let j = 0; j < 8; j++) {
                    if ((crc & 0x80) !== 0) crc = ((crc << 1) ^ 0x07) & 0xFF;
                    else crc = (crc << 1) & 0xFF;
                }
            }
            return crc ^ 0x02;
        }

        let payload = [0x06, 0x0a, 0xbf, 0x11, targetId];
        let crc = calculateCRC(payload);
        let packet = [0x7e, ...payload, crc, 0x7e];

        let jArr = Java.type("java.lang.reflect.Array").newInstance(Java.type("byte"), packet.length);
        for (let i = 0; i < packet.length; i++) {
            jArr[i] = (packet[i] > 127) ? (packet[i] - 256) : packet[i];
        }

        dOut.write(jArr);
        dOut.flush();
        return true;
    } catch (e) {
        console.error("POOL-Socket FEL: " + e.message);
        return false;
    } finally {
        socket.close();
    }
}

rules.JSRule({
    name: "POOL Command Logic with Retry",
    triggers: Object.keys(POOL_MAP).map(it => triggers.ItemCommandTrigger(it)),
    execute: (event) => {
        const itemName = event.itemName;
        const mapping = POOL_MAP[itemName];
        const requested = event.receivedCommand.toString().toUpperCase();

        if (spaTimers[itemName]) {
            spaTimers[itemName].cancel();
            delete spaTimers[itemName];
        }

        const runAttempt = (currentAttempt) => {
            const statusItem = items.getItem(mapping.status);
            const actualStatus = (statusItem && statusItem.state) ? statusItem.state.toString().toUpperCase() : "OFF";
            let isCurrentlyOn = (actualStatus === "ON" || actualStatus === "HIGH");
            let normalizedStatus = isCurrentlyOn ? "ON" : "OFF";

            console.info("SPA: Check " + itemName + ". Forsok " + currentAttempt + "/3. Mal:" + requested + ", Status:" + actualStatus);

            if (requested !== normalizedStatus) {
                if (currentAttempt <= 3) {
                    sendSpaCommand(mapping.id);
                    spaTimers[itemName] = actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plusSeconds(20), function() {
                        runAttempt(currentAttempt + 1);
                    });
                } else {
                    console.warn("POOL: Gav upp efter 3 forsok for " + itemName);
                    delete spaTimers[itemName];
                }
            } else {
                console.info("POOL: Lyckades! " + itemName + " matchar nu mal.");
                delete spaTimers[itemName];
            }
        };
        runAttempt(1);
    }
});

rules.JSRule({
    name: "POOL UI Synchronizer and Counter",
    triggers: Object.values(POOL_MAP).map(m => triggers.ItemStateChangeTrigger(m.status)),
    execute: (event) => {
        const statusItemName = event.itemName;
        
        // 1. Uppdatera Counter (+1 vid varje korrekt statuspaket/andring)
        const counterItem = items.getItem("PoolSuccessUpdateCounter");
        if (counterItem) {
            let currentCount = parseInt(counterItem.state.toString()) || 0;
            counterItem.postUpdate(currentCount + 1);
        }

        // 2. Synkronisera UI-Switchar
        const uiItemName = Object.keys(POOL_MAP).find(k => POOL_MAP[k].status === statusItemName);
        if (uiItemName) {
            let state = event.newState.toString().toUpperCase();
            let toggle = (state === "ON" || state === "HIGH") ? "ON" : "OFF";
            items.getItem(uiItemName).postUpdate(toggle);
        }
    }
});

have you tested my above port? Till now it’s working without problems in my setup. The network failures (I always had about 2/day with the old binding) are gone. And it’s reacting much faster too.

just came home and dropped the JAR (org.openhab.binding.balboa-5.2.1-beta2.jar). As of now: everything works without an error.
What I did a month ago: placed a (temporary) access point near the hot tub, which increased stability. I only have 2 weeks worth of persistence for this item, but before there was more “blue” intervals! :wink:
but still there’s the typical 00:59h dropouts. (network binding with ping on the wifi-module)

As an idea: I was able to eliminate those dropouts by disabling 5 GHz within the repeater.

still going strong without a drop-out since this morning. knock on wood