General driver development

The local and remote nodes support devices implemented via general drivers. These drivers have a .drv extension and may be placed in a directory under the domotcl drivers directory.

Common structure

The code in a driver is executed in different stages. When writing a driver, care should be taken that only actions necessary for the applicable stage are performed.
Definition
Domotcl will initially source the driver file to obtain the information necessary to present to a user for configuring the driver parameters.

The code should only do the minimal amount of work necessary to define the driver parameters and the supported device classes. The driver should not yet load support packages it may need for its operation, schedule events, or start any kind of program loop. To enforce this, domotcl puts restrictions on the number of commands that may be executed in this stage. It also prohibits certain actions, like entering the event loop.

Driver initialization
The initialization stage is intended for performing any initial setup for actually running the driver. This is the time to load support packages, set global variables to their initial values and initiate the connection to the hardware the driver interfaces with.

Device initialization
During device initialization, the defined devices are created one by one by running their constructor code. Subdevices are always created after their parents.

Normal execution
After the initialization stages are complete, the driver starts its normal operation. Any code executed during the normal operation of a driver should not be blocking the application for significant amounts of time.

Simple drivers may use the init command for the definition and driver initialization stages, as well as cleanup when the driver is terminated. The init command takes two or three arguments: a list of driver parameters, the code body, and optional cleanup code.

Example:

    init {busname number:q} {
        global bus num
        package require dbus
        set bus [dbus connect $busname]
        set num $number
    } {
        # Stop any running timers
        foreach id [after info] {after cancel $id}
    }
    
More complex drivers will want to use the connect module for driver definition and initialization. Drivers should normally use the device module for device definition and initialization. See the relevant sections below for a detailed description of these modules.

The main command is available if a driver needs to run some code to start its normal operation. This command only takes a code body argument.

Example:

    main {
        coroutine startup try {
            global bus num
            while 1 {
                getvalues $bus $num
                after 30000 [list [info coroutine]]
                yield
            }
        } on error {msg opts} {
            alert [dict get $opts -errorinfo]
        }
    }
    
config config {*}[dict map {key val} [config] {}] make type name description args

Connect module

Many drivers will need to connect to some piece of equipment to perform their function. The connect module simplifies the use of such connections by taking care of setting up the connection, monitoring, recovering dropped connections, and communicating over the connection. The connect module is included in a driver through the command:
    use connect

A connection is defined using connection arguments. The first argument is the connection type. The remaining arguments depend on the value of the connection type. There are currently two connection types supported:

socket host port
Create a connection using a TCP socket.
comport device baudrate
Create a connection using an RS232 serial device.

Defining a connection

The connect module provides a command to be used during the driver definition stage:
definition initcmd [name default ...]
Build a driver definition. The initcmd argument specifies a command prefix to be invoked for driver initialization. Any following name default pairs specify default values for the connection arguments.
During driver initialization, the configured connection arguments are appended to the command prefix, which is then executed.

Creating a connection

The connect module provides a connection class. Instances of the class can be created to set up, maintain, and re-establish connections. A new connection can be created with a specified name, or an auto-generated name:
connection new type arg ... [option value ...]
Create a connection with a new unique auto-generated name
connection create name type arg ... [option value ...]
Create a connection with the specified name.
Both commands expect connection parameters consisting of a connection type followed by the applicable arguments for that connection type. These may be follwed by additional option value pairs to configure the connection.

The following options are available:

  • -blocking boolean
  • -buffering {none|line|full}
  • -buffersize size
  • -encoding name
  • -eofchar spec
  • -translation spec
Both commands return the name of the connection object, which can be used to transfer data over the connection.

Connection methods

