Indigo Plugin Developer's Guide v1.0
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.
With Indigo 5.0, we wanted to add a new server plugin API that would allow 3rd party developers to more natively add devices, 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.
To deliver this additional functionality, we've introduced a new concept: the Indigo plugin bundle. First, let’s look at the new Application Support folder structure.
A note about version numbers: As we've revised the API and added features, we've incremented the API version number. Up until Indigo 5 v5.1.1, the API remained 1.0 and the documents were just updated with new features (which could lead to incompatible features if using an older release of Indigo). As of Indigo 5 v 5.1.2, we've begun incrementing the API minor version number (1.X) whenever we add new features. The major number (X.0) will only be incremented when we do something that will break backwards compatibility. See the API version chart to see which API versions were released in which Indigo version.
If any of the API tables in the documentation don't have a version number you can assume that the feature is available in API version 1.0 and later.
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:
in this location:
/Library/Application Support/Perceptive Automation/Indigo 5
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.
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.
So, why did we go to the trouble of using the bundle format when there's just a file and a couple of folders? Because in future versions, we're going to add capabilities to the plugin bundle (AppleScript Attachments, AppleScript background scripts, shared images, IWS plugins, etc). We didn't quite get to them for 5.0, but expect them to follow very soon after the 5.0 release.
The Info.plist File
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>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.- 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. Your plugin will have a menu item called “About [PLUGIN NAME]” - when the user selects this menu item, the default 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. Use the IndigoPluginTester Mac application to do error checking on your Info.plist and bundle construction as well as other formatting checks.
Menu Items Folder
You can drop Python scripts and AppleScripts into this folder and they will show up in your plugin's sub-menu on the new “Plugins” menu. When the user selects the menu item, the script is executed. This is a really simple way of giving your plugin some visible UI. In Indigo 4, if you had loose scripts in the “Scripts” folder, they showed up under the “Scripts” menu item in the Indigo Mac Client. This is no longer the case - if you want scripts to show up in a menu somewhere, you'll need to create a plugin bundle with the scripts you want in the “Menu Items” folder. You don't need to implement a Server Plugin to do this - only have a valid Info.plist file and a Menu Items folder with your scripts.
Indigo Server Plugins
The most comprehensive way to extend Indigo is by implementing a 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.6 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 of 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 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.
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”)
To have the server speak a text message using your Mac's built-in speech synthesizer:
indigo.server.speak("Message to speak")
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 capital 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.
Important: Most of the major elements will have an id
of some kind. It's extremely important that you follow these rules when creating the id
:
id
's can contain letters, numbers, and other ASCII charactersid
's cannot start with a number or punctuation characterid
's cannot start with the letters xml (or XML, or Xml, etc)id
's cannot contain spaces
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="false"> <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="false"> <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="false"> <Label>Node ID:</Label> </Field> <Field id="simpleSeparator1" type="separator"/> <Field type="checkbox" id="manualEntry" defaultValue="false"> <Label>Manually Enter Node ID:</Label> <Description>(Not Recommended)</Description> </Field> <Field id="manualNodeID" type="textfield" visibleBindingId="manualEntry" visibleBindingValue="true"> <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. |
readonly | No | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user - in the example above clicking the Find Node ID button will populate the ID field. |
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 false will mean the text field is visible only when the checkbox is unchecked, and a true 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. |
readonly | No | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. |
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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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.
Like button fields, you can specify a CallbackMethod
element in your field definition:
<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> <CallbackMethod>menuChanged</CallbackMethod> </Field>
This method will be called every time the user changes the menu. The method in your plugin.py file should look something like this:
def menuChanged(self, valuesDict, typeId, devId): # do whatever you need to here # typeId is the device type specified in the Devices.xml # devId is the device ID - 0 if it's a new device return valuesDict
depending on which object type (plugin config, device, trigger, action, etc.) the dialog is being called for. This will allow you to adjust other fields in the valuesDict based on what's selected in the menu field. For instance, if you've selected a device type, you may decide to alter some hidden fields so that other fields are shown/and hidden. Used in combination with Dynamic Lists, you may also repopulate other lists with more appropriate values.
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

