NxPanel - Replacement Firmware for Sonoff NSPanel

You guys are ahead of me!!! :slight_smile: :slight_smile:

I’m so busy actually writing it, I don’t get time to even set my own system up to use it! lol

Ok, I did and example on mine which works to show you. First add this little utilities to help make the json easier.

/*
 * Utility functions
 */

def makeButton(bid,label,type,icon=null,state=null,next=null) {
  var str = ""<<((bid==1)?"":",")
  str<<'{"bid":'<<bid<<',"label":"'<<label<<'","type":'<<type
  if (next!=null) {
    str<<',"next":'<<next
  }
  if (state!=null) {
    str<<',"state":'<<state
  }
  if (icon!=null) {
    str<<',"icon":'<<icon
  }
  str<<'}'
  return str
}

def makePage(pid,name,format) {
  var str = new StringBuilder('{"refresh":')
  str<<'{"pid":'<<pid<<',"name":"'<<name<<'","format":'
  return str
}

def makeEmptySync(pid) {
  var str = new StringBuilder('{"sync":')
  str<<'{"pid":'<<pid<<'}}'
  return str
}

def makeSyncButtonStart(pid,bid,state) {
  var str = new StringBuilder('{"sync":')
  str<<'{"pid":'<<pid
  str<<',buttons:[{"bid":'<<bid<<',"state":'<<state<<'}'
  return str
}

def addSyncButton(bid,state) {
  var str = ',{"bid":'<<bid<<',"state":'<<state<<'}'
  return str
}

/*
 * Utility functions
 */

you will have most but I’ve put them all in that I have.

Then for you page build

  
  case "10" :
    movie = ir.getItem("movie_room_lights").state==ON?1:0
    lounge = ir.getItem("LoungeLights_LoungeLights").state==ON?1:0
    if (refresh) {
      json = makePage(id,'Lounge',format)
      json<<format<<',buttons:['
      json<<makeButton(1,"Movie",1,1,movie)
      json<<makeButton(2,"Lounge",1,1,lounge)
      json<<makeButton(3,"Hall",2,6)
      json<<makeButton(4,"Bedroom",10,5,5,11)
      json<<makeButton(5,"Temp",10,9,9,12)
      json<<makeButton(6,"Light",1,3)
      json<<makeButton(7,"Heat",10,9,3,15)
      json<<makeButton(8,"Status",10,16,15,17)
      json<<"]}}"
    } else {
      //json = makeEmptySync(10)
      json = makeSyncButtonStart(10,1,movie)
      json<<addSyncButton(2,lounge)
      json<<"]}}"
    }
    events.sendCommand("nxpanel_command",json.toString())
    break

You will see;

  1. You get the state of your items at the start
  2. You set the state, in both refresh and sync messages

This keeps it right every time you open the page.

I suggest you could also send another sync form a trigger when your item changes too. That way, if the page it open, it will update in real-time too.

Shortly I will add an auto-sync, every minute or so. It will mean you do not need to trigger it like that, but it might be 1 minute out of sync. You can choose how you wish. For me, I’d do the extra work and send the sync when the item changes, as I prefer it live, even if more work. Just depends how much effort you want to put in.

I hope that makes if clearer, so you guys can continue configuring while I keep developering.

As as a reminder, the rule for the other direction. When you press a button on the panel and you want it to turn the light off/on on your system would be this;

import org.slf4j.LoggerFactory

def logger = LoggerFactory.getLogger("org.openhab.core.automation.nspanel")

def str = event.getEvent()
if (str.indexOf('{"button": ')!=0) {
  return
}

// {"button": {"pid": 15, "bid": 1, "state": 1, "next": 0}}

var i = str.indexOf("\"pid\"")
var i2 = str.indexOf(",",i+7)
var pid = str.substring(i+7,i2) as int
i = str.indexOf("\"bid\"")
i2 = str.indexOf(",",i+7)
var bid = str.substring(i+7,i2) as int
i = str.indexOf("\"state\"")
i2 = str.indexOf(",",i+9)
var state = str.substring(i+9,i2) as int

if (pid==10 && bid==1) {
  events.sendCommand("movie_room_lights",state==1?"ON":"OFF")
}
else if (pid==10 && bid==2) {
  //
}

