Display Number:Time total day count in sitemap

There have been several posts explaining how to format Number:Time as strings, but most have concentrated on formatting hours, minutes, seconds like “HH:mm:ss” … my question is how to format my CPU uptime as DAYS, hours, minutes “DD:hh:mm” i.e. to display the total elapsed number of days (up time) in the state description text. I have tried various variations of the following without success. (The hours and minutes parts are Ok…)

Number:Time System_CPU_Uptime "System CPU Uptime [%1$td:%1$tH:%1$tm]" <time> {channel="systeminfo:computer:g24:cpu#uptime"}

Number:Time System_CPU_Uptime "System CPU Uptime [%1$tD:%1$tH:%1$tm]" <time> {channel="systeminfo:computer:g24:cpu#uptime"}

EDIT: umm, I am embarassed to note that I posted this two years ago. And I had found a solution at that time. But unfortunately it seems that the recent UoM changes in OH 4.x have broken that old solution. So it seems not entirely stupid to post the same question again…

The Number:Time formatting is kind of a hack so it has limitations. It will only work for days under 31. At that point it goes back to zero.

The way it works is it takes epoch (i.e. 1970-01-01T00:00:00.000) and adds the Number:Time to that. So when you are formatting it you are really formatting 1970-01-05T01:02:03. Once you get to 32 days you are now in February so you have 1970-02-01T01:02:03.

And as was described in that thread, trying to use the Julian data isn’t going to work. So I think you need to use a transform unless you are certain that your uptime will never exceed 31 days.

The tranform that you posted in that thread isn’t working not because of units but because Nashorn JS was a lot more “forgiving” in how it converted various data types from one to another. Given the following in GraalVM JS:

(function(mins) {
    var days = Math.floor(mins / (24 * 60));
    mins = mins % (24 * 60);
    var hours = Math.floor(mins / 60);
    mins = mins % 60;
    return days + "d, " + hours + "h, " +  mins + "m";
})(input) 

input and therefore mins is a String. They have always been a String but Nashorn auto converted the String to a Number because that’s what Math.floor wants. And frankly it’s a little shady about it because 123 s isn’t a number, but Nashorn just ignored the s.

ECMAScript 11 treats that as an error. If you want to do math with the input, you need to parse it.

But all is not lost. The JS Scripting helper library is available so we can use the tools available there and don’t have to mess with the math ourselves too much.

(function(durStr) {
    const zeroPad = (num, places) => String(num).padStart(places, '0');

    const dur = Quantity(durStr); // parse the String to a Quantity
    const seconds = dur.toUnit('s').number; // convert the value to seconds and get the number
    const durationObj = time.Duration.parse('PT'+seconds+'S'); // Create a Duration Object

    const days = durationObj.toDays();
    const hours = durationObj.minus(time.Duration.ofDays(days)).toHours(); // subtract off the days
    const minutes = durationObj.minus(time.Duration.ofDays(days).plus(hours)); // subtract off the days and hours

    return days + ':' + zeroPad(hours, 2) + ':' + zeroPad(minutes, 2);    
})(input)

Or if you are OK with ISO8601 formatting you can just return the toString of durationObj but that’s not going to show days, just hours.

The Duration built into Java is a little more convenient as it has getHoursOfDay() and such which completely eliminates the need to do the math. But I like to stick to JS where I can. But that code would look something like:

(function(durStr) {
    const zeroPad = (num, places) => String(num).padStart(places, '0');
    const Duration = Java.type('java.time.Duration');

    const dur = Quantity(durStr); 
    const seconds = dur.toUnit('s').number;
    const durationObj = Duration.ofSeconds(seconds)

    return durationObj.toDays() + ':' + zeroPad(durationObj.toHoursPart(), 2) + ':' + zeroPad(durationObj.toMinutesPart(), 2);    
})(input)

Of if you just want to do the math youself:

(function(minsStr) {
    var mins = Quantity(minStr).toUnit('min').number
    var days = Math.floor(mins / (24 * 60));
    mins = mins % (24 * 60);
    var hours = Math.floor(mins / 60);
    mins = mins % 60;
    return days + "d, " + hours + "h, " +  mins + "m";
})(input) 
1 Like

