NSPanel Lovelace UI Helpers (part 1/5, v0.9)

Hi,

you need to call these cards via the callback. Check out the documentation on github (The important section on top).

Hope this helps, best regards,
Rene

Ahh, and maybe have a look at the installation description as well - it’s a little special how all of the different parts play together. But keeping the information of the Panel to work with out of the cards (and use it from callback) was required to have these cards running on different panels in your?/my home. This way you only need one callback per panel, seems to work nicely for me :slight_smile:

best regards, (I might be offline for a week),
Rene

HI Rene

Thanks for the swift feedback. I tried to add the callback but then the system gets into some kind of loop. I have to stop the rule and remove it to make it stable.

Screenshot of loop:

I went thru all documentation, I only have one difference.
I did define the item / thing in a file like this, the rest is exactly the same.

Things:

Items:

//HMI Displays
String NSPanel1 “NSPanel 1” { channel=“mqtt:topic:MQTT-HMI:NSPanel_1:NSPanel_UI_1” }

String NSPanel2 “NSPanel 2” { channel=“mqtt:topic:MQTT-HMI:NSPanel_2:NSPanel_UI_2” }

I don’t expect this to be the issue but that’s the only difference based on what you did.

Hi,

hard to say how the loop gets triggered in the first place without seeing your cardGrid script. Try to configure the callback in the beginning as close as possible like the example, try the cardGrid just without any other configuration around, just the plain cardGrid in some blockly script. Remove some external blocks, if you added some. Best build and extend from there.

As you see, not really a lot I can say, if you are still in trouble just post the callback and the nspanel1_cardGrid script somewhere. Those loops might also be caused by some previous wrong configuration - to go around this disable and re-enable the callback to reset any environment variables it keeps.

Best regards, Rene

is it possible to remove the hardware relais part, power the NSPanel with 5V and still use the hardware buttons (just to toggle a openhab item)?

and could somebody please post a configured media card?
can’t figure out the “Media Player Control Actions”…

and strange enough… i can’t find a specific item in this block:
image

this item controls volume:
Dimmer piCoreK_volume (piCoreK) { channel="squeezebox:squeezeboxplayer:unRaid:piCoreK:volume", alexa="VolumeLevel" }
but i can’t find this item (checkbox “show non-semantic” activated) :man_shrugging:

ok, there’s a bug in 4.1 that should soon be fixed: Blockly - can't find/pick some items

i got the volume control working but i’m struggling with PLAY/PAUSE…
this is my item: Player piCoreK_control "Küche Control" (piCoreK) ["Receiver"]
and its state is either “PLAY” or “PAUSE”.
i’m able to get the correct icon with this:

but i’m not able to send the correct command with this:


the card/script always sends the same command: “PLAY”
makes no difference if the piCoreK_control state is “PAUSE” or “PLAY”…

any help? :slight_smile:

Hi,

You can find some information about how to decouple the physical relays from the buttons at the nspanel documentation. I did so to prevent the useless clicking when using them as logical buttons and control their action in OpenHAB.

Here you can see my OpenHAB thing configuration, the buttons are triggering some OpenHAB rules which control some lights in my flat.

UID: mqtt:topic:0f11d1bffe:10df1b5c3f
label: NSPanel 3
thingTypeUID: mqtt:topic
configuration: {}
bridgeUID: mqtt:broker:0f11d1bffe
channels:
  - id: lovelaceui
    channelTypeUID: mqtt:string
    label: Lovelace UI
    description: this is the mqtt channel for lovelace UI
    configuration:
      commandTopic: cmnd/nspanel_3/CustomSend
      stateTopic: tele/nspanel_3/RESULT
  - id: buttonLeft
    channelTypeUID: mqtt:string
    label: Left Button
    description: null
    configuration:
      stateTopic: stat/nspanel_3/RESULT
      transformationPattern: JSONPATH:$.Button1.Action
  - id: buttonRight
    channelTypeUID: mqtt:string
    label: Right Button
    description: null
    configuration:
      stateTopic: stat/nspanel_3/RESULT
      transformationPattern: JSONPATH:$.Button2.Action

