Working with Number:Temperature Items in rules

openHAB provides Quantity Type features, which can seem baffling. On top of that, Temperature type Quantities have a few quirks all of their own. These notes intend to guide the rules author.

I’ve shown work examples in DSL rules language, see below for other scripts examples. All have similar features, if different syntax. Knowing what can be done is half the battle.

What is a Quantity Type?

Simply, Item states and rule variables may contain Quantities - that just means a number AND a unit of measurement. Taken together, this is treated as a single package.
Examples; 15km, 21.5°C
Items are defined as to what kind of Quantity - Number:Length, Number:Temperature etc.
Each type of Quantity can generally be expressed using a selection of different units.
But variables can often be used just as general Quantities, without specifying type. You would know what kind of variable you are dealing with in your rule.

The examples below will use these pre-defined Items -

Number:Temperature tempItemC, with a state of 10°C
Number:Temperature tempItemF, with a state of 20°F
Number:Temperature tempItemK, with a state of 285K (Kelvins, note absence of ° degree symbol in this SI unit)

What’s in my Quantity flavour Item?

Let’s find out

logInfo("testlog", "state contains " + tempItemC.state.toString)
 // logs 'state contains 10 °C'

Note, the framework adds a space separator to the string version.

First quirk

Updating a set of mixed-unit Items as above is not as easy as it looks.

logInfo("testlog", "state contains " + tempItemF.state.toString)
 // logs 'state contains -6.6666666666666666666666666666667 °C'

What’s gone on here? This was updated with 20°F, but it’s been silently converted to °C.
The problem is that the Item had no state presentation information, so the framework has used the system default unit (°C for me) during the state update.
The trick is to give the Item a state presentation, with a unit, and that will become default for this particular Item.
In the old Item files we would use -

Number:Temperature tempItemF "my label [%d °F]" ...

but now you’d edit state presentation in the UI.
Now, when we update again with 20°F -

logInfo("testlog", "state contains " + tempItemF.state.toString)
 // logs 'state contains 20 °F'

How do I make a Quantity Type variable or constant in a rule?

var fred = 5|°C   // the pipe character makes it a Quantity

How do I update my Quantity type Items?

// you can post Quantities directly
var fred = 5|°C
tempItemC.postUpdate(fred) // result 5°C
tempItemF.postUpdate(fred) // result 41.00°F
  // remember the framework auto-converts to Item default units

//you can post strings
tempItemC.postUpdate("15   °C")  // result 15°C
tempItemC.postUpdate("25°C")  // result 25°C
  // framework parser is not fussy about spaces
tempItemC.postUpdate("25 °K") // error!
  // parser IS picky about units - no ° with K, remember

// you can try your luck with ordinary numbers
tempItemC.postUpdate(35) // result 35.0°C
tempItemF.postUpdate(35) // result 35.0°F
  // NOT recommended - you might not get what you expect

Next quirk

You (or a linked channel) can update a plain Number Item with a Quantity Type value with a unit … but this Item will NOT be magically transformed into a “real” Quantity Item, and features like conversions will give unexpected results. Avoid doing this, and take care configuring channel links and types, especially if you use a transformation.
Similar applies to Number type variables, which may hold a quantity value but are not fully featured Quantity Types.

Comparisons in rules

Often we want to test “is it hotter than, for example, 5°C”?

if (tempItemC.state > 5|°C)
  // returns true, 10°C is hotter than 5°C

Because the state holds a Quantity, we must give the second value as a Quantity as well. Otherwise, we would be trying to compare apples and oranges.

But what if our sensor is sending °F to the Item? That’s fine, we don’t care, the openHAB framework takes care of conversions for us here and it works. Apples and oranges is about using similar Quantity type, not about the units.

if (tempItemF.state > 5|°C)
  // returns false, 20°F is colder than 5°C
      // of course, we can compare two Items
if (tempItemC.state > tempItemF.state)
 // returns true, 10°C is hotter than 20°F
     // but validators may complain here, as they cannot guess until runtime what sort of Quantity the state is.
