NxPanel - Replacement Firmware for Sonoff NSPanel

NxPanel Installation

To install, firstly flash tasmota as described here;

This will allow you to access the stock screen firmware only

Update: Install has been made much easier

Instead of using the nspanel.be described above. Use the nxpanel.be below;

Once you’ve done that, simple type this into the tasmota console

InstallNxPanel

This will download the current version, flash it, then reboot the device in NxPanel

NxPanel will update to the current version automatically without you having to flash anything, if you enable this setting in the configuration page.

You can install it manually if you prefer;

FlashNextion http://<your-openhab-server>:8080/blah/nxpanel.tft

Going back to Stock Screen firmware

You can easily revert back to stock HMI firmware at any time, if you want. Simply use the same process as above. You just need to use the original TFT file. A link to that is here;

Original Stock HMI Firmware

This will revert the Nextion HMI to stock, not the Tasmota side. This process would involve re-flashing the backup taking during the flash of Tasmota. This is not in scope here.

How is works

Before reading on how to configure openhab to work with NxPanel, you might find it useful to jump to this post for a short explanation of how it works. As the following rules are the things to make it happend.

Configuration in OpenHab

You’ll need a channel trigger for the MQTT thing;

  - id: nxpanel_command
    channelTypeUID: mqtt:string
    label: NxPanel Command
    description: ""
    configuration:
      commandTopic: cmnd/nspanel/nxpanel
  - id: nxpanel_page_trigger
    channelTypeUID: mqtt:trigger
    label: NxPanel Page Trigger
    description: ""
    configuration:
      stateTopic: tele/nspanel/RESULT

And you’ll need a rule something like this configure your screens;

This is a groovy script. Ensure you have install the Groovy Script binding and choose Groovy when you create the rule. This is a template for you to start with. It works with a couple of components on my system and should give you an idea what you need to do for yours.

Note: There is also a javascript version of the following rule here which many might find easier.

import org.slf4j.LoggerFactory

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

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

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

def NONE                 = 0

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

def mqtt = actions.get("mqtt","mqtt:broker:mqtt_broker")

def str = event.getEvent()

logger.info("Demo page rules called")

if (str.indexOf('{"page":')!=0) {
  return
}

/*
 * Utility functions - start
 */

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) {
  var str = new StringBuilder('{"refresh":')
  str<<'{"pid":'<<pid<<',"name":"'<<name<<'",'
  return str
}

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

