Skip to content

Rules DSL: Support arithmetic operations directly on Number Item's State - #5618

Draft
jimtng wants to merge 1 commit into
openhab:mainfrom
jimtng:dsl-direct-state-operations
Draft

Rules DSL: Support arithmetic operations directly on Number Item's State#5618
jimtng wants to merge 1 commit into
openhab:mainfrom
jimtng:dsl-direct-state-operations

Conversation

@jimtng

@jimtng jimtng commented May 30, 2026

Copy link
Copy Markdown
Contributor

Currently to perform numeric calculations in Rules DSL / DSL script, one has to do this:

(MyItem.state as Number)- (MyOtherItem.state as Number)

(MyItem.state as Number) * 1.5

This PR makes it easier by allowing:

MyItem.state - MyOtherItem.state
or
MyItem.state * 1.5

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
@openhab-bot

Copy link
Copy Markdown
Collaborator

This pull request has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/another-calculation-in-dsl-rules-question-item-value-minus-item-value/169406/5

@mherwege

Copy link
Copy Markdown
Contributor

If the arguments are QuantityType, you need to do QuantityType arithmetic. I think you run into trouble with this if the units don't match. And arithmetic with QuantityType should return a QuantityType unless the arguments are first converted explicitly to Number, in which case it is the responsibility of the script to use the proper unit.

@Nadahar

Nadahar commented May 30, 2026

Copy link
Copy Markdown
Contributor

I can't understand that you can't already do this. NumberExtensions already contains methods like e.g:

    public static BigDecimal operator_plus(Number x, Number y) {
        BigDecimal xValue = numberToBigDecimal(x);
        BigDecimal yValue = numberToBigDecimal(y);
        if (xValue == null) {
            return yValue;
        } else if (yValue == null) {
            return xValue;
        } else {
            return xValue.add(yValue);
        }
    }

If both States are Numbers, that should already work, erasing the unit (by converting to "standard unit") along the way. Has anybody tried to do this? This PR still just supports doing arithmetic with States that implement Number, so I'm a bit at a loss about what would happen if you add this code.

I can only guess at how Xtend "matches" types, but if it matches by "specificity", it might consider State more specific than Number and select the new methods, but wouldn't the result be the same?

QuantityTypes already seem to have "problematic" behavior here, since they are converted to their "standard form" for the unit and then converted into a BigDecimal. So, you can add apples and oranges as far as I can tell from the code.

@Nadahar

Nadahar commented May 31, 2026

Copy link
Copy Markdown
Contributor

I've done a bit of testing, and I've found that the reason it doesn't work with the existing Number extensions is that it doesn't evaluate the type at runtime, but at compile time. State itself doesn't extend Number (only some of its implementations), and thus it doesn't "match" Number. Which is why doing what you do here should actually work, but it would cause an error for any States that don't implement Number. The question is how that compares with what's being done today, because + is also used for string concatenation. I'm wondering if that would mean that some situations where it works as string concatenation today, would instead result in an exception?

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

The reason why questions are raised on the forum is lack of examples in the openHAB documentation (and users not understanding the openHAB Java API, including all sub-classes of State). The procedure to enhance the documentation is currenlty very cumbersome: it takes too much time to verify if some proposed text change is correct, even if verifying the changes would be trivial; written text in github, as part of proposed text rewording, must be deleted, before the proposed change make progress. And updates, for which it is clear that on their own they are correct, are not integrated for more than a year or more than half a year.

All this leads to a situation, where nobody wants to propose changes to the documentation, because simply said - currently changes to the documentation are proposed faster, that they are handled.

As a matter of fact https://www.openhab.org/docs/configuration/rules-dsl.html contains this example:

if (Temperature.state < 20) {
    Heating.sendCommand(ON)
}

but the example is not correct. In an openHAB 5.2. test:

rule a
when
  System reached start level 100
then
  logError("B", termo1.state.toString)
  if (termo1.state > 3) logError("C", "TRUE") else logError("C", "FALSE")
  if (termo1.state < 3) logError("D", "TRUE") else logError("D", "FALSE")
  // Script execution of rule with UID 'a-1' failed: An error occurred during the script execution: Could not invoke method: org.eclipse.xtext.xbase.lib.IntegerExtensions.operator_lessThan(int,byte) on instance: null in a
  //ERROR : if (3 < termo1.state) logError("E", "TRUE") else logError("E", "FALSE")
  //ERROR : if (3 > termo1.state) logError("F", "TRUE") else logError("F", "FALSE")
  if ((termo1.state as Number) > 3) logError("G", "TRUE") else logError("G", "FALSE")
  if ((termo1.state as Number) < 3) logError("H", "TRUE") else logError("H", "FALSE")
  if (termo1.state > 3|℃) logError("J", "TRUE") else logError("J", "FALSE")
  if (termo1.state < 3|℃) logError("K", "TRUE") else logError("K", "FALSE")
