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!
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.
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)
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)
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.
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)