[SOLVED] Working with times in OH3 rules using JSScripting

I’ve been trying to get the current month Kw/h, previous month, current year and previous year for the consumption of the heatpump, but I simply cannot get this working. :frowning:

This is what I have right now:

rule "Calculate HeatPump Current Month Consumption"
when
    Time cron "* * * * * ?" // Every minute
then
    val ZonedDateTime startOfMonth = now.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0)
logInfo("StartOfMonth", new DateTimeType(startOfMonth))
    var monthlyConsumption = 0.0

(1..now.dayOfMonth).forEach[ day |
    val dayStart = startOfMonth.plusDays(day - 1)
logInfo("EachDay","Day: "+day+" - dayStart: "+dayStart)
    val dailyPhase1 = HeatPumpPhase1.averageSince(dayStart)
    val dailyPhase2 = HeatPumpPhase2.averageSince(dayStart)
    val dailyPhase3 = HeatPumpPhase3.averageSince(dayStart)
    val dailyVMC = VMCEnergy.averageSince(dayStart)

    logInfo("Debug", "DailyPhase1: " + dailyPhase1)
    logInfo("Debug", "DailyPhase2: " + dailyPhase2)
    logInfo("Debug", "DailyPhase3: " + dailyPhase3)
    logInfo("Debug", "DailyVMC: " + dailyVMC)

    if (dailyPhase1 !== null && dailyPhase2 !== null && dailyPhase3 !== null && dailyVMC !== null) {
        val dailyTotal = (dailyPhase1 + dailyPhase2 + dailyPhase3 - dailyVMC)
        monthlyConsumption += dailyTotal
        logInfo("DailyTotal", "Total for day " + day + ": " + dailyTotal)
    } else {
        logInfo("NullCheck", "One or more values are null for day " + day)
    }
]
logInfo("MonthlyTotal", monthlyConsumption)
    //HeatPumpCurrentMonthkWh.postUpdate(monthlyConsumption)
end

This is what I see in my log:

2024-01-17 18:44:34.338 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'energy.rules'
2024-01-17 18:44:36.255 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'energy-2' failed: An error occurred during the script execution: Could not invoke method: org.openhab.core.model.script.actions.Log.logInfo(java.lang.String,java.lang.String,java.lang.Object[]) on instance: null in energy

So I’m completely out of ideas. I’ve tried to implement some of the examples here but I haven’t been successfull at all because I am always getting that same error, again and again. :frowning:

First:

Time cron "* * * * * ?" // Every minute

This is every Second, not every Minute (it’t Quartz Cron)

Better don’t set the val type and do it the easy way:

val startOfMonth = now.with(LocalTime.MIDNIGHT).withDayOfMonth(1)

log action: the log action will always need two strings, where the first string is the logger name.
So instead of

logInfo("StartOfMonth", new DateTimeType(startOfMonth))

better use

logInfo("heatPump","Start of month: {}",startOfMonth)

The loop values will be from date x to now. If you want to see the daily average consumption, you will have to use

val Startdate = startOfMonth.plusDays(day - 1)
val Nextdate = startOfMonth.plusDays(day)
al dailyPhase1 = HeatPumpPhase1.averageBetween(Startdate,Nextdate)
1 Like

One of the things that is frustraiting about Rules DSL is it tries really hard to be typeless and it far too often fails.

It’s almost certainly this line:

logInfo("StartOfMonth", new DateTimeType(startOfMonth))

There isn’t enough context there for the engine to figure out that it needs to call toString() on that DateTimeType Object. Call toString yourself and it should work.

null errors that look like that are Rules DSL’s way to say "I couldn’t figure out how to cast this to a type I can use.

For this and a host of other reasons I cannot recommend Rules DSL for new rules development. Any of the other rules languages are:

  1. more consistent
  2. provide access to the full OH API (Rules DSL lacks access to many things like calling other rules and access to Item metadata)

The above rule as JS Scripting Action would look something like:

var startOfMonth = time.toZDT('00:00').withDayOfMonth(1);
console.info(startOfMonth.toString());
var monthlyConsumption = 0.0;

