Transform absolute time (DateTimeType) to relative time

Hi everyone,
I am really enjoying for example my iPhone telling me that I received this mail 5 minutes ago etc.

This is something I would love to see in openHAB! :heart_eyes:

I have some motion sensors (you win a special virtual free hug, if you guess which ones :wink:), I want to track also with some items like “last time triggered”.

But seeing the same format all the time is boring, I want this to be more natural, which I think relative time formats are built for right?

So instead of OH showing me this: 2017-04-06 21:02 or even 2017-04-06T21:02:21.708+0200
I want it to be : 45 minutes ago or yesterday 21:02

I found a really chunky lib in JS for that --> Moment.js
It seems perfect!

Now I’m not an JS expert, so who can give me a quick startup “howto” with this one?

Something like this
var DateTime Startup_time // must be set at the event
in a different rule
var Long zeit = (now.getMillis() - Startup_time.getMillis())/60000
var String out
days = zeit/1440
zeit = zeit - days*1440
sec = zeit % 60
hour = zeit/60
out = days+ " d "+ hour + “:”+ sec + " h "

Hey @Marty56, thanks for your answer!
Getting only the relative time value is not hard, that’s true.
But I want to avoid writing a lot of rules to transform one output line to “yesterday” or “15 minutes ago”, as I would have to distinguish a lot of time ranges.
The lib I linked to does a lot of this already, I just have to hook it up somehow to OH.

So the question is, is there perhaps somebody who has already done this?

My basic idea would be, get the DataTimeType and throw it at Moment.js which then gets me back some nice formatted relative time string.

Hey @dimalo,

It’s not the greatest solution available, but it should work as you expect.

  1. First, you’ll need the JavaScript transformation installed.
  2. Create a file in /transform/ folder called relative.js
  3. Get the latest moment.min.js (16 kb)
  4. Open relative.js file and paste the code below:
(function(i) {
    /* PASTE moment.min.js HERE */
    return moment(new Date(i)).fromNow();

Finally, paste the moment.js source where the comment is - just above the return ... line.

Then in your *.items file:

DateTime LastTimeTriggered "Last time triggered [JS(relative.js):%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]"

(didn’t check it - there might be error somewhere).

The downside of this solution is that it will probably load the whole script each time Item receives the value.
I’d love to see Moment.js integrated with the UIs, though! :wink:


@kubawolanin exactly, what I was looking for!
changed this via vpn as I’m not at home, because I was so curious if it works! now guess what, who is going to trigger my motion sensor?! :smile:

Free Hug to @kubawolanin

only thing I changed is the format of the return value as I expect it to be a string - but didn’t check either

DateTime LastTimeTriggered "Last time triggered [JS(relative.js):%s]"

Also I wanted to go with moment_with_locales.

I’ll check this with some workaround and feedback then…


already set up and getting some complicated output:

2017-04-14 22:02:08.971 [ERROR] [ui.internal.items.ItemUIRegistryImpl] - transformation throws exception [transformation=org.eclipse.smarthome.transform.javascript.internal.JavaScriptTransformationService@84a8aa, value=java.util.GregorianCalendar[time=1492200124000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Berlin",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2017,MONTH=3,WEEK_OF_YEAR=15,WEEK_OF_MONTH=2,DAY_OF_MONTH=14,DAY_OF_YEAR=104,DAY_OF_WEEK=6,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=2,SECOND=4,MILLISECOND=0,ZONE_OFFSET=3600000,DST_OFFSET=3600000]]
org.eclipse.smarthome.core.transform.TransformationException: An error occured while executing script.
	at org.eclipse.smarthome.transform.javascript.internal.JavaScriptTransformationService.transform([189:org.eclipse.smarthome.transform.javascript:]
	at org.eclipse.smarthome.ui.internal.items.ItemUIRegistryImpl.transform([140:org.eclipse.smarthome.ui:]
	at org.eclipse.smarthome.ui.internal.items.ItemUIRegistryImpl.getLabel([140:org.eclipse.smarthome.ui:]
	at org.eclipse.smarthome.ui.basic.internal.render.AbstractWidgetRenderer.getValue([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.render.AbstractWidgetRenderer.preprocessSnippet([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.render.TextRenderer.renderWidget([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.render.PageRenderer.renderWidget([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.render.PageRenderer.processChildren([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.render.PageRenderer.processPage([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.smarthome.ui.basic.internal.servlet.WebAppServlet.service([192:org.eclipse.smarthome.ui.basic:]
	at org.eclipse.jetty.servlet.ServletHolder.handle([85:org.eclipse.jetty.servlet:9.2.19.v20160908]
	at org.eclipse.jetty.servlet.ServletHandler.doHandle([85:org.eclipse.jetty.servlet:9.2.19.v20160908]
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceServletHandler.doHandle([176:org.ops4j.pax.web.pax-web-jetty:4.3.0]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.session.SessionHandler.doHandle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceContext.doHandle([176:org.ops4j.pax.web.pax-web-jetty:4.3.0]
	at org.eclipse.jetty.servlet.ServletHandler.doScope([85:org.eclipse.jetty.servlet:9.2.19.v20160908]
	at org.eclipse.jetty.server.session.SessionHandler.doScope([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.handler.ContextHandler.doScope([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.ops4j.pax.web.service.jetty.internal.JettyServerHandlerCollection.handle([176:org.ops4j.pax.web.pax-web-jetty:4.3.0]
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.Server.handle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.HttpChannel.handle([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.server.HttpConnection.onFillable([84:org.eclipse.jetty.server:9.2.19.v20160908]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob([87:org.eclipse.jetty.util:9.2.19.v20160908]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$[87:org.eclipse.jetty.util:9.2.19.v20160908]
Caused by: javax.script.ScriptException: <eval>:2:17 Unsupported RegExp flag: o
                 ^ in <eval> at line number 2 at column number 17
	at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException([nashorn.jar:]
	at jdk.nashorn.api.scripting.NashornScriptEngine.compileImpl([nashorn.jar:]
	at jdk.nashorn.api.scripting.NashornScriptEngine.compileImpl([nashorn.jar:]
	at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl([nashorn.jar:]
	at jdk.nashorn.api.scripting.NashornScriptEngine.eval([nashorn.jar:]
	at javax.script.AbstractScriptEngine.eval([:1.8.0_121]
	at org.eclipse.smarthome.transform.javascript.internal.JavaScriptTransformationService.transform([189:org.eclipse.smarthome.transform.javascript:]
	... 30 more
Caused by: jdk.nashorn.internal.runtime.ParserException: <eval>:2:17 Unsupported RegExp flag: o
	at jdk.nashorn.internal.parser.AbstractParser.error([nashorn.jar:]
	at jdk.nashorn.internal.parser.AbstractParser.error([nashorn.jar:]
	at jdk.nashorn.internal.parser.AbstractParser.getLiteral([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.primaryExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.memberExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.leftHandSideExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.unaryExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.expression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.expressionStatement([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.statement([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.sourceElements([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.functionBody([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.functionExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.memberExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.leftHandSideExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.unaryExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.expression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.primaryExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.memberExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.leftHandSideExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.unaryExpression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.expression([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.expressionStatement([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.statement([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.sourceElements([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.program([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.parse([nashorn.jar:]
	at jdk.nashorn.internal.parser.Parser.parse([nashorn.jar:]
	at jdk.nashorn.internal.runtime.Context.compile([nashorn.jar:]
	at jdk.nashorn.internal.runtime.Context.compileScript([nashorn.jar:]
	at jdk.nashorn.internal.runtime.Context.compileScript([nashorn.jar:]
	at jdk.nashorn.api.scripting.NashornScriptEngine.compileImpl([nashorn.jar:]
	... 35 more

Yeah perhaps I should not just paste the path. Really uncertain with js ar the moment :slight_smile:

1 Like

OK now I’m so far:

@kubawolanin I should have listened to you…

If set it up as you proposed everything works great!
Even moment with locales works fine when adding for example moment.locale('de'); before return.
But as you said, response times when reloading the sitemap are not satisfying.
Accessing the sitemap from the iPhone app is just impossible as it throws an internal server error response…

Really sad :frowning2:

Thanks for this topic! I’ve been using this method to simplify time information and it really helps a lot.

Aside from the heaviness of it, this method works quite brilliantly and quickly in my environment. The one thing I haven’t been able to figure out after 3 weeks is a strange error which appears regularly. Can anyone help?

The warnings appear regularly for any item (about 25 right now) where I’m using this transformation. However, there are no detectable problems in usability, and as far as I can tell, the conversions are always working correctly.

The DateTime formats OH is complaining about seem correct.

2019-04-15 14:45:40.603 [WARN ] [rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state '2019-04-15T14:41:07.132-0700' on item 'KitchenDoorLastUpdate' with pattern 'JS(relative.js):%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS': Cannot format state '2019-04-15T14:41:07.132-0700' to format '%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS'

Is there any update on that issue? I’d love to display in the sitemap the datetime object in “time ago”

I was never able to find the cause for the warnings, but it still works fine

I have 50 wifi devices that return sensors every 10 seconds. I update “last seen” item for every of those 50. Do you thing that loading 16kb of javascript every 200ms on average is not going to kill my OH server?
Is there a leaner way to achieve that?

I have no idea what will or won’t kill your OH server. I’m just sharing that it works for me, even though it’s obviously not ideal.

I don’t know what you expect out of an open source community with such an entitled tone, but that’s the great part about open source isn’t it? If you have a better way, feel free to add.

Stumbled on this thread, bit late :crazy_face:

Transformations used in label [format] always return strings.
Strings are not datetimes and cannot be formatted using %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
The solution is just to format as string, %s, as @dimalo has done

DateTime LastTimeTriggered "Last time triggered [JS(relative.js):%s]"
1 Like

Wow, that solved the warnings! I totally overlooked that. Thanks so much!!!

It wasn’t a breaking issue, but definitely helped clear up something that’s been nagging at me for a while. Appreciate your help!

1 Like

Out of interest, how effective is this technique at keeping an up-to-date display?

I ask because openHAB UI refreshes are usually linked to Item state changes.
Here, the actual state (timestamp) does not change, even if the transform is going to give you different “display state” each time you run it.

It seems to allow the possibility that a transformed “5 minutes ago” may never refresh to “6 minutes”.
Might all come right if other Item state changes are triggering UI refresh.

Hrm, excellent question. To be honest, that hasn’t crossed my mind since we don’t use relative time on a UI which needs to be shown persistently.

In our case, relative time is shown on either the OH android client on our phones when we actively check, or its piped to other apps with a video stream, so the content refreshes on update.

I’ve just tested out your question and you’re right, the behavior is as you’ve described. The relative time fields only update when an item triggers the UI refresh. This means for something like a wall mounted tablet or dedicated OH monitoring station, it might not work well.

Put another way, if the transformed field itself is changed (“5 minutes ago” changes to “a minute ago”) the field itself will refresh correctly. However, if there is never a change, and nothing else triggers a refresh, the page will continue to show the time relative to when the page was loaded (“5 minutes ago” will always remain). At least this is the behavior for the Android OH client.

1 Like

Thankyou, that makes sense.
In many use-cases it will most likely work reasonably, but not all.

I confirm that.
In my specific case, I implemented this solution and use it in two ways:

  • in the openHAB android app, in a dedicated separate page
  • in HABPanel shown in a wall mounted tablet, inside a modal window

Since in both cases I have to click/tap something to show the update time in relative format, it is working for me. In other words, if I leave the page there, without refreshing it, “5 minutes ago” doesn’t automatically change to “6 minutes ago” after one minute has passed. If I refresh the page, or if I navigate elsewhere and then back to the page, it updates. If the item state changes in the meanwhile, of course even without touching anything “5 minutes ago” turns to “a few seconds ago”.

So it works if you want to receive the relative time “on demand”, so the UI is somehow refreshed. It doesn’t work if you leave the page untouched. This of course unless in the meanwhile there is an item state change that triggers a refresh of the UI.

instead of using the moment.js, i am using the below crudely written JavaScript code to do the same job.
Then using transformation, a string type with a data time stamp is converted to relative time.
Just sharing in case its helpful to anyone.

(function(i) {
    var eTime = "00:00:00";
    var i_date = new Date(i);
    var z_date = new Date ();   
    if (i !== null) {
        var eSecs = (z_date.getTime()-i_date.getTime())/1000;
        // Calculate d/h/m/s without using modulus
        var d = parseInt((eSecs / (60*60*24)));                       // Div by 86400
        var h = parseInt(((eSecs - (d*60*60*24)) / (60*60)));         // Div by 3600
        var m = parseInt(((eSecs - (d*60*60*24) - (h*60*60)) / 60));  // Div by 60
        var s = parseInt((eSecs - (d*60*60*24) - (h*60*60) - (m*60)));         // Remainder = secs
        if (d >=1) { eTime = d+" days "+h+" hours "+m+" minutes Ago"; }
        if (d < 1 && h >=1) { eTime = h+" hours "+m+" minutes Ago"; }
        if (h<1) { eTime= m+" minutes Ago"; }
        return eTime;

You can use d, h, m, s to format output string as you like.
full credits to this code here: Lambda Function to get Elapsed Time

1 Like

I think this is only run when the values are displayed in BasicUI - it’s not actually run every time there is an update.