Auto stateDescription rule for UoM

Here is a handy little javascript rule to auto update the state description of items.
I use this for power and energy readings from solar PV system.

This is just the one for Energy (Wh, kWh, MWh etc). Duplicate the rule and change the UoM for Power and other UoMs.

It can take a second or two to update when it happens, so there can be a bit of a lag as it switches.

rules.JSRule({
    name: "Update Energy stateDescription pattern",
    description: "",
    triggers: [
      triggers.GroupStateChangeTrigger('groupItemContainingEnergyItems', undefined, undefined),
      triggers.ItemStateChangeTrigger('groupItemContainingEnergyItems', undefined, undefined),
    ],
    execute: (event) => {
      let itemName = items.getItem(event.itemName)
      let newData = Quantity(event.newState).toUnit('Wh')
  
      let Wh = '%.0f Wh'
      let kWh = '%.1f kWh'
      let MWh = '%.2f MWh'
      let GWh = '%.2f GWh'
  
      let meta = JSON.stringify(items.getItem(itemName.name).getMetadata("stateDescription")).toString()
    
      let newUnit;
      if (newData.greaterThan('1000 Wh') && newData.lessThan('1000000 Wh')) {
        newUnit = kWh
      } else if (newData.greaterThan('1000000 Wh') && newData.lessThan('1000000000 Wh')) {
        newUnit = MWh
      } else if (newData.greaterThan('1000000000 Wh')) {
        newUnit = GWh
      } else {
        newUnit = Wh
      }
      
      if (!meta.includes(newUnit)) {
        console.info("Updating stateDescription pattern for " + itemName.name + " to " + newUnit.toString().split(" ")[1] )
  
        items.getItem(event.itemName).replaceMetadata('stateDescription', '', { pattern: newUnit } )
      } 
    }
  });
6 Likes

This is a great candidate for a rule template. You could make the unit a property and then the users could just instantiate and configure the rules as needed.

I think there could be a way to make it completely generic but it would require the unit if the Item to be the lowest level (e.g. it would already have to be Wh).

    let newData = Quantity(event.newState);
    let unit = new data.getUnit();
  
      let Wh = '%.0f ' + unit;
      let kWh = '%.1f k' + unit;
      let MWh = '%.2f M' + unit;
      let GWh = '%.2f G' + unit;

So there is a compromise either way. You’ll either need multiple rules or have to fix your Item’s unit to Wh.

This is because the new state description won’t actually apply until the Item gets updated. Because the rule runs after the update there’s a delay.

However, you could put this into a script transform profile. Then the metadata will be updated before the Item gets updated and therefore it will immediately apply.

There are lots of little adjustments anyone can make here to fit you needs. It’s a great example! Thanks for posting!

Is it possible to get the Number type for an item? items.xxxx.type gets ‘NumberItem’, but not the :Power or :Energy bits

Quantity has getDimension() which does that. I don’t know if it’s documented but it’s in the code. quantity.js - Documentation (go down to around line 122).

Quantity(items.getItem(‘PowerW_Item’).state).getDimension() gets “TypeError: (intermediate value)(…).getDimension is not a function”

Quantity(items.getItem(“PowerW_Item”).state).rawQtyType.getDimension()
or
Quantity(items.getItem(“H1_SolarEdge_AggregateDay_Consumption”).state).dimension
gets “[L]²·[M]/[T]³”

Energy (Wh) items get “[L]²·[M]/[T]²”

The doc says :

  • Dimension of this Quantity, e.g. [L] for metres or [L]² for cubic-metres

so I shouldn’t be seeing [L]??? Never mind what M and T are supposed to be…

I expected you to see “Energy” based on the docs. But maybe I’m going about this wrong and the dimension of the QuantityType is different from the dimension of the NumberType, which also has a getDimension() method: NumberItem (openHAB Core 4.2.0-SNAPSHOT API)

Try

items.PowerW_Item.rawItem.getDimension()

Note, in all the examples you used above you’ve created a new Quantity which is not necessary. Use quantityState to get the state of the Item as a Quantity instead of creating a new one yourself.

items.PowerW_Item.quantityState

I liked the idea of a transform instead of rules.

I haven’t figured out the dimension thing properly yet, but there is enough of a difference between energy and power ‘.dimesion’ that this works for now.

This works for incoming W, kW, Wh, kWh etc

I tried to keep it in quantities originally, but ended up going numbers as soon as possible for any maths and comparisons. Not sure which would be the tidiest/most robust; numbers or quantities.