So that’s both parts.

  1. The light changing and the panel knowing
  2. You changing the panel and your system being changed.

If you can help other newer users here too as you learn more it would help me, so I dont need to answer everything… it’s exhausting. lol…

This thread is by far the most active in openhab community and I’m feeling it!! :slight_smile:

I think i managed to port the groovy code to javascript, would you or anyone else be interested in it?

1 Like

Thank you
you are awesome :smiley:

I personally wouldn’t as Groovy is more than enough for my small brain to cope with! But I think that’s great if you have!! :slight_smile: I’m sure it would be worth a post as I’m sure there are many more users that know Javascript than groovy.

Something in the older text based DSL? rules would be handy too. as most OH 2 users don’t; have groovy. I never really understood those sripts so vey happy for the addition of groovy! :slight_smile:

Roadmap

The only things still not complete are the dimmer controls. I will tackle them next. Then I think all main parts should be done.

After that these are the next features;

Auto Sync - an option to (optionally) turn on which we sync the active view page every min or so. This will be a shortcut so you dont need to make triggers on all your items to push back to nxpanel. Although, I’d say that’s the best way.

Auto Update - I intend to save you all the hassle of having to flash new versions. If turned on, NxPanel will track new versions. If it finds one it will trigger Tasmoto to download and flash the new version. You will never need to flash, your unit will also been on newest version.

New component pages…? :slight_smile:

// Enum
const PAGE_HOME            = 1
const PAGE_2_BUTTON        = 2
const PAGE_3_BUTTON        = 3
const PAGE_4_BUTTON        = 4
const PAGE_6_BUTTON        = 5
const PAGE_8_BUTTON        = 6
const PAGE_DIMMER          = 7
const PAGE_DIMMER_COLOR    = 8
const PAGE_THERMOSTAT      = 9
const PAGE_ALERT_1         = 10
const PAGE_ALERT_2         = 11
const PAGE_ALARM           = 12
const PAGE_MEDIA           = 13
const PAGE_PLAYLIST        = 14
const PAGE_STATUS          = 15

const BUTTON_UNUSED        = 0
const BUTTON_TOGGLE        = 1
const BUTTON_PUSH          = 2
const BUTTON_DIMMER        = 3
const BUTTON_DIMMER_COLOR  = 4
const BUTTON_PAGE          = 10

const ICON_BLANK           = 0 
const ICON_BULB            = 1 
const ICON_DIMMER          = 2 
const ICON_DIMMER_COLOR    = 3
const ICON_VACUUM          = 4 
const ICON_BED             = 5 
const ICON_HOUSE           = 6 
const ICON_SOFA            = 7 
const ICON_BELL            = 8 
const ICON_HEAT            = 9
const ICON_CURTAINS        = 10 
const ICON_MUSIC           = 11
const ICON_BINARY          = 12 
const ICON_FAN             = 13
const ICON_SWITCH          = 14
const ICON_TALK            = 15
const ICON_INFO            = 16

const NONE                 = 0
// End Enum

function makeButton(bid,label,type,icon,state,next){
  let json = JSON.parse("{}");
  json["bid"] = bid;
  json["label"] = label;
  json["type"] = type; 
  if (typeof next != "undefined"){
    json["next"] = next;
  }
  if (typeof state != "undefined"){
    json["state"] = state;
  }
  if (typeof icon != "undefined"){
    json["icon"] = icon;
  } 
  let stringJson = JSON.stringify(json)
  if (bid > 1){
    stringJson = "," + stringJson;
  }
  return stringJson;
}

function makePage(pid,name,format){
  let json = JSON.parse('{"refresh": {}}');
  json["refresh"]["pid"] = pid;
  json["refresh"]["name"] = name;
  let stringJson = JSON.stringify(json)
  stringJson = stringJson.substr(0,stringJson.length - 2) + `,${format}buttons:[`
  stringJson = stringJson.replace('"refresh":"",','"refresh":{');
  return stringJson
}

function makeEmptySync(pid){
  let json = JSON.parse("{}");
  json["sync"]["pid"] = pid;
  return JSON.stringify(json);
}