for(let day = 1; day <= time.toZDT().dayOfMonth(); day++) {
  const dayStart = startOfMonth.plusDays(day - 1);
  console.info("Day: " + day + " - dayStart: " + dayStart);
  const dailyPhases = [items.HeatPumpPhase1.history.averageSince(dayStart),
                       items.HeatPumpPhase2,history.averageSince(dayStart),
                       items.,HeatPumpPhase3.history.averageSince(dayStart)];
  const dailyVMC = items.VMCEnergy.history.averageSince(dayStart);
  console.info("DailyPhases: " + dailyPhases);
  console.info("DailyVMC: " + dailyVMC);

  if(!dailyPhases.includes(null) && dailyVMC !== null) {
    dailyTotal = dailyPhases.reduce((total, curr) => total + curr, 0) - dailyVMC;
    monthlyConsumption += dailyTotal;
  }
  else {
    console.info("One or more values are null for day " + day);
  }
}
console.info("Monthly total: " + monthlyConsumption);

This is a fairly straight forward translation of what you wrote. However, as @Udo_Hartmann correctly points out, averageSince gives the average of all the entries in the database from dayStart to today, not the average values on dayStart which is what it appears you want. averageBetween is what you probably want.

But then one wonders what does the average of the days summed together tell you. It’s not really the total consumption. It’s the sum of the daly averages of the consumption which I would not expect to match the total monthly consumption.

We could also get a little more clever (I kind of hinted at some of what’s possible if we can get these values into an array) with the JavaScript above to make the rule a little more terse.

