Number:Time UoM formatting question

Agreed. But it MUST be a valid string.

Let me put it another way … if you pipe your number:time channel output into a transform profile, what do you get? That’s the part that needs to work. Linking a number:time to a String Item without a profile is not valid, and indeterminate results might be expected.

I’m sorry but I don’t understand your meaning. Can you please clarify?

Well, linking a rollershutter type channel to a Player type Item isn’t going to give any sensible result. Or a datetime channel to a Color Item.
The framework doesn’t support currently linking mis-matched types (except via the intermediary of a profile, where it’s up to you to manage a sensible output).

You’ve got an expectation that linking a number, specifically a number:time subtype, channel directly to a String Item should give you a sensible result. I don’t think that’s in the specification at all, it’s just a type mis-match. Getting a datetimey looking thing in this case is just some artefact of breaking the rules.

It gets harder when considering commands; what’s a number:time channel going to do with valid String command “banana”, or a rollershutter channel with command PLAY?

However, we can’t just block such linkages in the framework. It’s perfectly correct to link a contact channel to a DateTime Item - using a timestamp profile.
You might get fancy with the UI and not offer silly linkages unless a profile is selected. People using xxx.items files will still be able to break it, though.

^
@rossko57 I am still not sure that I understand your issue, but…

… please rest assured that I am not planning to “block” anything. And even if I were to propose a PR then obviously it would be subject to the normal review process.


To reiterate, my issue is/was that the binding/framework was sending the string 1970-01-01T01:03:18+01:00 from its “upTime” Channel to a String Item, and my (and your) initial reaction was that that string is wrong. However on reflection it is actually completely correct…

  • 1970-01-01T01:03:18+01:00 semantically equals 1970-01-01T00:03:18Z (in my time zone BST)

  • The binding was reporting ‘uptime = 198 minutes’ = 3 hours 18 minutes

  • The binding was not providing UoM so the framework was assuming ‘uptime = 198 seconds’ (i.e. the default UoM) = 3 minutes 18 seconds

  • … which is exactly the same as 1970-01-01T00:03:18Z

Ergo: it is all actually working ‘as designed’…

So … it’s just a number channel, after all? (Which is what the docs suggest)

And you think a plain number type channel feeding into a String type Item should give a DateTime string.

There’s no need to be snide. I’m not thinking anything of the sort. But it seems to be doing just that.

Not meant to be snide. I think it’s a bizarre thing to do, transform a number to a datetime-ish string. But I don’t think it’s a bug nor working as designed, it’s an odd side effect of an invalid configuration.

It’s a short cut. When one has a Number:time almost everyone is going to want to see that in a more human readable format from 18936 s. They want hh:MMM:ss or in this case dd:hh:mm:ss.

OH already has a way to format something into dd:hh:MM:ss, DateTimeType. So it seems relatively easy to convert the 18936 s to a DateTimeType and use the built in formatting to get to dd:hh:MM:ss. I suspected this was how the formatting was implemented. It takes advantage of the fact that you can easily convert a number of seconds (or what ever units) to a number of seconds since epoch which can be converted to a DateTimeType which can be formatted.

The months and years will always be meaningless and the days seem to not behave as expected. But for up to 23 hours, 59 minutes and 59 seconds it works great. I think with %td instead of %tj it might work for up to 31 days, 23 hours, 59 minutes, and 59 seconds. It definitely doesn’t cover all the possible use cases but it covers a lot with just a couple of lines of code.

JavaScript shouldn’t be involved here. The %th:%tM:%ts formatting Strings are Java formatting strings, not JavaScript. So the transformation is actually taking place on the server side using Java, not in the browser.

If there is an error, it’s most likely in the code that converts the value of the Number:time Item to a DateTimeType.

To cover all the use cases something like the posted JS transformation would probably have to be implemented, but that becomes really complicated really fast (because those formatting strings need ot be parsed) which is I’m sure why they didn’t do that in the first place

Indeed. Looking at the binding code reveals that it returns a DecimalType instance. So the cast to a DateTime is indeed in the framework rather than the binding…

… so, looking at the StringItem code reveals that StringItem can accept UnDefType, StringType, or DateTimeType values…

    private static final List<Class<? extends State>> ACCEPTED_DATA_TYPES = List.of(UnDefType.class, StringType.class,
            DateTimeType.class);

