Problems due to proper and improper types and units of varables, especially temperatures

Dear all!

I’m having troubles formatting logfile entries with units.
I have an item X defined as a temperature item, so it carries units of K,°C,°F:
Number:Temperature X "X [%.1f %unit%]"

When I log this item to the logfile, I get it in °C:
logInfo("test.rules", "X.state is: " + X.state)
produces:
2021-01-25 09:53:50.289 [INFO ] [se.smarthome.model.script.test.rules] - X.state is: 5.0 °C

I already suceeded in formatting the number itself:
logInfo("test.rules", "X.state is: " + X.state.format("%.2f"))
produces
2021-01-25 09:53:50.289 [INFO ] [se.smarthome.model.script.test.rules] - X.state is: 5.00 °C
fine.

But I have not succeeded in converting the log-entry to different units, like Kelvin or Fahrenheit.

The only way I could - sort of - do it was
logInfo("test.rules", "X.state is: " + ((X.state as Number) + 0))
produces
2021-01-25 09:53:50.289 [INFO ] [se.smarthome.model.script.test.rules] - X.state is: 278.15
obviously in Kelvin, but without the unit.

How to convert units in a controlled way? I tried “.toUnit”, .format("%.2f K")… but to no success.

thanks for your support,
Sulla

Why do you need the correct value in the logfile? or do you want to chage the value for your sitemap or habpanel?

If it’s only for the logfile you can manually add the K like this:
logInfo("test.rules", "X.state is: " + ((X.state as Number) + 0)) +" K"

If you want to change the actual value of the item I’d suggest looking into a js transformation

I’m having troubles with calculations with temperatures: I tried to add 1°C (in form of a constant) to 5°C (in form of the state of an item) but instead of resulting in 6°C it resulted in 279°C, because “the system” did not add 5+1 (°C) but 278+274 (K) = 552 K = 279°C.
The logfile gave me 1°C and 5°C and 552 or 279 (without units) for the result of the addition, and 1+5=279 was a bit strange at first sight and lead to completely nonsensical comparisions in if-statements.
(As a sidenote, I’m still not completely sure of how to add 1°C to a given temperature, but I can handle that sufficiently well…)

Therefore I tried to make sure what the items “really” were. So I tried to log them together with their units.

Likewise I tried to add a constant 1 km/h to the value of a Number:Speed-item, but instead of just adding 1 to the item state in km/h, it added 1 km/h to the item in m/s again, not behaving in the way I expected.

Thus the desire to log the item state in various units to trace back my error of thinking.
The trick with the “(X as Number) + 0” gives mit SI-units, at least, albeit without the unit designator.

Doing maths on Temperature Quantities is a bit fraught. The temperature type Quantity represents an absolute value, that is to say 1°C represents a particular temperature, not a difference in temperature.

From that viewpoint, 2°C + 2°C makes no sense. Humans can glance at that and think “oh yes, that’s 2°C on the thermometer, plus another 2 graduations makes 4°C” but that’s not so obvious to a computer. The same collection of symbols “2°C” carries two different meanings here.
It’s a bit weird, but consider, 2°C is the same as 275K.
275K + 275K is therefore an identical sum, but now what’s the answer?
Depending on context, 2°C is the same as 2K as well.

We need the + function to be some special thing that works all this out in the specific context of temperatures. So we need to get our starting object as a Quantity object, because that’s the thing that knows what to do.
Note that someItem.state is a state type object, and will not do.

var tempOffset = (CurrentTemperature.state as QuantityType<Temperature>) + 2|°C
// the pipe thing | makes a Quantity type constant

Having said that, I think the handling of temperatures might be buggy. This ought to work with mixed units … but e.g.

logInfo("sums", "constants {}", 23|°C + 2|K)   // result -248.15 °C
logInfo("sums", "constant {}", 2|K + 23|°C)   // result 298.15 K
logInfo("sums", "constants {}", 23|°C - 2|K)   // result 294.15 °C

but it does not seem to work as expected at OH 2.5.11.
It looks like it takes the second Quantity as the base value, uses the first Quantity as an offset, but returns the value in the units of the offset not the base.
EDIT - added the subtraction test case, I’ve no idea now!
Can anyone confirm this behaviour continues in OH3?

One way to do reliable general maths is to extract the Quantity value in the units of your choice, do maths on pure numbers where you can now predict the units, and if you wish convert back to Quantity afterwards.
We need the to choose the units, because we don’t know what units the Quantity is in to begin with.

“extract the Quantity value in the units of your choice” is the non obvious part.

var tempInC = (CurrentTemperature.state as QuantityType<Temperature>).toUnit("°C")  // returns Quantity in units specified
var tempNumeric = (CurrentTemperature.state as QuantityType<Temperature>).toUnit("°C").toBigDecimal  // returns number only

Now we don’t care what units our sensor reports in, we can get the numeric part only of the Quantity and know it would be correctly in °C even if sensor reports °F. OH Framework has done the conversion for us.

Putting it all together …

   // applies offset to Item in any units 
   // get the unknown units of the Item
val targetUnit = (tempItem.state as QuantityType<Temperature>).getUnit
   // force into known units for offset to avoid weird bug