many thanks @rlkoshak … as ever a brilliant answer.

I have chosen to go with the solution quoted above.

EDITED !!

@rlkoshak just so you know, there are still a couple of issue…

  1. There is a typo in your code minsStr and minStr
  2. The code throws the following exception…
2023-11-30 17:25:00.392 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: Error: Failed to create QuantityType from NULL: java.lang.NumberFormatException: Invalid BigDecimal value: NULL
	at <js>.o(@openhab-globals.js:2) ~[?:?]
	at <js>.O(@openhab-globals.js:2) ~[?:?]
	at <js>.c(@openhab-globals.js:2) ~[?:?]
	at <js>.getQuantity(@openhab-globals.js:2) ~[?:?]
	at <js>.:anonymous(<eval>:2) ~[?:?]
	at <js>.:program(<eval>:1) ~[?:?]
	at org.graalvm.polyglot.Context.eval(Context.java:399) ~[?:?]
	at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458) ~[?:?]
	at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426) ~[?:?]
	at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262) ~[java.scripting:?]
	at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
	at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
	at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
	at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
	at org.openhab.core.automation.module.script.ScriptTransformationService.transform(ScriptTransformationService.java:213) ~[?:?]
	at org.openhab.core.transform.TransformationHelper.transform(TransformationHelper.java:168) ~[?:?]
	at org.openhab.core.transform.TransformationHelper.transform(TransformationHelper.java:143) ~[?:?]
	at org.openhab.core.transform.TransformationHelper.transform(TransformationHelper.java:124) ~[?:?]
	at org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper.considerTransformation(EnrichedItemDTOMapper.java:152) ~[?:?]
	at org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper.map(EnrichedItemDTOMapper.java:82) ~[?:?]
	at org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper.map(EnrichedItemDTOMapper.java:65) ~[?:?]
	at org.openhab.core.io.rest.core.internal.item.ItemResource.lambda$7(ItemResource.java:282) ~[?:?]
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
	at java.util.HashMap$KeySpliterator.tryAdvance(HashMap.java:1728) ~[?:?]
	at java.util.stream.StreamSpliterators$WrappingSpliterator.lambda$initPartialTraversalState$0(StreamSpliterators.java:292) ~[?:?]
	at java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.fillBuffer(StreamSpliterators.java:206) ~[?:?]
	at java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.doAdvance(StreamSpliterators.java:169) ~[?:?]
	at java.util.stream.StreamSpliterators$WrappingSpliterator.tryAdvance(StreamSpliterators.java:298) ~[?:?]
	at java.util.Spliterators$1Adapter.hasNext(Spliterators.java:681) ~[?:?]
	at org.openhab.core.io.rest.Stream2JSONInputStream.fillBuffer(Stream2JSONInputStream.java:91) ~[?:?]
	at org.openhab.core.io.rest.Stream2JSONInputStream.read(Stream2JSONInputStream.java:67) ~[?:?]
	at java.io.InputStream.read(InputStream.java:293) ~[?:?]
	at java.io.InputStream.transferTo(InputStream.java:782) ~[?:?]
	at org.openhab.core.io.rest.core.internal.GsonMessageBodyWriter.writeTo(GsonMessageBodyWriter.java:83) ~[?:?]
	at org.openhab.core.io.rest.core.internal.MediaTypeExtension.writeTo(MediaTypeExtension.java:84) ~[?:?]
	at org.apache.cxf.jaxrs.utils.JAXRSUtils.writeMessageBody(JAXRSUtils.java:1651) ~[bundleFile:3.6.2]
	at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.serializeMessage(JAXRSOutInterceptor.java:249) ~[bundleFile:3.6.2]
	at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.processResponse(JAXRSOutInterceptor.java:122) ~[bundleFile:3.6.2]
	at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.handleMessage(JAXRSOutInterceptor.java:84) ~[bundleFile:3.6.2]
	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) ~[bundleFile:3.6.2]
	at org.apache.cxf.interceptor.OutgoingChainInterceptor.handleMessage(OutgoingChainInterceptor.java:90) ~[bundleFile:3.6.2]
	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:225) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:304) ~[bundleFile:3.6.2]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doGet(AbstractHTTPServlet.java:222) ~[bundleFile:3.6.2]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:497) ~[bundleFile:4.0.4]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:279) ~[bundleFile:3.6.2]
	at org.ops4j.pax.web.service.spi.servlet.OsgiInitializedServlet.service(OsgiInitializedServlet.java:102) ~[bundleFile:?]
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1656) ~[bundleFile:9.4.52.v20230823]
	at org.ops4j.pax.web.service.spi.servlet.OsgiFilterChain.doFilter(OsgiFilterChain.java:100) ~[bundleFile:?]
	at org.ops4j.pax.web.service.jetty.internal.PaxWebServletHandler.doHandle(PaxWebServletHandler.java:320) ~[bundleFile:?]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:600) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:505) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:234) ~[bundleFile:9.4.52.v20230823]
	at org.ops4j.pax.web.service.jetty.internal.PrioritizedHandlerCollection.handle(PrioritizedHandlerCollection.java:96) ~[bundleFile:?]
	at org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:772) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487) ~[bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:732) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:479) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:409) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883) [bundleFile:9.4.52.v20230823]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034) [bundleFile:9.4.52.v20230823]
	at java.lang.Thread.run(Thread.java:840) [?:?]