… so, looking at DecimalType.as() reveals that there is a specific if () case to convert Value → DateTime, but there is no specific if () case to convert Value → String (the StringType case falls through to defaultConversion())…

    public <T extends State> @Nullable T as(@Nullable Class<T> target) {
        if (target == OnOffType.class) {
            return target.cast(equals(ZERO) ? OnOffType.OFF : OnOffType.ON);
        } else if (target == PercentType.class) {
            return target.cast(new PercentType(toBigDecimal().multiply(BigDecimal.valueOf(100))));
        } else if (target == UpDownType.class) {
            if (equals(ZERO)) {
                return target.cast(UpDownType.UP);
            } else if (toBigDecimal().compareTo(BigDecimal.valueOf(1)) == 0) {
                return target.cast(UpDownType.DOWN);
            } else {
                return null;
            }
        } else if (target == OpenClosedType.class) {
            if (equals(ZERO)) {
                return target.cast(OpenClosedType.CLOSED);
            } else if (toBigDecimal().compareTo(BigDecimal.valueOf(1)) == 0) {
                return target.cast(OpenClosedType.OPEN);
            } else {
                return null;
            }
        } else if (target == HSBType.class) {
            return target.cast(new HSBType(DecimalType.ZERO, PercentType.ZERO,
                    new PercentType(this.toBigDecimal().multiply(BigDecimal.valueOf(100)))));
        } else if (target == DateTimeType.class) {
            return target.cast(new DateTimeType(value.toString()));
        } else {
            return defaultConversion(target);
        }
    }

So to summarise, there are two possible reasons why the DecimalType → StringType conversion produces a DateTime string…

  1. Either the accepted types iterator hits DateTimeType before it hits StringType, or
  2. On StringType, the defaultConversion() method returns null; which means the accepted types iterator passes over StringType and then hits DateTimeType.

I am not entirely sure if 1. or 2. applies, as it may depend on code optimisation (sometimes iterators are optimised to count down rather than up the list).

But in any case it seems to me that the framework should probably add some specific code to handle DecimalType → StringType conversions…

EDIT: I traced back into the defaultConversion() and can confirm that it only handles DecimalType → DecimalType, and otherwise returns null. QED: it’s a bug!

^

I don’t know if I’ve completely lost what we’re talking about.

It’s important to use the right word here. This is transforming the value. Casting means something slightly different. In Object Oriented Programming there is a Concept of inheritance. In OH you might have an inheritance tree like:

Object.   State.   Command.   Number
    \       |         |         /
             DecimalType

So a DecimalType is an Object, a State, a Command, and a Number all a the same time. Casting is telling the program “right now, even though it is 5 different types, I want to use this one.” So when you have MyNumberItem.state as Number you are telling the language that you want to use it in it’s Number type aspect and none of the others.

When you call toString or parse or other similar things you are transforming the Object into something else entirely. The following would generate an error.

var str = MyNumberItem.state.toString() // transform it to a String
var cast = str as Number // Generates an error because you can't cast a String to a Number

I bring it up because it’s a distinction that matters in conversations like this.

So again, this is transforming to a JavaScript Date, not back to a DateTimeType or ZonedDateTime. The fact that it does successfully parse it though is interesting. It should be getting something like 1242 s or 999 min or whatever the default units is.

That means that even at this point the QuantityType's toString is out putting at least a Time String; something that JavaScript’s Date can parse.

You can see what exactly is being passed to the transform using \

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.dt transform");
logger.info(dateTimeString);

That’s because the JavaScript Date.parse function returns epoch, number of milliseconds since 1970-01-01t00:00:00.

There is no error there. It’s working exactly as it’s documented to work. Date.parse() - JavaScript | MDN

So what changed here is:

  • you’ve gone from an erroneous configuration to a correct configuration; you can’t link a Channel to any old Item, the types must match and the required type for that Channel is Number:Time

  • You are explicitly formatting the state of the Number:Time as an integer with the %d

  • Presumably the default units is minutes.

So this of course works as expected.

True, but by that point you’d be dealing with a String along the lines of 123 min right?

I think the DateTime string being passed to the JS transformation was a red herring.

It was a valid String. Both 1970-01-01T03:18:00 and 1970-01-01T01:03:18+01:00 are valid ISO8601 formatted date time string that Date.parse() can handle. The problem wasn’t a bad String, it was a misunderstanding of what Date.parse() returns.