(function(data) {

  function roundTo1(input) {
    return Math.round(input * 10) / 10;
  }

  function roundTo2(input) {
        return Math.round(input * 100) / 100;
  }

    let newData = Quantity(data)
    let dimension = newData.dimension

    if (dimension == "[L]²¡[M]/[T]²") {
        newData = Quantity(newData).toUnit("Wh")
    } else if (dimension == "[L]²¡[M]/[T]³") {
        newData = Quantity(newData).toUnit("W")
    }

    let dataNumber = newData.float

    let unit = newData.symbol;

    let xUnit =  Math.round(dataNumber) + " " + unit
    let kxUnit = roundTo1(dataNumber / 1000) + " k" + unit
    let MxUnit = roundTo2(dataNumber / 1000000) + " M" + unit
    let GxUnit = roundTo2(dataNumber / 1000000000) + " G" + unit

    let output;
    
    if (dataNumber >= 1000 && dataNumber < 1000000) {
        output = kxUnit
      } else if (dataNumber >= 1000000 && dataNumber < 1000000000) {
        output = MxUnit
      } else if (dataNumber >= 1000000000) {
        output = GxUnit
      } else {
        output = xUnit
      }

    return output.toString()
  })(input)

Then apply to an item like this

Number:Power Power_Item "Power Item [JS(autoStateDescription.js):%s]" ["Measurement", "Power"] { binding }

1 Like

With a JS transform like that, you could pass in the base unit as a argument.