val offsetTemp = 1|°C + (tempItem.state as QuantityType<Temperature>).toUnit("°C")
   // that's a Quantity in C, but we can return to original units
tempItem.postUpdate(offsetTemp.toUnit(targetUnit))
1 Like

This is well put. To my knowledge this is the way it works in OH3 as well.

This is quite a tricky detail with temperatures.

Some other software have different syntax for absolute temperature and temperature increment, e.g. GNU Unit

This makes the distinction clear. 1K + 1 C could mean 2K or 2C depending how you look at it… In openHAB, everything is always absolute temperatures, I think, avoiding any ambiguity

I have not noticed anything similar for openhab but I might be wrong.

I’ve raised an issue for this,because of deceptive results. Even if it just didn’t work, or constrained you to deal in same units, it would be better.

1 Like

Huh those examples are pretty sick! Good job publishing issue on this one

thanks for your support, this thread was guided a bit off the original topic, perhaps I should adatpt the title for the benefit of future readers.

I’m a physicist, and as a physicist I love doing stuff in SI-units, so the behaviour of openahb is a bit obsure, but quite ok after all.

I’m doing calculations by converting item states to SI-units by
(X.state as Number)
after which calculations are not a problem:
((X.state as Number) - 0.2)
noteworthy, the minus operation works fine anyway, because a difference in °C works as well as a differnce in K… :wink:

likewise, to add an offset of 1 km/h to a Number:speed item, I just do a converstion to SI units (m/s) and then add 1km/h as 1000/3600 m/s:
((Y.state as Number) + 1/3.6)

SI is such an ingenious invention.

still, it would be nice to be able to log values together with their units, which works when logging item states, but not variables…

@Sulla what kind of problem exactly you have with variables logging? If something is Quantity it’s logged as quantity with unit.

var QuantityType<Temperature> t
t = 5|°C
t = t + 2|K
logInfo("test", "value {}", t)
logInfo("test", "value {}", t.toUnit("K"))

this nicely logs

 value -266.15 °C
 value 7.00 K
1 Like

Absolutely quickly made and dirty function. It screams like hell about unparameterized QuantityTypes and does not care if you wish to add miles to °C but does it’s job - adds “interval” in one units to value in another units the way it should be done.

val org.eclipse.xtext.xbase.lib.Functions$Function2<QuantityType, QuantityType> addInterval = [
  value, interval |
  var intervalZero = new QuantityType(0, interval.getUnit)
  return value + interval - intervalZero
]
var QuantityType<Temperature> t
t = 5|°C
t = addInterval.apply(t, 2|K)
logInfo("test", "value {}", t)
t = addInterval.apply(t, -2|°F)
logInfo("test", "value {}", t)

var QuantityType<Length> l
l = 5|km
l = addInterval.apply(l, 2|mi)
logInfo("test", "value {}", l)

Output

value 7.00 °C
value 5.8888888888888888888888888888889 °C
value 8.218688 km

Of course if you don’t need such adding interval in many places in code you can simply hardcode like

var value = <x>|anyunit
var result = value + <interval>|otherunit - 0|otherunit

For fun, handling Item states in different ways

logInfo("sums", "state {}", (nmt_ForecastTemperature.state as Number))   // result 4.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Number>))   // result 4.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Temperature>))   // result 4.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as Number) + 1)   // result 278.32
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as Number) + 1|°C)   // result 551.47
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as Number) + 1|K)   // result 278.32
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as Number) + 1|°F)   // result 533.2477777777777777777777777777778
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Number>) + 1)   // result 5.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Number>) + 1|°C)   // result 5.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Number>) + 1|K)   // result -267.98 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Number>) + 1|°F)   // result -13.0522222222222222222222222222222 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Temperature>) + 1)   // result 278.32
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Temperature>) + 1|°C)   // result 5.17 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Temperature>) + 1|K)   // result 267.98 °C
logInfo("sums", "state {}", (nmt_ForecastTemperature.state as QuantityType<Temperature>) + 1|°F)   // result -13.0522222222222222222222222222222 °C

Points of interest -

A Number type can still hold a Quantity value, with units, like “4.17 °C”
But it won’t behave the same as a “real” Quantity. Beware!

Not much difference in behaviour between as QuantityType<Temperature> and as QuantityType<Number> … unless you interact with a non-Quantity.
I suspect system default units may come into play here.

Oh, thanks a lot, yes, this indeed works perfectly. I tried it with .toUnit before, but didn’t succeed, now, this does the trick for me! That will help greatly with with debugging!

I think my problem was that I did not define the variable as
var QuantityType<Temperature> t
but as
var Number t = 5|°C
and in this case .toUnit does not work!
I really should review all my rules in this regard.

This was exactly the trap I fell into.

Your example very nicely pinpoints the problems. In some examples the results make perfect sense, in other, very similar cases, the results are total nonsense.

I don’t want to give anyone a project, but it seems these examples might fit in the documentation section somewhere. I was getting away with “Number” until I tried to add/subtract. I did not find any guidance in the current documentation until you pointed me to this thread and solved my problem.

Bob