function makeEmptyRefresh(pid){
  let json = JSON.parse("{}");
  json["refresh"]["pid"] = pid;
  return JSON.stringify(json);
}

function makeSyncButtonStart(pid,bid,state){
  let json = JSON.parse('{"sync":{"pid": 5}}');
  json["sync"]["pid"] = pid;
  stringJson = JSON.stringify(json);
  stringJson = stringJson.substr(0,stringJson.length - 2);
  stringJson = stringJson + `,buttons:[{"bid":${bid},"state":${state}}`;
  return stringJson;
}

function addSyncButton(bid,state) {
  return `,{"bid":${bid},"state":${state}}`;
}

//var str = `this event has been triggered by {"page": {"format": 6, "pid": 10, "type": "refresh"}}`
var str = this.event.toString();

var eventJSON = JSON.parse(str.substr(str.indexOf("{"),str.length()));
var pid = parseInt(eventJSON["page"]["pid"],10);
var format = eventJSON["page"]["format"].toString();
var refresh = str.indexOf("refresh")>0;
var messageJson

console.log("Updating NxPanel page: " + pid.toString());

const PANEL_MAIN          = 10
const PANEL_BEDROOM_1     = 11
const PANEL_BEDROOM_2     = 12
const PANEL_LOUNGE        = 13
const PANEL_CABIN         = 14
const PANEL_CABIN_THERMO  = 15
const PANEL_CABIN_LIGHTS  = 16
const PANEL_LOUNGE_FAN    = 17
const PANEL_LOUNGE_LIGHT  = 18
const PANEL_STATUS        = 19
const PANEL_MUSIC         = 20

switch(pid){
  case PANEL_MAIN:
    let movie_state = 1
    let lounge_state = 0
    let cabin_state = 0
    let hall_light_state = 1
     if (refresh) {
      messageJson = makePage(PANEL_MAIN,"Lounge",PAGE_8_BUTTON);
      messageJson = messageJson + makeButton(1,"Movie",BUTTON_TOGGLE,ICON_BULB,movie_state);
      messageJson = messageJson + makeButton(2,"Lounge",BUTTON_TOGGLE,ICON_BULB,lounge_state);
      messageJson = messageJson + makeButton(3,"Hall",BUTTON_PUSH,ICON_HOUSE);
      messageJson = messageJson + makeButton(4,"Bedroom",BUTTON_PAGE,ICON_BED,PAGE_6_BUTTON,PANEL_BEDROOM_1);
      messageJson = messageJson + makeButton(5,"Temp",BUTTON_PAGE,ICON_HEAT,PAGE_THERMOSTAT,PANEL_CABIN_THERMO);
      messageJson = messageJson + makeButton(6,"Light",BUTTON_DIMMER,ICON_DIMMER,hall_light_state,PANEL_LOUNGE_LIGHT);
      messageJson = messageJson + makeButton(7,"Dimmer",BUTTON_DIMMER_COLOR,ICON_DIMMER_COLOR,cabin_state,PANEL_CABIN_LIGHTS);
      messageJson = messageJson + makeButton(8,"Status",BUTTON_PAGE,ICON_INFO,PAGE_STATUS,PANEL_STATUS);
      messageJson = messageJson + "]}}";
    } else {
      messageJson = makeSyncButtonStart(10,1,1);
      messageJson = messageJson + addSyncButton(2,1);
      messageJson = messageJson + "]}}";
    }
    //console.log(messageJson);
    items.getItem("nxpanel_command").sendCommand(messageJson);
    break  
}

It’s more readable instead of line efficient, but it does give me the same outputs for sync and refresh of pages.
EDIT: Note this is in emcascript 2021, sorry OH 2.0 users
EDIT2: Updated script to work with the latest nxpanel version as of 2/20/2022 (american dating)
EDIT3: There are many options besides an nspanel, if you don’t have one yet i recommend looking at other options like the lanbon l8 with openhasp

3 Likes

That’s good. So long as it spits the same thing out it’s all good.

Ideally. I want to do a YAML config file. Which would define the page and also the linked OH item. Then you could just have a generic rule that could process that file and skip all this clunky stuff we are doing now. It shouldn’t be too hard. It’s just time. I needed to get a way so people could actually start using it, rather that wait for perfection.