Note, I find for transformations the whole (function(data)... proforma stuff to be pointless. It’s purpose is to protect against something that is never going to be a problem in an OH transformation. However, it does mean you can’t use let or const if you don’t have it.

Also, you can use toFixed() to round to 1/2 decimal places instead of the functions.

var newData = Quantity(input).toUnit(baseUnit);
var dataNumber = newData.float
var unit = newData.symbol;

var xUnit =  Math.round(dataNumber) + " " + unit
var kxUnit = (dataNumber / 1000).toFixed(1) + " k" + unit
var MxUnit = (dataNumber / 1000000).toFixed(2) + " M" + unit
var GxUnit = (dataNumber / 1000000000).toFixed(2) + " G" + unit

var output;
    
if (dataNumber >= 1000 && dataNumber < 1000000) {
  output = kxUnit
} else if (dataNumber >= 1000000 && dataNumber < 1000000000) {
  output = MxUnit
} else if (dataNumber >= 1000000000) {
  output = GxUnit
} else {
  output = xUnit
}

output.toString()

or, because I like to stay working inside Quantity when I have Quantity to begin with:

// Create some constants to compare to to get the scale modifier
var KILO = Quantity("1000 " + baseUnit);
var MEGA = Quantity("1000000 " + baseUnit);
var GIGA = Quantity("1000000000 " + baseUnit);

// Get the value in the base unit
var newData = Quantity(input).toUnit(baseUnit);

var scale = "";
if(newData.greaterThanOrEqual(GIGA)) {
  scale = "G";
} else if(newData.greaterThanOrEqual(MEGA)) {
  scale = "M";
} else if(newData.greaterThanOrEqual(KILO)) {
  scale = "k";
}

// Leave the rounding to the state description
newData.toUnit(scale+baseUnit);

or if you really want to do the rounding in the transform

// Create some constants to compare to to get the scale modifier
var KILO = Quantity("1000 " + baseUnit);
var MEGA = Quantity("1000000 " + baseUnit);
var GIGA = Quantity("1000000000 " + baseUnit);

// Get the value in the base unit
var newData = Quantity(input).toUnit(baseUnit);

if(newData.greaterThanOrEqual(GIGA)) {
  scale = "G";
} else if(newData.greaterThanOrEqual(MEGA)) {
  scale = "M";
} else if(newData.greaterThanOrEqual(KILO)) {
  scale = "k";
}
var newUnit = scale + baseUnit;

var newValue = newData.toUnit(newUnit).float;
if(scale == "k") newValue = newValue.toFixed(1);
else if(["M", "G"].includes(scale)) newValue = newValue.toFixed(2);

Quantity(newValue, newUnit).toString();

Now where does baseUnit come from? You can pass it in the call to the transform.

JS(autoStateDescription.js?baseUnit='Wh')

This will allow the same transform to work for any unit with the relatively small cost of needing to pass in the base unit.

Although I would also like to stay in quantities when possible, my number version doesn’t require a passed variable. It might sound like a small thing, but it’s just another tiny piece to get wrong or lost or forgotten.

I think toFixed only chops off the end of the decimal places. But I have a nicer function now to replace my original two to make it a little easier to swallow, and allows for easier changing of the decimal places (or with an item or something)

function round(input, points) {
    return Math.round(input * Math.pow(10, points)) / Math.pow(10, points);
  }
let xUnit =  Math.round(dataNumber) + " " + unit
let kxUnit = round(dataNumber / 1000, 1) + " k" + unit
let MxUnit = round(dataNumber / 1000000, 2) + " M" + unit
let GxUnit = round(dataNumber / 1000000000, 2) + " G" + unit

I’m not sure how to apply getDimension() to the data input without adding a passed variable (the item name) to be able to try that.
I’m not sure what the implications are for using .dimension, given that it works in this form.

Thanks for the input by the way

No, it rounds. Number.prototype.toFixed() - JavaScript | MDN

The toFixed() method returns a string representation of a number without using exponential notation and with exactly digits digits after the decimal point. The number is rounded if necessary, and the fractional part is padded with zeros if necessary so that it has the specified length.

I’m not certain how the zero padding is handled though. Since it’s going to become a Java number eventually I think that zero padding part gets lost.

But the fact that it’s a String doesn’t matter because Quantity will parse it back to a number.

Because the .dimension from the Quantity doesn’t really obviously map to anything and event doesn’t get passed into a transform so you can’t get at event.itemName I think your choices are:

  • pass something to the transform (Item name, the base unit itself, etc), I figure if you have to pass something, you may as well pass the base unit. And even if you have the Item, there’s no guarantee that .getDimension() from the Item is going to be particularly useful actually. You’d still have to map that to the base unit.
  • create a mapping and a big switch statement to go from the inscrutable [L]²¡[M]/[T]² dimension to the base unit
  • only support one or two units and no more.

I chose the first option above because in order to support other units (e.g. data rate, distance, speed, etc.) the end user will have to edit the transform and it’s less risky and easier to have them change how they call the transform than it is to have them modifying the transform or having multiple copies of the same transform floating around.

Seems I was wrong, but you are right about the extra zeros. my round function drops any extra zeros.

I’m mostly sold on option 1, but I think this would need more in it to make it work for other UoM so maybe not worth it. I don’t think a MegaMetre is a very common or wanted display? I can’t think of any other units I might use that would work in quite the same way as Energy and Power do like this. And the ones that do can probably be hardcoded into the script as I have done for W and Wh.

That would probably need to be controlled through additional optional arguments. But Mega does make sense for data amount, data rate, various electric and energy units, frequency, power, pressure, and radiation related units. Though for the data units the binary prefixes might be preferred to the base 10 ones.

It would definitely make use more complicated, but making something generic always is more complicated because there are so many edge cases. But one could pass in the list of the prefixes one wants to use as an argument (e.g. JS(autoStateDescription.js?baseUnit='Wh',scales='k,M,G')) and then set up the if statements to check for only those (e.g. if(scales.split(',').includes('G') && newData.greaterThanOrEqual(GIGA))). If the if statements are ordered from largest to smallest you can cover all the units including the small units like milli.

It would be tedious but very doable. This would even support the binary prefixes.

I don’t know if it’s worth doing but this could definitely be made such that it supports everything in the tables in the UoM docs. If it were possible to publish a transform like a rule template that would definitely be worth considering doing. Note I think I have opened an issue to add that feature but there have been no takers thus far to implement it. As a copy and paste rule, maybe it’s worth doing, maybe not.

There are a lot of units to test for (see above) and new units being added all the time. Without doing the mapping not only does it work for all supportable units, it works for all future supportable units too without modification.

This is all great.
I’ve had a look through the UoM list and if I’m measuring MegaVolts or MegaMeter winds I have bigger problems!!! So I’m going to stick with the hardcoded, no passed variable option myself. Easier to maintain in my items files for me.

I did a few tidy ups. Here is my code for anyone who wants it

new file: autoStateDescription.js in conf/transform

(function(data) {

  function round(input, points) {
    return Math.round(input * Math.pow(10, points)) / Math.pow(10, points);
  }

  let newData = Quantity(data)
  let dimension = newData.dimension

  if (dimension == "[L]²¡[M]/[T]²") {
      newData = newData.toUnit("Wh")
  } else if (dimension == "[L]²¡[M]/[T]³") {
      newData = newData.toUnit("W")
  }

  let dataNumber = newData.float
  let unit = newData.symbol;

  let xUnit =  Math.round(dataNumber) + " " + unit
  let kxUnit = round(dataNumber / Math.pow(10, 3), 1) + " k" + unit
  let MxUnit = round(dataNumber / Math.pow(10, 6), 2) + " M" + unit
  let GxUnit = round(dataNumber / Math.pow(10, 9), 2) + " G" + unit
  let TxUnit = round(dataNumber / Math.pow(10, 12), 2) + " T" + unit

  let output;
  
  if (dataNumber >= Math.pow(10, 3) && dataNumber < Math.pow(10, 6)) {
    output = kxUnit
  } else if (dataNumber >= Math.pow(10, 6) && dataNumber < Math.pow(10, 9)) {
    output = MxUnit
  } else if (dataNumber >= Math.pow(10, 9) && dataNumber < Math.pow(10, 12)) {
    output = GxUnit
  } else if (dataNumber >= Math.pow(10, 12)) {
    output = TxUnit
  } else {
    output = xUnit
  }

  return output.toString()

})(input)

Item file definition usage:

Number:Power Power_Item "Power Item [JS(autoStateDescription.js):%s]" ["Measurement", "Power"] { binding }
Number:Energy Energy_Item "Energy Item [JS(autoStateDescription.js):%s]" ["Measurement", "Energy"] { binding }
2 Likes