end

prints

2026-05-31 09:09:01.710 [ERROR] [org.openhab.core.model.script.B     ] - 18.7 °C
2026-05-31 09:09:01.714 [ERROR] [org.openhab.core.model.script.C     ] - FALSE
2026-05-31 09:09:01.719 [ERROR] [org.openhab.core.model.script.D     ] - FALSE
2026-05-31 09:09:01.724 [ERROR] [org.openhab.core.model.script.G     ] - TRUE
2026-05-31 09:09:01.731 [ERROR] [org.openhab.core.model.script.H     ] - FALSE
2026-05-31 09:09:01.738 [ERROR] [org.openhab.core.model.script.J     ] - TRUE
2026-05-31 09:09:01.744 [ERROR] [org.openhab.core.model.script.K     ] - FALSE

So termo1.state < 3 produces the same result as termo1.state > 3, while termo1.state > 3|℃ produces different result compared to termo1.state < 3|℃. And this is kind of correct, because you cannot compare temperature to 3, you can compare it to 3 degree Fahrenheit, or 3 degree Celsius/Kelvin.

The current change does not address comparing state with unit to a number.

All that said, instead of changing how the system currently works, it should be described in the documentation how the system currently works. Otherwise - before and after this change - there is still no documentation how the system works - so nothing changes. This change can cause also further surprizes.

For all the above reasons, I am against the current changes. I think the right first step is to update the documentation and explain there how numbers and states are supposed to be used in Xbase/openHAB DSL Rules/Scripts/Transformations/UI-Rule-bodies.

@mherwege

mherwege commented May 31, 2026

Copy link
Copy Markdown
Contributor

As a matter of fact https://www.openhab.org/docs/configuration/rules-dsl.html contains this example:

if (Temperature.state < 20) {
    Heating.sendCommand(ON)
}

but the example is not correct.

Did you read further down the page, specifically this section: https://www.openhab.org/docs/configuration/rules-dsl.html#number-item

The example is correct if the item is a Number type without dimension (carries a DecimalType and not a QuantityType). In the section I point to, this is explained with appropriate examples. I would say Temperature as an Item name for the first example is an unlucky choice, as it may imply a dimension, but it is not wrong by itself. And it is properly documented on the page.
Again, room for improvement for sure, but claiming this is not documented is not correct. Please read the full documentation first.
You can do arithmetic with QuantityTypes, so most of the problem in the post is because of forcing conversion before doing the arithmetic, which is not needed.

@Nadahar

Nadahar commented May 31, 2026

Copy link
Copy Markdown
Contributor

The procedure to enhance the documentation is currenlty very cumbersome: it takes too much time to verify if some proposed text change is correct, even if verifying the changes would be trivial; written text in github, as part of proposed text rewording, must be deleted, before the proposed change make progress. And updates, for which it is clear that on their own they are correct, are not integrated for more than a year or more than half a year.

All this leads to a situation, where nobody wants to propose changes to the documentation, because simply said - currently changes to the documentation are proposed faster, that they are handled.

Some of this is a problem with openHAB that isn't limited to documentation. I've also found that if you touch somewhat advanced topics, there might not be anybody that wants to approve or reject it because they don't feel that they themselves have the required knowledge. Yes, I know it's frustrating, but it's also somewhat understandable. It can help if you explain the topic so that it's easier for people to understand the problem.

But, there's another problem that I've encountered with some of the changes you want to do to the documentation: If you find an inconsistency/a problem, you want to document that - when really, the problem should instead be solved. When something is documented, it kind of becomes "the official way it's supposed to work". This doesn't always fit with the problems you find, in which case the effort should be on fixing the design, not documenting the flaws.

@jimtng

jimtng commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

This PR is still a very early draft, but I appreciate all the feedback.

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

The text at https://www.openhab.org/docs/configuration/rules-dsl.html#number-item

A Number Items carries either a DecimalType or a QuantityType in case the Number Item has a dimension attached

