Node-red: adding a time to a timestamp

Hi!
I have a timer where I set the time manually. And I need to know the time when the timer will come to end. For example, if I set the timer to 20 minutes at 20:00 I want to get an output “20:20”
I tried to do it with node-red-contrib-moment but didnt find an option to add a time to a timestamp with msg.payload…

There are several ways to do this. The $moment() function I was not aware of (not documented?) but a problem is that this works in UTC, hence most of us will find the time out by the local timezone offset, and it can be a lot of work to get and use the local timezone, and account for DST (which the contrib moment node does for you).

Working on the basis that you want just the time, and you want it as a string “hh:mm”, and you want to work in your local time (and you don’t particularly care about DST changes), and your input msg.payload is the minutes difference as an integer number.

You can parse the time now hh:mm, turn into minutes, add your minutes delta, then convert back into hh:mm

If you have the date/time integration added, you can easily get hold of the sensor.time which is the local time in hh:mm, which makes working with local time quite easy.

The following uses a Change node, and gets the sensor.time value from the HA Global context variable state value (which you will have if you have the WebSocket nodes setup) and adds msg.payload minutes to ‘now’ returning the new time ‘hh:mm’

[{"id":"51563fc88252336f","type":"change","z":"227fcbf9d11adcdf","name":"Add delta minutes to now","rules":[{"t":"set","p":"time","pt":"msg","to":"homeassistant.homeAssistant.states['sensor.time'].state","tot":"global"},{"t":"set","p":"payload","pt":"msg","to":"(\t    $x:=$split(time,\":\").$number();\t    $y:=$x[0]*60+$x[1]+payload;\t    $mins:=$y%60;\t    $hours:=(($y-$mins)/60)%24;\t    $pad($hours & \"\", -2, \"0\") & \":\" & $pad($mins & \"\", -2, \"0\");\t    \t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":580,"wires":[["5f2e6cadfa2c5589"]]},{"id":"1f283c436b05526e","type":"inject","z":"227fcbf9d11adcdf","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"37","payloadType":"num","x":290,"y":580,"wires":[["51563fc88252336f"]]},{"id":"5f2e6cadfa2c5589","type":"debug","z":"227fcbf9d11adcdf","name":"debug 210","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":690,"y":580,"wires":[]}]

The add function?

https://momentjs.com/docs/#/manipulating/add/

Moment has 2 ways to cast to local

The local() flag and using format with the local format flags.

Yup I was aware of the moment.js library. I’m currently using it in a function node, with the timezone library. Just did not know it was in JSONata. I saw your earlier post, but I assume it must be a special function added for Node-RED?

So, $moment().local() works, and $moment().add($$.payload,‘m’) works. The question is, how do you apply two methods to the one moment?

$moment().add($$.payload, ‘m’).local() certainly does not do both!

I haven’t really got the hang of creating variables in jsonata. It looks like you would need to set the value to a variable and then use the local function on the variable. Something like.

(
    $a := $moment().add($$.payload, ‘m’);
    $b := $moment($a).local();
)

Funny enough I can’t get moment to work in a function node only jsonata. I add the module on the setup tab but it still doesn’t seem to be available.

It looks like rearranging the function works,

$moment().local().add($$.payload, ‘m’)

Hmmm. The plot thickens.

$moment() is not a function in the JSONata package, it certainly does not work in the try-jsonata online. It must therefore be added (as several others are) for Node-RED. The question I have is who or what adds it. NR adds the context variable functions, and the WebSocket nodes add the $entity() functions. I am always suspicious of things that “work” but are not documented.

moment library I have working in a function node (but I would much rather use it in JSONata, thank you for this!). I add it using the new ‘module’ feature in function node setup. Of course, the first time the node is called, the module library is pulled down and added to the local module directory, so all I can really say is that it worked a few months ago. Be a real pain if this feature has been broken (to add $moment() to JSONata).

Variables in JSONata I am confident with, and your code should work. A moment is an object that gets created, and can then be manipulated, so I can’t see anything wrong with your code.

I did try both two methods chained, and two methods nested. Using

$moment().local().add($$.payload, ‘m’)

has and does not work for me.

Earlier, I found $moment().local() was working. Now, it is not. I wonder if this is a ‘false feature’ similar to the apparent ability for Node-RED to execute mustache templates, but it does not really work.

The question I have remains - where did you first find out about $moment() as a JSONata function? Is this actually documented somewhere?

Note:
I think .local() sets an internal flag in the moment, which acts like a switch. Using .local(true) on a moment locks that moment instance to the local TZ. Does not seem to be working consistently though…

In my struggles of trying to deal with date times and looking at the documentation of the time formatter node led me to moment. Some where along the line of researching moment, I found a post saying it could be added to the system packages.

I’m using the addon and added moment to the system package list.

system_packages:
  - moment
  - nmap
  - musl
  - build-base

I think I also had to add musl and build base for it but I don’t remember nor can find the post I originally saw it in. I used to use it a lot a few years ago and it worked reliably. I’ve gotten used to the js date obj and all time related processes are now in functions.

All this needs to be taken with a grain of salt as this is all stuff I’ve cobbled together. I don’t know if I was using it correctly or it was just happen stance that I got my desired result.

In case you are interested…

moment.js is I understand the current preferred time manipulation library, and with the addtional timezone package it manages timezones and DTS.

$moment() appears to be something someone packaged for JSONata, and I suspect managed to get it added to the Node-RED release at some point. Hence $moment() in JSONata works. Just.

It may well not be documented as it may well have gone in under the radar and is unsupported. Since there is a plan to move away from moment.js to luxon, I believe the NR team are not keen on adding dependencies that may later go, although my reading up suggests that moment.js is already there in Node-RED for cron.

Given that $moment() appears therefore to be a private addition to NR, and without formal documentation, using it is going to be tricky. It does support

$moment().format(‘LL’)

which is rather odd, since the ‘.’ operator in JSONata is the JSON path or mapping operator, suggesting that $moment() in JSONata returns an object that can be mapped by a field ‘format’. Hence $moment() returns an object with pre-defined fields, and does not have “methods” as such. Yuk.

Experimenting shows that the .format() “method” works (as does .add() and they can be chained) but I don’t think .local() does. Using .format(‘LT’) gives local time, but since JSONata does not pick up the locale setting (yes a real pain…) this just chucks out “11:19 PM” when I am in the UK and I am set for time format as “23:19”.

Therefore, given that the OP wanted the time from now, incremented by msg.payload minutes, and processed as local time “hh:mm” using the 24 hour format, I believe the following JSONata code is required to deal with AM / PM and square 12:05 AM to 00:05.

(
    $endtime:=$moment().add($$.payload, 'm').format('LT');
    $time:=$substringBefore($endtime, " ");
    $hours:=$substringBefore($time, ":");
    $hours:=$contains($endtime, "PM") ? "" & ($number($hours)+12) : $hours;
    $hours:=$contains($endtime, "AM") and $hours="12" ? "00" : $hours;
    $hours & ":" & $substringAfter($time, ":");

)

An “interesting” [ie pointless] exercise that goes some way to satisfying my inherent curiosity as to where $moment() in JSONata has come from (although I still don’t really know for sure) however this is just so messy and unreliable that I would still set Home Assistant with the local (time) sensor and use that.

:smiley:

@Jojioe Sorry for going off on a tangent here please let us know if you have yet to find a working solution.

Always :+1:

It’s a shame they are scraping moment, it’s one of the few things I didn’t have read 10x to get an understanding of. Luxon at first glance seems convoluted like the js datetime obj.

I guess I will need to use it to see. Do you know how I can get it installed in the addon. Adding luxon to system packages fails with a warning. Adding to npm I don’t see anything either way installed/failed, in the logs.

From what I read should be able to create a time stamp with DateTime.now() in a function node? I get DateTime not found.

Edit: I can see luxon is installed in node_modules and I added

  functionGlobalContext: {
    env: process.env,
    luxon:require('luxon')
  },

To settings.js but I still don’t have access to it in a function.

Very tangential, but interesting and still relevant for date-time manipulation.

I am sure you have a lot more experience with this stuff than I do. I am just good at searching, reading up on a subject, and giving a problem a bit of thinking about.

moment.js must take quite a bit of upkeep, particularly the timezone stuff, and I think there is a general feeling it is time to move to something else (ie it has had its moment in time). I have not, yet, experimented with Luxon, so this will be new to me.

I have managed to install and use it in a function node.
Since this is a library module, it needs to be ‘required’, but Node-RED does not like requiring things, so I use the new module load feature in the function node settings.

I have set this to load module ‘luxon’ as ‘luxon’.

Of course, DateTime() is unknown as it is an exposed function in the luxon library, hence once imported as ‘luxon’ the call is made using

const now = luxon.DateTime.now();

which finds DateTime() inside of the module ‘luxon’. Messy but it works.

The following also works perfectly, returing a JS DateTime object, which the Node-RED debug window correctly displays using the locale settings. Neat!

msg.payload = luxon.DateTime.now().plus({minutes: msg.payload});
return msg;

Maybe it will get added to JSONata at some point! That would improve the time/date handing hugely.

1 Like

Thank you for your replies! I’ll try your methods and leave a comment soon