Elapsed time rule for an Xbox in Javascript

Hi there

Lately I have a bit of a challenge with my son spending significant time on his Xbox and perhaps not doing enough real life things.

I wanted to develop a rule that,

  • Alerts all of the family when the Xbox turns ON/OFF
  • Alerts all of us if it is on longer than an agreed time, 2hr via a expire metadata

So far these two are working just fine with the script below.

I would also like to record the amount of time the Xbox has been on over a week and display it in the mainUI. This I am having a challenge with.

I have created a few DateTime items, xbox_ON, xbox_OFF & xbox_Elapsed. My idea was to be able to update the xbox_ON/OFF times when the rule runs and when it triggers on an OFF add the elapsed time (xbox_OFF minus xbox_ON) to the xbox_Elapsed item and display on the mainUI. Then perhaps weekly reset the value to 0. Sadly I’m not that familiar working with DateTime items.

The Xbox ON/OFF and LastSeen are coming from the network binding, it has a static IP set.

I have tried a number of things without luck.
Could someone point me in the right direction?

XBoxchanged();

function XBoxchanged(){
  
let timestamp = new Date         
let date = new Date(timestamp); 

const timeNow = time.ZonedDateTime.now();
console.log(timeNow);   
  
  
  if (items.getItem('XBox1921681169_Online').state === 'ON' && items.getItem('Xbox_AntiFlap').state === 'OFF') {  
    actions.NotificationAction.sendBroadcastNotification('XBox turned ON, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
    items.getItem('xbox_ON').postUpdate(timeNow); 
    items.getItem("Xbox_AntiFlap").sendCommand("ON");        // Expire 2hr, send notification if exceeded.
    
  } else if (items.getItem('XBox1921681169_Online').state === 'OFF') {
    actions.NotificationAction.sendBroadcastNotification('XBox turned OFF, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
  }   
}

I have tried a number of things without luck.

Not sure what exactly you ask for: is the code you have posted is not working (any log and error messages?) or is the code you have working & you need help to extend the script to calculate the usage time?

What is the antiflap item doing and why is it only set to ON once, but there is no code to revert it back to OFF?
Your first if statement will never be true, if the condition requires antiflap to be OFF, but you only change it to ON once …

Regarding the elapsed time:
I would keep one item to always store the total usage time and not rest this item.

In addition I would create an another item and with the help of persistence either show the Delta since last 7 days (than you will have usage time for rolling 7 days) or delta since a fixed day (e.g. always since last Monday; than you will get usage time for a week)

Hi Matthias

The code I have is working for the first few parts but I am stuck with calculating the usage time part.

The anitflap item turns on and then expires via a metadata setting and triggers another rule to let us know if the xbox is on longer than it should be.

Any help you could offer with calculating the time ON part would be greatly appreciated.

cheers

To calculate the duration I found a similar example in the forum, maybe that’s helpful:

I would do the following:

  1. Create 1 new datetime item, that will always store time & date when the Xbox was last time turned on

  2. Create 1 new number item, that will later store total usage time in seconds (how long was the Xbox turned on). Initialize the item and set it to 0.

In your script:

  1. When the Xbox is turned on, update the datetime item with current date & time

  2. When the Xbox is turned off, calculate the delta between now () and the datetime from item 1

  3. Read the current value from the number item, add the delta time calculated in step 2 and update the number item with the new value

With this approach you should get the total usage time. If that’s working fine you can start with implementing the weekly number (instead of total over all time)

Hi Matthias

Thanks for the example, I am struggling with the code in the syntax to work with DateTime items in Javascript.

I think am struggling to get a start / end time and post them to the DateTime items and then do some calculation with them.
If you are familiar with this it would be a great help.

function XBox(){
  
let xboxOnline, xboxAntiflap; 
xboxOnline = items.getItem('XBox1921681169_Online').state;            //ON or OFF
xboxAntiflap = items.getItem('Xbox_AntiFlap').state;                  //ON or OFF
  
let timestamp = new Date
let date = new Date(timestamp);

// Not sure here
let startTime = (items.getItem('xbox_ON').state).millis;
console.log(startTime);  //logs 'undefined'
//  
  
  if (xboxOnline === 'ON' && xboxAntiflap === 'OFF') 
  {  
    actions.NotificationAction.sendBroadcastNotification('XBox turned ON, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
    //items.getItem('xbox_ON').postUpdate(timeNow); 
    items.getItem("Xbox_AntiFlap").sendCommand("ON");        // Expire 2hr, send notification if exceeded.
    
  } else if (xboxOnline === 'OFF') 
  {
    actions.NotificationAction.sendBroadcastNotification('XBox turned OFF, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
  }   
}

/*
Items

XBox1921681169_Online    Switch
xbox_ON                  DateTime
xbox_OFF                 DateTime
xbox_seconds             Number

*/

In Javascript you also need to work with datetime and not with date

E.g. create a new datetime to get now()

var current = new DateTimeType();

Or read datetime from an item:

var datetime_from_item = new DateTimeType(itemRegistry.getItem("item_name").getState());

Hi Matthias

When I use, I get an error on the log.

org.graalvm.polyglot.PolyglotException: ReferenceError: "DateTimeType" is not defined

var current = new DateTimeType();
console.log('Current DateTime is ' + current);

I am using JS2021, it logs ok with,

let current = time.ZonedDateTime.now();  
console.log('Current DateTime is: ' + current);

How do I update a DateTime item and how would I calculate the difference between two DateTime items, and then add the difference to another item?
Apologies if I am a bit slow, it is the first time working with DateTime items.

Cheers

I’m still on nashorn engine and not yet using graalvm, therefore cannot help on this.

Maybe search within the forum, as there are multiple topics on date time conversion.

Thanks anyway Matthias, I am trying to change my rules over to ECMA script. I had a look around but couldn’t find any threads that show it.

Hopefully someone else have updated DateTime items and calculated time differences with ECMA script that could help out a little.

Cheers

I think the following will work:

items.getItem('MyItem').postUpdate(current);

If not try current.toString().

Also look at the timestamp profile which might do what you need without a rule at all.

Assuming the latest release of OH

let delta = time.Duration.between(time.toZDT(Items.getItem('FirstDateTime')), time.toZDT(Items.getItem('SecondDateTime')); 

I think toZDT can handle that. If not, use items.getItem('FirstDateTime').rawState. I know that will work.

See https://js-joda.github.io/js-joda/class/packages/core/src/Duration.js~Duration.html for how to use a Duration.

See the JS Scripting add-on for more info and links to references on how to do stuff with time.

What kind if Item? If it’s a Number:Time

items.getItem('MyDuration').postUpdate(delta.seconds() + ' s');

Hi Rich

Thanks for you input, really appreciated. I am still having a quite some difficulty.
Using openHabian updated to OH3.3.0.

When I use,

let current = time.ZonedDateTime.now();
console.log(current);  

items.getItem('xbox_ON').postUpdate(current);

xbox_ON is a DateTime item.

I get the time logged ok but an error updating the item,

2022-07-26 19:20:50.518 [INFO ] [tomation.script.ui.JS2021TimeElapsed] - "2022-07-26T19:20:50.508+10:00[SYSTEM]"
2022-07-26 19:20:50.526 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'JS2021TimeElapsed' failed: java.lang.UnsupportedOperationException: Unsupported operation identifier 'toFullString' and  object '[object Object]'(language: JavaScript, type: ZonedDateTime). Identifier is not executable or instantiable.

When I use the below I get the error below and it does not update the item.
items.getItem('xbox_ON').postUpdate(current.toString());

2022-07-26 19:23:25.139 [INFO ] [tomation.script.ui.JS2021TimeElapsed] - "2022-07-26T19:23:25.129+10:00[SYSTEM]"
2022-07-26 19:23:25.187 [WARN ] [rnal.defaultscope.ScriptBusEventImpl] - State '2022-07-26T19:23:25.129+10:00[SYSTEM]' cannot be parsed for item 'xbox_ON'.

When I try to calculate the delta I get errors using both methods,

let delta = time.Duration.between(time.toZDT(Items.getItem('AstroSunData_Rise_Start')), time.toZDT(Items.getItem('AstroSunData_Set_Start'));  
console.log(delta);

2022-07-26 19:37:12.071 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: SyntaxError: <eval>:13:139 Expected , but found ;
let delta = time.Duration.between(time.toZDT(Items.getItem('AstroSunData_Rise_Start')), time.toZDT(Items.getItem('AstroSunData_Set_Start'));  

  
let delta = time.Duration.between(items.getItem('AstroSunData_Rise_Start').rawState, items.getItem('AstroSunData_Set_Start').rawState);  
console.log(delta);
2022-07-26 19:39:01.384 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'JS2021TimeElapsed' failed: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (until) on org.openhab.core.library.types.DateTimeType@17376d0 failed due to: Unknown identifier: until

Am I doing something clearly incorrect?

Cheers,

OK, there is something wrong with the parser. Try ...postUpdate(current.toLocalDateTime().toString());

I thought that was fixedbut I guess not. But it should work without the timezone.

Hi Goerge,

some time ago, I created something to record the runtime of my TV. Unfortunately I have no clue on Java so it’s in Rules DSL but maybe it will help you or somebody else :wink:

To avoid all this DateTime and conversion issues I’m using seconds.

I used some items to store the current runtime and the runtime of today both as number and human readable string for a GUI. Enigma2_Power gives me the state of my TV.

Switch  Enigma2_Power              "Power: [%s]"          <switch>           (gAutoOff) { channel="enigma2:device:VuPlus:power" }

Number Enigma2_RuntimeToday
String Enigma2_RuntimeTodayStr
Number Enigma2_RuntimeNow
String Enigma2_RuntimeNowStr

For calcualtion I’m using four rules:

  1. first one for storing the start time in a variable and resetting the current runtime
  2. doing one last update when switching of the TV and for logging (this is not really needed)
  3. updating the timers each second if the TV is running
  4. resetting the daycounter at midnight and again some logging
var Number onTimeTodayStartSec = null
var Number startTime = null

// ########## onTime  ##########
   rule "ruVuPlus_onTime_Einschalt"
   when
      Item Enigma2_Power changed to ON 
   then
      startTime = now.toInstant.toEpochMilli
      Enigma2_RuntimeNow.postUpdate(0)
      onTimeTodayStartSec = Enigma2_RuntimeToday.state as Number
   end
   
   rule "ruVuPlus_onTime_Ausschalt"
   when
      Item Enigma2_Power changed to OFF 
   then
      logInfo("ruVuPlus_onTime_Ausschalt", "Receiver gestoppt: " + now.toLocalDateTime().toString("HH:mm:ss"))

      var onTimeNowSec = ((now.toInstant.toEpochMilli - startTime) / 1000).intValue
      
      var min = (onTimeNowSec / 60) % 60
      var hrs = (onTimeNowSec / (60*60)) % 24
      var sec = onTimeNowSec % 60
      var stringToSend = String::format("%1$02d:%2$02d:%3$02d", hrs, min, sec)
      logInfo("ruVuPlus_onTime_Update", "Laufzeit aktuell: " + stringToSend)
      
      Enigma2_RuntimeNow.postUpdate(onTimeNowSec)
      Enigma2_RuntimeNowStr.postUpdate(stringToSend)

      var onTimeTodaySec = (onTimeTodayStartSec + onTimeNowSec).intValue
      min = (onTimeTodaySec / 60) % 60
      hrs = (onTimeTodaySec / (60*60)) % 24
      sec = onTimeTodaySec % 60
      stringToSend = String::format("%1$02d:%2$02d:%3$02d", hrs, min, sec)
      logInfo("ruVuPlus_onTime_Update", "Laufzeit heute: " + stringToSend)

      Enigma2_RuntimeToday.postUpdate(onTimeTodaySec)
      Enigma2_RuntimeTodayStr.postUpdate(stringToSend)
   end

   rule "ruVuPlus_onTime_Update"
   when
      Time cron "0/1 * * * * ?"
   then
      if (Enigma2_Power.state == ON) {
         var onTimeNowSec = ((now.toInstant.toEpochMilli - startTime) / 1000).intValue
       
         var min = (onTimeNowSec / 60) % 60
         var hrs = (onTimeNowSec / (60*60)) % 24
         var sec = onTimeNowSec % 60
         var stringToSend = String::format("%1$02d:%2$02d:%3$02d", hrs, min, sec)
         
         Enigma2_RuntimeNow.postUpdate(onTimeNowSec)
         Enigma2_RuntimeNowStr.postUpdate(stringToSend)

         var onTimeTodaySec = (onTimeTodayStartSec + onTimeNowSec).intValue
         min = (onTimeTodaySec / 60) % 60
         hrs = (onTimeTodaySec / (60*60)) % 24
         sec = onTimeTodaySec % 60
         stringToSend = String::format("%1$02d:%2$02d:%3$02d", hrs, min, sec)
         // logInfo("ruVuPlus_onTime_Update", "Laufzeit heute: " + stringToSend)

         Enigma2_RuntimeToday.postUpdate(onTimeTodaySec)
         Enigma2_RuntimeTodayStr.postUpdate(stringToSend)
      }
   end

   rule "ruVuPlus_onTime_Reset"
   when
      Time cron "59 59 23 * * ?"     
   then
      logInfo("ruVuPlus_onTime_Reset", "Laufzeit heute: " + Enigma2_RuntimeTodayStr.state)
      Enigma2_RuntimeToday.postUpdate(0)
      Enigma2_RuntimeTodayStr.postUpdate("00:00:00")
      startTime = now.toInstant.toEpochMilli
      onTimeTodayStartSec = 0
   end
// 

For your used case you may change the daycounter to a weekly counter or implement an addional counter.

Thanks Juergen & Rich for the pointers, really helpful!

I currently have the below that is working well. I think I will create a rule to reset it each Monday morning.

XBox();

function XBox(){
  
let xboxOnline, xboxAntiflap; 
xboxOnline = items.getItem('XBox1921681169_Online').state;
xboxAntiflap = items.getItem('Xbox_AntiFlap').state;  
  
let timestamp = new Date
let date = new Date(timestamp);  
  
let current = time.ZonedDateTime.now();     
  
  if (xboxOnline === 'ON' && xboxAntiflap === 'OFF') 
  {  
    actions.NotificationAction.sendBroadcastNotification('XBox turned ON, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
    items.getItem("Xbox_AntiFlap").sendCommand('ON');                                // Expire 2hr, send notification if exceeded
    
    items.getItem('xbox_ON').postUpdate(current.toLocalDateTime().toString());       // Updates ON time stamp to item
  } 
  else if (xboxOnline === 'OFF') 
  {
    actions.NotificationAction.sendBroadcastNotification('XBox turned OFF, ' + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds());
    items.getItem('xbox_OFF').postUpdate(current.toLocalDateTime().toString());      // Updates OFF time stamp to item

    // Xbox Elapsed Time
    let xbox_ON = items.getItem('xbox_ON').rawState.getZonedDateTime(); 
    console.log('Xbox turned ON at: ', xbox_ON.toString());
  
    let xbox_OFF = items.getItem('xbox_OFF').rawState.getZonedDateTime();
    console.log('Xbox turned OFF at: ', xbox_OFF.toString());
  
    // Differene between ON & OFF (hrs)
    let sessionHrs = ((xbox_OFF - xbox_ON) / (60*60*1000)).toFixed(2);          //Hours between OFF & ON
    sessionHrs = Number(sessionHrs);
    console.log('XBox session: ' + sessionHrs + 'hrs.');
    actions.NotificationAction.sendBroadcastNotification('XBox session: ' + sessionHrs + 'hrs');
    
    // Update Weekly total Xbox hours
    let previousHrs = Number(items.getItem('xbox_hours').state);
    let newHrs = Number(previousHrs + sessionHrs);
    items.getItem('xbox_hours').postUpdate(newHrs);  
  }

This is logging the below and updating the elapsed hrs (xbox_hours) variable nicely.

2022-07-28 21:05:16.366 [INFO ] [openhab.automation.script.ui.Xbox-ON] - Xbox turned ON at:  2022-07-28T04:58:00.033+10:00
2022-07-28 21:05:16.369 [INFO ] [openhab.automation.script.ui.Xbox-ON] - Xbox turned OFF at:  2022-07-28T21:05:16.352+10:00
2022-07-28 21:05:16.370 [INFO ] [openhab.automation.script.ui.Xbox-ON] - XBox session: 16.12hrs.

It may help someone else working with DateTime items which I found a little tricky.

I do something similar for my lawnmover. I want to get a notification if it did not return to the charging dock within 2 hours.

My rule in ECMA script using timers; trigger for the rule is a state-changed event of the Open-Close-Sensor:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var NotificationAction = org.openhab.io.openhabcloud.NotificationAction;
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var ZonedDateTime   = Java.type("java.time.ZonedDateTime");
var LocalDateTime = Java.type("java.time.LocalDateTime");
var DateTimeFormatter = Java.type("java.time.format.DateTimeFormatter")
var Duration = Java.type("java.time.Duration");
var String = Java.type("java.lang.String");

var MaxMowingTimeMinutes = 120;


//create the current DateTime in a nice look for the state string
var localDateTime = LocalDateTime.now();
var dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss");
var localDateTimeFormatted = localDateTime.format(dateTimeFormatter);

//note that keyword 'this' indicates a variable that will be preserved for the next run
this.LastStateChanged = (this.LastStateChanged === undefined) ? localDateTime : this.LastStateChanged;
this.RobbiMowingTimer = (this.RobbiMowingTimer === undefined) ? null : this.RobbiMowingTimer;

//calculate the duration since the last state change
var durationSinceLastStateChanged = Duration.between(this.LastStateChanged, localDateTime);
//logger.info("Robbi_StatusRule: LastStateChanged: {}", LastStateChanged);
//logger.info("Robbi_StatusRule: localDateTime: {}", localDateTime);
//logger.info("Robbi_StatusRule: durationSinceLastStateChanged: {}", durationSinceLastStateChanged);

//format the duration
var durationSinceLastStateChangedFormatted = String.format("%d:%02d:%02d", durationSinceLastStateChanged.toHours(), durationSinceLastStateChanged.toMinutesPart(), durationSinceLastStateChanged.toSecondsPart());
//logger.info("durationSinceLastStateChanged is " + durationSinceLastStateChangedFormatted);

//get the required item states
var state = itemRegistry.getItem('RobbiSensor_OpenClose').getState();
var stateString = itemRegistry.getItem('RobbiSensor_StateString').getState();

//logger.info("Robbi state is now {}", state);
//logger.info(state.class.toString());



//stop RobbiMowingTimer, if it is running
if(this.RobbiMowingTimer !== null)
{
  this.RobbiMowingTimer.cancel();
  this.RobbiMowingTimer = null;
  //logger.info("Robbi_StatusRule: RobbiMowingTimer stopped");
} 

switch (state)
{
  case OPEN:
    var newStateString = 'Mähen seit ' + localDateTimeFormatted;
    if(stateString != newStateString)
    {
      logger.info("Robbi_StatusRule: State changed to {}", state);
      events.postUpdate('RobbiSensor_StateString', newStateString);
    }
    
    events.postUpdate('RobbiSensor_LastChargeCycle', durationSinceLastStateChangedFormatted);
    
    //start RobbiMowingTimer
    //logger.info("Robbi_StatusRule: RobbiMowingTimer started");
    this.RobbiMowingTimer = ScriptExecution.createTimer(ZonedDateTime.now().plusMinutes(MaxMowingTimeMinutes), TimerElapsedHandler);
    
    break;
  case CLOSED:
    var newStateString = 'Ladestation seit ' + localDateTimeFormatted;
    if(stateString != newStateString)
    {
      logger.info("Robbi_StatusRule: State changed to {}", state);
      events.postUpdate('RobbiSensor_StateString', newStateString);
    }
    
    events.postUpdate('RobbiSensor_LastMowCycle', durationSinceLastStateChangedFormatted);
    
    break;
  default:
    var newStateString = 'unbekannt'
    if(stateString != newStateString)
    {
      logger.info("Robbi_StatusRule: State changed to {}", state);
      events.postUpdate('RobbiSensor_StateString', newStateString);
    }
}

//update the LastStateChanged date
this.LastStateChanged = localDateTime;

function TimerElapsedHandler() 
{
  logger.info("Robbi_StatusRule: MaxMowingTimeMinutes exceeded.");

  var message = "Robbi war zu lange nicht mehr in der Ladestation! Bitte mal nachschauen, wo er ist.";
  NotificationAction.sendBroadcastNotification(message);

  this.RobbiMowingTimer = null;
}
3 Likes

Thanks to your post I figured out how to make use of DateTimeFormatter in OH3.4 rules.
I needed to preface the code with import java.time.format.DateTimeFormatter

If I don’t I get this error:

[ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule xxxx failed: The name 'DateTimeFormatter' cannot be resolved to an item or type;