Indigo Plugin Developer's Guide v1.1
Indigo Plugins and APIs
Indigo has a long history of extensibility - AppleScript Attachment scripts were available from the start. Later, the Indigo Web Server was added along with the IWS plugin and the ability to add custom images.
A couple of problems with this though: the scripts in the Scripts folder were a mix of scripts installed by Indigo and user-installed, the images and plugins for IWS were buried in the IndigoWebServer folder. This led to confusion about how to upgrade.
With Indigo 5.0, we also wanted to add some new functionality: the ability for Indigo to launch scripts at startup and optionally keep them running (if they quit), a better way to encapsulate multiple parts to a solution (for instance, some AppleScripts that also had some additional control page images), a way to easily disable/enable various solutions without having to restart the server, and, most importantly, a new server plugin API that would allow 3rd party developers to more natively add devices, events (triggers), and actions to Indigo.
This new server API would allow users and 3rd party vendors to implement their own functionality in Python with full access to all the objects and events that Indigo understands (referred to as the Indigo Object Model, or IOM). Finally, users wouldn’t have to wait for us to add support for, say, the Global Caché IR devices, the THUM devices from Practical Design, etc. Someone with sufficient skills could implement support for those devices, have them integrated into the Indigo UI as first-class citizens. No strange variable state hacks, fake X10 devices, etc. - these new devices are actually recognized as real devices in Indigo.
What’s more, we wanted to ensure that Python programmers have complete access to everything that AppleScript scripts have access to. While AppleScript is a good language for a lot of things (and we aren’t dropping support for it) we believe that using a more modern, standard, capable, and platform agnostic scripting language is much better for our users in the long run.
FIXME add a common terms (glossary) Indigo IndigoServer plugin host plugin base class
So, given all of these goals, we’ve made some changes to the Indigo support folder structure and introduced a new concept: the Indigo plugin bundle. First, let’s look at the new Application Support folder structure.
Indigo Support Folder Structure
Indigo has always supported customization by the user via various folders in the main Indigo directory located here:
/Library/Application Support/Perceptive Automation/Indigo 4
Located inside this folder were several sub-folders where users could put various scripts, web plugins and images. In Indigo 5.0, we changed the folder structure such that it looks like this:

The first thing you’ll probably notice are the two new folders: Plugins and *Plugins (Disabled)*. These two folders are where the new plugin bundles are stored (see the next section for details). Plugins that are enabled from the UI are located in the Plugins folder and when a user disables a plugin it’s moved to the *Plugins (Disabled)* folder.
Another new folder is the Images folder - this folder mirrors the old folder structure for images that was buried in the IndigoWebServer folder. FIXME: The images in this folder will be prefixed with the string user/ so that they will be distinguishable from images that are included by default with Indigo.
Those who are familiar with previous versions of Indigo will notice a few folders are gone - these have been moved/replaced/superseded. As the developer of some kind of Indigo extension, you’ll really only be interested in what you put into the Plugins folder, and that’s our next topic.
The Indigo Plugin Bundle
One of the biggest problems with extending Indigo was that there were pieces/parts all over the Application Support folder. As the developer of a solution, you often had to tell people to put various parts of your solution in various places, which of course has a huge margin for user error. To address that problem, we’ve created a new bundle type, the Indigo plugin bundle (.indigoPlugin), which has a very specific structure:

The first thing you’ll notice is that this is actually a real Finder bundle - so it appears to be a single file called Example.indigoPlugin. It’s moved around and treated as a single file, and all the user has to do to install your solution is to drag the plugin file (bundle) to the Plugins folder.
Creating a bundle is really easy: just create a folder in the Finder and end the name with .indigoPlugin. The Finder will prompt you about adding the extension .indigoPlugin. Click “Add”, and now your folder appears as a file. To get to the contents, right click on it and select Show Package Contents and it will open a separate window that is, in fact, just a new Finder window just like any other. In this window, you can create the folder structure above to have the elements that your plugin will need. Let’s go through each folder/file and discuss what it does.
First, though, you can see that there is only one top-level folder in the bundle - Contents. All other files/folders are inside that folder. This is the standard Mac OS bundle construction, so we decided to follow the pattern.
The Info.plist File and the Resources Folder
There’s only one file that’s directly inside the Contents folder and it’s required (read very important). The Info.plist file is a standard XML property list file that contains several important key/value pairs. The keys in this file will help Indigo understand what functionality your plugin provides, what it’s name and version number are, etc.
Editing a plist file isn’t difficult, but it’s much easier using the Property List Editor application that Apple ships with the Xcode tools. However, it is just a text XML file that looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PluginVersion</key>
<string>1.0</string>
<key>ServerApiVersion</key>
<string>1.0.0</string>
<key>IwsApiVersion</key>
<string>1.0.0</string>
<key>CFBundleDisplayName</key>
<string>Example Indigo Plugin</string>
<key>CFBundleIdentifier</key>
<string>com.yourdomain.plugin</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>http://www.yourdomain.com/plugin/</string>
</dict>
</array>
</dict>
</plist>
In Property List Editor, it looks like this:

Here is what they keys are for:
PluginVersion- this is the version number for your plugin - it’s shown to the user in the UI and will help you when supporting your plugin users. This key is required.ServerApiVersion- this is the version of the server plugin API as defined by us - for Indigo 5.0, it is 1.0.0. While we hope to keep API changes to a minimum, it’s prudent to version the server API used. This key is only required if your plugin implements a server plugin.IwsApiVersion- much the same as the server plugin version (and defined by us), the web server plugin API is also now versioned - it’s also 1.0.0 with 5.0. This key is only required if your plugin implements a web server plugin.- Bundle display name (
CFBundleDisplayName) - this is a standard Mac OS X key, and its value represents the name of your plugin. It’s used in a bunch of places in the UI, so make sure that it appropriately identifies your plugin. This key is required. - Bundle identifier (
CFBundleIdentifier) - this is another standard Mac OS X key, and it represents a unique string that represents your plugin. This is used for namespacing where necessary in the code, so it is critical that it is unique. The standard reverse DNS naming scheme is what should be used, although if you aren’t a company you’ll need to figure something out (maybe your blog, etc.). You should limit your bundle id to standard alphanumerics as special/extended characters may cause problems. This key is required. - Bundle version (
CFBundleVersion) - another standard Mac OS X key, and it represents the layout of the bundle. This is controlled by us, and for Indigo 5.0 it will be 1.0.0. This key is required. - URL types (
CFBundleURLTypes) - you must specify one URL that represents a web page where your user can get support. In the UI, a help button will be present when a user is looking at the description of your plugin, and if clicked, the user’s browser will open to this URL. Note - this can be a forum topic in the “User Contributions” section of our user forums if you don’t have any other place to host the support page. This key is required.
We want the user experience to be very similar for plugins, at least until it comes to configuration and use of the plugin, so you should be careful to get the Info.plist correct. We will be supplying a Mac application that will help you identify errors in the Info.plist as well as other parts of your plugin.
One of the folders, Resources, is also used for general information about your plugin. FIXME: For this version of the bundle, there should be only one file in there: Description.html. This file, as it’s name implies, is a nice HTML description of your plugin. It isn’t a full HTML file, though - it’s really just some very simple marked up text. No CSS, JavaScript, etc is allowed. You can do link (a href) and image (img) tags, as well as standard HTML style tags like strong and em. This description is going to be displayed in a pretty small view embedded in a dialog box - so be sure to test it thoroughly to make sure that it looks good when viewed in the Indigo Client.
The Scripts Folders
The plugin bundle contains several folders that are (almost) directly parallel the folders in the old Indigo application support Scripts folder.
Attachment Scripts
This folder functions exactly like the old one did - any script that's in this folder will have it’s handlers exposed inside the IndigoServer’s (IS) AppleScript context. When a plugin is disabled, IS will reload all the other active plugins Attachment Scripts folder excluding your plugin - so it’s quite important that you don’t create two plugins that have script dependencies. Each plugin should function completely and self-contained.
Background Items
The purpose of this folder is similar to the old Background Tasks folder. However, it’s much better than the old version: when IS loads the plugin, it launches all the scripts in this folder, and it monitors them to make sure they keep running. If one of these scripts stops running, IS will start it up again.
FIXME: Another bit of news here - these scripts can also be any script file that has the standard shell search string as the first line:
#!/usr/bin/env Python
If you’re using Python, the script will have access to the Indigo Object Model (IOM). FIXME: Really??
Launch Items
This folder works almost exactly the same way as the Background Items folder, but with one critical difference - any script in this folder is run at plugin start time, but IS won’t try to keep it running. The scripts in this folder would be used to initialize some aspect of your plugin. For instance, if your other scripts require a specific Indigo variable to be in place, you can have a script in this folder create the needed variables (if they don’t already exist) and populate them with valid values. The scripts in this folder may also be AppleScripts or any shell script.
Menu Items
This folder takes the place of old functionality. Before Indigo 5.0, if you put an AppleScript inside the Scripts folder (but not in any of the sub-folders), that script showed up in the Scripts menu in the Indigo Client. However, there is no more Scripts menu - it’s been replaced with a menu which contains submenus for each plugin. That submenu, specific to your plugin, will have all the scripts in this folder (minus the extension) added to it. So, it’s an easy way to add functionality to Indigo without having to write a full server plugin.
That pretty much covers the folders that would have scripts of various types in them. The functionality is a superset of the old Scripts folder and is plugin specific so your solution is now more integrated and easier for the user to deal with.
The Indigo Web Server (IWS) Plugin Folder
Another folder, IWS Plugin, may look familiar to you. In previous versions of Indigo, you created a folder in the plugins directory of the IndigoWebServer folder. The contents of this folder were also well defined: it has a reqhandler.py file, maybe a static, templates, and css folder, etc. Well, IWS plugins are now encapsulated inside the IWS Plugin folder in your bundle.
Now, you may be wondering - since it says IWS Plugin, does that mean that it’s only one plugin? The answer would be yes. We believe that an IWS Plugin encapsulate all related functionality - it should operate without the need for any other plugin and within a fixed context. Also, since you have the ability to have as many request handlers as you need, it seems appropriate that you have only one IWS plugin per bundle. We’ll discuss building an IWS Plugin in a later chapter.
FIXME what does the path look like in the browser to the IWS plugin?
The Images Folder
The Images folder mirrors the old IndigoWebServer/images folder. Images available in this folder will be available in the graphical control page editor. Just as a reminder, here’s how that folder is constructed and how the images are used:
backgrounds- put images here that you want to use for control page backgroundscontrols- in this directory, there are 3 more:devices- in this directory, put in images to represent devices. In Indigo 4.1 and higher, we've added some heuristics that will allow you to add many more images. See Image Selection Heuristics below for details.static- in this directory, you can just put static images that are shown when you selectStatic Image / Captionfrom theDisplay:popup in the control page editor.variable- in this directory, you can put images that represent variables. In Indigo 4.1 and higher, we've added some heuristics that will allow you to add many more images. See Image Selection Heuristics for details.
Indigo Web Server (IWS) Plugins
IWS Plugins add functionality to the Indigo Web Server. If you’re familiar with how web forms are processed then it should be pretty straight-forward to understand how to develop an IWS plugin. If you’re familiar with Python, then your job will be even easier. It’s important to note, however, that IWS Plugins that show HTML pages aren’t compatible with Indigo Touch - Touch doesn’t render control pages via HTML so if your plugin presents a UI, Indigo Touch will direct any request to your plugin to Safari rather than render your plugin’s UI within the Indigo Touch application.
As we explained earlier, there can only be one IWS Plugin per plugin bundle. Since an IWS Plugin can handle multiple request types (identified on the URL) either by defining several handlers:
http://localhost:port/examplePlugin/handler1
http://localhost:port/examplePlugin/handler2
http://localhost:port/examplePlugin/handler3
and so on, and any handler can take parameters either via a POST or a GET:
http://localhost:port/examplePlugin/handler1?param1=p1val¶m2=p2val
it seemed likely that most plugin functions could be implemented in a single IWS Plugin. We’ll revisit if we need to in a later release. FIXME discuss where to add the name part of the plugin’s URL.
So, how do you implement an IWS Plugin? Not unlike the plugin bundle in which it resides, an IWS Plugin requires a very specific layout of files/folders in its directory. Namely, something that looks like this:

The folders in this structure are as follows:
*css*- store CSS files used in templates or static pages (see below for details on templates)*images*- store image files in this directory*js*- store javascript files in this directory*static*- store any static HTML files that your plugin would like to serve. For instance, you might want an about HTML file that gets displayed when the user selects a help link.*templates*- store template files here (see below for details on templates)
The ***init**.py* file is unused, but it’s required - it can just be empty. The meat of the functionality is in the reqhandler.py. The structure of this file goes something like this: imports, static hook methods, request handler class.
Imports are just the imports necessary for your plugin to work. These are standard Python import statements. Some common imports you’ll need are FIXME.
There are five optional static hook methods that get called by IWS for various purposes:
*PlugInName()*- this method should return a string with a human readable name for your plugin. It will be used in the Event Log when the plugin loads and will (optionally) be shown on the IWS index (landing) page.*PluginDescription()*- this method should return a string with a human readable description of what your plugin does. This will (optionally) be shown on the IWS index (landing) page. to the right of the name specified above.*ShowOnControlPageList()*- this method should return true if you want your plugin listed on the IWS index page (using the name and description above). If you want your plugin to show up there, you’ll need to make sure you implement an index method in your handler class (described below) so that when the user clicks on the name of your plugin in the list the index page will load.*IndigoConnected()*and*IndigoDisconnected()*- these methods are called just when their name implies - when IWS connects and disconnects from the IndigoServer process. Use these methods if you need to initialize and/or deallocate resources of some type.
The last, and most important part of the code, is your request handler class. The methods defined in this class are automatically called based on the URL requested. The class itself may be named anything, but must be a subclass of BaseRequestHandler:
class ExampleRequestHandler(BaseRequestHandler):
The plugin request handler must subclass from BaseRequestHandler and must call the parent class ***init*** method:
def __init__(self, logFunc):
BaseRequestHandler.__init__(self, logFunc)
Most plugins will have an index method - this is what’s called when the plugin’s path doesn’t contain an actual handler:
http://localhost:port/examplePlugin/
The method should do something for the user - some plugins just call another handler with default values, others will just display a control page like UI, and still other will show a simple text page that tells the user how to use the plugin. This index method, for example, just calls another handler in the class:
def index(self):
thePage = self.getReportForDay()
return thePage
index.exposed = True # exposed = True must be set for handler func to be called
You’ll notice that there’s an extra line after the definition of the index handler:
index.exposed = true
This tells IWS that this method is available as an externally called handler. You can define helper methods in your class that shouldn’t be called directly by the user - adding this line (handlername.exposed) will restrict user access to only those methods which you want the user to use.
That’s the basic structure of the reqhandler.py file for all IWS plugins. You can have as many handlers as you like and they can process parameters just like any forms processing software. For instance, let’s look at the definition of the getReportForDay() method that the index handler calls:
def getReportForDay(self,inDate=None, submit=None, filePrint=None):
The first parameter to all methods is always self, and represents your request handler class. Any parameters defined after that will be taken either from the URL (via GET processing) or from the body via a POST. So, from looking at this method, we can see that the handler is looking for 4 parameters. You could call this method like this:
http://localhost:port/examplePlugin/examplePlugin/getReportForDay?inDate=SOMEDATE&submit=OK&filePrint=no
It would also be processed via a POST with those values in the form. Now that you know how to construct request handlers, let’s talk about how the reqhandler.py file can communicate with IndigoServer. FIXME: add the appropriate stuff here.
So, now you know how to receive a request and how to communicate with IndigoServer. The last piece is how to display the page in the user’s browser. The simplest answer is this: return an HTML string. However, generating HTML manually can be pretty ugly depending on how complex the HTML is. We addressed this problem by integrating the Cheetah template engine into IWS.
In a nutshell, a Cheetah template file is an HTML file with markups in it. Cheetah will allow many Python variable types to be embedded in your HTML template. It also offers some basic flow-control statements like IF and FOR. Check the Cheetah documentation for the complete set of options.
For instance, here is a small snippet of a Cheetah template that generates table rows, with alternating color, one row for each string in an array:
<table>
<tr>
<td colspan=2 align="center"><h1>Report for $inDate</h1></td>
</tr>
<tr><th align="left">Device Name</th><th>Minutes ON</th></tr>
#set $i=1
#for $deviceKey in $totalKeys
#if (($i % 2) != 0)
<tr class="darkRowBg">
#else
<tr class="lightRowBg">
#end if
#set $i=$i+1
<td class="deviceName">$deviceKey</td>
<td class="deviceStateOff">$totals[$deviceKey]</td>
</tr>
#end for
</table>
First we start the table and put in a header row. Then the local variable, $i, is declared and set to 1. Then we loop for every key in the $totalKeys associative array. If $i is an odd number, we make the background for the row dark, otherwise we make it light. We then put the key (which in this case is a device name) in column 1 of the row, and something else (in this case the # of minutes the device was on) in the second column.
Cheetah templates can contain javascript, CSS, etc. - basically everything a normal HTML page can. lines that start with # are interpreted by the template engine, and words starting with $ are interpreted as variables.
Now that you have your template defined, how do you get it processed? That’s actually pretty simple. In your request handler method, you do four things: 1) get (and lock) the plugin template, 2) set the variables used in the template, 3) tell the engine to render the template into a full HTML document, 4) unlock the template.
Here’s an example of how you might render the template file that the snippit above came from:
tmpl = self._GetAndLockPluginTemplate('usageReporter/templates/usageReport.html')
try:
tmpl.totals = totals
tmpl.totalKeys = totalKeys
return tmpl.RenderTemplate()
finally:
tmpl.ReleaseLock();
So, the first line gets the template and locks it so that it can’t be altered during the rendering. The next step is to process the template. Notice that we put all the template processing code inside a try block - we have to do that because we must always release the lock on the template no matter what errors might be encountered during the processing of the template.
Inside the try block, we see these lines:
tmpl.totals = totals
tmpl.totalKeys = totalKeys
These two lines set the cheetah template variables (tmpl.totals and tmpl.totalKeys) to some the local Python variables (totals and totalKeys). This is how you pass data to a template - you just assign the template variable a value from your Python code.
Next, we want to process the template:
return tmpl.RenderTemplate()
This line calls the Cheetah RenderTemplate() method that actually processes your template file along with the variables you pass in and creates a fully rendered HTML template. That gets returned by your handler to the browser for display.
The last two lines are critical:
finally:
tmpl.ReleaseLock();
This will ensure that the template is unlocked even if an error occurred during template processing. Not doing so will cause this template to be unavailable until IWS is restarted.
So, that’s how an IWS plugin is built. It’s quite flexible and can be used for a variety of purposes, from getting data into Indigo (have a handler that “catches” a POST or GET from some other system and stored data into Indigo variables) to creating custom web-based UIs.
Indigo Server Plugins
The final and most comprehensive way to extend Indigo is the Indigo Server Plugin. This mechanism allows you to add native components such as device types, events, actions, and menu items. Because we didn’t want to make the server plugin mechanism dependent on any single OS architecture, we decided to implement them in Python and have the description and user interface for the plugins’ components described in HTML and XML files respectively. We chose Python for several reasons:
- it’s object-oriented nature fits well with the IndigoServer’s representations of various objects
- it is easily interfaced with C++ - which is what the server is written in
- we’re familiar with it (and like it - IWS is based on the Python cherrypy http server)
- it’s cross platform so there’s a lot of documentation and expertise out there (many hardware makers supply a Python interface to their hardware)
- it’s easy to learn (really - we promise)
An important note here: Indigo uses Python v2.5. Python 2.7 is the default installation on Snow Leopard (2.5 is default on Leopard), and there are differences, so make sure that you're using Python 2.5 for your development or you could get a nasty surprise when you turn your code into a plugin (if you're developing code outside the plugin architecture). Snow Leopard does have 2.5 installed, it's just not the default install. It's in this directory:
/System/Library/Frameworks/Python.framework/Versions/2.5/bin/
Likewise, we chose XML because it’s very readable and universally understood and supported. We will not discuss XML in general in this document but we believe that even those developers that aren’t familiar with XML will be able to quickly grasp the concepts since HTML is structurally similar. If you find understanding XML challenging, there are plenty of resources both on the web and in print that can help you get up to speed.
Building a Server Plugin vs Scripting IOM
The IOM is used for two similar purposes: scripting Indigo and building Server Plugins. They aren’t mutually exclusive, but they serve different needs. For instance, you may just want to write an embedded Python script action or condition vs building a full Server Plugin. Likewise, you may be interested in building a Server Plugin that doesn’t actually create any new device types, but simply adds events, actions, and menus to Indigo. This is fundamentally different from how AppleScript works with the IndigoServer.
FIXME here, we need to describe the old AppleScript embedded vs File concept and how it’s no longer necessary for IOM since IOM scripts will be run in a different process anyhow and the fact that Python scripts have to be executed within a specific environment to get access to IOM.
Indigo Plugin Host
Before we get to the specifics, let’s describe the process by which your plugin will get executed. Each Server Plugin will be launched in a special application called the Indigo Plugin Host (IPH). Each plugin will have it’s own instance of an IPH as well, so one plugin isn’t likely to bring down another or the IndigoServer.
The IPH communicates with the IndigoServer through the XML interface the IndigoServer provides. But, fear not, the IPH hides all this complexity from you. It creates and manages C++ objects (and bridges them to native Python objects) that represent the Indigo Object Model (IOM), deals with communication with the IndigoServer, and makes sure that the IOM is kept in sync with the IndigoServer.
More specifically, the IOM is presented to your plugin as a module, indigo, that all plugins automatically import. Every object that represents an Indigo object and every command that you use to communicate with Indigo is done through the indigo module. For example, to write something to the Indigo Log, you would do this:
indigo.server.log(“Write this to the event log”)
We describe the IOM in detail in the IOM Reference Guide.
The IndigoServer will manage the IPH for your plugin - starting it at IndigoServer startup or when the user enables your plugin, shutting it down at IndigoServer shutdown time or when the user disables your plugin. You never need to worry about process management or what happens when your plugin fails. IndigoServer will attempt to restart a failed plugin and warn the user when it can’t.
Server Plugin Folder
The structure of the Server Plugin directory in the plugin bundle is something like this:

Each of the XML files describes the components that your plugin provides. You must also have at least the plugin.py file which is the entry point into the Python code that executes your plugin. Beyond that, you may create any other structure you like inside the folder. We’ll go over each file in detail, but first we should discuss the general characteristics of the XML files.
Note: you can’t edit generic XML documents like these with the Property List Editor application - it will only edit correctly formatted property lists (which are XML, but specially formatted). You’ll need to use a generic text editor such as TextMate, TextWrangler, BBEdit or Xcode.
Indigo Plugin XML Conventions
The Indigo plugin XML is formulated with a few rules that will help you navigate it’s constituent parts. Elements, such as Field, Action, Device, etc., will always start with a capitol letter. Attributes, such as id, type, defaultValue, etc, will always start with a lowerCase letter. Both elements and attributes will be camel case - that is, aside from the initial character described above, each new word will be capitalized.
Configuration Dialogs
The Indigo plugin XML contains some structures that are used in the various component XML files. Devices, Events, Actions, and the PluginConfig each may describe some type of user interface, so we needed a simple description language that described the user interface elements and layout. So, for the first 3, we created an XML element called a ConfigUI (we’ll discuss the PluginConfig a bit later). Here’s an example for a device:
<ConfigUI>
<SupportURL>http://www.yourdomain.com/plugin/ApplianceModule.html</SupportURL>
<Field id="autoLabel"
type="label"
visibleBindingId="manualEntry"
visibleBindingValue="0">
<Label>First, put your device into linking mode.</Label>
</Field>
<Field id="exampleButton"
type="button"
tooltip="Click this button to start the automatic discovery process."
visibleBindingId="manualEntry"
visibleBindingValue="0">
<Label>Then click this button:</Label>
<Title>Find Node ID</Title>
<Action>validPythonMethodName</Action>
</Field>
<Field id="nodeID"
type="textfield"
readOnly="YES"
visibleBindingId="manualEntry"
visibleBindingValue="0">
<Label>Node ID:</Label>
</Field>
<Field id="simpleSeparator1" type="separator"/>
<Field type="checkbox"
id="manualEntry"
defaultValue="no">
<Label>Manually Enter Node ID:</Label>
<Description>(Not Recommended)</Description>
</Field>
<Field id="manualNodeID"
type="textfield"
visibleBindingId="manualEntry"
visibleBindingValue="1">
<Label>Enter Node ID:</Label>
</Field>
</ConfigUI>
The ConfigUI definition will result in the following dialog being presented to the user:

When each component type (device, event, action) needs a configuration user interface, and most will need some kind of configuration, it will have a ConfigUI element that describes the UI field elements along with a URL. When the user clicks on help button in the lower left corner (as shown above) their browser will be opened to the URL provided. If no URL is specified then the user will be directed to the main URL specified in the Info.plist file.
The rest of the elements in the ConfigUI represent fields that are shown in the dialog. We’ll go through each field type, starting with a screen shot of how it’s rendered, then the XML, and finally the details for each. The order in which you specify the fields is the order that they will show up in the dialog, and the id attribute is required for every field and must be unique within the dialog - it’s used to establish enabled and visible bindings between fields and more importantly it’s the key used in the dictionary to get the values when you get messages from the dialog (discussed more later).
You'll have the opportunity to validate the fields before they're saved (as well as know when the user cancels out of a dialog). How that's done is discussed at the end of this section.
Text Field

<Field id="simpleTextField" type="textfield" enabledBindingId="checkboxSample" defaultValue="Default Value">
<Label>Text Field:</Label>
</Field>
A text field is use to collect text input from the user. The following table describes the attributes that are available for text fields.
| Attribute | Required | Notes |
|---|---|---|
defaultValue |
No | You can enter a default value here. |
enabledBindingId |
No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field. |
hidden |
No | If this attribute is set to true then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. |
id |
Yes | This is a unique identifier for this Field within the context of this ConfigUI element. |
tooltip |
No | Unfortunately, enabled text fields won’t show tooltips (Cocoa limitation), so your tooltip should probably tell the user why it’s disabled (since that’s the only time it’ll show) if the field is ever disabled. |
type |
Yes | This must be textfield. |
visibleBindingId |
No | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a visibleBindingValue |
visibleBindingValue |
No | If you specify a visibleBindingId, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to 0 will mean the text field is visible only when the checkbox is unchecked, and a 1 means the opposite. To make the field dependent on a list or menu, set this value to a comma separated list of option values (item1, item2). You can even bind it to the value in another text field, but that’s probably of limited use. |
A text field contains only one element: Label. This label is shown to the left of the field and is optional, although there are probably very few times when you won’t want a label. Labels are available on every Field type except for the separator.
Popup Menu

<Field type="menu" id="simplePopUpButton" defaultValue="item2">
<Label>Popup Menu:</Label>
<List>
<Option value="item1">Item 1</Option>
<Option value="item2">Item 2</Option>
<Option value="item3">Item 3</Option>
</List>
</Field>
Popup menu fields are used to select a single fixed value from a list with no inherent hierarchy. The following table describes the attributes that are available for menu fields.
| Attribute | Required | Notes |
|---|---|---|
defaultValue |
No | You can enter a default value here. The value must be one of the values from the List element described below. |
enabledBindingId |
No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field. |
hidden |
No | If this attribute is set to “true” then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. |
id |
Yes | This is a unique identifier for this Field within the context of this ConfigUI element. |
tooltip |
No | Unfortunately, enabled menus won’t show tooltips (Cocoa limitation), so your tooltip should probably tell the user why it’s disabled (since that’s the only time it’ll show) if the field is ever disabled. |
type |
Yes | This must be “menu”. |
visibleBindingId |
No | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a visibleBindingValue |
visibleBindingValue |
No | If you specify a visibleBindingId, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to “0” will mean the menu field is visible only when the checkbox is unchecked, and a “1” means the opposite. To make the field dependent on a list or another menu, set this value to a comma separated list of option values (“item1, item2”). You can even bind it to the value in a text field, but that’s probably of limited use. |
Static popup menu fields contain two elements. The first is the same Label element that every field has (except Separator which we’ll talk about later). The other is a List element which defines the menu items in your popup menu field. The List element contains multiple Option elements, each of which have a required value attribute. The value attribute is what is what will be passed back to your plugin when the dialog is validated; it may not contain comma characters. The text inside the Option element is what is displayed for each menu item in the user interface.
You may also specify a menu dynamically at runtime - that is, when the UI is shown, the client will call into your plugin to get the menu items. This will allow you to dynamically adjust the menu items every time the UI comes up. You can also specify some built-in dynamic items that the IndigoServer will provide automatically for you. See the #Dynamic Lists section below for details.
List

```xml
List fields allow for the selection of 0-N items with no implicit hierarchy. The following table describes the attributes that are available for list fields.
| Attribute | Required | Notes |
| --- | --- | --- |
| *`defaultValue`* | No | You can enter a default value here. The value must be a comma separated list of values, each value must be listed as an option in the *`List`* element. |
| *`enabledBindingId`* | No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the *`id`* of that field. |
| *`hidden`* | No | If this attribute is set to *`true`* then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. |
| *`id`* | Yes | This is a unique identifier for this *`Field`* within the context of this *`ConfigUI`* element. |
| *`tooltip`* | No | The tooltip will be shown when you hover over the actual list control. |
| *`type`* | Yes | This must be *`list`*. |
| *`visibleBindingId`* | No | If you want to conditionally show/hide this field based on the value of another field, set this value to the *`id`* of another field. If you set this, you must also include a *`visibleBindingValue`* |
| *`visibleBindingValue`* | No | If you specify a *`visibleBindingId`*, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to *`0`* will mean the menu field is visible only when the checkbox is unchecked, and a *`1`* means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of *`Option`* values (*`item1, item2`*). You can even bind it to the value in a text field, but that’s probably of limited use. |
Static list fields are constructed almost identically to static popup menu fields - with a *`Label`* element and a *`List`* element which contains multiple *`Option`* elements. Each *`Option`* element must have a *`value`* attribute and that attribute is what is what will be passed back to your plugin when the dialog is validated; it may not contain comma characters. The text inside the *`Option`* element is what is displayed for each line in the user interface.
As with *`menu`* fields, you may also specify a list dynamically at runtime - that is, when the UI is created, the client will call into your plugin to get the list items. This will allow you to dynamically adjust the list items every time the UI comes up. You can also specify some built-in dynamic items that the IndigoServer will provide automatically for you. See the [#Dynamic Lists](#dynamic-lists) section below for details.
[Back to Configuration Dialogs](#configuration-dialogs)
#### Dynamic Lists
You may create dynamic menus and lists that are created at runtime. Indigo will supply some specific ones for you or your plugin may be called each time the UI is presented that will allow you to return anything you like.
To specify a dynamic list, you add some attributes to the List element and skip adding Option elements. To request any of the built-in lists, you specify a class attribute and (optionally) a filter attribute. For example, this field:
```python
<Field id="insteonDimmers" type="menu">
<Label>INSTEON Dimmers:</Label>
<List class="indigo.devices" filter="indigo.dimmer,indigo.insteon"/>
</Field>
would request a list of all INSTEON dimmer devices. The list would be constructed with the device name as the menu or list item viewable by the user, and the value would be the device ID.
The indigo.devices class specifies that the list will contain Indigo devices. If you don’t specify a filter, all devices defined by the user in Indigo will be returned. You can supply two filters if and only if one is an interface filter (see the first two in the list below). The filters are ANDed together, so one interface filter and one device type filter are the only combinations that will result in a non-empty list.
Class: indigo.devices |
|
|---|---|
| Optional Filters | Description |
indigo.insteon |
include INSTEON devices - this is an interface filter that can be used with other filters |
indigo.x10 |
include X10 devices - this is an interface filter that can be used with other filters |
indigo.responder |
include devices whose state can be changed |
indigo.controller |
include devices that can send commands |
indigo.relay |
relay devices |
indigo.dimmer |
dimmer devices |
indigo.sprinkler |
sprinklers |
indigo.thermostat |
thermostats |
indigo.iodevice |
input/output devices |
indigo.sensor |
all sensor type devices: motion sensors, TriggerLinc, SynchroLinc (sensor devices that have a virtual state in Indigo) |
The indigo.triggers class specifies that the list will contain Indigo triggers. Only a single filter from the list below will return a useful list.
Class: indigo.triggers |
|
|---|---|
| Optional Filters | Description |
| indigo.insteonCmdRcvd | INSTEON command received triggers |
| indigo.x10CmdRcvd | x10 command received triggers |
| indigo.devStateChange | device state changed triggers |
| indigo.varChange | variable changed triggers |
| indigo.serverStartup | startup triggers |
| indigo.powerFailure | power failure triggers |
| indigo.interfaceFail | interface failure triggers - can be used with or without a specified protocol |
| indigo.interfaceInit | interface connection triggers - can be used with or without a specified protocol |
| indigo.emailRcvd | email received triggers |
Using the indigo.schedules class will result in a list of all schedules, as will indigo.actionGroups and indigo.controlPages for their respective object types. None of these classes support any kind of filter. Lastly, indigo.variables will get you a list of variables. If you include filter="indigo.readWrite", you’ll get only variables for which you can change the value. Currently, there’s only one read-only variable (isDaylight) defined by the system, but it may be possible to create them in future versions of the API.
You may also include custom dynamic lists that are constructed on-the-fly by your plugin. If you defined your list like this:
<Field id="insteonDimmers" type="menu">
<Label>INSTEON Dimmers:</Label>
<List class="com.mycompany.myplugin" filter="stuff" method="myListGenerator"/>
</Field>
Then your plugin will have the method specified called with the filter. For the above example, you must define a method like this:
def myListGenerator(self, filter)
# From the example above, filter = “stuff”
# You can pass anything you want in the filter for any purpose
# Create an array where each entry is a list - the first item is
# the value attribute and last is the display string that will
# show up in the control
myArray = [(“option1”, ”First Option”),(“option2”,”Second Option”)]
return myArray
The field would then be created as if you had specified it statically like this:
<Field id="customList" type="menu">
<Label>Select an Option:</Label>
<List>
<Option value="option1">First Option</Option>
<Option value="option2">Second Option</Option>
</Field>
This mechanism allows you to create any number of lists: devices dynamically discovered through some discovery protocol, calendars available, email addresses, iTunes playlists, etc.
Checkbox

```xml
Checkbox fields allow for binary selections (yes/no, true/false, etc). The following table describes the attributes that are available for checkbox fields.
| Attribute | Required | Notes |
| --- | --- | --- |
| *`id`* | Yes | This is a unique identifier for this Field within the context of this *`ConfigUI`* element. |
| *`type`* | Yes | This must be “list”. |
| *`defaultValue`* | No | You can enter a default value here. The value must be either *`yes`* or *`no`*. |
| *`enabledBindingId`* | No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the *`id`* of that field. |
| *`hidden`* | No | If this attribute is set to *`true`* then the field will never be displayed regardless of what other options you have set for it. It’s useful if you need to contain some kind of state variables for the dialog that are controlled by button presses rather than controlled directly by the user. |
| *`visibleBindingId`* | No | If you want to conditionally show/hide this field based on the value of another field, set this value to the *`id`* of another field. If you set this, you must also include a *`visibleBindingValue`* |
| *`visibleBindingValue`* | No | If you specify a *`visibleBindingId`*, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to *`0`* will mean the menu field is visible only when the checkbox is unchecked, and a *`1`* means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (*`item1, item2`*). You can even bind it to the value in a text field, but that’s probably of limited use. |
| *`tooltip`* | No | The tooltip will be shown when you hover over the actual list control. |
Checkboxes contain the standard required Label element. They also contain an optional element, *`Description`*, that’s used to deliver extra information on the right side of the checkbox. Many checkboxes may not need this extra information.
[Back to Configuration Dialogs](#configuration-dialogs)
#### Label

```xml
<Field id="exampleLabel" type="label"
<Label>This is where you can put your label text. If need be it'll wrap.
</Label>
</Field>
Label fields are used to present text that span the width of the dialog. As you may have noticed, each control has an element called Label (with the exception of Separator, discussed next). When the dialog is displayed, each of those Label elements is right aligned and the actual control is left aligned directly to the right of the label. This field gives you a way to communicate a much longer chunk of text - like instructions, etc. It will actually span the entire width of the dialog and will wrap as necessary. The following table describes the attributes that are available for label fields.
| Attribute | Required | Notes |
|---|---|---|
id |
Yes | This is a unique identifier for this Field within the context of this ConfigUI element. |
type |
Yes | This must be label. |
enabledBindingId |
No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field. |
visibleBindingId |
No | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a visibleBindingValue |
visibleBindingValue |
No | If you specify a visibleBindingId, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to 0 will mean the menu field is visible only when the checkbox is unchecked, and a 1 means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (item1, item2). You can even bind it to the value in a text field, but that’s probably of limited use. |
Labels have a single element, Label, that holds the text to be displayed. Note: in a label field type, the Label element is required. This field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details).
Separator

<Field id="simpleSeparator1"
type="separator"
visibleBindingId="checkboxSample"
visibleBindingValue="1"/>
Separators work very similarly to labels, except that they just draw a nice embossed line. You can use it to separate sections of your config dialogs. Used in conjunction with the visible attributes, you can make whole sections appear and disappear based on other data in the dialog. The following table describes the attributes that are available for separator fields.
| Attribute | Required | Notes |
|---|---|---|
id |
Yes | This is a unique identifier for this Field within the context of this ConfigUI element. |
type |
Yes | This must be separator. |
enabledBindingId |
No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the id of that field. |
visibleBindingId |
No | If you want to conditionally show/hide this field based on the value of another field, set this value to the id of another field. If you set this, you must also include a visibleBindingValue |
visibleBindingValue |
No | If you specify a visibleBindingId, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to 0 will mean the menu field is visible only when the checkbox is unchecked, and a 1 means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (item1, item2). You can even bind it to the value in a text field, but that’s probably of limited use. |
Separators are the only field type that contain no Label element. In fact, they don’t contain any elements at all, which is why the Field element that represents it is self terminating (notice the forward slash just before the closing bracket for the opening Field tag). This field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details).
Button

```xml
Button fields allow your user to communicate with your plugin during configuration. The following table describes the attributes that are available for button fields.
| Attribute | Required | Notes |
| --- | --- | --- |
| *`id`* | Yes | This is a unique identifier for this Field within the context of this *`ConfigUI`* element. |
| *`type`* | Yes | This must be *`button`*. |
| *`enabledBindingId`* | No | If you want to conditionally enable/disable this field based on the value of a checkbox, set this value to the *`id`* of that field. |
| *`visibleBindingId`* | No | If you want to conditionally show/hide this field based on the value of another field, set this value to the *`id`* of another field. If you set this, you must also include a *`visibleBindingValue`* |
| *`visibleBindingValue`* | No | If you specify a *`visibleBindingId`*, you must specify the value(s) here. For instance, if you are binding to a checkbox, setting this to *`0`* will mean the menu field is visible only when the checkbox is unchecked, and a *`1`* means the opposite. To make the field dependent on another list or menu, set this value to a comma separated list of option values (*`item1, item2`*). You can even bind it to the value in a text field, but that’s probably of limited use. |
Aside from the standard *`Label`* element, the button field also contains two other required elements. The first is the *`Title`* element - this is the string that is displayed inside the button. The other required element is the *`CallbackMethod`* element, which contains the name of a Python method in your code that’s called when the user presses the button. This field type is read-only, which means that you can't have an error message attached to it from a validation method or a button method (see below for details).
As an example, if your plugin needs to start listening for a specific network broadcast packet that a device sends when it’s in a special discover mode, then you probably only want to listen for that packet when the user actually makes the device start to broadcast. So, as in the example device dialog above, you instruct the user to press a button on the device, then have them click the button. This would give your plugin an opportunity to do something while the configuration dialog was up and return the results to the dialog.
The process flow is this:
1. The user clicks on the button.
1. A dictionary containing all the values of the fields in the dialog are sent to the method that you identify in the *`<CallbackMethod>`* element.
1. Your plugin can then do whatever it needs to and it returns a the dictionary back to the dialog containing any field changes.
1. The dialog will update the values (and enable/visible bindings appropriately) for any changed fields.
Each button's method signature will match the parameters that are used in the validation methods described below. For instance, if you specify a method called "beginPairing" in a button field of the *`<ConfigUI>`* XML for a device like this:
```xml
<Device type="custom" id="mediaserver">
<Name>Music Server</Name>
<ConfigUI>
<Field id="pair" type="button">
<Label>Click to begin pairing:</Label>
<Title>Begin Pairing</Title>
<CallbackMethod>beginPairing</CallbackMethod>
</Field>
<!-- SNIP -->
</Device>
The method for that event in your plugin.py file would look something like this:
def beginPairing(self, valuesDict, typeId, devId):
# do whatever you need to here
return valuesDict
Note that the parameters match those specified below for the validateDeviceConfigUi method.
Validation Methods
When the user clicks on the save button, a validation method is automatically called with all the field/value pairs from your dialog. This table shows which method names are called based on the calling dialog type:
| Dialog Type | Method Signature | Parameters |
|---|---|---|
| device | validateDeviceConfigUi(self, valuesDict, typeId, devId) |
typeId - device type specified in the type attribute \ deviceId - the unique device ID for the device being edited (or 0 of it's a new device) \ *valuesDict* - the dictionary of values currently specified in the dialog |
| event | *validateEventConfigUi(self, valuesDict, typeId, eventId)* |
typeId - event type specified in the type attribute \ eventId - the unique event ID for the event being edited (or 0 of it's a new event) \ *valuesDict* - the dictionary of values currently specified in the dialog |
| action | validateActionConfigUi(self, pluginProps, typeId, devId) |
typeId - action type specified in the type attribute \ actionId - the unique action ID for the action being edited (or 0 of it's a new action) \ *valuesDict* - the dictionary of values currently specified in the dialog |
| PluginConfig | *validatePrefsConfigUi(self, valuesDict)* |
*valuesDict* - the dictionary of values currently specified in the dialog |
Before the dialog is dismissed, this method will be called with a dictionary containing all the fields in the dialog. In your validation method, you’d do whatever validation is necessary. If everything validates correctly, then just return //True//:
def validateEventConfigUi(self, valuesDict, typeId, eventId):
# Do your validation logic here
return True
If you need to adjust the values, return the valuesDict with changes:
def validateEventConfigUi(self, valuesDict, typeId, eventId):
# Do your validation logic here
valuesDict["someKey"] = someNewValue
return (True, valuesDict)
If you have errors that the user must correct, then you’ll return 3 things:
False, to indicate that validation failed- The values dictionary (with or without any changes)
- An error dictionary. The keys will be the fieldId and the value will be the error string. When the dialog receives this return, it will turn each field with an error red and add a tooltip to the label part of the field so the user can mouse over the label to see what’s wrong.
def validateEventConfigUi(self, valuesDict, typeId, eventId):
# Do your validation logic here
errorDict = indigo.Dict()
errorDict["someKey"] = "The value of this field must be from 1 to 10"
valuesDict["someOtherKey"] = someNewValue
return (True, valuesDict, errorDict)
If you don't define these methods, then the default behavior is to have the validation calls always return True.
Dialog Close Notification
So you know that when the user clicks the "Save" button in the dialog, your validation method will get called (if it exists). But, what if your dialog starts some separate thread (perhaps as a result of a button click), then the user clicks the "Cancel" button? You still need to know to stop the thread that's doing something, right? There is another, final method that's called at the very end of the dialog's lifecycle. It's very similar to the validation method, but with an additional parameter that tells you whether the user cancelled the dialog. Here are the various closed notification methods:
| Dialog Type | Method Signature |
|---|---|
| device | closedDeviceConfigUi(self, valuesDict, userCancelled, typeId, devId) |
| event | closedEventConfigUi(self, valuesDict, userCancelled, typeId, eventId) |
| action | closedActionConfigUi(self, valuesDict, userCancelled, typeId, actionId) |
| PluginConfig | closedPrefsConfigUi(self, valuesDict, userCancelled) |
The parameters are exactly the same as in the validation methods with the addition of userCancelled - which is a boolean. We're sure there are other reasons why one would need to know when the dialog closes so we wanted to make sure that the complete lifecycle of the dialog was exposed to you.
PluginConfig.xml
We started the discussion of the Indigo XML by describing the ConfigUI element that’s present for Devices, Events, and Actions. But, how do you configure your plugin itself? For instance, let’s say your plugin requires a connection to some hardware interface, say something akin to the INSTEON PowerLinc interface. How do you specify which serial port that interface is connected to?
The simple answer is that, just as there is a configuration interface for an INSTEON hardware adaptor, your plugin has a configuration interface as well. The PluginConfig.xml file describes the UI to configure your plugin. The root element in that file is PluginConfig, and it contains exactly the same elements that the ConfigUI element described above does. Here’s an example:
<?xml version="1.0"?>
<PluginConfig>
<SupportURL>http://www.yourdomain.com/plugin/config.html</SupportURL>
[SNIP - lots of <Field> definitions]
</PluginConfig>
The IPH will handle retrieving saved preferences and passing them to your plugin as well as saving changed preferences to disk. Your preferences will be stored in a file in this directory:
/Library/Application Support/Perceptive Automation/Indigo 5/Preferences/Plugins/
and it will be named by using your plugin’s ID (as defined by the CFBundleIdentifier in the Info.plist file). So, using the example Info.plist at the beginning of this doc, the pref file would be named:
com.yourdomain.plugin.indiPref
We write each plugin’s preferences into individual files so that it will be easier for you to help your users troubleshoot problems by deleting the prefs and starting over. We also write them into the standard Preferences folder so that when you upgrade your plugin or we upgrade Indigo, the preferences won’t get lost.
Devices.xml
One of the main components that a server plugin can define is a device. In Indigo, devices are the primary objects that users deal with. Lights, thermostats, sprinklers, I/O devices are all examples of device types. One of the biggest requests we get is to support the [name your favorite device]. Obviously, we can’t keep up, so the server plugin architecture was designed to allow you to add device support to Indigo.
The root element in the Devices.xml file is Devices. Contained in the Devices element are an unlimited number of Device elements that define the device types that your plugin will define. Each Device will contain a ConfigUI element to collect the specific configuration information for a device (note that you don’t need to include name, description, or typeId from the user - Indigo will collect those for you). Here are the specific attributes and elements that can be defined in a Device element:
| Name | Type | Required | Notes |
|---|---|---|---|
type |
Attribute | Yes | This must be one of: relay, dimmer, or custom. We’ll discuss each below. |
typeId |
Attribute | Yes | This is your plugins unique identifier for this device type. It must be unique in this plugin. |
DeviceTypeName |
Element | Yes | This is the name of the device type that users will see when selecting the type in the Devices dialog. |
ConfigUI |
Element | No | This is the custom UI for configuring the device. It’s optional but in reality we can’t envision a device that needs no configuration. |
States |
Element | No | This element is only used for custom device types. It describes all the possible states for the device. See the description below for more details. |
In this first iteration of our plugin API, we are going to allow you to define three types of devices: relay, dimmer, and custom. Relay and dimmer devices will share the common UI, states, actions that the corresponding INSTEON and X10 devices do.
FIXME: describe type and how it effects the control UI for the device
Relay Device Type
Relay devices are functionally equivalent to appliance modules - they support only one state: on state. They also support a status request. The definition of a relay device is pretty simple:
<Device type="relay" typeId="appliance">
<DeviceTypeName>Appliance Module</DeviceTypeName>
<ConfigUI>
<SupportURL>http://www.yourdomain.com/plugin/relay.html</SupportURL>
<Field id="autoLabel" type="label"
<Label>First, put your device into linking mode.</Label>
</Field>
<Field id="exampleButton" type="button" >
<Label>Then click this button:</Label>
<Title>Find Node ID</Title>
<Action>validPythonMethodName</Action>
</Field>
<Field id="nodeID" type="textfield">
<Label>Node ID:</Label>
</Field>
<Field id="simpleSeparator1" type="separator"
visibleBindingId="manualEntry" visibleBindingValue="0"/>
<Field type="checkbox" id="manualEntry" defaultValue="no">
<Label>Manually Enter Node ID:</Label>
<Description>(Not Recommended)</Description>
</Field>
<Field id="manualNodeID" type="textfield"
visibleBindingId="manualEntry" visibleBindingValue="1">
<Label>Enter Node ID:</Label>
</Field>
</ConfigUI>
</Device>
The device definition above will show the user the following when a new device is created in Indigo:

When the user selects “Plugin” from the “Type:” menu, the “Plugin” and “Device” popups will be shown. When the user selects your plugin from the “Plugin” popup, a list of all the device types you’ve defined will be populated in the “Device” popup. The string in the <DeviceTypeName> element (see above) is what’s used for the menu items.
FIXME: insert the Python handling code here for turnOn, turnOff, statusRequest
Dimmer Device Type
Like a relay device, a dimmer device type will inherit the UI, actions, etc. that all other dimmer type devices have. They have two states: on state and brightness level. They also have a status query action. The XML for a dimmer device would look exactly the same as the XML for a relay except that the typeId attribute would be dimmer.
Custom Device Type
The custom device type is where the real action happens. Because it’s completely custom, you must specify everything about the device - it’s states and their types (Integer, Float, String, List (enumeration), Boolean). So, here’s a simple example of the <States> element of a custom device, in this case a thermostat:
<States>
<State id="temperature" readOnly="YES">
<ValueType>Float</ValueType>
<TriggerLabel>Temperature</TriggerLabel>
<ControlPageLabel>Temperature</ControlPageLabel>
</State>
<State id="heatSetPoint">
<ValueType>Integer</ValueType>
<TriggerLabel>Heat Setpoint</TriggerLabel>
<ControlPageLabel>Heat Setpoint</ControlPageLabel>
</State>
<State id="coolSetPoint">
<ValueType>Integer</ValueType>
<TriggerLabel>Cool Setpoint</TriggerLabel>
<ControlPageLabel>Cool Setpoint</ControlPageLabel>
</State>
<State id="mode">
<ValueType>
<List>
<Option value="off">Off</Option>
<Option value="heatOn">Heat On</Option>
<Option value="coolOn">Cool On</Option>
<Option value="heatCoolOn">Heat and Cool On</Option>
</List>
</ValueType>
<TriggerLabel>Operation Mode Changed</TriggerLabel>
<TriggerLabelPrefix>Mode Changed to</TriggerLabelPrefix>
<ControlPageLabel>Current Mode</ControlPageLabel>
<ControlPageLabelPrefix>Mode is</ControlPageLabelPrefix>
</State>
</States>
Each State listed in the States element will be available in various parts of the UI, including the “Device State Changed” Event dialog.
The State element has several attributes and elements that should be defined:
| Name | Type | Required | Notes |
|---|---|---|---|
id |
Attribute | Yes | This is a unique identifier for this State within the context of this Device element. |
readOnly |
Attribute | No | If you want this state to be read only, set this attribute to “YES”. For instance, as in this example, the temperature the thermostat is sensing isn’t user settable. (Currently unused but it may be used in the future) |
ValueType |
Element | Yes | This is the type of the state, which must be one of the following: Boolean, Number, List (enumeration constructed just like the <List> element of a <Field> as described above), String, and Separator (used only to visually group your states in the various popups). The <ValueType> element may also have an optional attribute if the value is Boolean//: boolType can be: “TrueFalse” (default), “OnOff”, “YesNo”, and “OneZero”. Example: <ValueType boolType=”OnOff”>Boolean</ValueType>// |
TriggerLabel |
Element | Yes | When selecting “Device State Changed” type trigger events, you can select your device in the resulting list. A popup below the device will list all of your States using this text. FIXME: Add image below |
TriggerLabelPrefix |
Element | No | When defining an enumeration, trigger change label is usually going to need to be a bit different than just the trigger label (“Mode Changed to Heat” vs “Operation Mode Changed” - the former is to test a specific change, the latter will trigger on any change). To accomplish this, you can specify a prefix that’s prepended to the actual state value (with a space in between). See the example above. |
ControlPageLabel |
Element | Yes | In the Control Page Editor, when selecting “Device State” as the display type, then select your device, the next popup will show all your states using this text. FIXME: Add image below |
ControlPageLabelPrefix |
Element | No | When defining an enumeration, the control page label is usually going to need to be a bit different (“Mode is Heat” vs “Current Mode” - the former will show whether a the state is true, the latter shows the state value). You can specify a prefix that’s prepended to the actual state value (with a space in between). See the example above. |
FIXME: we also use this to create an ad-hoc control interface for devices of this type that will show up in the IWS pages as well as in Indigo Touch.
Events.xml
The XML in this file describes all events that your plugin will generate for use in Indigo. Your users will use these in the Trigger Events dialog just like any of the built-in Indigo events (like Power Failure, Email Received, etc). Device State Changed events are handled by the <States> defined in the <Device> elements described above, but your plugin can offer other types of events, including update notifications, battery low notifications, button press notifications, etc.
Here’s a very small Events.xml file that just defines a plugin update event:
<?xml version="1.0"?>
<Events>
<SupportURL>http://www.yourdomain.com/plugin/pluginEvents.html</SupportURL>
<Event eventId="updateAvailable">
<Label>Plugin Update Available</Label>
</Event>
</Events>
As you can see, your <Event> elements can define a <SupportURL> element as well - the trigger events dialog now has a help button on it and if one of your events is selected clicking on the help button will take your user to the specified URL. If you don’t specify one then the default help page for all trigger events will show.
Here’s how to construct your <Event> elements:
| Name | Type | Required | Notes |
|---|---|---|---|
id |
Attribute | Yes | This is a unique id for the event in this Events.xml file. |
Label |
Element | Yes | This is the text that’s shown in the trigger event dialog that represents this event. |
ConfigUI |
Element | No | If your event requires any configuration, you can specify a <ConfigUI> element that’s defined exactly as above. |
FIXME need to add some Python examples of how to notify the server that one of these events has occurred
Actions.xml
Your plugin will also very likely define some actions that a user can take. This is where you define those. Here’s a simple example:
<?xml version="1.0"?>
<Actions>
<SupportURL>http://www.yourdomain.com/plugin/pluginActions.html</SupportURL>
<Action actionId="resetInterface">
<Label>Reset Interface</Label>
</Action>
</Actions>
As with <Event>, your <Action> elements can define a <SupportURL> element as well - the actions dialog now has a help button on it and if one of your actions is selected clicking on the help button will take your user to the specified URL. If you don’t specify one then the default help page for all trigger events will show.
Here’s how you construct your <Action> elements:
| Name | Type | Required | Notes |
|---|---|---|---|
id |
Attribute | Yes | This is a unique id for the event in this Actions.xml file. |
Label |
Element | Yes | This is the text that’s shown in the actions dialog that represents this action. |
CallbackMethod |
Element | Yes | This is the name of the method that implements the action in your code. |
ConfigUI |
Element | No | If your action requires any configuration (as most will), you can specify a <ConfigUI> element that’s defined exactly as above. |
FIXME Python example of a complete action method definition
MenuItems.xml
Your plugin may also define menu items, which will be shown at the bottom of your plugin’s sub-menu on the Plugins menu - they will be the last thing in the menu unless you also include scripts in the Menu Items folder of your plugin’s bundle. If there are scripts there, they will be last, and a separator will be placed between the menu items defined in this XML file and any script files.
Here’s a sample MenuItems.xml file:
<?xml version="1.0"?>
<MenuItems>
<MenuItem id="menu1">
<Title>Reset Interface</Title>
<Action>resetInterface</Action>
</MenuItem>
<MenuItem id="menu2">
<Title>Check for Updates</Title>
<Action>checkForUpdates</Action>
</MenuItem>
</MenuItems>
It’s pretty straight-forward. The MenuItems element will contain multiple <MenuItem> elements. Here’s how to construct a //<MenuItem>//:
| Name | Type | Required | Notes |
|---|---|---|---|
id |
Attribute | Yes | This is a unique id for the menu item in this file. |
Title |
Element | Yes | This is the text that’s shown in your plugin’s sub-menu. |
CallbackMethod |
Element | Yes | This is the name of the method defined in your plugin that will be called when your user selects this menu item. \ \ Your method won’t be able to present any user interface as a result of the menu item being selected except for adding an entry to the event log. However, if the result of the menu execution is that an Event is fired (for instance, the update check above may fire any “update available” events described above that your user has defined) then those events will be executed in Indigo. |
Here's an example of a menu item method definition in your plugin.py file for the "Check for Updates" menu item above:
def checkForUpdates(self):
indigo.server.log(u"checkForUpdates called")
# Do the actual work here to see if there are any updates
plugin.py
Once you have your UI all described, it’s time to write some code. Your plugin.py file is just like any other Python file - it will start with any import statements to include various libraries and it can define global variables. The most important part of the plugin.py file is the definition of your plugin’s main class:
class Plugin(indigo.PluginBase):
This is the class that will define all the entry points into your plugin from the host process and the object bridge between your Python objects and the host process’s C++ objects. Your class must inherit from the indigo.PluginBase class. A quick note here - all bridge objects and communication with the IndigoServer will be done through the indigo module. Because it’s so important, we automatically import it for you so you don’t need an import statement.
There are a few methods that the host process will call at various times during your plugins lifecycle, some required and others are optional:
| General Plugin Methods | ||
|---|---|---|
| Name | Required | Notes |
%%**init**%% |
Yes | This method gets a dictionary of preferences that the server read from disk. You have the opportunity here to use them or alter them if you like although you most likely will just pass them on to the plugin base class: \ \ ```python |
| def init(self, pluginPrefs): | ||
| indigo.PluginBase.init(self, pluginPrefs) | ||
`` You'll most likely use thestartup` method, described below, to do your global plugin initialization. |
||
%%**del**%% |
Yes | This is the destructor for the class. Again, you’ll probably just call the super class’s method: \ \ ```python |
| def del(self): | ||
| indigo.PluginBase.del(self) | ||
| ``` | ||
startup |
No | This method will get called after your plugin has been initialized. This is really the place where you want to make sure that everything your plugin needs to do gets set up correctly. It’s passed no parameters. If you’re storing a config parameter that’s not editable by the user, this is a good place to make sure it’s there and set to the right value. This is not, however, where you want to initialize devices and triggers that your plugin may provide - those are handled after this method completes (see the methods below). \ \ ```python |
| def startup(self): | ||
| indigo.server.log(u"Start called") | ||
| ``` | ||
shutdown |
No | This method will get called when the IndigoServer wants your plugin to exit. If you define a global shutdown variable, this is the place to set it. You’ll use it in the next method. Other things you might do in this method: if your plugin uses a single interface to talk to multiple devices, this is the place where you would want to shut down that interface (close the serial port or network connection, etc). Each device and trigger will already have had a chance to shutdown by the time this method is called (see the methods below). \ \ ```python |
| def shutdown(self): | ||
| # do any cleanup necessary before exiting | ||
`` Note: *shutdown* will be called after *runConcurrently* (discussed next) so cleanup here will be after any changes that might result from a loop in *runConcurrently`*. |
||
runConcurrentThread |
No | This method is called in a newly created thread after the Start method finishes executing. It’s expected that it should run a loop continuously until asked to shutdown. \ \ ```python |
| def runConcurrentThread(self): | ||
| try: | ||
| while True: | ||
| # Do your stuff here | ||
| self.sleep(60) # in seconds | ||
| except self.StopThread: | ||
| # do any cleanup here | ||
| pass | ||
`` You must call *self.sleep()* with the number of seconds to delay between loops. *self.sleep()* will raise an *indigo.StopThread* exception when you should end *runConcurrentThread*. You don’t have to catch that exception if you don’t need to do any cleanup before returning - it will just throw out to the next level. Note: *shutdown* will be called after *runConcurrently`* finishes processing so you can do your cleanup there. |
||
stopConcurrentThread |
No | This method will get called when the IndigoServer wants your plugin to stop any threads that it may have created. The default implementation (below) will set the stopThread instance variable which causes the self.sleep() method to throw the exception that you handle in runConcurrentThread above. In most circumstances, your plugin won't need to implement this method. \ \ ```python |
| def stopConcurrentThread(self): | ||
| self.stopThread = True | ||
| ``` | ||
| Device Specific Methods | ||
| Name | Required | Notes |
*deviceStartComm(self, dev)* |
No | If your plugin defines devices, this is likely the place where you'll want to do the work of starting your device up. For instance, let's say that you have a device somewhere out on the network - the easiest way to "start" your device is to implement this method. You would open the network address:port (that's defined in *dev.pluginProps*), get it's current state(s) and tell the IndigoServer to set those states (using the *dev.updateStateOnServer()* method). |
*deviceStopComm(self, dev)* |
No | This is the complementary method to *deviceStartComm()* - it gets called when the device should no longer be active/enabled. For instance, when the user disables or deletes a device, this method gets called. |
didDeviceCommPropertyChange(self, origDev, newDev) |
No | This method gets called by the default implementation of deviceUpdated() to determine if any of the properties needed for device communication (or any other change requires a device to be stopped and restarted). The default implementation checks for any changes to properties. You can implement your own to provide more granular results. For instance, if your device requires 4 parameters, but only 2 of those parameters requires that you restart the device, then you can check to see if either of those changed. If they didn't then you can just return False and your device won't be restarted (via *deviceStopComm()*/*deviceStartComm()* calls). |
*deviceCreated(self, dev)* |
No | This method will get called whenever a new device defined by your plugin is created. In many circumstances, you won't need to implement this method since the default behavior (which is to call the *deviceStartComm()* method if it's your device and it's enabled) is what you want anyway (see the *deviceStartComm()* method above for details). However, if for some reason you need to know when a device is created, but before your plugin is asked to start communicating with the device, this method can provide that hook. If you implement this method, you'll need to either call *deviceStartComm()* or duplicate the functionality here. \ \ You can also have this method called for devices that don't belong to your plugin. If, for instance, you want to know when all devices are created (and updated/deleted), you can call the *indigo.devices.subscribeToChanges()* method to have the IndigoServer send all device creation/update/deletion notifications. Why? Maybe you're logging all device changes - or maybe you're managing a scene and need to know when the devices in your scene change. Subscribing to changes should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. |
*deviceUpdated(self, origDev, newDev)* |
No | Complementary to the deviceCreated() method described above, but signals device updates. You'll get a copy of the old device object as well as the new device object. The default implementation of this method will do a few things for you: if either the old or new device are devices defined by you, and if the device type changed OR the communication-related properties have changed (as defined by the *didDeviceCommPropertyChange()* method - see above for details) then deviceStopComm() and deviceStartComm() methods will be called as necessary (stop only if the device changed to a type that isn't your device, start only if the device changed to a type that belongs to you, or both if the props/type changed and they both both belong to you). |
*deviceDeleted(self, dev)* |
No | Complementary to the *deviceCreated() method* described above, but signals device deletes. The default implementation just checks to see if the device belongs to your plugin and if so calls the *deviceStopComm()* method. If you implement this method you'll need to call *deviceStopComm()* yourself or duplicate the functionality here. |
| Trigger Specific Methods | ||
| Name | Required | Notes |
*triggerStartProcessing(self, trigger)* |
No | If your plugin defines events, this is likely the place where you'll want to do the work to start watching for those events to occur. For instance, let's say that you have an event for a plugin update, then you'll want to periodically check your site to see if there's a new version available. This is where you'd start that process. |
*triggerStopProcessing(self, trigger)* |
No | This is the complementary method to *triggerStartProcessing()* - it gets called when the event should no longer be active/enabled. For instance, when the user disables or deletes a trigger, this method gets called. |
didTriggerProcessingPropertyChange(self, origTrigger, newTrigger) |
No | Much like it's device counterpart above (*didDeviceCommPropertyChange()*), this method gets called by the default implementation of *triggerUpdated()* to determine if any of the properties needed for recognizing an event have changed. The default implementation checks for any changes to any properties. |
*triggerCreated(self, dev)* |
No | This method will get called whenever a new trigger defined by your plugin is created. In many circumstances, you won't need to implement this method since the default behavior (which is to call the *triggerStartProcessing()* method if it's your trigger and it's enabled) is what you want anyway (see the *triggerStartProcessing()* method above for details). However, if for some reason you need to know when a trigger is created, but before your plugin is asked to start watching for the appropriate conditions, this method can provide that hook. If you implement this method, you'll need to either call *triggerStartProcessing()* or duplicate the functionality here. \ \ You can also have this method called for triggers that don't belong to your plugin. If, for instance, you want to know when all triggers are created (and updated/deleted), you can call the *indigo.triggers.subscribeToChanges()* method to have the IndigoServer send all trigger creation/update/deletion notifications. As with other change subscriptions, this should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. |
*triggerUpdated(self, origDev, newDev)* |
No | Complementary to the triggerCreated() method described above, but signals trigger updates. You'll get a copy of the old trigger object as well as the new trigger object. The default implementation of this method will do a few things for you: if either the old or new trigger are triggers defined by you, and if the trigger type changed OR the communication-related properties have changed (as defined by the *didTriggerProcessingPropertyChange()* method - see above for details) then triggerStopProcessing() and triggerStartProcessing() methods will be called as necessary. |
*triggerDeleted(self, dev)* |
No | Complementary to the *triggerCreated() method* described above, but signals trigger deletes. The default implementation just checks to see if the trigger belongs to your plugin and if so calls the *triggerStopProcessing()* method. If you implement this method you'll need to call *triggerStopProcessing()* yourself or duplicate the functionality here. |
| Schedule Specific Methods | ||
| Name | Required | Notes |
| FIXME add schedule stuff | ||
| Action Group Specific Methods | ||
| Name | Required | Notes |
| FIXME add action group stuff | ||
| Variable Specific Methods | ||
| Name | Required | Notes |
*variableCreated(self, var)* |
No | This method will get called whenever a new variable is created. You can call the *indigo.variables.subscribeToChanges()* method to have the IndigoServer send all variable creation/update/deletion notifications. As with other change subscriptions, this should be used very sparingly since it's a lot of overhead both for your plugin and, more importantly, for the IndigoServer. |
*variableUpdated(self, origVar, newVar)* |
No | Complementary to the variableCreated() method described above, but signals variable updates. You'll get a copy of the old variable object as well as the new variable object. |
*variableDeleted(self, var)* |
No | Complementary to the *variableCreated() method* described above, but signals variable deletes. |
That’s all the methods that will be called automatically by the host process. You may, of course, define many more methods. Some that you will probably want to define: methods to be called when a button is clicked in a <ConfigUI> dialog and methods called by <MenuItems> and <Actions>.
You can also define your own classes, either in plugin.py or more likely in separate files. We believe the plugin host process offers you a great deal of flexibility in how you construct your Python code.
The base plugin also provides some helper methods listed below.
| Method | Parameters | Return Value | Notes | |
|---|---|---|---|---|
*applicationWithBundleIdentifier(self, bundleID)* |
*bundleID* - this is the bundle identifier for the app |
SBApplication instance | Bundle id's are usually fully qualified strings - for iTunes it's com.apple.iTunes. What's returned is a scripting bridge SBApplication instance. See the Scripting Bridge documentation for more information. | |
*browserOpen(self, url)* |
*url* - the url to open in the browser |
None | This method will open the specified in the default browser - obviously it does so on the server machine | |
*debugLog(self, msg)* |
*msg* - the string to insert into the event log |
None | If, at any point in your plugin, you set *self.debug = True*, then any time debugLog is called the string will get inserted into Indigo's event log. If *self.debug = False* (the default) any call to debugLog does nothing. |
|
*errorLog(self, msg)* |
*msg* - the string to insert into the event log |
None | If you want an error to show up in the event log (in red text), use this log method rather than indigo.server.log(). |
FIXME should discuss the plugin flow for several types of plugins: with devices, events, actions - and plugins that use runConcurrentThread and plugins that start up threads, etc., so that it'll be easier to try and envision how to approach thinking about a plugin.
FIXME Discuss the prefs and prefs file (cached and written only periodically or when the plugin quits)
3rd Party Python Libraries
FIXME decide how much to put here
PySerial v2.5 - just "import serial" at the top of your script