Battery monitoring for Hunter Douglas PowerView

As I’m progressing with my DSL to JavaScript migration, here’s a small sequel to Automating Hunter Douglas PowerView automations.

These rules are created for monitoring the voltage/battery level on battery-powered Hunter Douglas/Luxaflex PowerView shades (< Gen 3).

By principle this should be a matter of reacting when channel lowBattery changes to ON.

However, from my experience the voltage reported by some of these shades (such as Top Down/Bottom Up with two motors) is very unreliable:

This is the cause of false positives - not only in openHAB, but also shown as notifications when launching the PowerView app. By default battery status is refreshed once per week; however the frequency can be overridden by Bridge configuration. This can not solve the problem though, it will only increase the resolution, but cause more spikes.

In my use-case I have two battery-powered shades, Blind10 and Blind11, which I’m monitoring in order to get notifications when I need to recharge the batteries - for real!

powerview.items:

Number Blind10_BatteryLevel "Battery Level" <battery> (Blind10) ["Point"] { channel="hdpowerview:shade:hub:blind10:batteryLevel" }
Switch Blind10_LowBattery "Low Battery" <lowbattery> (Blind10) ["Point"] { channel="hdpowerview:shade:hub:blind10:lowBattery" }
Number:ElectricPotential Blind10_BatteryVoltage "Battery Voltage" <energy> (Blind10) ["Point"] { channel="hdpowerview:shade:hub:blind10:batteryVoltage", unit="V" }

Number Blind11_BatteryLevel "Battery Level" <battery> (Blind11) ["Point"] { channel="hdpowerview:shade:hub:blind11:batteryLevel" }
Switch Blind11_LowBattery "Low Battery" <lowbattery> (Blind11) ["Point"] { channel="hdpowerview:shade:hub:blind11:lowBattery" }
Number:ElectricPotential Blind11_BatteryVoltage "Battery Voltage" <energy> (Blind11) ["Point"] { channel="hdpowerview:shade:hub:blind11:batteryVoltage", unit="V" }

persistence:

        Blind10_BatteryVoltage,
        Blind11_BatteryVoltage: strategy = everyChange

powerview.js:

function scheduleBatteryStateRefresh(item) {
    console.log("Scheduling refresh of battery state in 1 minute for " + item.name);
    setTimeout(() => {
        console.log("Triggering REFRESH for " + item.name);
        item.sendCommand("REFRESH");
    }, 60*1000);
}

function updateBatterWarningTimer(item) {
    if (item.isUninitialized) {
        var timeoutId = item.name.startsWith("Blind10_") ? blind10BatteryWarningTimeoutId : blind11BatteryWarningTimeoutId;
        if (timeoutId > 0) {
            // Already scheduled.
            console.log(item.name + " -> battery warning timer already scheduled");
            return;
        }
        console.log(item.name + " -> scheduling battery warning timer");
        var timeoutId = setTimeout(() => {
            notification.send("PowerView" , "Duette battery voltage in " + getRoomFromItem(item) + " could not be read for two hours.");
        }, 2*60*60*1000);
        if (item.name.startsWith("Blind10_")) {
            blind10BatteryWarningTimeoutId = timeoutId;
        } else {
            blind11BatteryWarningTimeoutId = timeoutId;
        }
    } else {
        if (item.name.startsWith("Blind10_")) {
            if (blind10BatteryWarningTimeoutId > 0) {
                console.log(item.name + " -> cancelling battery warning timer");
                clearTimeout(blind10BatteryWarningTimeoutId);
                blind10BatteryWarningTimeoutId = 0;
            }
        } else {
            if (blind11BatteryWarningTimeoutId > 0) {
                console.log(item.name + " -> cancelling battery warning timer");
                clearTimeout(blind11BatteryWarningTimeoutId);
                blind11BatteryWarningTimeoutId = 0;
            }
        }
    }
}

function isBatteryStateValid(item) {
    if (item.isUninitialized) {
        console.log(item.name + " is UNDEF");
        return false;
    }
    var currentState = item.quantityState;
    if (currentState.lessThan("11 V")) {
        console.log(item.name + " is suspiciously low: " + currentState);
        return false;
    }
    var previousState = item.history.previousState(true, "jdbc").quantityState;
    var average = item.history.averageSince(time.ZonedDateTime.now().minusHours(6), "jdbc");
    var diff = average - item.numericState;
    if (diff >= 0.5) {
        console.log(item.name + " dropped more than 0.5 V compared to average voltage " + diff + " V in the last 6 hours: " + currentState);
        return false;
    }

    return true;
}

function verifyAndRefreshBatteryState(item) {
    updateBatterWarningTimer(item);

    if (!isBatteryStateValid(item)) {
        scheduleBatteryStateRefresh(item);
    }
}

function getRoomFromItem(item) {
    return item.name.startsWith("Blind10_") ? "bedroom" : "walk-in";
}

rules.when()
    .item("Blind10_BatteryVoltage").changed()
    .or()
    .item("Blind11_BatteryVoltage").changed()
    .then(event =>
    {
        verifyAndRefreshBatteryState(items.getItem(event.itemName));
    })
    .build("PowerView Duette voltage monitoring (event)", "Monitor and correct PowerView voltage readings");

rules.when()
    .cron("0 0/5 * * * ?")
    .then(event =>
    {
        verifyAndRefreshBatteryState(items.Blind10_BatteryVoltage);
        verifyAndRefreshBatteryState(items.Blind11_BatteryVoltage);
    })
    .build("PowerView Duette voltage monitoring (periodic)", "Monitor missing PowerView voltage readings");

rules.when()
    .item("Blind10_LowBattery").changed().toOn()
    .or()
    .item("Blind11_LowBattery").changed().toOn()
    .then(event =>
    {
        var item = items.getItem(event.itemName.substring(0, 8) + "BatteryVoltage");
        if (!isBatteryStateValid(item)) {
            return;
        }
        notification.send("PowerView", "Battery level in " + getRoomFromItem(item) + " is low: " + item.state);
    })
    .build("PowerView Duette battery low", "Send notification when Duette battery level is low");

Another approach could be to apply a transform profile to the link that ignores a change that is too large from the last reading. Essentially you’d implement your isBatteryStateValid as a transformation. It it’s not “good” return the previous state. If it is good, return the new state.

Note, you’d have to change to use the cache instead of persistence to keep the previous state as the timing of persistence could get in the way.

1 Like

Thanks, interesting proposal - I will definitely give that some thoughts. It could probably replace the parts considering whether to “trust” the values, but I would still need logic for triggering refreshes which would probably be better to keep outside of such a profile?

I would also like to get rid of the cron events every 5 minutes. The issue I’m having is that if item is already UNDEF, gets refreshed, and then is still UNDEF, I need to force another refresh after some time (without being too aggresive). Perhaps I could use updated() rather than changed() and still get an event, and then use a timer for that also? I would then only lack handling the state initially being UNDEF when the system is started, in which case I need to trigger a refresh right away.

seems reasonable

On system restart the Item should initialize to NULL. You really should rarely see UNDEF.

Not knowing the binding involved here, I would expect the binding to refresh itself on startup.