Working with Number:Temperature Items in rules

Tags: #<Tag:0x00007f17355efaf8>

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

// 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 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 Number).toBigDecimal
  // returns 20 - no use if our maths is assuming °C

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!
    // to be safe, we should force units to match
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 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, 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
14 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