<Field type="list" id="listSample" defaultValue="item1,item3"> <Label>List:</Label> <List> <Option value="item1">Item 1</Option> <Option value="item2">Item 2</Option> <Option value="item3">Item 3</Option> <Option value="item4">Item 4</Option> </List> </Field>
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. |
readonly | No | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. |
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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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 section below for details.
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:
<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.zwave | include Z-Wave devices - this is an interface filter that can be used with other filters |
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) |
self | all device types defined by the calling plugin |
self.devTypeId | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified by the calling plugin |
com.somePlugin | all device types defined by some other plugin |
com.somePlugin.devTypeId | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified in some other plugin |
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.varValueChange | 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.
Another special built-in list is serial ports - use indigo.serialPorts
to get a list of available serial ports, and what's returned to your plugin is the full path to the plugin (e.g. “/dev/tty*”). This is suitable for using with PySerial, which we include with Indigo. See the “serialport” field type below for an even better way of collecting serial communication information.
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="self" 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="", valuesDict=None, typeId="", targetId=0) # 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. All parameters are read-only. myArray = [(“option1”, ”First Option”),(“option2”,”Second Option”)] return myArray
The valuesDict
parameter will contain the valuesDict for the object being edited - if it's a device config UI then it'll be the valuesDict from that device (just like what's passed in to validation methods), etc. Note: if it's a new object that hasn't been saved yet, valuesDict may be None or empty so test accordingly.
The typeId
parameter will contain the type - for instance, if it's an event, it will be the event type id. The targetId
is the ID of the object being edited. It will be 0 if it's a new object that hasn't been saved yet.
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.
One further option to dynamic lists is the ability to have them reload whenever the config dialog returns from a button or menu callback. So, for instance, if you have a button that somehow changes the contents of the dynamic list, you can specify the list as a one that's dynamically reloaded:
<Field id="insteonDimmers" type="menu"> <Label>INSTEON Dimmers:</Label> <List class="self" filter="stuff" method="myListGenerator" dynamicReload="true"/> </Field>
Note: this will cause an extra round-trip (client→Indigo server→plugin→Indigo server→client) so you should only use this when the list can change while the dialog is actually running.
Serial Port

<Field type="serialport" id="devicePortFieldId" />
PySerial, the Python-based serial communication library that we ship with Indigo, supports not only doing traditional serial communications via physical serial connections, but also by doing serial communications over the network using serial over sockets or RFC 2217 (a standard for serial communication over the network). If the device you're connecting to supports one of these methods of connection then you can use the serialport
field type. This allows you to specify a single field type that, when rendered in the UI, will expand to multiple fields that will allow the user to select Local (physical)
, Network Socket
, or Network RFC-2217
. Then, based on which option they select, we'll also present the other controls needed to completely select the connection method: a serial port list for the first or the hostname/ip address and port for the last two. Make sure you leave “socket:” or “rfc2217:” as the beginning of the address field for the last two.
This field type generates multiple controls in your dialog automatically. We've also included a helper method, validateSerialPortUi(valuesDict, errorsDict, u“devicePortFieldId”)
that will help you validate that the serialport control was used properly by the user. See the Validating Serialport Fields section for more information.
Checkbox

<Field type="checkbox" id="checkboxSample" defaultValue="true"> <Label>Checkbox:</Label> <Description>What's on the right side of the checkbox</Description> </Field>
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 “checkbox”. |
defaultValue | No | You can enter a default value here. The value must be either true or false . |
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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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. |
readonly | No | This attribute will make the field readonly - useful to show the user data that changes in some other way rather than being manipulated directly by the user. |
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.
Like button fields, you can specify a CallbackMethod
element in your field definition:
<Field type="checkbox" id="myCheckbox"> <Label>Here's a checkbox:</Label> <CallbackMethod>checkboxChanged</CallbackMethod> </Field>
This method will be called every time the user clicks the checkbox. The method in your plugin.py file should look something like this:
def checkboxChanged(self, valuesDict, typeId, devId): # do whatever you need to here # typeId is the device type specified in the Devices.xml # devId is the device ID - 0 if it's a new device return valuesDict
Label

<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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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

<Field id="exampleButton" type="button" tooltip="Click this button to do something cool" visibleBindingId="checkboxSample" visibleBindingValue="1"> <Label>Visible button's label:</Label> <Title>Visible Button Title</Title> <CallbackMethod>ConfigButtonPressed</CallbackMethod> </Field>
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 false will mean the menu field is visible only when the checkbox is unchecked, and a true 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:
- The user clicks on the button.
- A dictionary containing all the values of the fields in the dialog are sent to the method that you identify in the
<CallbackMethod>
element. - Your plugin can then do whatever it needs to and it returns a the dictionary back to the dialog containing any field changes.
- 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:
<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 # typeId is the device type specified in the Devices.xml # devId is the device ID - 0 if it's a new device 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, valuesDict, typeId, actionId) | 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, typeId, eventId, valuesDict): # Do your validation logic here return True
If you need to adjust the values, return the valuesDict with changes:
def validateEventConfigUi(self, typeId, eventId, valuesDict): # 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, typeId, eventId, valuesDict): # 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 (False, valuesDict, errorDict)
If you don't define these methods, then the default behavior is to have the validation calls always return True
.
Validating serialport Fields
Because serialport fields are a bit special (they generate multiple visible fields), we've provided a helper method that you can use to make sure the user used the control correctly:
def validateDeviceConfigUi(self, valuesDict, typeId, devId): errorsDict = indigo.Dict() self.validateSerialPortUi(valuesDict, errorsDict, u"devicePortFieldId") # Put other config UI validation here -- add errors to errorDict. if len(errorsDict) > 0: # Some UI fields are not valid, return corrected fields and error messages (client # will not let the dialog window close). return (False, valuesDict, errorsDict) # User choices look good, so return True (client will then close the dialog window). return (True, valuesDict)
Notice that at the top of the validation method after we define the errorsDict, we call self.validateSerialPortUi()
. This method will look for the fields that make up the specified field (“devicePortFieldId” in this case). It will make sure that a valid serial port is selected if it's a physical connection or that the address field is formatted properly if it's one of the network connections. If not, it will automatically add the appropriate errors to the errorsDict and will attempt to correct any URL prefix errors (“socket://” or “rfc2217://”). You can then continue to validate the rest of the fields in your dialog and use the length check on errorsDict to see if there were any errors discovered. If so, return False along with both dicts. If it's empty, just return true with the valuesDict.
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. NOTE: the parameters in this instance are all read-only. If you need to modify the valuesDict, for instance, you need to do it in the validate UI (which is fine since a cancel should never change anything).
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. |
id | Attribute | Yes | This is your plugins unique identifier for this device type. It must be unique in this plugin. |
Name | 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.
: 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" id="appliance"> <Name>Appliance Module</Name> <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 <Name>
element (see above) is what’s used for the menu items.
: 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 id 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"> <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. IDs must follow the XML standard in terms of construction with one addition: they may not include periods ('.') - periods are reserved for internal use only. |
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. : 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. : 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. |
The ValueType element is particularly important - it controls what types of controls are presented to the user in the UI: an integer tells it to show greater than, less than, equal to, not equal to, etc and a way to enter a number (in the case of a trigger).
See the States section of the Devices Class page for details on how to set states once the IndigoServer understands the structure of your device's states.
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 id="updateAvailable"> <Name>Plugin Update Available</Name> </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. You can specify an Action to be a separator in your action list so that when they're displayed in the UI there is a visual separation. Simply insert an Event defined like this between two other Event elements:
<Event id="sep1"/>
While you don't have to include any elements, the id still must be unique.
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. |
Name | 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. |
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 id="resetInterface"> <Name>Reset Interface</Name> <CallbackMethod>resetInterface</CallbackMethod> </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. You can specify an Action to be a separator in your action list so that when they're displayed in the UI there is a visual separation. Simply insert an Action defined like this between two other Action elements:
<Action id="sep1"/>
While you don't have to include any elements, the id still must be unique.
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. |
deviceFilter | Attribute | No | If present a popup list of devices that match the specified device filter will be shown in the UI. Many actions may not need any configuration beyond a device selection so we've enabled you to specify that a device must be selected and passed to the action's CallbackMethod . This will avoid the necessity to create a ConfigUI element with just a device popup. |
Name | 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. |
Here is the code that implements the Write to Log action defined in the Action Collection plugin:
def writeToLog(self, action): # call for variable substitution on the message field they entered theMessage = self.substituteVariable(action.props.get("message", "")) # set the type for the message if they configured one theType = action.props.get("type") # debugging - show the message if debugging is enabled self.debugLog(u"Write to log: " + theMessage) # if they entered a type, log the message with it, otherwise log the message without a type if theType: indigo.server.log(theMessage,type=theType) else: indigo.server.log(theMessage)
Note: To increase the value of your plugin, you should ensure that you test and document the necessary information so that Python scripters can script your plugin's actions - for instance, a scripter can tell the iTunes to pause playback. You can provide that information in a relatively straight-forward way in your plugin's documentation as we've done with our plugins (check the Airfoil and iTunes docs for examples). You shouldn't really have to do much - but you should test each action. Sometimes you can make assumptions about the data that you get from your action's ConfigUI that you may want to change - for instance you may expect data when it would be advantageous to not include it (optional data) from a scripters perspective.
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"> <Name>Reset Interface</Name> <CallbackMethod>resetInterface</CallbackMethod> </MenuItem> <MenuItem id="menu2"> <Name>Check for Updates</Name> <CallbackMethod>checkForUpdates</CallbackMethod> </MenuItem> <MenuItem id="menu3"> <Name>Turn Off Light...</Name> <CallbackMethod>turnOffLight</CallbackMethod> <ButtonTitle>Turn Off</ButtonTitle> <ConfigUI> <Field id="targetDevice" type="menu"> <Label>Light:</Label> <List class="self" filter="self.myLightType" method="getDynamicList" /> </Field> </ConfigUI> </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 | API Version | Notes |
---|---|---|---|---|
id | Attribute | Yes | 1.0 | This is a unique id for the menu item in this file. |
Name | Element | Yes | 1.0 | This is the text that’s shown in your plugin’s sub-menu. |
CallbackMethod | Element | No | 1.0 | This is the name of the method defined in your plugin that will be called when your user selects this menu item. API v1.1: If you specify a <ConfigUI> for the menu, then this property is optional. If it's present, then the the method will be called with arguments much like a validation (see example below). If it's not present (in which case a ConfigUI must be present), then the dialog will contain only a single “Close” button. In that case the functionality in the dialog will be completely left up to buttons contained within the dialog. |
ConfigUI | Element | No | 1.1 | If you want a menu to open an associated dialog, you can include a standard ConfigUI element (see Configuration Dialogs above for details). |
ButtonTitle | Element | No | 1.1 | If you have a ConfigUI element and a CallbackMethod, you can add this element to specify the title used on the button that executes the dialog. By default it's “Execute”. |
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
As of API v1.1 you can have your menu item open a dialog which will allow you to gather input from a user before executing the action. Just add a <ConfigUI> element to your MenuItem definition and define that dialog just like any other. There are a couple of differences between this dialog and other dialogs. First, the CallbackMethod for menu items will be used rather than a validation method. It will, however, operate in much the same way. If you specify a CallbackMethod then it will get called with the valuesDict and the menu item's ID. You can return True, which will cause the dialog to close, or return false with an error dictionary which will mark the invalid fields just like the validation method would.
You also have the option of adding a ButtonTitle element - which will allow you to change the name of the button (it defaults to “Execute”).
If you don't specify a CallbackMethod then the dialog will not have Execute and Cancel buttons but rather will have just a single “Close” button. Why? We wanted to allow you the flexibility of making a dialog perform multiple actions if you like using button field types within the dialog itself. When doing that it would be awkward to have an “Execute” button that didn't really do anything.
Here's an example of a menu item method definition in your plugin.py file for the “Turn Off Light” menu item above:
def turnOffLight(self, valuesDict, typeId): indigo.server.log("Turning off light: %s" % valuesDict["targetDevice"]) # perform the action here. If there are errors in any of the fields then # you can return (False, valuesDict, errorsDict) just like a validation # method and the dialog won't close and will show the errors. If you # return True then the dialog will close when the method completes. errorsDict = indigo.Dict() return (True, valuesDict, errorsDict)
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 | ||
---|---|---|
Method definition | Required | Notes |
__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs) | 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: def __init__(self, pluginPrefs): indigo.PluginBase.__init__(self, pluginPrefs)
You'll most likely use the |
__del__(self) | Yes | This is the destructor for the class. Again, you’ll probably just call the super class’s method: def __del__(self): indigo.PluginBase.__del__(self) |
startup(self) | 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). def start(self): indigo.server.log(u"Start called") |
shutdown(self) | 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. 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). def shutdown(self): # do any cleanup necessary before exiting
Note: |
runConcurrentThread(self) | 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. 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 |
stopConcurrentThread(self) | 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. def stopConcurrentThread(self): self.stopThread = True |
prepareToSleep(self) | No | The default implementation of this method will call deviceStopComm() for each device instance and triggerStopProcessing() for each trigger instance provided by your plugin. You can of course override them to do anything you like. |
wakeUp(self) | No | The default implementation of this method will call deviceStartComm() for each device instance and triggerStartProcessing() for each trigger instance provided by your plugin. You can of course override them to do anything you like. |
Device Specific Methods | ||
Method definition | Required | Notes |
getDeviceStateList(self, dev) | No | If your plugin defines custom devices, this method will be called by the server when it tries to build the state list for your device. The default implementation just returns the <States> element (reformatted as an indigo.List() that's available to your plugin via devicesTypeDict[“yourCustomTypeIdHere”]) in your Devices.xml file. You can, however, implement the method yourself to return a custom set of states. For instance, you may want to allow the user to create custom labels for the various inputs on your device rather than use generic “Input 1”, “Input 2”, etc., labels. Check out the EasyDAQ plugin for an example. |
getDeviceDisplayStateId(self, dev) | No | If your plugin defines custom devices, this method will be called by the server to determine which device state ID to display in the device list UI state column. The default implementation just returns the <UiDisplayStateId> element in your Devices.xml file. You can, however, implement the method the plugin needs to dynamically determine the which state ID to display. |
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 addrss: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 | ||
Method definition | 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. When conditions are met in your plugin for a trigger to be executed, you would call indigo.trigger.execute(triggerReference) to tell the Server to execute the trigger (and it's conditions). |
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 | ||
Method definition | Required | Notes |
add schedule stuff | ||
Action Group Specific Methods | ||
Name | Required | Notes |
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 definition | 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. Note it does so on the server machine and not on any remotely connected clients. |
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() . |
openSerial(self, ownerName, portUrl, baudrate, bytesize, parity, stopbits, timeout, xonxoff, rtscts, writeTimeout, dsrdtr, interCharTimeout) | ownerName - the name of the device or plugin that owns this serial port (used for error logging) - make sure that it's ASCII text with no unicode characters other args - all other arguments are passed directly to the pySerial's Serial contructor | serial.Serial instance | This method is identical to creating a new pySerial Serial object except that it never throws an exception. If the serial connection cannot be openned then None is returned and an error will be automatically logged to the Indigo Server event log. |
sleep(self, seconds) | seconds - the sleep duration as a real number | None | This method should be called from within your plugin's runConcurrentThread() defined method, if it is defined. It will automatically raise the StopThread exception when the Indigo Server is trying to shutdown or restart the plugin. See runConcurrentThread documentation above for more details. |
substituteVariable(self, inString, validateOnly=False) | inString - the string which contains valid variable ID validateOnly - whether to return the actual string or a validation tuple where the first item is a boolean whether the formatting is valid and the variable ID exists and the second item is the error string to show (defaults to False) | String if validateOnly is False (BOOL, errStr) if validateOnly is True | This method will allow any string with the following markup to have a variable value substituted: %%v:VARID%% VARID is the unique variable ID as found in the UI in various places. It's recommended that you call this method twice: first, when the validation method for your action is called so that you can validate at that point if the syntax is correct and make sure the variable exists. The second time you call this method would be when you execute the action - call it before you actually use the string so that the substitutions will be made. Errors will show up in the event log if the variable doesn't exist (or if there's a formatting problem) at action execution time. |
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.
Discuss the prefs and prefs file (cached and written only periodically or when the plugin quits)
3rd Party Python Libraries
PySerial v2.5 - just “import serial” at the top of your script. PySerial allows your plugins/scripts to access any serial ports on your Mac. The EasyDAQ plugin uses PySerial for it's serial communication so you may find it useful to look at the code for that plugin for examples.
py-appscript v1.0.0 - “import appscript” at the top of your script. Appscript allows you to use apple events to control other applications. Apple events are what AppleScript uses - py-appscript uses the same underlying events. The syntax is obviously Python but it requires a little getting used to. The Airfoil plugin uses py-appscript so you may find it useful to look at the code for that plugin for examples.
simplejson v2.2.1 - simplejson is included in Python v2.6 and higher - but since we're using 2.5 we've included it in the install. Just “import simplejson”.