could be understood as:

In case the Number Item has a dimension attached, then a Number Item carries either a DecimalType or a QuantityType.

Irrespective of this, at the same time for a number item of QuantityType (without dimension) if (Temperature.state > 20) will work as expected; and https://www.openhab.org/docs/configuration/rules-dsl.html contains once if (Temperature.state > 20), once if (Temperature.state < 20) without writing next to it that in the most common case (Temperature is QuantityType) this comparisson is tautology (result of boolen expression does not depend on the concrete number).

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

Currently to perform numeric calculations in Rules DSL / DSL script, one has to do this:

(MyItem.state as Number)- (MyOtherItem.state as Number)

In fact currently is sufficient to write:

var a = MyItem.state as Number - MyOtherItem.state

or (probably what is meant):

var b = MyItem.state as QuantityType - MyOtherItem.state

In the first case a is of type BigDecimal when I try it, in the second case there is a warning ("Validation issues"): QuantityType is a raw type. References to generic type QuantityType<T> should be parameterized, so after:

var c = MyItem.state as QuantityType<javax.measure.quantity.Temperature> - MyOtherItem.state

there is no warning.

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

Looking at this again, currently there are two minus operators for two parameters:

  • public static BigDecimal operator_minus(Number x, Number y), and
  • public static QuantityType<?> operator_plus(QuantityType<?> x, QuantityType<?> y)

When MyItem.state is a Number and a QuantityType at the same time, my guess is, that the expression MyItem.state - MyOtherItem.state is ambigious and therefore Xbase aborts on it. The purpose of the cast MyItem.state as Number - MyOtherItem.state and MyItem.state as QuantityType - MyOtherItem.state is to remove the ambiguity, which of both operators to execute.

Likewise for multiplication these operators exist in NumberExtensions:

  • public static BigDecimal operator_multiply(Number x, Number y),
  • public static QuantityType<?> operator_multiply(Number x, QuantityType<?> y),
  • public static QuantityType<?> operator_multiply(QuantityType<?> x, Number y) - just calling operator_multiply(y, x) - switching the parameters, and finally
  • public static QuantityType<?> operator_multiply(QuantityType<?> x, QuantityType<?> y)

The expression MyItem.state * 1.5 is ambiguous, again this is my guess, because Xbase does not know whether to call for it operator_multiply(Number, Number) or operator_multiply(QuantityType<?>, Number). This ambiguity is resoled by explicit casts.

The discussion here should be, whether this ambiguity should stay, so that users must use casts to be explicit on which operator exactly to call.

  • Do you think in public static QuantityType<?> operator_multiply(Number x, QuantityType<?> y) the question marks can be replaced with same type (T) in parameter and result?

What can be tried, and I have no idea if it will work, is when the user explicitly provides the destination type, to call the desired operator_, otherwise to resort to some default, e.g. giving preference to operator_minus(QuantityType, …):

val Number a = MyItem.state - MyOtherItem.state  // calls operator_minus(Number, …)
var b = MyItem.state - MyOtherItem.state // calls operator_minus(QuantityType, …)

This is how Collection Literals work:

val a = #['Hello','World'] // a is LIST, almost same expression as next line
val String[] b = #['Hello','World'] // b is ARRAY, because the destination type is set explicitly.

@Nadahar

Nadahar commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

The expression MyItem.state * 1.5 is ambiguous, again this is my guess, because Xbase does not know whether to call for it operator_multiply(Number, Number) or operator_multiply(QuantityType<?>, Number). This ambiguity is resoled by explicit casts.

I'm not saying that this is wrong, I don't know enough about how Xtext/Xtend works, but from what I read, it should behave in the same manner as standard Java does. For standard Java the rule is clear: Pick the version with the highest "specificity", which means that QuantityType one. It might be that the use of generics throws some wrench into the machinery though.

  • Do you think in public static QuantityType<?> operator_multiply(Number x, QuantityType<?> y) the question marks can be replaced with same type (T) in parameter and result?

I think it could (and should), but it would require refactoring of QuantityType itself, because it doesn't seem to apply generics properly. By making this change to QuantityType, it becomes possible to call this method without "breaking generics":

 .../src/main/java/org/openhab/core/library/types/QuantityType.java      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java
index fc0f7d17ed..c4026beb7e 100644
--- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java
@@ -688,7 +688,7 @@ public class QuantityType<T extends Quantity<T>> extends Number
      * @param value the value this {@link QuantityType} should be multiplied with.
      * @return the product of the given value with this {@link QuantityType}.
      */