var startOfMonth = time.toZDT("00:00").withDayOfMonth(1);
var days = Array.from({length: time.toZDT().dayOfMonth()}, (e, i)=> startOfMonth.plusDays(i); // Array of all the start times

// calculate the daily averages minus VMC
var totals = days.map (dayStart => {
  const dayEnd = dayStart.plusDays(1);
  const dailyPhases = [ items.HeatPumpPhase1.history.averageBetween(dayStart, dayEnd),
                        items.HeatPumpPhase2.history.averageBetween(dayStart, dayEnd),
                        items.HeatPumpPhase3.history.averageBetween(dayStart, dayEnd) ];
  const dailyVMC = items.VMCEnergy.history.averageBetween(dayStart, dayEnd);

  if(dailyPhases.includes(null) || dailyVMC === null) {
    return null;
  }
  else {
    return dailyPhases.reduce((total, curr) => total + curr, 0) - dailyVMC;
  }
}

// Log out the values calculated above
totals.forEach( (total, day) => {
  if(total === null) {
    console.info('Daily total for day " + day + ": " + total);
  }
  else {
    console.info("One or more values are null for day " + day);
  }
});

// Sum them up
var monthlyConsumption = totals.filter(total => total !== null).reduce((sum, curr) => sum + curr, 0)
console.info('Sumof averages is ' + monthlyConsumption);

I just typed in both of these so there is likely going to be a typo. I had to make some assumptions as well.

The cron trigger probably should be just once a day though. Or trigger when some Item(s) change.

1 Like

What does your items represent? Do they contain power (W), or energy (Wh)? I’m confused as to why you’re taking an average of them in order to come up with “daily consumption” (which I take it as average kWh per day).

If it’s energy, it’s usually an increasing counter, in which case you need to take a delta, not average.

1 Like

Thank you everyone first of all.

@rlkoshak tried to use the JS you mentioned but I was getting all sort of errors and then I noticed that although I am with OH 3.4.0.M3, the JS seems to run with ECMA - 262 Edition 5.1 (which is from 2011).

Now, I’m very afraid of updating something in a machine I am working remotely and where I only have physical access from time to time.

Each represent the register in my database of the Shelly 3EM for a 3 phase sensors. Here’s a very small example of one of those phases:

They contain power (W).

Now, regarding my old JS, how can I solve this? I reached this point:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

var date = new Date();
date.setDate(1);
var startOfMonth = date;
logger.info(startOfMonth.toString());
var monthlyConsumption = 0.0;

for(var day = 1; day <= date.getDate(); day++) {
  var dayStart = new Date(startOfMonth.getTime());
  dayStart.setDate(dayStart.getDate() + (day - 1));
  logger.info("Day: " + day + " - dayStart: " + dayStart);
  var dailyPhases = [items.HeatPumpPhase1.history.averageSince(dayStart),
                       items.HeatPumpPhase2.history.averageSince(dayStart),
                       items.HeatPumpPhase3.history.averageSince(dayStart)];
  var dailyVMC = items.VMCEnergy.history.averageSince(dayStart);
  logger.info("DailyPhases: " + dailyPhases);
  logger.info("DailyVMC: " + dailyVMC);

  if(!dailyPhases.includes(null) && dailyVMC !== null) {
    var dailyTotal = dailyPhases.reduce(function(total, curr) { return total + curr; }, 0) - dailyVMC;
    monthlyConsumption += dailyTotal;
  }
  else {
    logger.info("One or more values are null for day " + day);
  }
}
logger.info("Monthly total: " + monthlyConsumption);

But now I’m getting the error:

2024-01-21 16:55:31.570 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'MonthlyHeatPumpEnergy' failed: TypeError: Cannot read property "averageSince" from undefined in <eval> at line number 13

ecma 5.1 (helper library?) doesn’t support this syntax.

ok so HeatPumpPhaseX is for power (W), but what about VMCEnergy? If it contains energyy (Wh) it doesnt make sense to add/subtract power with energy. What does VMC stand for?

It doesn’t make sense to add up daily average power to come up with monthly “total power”.

You need to get the energy usage reading from shelly, not power.

I’ve just been reading a bit about shelly binding. Are these “HeatPumpPhaseX” linked to Shelly’s lastPower channel? The documentation is confusing and seemingly incorrect. It says “Energy consumption for a round minute, 1 minute ago” but its unit is WATT which is the unit of Power, not energy, so something weird is going on there. Either the unit is wrong, or the description is.

You need to get (and persist) the totalKWH and use deltaSince. In this case you don’t even need to add them up for monthly. You’d just call deltaSince(start of month)

1 Like

That’s Nashorn JS and moving to GraalVM JS which has a helper library and modern capabilities and syntax isn’t an upgrade. Both can live side by side on the same OH instance. They are completely separate rules engines.

Just install the JS Scripting add-on and you’ll have both as an option.

Either install the JS Scripting add-on or you’re going to have to get very familiar with:

Nashorn JS does not come with a helper library. There is a third party helper library but it’s not super complete, it’s awkward to install, and it actually doesn’t do a whole lot to abstract away from the raw Java JSR223 API.

It’s also deprecated so probably not worth spending a whole lot of time doing new development with.

But just to see the difference, here is the above rule in Nashorn JS. I’m just typing this in for demonstration purposes. It almost certainly has typos and errors.

// Indeed this is how to get the logger as `console` doesn't exist 
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

// We need to import some Java types because OH doesn't understand JavaScript
var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var PersistenceExtensions = Java.type('org.openhab.core.persistence.extensions.PersistenceExtensions');

// The PersistenceExtensions do not understand JS Dates, you have to use the Java ZonedDateTime
var now = ZonedDateTime.now();
var startOfMonth = now.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNanos(0);
logger.info(startOfMonth.toString());
var monthlyConsumption = 0.0;

for(var day = 1; day <= now.getDayOfMonth(); day ++) {
  var dayStart = startOfMonth.plusDays(day-1);
  logger.into('Day: ' + day + ' - dayStart: ' + dayStart.toString());
  // I don't think ECMAScript 5.1 has the same Array and Dict processing features, we can't use the trick from above
  var pase1 = PersistenceExtensions.averageSince(itemRegistry.getItem('HeatPumpPhase1'), dayStart);
  var pase2 = PersistenceExtensions.averageSince(itemRegistry.getItem('HeatPumpPhase2'), dayStart);
  var pase3 = PersistenceExtensions.averageSince(itemRegistry.getItem('HeatPumpPhase3'), dayStart);
  var dailyVMC = PersistenceExtensions.averageSince(itemRegistry.getItem('VMCEnergy'), dayStart);
  logger.info('Daily Phases: ' + phase1 + ' ' + phase2 + ' ' + phase3);
  logger.info('DailyVMC: ' + dailyVMC);

  if(phase1 === null || phase2 === null || phase3 === null || dailyVMC === null) {
    logger.info('One or more values are null for day ' + day);
  }
  else {
    var dailyTotal = phase1 + phase2 + phase3 - dailyVMC;
    monthlyConsumption += dailyTotal;
  }
}
logger.info('Monthly total: ' + monthlyConsumption);

If you ever do need to mess with something like the members of a Group, you’ll have to use Stream (Java SE 11 & JDK 11 ). You’ll be working with a Java List, not an JavaScript Array, so you need to use the Java APIs to work with it.

I only show the above to compare with the versions from above that use GraalVM JS Scripting. Not only can you stick to all JavaScript classes and Objects (except in extreme circumstances) but the more than 10 years since ECMAScript 5.1 was released has introduced a lot of great features to JS itself.

And I agree with and will reiterate @JimT’s comment. These calculations don’t make and sense at all.

1 Like

Thank you guys for all your help. Seriously, if it wasn’t for your great help, not only I would have not reached this conclusion but I would also be dumb enough to be mixing energy with power calculations.

Even worst than that, I was incorrectly performing calculations!

Anyway, my main focus in the first place was actually making the code run and only then I focused on having this retrieving the information successfully.

For whoever faces similar problems, here’s what I did:

I’ve installed the addon JSScripting.

Had to use a weird MQTT State Topic for the Shelly 3EM, otherwise I wouldn’t capture the correct energy consumption. This is it: shellies/[CUSTOM_MQTT_NAME]/emeter/0/energy (0 returns energy for phase 1, 1 for phase 2 and 2 for phase 3). In another Shelly that is counting reversed, I had to use “returned_energy” instead of “energy”.

Then, to get the current monthly consumption in Kwh, I added a new rule, selected the JS based on version=ECMAScript-2021 and then used this code:

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startMonth = ZonedDateTime.now().withDayOfMonth(1)
var now = ZonedDateTime.now()

let p1 = items.getItem('TeslaPhase1').history.sumBetween(startMonth, now);
let p2 = items.getItem('TeslaPhase2').history.sumBetween(startMonth, now);
let p3 = items.getItem('TeslaPhase3').history.sumBetween(startMonth, now);

items.getItem('TeslaCurrentMonthkWh').sendCommand((p1+p2+p3)/60000);

To calculate for the last month, I used this one:

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startDate = ZonedDateTime.now().minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)
var endDate = ZonedDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)

let p1 = items.getItem('TeslaPhase1').history.sumBetween(startDate, endDate);
let p2 = items.getItem('TeslaPhase2').history.sumBetween(startDate, endDate);
let p3 = items.getItem('TeslaPhase3').history.sumBetween(startDate, endDate);

items.getItem('TeslaLastMonthkWh').sendCommand((p1+p2+p3)/60000);

To calculate the current year I used this:

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startYear = ZonedDateTime.now().withMonth(1).withDayOfMonth(1)
var now = ZonedDateTime.now()

let p1 = items.getItem('TeslaPhase1').history.sumBetween(startYear, now);
let p2 = items.getItem('TeslaPhase2').history.sumBetween(startYear, now);
let p3 = items.getItem('TeslaPhase3').history.sumBetween(startYear, now);

items.getItem('TeslaCurrentYearkWh').sendCommand((p1+p2+p3)/60000);

And finally, to calculate the last year, I used this one:

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startLastYear = ZonedDateTime.now().minusYears(1).withMonth(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)
var endLastYear = ZonedDateTime.now().minusYears(1).withMonth(12).withDayOfMonth(31).withHour(23).withMinute(59).withSecond(59)

let p1 = items.getItem('TeslaPhase1').history.sumBetween(startLastYear, endLastYear);
let p2 = items.getItem('TeslaPhase2').history.sumBetween(startLastYear, endLastYear);
let p3 = items.getItem('TeslaPhase3').history.sumBetween(startLastYear, endLastYear);

items.getItem('TeslaLastYearkWh').sendCommand((p1+p2+p3)/60000);

And this is it. It’s finally working! :slight_smile:

Note that I had to divide by 60000 because these devices register watts per minute (which is very weird…).

You really should not buy using java.time.ZonedDateTime in a JS Scripting rule. That’s why the helper library provides time.

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startMonth = ZonedDateTime.now().withDayOfMonth(1)
var now = ZonedDateTime.now()

should be replaced with

var start month = time.toZDT().withDayOfMonth(1);
var now = time.toZDT();

And there are conversation factors but into the helper library to simplify things too. For example

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startDate = ZonedDateTime.now().minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)
var endDate = ZonedDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)