This should also work if you just remove the backplate of your NSPanel and supply 5V. Best regards,

Rene

Hi,

there is no returnValue available in these Media Player Control Actions except in the Volume action. The Panel does not know the state of your player, it’s just stupid and triggering some action. You should check the state on your own, as you can see in the example below (with Owntone, not Squeezebox - you need to adapt this)

Hope this helps, best regards,

Rene

1 Like

HI Rene

I got it to work. Thanks for help. There was an error in the callback script.

1 Like

Hi again:

I have run into an interesting problem

I have 3 panels, 2 use the “standard” screen saver and 1 uses the complex screen saver.
For all of them I created a rule using the helper to change the page brightness. The panels with the standard screen saver work without a problem

Here is a very interesting bug
The panel with the complex screen saver runs into an issue after changing the brightness, if I touch the screen to go to the main page it does not go back to the screen saver. I have to run the callback to fix the issue and then it works fine until I change the brightness again.

It seems to happen only when the andscreensaverbrightness part of the helper is set to 50, I have not played with the SetPageBrightness to see if the bug is also dependent on that value. I currently have it setup to 50.

Carlos

Hi,

Interesting, really. Are you sure all your libraries on the same version? Especially the callback might be older to trigger some crazy things, including this… Check if your Callback has INITIAL SCREENSAVER SETTINGS written as caption, as below

Else I can’t really help, you might sniff the MQTT traffic to see whats really going on. See some example here:

 ~ $ mosquitto_sub -v -h squid -t "+/nspanel_1/#"
...
cmnd/nspanel_1/CustomSend dimmode~50~100
stat/nspanel_1/RESULT {"CustomSend":"Done"}
...
tele/nspanel_1/RESULT {"CustomRecv":"event,buttonPress2,screensaver,bExit,1"}
cmnd/nspanel_1/CustomSend pageType~cardGrid2
...
tele/nspanel_1/RESULT {"CustomRecv":"event,sleepReached,cardGrid"}
cmnd/nspanel_1/CustomSend pageType~screensaver2

First I’m sending the default brightness values with the helper, than I press a button and later the panel triggers some sleepReached, which tells the callback to display the screensaver again. (in between there are a lot of lines skipped here)

Best regards, Rene

this helped a lot, thank you!

but before i go “live” with this solution, i have another problem to solve:
i use two entity buttons to change to specific favorites.
when i change the favorite my play/pause item changes to PAUSE and some ms later to PLAY again.
a possible solution would be to wait for some time and then send “refresh” to NSPanel_Item (for every ENTITY BUTTON):

but i have a feeling this could be solved in a smoother way…
it would also be nice if song title and author would be refreshed as soon one of these changes.

i have tried a rule that sends “refresh” to NSPanel_Item when one the item changes but that also interrupts the screensaver.

Hi Peter,

That’s how I implemented this as well, but I’m using the helper to reload:


(old-style vs. new-style)

You can also modify the Refresh Time of the Card to some low value. Shouldn’t be too short, 5sec. might be suitable.

You can use some other refresh option I used to prevent refresh timers to disable the screensaver (that’s why the name):


Maybe I should create some helper (with a better name) for that as well.

Btw, I’m just using “post update” in the examples above, it does not really make any difference, you can also “send command” to achive the same result.

Best regards, Rene

:+1:

it this a rule or is this part of a card?

You can just create this on your own.(?) The item is my own NSPanel Item, just send the “refreshTimer” string to the callback via the Item.

But I probably got the question wrong… Best regards, Rene

yes, i can do it :slight_smile:
i wasn’t sure where this block should be placed (or if it should be a RULE outside of any cards).

i have tried it with a rule:

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: piCoreK_title
    type: core.ItemStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: NSPanel_Item
      state: refreshTimer
    type: core.ItemStateUpdateAction

seems to work!

1 Like

Hi,
i have a problem with the script.
The callback script produces this error message in openhab.log:

Script execution of rule with UID ‘ff5ee7ceb7’ failed: org.graalvm.polyglot.PolyglotException: ReferenceError: “contextValues” is not defined

Can anyone tell me what could create this issue ?

Hi,

Which version of openHAB you are using? The add-on only works with openHAB 4. If you are using version 4, the only way to help you is when you are posting some screenshot of the callback script (best the source code, but also blockly is ok) and the line number of the error and I will try to reproduce the issue here.

Best regards, Rene

Hi Rene,

i use Openhab 4.1.0 Release Build

This is the error log:

2024-03-07 20:27:34.723 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'ff5ee7ceb7' failed: org.graalvm.polyglot.PolyglotException: ReferenceError: "contextValues" is not defined
2024-03-07 20:27:40.571 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: ReferenceError: "contextValues" is not defined
        at <js>.absorb_it_nspanel_callback(<eval>:174) ~[?:?]
        at <js>.:program(<eval>:276) ~[?:?]
        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.internal.handler.ScriptActionHandler.lambda$0(ScriptActionHandler.java:71) ~[?:?]
        at java.util.Optional.ifPresent(Optional.java:178) [?:?]
        at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.execute(ScriptActionHandler.java:68) [bundleFile:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1188) [bundleFile:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.runRule(RuleEngineImpl.java:997) [bundleFile:?]
        at org.openhab.core.automation.internal.TriggerHandlerCallbackImpl$TriggerData.run(TriggerHandlerCallbackImpl.java:87) [bundleFile:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) [?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) [?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [?:?]
        at java.lang.Thread.run(Thread.java:840) [?:?]

This is the code from the blocky callback script:

function absorb_it_nspanel_callback_checkParam(param, noOfItems) {
  let result = String(param);
  if (result.charAt(0) != '~')
      result = '~' + result;
  while ((result.match(/~/g) || []).length < noOfItems)
      result += '~'
  while ((result.match(/~/g) || []).length > noOfItems)
      result = result.replace(/~/, '');
  return result;
}

function getTimerName() {
  return 'absorb_it_nspanel_refreshTimer_' + ctx.ruleUID;
}

function absorb_it_nspanel_callback_startScript(scriptName, context, updateCacheLink) {
  const RuleManager = osgi.getService('org.openhab.core.automation.RuleManager');
  var thread = Java.type('java.lang.Thread');

  if (!scriptName || scriptName === "Script Id")
    return 0;
  context["timerName"] = getTimerName();
  try {
      let timer = 10;
      while (RuleManager.getStatus(scriptName) && RuleManager.getStatus(scriptName).toString() === "RUNNING") {
        if (!timer--) {
          console.log("finished waiting for rule " + scriptName);
          break;
        }
        console.log("waiting 1 sec. for running rule " + scriptName);
        thread.sleep(1000);
      }
      rules.runRule(scriptName, context);
      cache.private.put('lastPage', scriptName); // any last page
      if (updateCacheLink)
        cache.private.put('currentCard', scriptName); // only some last pages (excluding popupNotify, Screensaver)
      return 1;
  }
  catch (e) {
      console.error("Error", e.stack);
      console.log("Error", e.toString());
      console.log("can't start/find rule with id '" + scriptName + "'");
  }
  return 0;
}

function absorb_it_nspanel_callback_safeEval(command, returnValue = '') {
  // evaluate command, use returnValue as context
  let result = "";
  if (command) {
      try { result = eval(command); }
      catch (e) {
        console.error("Error", e.stack);
        console.log("Error", e.toString());
        console.log("eval failed: '" + command + "'");
      }
  }
  return result;
}

function absorb_it_nspanel_callback_killRefreshTimer() {
  if (cache.shared.exists(getTimerName())) {
    console.debug("remove existing refresh timer " + getTimerName());
    cache.shared.remove(getTimerName()).cancel();
  }
}

function absorb_it_nspanel_callback(target, timeout, screenSaverBrightness, activeBrightness, screensaver2, screensaverStartupScript, startupScript, startupAction, startupRedo) {
  // on first run only...
  if (!cache.private.exists('initialized')) {
    // allow refreshing of dimming and timeout when saving configured callback
    items.getItem(target).sendCommand(
      "timeout" +
      absorb_it_nspanel_callback_checkParam(timeout, 1)
    );
    items.getItem(target).sendCommand(
      "dimmode" +
      absorb_it_nspanel_callback_checkParam(screenSaverBrightness, 1) +
      absorb_it_nspanel_callback_checkParam(activeBrightness, 1)
    );
    // store settings from blockly interface to be able to change them on-the-fly
    cache.private.put('complexScreenSaver', screensaver2);
    cache.private.put('enterScriptName', screensaverStartupScript);
    cache.private.put('leaveScriptName', startupScript);
    cache.private.put('startupRedo', startupRedo);
    // done
    cache.private.put('initialized', 1);
  };

  try {
    contextValues = (JSON.parse(event.itemState))["CustomRecv"].split(',');
  } catch {
    if (event.itemState) {
      // if callback received non-JSON string check for local requests
      let callbackParam = event.itemState.toString().split('?');
      switch (callbackParam[0]) {
        case "refresh":
        case "ON":    // triggered by Hardware Button press (if rule configured to fire)
        case "OFF":   // triggered by Hardware Button press (if rule configured to fire)
          if (cache.private.get('currentCard')) {
            let context = {
              "previousPage": cache.private.get('lastPage'),
              "target": target
            };
            if (cache.private.exists('DetailPage')) {
              context["request"] = "pageOpenDetail";
              context["item"] = cache.private.get('DetailPage');
            };
            absorb_it_nspanel_callback_startScript(
              cache.private.get('currentCard'),
              context,
              0
            );
          }
          return;
        case "refreshTimer":
          if (cache.private.get('currentCard') && (cache.private.get('lastPage') !== "screensaver"))
            absorb_it_nspanel_callback_startScript(
              cache.private.get('currentCard'),
              {
                "previousPage": cache.private.get('lastPage'),
                "target": target
              },
              0
            );
          return;
        case "loadPage":
          if (callbackParam[1])
            contextValues = [ , "buttonPress2", callbackParam[1] ];
          break;
        case "loadScreensaver":
          contextValues = [ , "sleepReached" ];
          break;
        case "newTimeout":
          if (callbackParam[1])
            items.getItem(target).sendCommand(
              "timeout" +
              absorb_it_nspanel_callback_checkParam(callbackParam[1], 1)
            );
          return;
        case "newBrightness":
        case "newBrigthness":   // keep old value, type of parameter was fixed in 0.6
          if (callbackParam[1] && callbackParam[2])
            items.getItem(target).sendCommand(
              "dimmode" +
              absorb_it_nspanel_callback_checkParam(callbackParam[1], 1) +
              absorb_it_nspanel_callback_checkParam(callbackParam[2], 1)
            );
          return;
        case "complexScreenSaver":
        {
          cache.private.put('complexScreenSaver', callbackParam[1]);
          return;
          }
        case "enterScriptName":
        {
          cache.private.put('enterScriptName', callbackParam[1]);
          return;
        }
        case "leaveScriptName":
        {
          cache.private.put('leaveScriptName', callbackParam[1]);
          if (callbackParam[1])
            cache.private.put('startupRedo', "TRUE");
          else
            cache.private.put('startupRedo', "FALSE");
          return;
        }
        default:
          return;
      }
    }
  }
  switch(contextValues[1]) {
    case "startup":
      cache.private.put('screensaverActive', "FALSE");
      items.getItem(target).sendCommand(
        "timeout" +
        absorb_it_nspanel_callback_checkParam(timeout, 1)
      );
      items.getItem(target).sendCommand(
        "dimmode" +
        absorb_it_nspanel_callback_checkParam(screenSaverBrightness, 1) +
        absorb_it_nspanel_callback_checkParam(activeBrightness, 1)
      );
      let success = absorb_it_nspanel_callback_startScript(
        cache.private.get('leaveScriptName'), { "target": target }, 1
      );
      absorb_it_nspanel_callback_safeEval(startupAction);
      if (success || startupAction != '')
        break;
    case "sleepReached":
      absorb_it_nspanel_callback_killRefreshTimer;
      cache.private.put('screensaverActive', "TRUE");
      if (cache.private.get('complexScreenSaver') == "TRUE")
        items.getItem(target).sendCommand("pageType~screensaver2");
      else
        items.getItem(target).sendCommand("pageType~screensaver");
      absorb_it_nspanel_callback_startScript(
        cache.private.get('enterScriptName'), { "target": target }, 0
      );
      cache.private.put('lastPage', 'screensaver'); // therefore ignore timer updates during screensaver
      break;
    case "buttonPress2":
      if (cache.private.get('screensaverActive') == "TRUE") {
        cache.private.put('screensaverActive', "FALSE");
        if (cache.private.get('startupRedo') == "TRUE")
          cache.private.remove('currentCard');
      }
      switch(contextValues[3]) {
        case "bExit":
          cache.private.remove('DetailPage');
          if (cache.private.get('currentCard'))
            absorb_it_nspanel_callback_startScript(
              cache.private.get('currentCard'), { "target": target }, 0
            );
          else
            absorb_it_nspanel_callback_startScript(
              cache.private.get('leaveScriptName'), { "target": target }, 1
            );
        break;
        default:
          if (contextValues[2].split('?')[0] !== cache.private.get('lastPage')) {
            // new page called, probably clicked on navigation buttons
            absorb_it_nspanel_callback_startScript(
              contextValues[2].split('?')[0], { "target": target },
              (contextValues[2] !== "popupNotify" && contextValues[3] !== "notifyAction")
            );
          }
          // find the clicked item, if there hade been multiple on the page
          let item = "";
          // alarmButtons: third context field contains 'cb' and item id, reparated by '?'
          // {"CustomRecv":"event,buttonPress2,*cardAlarmScriptName*,item?1,1"}
          if (contextValues[3] && contextValues[3].split('?')[0] === "item")
            item = contextValues[3].split('?')[1];
          // HVACButtons: third context field contains script name and item id, reparated by '?'
          // {"CustomRecv":"event,buttonPress2,*cardThermoScriptName*,hvac_action,item?1"}
          if (contextValues[4] && contextValues[4].split('?')[0] === "item")
            item = contextValues[4].split('?')[1];
          // entities: third context field contains both script name and item id, reparated by '?'
          // {"CustomRecv":"event,buttonPress2,*cardEntitiesScriptName*?2,OnOff,0"}
          else item = contextValues[2].split('?')[1];
          absorb_it_nspanel_callback_startScript(
            contextValues[2].split('?')[0],
            {
              "request": cache.private.exists('DetailPage')?"pageOpenDetail":"pageUpdate",
              "item": item,
              "trigger": contextValues[3],
              "newState": contextValues[4],
              "previousPage": cache.private.get('lastPage'),
              "target": target
            },
            (contextValues[2] !== "popupNotify" && contextValues[3] !== "notifyAction")
          );
        }
        break;
    case "pageOpenDetail":
      let item = contextValues[3].split('?')[1];
      cache.private.put('DetailPage', item?item:0);
      absorb_it_nspanel_callback_startScript(
        contextValues[3].split('?')[0],
        {
          "request": "pageOpenDetail",
          "item": item,
          "trigger": contextValues[2],
          "target": target
        },
        1
      );
      break;
    default:
  }
}
absorb_it_nspanel_callback('nxpanel_command', 20, 50, 100, 'FALSE', '', 'nxpanel_card_grid', `items.getItem('nxpanel_command').sendCommand('loadScreensaver');`, 'TRUE');

The mqtt items work, because i tried nxpanel before.

Regards B-Tronic

Hi B-Tronic,

thanks for posting this. It seems like you are calling the callback directly, this error also comes up if you press run in the blockly editor. The context information is missing, mainly event.itemState. Do you have the callback configured in a rule, as mentioned in the documentation? … I surely have to add some more meaningful error message in this case.

Hope this helps, best regards, Rene