I completely agree except for the fact that the DateTime String format patterns work with a Number:Time which means that somewhere in the code, the Number:Time is either stored as a Temporal (e.g. ZonedDateTime) or it’s converted to a Temporal. That could explain where the DateTime String came from.

I agree but I also agree with rossko. OH was never designed to support linking a Number:Time Channel to a String Item in the first place. Doing so results in strange behavior. But in this case I’m not certain the behavior is all that strange.

I’ve been through the QuantityType code quite a bit this afternoon. I can say with some confidence this is not happening. QuantityType and DateTimeType have nothing to do with eachother.

I don’t think that’s happening either. I think what’s happening is that the QuantityType, way up in the upstream libraries is using a temporal type to actually store the value and when you linked to a String Item it went through a default conversion which resulted in a date time formatted String.

But QuantityType and DecimalType are not the same. Number:Time is a QuantityType, not a DecimalType.

Looking at GitHub - unitsofmeasurement/indriya: JSR 385 - Reference Implementation I’m seeing all sorts of stuff from java.time being used with quantity type Time so it seems possible that something has gone awry when linking the Number:Time to a String Item without a transform profile.

But then that’s not supported by OH anyway so is it really a bug?

Why? It’s invalid to link a number type channel to a String type Item. This is what profiles are for.

Have you tried linking say a number:temperature type Item to a String? I think perhaps there’s some confusion because you started off with a number that just happened to represent some time value.

Yeah. Let’s not talk any more about about Number:Time Items – the binding does not send a QuantityType(Time) value; it sends a DecimalType; my OP was a mistake. Let us just focus on the DecimalType → StringType Item conversion.

Concerning your second point first (“This is what profiles are for.”): Could you please give an example of how you would use a profile to link a Number/DecimalType Channel to a StringType Item?

Concerning your first point ("Why? "): I dunno really. It’s just that if one does make an “invalid” link between a Number/DecimalType Channel to a StringType Item, it is very odd (at least to me) that a Number/DecimalType value of (say) 123 would appear in the String Item as the string “1970-01-01T00:02:03Z” rather than the more obvious string “123”. (PS I thought you yourself complained about the framework sometimes delivering unexpected date time string values?)

1 Like

I guess so, partly. :slight_smile: The OP talked about Number:Time but the subsequent discussion has diverged from that. The Number:Time was my mistake (sorry), so lets not talk about that. My discussion is about how a Number/DecimalType Channel value (say 123) appears in a StringType Item as a date-timey string (say “1970-01-01T00:02:03Z”) rather than the more obvious string “123”…

Sure

String myTarget "some string" { channel="blah:bleh:temperature:number" [profile="transform:JS", function="silly.js"] }

silly.js

(function(i) {
    if (i == 'UNDEF') { return i; }
    var num = parseFloat(i); // The value sent by OH is a string so we parse
    if (num > 21) { return 'Its too hot'; }
    else { return 'Its too cold'; }
})(input)

Obviously similar methods can be used to format or scale numbers, taking account of any units if Quantities are involved.

I think what you’re worrying at is the weird datetimey turning up in the profile - it doesn’t. It’s an artefact coming from trying to stuff a “naked” numeric into a String. The transform profile machinery specifically delivers a “.toString” version of the channel state to the script.

“But wait” why don’t we make an enhancement that just passes along this same string that gets given to a profile, i.e. create/amend a default profile? It seems simple enough for this specific instance of stuffing number into String.

But it’s still going to do weird stuff when we link number channel to Color type Item etc. It can only ever be a ‘partial fix’.
In my view, do nothing, don’t second-guess what the user wants, make them be specific and set up a proper profile.
This is mi-matching not a beginners thing to do after all, it is usually a misconfiguration made in error, unless you have some specific manipulation in mind (i.e. a profile)

It’s quite bizarre. But my view is that you’ve no reason to expect anything else in particular, it’s just an invalid link.
It’s difficult for the framework or UI to actively block or highlight such invalid links though - you might have a perfectly legit profile in use to make it okay.
But maybe that’s the area that needs attention.

Note that MainUI did previously not offer to make mis-matched channel-Item links. That had to be changed because it prevented people using profiles correctly.