Generator Hours Counter (10 years later)

Ten years ago, @rlkoshak helped me calculate the running time for my generator. Well, I have built a small datacenter on my property and have a 65KW generator I would like to calculate the running time based on how long an item is on. After a decade, my thought would be that there are more elegant ways of doing this, so I thought I would ping the community for new ideas.

1 Like

Details will matter but I think the overall approach will be the same.

Though if I were to implement this today I’d use timestamps. I’d also use JS instead of Rules DSL and definitely eliminate the locks which are no longer necessary even in Rules DSL. I’d use a Number:Time Item to store the runtime also.

In general working with times and durations is much easier in JS and using UoM gives more flexibility in the rules and in how the data is displayed.

For just the runtime calculations, using timestamps could look something like this using JS Scripting’s Rule Builder.

rules.when()
         .item('Generator_Auto').receivedCommand()
         .item('Generator_Override').receivedCommand()
      .then( event => {
        if(event.receivedCommand == "ON")
          cache.shared.put('GeneratorOn', time.toZDT().toEpochSecond()); // do not put JS Objects in the shared cache
        else 
          cache shared.put('GeneratorOff', time.toZDT().toEpochSecond());
      })
      .build("Generaor ON/OFF", "Records when the generator turns on or off to the cache.");

rules.when()
         .cron('0 0/1 * * * ?')
     .if( () => {
       // Let's not run the rule if the generator is OFF
       const generatorOnEpoch = cache.shared.get('GeneratorOn');
       const generatorOffEpoch = cache.shared.get('GeneratorOff');
       if( generatorOnEpoch === null ) return false; // Nothing to do if start time never happend
       if( generatorOffEpoch === null) return true; // On but hasn't turned off, run the rule

       // If the on time is after the last off time but before now run the rule
       // setting the onTime to 45 seconds to the future might be a way to eliminate that first sleep
       const onTime = time.toZDT(time.Instant.ofEpochSecond(generatorOnEpoch));
       const offTime = time.toZDT(time.Instant.ofEpochSecond(generatorOffEpoch));
       const now = time.toZDT();
       return onTime.isAfter(offTime) && onTime.isBefore(now);
     })
     .then( event => {
       const onTime = time.toZDT(time.Instant.ofEpochSeconds(cache.shared.get('GeneratorOn')));
       const currentRuntime = time.Duration.between(onTime && now);
       const lastRuntime = cache.private.get('lastRuntime', () => time.duration.ofSeconds(0)); // the anonymous function gets called when there is not value stored yet, initializing it to zero
       const delta = (lastRuntime.seconds() == 0): currentRuntime ? 
                         (lastRuntime.compareTo(currentRuntime) < 0) : currentRuntime.minus(lastRuntime) ? lastRuntime.minus(currentRuntime);
       const totalRuntime = items.Today_Generator_Runtime;
       totalRuntime.postUpdate(totalRuntime.quantityState.plus(Quantity(delta.seconds() + " S")));
     })
     .build("Calculate the total runtime", "Keeps a running total of the total runtime of the generator");

I’m sure there are some edge cases here I’m not covering (this sort of thing is rather complicated) but I hope this makes the overall approach clear.

Notes:

  • Rule builder is not the only way to create Rules using JS. Personally I prefer managed rules but there is also JSRule which is a little more like Rules DSL in it’s approach. See the docs for detail.
  • JS uses joda-js for all time based stuff and there are additions OH has made to that library to make it even easier to use. time.toZDT() is able to convert almost anything into a ZonedDateTime. It’s almost always going to be more straight forward and the code easier to read and write when using the classes instead of backing out to epoch then dividing back to times,
  • Number:Time Items can be formated for display using the same formatting strings as a DateTime would use. For example, the current runtime on my UPS is displayed as 00:41:57 but the state of the Item is 2517 s.
  • The cache is usually preferable to dummy Items or global variables as they work for managed rules and between rules of different languages. Also, when a rule is unloaded any timers it has running will be cancelled automatically instead of throwing an error. Private cache only saves the data for that one rule. Shared cache shares the data across all rules. But there is a limitation in JS which causes problems if you try to put anything except primitives in the shared cache. If you used global variables for the on and off timestamps you’d not need to convert to and from epoch seconds and could just store the ZDT itself. That would make that if() part of the second rule just be a couple of lines of code.
  • The if() clause of the second rule is a rule condition. As far as I know these are only available for managed rules (but only if… part of the rule), JS rule builder, and jRuby. They need to be implemented as if statements in the body of the rule in all other cases.
  • My intent above is not to show a complete working solution but to show an example of some of the new stuff which you might be able to employ to rewrite your existing rules in a straight forward translation, as well as show some ways it could be done differently. I am more than willing to help if you need help with either translating your existing rules or to make the above more complete and comprehensive,
  • The rules are really easy if you don’t need the countdown and only calulate the runtimes at start and stop.
  • The rules might be simpler if we used LoopingTimer from OHRT.
  • I’ve not addressed the sleeps in your original rule nor the error checking to make sure the generator successfully started.

Wow that looks complex. I will have to research using JS Rules, don’t really use the UI at all. My new requirements are simple, I just need to keep a tally of on time, I built a controller to deal with startup, shutdown and exceptions. Thanks so much for your example.

Depending on the accuracy you need, I do something similar and keep a cumulative duration for how long my pool pump runs. Basically, I poll every minute and if the pool mode is running, I increment an item with the time. I’m using python automation.

Here is my item:

Number:Time Pool_PoolModeRun "Pool Mode Run Time [%d min]"

Here is my script:

@rule("Increment Pool Minutes", description="This will increment the number of minutes the pool motors have run.  If the items are persisted, this should be an odometer type reading",
    tags=["Pool"])
@when("Time cron 0 * * * * ?")
@onlyif("Item Pool_PoolMode is ON")
def Increment_Pool_Minutes(module, input):
    item_minsrunpool = Registry.getItem("Pool_PoolModeRun")
    current_state = item_minsrunpool.getState()
    item_minsrunpool.postUpdate(current_state.intValue() + 60 if current_state not in (NULL, UNDEF) else 0)

Couldn’t the ON/OFF state simply be persisted on change, and then the total duration calculated from there when needed?

@sipvoip wanted a running counter that increases while it’s running. I didn’t want to hit persistence that often.

Also, it depends on the perspective strategy used in order to work. It must save at least every change. I tend to shy away from solutions that require specific strategies to work.

But yes, one could pull all the recorders for the day and sum up the times between the ONs and the OFFs. That would certainly work.