can be simplified to

var startDate = time.toZDT('00:00').minus months(1). withDayOfMonth(1);
var endDate = time.toZDT('00:00').withDayOfMonth(1);

and

var ZonedDateTime = Java.type('java.time.ZonedDateTime');
var startLastYear = ZonedDateTime.now().minusYears(1).withMonth(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)
var endLastYear = ZonedDateTime.now().minusYears(1).withMonth(12).withDayOfMonth(31).withHour(23).withMinute(59).withSecond(59)

can be simplified to

var startLasyYear = time.toZDT('00:00').withDayOfYear(1).minus years(1);
var endTastYeat = time.toZDT('23:59:59').minus years(1).with month(12).withDayOfMonth(31); // can't use dayOfYear because of leap years

You get the idea. Definitely look at the docs for time.toZDT() as it can convert almost anything reasonable to a ZonedDateTime.

1 Like

Even simpler:

var startDate = time.toZDT('00:00').minusMonths(1).withDayOfMonth(1);
var endDate = startDate.plusMonths(1).minusSeconds(1);
var startLastYear = time.toZDT('00:00').withDayOfYear(1).minusYears(1);
var endLastYear = startLastYear.plusYears(1).minusSeconds(1);

You can probably omit the minusSeconds(1) if you don’t care about one second difference.