2023-11-30 17:25:00.523 [WARN ] [rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state 'NULL' on item 'System_CPU_Uptime' with pattern 'JS(24g-uptime.js):%s': Failed to execute script.

I didn’t try to handle the NULL and UNDEF cases. Add to the top of the transform:

if(minsStr == "NULL" || minsStr == "UNDEF") return minStr;

That just passes NULL and UNDEF updates on through to the Item untransformed.

But it even fails when the passed value is NOT null (it still gives the Null exception). Very odd…

Indeed because it’s complaining about "NULL", not null. "NULL" is the string version of an Item state and that’s what the error is complaining about.

Log out minsStr (you can do anything you can do in a rule so console.info(...)

Thanks for the tip :slight_smile:
Yet unfortunately the code below still produces the log further below :frowning:

(function(minsStr) {
  console.info(minsStr);
    if (minsStr == "NULL" || minsStr == "UNDEF") return minsStr;
    var mins = Quantity(minsStr).toUnit('min').number;
  console.info(mins);
    var days = Math.floor(mins / (24 * 60));
    mins = mins % (24 * 60);
    var hours = Math.floor(mins / 60);
    mins = Math.floor(mins % 60);
    return days + "d, " + hours + "h, " +  mins + "m";
})(input)

2023-11-30 18:12:07.181 [INFO ] [org.openhab.automation.script       ] - 17994 s
2023-11-30 18:12:07.209 [INFO ] [org.openhab.automation.script       ] - undefined

It’s not generating an error at least.

It doesn’t make sense what it’s logging out though. Maybe the number data member is somehow private?

Try

var mins = Quantity(minsStr).toUnit('min').getInt();

Double check that the unit for minutes is min also.

According to OH docs, min is indeed minutes. (as m is meters)

Ok, I will try that. (I already tried intValue, but that failed…)

Yes, that’s how you would do it in Rules DSL. In JS Scripting there’s a wrapper to make it a JS Object instead of a Java Object.

Nope: getInt() produces the error below; whereas getInt without brackets returns ‘undefined’

2023-11-30 21:41:52.100 [INFO ] [org.openhab.automation.script       ] - 10338 s
2023-11-30 21:41:52.117 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: TypeError: (intermediate value)(...).toUnit(...).getInt is not a function

The code is quantity.js - Documentation

Maybe it’s just int?

Yup. That works! Many thanks @rlkoshak

(function(minsStr) {
    if (minsStr == "NULL" || minsStr == "UNDEF") return minsStr;
    var mins = Quantity(minsStr).toUnit("min").int;
    var days = Math.floor(mins / (24 * 60));
    mins = mins % (24 * 60);
    var hours = Math.floor(mins / 60);
    mins = Math.floor(mins % 60);
    return days + "d, " + hours + "h, " +  mins + "m";
})(input)