callback prefix
Configure a command to be invoked whenever data is received.
chat send expect ...
Run the specified chat script.
check send expect ...
Define a chat script to be used to check the connected device. This script will be executed whenever a chat script fails. If the check script also fails, recovery actions will be performed.
destroy
Close the connection
fd
Returns the channel descriptor, or an empty string if the connection is currently down.
filter prefix
Configure a command to manipulate the received data.
login send expect ...
Define a chat script to be executed every time after reconnecting.
output data
Transmit data over the connection
read [count] [timeout]
Receive data.
verify prefix
Configure a command to be invoked just before data is transmitted in a chat script. The command is invoked with the "send"-string and "expect"-string as additional arguments. If the command returns 0, the send/expect pair is skipped.
stateevent prefix
Configure a command to be invoked whenever the state of the connection changes. At such a time, the command prefix is invoked with two additional arguments appended; the connection name and the event, which is either connect or disconnect.

Chat scripts

A chat script defines a conversational exchange between the driver and the connected device. The script consists of one or more "send-expect" pairs of strings. The script may also contain directives. The chat engine will send each "send"-string to the device and then waits for the "expect"-string. The following directives are available:
LITERAL
The following string is a literal "send"-string. This should be used to send strings that look like a directive.
TIMEOUT ms
Set the time to wait for a response. Default is 1000.
RETRY count
Set the number of times to repeat a "send"-string until the associated "expect"-string is received. Default is 3.
MATCH {exact|glob|regexp [-nocase]}
Set the method used to match the "expect"-string against the received data. Default is "exact".
DELAY ms
Pause some time before transmitting the next "send"-string.
NEWLINE
Send a newline sequence. The exact characters being sent depend on the channel configuration.

Additional commands

connect type arg ...
Convenience procedure that creates a connection instance called fd.
usbreset device
USB devices may sometimes lock up. When the driver detects this situation, this command can be used to reset the USB interface. This command needs the binary command /usr/local/sbin/usbreset to be available.

Device module

The device module can be used to facilitate writing a driver for a class of devices. The module is automatically loaded if one of the commands device, or foundation is used. The module can also be loaded explicitly through the command:
    use device
The module provides the commands: "device", and "foundation".

Device type implementation

The device create command is used to define a device type. The command takes two arguments: a device type name and the definition block. The device type name must be unique within the driver and should normally be short and describe the function of the device type. This name will be presented to the user when creating a new device.

The definition block specifies various features of the device type. It is normally enclosed in braces. The keywords used to specify the features are described below. All keywords are optional.

    constructor
    The constructor keyword specifies the name and type of parameters of a device, the code to initialize a new device, and the code to configure the device. More extensive specification of the parameters of a device can be done with the typedef keyword described later. Both code blocks are executed for every new device of the current device type. Only the second code block gets executed when a device is reconfigured.

    Example:

      constructor {address:i} {
          variable value 0
      } {
          if {$address < 0} {
              error "address must not be negative"
          }
          my UID $address
      }
      
    The device type in the example has one integer parameter called address. The code checks that the specified address is not negative, and then registers the address as the unique identifier for the device using the my UID command. As the name implies, not more than one device can have a specific unique identifier. If an attempt is made to create a device with an already existing unique identifier, an error will be raised and the device is not created.
    destructor
    The destructor keyword allows the developer to specify some code to be executed when a device is deleted. This code will typically clean up some resources used by the device.

    Example:

      destructor {
          my variable afterid
          after cancel $afterid
      }
      
    extend
    When a device type is an extended version of another device type, the definition can build on the existing device type by using the extend keyword followed by the name of the other device type. If the new device type redefines items that were defined in the original device type, the definition in the new device type takes precedence. Procs, funcs and methods defined in the new device type may invoke their counter parts in the original device type using the next and nextto commands. The next command invokes the method in the next device type in the stacking order. Using the nextto command, the desired source device class can be selected.
    Example:
      nextto [device class switch] $data
      
    event
    The event keyword is used to specify which events a device can generate. Events can optionally have a value, in which case they are also known as properties.

    Example:

      event fire
      event amount value:d
      
    The device implementation code generates an event using the my signal command. For properties the command must specify the new value.

    Examples:

      my signal fire
      my signal amount 42.0
      
    proc
    A proc defines a command, its arguments, and implementation that can be invoked on a device. The proc body may optionally be followed by a set of options in the form of key/value pairs. Currently supported options are:
    • confirm A conditional expression indicating whether the user must be prompted for confirmation when manually running the command. Defaults to true when a prompt option exists, and false otherwise.
    • prompt The text to show to the user when prompting for confirmation. Defaults to "Are you sure?"

    Example:

      proc add {num:i} {
          my variable value
          my signal amount [expr {$value + $num}]
      } {
          confirm {$num < 0}
          prompt {Decrease the amount?}
      }
      
    func
    A func defines a function of the device, its return value, arguments, and implementation. A func is very similar to a proc, except it has a return value.

    Example:

      func when sec:i {time:i} {
      	return [expr {$time - [clock seconds]}]
      }
      
    method
    A piece of helper code that can be called by other code that implements the device. Methods are not visible for the user.
    typedef
    The parameters of the constructor, values of events, arguments of procs and funcs, and return values of funcs can be refined using the typedef keyword. The typedef keyword takes a parameter/argument/value name and one or more definitions. See type definitions below for the details.
    graph
    The graph keyword indicates that the user interface may present a graph of the changes to the value over time. For each value, the following attributes can be specified:
    type
    currently the only supported value is line
    color
    default is blue.
    The minimum and maximum values specified via the typedef keyword are used to determine the boundaries of the graph.
    variable
    Specifies variables that are automatically available to all code blocks of the device implementation without the need to declare them inside the code block using the my variable or variable command.
    extend
    Base the device type on previously defined device type.
    track
    Snapshots of the value can be stored in the track database. The code will need to call the store command at appropriate times to actually store the value in the database.
    device
    On driver installation, create a device of the current type. The device command must be followed by the relative name for the device, the arguments for the constructor, and an optional description for the device.

Foundation

A foundation is very similar to a device type, except that it won't be listed as a device type the user can choose. Its purpose is to be used as a common building block for several device types that share only some features.

Feature

A feature is another way to add features to a device definition. The most important difference is that this can be done dynamically. A device may auto-detect that it supports a certain feature and then load that feature definition.

One or more features are loaded into a device using the features method. This method takes a list of feature names. The list replaces any previously defined list of features. This allows features to be both added and removed.

The "feature" command is auto-loaded when used, but can also be explicitly loaded through the use feature command. Features end up at the top of the stacking order. This means that procs, funcs and methods defined in features override the definitions in the device type. The feature definitions may invoke the device type methods using the next and nextto commands.

Type definitions

The general type of parameters, arguments and values is specified by following the name with a colon and a single letter. The following types are supported:
    bboolean, false (0) or true (1)
    ybyte (8-bit unsigned integer)
    n16-bit signed integer
    q16-bit unsigned integer
    i32-bit signed integer
    u32-bit unsigned integer
    x64-bit signed integer
    t64-bit unsigned integer
    d8-byte double in IEEE 754 format
    sstring
    oobject
The typedef keyword defines various aspects of arguments or values. The definition part must be a sequence of option/value pairs. The following options are supported:
    buttons
    Show a series of buttons instead of a pull-down menu and a Run button in the device control section of the editor. This is only applicable for commands that have a single argument that is defined as a non-editable enum (see the edit and enum options). A button will be shown for each enum value. The buttons option specifies a mapping of enum values to zero or more button settings. The icon setting will cause the specified icon to be shown on the button, if found. Otherwise the text setting is used to display text on the button. If neither is specified, the button will show the enum value. A help setting may also be specified to provide additional information to the user when they hover the mouse over the button.
    default
    The default value for an argument.
    display
    How to present the name of the argument to the user.
    edit
    Indicates whether a value may be entered that differs from the predefined enumeration values.
    enum
    Specifies a list of names for the possible values of an argument.
    flags
    A list of options that can individually be set to on or off. The value of the argument is a list of all options that are on.
    format
    A specification of how to present the value to the user.
    help
    An explanation of the argument to present to the user when they hover the mouse over the entry field.
    mask
    Mask the entered text by showing a circle for each character. Commonly used for entering passwords.
    maximum
    The highest value an argument may have.
    minimum
    The lowest value an argument may have.
    pattern
    A list of format strings defining the allowed values for the argument.
    presentation
    Tcl code to transform the property value into a string for presenting to the user. The property value is available via the value variable.
    size
    A suggestion for the width of the entry field.
    step
    The size of the increments when changing the value.
    types
    A list of allowed object types (only applies to object arguments).
    visibility
    Whether the property should be visible in the explorer. Supported values are:
    normal
    The property is always visible in the Status area of the device.
    optional
    The property is visible when the Status area is expanded and hidden when it is collapsed.
    hidden
    The property is never shown in the Status area of the device.

Formatting

The format keyword understands a specification similar to the ANSI C sprintf function. In addition it can also be used to format an integer value representing seconds since epoch as a time & date string. The table below shows how 17:35:15 on Dec 4, 2016 would be formatted using different format specifiers:
    Standard format Alternate format
    %T17:35:15%#T05:35:15 PM
    %hT17:35%#hT05:35 PM
    %lTSun 17:35:15%#lTSun 05:35:15 PM
    %D2016-12-04%#D12/04/2016
    %hD04-12%#hD12/04
    %lD04 Dec 2016%#lDDec 04, 2016
    %llDSun 04 Dec 2016%#llDSun Dec 04, 2016
These format specifiers can be combined with the regular field width- and positional specifiers. For example, to show both the time and date, the following format specification can be used: %1$T %1$D

Proc options

The options dictionary that can be specified for a proc is currently only used by the explorer. The following options are available:
    confirm boolean
    Ask for confirmation when the user clicks the "Run" button, via an "Are you sure?" pop up dialog.
    display string
    How to present the command to the user. If not specified, the command name is used.
    help string
    An explanation of the command to present to the user when they hover the mouse over the "Run" button.
    prompt string
    The prompt to use in the confirmation dialog instead of "Are you sure?". This option implies confirm true.

Predefined methods

The implementation code for the device can use some predefined methods. As with all methods, these can be called using the my command:
    name
    Returns the relative name of the device.
    signal name [value] [repeat]
    Generate an event or notify domotcl of a changed property. If this method is executed with an unchanged property value, it will not actually generate an event unless reapeat is true.
    property name [default]
    Retrieves the current value of a property. If the property does not currently have a value, the method call returns the optional default value or an empty string.
    init var value ...
    Initialize variables for the device. Sometimes the device parameter names may be equal to some variable names. This method allows the variables to be set without causing a conflict.
    callback method [arg ...]
    A convenience method that simplifies attaching a method to an event handler.
    UID [address] [class]
    Claim a unique identifier for the device. Optionally a class may be specified if the identifier must be unique across multiple device types. The method may be used without any arguments to ensure that there will only be a single device of the current type.
    store
    Store a snapshot of the tracked properties in the track database.

Interface code

The device connection command may be used to define the driver parameters and code for a simple interface. For a more complex interface, the connect module can be used.

The interface code will regularly need to locate a device and pass information to it. The device address command can be used for this purpose. The command returns a reference to the device of the specified type that registered the address as its unique identifier. This reference can be used to invoke public methods of the device. If no device registered the address, a dummy reference is returned. The dummy reference accepts any method call with an arbitrary number of arguments without producing an error. This allows the reference to be used without the need to check if there actually is a matching device.

If the interface code needs a list of all devices of a specific type, the device list command can be used.

MQTT device types

A communication protocol that is frequently used in home automation is MQTT. To simplify the definition of device types that use this protocol, the mqttdevice command can be used. These device types should only be used in drivers loaded into a node of the mqtt type (i.e. in drivers with the .mqd extension). The command gets auto-loaded when the mqttdevice command is used.

The mqttdevice command works just like the device command, but it recognizes some additional or changed keywords:

    topic
    Define the MQTT base topics that the device uses to communicate. The topics may be provided as alternating name and value arguments, or as a single argument containing a name/value dict. There are two relevant topic names:
    action
    The topic to use to send instructions to the device. Default: actions/<deviceid>
    event
    The topic the device uses to report information. Default: events/<devicetype>/<deviceid>

    Any "%" character in the topic definitions is replaced by the device ID. Use "%%" for a literal "%".

    action
    Define an MQTT action name and arguments. When the named command is executed in a domotcl action, the associated MQTT message is sent.

    Example: Suppose a domotcl device called /lights/lamp1 contains action level {pct:y} in its device type definition. Then /lights/lamp1 level 50 will result in an MQTT message with topic "actions/lamp1" and body "50".

    event
    The event definition is the same as for a regular device type. By default the event will fire when a matching MQTT message is received.
If the default behavior is not correct for the device type, the definition can include replacements for the action and event methods. These methods are invoked when a command is executed or an MQTT message is received. This may be necessary when the device works with JSON encoded messages, for example.

Note: It is not mandatory to use mqttdevice definitions in an mqtt type driver. If the device uses a very different strategy concerning its MQTT messages, it may make more sense to use a regular device definition and handle the MQTT messages using subscribe and publish commands.

Driver evolution

With increasing insights it may become desirable to modify the driver, which may include adding driver- or device parameters. However, the current schedule may already have instances of the driver and devices without those parameters. To handle this situation, it is possible to provide evolution instructions.

The evolution instructions have the following format:

evolve {
    version 1 {
        interface {
            <rule>
            <rule>
            ...
        }
        type <devtype1> {
            <rule>
            <rule>
            ...
        }
        type <devtype2> {
            <rule>
            <rule>
            ...
        }
        ...
    }
    version 2 {
        interface {
            <rule>
            <rule>
            ...
        }
        type <devtype1> {
            <rule>
            <rule>
            ...
        }
        type <devtype2> {
            <rule>
            <rule>
            ...
        }
        ...
    }
    ...
}
Comments may be placed in the evolve specification by starting a line with #.

A driver that does not contain any evolve instructions is considered to be at version 0. The rules in the "version 1" evolve section describe the changes needed to go from version 0 to version 1. The rules in the "version 2" section describe the changes from version 1 to version 2, etc. The version sections should be listed in increasing order. The last version section determines the current version of the driver. When a user replaces a driver with a newer version. All version sections between the old driver version and the new version will be applied.

Rules

The following rules are currently implemented:
add arg name init [selection]
A device argument has been added. Specify the name and the value to use for existing devices.
remove arg name [selection]
The indicated device argument has been removed. This doesn't actually have to be specified because the code can figure that out.
rename arg old new [selection]
A device argument was renamed from old to new.
rename event old new
A device event was renamed from old to new. "trigger" may be used as an alias for "event".
replace name [selection]
The specified device type replaces the old device type name.

Selection

Some rules accept an additional selection specification to limit which devices the rule applies to. The following selection criteria are supported:
arg name operation value
Selection based on the value of a named argument. Valid operations are = (or ==), <, >, <=, >=, != (or <>).

Example

The system/time driver used to have a device type called event, that had 3 arguments: base, time, and random. The base argument could have 1 of 4 values: clock, sunrise, sunset, or dark. For values other than clock, the time argument actually represented the offset from the selected base. In those cases the time value was allowed to be negative, but was limited to +/- 4 hours. So, the combination of these 4 possibilites in one device type was a bit awkward. It was therefor decided to split the old event device type into two new ones: clock, for events based on wall-clock time, and solar, for events based on the sun rise or sun set. The follwoing evolve section was added to the driver:
evolve {
    version 1 {
        type clock {
            replace event arg base = clock
            remove arg base
        }
        type solar {
            replace event
            rename arg time offset
        }
    }
}
This means:
  • The new clock device type replaces the old event device type for only those devices where the base argument was "clock".
  • The base argument no longer exists on the new clock device type and must be removed.
  • The new solar device type replaces the old event device type for all remaining devices.
  • The time argument of the old event device type has been renamed to offset on the new solar device type.