`modbus:gainOffset gain="x %"` is hard to reason about in context of units

I have a data read out from a modbus endpoint. This data is scaled by 100 and represents 0..100%. That is: if I read out 2345, that refers to 23.45%. This same modbus endpoint also outputs temperatures with much the same kind of scaling.

Here are example item definitions I have. First the temperature one, which works much the way I would expect:

Number:Temperature Some_Temperature "Yep, it is some temperature" (InAGroup) {
    channel = "modbus:data:thing:poller:temperature:number"[ 
        profile="modbus:gainOffset", gain="0.01 °C", pre-gain-offset="0"
    ]
}

This item figures out that the unit for this item is °C based on the unit specified in gain, as documented in the modbus binding page.

However, when I write

Number:Dimensionless Some_Percentage "Yep, it is some percentage" (InAGroup) {
    channel = "modbus:data:thing:poller:percentage:number"[ 
        profile="modbus:gainOffset", gain="0.01 %", pre-gain-offset="0"
    ]
}

I’m now getting not 23.45% (for modbus register of 2345,) but rather 0.2345%. The value has been scaled not by 0.01, but rather by 0.0001! On the other hand, either gain = "1 %" or gain = "0.01" produce the expected number (23.45,) This in isolation wouldn’t be particularly hard to adapt to, except weird things happen when I also add the new unit = "%" specification.

Number:Dimensionless Some_Percentage "Yep, it is some percentage" (InAGroup) {
    unit = "%",
    channel = "modbus:data:thing:poller:percentage:number"[ 
        profile="modbus:gainOffset", gain="1 %", pre-gain-offset="0"
    ]
}

produces a different value (2345%) than the options above, even though for the temperature item above adding unit = "°C" results in no change whatsoever (since the thing was already providing Celsiuses).

Without having investigated much further it would seem to me that gainOffset is interpreting the 1 % as a… request to scale the value by 0.01 rather than a <gain> <unit> like most other units appear to work. Is it just me not being able to wrap my head around this or is this a real bug?

You didn’t just add a the metadata. You also changed the gain to 1 %. What happens if you return the gain to 0.01 % and have the unit metadata defined as well.

Note that the default unit for Number:Dimensionless is ONE which is a straight forward ratio. It is probably automatically multiplying by 0.01 to convert the percent sent in the first example to a unit of ONE.

No matter what units you apply in the gain transformation, that value is going to be converted to the unit defined by the unit metadata, or if it’s not defined, the system default.

As I explained (perhaps, poorly), in saying “On the other hand, either gain = "1 %" or gain = "0.01" produce the expected number…” I was referring to the following starting point (i.e. with gain set to 1 %):

Number:Dimensionless Some_Percentage "Yep, it is some percentage" (InAGroup) {
    channel = "modbus:data:thing:poller:percentage:number"[ 
        profile="modbus:gainOffset", gain="1 %", pre-gain-offset="0"
    ]
}

which produces the expected values (e.g. 23.45.) It was my understanding in specifying gain="1 <unit>" I also make the modbus thing to produce/consume values with the specified unit (or so the documentation says). In particular this sentence:

In addition, the profile allows attaching units to the raw numbers, as well as converting the quantity-aware numbers to bare numbers on write.

My understanding was that additionally specifying unit = "%" should be effectively a no-op, since this is an identity conversion and all values already have the correct unit associated with them. But that is not what I’ve seen happening.

Do I understand the last paragraph of your answer correctly that it doesn’t matter what “unit” the thing returns, since the unit provided by the thing is ignored and then a new conversion to the specified unit type occurs as if the value had no unit associated with it at all?

What happens if you return the gain to 0.01 % and have the unit metadata defined as well.

This, of course, produces the expected value, but that’s exactly why I am confused. My understanding of the unit-aware conversion semantics is that adding unit = "%" should not change the result seen at all, because the values coming from the modbus addon should already have the percentage units associated with the values.

Not quite. It very much needs the unit returned by the Thing so it knows how to convert it to the unit the the user has expressed they want the Item to carry. For example, if a Thing updates an Item with °C but the unit is set to °F, that number will be converted from °C to °F before the Item is updated.

You can’t do that sort of conversion if the Thing doesn’t publish what unit the number is in.

In the case where there isn’t a unit metadata defined, the default unit for that Item type is assumed. As mentioned, that is ONE for Number:Dimensionless.

Thus, if you have your profile apply % but fail to define the unit metadata, the value will be converted from % to ONE, since that’s the system default.

In the case where the Thing doesn’t supply units, no conversion takes place. If unit is defined, the raw value is assumed to be in that unit. If unit is not defined, the system default is assumed. But no conversion takes place.

It’s not the units that are the problem. It’s the value. If Modbus returns 2345 (blank means not defined)

Example Gain Unit Result
1 2345 ONE
2 % 2345 %
3 1 % % 2345 %
4 1 % 23.45 ONE
5 0.01 % % 23.45%
6 0.01 % .2345 ONE
7 1 2345 ONE
8 1 % 2345 %
9 0.01 23.45 ONE
10 0.01 % 23.45 %

If you fail to define the unit metadata at all, the unit will be assumed to be ONE (examples 1, 4, 6, 7, and 9). If you define % on the gain and fail to on the Item, your % will be converted to the default unit ONE (examples 4 and 6).

The unit metadata on the Item is completely independent from the unit defined on the gain profile. If you don’t define it, the system default is assumed and that % value will be converted to the system default which is ONE as you see in the table. If you don’t want the system default, you must define the unit metadata on the Item.

All that gain unit does is tell OH what unit the value is in. It doesn’t tell it what unit to store in the Item.

Aha, got it. This is where my misunderstanding was: I was assuming that if a thing provided a value with a unit, these units would be used for the item, unless specified otherwise (and the default would kick in only if the thing did not provide any units with the values.) I guess the behaviour makes some sense.