Custom web pages

With the domotcl web server running, data located under the domotcl/web/html directory can be accessed by a http client (typically a web browser).

Most files are simply served exactly as they are stored on disk. There are two exceptions: If a version of a requested file exists with a .script or .tmpl extension, then the web server generates the output from either of these files. A file with a .tmpl extension functions as a template. A file with a .script extension is executed by the web server as a Tcl script.

Template files

Lines in a template file that start with a percent sign (%) are executed as Tcl code, after stripping off the %. All other lines are simply returned, after substituting variables and code in square brackets.

Example: The following template file will list all png files in the images directory and their sizes:

    images.html.tmpl:
<table>
% set dir [dict get $state options root]
% foreach file [glob -directory [file join $dir images] *.png] {
<tr><td>$file</td><td align="right">[file size $file]</td></tr>
% }
</table>

Script files

A script file is executed by the web server. The return value of the script is served to the http client. The value can be returned using an actual return command. Otherwise, the return value will be whatever the last command of the script produced.

The same example as above, but written as a script file, could be done like this:

    images.html.script:
set dir [dict get $state options root]
set lines "<table>\n"
foreach file [glob -directory [file join $dir images] *.png] {
    append lines "<tr><td>$file</td><td align=\"right\">[file size $file]</td></tr>"
}
append lines "</table>\n"
Note that no explicit return command is used, but the script still works because the append command returns the value of the variable after the specified strings have been appended to it.

The $state variable

In the examples above, the $state variable was used. This variable contains a dict with information about the request. The dict has 3 sections:
options
The value of this key is a dict with information about the script:
prefix
The prefix part of the url path of the request URL.
suffix
The suffix part of the url path of the request URL.
root
The file system path of the directory containing the script or template file.
fspath
The full file system path of the script or template file (without the .tmpl or .script extension!).

request
This dict contains information about the request. These are some of the more interesting values:
peerhost
The IP address of the http client.
peerport
The TCP port of the http client used for this connection.
port
The TCP port of the http server.
rawtime
The time the request was received in seconds since the epoch.
time
The time the request was received, formatted as: Thu Jan 23 21:52:23 CET 2020
method
The request method (GET, POST, HEAD, etc.)
uri
The URL path without the host:port part, but including any query
path
Same as the uri, but without the query part
protocol
The protocol and version, such as HTTP/1.1
query
A list of query parameters and values. The values are a key/value list. But as query parameters don't have keys, the key is always {}.
rawquery
The query part of the URL as received.
rawheader
A list of headers, exactly as they were received.
header
The headers in processed form.

response
This part is used to create the response to the http client. The script or template file can populate data in this section to influence the response.
status
The response code. This is normally 200, but can be changed to redirect the client to another URL, or provide failure indications.
header
Headers to be included in the response. Commonly used headers are content-type and location (in case of a redirect).

HTML as Tcl code

In script files, HTML code can easily be generated by writing HTML as Tcl commands. For example, the code on the left produces the HTML code shown on the right:
html {
    head {
        title {
            str "Test page"
        }
    }
    body {
        str "Hello, World"
    }
}
<!DOCTYPE html>
<html>
<head>
<title>Test page</title>
</head>
<body>Hello, World</body>
</html>
This method becomes very powerful when other Tcl constructs such as loops are incorporated:
html {
    upvar 1 state state
    body {
        foreach key [dict keys $state] {
            str $key
            br
        }
    }
}
The code within the html body runs in a stack frame that is one level below the top level for the script. That is why the $state variable has to be pulled into scope to be able to use it.

Attributes of HTML tags can be specified as options, for example:

a -href index.html {str Home}

Domotcl data and commands

While the information provided above makes it possible to generate web pages, the question arises how domotcl comes into play.

commands

call <type> [<option> <value> ...] <path> <method> [<argument> ...]

The call command provides an easy way to obtain data from other domotcl components (core, tracker) that is exposed via dbus.

The type is an alias for the dbus interface name. Valid types are:

core
com.tclcode.domotcl
node
com.tclcode.domotcl.Node
module
com.tclcode.domotcl.Module
device
com.tclcode.domotcl.Device
control
com.tclcode.domotcl.DevCtrl
action
com.tclcode.domotcl.Action
track
com.tclcode.domotcl.Track

sched eval <SQL query>

The sched command provides access to the schedule database.

websockets

Most of the time, a web page showing data from domotcl should be updated dynamically when anything changes. This is where websockets come into play. Because this is such a common situation, it has been made very easy for the web page developer. The websocket command takes a list of property patterns. Changes of properties matching the patterns are then sent to the web page. An HTML element with an id equal to the device:property will automatically be updated.

The websocket can also be used to send commands to domotcl without reloading the web page. To use this, the javascript wscommand() function is provided when the websocket command is included in the web page body. The function takes the name of the device, the command, and any arguments. If the arguments to the command are something else than strings, you must pass a signature to the wscommand() function.

Example: Assuming you have an integer variable called /variable/counter, the following code will show the value on a button. Clicking the button increments the variable:

html {
    body {
        websocket /variable/counter:value
        button -id /variable/counter:value \
          -onclick {wscommand("-signature i", "/variable/counter", "add", 1)}
    }
}
If you want to show something else than the default presentation of a device property, you can provide your own javascript function. You then have to tell the websocket code to use this function instead of the default function. This can be achieved by providing an onload handler to the body element.

The following code obtains all x10 appliance modules from the database and displays them as checkboxes on the web page. The checkbox shows when the module is on. Clicking the checkbox toggles the module on and off.

html {
    head {
        javascript {
            function wsfunc(evt) {
                var msg = JSON.parse(evt.data)
                for (var name in msg) {
                    var w = document.getElementById(name)
                    if (w) w.checked = msg[name].value === "on"
                }
            }
        }
    }
    body -onload {connect("status.ws", wsfunc)} {
        websocket /x10:state /x10/*:state
        foreach name [sched eval {select name from device \
          where class = 'x10' and type = 'appliance'}] {
            input -type checkbox -id $name:state \
              -onclick "wscommand('$name', 'toggle')"
            str $name
            br
        }
    }
}