-    public QuantityType<?> multiply(BigDecimal value) {
+    public QuantityType<T> multiply(BigDecimal value) {
         Quantity<T> quantity = Quantities.getQuantity(this.quantity.getValue(), this.quantity.getUnit(),
                 Scale.ABSOLUTE);
         return new QuantityType<>(quantity.multiply(value));

Similar changes would need to be made throughout the class. The thing is that, as soon as something returns <?>, it can't be "turned back to a specific type" without doing a "raw conversion" with the warnings that follow.

It's long bothered me that it seems to be impossible to deal with QuantityType without generating warnings (unless you suppress them), but I know that generics has its limitations, and sometimes it's just not possible to make it work. This quick change indicates that it could at the very least be made to work better.

But, there are other problems here too, in NumberExceptions:

    public static QuantityType<?> operator_multiply(Number x, QuantityType<?> y) {
        BigDecimal xValue = numberToBigDecimal(x);
        if (xValue == null) {
            return QuantityType.ZERO;
        } else if (y == null) {
            return QuantityType.ZERO;
        } else {
            return y.multiply(xValue);
        }
    }

This is just wrong and can never work, it actually returns ZERO in a completely different unit than the argument is. It might however, be difficult to avoid, because you can't instantiate a type based on a generic type, at least not unless you do a lot of trickery with reflection and whatnot. If y is null, you have no way at all to resolve the type to return.

I think returning zero for null arguments is the wrong thing to do, it should return null instead. That would allow this to work properly with generics. For an extension, the first argument, x, can't be null, since then the extension wouldn't apply in the first place.

But, it's probably way too late to correct the design at this time, I can only imagine how many rules would break if it started returning null. That said, I can't understand anything else that returning the wrong type also causes some problems. If you try to "hard cast" it into the type you expect it to be, it will throw an exception.

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

I also think that it makes no sense for an extension method to handle the case, when its first parameter is null. Currenlty

rule a 
when
  System reached start level 100
then
  logError("A", (null as QuantityType<javax.measure.quantity.Energy> + 4|kWh).toString)
  var QuantityType<javax.measure.quantity.Energy> s = r2_energy.state
//same as above  var s = r2_energy.state as QuantityType<?>
//same as above  var QuantityType<?> s = r2_energy.state
  logError("B", s.toString)
  logError("C", (s + 6|kWh).toString)
  s = null;
  logError("D", (s + 7|kWh).toString)
end

prints

2026-06-03 11:33:40.062 [ERROR] [org.openhab.core.model.script.A     ] - 4 kWh
2026-06-03 11:33:40.065 [ERROR] [org.openhab.core.model.script.B     ] - 223.226000 Wh
2026-06-03 11:33:40.069 [ERROR] [org.openhab.core.model.script.C     ] - 6223.2260000000000000000000000000004978580800000 Wh
2026-06-03 11:33:40.074 [ERROR] [org.openhab.core.model.script.D     ] - 7 kWh

so null is converted to zero and this is the way to invoke a NumberExtension with null as first parameter.

I think the operator_s in NumberExtensions should not check if the first parameter is null, callers/users have to make sure that they are not doing number arithmetic with null as argument.

@Nadahar

Nadahar commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

so null is converted to zero and this is the way to invoke a NumberExtension with null as first parameter.

Yeah, I keep forgetting that it's the declared type that decides, not the actual type. So it is possible. But, I still don't think it's "correct" to translate null to zero. Sure, it works somewhat if you add or subtract, but it does not give meaningful results for other operations, and it makes proper generics impossible. The operation should just return null if one of the arguments are null.

@rkoshak

rkoshak commented Jun 3, 2026

Copy link
Copy Markdown

A Number Items carries either a DecimalType or a QuantityType in case the Number Item has a dimension attached

could be understood as:

In case the Number Item has a dimension attached, then a Number Item carries either a DecimalType or a QuantityType.

I do not see how the first sentence could be interpreted as the second. And the second sentence is wrong. If you have a Number Item without a dimension, the Item always carries a DecimalType or UnDefType. If you have a Numer:<some dimension> the Item always carries a QuantityType or UnDefType. I just want to make sure the discussion doesn't go down a path assuming the second sentence is correct.

Note, QuantityType and DecimalType are not the same but both do inherit java.lang.Number so they should be compatable.

When MyItem.state is a Number and a QuantityType at the same time, my guess is, that the expression MyItem.state - MyOtherItem.state is ambigious and therefore Xbase aborts on it.

Unless the erorr is suppressed now thorugh some change, Xbase explicitly reports when there is and error when it encounters and ambiguous method call. I used to have to fight with that all the time in Rules DSL. So if there is no error reported cliaming an ambiguous function call, I doubt that is the case here.

However, the cases you point out indeed used to generate that type of error in a DSL rule. So maybe the reporting of that error has indeed been supporessed somehow.

The expression MyItem.state * 1.5 is ambiguous, again this is my guess, because Xbase does not know whether to call for it operator_multiply(Number, Number) or operator_multiply(QuantityType<?>, Number). This ambiguity is resoled by explicit casts.

Try it as 1.5 * MyItem.state. I think you'll find that works. Based on my experience, what I think happens is as follows.

  1. MyItem.state returns an Object of type State.
  2. Xtend uses the first operand to fix the type of the operation
  3. If you put the Item.state first, the operation gets fixed to State which obviously doesn't have any mathematical methods or overrides.
  4. If you put the Number first that fixes the operation to a Number operation and Xtend then tries to cast the State to a type that is compatable (java.lang.Number).

In short, if you put the constant first, you don't need to cast the state. If you put the state first, you must cast the state to at least a Number in order to do math operations on it.

I'm not sure what this means but it's a data point which might inform the issue. I don't think the problem is with the underlying classes so much as it's that Xtend seems to only try to coerce the second operand's type and not both operands.

@Nadahar

Nadahar commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Unless the erorr is suppressed now thorugh some change, Xbase explicitly reports when there is and error when it encounters and ambiguous method call. I used to have to fight with that all the time in Rules DSL. So if there is no error reported cliaming an ambiguous function call, I doubt that is the case here.

However, the cases you point out indeed used to generate that type of error in a DSL rule. So maybe the reporting of that error has indeed been supporessed somehow.

I'm not aware of any recent changes that have led to more suppression of errors - changes have been in the opposite direction, if anything. But I doubt this is an "ambiguous situation", because they differ in specificity. If these situations were deemed ambiguous, there would be ambiguity "everywhere". I think ambiguity occurs when it lacks any "rule" for which one to pick, if they have equal "priority"/specificity.

In short, if you put the constant first, you don't need to cast the state. If you put the state first, you must cast the state to at least a Number in order to do math operations on it.

I'm not sure what this means but it's a data point which might inform the issue. I don't think the problem is with the underlying classes so much as it's that Xtend seems to only try to coerce the second operand's type and not both operands.

By looking at the existing methods in NumberExtensions, it's quite clear why it behaves like this. There are defined methods for Number, but not for State. The first argument is used to "match" which extension method to pick, so only when the first argument is a Number will it find a suitable extension. This PR would change that, so that State would also have extensions.

It doesn't matter if the States here implement Number, because that's something individual State implementations do, it's not a part of the definition of State itself. It's the same "trap" I fell into, failing to differentiate between static/compile time types and runtime types. The extensions are "mapped" using static analysis, so it doesn't matter if the runtime type actually implements Number. They have already "disqualified" because State itself doesn't implement Number.

Regarding Number, I suspect that two concepts might be conflated here. There's the "OH Number concept" found in DSL, and then there's the Java type Number. The latter has no generics/type assigned, so it can't be expressed as Number<?>.

@dilyanpalauzov

Copy link
Copy Markdown
Contributor

A Number Items carries either a DecimalType or a QuantityType in case the Number Item has a dimension attached

could be understood as:

In case the Number Item has a dimension attached, then a Number Item carries either a DecimalType or a QuantityType.

I do not see how the first sentence could be interpreted as the second. And the second sentence is wrong.

I think the first sentence, taken from https://www.openhab.org/docs/configuration/rules-dsl.html, allows this interpretation (which I emphasized by rewriting). . And then the problem is, that this interpretation is wrong.

@wborn wborn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an AI-assisted review performed before manual maintainer review.

The goal of making arithmetic with Number Item states less cumbersome makes sense. Item.state is statically typed as State, so Rules DSL currently requires casts even though Number Item states are represented by numeric types at runtime.

However, the current approach introduces some potentially surprising changes to arithmetic semantics and overload resolution that should be addressed before this is merged.

In particular:

  • The new State operators return Number, even when the runtime result is a QuantityType. Because Rules DSL/Xbase resolves subsequent operators using the static type, chained expressions can therefore lose quantity semantics.
  • QuantityType + Number and QuantityType - Number are now accepted implicitly but fall back to the ordinary Number operators, which convert the quantity to a numeric value and discard its unit. Previously this required an explicit cast to Number.
  • The casts added to the existing Java tests show that the new overloads introduce source-level ambiguity for classes such as DecimalType that implement both State and Number. org.openhab.core.model.script.lib is an exported package, so this is not necessarily limited to internal test code.
  • The new tests call NumberExtensions directly and therefore do not exercise the Rules DSL type resolution that this PR is intended to change.

No direct NumberExtensions.operator_* callers were found in the other main openHAB repositories, so there does not appear to be an immediate Java build break there. The larger cross-repository impact is documentation: openhab-docs, openhab-addons, and openhab-vscode contain examples that currently teach explicit state-to-Number conversions. Those can be updated once the intended arithmetic semantics are settled.

The usability improvement is worth pursuing, but it would be good to make sure direct state arithmetic retains the same unit safety and predictable type behavior as the existing explicit operations.

A human maintainer review is still required after the AI-assisted review.


// Calculation operators for states

public static Number operator_plus(State x, State y) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Number return type can lose the fact that the runtime result is a QuantityType when this expression is used as part of another expression.

For example, with two dimensional Number Items:

(LengthA.state - LengthB.state) * 2

the first operation can correctly produce a QuantityType at runtime, but its static result type is Number. Xbase will therefore resolve the following multiplication as a Number operation rather than a QuantityType operation, which converts the quantity to a BigDecimal and loses the unit.

The same issue occurs when such a result is assigned to an inferred variable and used later.

Could this be implemented without reducing a quantity result to Number in the DSL type system? Otherwise simple direct state arithmetic may work while equivalent chained arithmetic silently changes semantics.

return plus(stateToNumber(x), stateToNumber(y));
}

public static Number operator_plus(State x, Number y) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes expressions such as:

Temperature.state + 1

valid, but when the state is a QuantityType, plus() only chooses the quantity-specific operator when both operands are QuantityType. A quantity plus a plain number therefore falls through to operator_plus(Number, Number), which converts the quantity to its system-unit numerical value and returns a BigDecimal.

The same problem applies to State - Number.

That seems particularly risky because previously discarding the unit required an explicit as Number conversion. With this change an apparently unit-aware Item expression can discard the unit implicitly.

Could mixed quantity/plain-number addition and subtraction either be rejected or given explicitly defined unit-safe semantics instead?

@Test
public void operatorPlusNumberNumber() {
assertThat(NumberExtensions.operator_plus(DECIMAL1, DECIMAL2), is(BigDecimal.valueOf(3)));
assertThat(NumberExtensions.operator_plus((Number) DECIMAL1, (Number) DECIMAL2), is(BigDecimal.valueOf(3)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need to add these casts to previously unambiguous calls looks like an API compatibility regression caused by the new overloads.

DecimalType implements both Number and State, so after adding both:

operator_plus(Number, Number)
operator_plus(State, State)

neither overload is more specific for a DecimalType argument and Java requires an explicit cast.

This package is exported by org.openhab.core.model.script, so external Java code directly using these extension methods could also stop compiling after updating openHAB, even though existing binaries would continue to work.

It would be preferable to avoid introducing this ambiguity rather than adapting the existing tests to it. It would also be useful to verify that equivalent Rules DSL expressions with variables statically typed as DecimalType, PercentType, etc. do not become ambiguous for the same reason.

}

@Test
public void operatorMinusStateState() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests verify the runtime dispatch helpers, but they bypass the Rules DSL/Xbase operator resolution that this PR is intended to change by explicitly calling NumberExtensions.operator_*.

Could this also be covered by Rules DSL-level tests that parse/type-resolve and execute representative expressions?

In particular, it would be useful to cover:

NumberItem.state - OtherNumberItem.state
NumberItem.state * 1.5
QuantityItem.state - OtherQuantityItem.state
(QuantityItem.state - OtherQuantityItem.state) * 2

as well as explicitly typed numeric variables, NULL/UNDEF, and a nonnumeric Item state.

That would catch both overload-resolution regressions and cases where the static type of an intermediate expression differs from its runtime numeric type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants