LuCI Custom Page for IoT Device

LuCI is a MVC based web framework but it is mainly used as a Configuration Interface for OpenWrt. 

This guide focuses on addition of new pages (modules) to the interface. Since LuCI is mostly use for configuration management so it supports and works with UCI (Unified Configuration Interface). This centralises all the OpenWrt configurations and are saved as files in /etc/config/ directory. The configuration files can also be edited manually using vi or any text editor, through cli (set of commands to set/get the configurations) and through APIs. LuCI uses these APIs to manage the configurations of the UCI files. 

My intent here was to use LuCI to show some stats/data (new-real-time) from some IoT based device. So it doesn’t directly require to work with CBI (Configuration Bind Interface). We can also use this to parse system logs and show specific alarms from syslog.

On OpenWrt LEDE 17.01 (including old version); LuCi installed directory is: /usr/lib/lua/luci. Make sure that LuCI is accessible at:

We will start with adding the controller. We will have to create a directory in controller which will be our app and then a .lua file which will be our module. Assuming we are going with “iot” as app and “device” as module. So we will create the following file:

Content will be:

module("luci.controller.iot.device”, package.seeall)

function index()


Note: Do Not put hyphen “-“ in app or module name. Hyphen means something :).

Index function above is important. LuCI required index function to build a dispatch tree. Now to register a function in the dispatch tree we will use an entry function of luci.dispatcher. 

entry(path, target, title=nil, order=nil)
  • path: table describing the position of the node in the tree
  • target: action when the user access the node (call|template|cbi)
  • title: title in the menu
  • order: order for the sort if multiple nodes are on the same level

Let’s add an entry to our module which will call an action. 

module("luci.controller.iot.device", package.seeall)

function index()
  entry({"admin", "iot", "device"}, call("action_hello"))

function action_hello()
  luci.http.write("Hello World!!!")

By accessing:

We will get:

Hello World!!!

Note: Always remove the cache before testing (super important). Can be done by:
rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/

More attributes can be assigned to table returned by entry call. 

  • dependent: true protects the nodes to be called out of their context if parent node is missing
  • leaf: true stops the request at this node. Dispatch will not proceed
  • sysauth: authorised user
  • sysauth_authenticator: authorisation method (htmlauth)
  • i18n: translation file to load
  • module: current module (luci.controller.iot.device)

To see all attributes applied I used:

x = entry({"admin", "iot", "device"}, call("action_hello"))
x.leaf = true
x.dependent = false

for k, v in pairs(x) do
  if (type(v) ~= "string") then vx = type(v) else vx = v end
  nixio.syslog("notice", k.."=>"..vx)
    if (type(v) == "table") then
      nixio.syslog("notice", "   going in ")
      for k1, v1 in pairs(v) do
        if (type(v1) ~= "string") then v1 = type(v1) end
        nixio.syslog("notice", "    "..k1.."=>"..v1)

Since now we know that our interface is working. We will try to go a bit more formal now.

module("luci.controller.iot.device", package.seeall)

function index()
  entry({"admin", "iot"}, firstchild(), "IoT", 10).dependent=false
  entry({"admin", "iot", "device"}, template("iot-device/index"), "IoT Device", 20).dependent=false

This adds a Tab to the menu. Second, we are using a template as target. So we will now have to add a file to view section.

Create the following file:


Notice that we added iot-device/index in the template in index() above. 

Content of the file should be:

<h1> <%:Hello World!!!%> </h1> 

Clear the cache and refresh the page. From the menu go to IoT > IoT Device

You will see Hello World!!! with LuCI’s header and footer.

To fetch the data of the device we will add a function to controller which will return JSON data by reading it from a file. If the device is connected to our modem over IP and return JSON; we can directly fetch it from view (as we do in the next step). 

Following function returns data from a file:

function action_devicedata()
  local fs = require "nixio.fs"
  local data = fs.readfile("/root/iot/data/temp_latest.json")

And an entry to index() will make it accessible:

entry({"admin", "iot", "device", "devicedata"}, call("action_devicedata")).leaf = true

In view, we will change our index to poll the data and show it in tabular form and update it every 10 seconds.

<script type=“text/javascript”> //<![CDATA[

  XHR.poll(10, '<%=build_url("admin/iot/device/devicedata")%>', null,
function(x, remdata)
{	var e;

        var time = "";
        var devid = "";
        var humidity = "0";
        var temperature = "0";
        if (typeof data != 'undefined' || data !== null) 
            var myDate = new Date( data.timestamp *1000);
            time = myDate.toLocaleString(); 
            devid = data.device_id;
            humidity = data.humidity;
            temperature = data.temperature;

        if (e = document.getElementById('devid'))
            e.innerHTML = devid;

        if (e = document.getElementById('time'))
            e.innerHTML = time; 

        if (e = document.getElementById('humidity'))
            e.innerHTML = humidity+" %";

        if (e = document.getElementById('temperature'))
            e.innerHTML = temperature+" &deg;C";


<h2 name="content"><%:Status%></h2>
<fieldset class="cbi-section">
<legend><%:Device Data%></legend>
  <table width="100%" cellspacing="10">
  	  <tr><td width="33%"><%:Device ID%></td><td id="devid">-</td></tr>
   	  <tr><td width="33%"><%:Time%></td><td id="time">-</td></tr>
   	  <tr><td width="33%"><%:Humidity%></td><td id="humidity">-</td></tr>
      <tr><td width="33%"><%:Temperature%></td><td id="temperature">-</td></tr>

Simple html and javascript. Polls the source (provided in controller) and updates the DOM if it finds the data every 10s. In XHR poll URL, we can use the url of the remote device if data can be directly fetched over ip. 

The source files are available at:

Only files which will be added to luci are uploaded. To test it just put the files in respective folders. 


Leave a Reply

Your email address will not be published. Required fields are marked *