openhab / openhab-js

openHAB JavaScript Library for JavaScript Scripting Automation
https://www.openhab.org/addons/automation/jsscripting/
Eclipse Public License 2.0
38 stars 31 forks source link

Add utilities to improve QuantityType handling #102

Closed rkoshak closed 1 year ago

rkoshak commented 2 years ago

QuantityTypes are somewhat of a pain to work with. I think the current implementation in openhab-js leaves much to be desired.

My understanding is that currently the library goes out of it's way to throw away the units for QuantityTypes and pretty much forces the users who actually want to use units in their calculations to revert to messing with the rawState and importing and creating QuantityTypes themselves.

This issue is to discuss and track ideas for ways to make it easier for end users to handle QuantityTypes.

Unfortunately, based on my research and experiments I've determined the following:

So we are looking at adding add(), subtract(), et. al. helper functions somewhere.

I've already done some experimentation using this approach and I think it could work. What I propose is adding all the operations as helper functions (to mirror the Java QuantityType I'd just create a compareTo instead of equals, lt, gt, et. al. functions). Each of these functions accept two arguments and follow these rules:

In experimenting I've basically already implemented everything but that last rule in a prototype and it all works as expected (though there are some gotchas with multiply and divide which I may need to think about, namely is it even reasonable to convert the non-QT operand to a QT?).

Before I go and create a new PR (given the trouble I've had already I want to wait until my outstanding PRs are merged before creating new ones) I want to know:

  1. Does this sound like a good idea? It's probably going to be less useful to people who are writing rules in files or who are very well aware of the types of their Items but I think it's going to be critical for Blockly users when Blockly moves to JS Scripting.
  2. Where to put it? utils.js? Item?

Your Environment

jpg0 commented 2 years ago

I'm posting now as I've only just been learning about QuantityTypes...

It seems that your proposed implementation would copy all the existing types from the Java side. Would it be worth taking a look at using JS proxies instead, so that you can wrap the Java object into a JS one and simply pass through method calls, rather than having to reimplement each one?

digitaldan commented 2 years ago

@rkoshak can you give a few examples of how you are using Quanity types (or how you would like to use them)? I have to admit, i don't actually pay attention to them much and want to make sure i see it from your point of view.

rkoshak commented 2 years ago

Sorry for the delay. I took a week's vacation to see the Grand Canyon and other AZ sites and chose not to take a computer with me.

Would it be worth taking a look at using JS proxies instead, so that you can wrap the Java object into a JS one and simply pass through method calls, rather than having to reimplement each one?

I thought that was what I was suggesting. Creating a JS wrapper that would handle all the interactions with QuantityType. The only type from Java that would need to be imported is QuantityType itself since it has constructor, conversion, and other methods that let you specify the quantity and units using Stings.

@rkoshak can you give a few examples of how you are using Quanity types (or how you would like to use them)?

Consider the following scenario.

Let's say I have a thermostat that has a setpoint channel. The binding author dictates that this Channel must be linked to a Number:Temperature Item and it populates that Item's state with °C.

Next let's say I have a temperature sensor connected to OH through a different binding. Again the binding author dictates that the Channel must be linked to a Number:Temperature but this time the binding author chooses to populate the Item's state with °F.

Because I'm in the US, I've set the State Description pattern on the Setpoint Item to %.0f °F. So everywhere I look in MainUI I see °F. So far so good.

The problem comes in because that Setpoint Item, despite showing that it's °F in MainUI, is actually holding °C. When you look at events.log, Persistence, and (what's important here) in rules, the state will be °C.

~~The current behavior of the library, when calling items.getItem('Setpoint').state is to throw out the units (I'm not actually sure how it does this actually). ~~ I was mistaken or this behavior has changed since I last tested this.

Now I have a rule I want to write where I add 5 °F to the Setpoint and compare it to the current temp.

Problems:

In Rules DSL this is pretty straight forward with the only prior knowledge needed is that I have units in the first place.

if(Setpoint.state + 5 |°F < Temperature.state) {

The only gotcha here is that all the operands must be QuantityTypes.

In JS Scripting I end up with

const QT = Java.type('org.openhab.core.library.types.QuantityType');
let sp = items.getItem('Stepoint').rawState.toUnit('°F').floatValue();
let tmp = items.getItem('Temperature').rawState.floatValue();
let five = new QT('5 °F').floatValue();
if(sp + five < temp){

Or if someone wanted to try to make it a one liner:

const QT = Java.type('org.openhab.core.library.types.QuantityType');
if(items.getItem('Stepoint').rawState.toUnit('°F').floatValue() + new QT('5 °F').floatValue() < items.getItem('Temperature).rawState.floatValue()){

Notice that we never actually have to deal with the fact that Setpoint is °C. That's the whole point behind QuantityTypes really. So long as the units are compatible it handles the conversions for you.

Anyway, what I am proposing is finding a way to get closer to that Rules DSL example or perhaps even go one better and making it easier to mix in numbers that don't have units into the calculation by making some reasonable assumptions (e.g. if I just used 5 above I'd assume it's the same units as the QuantityType. I'd add these auto-conversion to Item in managed.js but could see other approaches where this is encapsulated in a whole new state class too.

rkoshak commented 2 years ago

Circling back around to thing since the time utils PR was merged.

It seems that your proposed implementation would copy all the existing types from the Java side.

I not sure I addressed this but no, I'm not actually proposing copying the all the Java types. I think I'd only need to import QuantityType.

So an add(a, b) utility function would do something like the following:

const QT = Java.type('org.openhab.core.library.types.QuantityType');

const add = function(a, b) {
    const aIsQT = a instanceof QT;
    const bIsQT = b instanceof QT;

    // Neither are QT
    if(!aIsQT && !bIsQT){
        // Maybe can do more here to test that a and b can be added and throw a meaningful error if not
        return a + b;
    }

    // Both are QT
    if(aIsQT && bIsQT) {
        return a.add(b);
    }

    // a is QT
    if(aIsQT) {
        // Probably need to do some further tests here and perhaps a parse to coerce b into a Number
        return a.add(new QT(b, a.getUnit()));
    }

    // b is QT
    // Probably need to do some further tests here and perhaps a parse to coerce b into a Number
    return b.add(new QT(a, b.getUnit());
}

There would be a similar implementation for all of the operations. I'm sure something clever could be done to avoid a bunch of duplicated code. The above is for illustrative purposes.

So you see, I'm not looking to import anything more than QuantityType. The function should work whether you have zero, one, or both operands being of type QuantityType. We assume that if only one operand is a QuantityType, the second operand is of the same units, which is the most common use case I've seen on the forum.

I'm not sure creating a proxy Object does anything for us here though. QuantityType itself doesn't handle the ability to do math unless both operands are of QuantityTypes. So we will have to implement something to make it so it works with non-QuantityTypes too. Whether it makes sense to do that implementation in a proxy Object I can go either way. My only concern is that we reimpose the need for the end users to have to do it one way if they have QuantityTypes and another way if they do not. That's the root problem I'm trying to solve.

But with something like this my previous example:

const QT = Java.type('org.openhab.core.library.types.QuantityType');
let sp = items.getItem('Stepoint').rawState.toUnit('°F').floatValue();
let tmp = items.getItem('Temperature').rawState.floatValue();
let five = new QT('5 °F').floatValue();
if(sp + five < temp){

becomes

let sp = items.getItem('Setpoint').rawState
let temp = items.getItem('Temperature').rawState
if(utils.lt(utils.add(sp, 5), tmp)) {

Not super great. Kind of like Reverse-Polish notation. But it's better.

Maybe a proxy would be better but where would that go? Would we add a wrappedState to Item so there are three different ways to get the state, one as a String, one as the raw Java Object, and one wrapped in a proxy? I don't think we should replace rawState and too many are depending on state being a String now to change that I think.