if ( (tempItemC.state as QuantityType<Temperature>) > (tempItemF.state as QuantityType<Temperature>) )
 // returns true, 10°C is hotter than 20°F
      // spelling types out, just to satisfy the validator

Converting to plain ordinary Numbers in rules

Some people get frustrated with wrestling with Quantities, give up, and convert everything to ordinary numbers to work with.
That’s okay, but you must take care to see that the Quantities that you start with are in the units that you expected. That part is up to you.

// the obvious thing to try, but ...
var numericC = (tempItemC.state as Number)
  // returns 10°C
      // That's no use here - remember Quantities "fit" into the Number type.  Avoid that.
var numericC = (tempItemC.state as QuantityType<Number>).toBigDecimal 
 // returns 10
     // That's better, just the numeric.  (BigDecimal is the underlying type for Number)
 // But remember you are really just hoping the state was in the wanted units to begin with.
var numericF = (tempItemF.state as QuantityType<Number>).toBigDecimal
  // returns 20 - no use if our maths is assuming °C

EDIT - fixed up an error here

We can however force a conversion to the units of our choice, and then we will know what we are dealing with - instead of just hoping.

var numericToC = (tempItemF.state as QuantityType<Temperature>).toUnit("°C").toBigDecimal
 // returns -6.6666666666666666666666666666667
      // we have converted °F to °C and got the numeric part of the °C

If you need to actually know what units the Item state came with (perhaps you want to do your own conversion)

var units = (tempItemF.state as QuantityType<Temperature>).getUnit.toString
 // returns "°F"

Mathematics in rules

Here are more serious quirks. It’s easy to get unexpected answers.
2°C + 1°C
Result 3°C? This seems simple enough, but in fact it is ambiguous in meaning.
Here it is again, written with exactly the same temperatures expressed in Kelvin (rounded).
275K + 274K
Result 549K? Meaning 276°C? What happened there, we used the same temps?
Well, we can also write 2°C + 1°C as
275K + 1K
and get a different result 276K, equivalent 3°C.
Here we’ve treated the second value as an increment or interval, not as an absolute temperature reading.
The operation is open to interpretation.

The rules engine cannot tell if we meant to sum two absolute temperatures, or if we meant to add an increment to an absolute temperature.
We might be turning up the thermostat a little, or we might be trying to sum-then-divide to get an average temperature, so they are both valid things to do with the + sign.

openHAB has to do one or the other, and in reality it treats all Temperature types as absolute.
This means that we can for example get averages
(2°C + 1°C) / 2 , result 1.5°C
(275K + 274K) / 2 , result 274.5K (equivalent 1.5°C)
Good, that’s consistent.
This consistency means we can exploit Quantity conversion, and mix units.
(2°C + 274K) / 2 , result 1.5°C
(275K + 1°C) / 2 , result 274.5K (equivalent 1.5°C)
Note that the result uses the units of the first Quantity found, but as we’ve seen we can always force units of our choice.

//.  Three versions of the same averaging -
var avg = ( (tempItemC.state as QuantityType<Temperature>) + (tempItemF.state as QuantityType<Temperature>) ) / 2 
 // returns 1.66666666666666666666666666666665 °C
var avgC = (( (tempItemC.state as QuantityType<Temperature>) + (tempItemF.state as QuantityType<Temperature>) ) / 2).toUnit("°C") 
 // returns 1.66666666666666666666666666666665 °C
var avgF = (( (tempItemC.state as QuantityType<Temperature>) + (tempItemF.state as QuantityType<Temperature>) ) / 2).toUnit("°F") 
 // returns 34.9999999999999999999999999999998 °F
     // we have to spell out the type for this action to work properly

Wait a minute, how then DO I increment a temperature??

The magic of maths means that
2°C + 1°C
always returns 3°C, whether we meant it as an absolute sum or as an increment.
(Complicated stuff about both shared points of origin and unit step size)
But the trick for incrementing (or decrementing) is -
Always use the same units throughout.

var incC = var incC = (tempItemC.state as QuantityType<Temperature>) + 1|°C 
 // returns 11°C
    // Easy, but really we're just hoping the Item is in °C units.
    // But look here, for 20°F source -
var incX = (tempItemF.state as QuantityType<Temperature>) + 1|°C 
 // returns 53.80 °F - it's all gone wrong!
 // 1°C was converted to 33.8°F "absolute", not 1.8°F "interval"
 // remember openHAB always works in absolutes

    // to be safe, we should force source units to match the increment
var incU = (tempItemF.state as QuantityType<Temperature>).toUnit("°C") + 1|°C
 // returns -5.6666666666666666666666666666667 °C , that's better

// but I want the output in the same units as the source Item, whatever they were.
var incA = ( (tempItemF.state as QuantityType<Temperature>).toUnit("°C") + 1|°C).toUnit((tempItemF.state as QuantityType<Temperature>).getUnit)
 // returns 21.8 °F
    // must work in same units as increment, but can convert back afterwards

Fun with maths
Alright, that was the “proper” way to do increments or decrements with mixed units.
Here’s a mathematician’s cheat that saves some typing. Just subtract zero.
Yes, really. After the wanted increment, subtract zero - but expressed in the same units as the increment.
X + 2°C - 0°C
Works with any units of X

var incMC = (tempItemC.state as QuantityType<Temperature>) + 2|°C - 0|°C // results 12°C
var incMF = (tempItemF.state as QuantityType<Temperature>) + 2|°C - 0|°C // results 23.60°F
var incMK = (tempItemK.state as QuantityType<Temperature>) + 2|°C - 0|°C // results 287.00K

Advanced Maths

Well, not that advanced.
Setting aside what multiplying a temperature actually means in physics terms, the calculation does what you would expect.

var multC = (tempItemC.state as QuantityType<Temperature>) * 2 // results 20°C
// multiplied
var divK = (tempItemC.state as QuantityType<Temperature>).toUnit("K") / 2 // results 141.575 K
// converted to 283.15K, then divided
var nonsenseC = (tempItemC.state as QuantityType<Temperature>) * 2|°C // results 20 °C²
// temp x temp - well, its done its best, but the idea is a nonsense
// so we get a fairly meaningless result

For completeness, we should look at other Quantity types, like Length.
Here X is a Number:Length Item with state 2m

var addX = (X.state as QuantityType<Length>) + 2|in // results 2.0508m
// as you'd expect, a simple add with auto units conversion of inches
var multX = (X.state as QuantityType<Length>) * 2 // results 4m
// again, straightforward multiply length x number
// but wait ...
var areaX = (X.state as QuantityType<Length>) * 2|m // results 4m²
// length x length = area, this time it has meaning
// framework has understood and result is an Area type Quantity
// with appropriate units

var gradC = (tempItemC.state as QuantityType<Temperature>) / (X.state as QuantityType<Length>)  // results 5°C/m
// wow, we've invented the Temperature Gradient quantity type
// even if there is no formal Item type for that
var totalC = gradC * 10|m  // results 50 °C
// and it really works

String formatting

I want “21.3°C” in my message string, not “21.28478 °C”

var message = "Temperature is " + (tempItemC.state as QuantityType<Temperature>).format("%.1f%unit%")
// returns "Temperature is 10.0°C"
// uses usual Java formatter control string

Dimensionless

A brief word about the Number:Dimensionless quantity type.
No, it is not a ‘regular’ number without units. It represents a proportion or ratio. As such, it does have units. Typically, those might be % or ppm or dB.
So, when used for a Temp and Humidity sensor measurement, you’ll often find your Number:Temperature has a companion Number:Dimensionless for Relative Humidity, with units of %.

Number:Dimensionless has one quirk of its own; it can also be expressed in units of ONE. The meaning there is for a ratio x-to-1, for example 70% is the same as “0.7 to 1”.