page: 10
name: Maint Panel
format: 8-button
    button:
        bid: 1
        name: Lights
        type: toggle
        icon: light
        oh-item: movie-room-light

that find of thing, and that’s all you needed to do.

1 Like

That sounds less of running on a rule and moreso just a new binding. Which would make that idea much easier to implement. Either way that’s a good bit of work.

I think for people geared up for writing a binding, maybe. I think the learning curve for writing a new binding have never before would make it a big job, for me. Plus, binding seem to me to be where OH can’t access them, without some kind lower lever integration you just can’t do with existing OH. Because this just uses mtqq which OH is pretty good at as is, I think a scripted rule might work, and be much less work than a binding. I’d rate it a 7/10 on the possible scale! :slight_smile:

I’m not too sure how binding things can be setup, if you can just pass it the yaml or if you can make a little gui to build it. A rule template would certainly be a lot easier. I’m more leaning to just a template myself because I don’t want to make a binding and doubt others will when there is an easier solution.

I think one easy win though would be for on/switches. To create a group called ‘nxpanel_switch’ and just add the ones in you wanted to that group. then have one rule which was when nxpanel_switch changes and you could push them back to nxpanel via mtqq. but to do that, you’d need to store pid and bid on the item. so you could pull them from the triggeringItem in the script. just not sure where to set those values. maybe you can add them in item metadata or soemthing. That would be a quick win.

Thanks that makes it much easier to understand for us non coders. Are you able to put the other rule for sending commands to the panel into JavaScript?

Something strange on the new media page?

I put a temp vol slider on after you said about volume, just so it sent events. but i hated it so did it properly! lol

image

You have a version in the middle, just get the lastest and it should be fine. it will send

{"media":{"volume":60}}

on mqtt, when you use it.

pause/play is now a toggle button, sending;

11:34:32.275 MQT: tele/nspanel/RESULT = {"media":{"action":"play","volume":70}}
11:34:32.942 MQT: tele/nspanel/RESULT = {"media":{"action":"pause","volume":70}}

Incoming, you’d send;

{ refresh: {pid:n, name:x, media: { album: a, track:t, artist:a, volume:v }}}

(obviosuly, in valid json with all the quotes, not my shorthand)

Home Page Messages

Here is a summary in shorthand of control messages for home screen;

{start: {pid: x, format:x (1-15)}}
{favorite: {pid: x, format:x (1-15)}}
{dim: {low:n, normal:n}}
{notifications: {text: x, [or] reset:1}}
{weather: {temp:x, summary:x, feels:x, icon:x (openweather list)}}
{clock: {date:x, hour:x, min:x, month:x, weekday:x}} // called by tasmotoa
{switches: {switch1:x, switch2: x}} // called by tasmotoa
{summary: {title:x, temp:x, [or] text:x}}
{warnings: [{id:x (1-4) , type:x (0-7) , state:x (1-3)]},...]}} 
2 Likes

If you can send me that I can have a go at it.

This is the rule code …

import org.slf4j.LoggerFactory

def logger = LoggerFactory.getLogger("org.openhab.core.automation.nspanel")

def str = event.getEvent()
if (str.indexOf('{"button": ')!=0) {
  return
}

// {"button": {"pid": 15, "bid": 1, "state": 1, "next": 0}}

var i = str.indexOf("\"pid\"")
var i2 = str.indexOf(",",i+7)
var pid = str.substring(i+7,i2) as int
i = str.indexOf("\"bid\"")
i2 = str.indexOf(",",i+7)
var bid = str.substring(i+7,i2) as int
i = str.indexOf("\"state\"")
i2 = str.indexOf(",",i+9)
var state = str.substring(i+9,i2) as int

if (pid==10 && bid==1) {
  events.sendCommand("movie_room_lights",state==1?"ON":"OFF")
}
else if (pid==10 && bid==2) {
  //
}

@m-home are you sure you have uploaded? I just downloaded from github and I still have the volume at the top.

I thought I had, it was late, so maybe I forget. I’ll release again shortly with a few other tweeks

v1.0.0-beta11

  • Media Pages Impoved
  • Home Page warnings fixed
  • New start message to assign first swipe page
{"start": {"pid":n, "format":n}}