Also a heads up, persistence in core will return a QuantityType (subject to a PR being merged) when applicable, so your calculation will need to be adjusted.

2 Likes

I just discovered that this endpoint was actually NEVER giving any energy readings:

shellies/shellyem3-<deviceid>/emeter/<i>/energy

So I noticed in their docs that they do keep track of “energy totals” in Wh and then they have an endpoint to reset totals. Meaning, I don’t even need to do much calculations myself nor care about precision and whatnot, they do it already in their devices!

With this, we can have very simple rules in OH to get those totals and then work with them however we want to.

The above is very useful for those cases where the device only tells us how much energy is being used, but since in this specific case they do keep memory of the totals, then we might as well just use it and save on complexity! :slight_smile:

Anyway, thank you so much for this journey, I learned a lot about different things. It’s always great to know more about OH and how it works, so we can take as much advantage of it as possible.

I was wondering about that. It’s so much easier to calculate that on the device than it is to estimate it from reported instantaneous W readings.

I have a whole house energy meter and use the following rules to estimate my monthly power bill that works the same way. It counts up the kWh and I send a command to reset it at the end of the month. You or other visitors to this thread may find something useful in it. (Note I use the new Number:EnergyPrice unit).

Reset the meter on the first day of the billing month

configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: 0 0 0 9 * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        console.info("It's the first day of the electricity billing period, time to reset the running total.");


        items[HomeEnergyMeter_LastMonthBillEstimate].postUpdate(items.getItem('HomeEnergyMeter_CurrentBillEstimate').state);

        items[HomeEnergyMeter_ResetMeter].sendCommand('ON');

        items[HomeEnergyMeter_CurrentBillEstimate].postUpdate(items.getItem('HomeEnergyMeter_Access').state);
    type: script.ScriptAction

Calculate the bill based on the usage up to this point. Note that I have to pay an access fee and my per kWh rate does not vary.

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: HomeEnergyMeter_ElectricmeterkWh
    type: core.ItemStateChangeTrigger
  - id: "2"
    configuration:
      itemName: HomeEnergyMeter_Access
    type: core.ItemStateChangeTrigger
  - id: "3"
    configuration:
      itemName: HomeEnergyMeter_Rate
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "4"
    label: Calculate the power bill and update the Item
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >-
        var curr     = items.HomeEnergyMeter_ElectricmeterkWh.quantityState;

        var rate     = items.HomeEnergyMeter_Rate.quantityState;

        var access   = items.HomeEnergyMeter_Access.quantityState;

        var usage = curr.multiply(rate);

        var estimate = access.add((curr.multiply(rate)));

        items.HomeEnergyMeter_CurrentBillEstimate.postUpdate(estimate);
    type: script.ScriptAction

Luckily, my power meter retains the last reading even in a power outage. If yours does not you’ll need a bit more logic to identify when it returns to zero but you need to keep track of the last value, or restart your calculations.

1 Like