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 Item metadata / state description / pattern
in the GUI.
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.