def makeEmptyRefresh(pid) {
  var str = new StringBuilder('{"refresh":')
  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 - end
 */

/*
 * Get data from the page message
 * (would be good to use JsonSluper here but currently can't access)
 */

var i = str.indexOf("\"pid\"")
var i2 = str.indexOf(",",i+7)
var id = str.substring(i+7,i2)
i = str.indexOf("\"format\"")
i2 = str.indexOf(",",i+10)
var format = str.substring(i+10,i2)

// check if a full refresh or just a status update
var refresh = str.indexOf("refresh")>0

var json

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

def TOPIC = "cmnd/nspanel/nxpanel"

logger.info("updating page ... "+id)

switch (id as int) {  
  
  case PANEL_MAIN :
    logger.info("main panel")
    // set these from your own items
    movie_state = 1
    lounge_state = 0
    cabin_state = 0
    hall_light_state = 1
    if (refresh) {
      json = makePage(id,'Lounge')
      json<<format<<'buttons:['
      json<<makeButton(1,"Movie",BUTTON_TOGGLE,ICON_BULB,movie_state)
      json<<makeButton(2,"Lounge",BUTTON_TOGGLE,ICON_BULB,lounge_state)
      json<<makeButton(3,"Hall",BUTTON_PUSH,ICON_HOUSE)
      json<<makeButton(4,"Bedroom",BUTTON_PAGE,ICON_BED,PAGE_6_BUTTON,PANEL_BEDROOM_1)
      json<<makeButton(5,"Temp",BUTTON_PAGE,ICON_HEAT,PAGE_THERMOSTAT,PANEL_CABIN_THERMO)
      json<<makeButton(6,"Light",BUTTON_DIMMER,ICON_DIMMER,hall_light_state,PANEL_LOUNGE_LIGHT)
      json<<makeButton(7,"Dimmer",BUTTON_DIMMER_COLOR,ICON_DIMMER_COLOR,cabin_state,PANEL_CABIN_LIGHTS)
      json<<makeButton(8,"Status",BUTTON_PAGE,ICON_INFO,PAGE_STATUS,PANEL_STATUS)
      json<<"]}}"
    } else {
      json = makeSyncButtonStart(id,1,movie_state)
      json<<addSyncButton(2,lounge_state)
      json<<"]}}"
    }
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_BEDROOM_1 :
    // set these from your own items
    fan_state = 1
    if (refresh) {
      json = makePage(id,'Bedroom 1')
      json<<format<<'buttons:['
      json<<makeButton(1,"A",BUTTON_PUSH,ICON_HOUSE)
      json<<makeButton(2,"Fan",BUTTON_DIMMER,ICON_FAN,fan_state,PANEL_LOUNGE_FAN)
      json<<makeButton(3,"C",BUTTON_PUSH,ICON_SOFA)
      json<<makeButton(4,"Music",BUTTON_PAGE,ICON_MUSIC,PAGE_MEDIA,PANEL_MUSIC)
      json<<makeButton(5,"D",BUTTON_PUSH,ICON_TALK)
      json<<makeButton(6,"Alarm",BUTTON_PAGE,ICON_BELL,PAGE_ALARM,NONE)
      json<<"]}}"
    } else {
      json = makeEmptySync(id)
    }
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_BEDROOM_2 :
    json = makeEmptySync(id)
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_LOUNGE :
    json = makeEmptySync(id)
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_CABIN :
    json = makePage(id,'Cabin')
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_CABIN_THERMO :
    // set these from your own items
    var heater = 1
    var auto = 0
    var temp = 15
    var set = 21
    json = makePage(id,'Cabin')
    json<<format<<',"therm":{'
    json<<'"set":'<<set<<',"temp":'<<temp<<',"heat":'<<heater<<',"state":'<<auto<<'"'
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_CABIN_LIGHTS :
    json = makePage(id,'Cabin Lights')
    json<<'"power":'<<ON<<',"hsbcolor":'<<'"10,100,50"'
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_LOUNGE_FAN :
    // set these from your own items
    fan_state = ON
    fan_setting = 3
    json = makePage(id,'Lounge Fan')
    json<<'"power":'<<fan_state<<',"min":'<<1<<',"max":'<<4<<',"icon":'<<ICON_FAN<<',"dimmer":'<<fan_setting
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_LOUNGE_LIGHT :
    json = makePage(id,'Lounge Light')
    json<<'"power":'<<ON<<',"dimmer":'<<30
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_STATUS :
    json = makePage(id,'System Status')
    json<<'"status":['
    json<<'{"id":'<<1<<',"text":'<<'"Gate":'<<',"value":'<<'"Open"'<<',"color":'<<2<<'}'
    json<<','
    json<<'{"id":'<<2<<',"text":'<<'"Window":'<<',"value":'<<',"Shut"'<<',"color":'<<3<<'}'
    json<<','
    json<<'{"id":'<<5<<',"text":'<<'"Room Temp":'<<',"value":'<<',"20°C"'<<'}'
    json<<']}}'
    mqtt.publishMQTT(TOPIC, json.toString())
    break
  case PANEL_MUSIC :
    json = makePage(id,'Sonos Player')
    // set these from your own items
    json<<'"artist":'<<'"New Order"'<<',"album":'<<'"Movement"'<<',"track":'<<'"Power Play"'<<',"volume":'<<70
    json<<"}}"
    mqtt.publishMQTT(TOPIC, json.toString())
    break    
  default :
    logger.info("unknown page!")
    break

}

logger.info("rule done")


The Process

As NxPanel moves to a new screen, it will message over MQTT the page id to identify what room it’s looking for to render the page. If the screen was last used to show that room, it messages it only needs a sync of the room. the OH rule then only needs to send back a lightweight message with the states of buttons and so on. If the page was not showing this room last time, the message sent to OH will be a refresh one, and OH will send a heavyweight message back which will tell NxPanel how to render all the buttons and label and room titles.

Only the Home screen and Button pages will remember the last room, other screens will call refresh each time. This is purely down to memory of the Nextion. It only has 3K, which is not enough to remember that state of each screen page.

Useful Things

The buzzer on the NSPanel doesn’t seem to be set to work by default. I needed to do the following in Tasmota. The Alarm panel used this as do some other places.

buzzerpwm 1

UK Timezones

TimeZone 99
TimeDST 0,0,3,1,1,60
TimeSTD 0,0,10,1,2,0
4 Likes