Sunday, 30 October 2022

Dispatchables Part 3; Make It So

In the previous part of this series about implementing a "dispatchable" for solar-efficient charging of (AA and AAA) batteries, I'd worked out that with a combination of the Google Assistant's Energy Storage trait (made visible through the openHAB Google Assistant Charger integration) and a small amount of local state, it looked like in theory, I could achieve my aim of a voice-commanded (and -queryable) system that would allow efficient charging for a precise amount of time. Let's now see if we can turn theory into practice.

First step is to copy all the configuration from the openHAB Charger device type into an items file:

$OPENHAB_CONF/items/dispatchable.items
Group  chargerGroup 
{ ga="Charger" [ isRechargeable=true, unit="SECONDS" ] }

Switch chargingItem         (chargerGroup) 
{ ga="chargerCharging" }

Switch pluggedInItem        (chargerGroup) 
{ ga="chargerPluggedIn" }

Number capacityRemainItem   (chargerGroup) 
{ ga="chargerCapacityRemaining" }

Number capacityFullItem     (chargerGroup) 
{ ga="chargerCapacityUntilFull" }

You'll note the only alterations I made was to change the unit to SECONDS as that's the best fit for our timing system, and a couple of renames for clarity. Here's what they're all representing:

  • chargingItem: are the batteries being charged at this instant?
  • pluggedInItem: has a human requested that batteries be charged?
  • capacityRemainSecondsItem: how many seconds the batteries have been charging for
  • capacityFullSecondsItem: how many seconds of charging remain
I could have used the "proper" dead timer pattern of saying "any non-zero capacityFullSecondsItem indicates intent" but given the Charger type requires all four variables to be implemented anyway, I went for a crisper definition. It also helps with the rule-writing as we'll shortly see.

If we look at the openHAB UI at this point we'll just have a pile of NULL values for all these items:

Now it's time to write some rules that will get sensible values into them all. There are four in total, and I'll explain each one in turn rather than dumping a wall of code.


Rule 1: Only charge if it's wanted, AND if we have power to spare

$OPENHAB_CONF/rules/dispatchable.rules
rule "Make charging flag true if wanted and in power surplus"
when
  Item currentPowerUsage changed 
then
  if (pluggedInItem.state == ON) {
    if (currentPowerUsage.state > 0|W) {
      logInfo("dispatchable", "[CPU] Non-zero power usage");
      chargingItem.postUpdate(OFF);
    } else {
      logInfo("dispatchable", "[CPU] Zero power usage");
      chargingItem.postUpdate(ON);
    }
  } 
end
This one looks pretty similar to the old naïve rule we had way back in version 1.0.0, and it pretty-much is. We've just wrapped it with the "intent" check (pluggedInItem) to make sure we actually need to do something, and offloaded the hardware control elsewhere. Which brings us to...


Rule 2: Make the hardware track the state of chargingItem

$OPENHAB_CONF/rules/dispatchable.rules
rule "Charge control toggled - drive hardware" 
when
  Item chargingItem changed to ON or
  Item chargingItem changed to OFF
then
  logInfo("dispatchable", "[HW] Charger: " + chargingItem.state);
  SP2_Power.sendCommand(chargingItem.state.toString());
end
The simplest rule of all, it's a little redundant but it does prevent hardware control "commands" getting mixed up with software state "updates".


Rule 3: Allow charging to be requested and cancelled

$OPENHAB_CONF/rules/dispatchable.rules
rule "Charge intent toggled (pluggedIn)" 
when
  Item pluggedInItem changed
then
  if (pluggedInItem.state == ON) {
    // Human has requested charging 
    logInfo("dispatchable", "[PIN] charge desired for: ");
    logInfo("dispatchable", capacityFullSecondsItem.state + "s");
    capacityRemainSecondsItem.postUpdate(0);
    // If possible, begin charging immediately:
    if (currentPowerUsage.state > 0|W) {
      logInfo("dispatchable", "[PIN] Awaiting power-neutrality");
    } else {
      logInfo("dispatchable", "[PIN] Beginning charging NOW");
      chargingItem.postUpdate(ON);
    }
  } else {
    logInfo("dispatchable", "[PIN] Cancelling charging");
    // Clear out all state
    capacityFullSecondsItem.postUpdate(0);
    capacityRemainSecondsItem.postUpdate(0);
    chargingItem.postUpdate(OFF);
  }
end
This rule is where things start to get a little trickier, but it's pretty straightforward. The key thing is setting or resetting the three other variables to reflect the user's intent.

If charging is desired we assume that the "how long for" variable has already been set correctly and zero the "how long have you been charging for" counter. Then, if the house is already power-neutral, we start. Otherwise we wait for conditions to be right (Rule 1).
If charging has been cancelled we can just clear out all our state. The hardware will turn off almost-immediately because of Rule 2.


Rule 4: Keep timers up-to-date

$OPENHAB_CONF/rules/dispatchable.rules
rule "Update charging timers"
when
  Time cron "0 0/1 * * * ?"
then
  if (pluggedInItem.state == ON) {
    // Charging has been requested
    if (chargingItem.state == ON) {
      // We're currently charging
      var secLeft = capacityFullSecondsItem.state as Number - 60; 
      capacityFullSecondsItem.postUpdate(secLeft);
      logInfo("dispatchable", "[CRON] " + secLeft + "s left");
      
      var inc = capacityRemainSecondsItem.state as Number + 60; 
      capacityRemainSecondsItem.postUpdate(inc);

      // Check for end-charging condition:
      if (secLeft <= 0) {
        // Same as if user hit cancel:
        logInfo("dispatchable", "[CRON] Reached target.");
        pluggedInItem.postUpdate(OFF);
      }
    } 
  } 
end
This last rule runs once a minute, but only does anything if the user asked for charging AND we're doing so. If that's the case, we decrement the time left" by 60 seconds, and conversely increase the "how long have they been charging for" by 60 seconds. Yes, I know this might not be strictly accurate but it's good enough for my needs.

The innermost if statement checks for the happy-path termination condition - we've hit zero time left! - and toggles the flag which will once-again lower the intent flag, thus causing Rule 3 to fire, which in turn will cause Rule 2 to fire, and turn off the hardware.

UI Setup

This has ended up being quite the journey, and we haven't even got the Google integration going yet! The last thing for this installment is to knock up a quick control/status UI so that we can see that it actually works correctly. Here's what I've got in my openHAB "Overview" page:

The slider is wired to capacityFullSecondsItem, with a range of 0 - 21600 (6 hours) in 60-second increments, and 6 "steps" marked on the slider corresponding to integer numbers of hours for convenience. The toggle is wired to pluggedInItem. When I want to charge some batteries, I pull the slider to my desired charge time and flip the switch. Here's a typical example of what I get in the logs if I do this during a sunny day:
[PIN] charge desired for: 420 seconds
[PIN] Beginning charging immediately
[HW] Charger: ON
[CRON] 360s left
...
[CRON] 120s left
[CRON] 60s left
[CRON] 0s left
[CRON] Reached desired charge. Stopping
[PIN] Cancelling charging
[HW] Charger: OFF