The quirk is that the ONE unit can be invisible. If you update a Number:Dimensionless type with a plain number, 123, that is perfectly acceptable to the framework and it will be treated as a ratio.
Beware if you later display or chart your quantity as a %, you will see 12300% which is the same ratio expressed as percent. This is quite a common pitfall.

19 Likes

For the most part the Python code will use the same over all approach as JavaScript. Get the state sing items and create QuantityTypes for constants (no | notation is available). For example,

var incC = items["tempItemC"] + new QuantityType("1 °C");

in JavaScript would be

incC = items["tempItemC"] + new QuantityType("1 °C")

in Python.

JavaScript

logger.info("state contains " + items["tempItemF].toString());

JavaScript

if(items["tempItemC.state"] > new QuantityType("5 °C")) {


if(items["tempItemC"] > items["tempItemF"]){
// JavaScript is much better at casting correctly so you don't need the `as` example.

JavaScript

var numericC = items["tempItemC"].as(DecimalType); // I don't know for sure that works
var numericC = items["tempItemC"].toBigDecimal();
var numericC = items["tempItemC"].intValue(); // floatValue and doubleValue are also supported
var numericF = items["tempItemF"].toBigDecimal();

var numericToC = items["tempItemF"].toUnit("°C").toBigDecimal(); 

JavaScript

var units = items["tempItemF"].getUnit().toString();

JavaScript

var avg = (items["tempItemC"] + items["tempItemF"]) / 2;
var avgC = ((items["tempItemC"] + items["tempItemF"]) / 2).toUnit("°C");
var avgF = ((items["tempItemC"] + items["tempItemF"]) / 2).toUnit("°F");

JavaScript

var incC = items["tempItemC"] + new QuantityType("1 °C");
var incX = items["tempItemF"] + new QuantityType("1 °C");
var incU = items["tempItemF"].toUnit("°C") + new QuantityType("1 °C");
var incA = (items["tempItemF"].toUnit("°C") + new QunatityType("1 °C")).toUnit(items["tempItemF"].getUnit());

JavaScript

var incMC = items["tempItemC"] + new QuantityType("2 °C") - new QuantityType("0 °C");
var incMF = items["tempItemF"] + new QuantityType("2 °C") - new QuantityType("0 °C");
var incMK = items["tempItemK"] + new QuantityType("2 °C") - new QuantityType("0 °C");

JavaScript

var message = "Temperature is " + items["tempItemC"].format("%.1f%unit%");

Thanks for posting. I think this is an excellent candidate for inclusion in the docs.

4 Likes

Thank you, Rich.
I think I’ll re-jig the leading post for DSL only, and we can have separate posts for separate languages for sanity’s sake.

If you do include this into the official docs, which I would encourage, perhaps as part of the “manipulating states in rules” section, @Confectrician has come up with a way to have a tabbed table (I think I remember seeing that) so we can list examples in different languages inline, kind of like the Helper Library docs do.

The concepts are all the same, it’s just the syntax that is different. For this post, maybe I can clean up my post and you can just point to my reply for the JS examples.

I am trying to apply this to jython and get errors every time.
if (items[‘sensorLibrary_Temperature’] > 21|°C)
throws
Script execution of rule with UID ‘room_temps’ failed: SyntaxError: no viable alternative at character ‘°’

if(items["tempItemC.state"] > new QuantityType("5 °C"))

shows error as well

This useful shorthand is only available in DSL.

I think you need the jython library extension for this?

1 Like

[quote=“rossko57, post:6, topic:116197”]
his?
[/quote]no big deal, I can use:
if (items[‘sensorLibrary_Temperature’].floatValue > 20.0)

but again, when I try unit conversion:
items[‘sensorLibrary_Temperature’].toUnit(" °F")
I get error
Script execution of rule with UID ‘room_temps’ failed: javax.measure.format.ParserException: javax.measure.format.ParserException: Parse Error in at line number

what am I doing wrong?

It’s hard to tell but I don’t think toUnit can handle the leading space

"°F"

as opposed to

" °F"

thanks Rich, I mistakenly posted with leading space. In fact I did quite many tries of various versions, without leading space gets the same error.

by the way, converting to Kelvin works:

items[‘sensorLibrary_Temperature’].toUnit(“K”)

items[‘sensorLibrary_Temperature’].toUnit(items[‘sensorLibrary_Temperature’].getUnit())
works as well

Sounds like you’ve got the wrong ° character.

I do not think so - I copied the character from log, so it should be it

As a beginner I am not shure about the use of units.
I try to integrate a Powerwall and an inverter of different companies (Tesla Powerwall, Fronius Symo):
Using your example:

logInfo("PW", "PW_Haus: " + PW_Home.state.toString)
logInfo("FSY", "FSY_Load: " + FroniusSymoInverter_LoadPower.toString)

gives

2021-02-28 19:56:25.405 [INFO ] [org.openhab.core.model.script.PW    ] - PW_Haus: 0.30129491788679763 kW

2021-02-28 19:56:25.424 [INFO ] [org.openhab.core.model.script.FSY   ] - FSY_Load: FroniusSymoInverter_LoadPower (Type=NumberItem, State=-11.78, Label=FSY Load Power, Category=, Tags=[Point])

So: If I want to work with these I have to convert both to numbers, multiplying the PW power by 1000?

home
load
I understand the output of PW_Home in the UI with unit kW, how does the UI know that
FroniusSymoInverter_LoadPower hast to be display with the unit W?

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.

that would rather be var numericC = tempItemC.toBigDecimal, wouldn’t it ?
(yes it works and actually I wonder why the parser does not complain about unnecessary conversion, but still…)

On a sidenote, the following code is giving different values for t4,t5,t6. Not sure why and if that is a bug. Physics put aside, t3 is shown to be K² .

    var t = 20|°C
    var t2 = 68|°F
    var t3 = t2 * t

    var t4 = t3 / t2.toUnit("°C")
    var t5 = t3 / t2.toUnit("°F")
    var t6 = t3 / t2.toUnit("K")

Um, I suppose it depends what your tempItemC object is exactly. In my examples above, it was an Item, so we looked at tempItemC.state, which doesn’t have a .toBigDecimal method (in OH2 at least).

The “weird maths” is fun.
With numbers, for readers;

    var t = 20|°C
    var t2 = 68|°F
    var t3 = t2 * t  // result 1360 °C·°F

    var t4 = t3 / t2.toUnit("°C")  // result 68 °F
    var t5 = t3 / t2.toUnit("°F")  // result 20 °C
    var t6 = t3 / t2.toUnit("K")  // 4.639263175848541702200238785604639 °C·°F/K

1360 °C·°F gives me a headache. What does that mean, what does it represent? It is analogous to the example multiplication that gives °C² units, that is equally beyond my grasp.

Anyway, weird results are I think just like the temperature addition problem - different unit systems have different zero points, different points of origin.
The absolute vs. increment issue.
Does “1°C” represent the difference between 1°C and 2°C, and so 1.8°F is “the same”, or does it represent the temperature 1°C, and so 34°F is “the same”.
This really matters when we start mixing units and have to make conversions.

There’s no way to signal openHAB which you mean in any given case. “1°C” is just ambiguous in meaning.
Apparently “wrong” answers are more likely unexpected assumptions.

Let’s work some simpler examples, and dodge the temperature-squared headache.

    var t = 20|°C
        // simple division
    var t4 = t / 2  // result 10 °C
    var t5 = t.toUnit("°F") / 2  // result 34.00 °F, that's equivalent 1.1°C

This gives us clues about how the framework is going to work this.
“Half of 20°C” seems a simple enough calculation, but it is ambiguous.
Should we expect “Half of 68°F” to give the same answer? If not, why not? Starting conditions are the same.

In reality, the framework has simply assumed that dividing-a-Quantity-by-a-number means you want it to divide the numeric part only and then give the result in same units you started with.
That works fine with metres and inches, which have a common meaning for zero.
It might not be what you expected for temperatures or db/percentage, which have different zeroes and scales.

    var t = 20|°C
    var t2 = 10|°C
        // divide quantity by quantity
    var t4 = t / t2  // result 2
    var t5 = t.toUnit("°F") / t2  // result 6.80 °F/°C
    var t6 = t.toUnit("°F") / t2toUnit("°F")  // result 1.36

So now the question is “How many times does 10°C go into 20°C?”
Seems simple, but to a scientist it is ambiguous. 20°C = 293K so 10°C or 10K will “go” 29 and a bit times.

Framework has taken the easy option, “20°C divide by 10°C, just divide the numeric part only and the units cancel out, °C/°C is unity”.

“How many times does 10°C go into 68°F?” has apparently flummoxed the framework. It’s actually done the same thing really, “divide numeric part only, and the units … err … well look, here is what you asked for, expressed in units of °F/°C”
It’s a mathematically correct answer.

For “How many times does 50°F go into 68°F?” the framework has been consistent and applied the same rule, giving 1.36 and no units (°F/°F)

I think it is going to apply these same basic approaches to your temperature-squared calculations, which may be different from the assumptions humans make.

Yeah I don’t grasp it either, maybe some more intelligent Ph.D. than me does, but see it as just an intermediary result, part of a greater calculation.

So let’s consolidate that into one line to have even more fun:

    var t8 = t2 * t / t2                                       // 20 K
    var t9 = t2 * t.toUnit(t2.getUnit) / t2                    // 37.77777777777777777777777777777778 K
    var t10 = t2.toUnit(t.getUnit) * t / t2.toUnit(t.getUnit)  // 19.9999999999999999999999999999999999999999999999999999999999999999800 K
    var t11 = t2.toUnit(t.getUnit) * t / t2                    // 10.5882352941176470588235294117647145882352941176470588235294117647040 K

Now all of them are in K, which hasn’t been involved as a unit ?
Not a single one to have the obvious answer, t = 20 °C that is.

To be fair, you didn’t ask for any particular unit … did you want the system to make an assumption :wink:

One of the difficulties handling variables in rules is that we cannot assign a variable a ‘default unit’, so they don’t behave quite the same as Item states. We can specify a unit when loading any particular value into a variable, but unlike a state we can’t have it converted to some other desired unit as part of the update. Feels relevant here.

Now, here’s the thing … my results

    var t = 20|°C
    var t2 = 10|°C

    var t8 = t2 * t / t2     // result 20 °C
    var t9 = t2 * t.toUnit(t2.getUnit) / t2    // result 20 °C
    var t10 = t2.toUnit(t.getUnit) * t / t2.toUnit(t.getUnit)  // result 20 °C
    var t11 = t2.toUnit(t.getUnit) * t / t2   // result 20 °C

which is perhaps what you expected.

I’m different because that’s in OH2 … UoM had an overhaul for OH3.1

and it’s changed some of the built-in assumptions.

So, is the answer to t2 * t / t2 20°C or 20K ??

Well, I think the answer is “yes”, because the question is ambiguous.
20°C / 10°C is ambiguous. We can represent it as 20K / 10K or as 293K / 283K.
Did you mean this temperature difference divided by that temperature difference? Or did you mean this absolute temperature divided by that absolute temperature?
Did you even mean one of each … this absolute temperature divided by that temperature difference, 293K / 10K ?
These are all valid interpretations.

I think what you’ve discovered is the absolute/difference assumptions are now more complicated and depending on context.

You might try these variations

var t12 = (t2 * t) / t2    // OH2 result  20 °C
var t13 = t2 * (t / t2)    // OH2 result  20 °C

I suspect when used in the .toUnit() cases, this might give insight into the unexpected results

I didn’t repeat this but I also had

var t = 20|°C
var t2 = 68|°F

So t2 * t / t2 equals t which has a well-defined unit, °C that is. Yet the system chose K.

So to be fair as well, you meanwhile have been cheating a little bit by setting both t2 AND t to °C so you effectively created the unambiguous unit.
Then again you’re right in that it changed in OH3, I always get K even if t and t2 have the same unit.