Indigo Plugin Developer's Guide v2.0
Indigo Plugins and APIs
Indigo has a long history of extensibility - AppleScript Attachment scripts were available from the start (and deprecated in Indigo 7.4). Later, the Indigo Web Server was added along with the IWS plugin and the ability to add custom images.
With Indigo 5.0, we added a new server plugin API that allows 3rd party developers to more natively add devices, triggers, and actions to Indigo. This server API allows 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). Someone with sufficient skills can implement support for any kind of device and have them integrated into the Indigo UI as first-class citizens.
To deliver this additional functionality, we created the Indigo plugin bundle. Let’s start by first looking at the Application Support folder structure which the Indigo installer creates.
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 7's folder structure looks like this:
in this location:
/Library/Application Support/Perceptive Automation/Indigo 7(.X)
You'll notice these two folders in particular: Plugins
and Plugins (Disabled)
. These two folders are where the 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
We created a macOS Finder bundle type, the Indigo plugin bundle (.indigoPlugin
), which has a very specific structure to encapsulate everything that a plugin needs to perform it's functions:
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 double-click it in the Finder and Indigo will install and enable it for you.
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 macOS 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.
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 since it's 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.2.3</string> <key>ServerApiVersion</key> <string>2.0</string> <key>CFBundleDisplayName</key> <string>Rachio Sprinklers</string> <key>CFBundleIdentifier</key> <string>com.yourorgidentifier.yourpluginidentifier</string> <key>CFBundleVersion</key> <string>1.0.1</string> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>https://somehost.com/path/to/help/</string> </dict> </array> </dict> </plist>
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 and should only contain numerical characters and periods (0-9 and .). For example, “1.0.5.2” is valid, but “1.0.5b2” is not. This is very important as it will help Indigo determine what do to when a user double-clicks a plugin to install it (it won't install old ones over new ones) and will eventually be used in version checking and automatic updates.ServerApiVersion
- this is the version of the server plugin API as defined by us - for Indigo 7.0, it is 2.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 macOS 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 name (
CFBundleName
) - this is another standard macOS key, and it's a name that should be less than 16 characters long and be suitable for displaying in menu items with various strings appended (i.e. “ShortName Device Controls”). If it's not provided we'll use the bundle display name instead. - Bundle identifier (
CFBundleIdentifier
) - this is another standard macOS 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. You should not use thecom.yourorgidentifier.*
namespace. This key is required. - Bundle version (
CFBundleVersion
) - another standard macOS key, and it represents the layout of the bundle. This is controlled by us, and for Indigo 7.0 it will be 1.0.1. 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 link to your plugin's Github repo wiki if there is one, or could 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.
Menu Items Folder
You can drop Python scripts 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.
Resources Folder
This folder is meant to contain any assets (like images, templates, etc.) that your plugin might need.
If there is an icon.png
file in here, it will be displayed in the Plugin Store once you submit
the plugin (see Plugin Store below for more details).
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)
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 that just does some specific things 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.
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 python module called 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.
Plugin Failure Handling
For robustness and performance, plugins are executed inside their own process sandbox by a special Indigo application wrapper called the Indigo Plugin Host (see above). Runtime errors or crashes that occur within a plugin are handled differently based on when and where they occur:
- Plugin Fatal: Errors that occur because of invalid XML or a failure inside the plugin initialization code (init method) are considered fatal. They will log an error and cause the Indigo Server to temporarily suspend the plugin – the plugin will remain enabled but won't be running. Once the errors are corrected, the plugin can be restarted by its Reload menu option. Because the plugin is still in the enabled state, it will also be restarted on the next Indigo Server restart.
- Plugin Auto-Restarted: If a plugin successfully initializes but then experiences a runtime crash (or is terminated via a kill signal not originating from an Indigo Server plugin disable request), then the Indigo Server will log an error and automatically restart the plugin after several seconds. This is only a crash fail-safe – if your plugin has crashes then please fix the underlying problem (or forward the information to us if you think the problem is in the Indigo Plugin Host). If a plugin crashes multiple times over a short period of time, then the Indigo Server may slow or suppress its plugin auto-restart functionality requiring the plugin to be manually restarted via the Reload menu item.
- Concurrent Thread Auto-Restarted: If a plugin defines runConcurrentThread() and an uncaught python exception is thrown within that method, then the Indigo Plugin Host will automatically (after several seconds) create a new thread and again call runConcurrentThread(). An error will also be logged with a call stack trace. In general, plugins should catch and handle common errors (hardware communication problems, out-of-bounds parameters, etc.) themselves inside runConcurrentThread(). The thread restart functionality implemented by the plugin Indigo Plugin Host is a fail-safe in case an error isn't handled, and should only be relied on for unexpected errors (if at all).
- Error Logged: If an uncaught python exception is thrown out of any callback method (deviceStartComm, deviceStopComm, validatePrefsConfigUi, validateDeviceConfigUi, actionControlDimmerRelay, etc.) then an error will be logged with a call stack trace. In general, plugins should catch and handle common errors (hardware communication problems, out-of-bounds parameters, etc.) themselves. Errors logged for uncaught exceptions should be used by developers to implement their own (and more user friendly) error handling.
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). The id
must be alphanumeric (and may contain underscores) and must begin with an alphabetic character.
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 | API Version | Notes |
---|---|---|---|
defaultValue | No | 1.0 | You can enter a default value here. |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
hidden | No | 1.0 | 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 | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. |
readonly | No | 1.0 | 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. |
secure | No | 1.4 | If you specify true for this value then when the user types into the field the actual characters won't show but rather will be replaced with the bullet (•) character. The values typed into these fields are not stored securely - this will solely mask the value in the field from viewing in the UI. |
tooltip | No | 1.0 | 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 | 1.0 | This must be textfield . |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 | API Version | Notes |
---|---|---|---|
defaultValue | No | 1.0 (1.9) | You can enter a default value here. The value must be one of the values from the List element described below. API v1.9: If you specify a default value of “” (empty string) and there is no matching option, then the first menu item will be selected when the dialog is first run. This way you can avoid the user seeing “- no selection -” at the bottom of the menu. |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
hidden | No | 1.0 | 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 | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. |
readonly | No | 1.0 | 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 | 1.0 | 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 | 1.0 | This must be “menu”. |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 | API Version | Notes |
---|---|---|---|
defaultValue | No | 1.0 | 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 | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
hidden | No | 1.0 | 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 | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. It must be alphanumeric (and may contain underscores) and must begin with an alphabetic character. |
readonly | No | 1.0 | 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. |
rows | No | 1.4 | Use this attribute to specify the number of rows that will show in the list. The minimum (and default) is 4. |
tooltip | No | 1.0 | The tooltip will be shown when you hover over the actual list control. |
type | Yes | 1.0 | This must be list . |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 or semicolon 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.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.
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 a list 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
Note: both return tuple values should be strings. Specifically, items with None
as the value (first item) will be skipped. Additionally, the first tuple value (option1 and option2 in the above example) must be a string that does not contain comma or semicolon characters.
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> </List> </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 | API Version | Notes |
---|---|---|---|
id | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. |
type | Yes | 1.0 | This must be “checkbox”. |
defaultValue | No | 1.0 | You can enter a default value here. The value must be either true or false . |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
hidden | No | 1.0 | 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. |
readonly | No | 1.0 | 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 | 1.0 | The tooltip will be shown when you hover over the actual list control. |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 | API Version | Notes |
---|---|---|---|
alignText | No | 1.4 | This will control how the label text is justified. Valid options are: “left” (default), “center”, and “right”. |
alignWithControl | No | 1.4 | If set to “true”, this will align the left margin of the label with the left margin for the actual controls (not their labels). Anything other than “true” will be considered “false” (which is also the default if not specified). This is very useful in conjunction with specifying font characteristics described below when you want to add some help specific to a control - just insert the label right below the control field and it will align with the actual control above. |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
fontColor | No | 1.4 | Use this attribute to control the color of the label text. Valid options are: “black (default), “darkgray”, “red”, “orange”, “green”, and “blue”. |
fontSize | No | 1.4 | Use this attribute to specify the size of the font. Valid options are: “regular” (default, same size as all other text in the dialog), “small, which is a point or two smaller, and “mini which is yet another point size or two smaller. |
id | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. |
type | Yes | 1.0 | This must be label . |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 | API Version | Notes |
---|---|---|---|
id | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. |
type | Yes | 1.0 | This must be separator . |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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 | API Version | Notes |
---|---|---|---|
id | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. |
type | Yes | 1.0 | This must be button . |
enabledBindingId | No | 1.0 (1.9) | 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. API v1.9: You can also bind to a list field and this field will only be enabled if something is selected in the list. |
enabledBindingNegate | No | 1.9 | Set this to “true” to negate the binding. So if the target binding field's value is true then the binding will be false. |
visibleBindingId | No | 1.0 | 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 | 1.0 | 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. Note: the string comparison is a contains - so if any option contains the specified string it will match. This offers some extra flexibility in defining dependency groups. |
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, deviceId) | typeId - action type specified in the type attribute deviceId - the unique device ID for the device the user selected for the action if you specify a deviceFilter 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. API v1.4+: You can also add a special dictionary entry to your error message dictionary that will cause a sheet to drop down with the specified text. This will help the user identify what went wrong in the validation method. Just add a string with the key “showAlertText” to your error dictionary before return from your dialog.
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" errorDict["showAlertText"] = "Some very descriptive message to your user that will help them solve the validation problem." 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 7.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 , thermostat , sensor , speedcontrol , 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 used to describe the possible states for the device. For custom devices it is all available states. For subclassed devices it will describe any additional states for the device. See the description below for more details. |
Check out the SDK for example plugins of each type of device listed above. The examples show config dialogs, required method stubs, etc., to implement the respective device types.
Custom Device Type
The custom
device type is special because it’s completely custom. Therefore, you must specify everything about the device - it’s states and their types (Number
, 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>Number</ValueType> <TriggerLabel>Temperature</TriggerLabel> <ControlPageLabel>Temperature</ControlPageLabel> </State> <State id="heatSetPoint"> <ValueType>Number</ValueType> <TriggerLabel>Heat Setpoint</TriggerLabel> <ControlPageLabel>Heat Setpoint</ControlPageLabel> </State> <State id="coolSetPoint"> <ValueType>Number</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. |
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. |
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. |
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 actions 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. |
uiPath | Attribute | No | Added in API v1.4. You can specify where in the menu hierarchy your action will be placed. By default, Indigo will create a submenu at the bottom of the Actions list and put your actions there. If you specify “DeviceActions”, Indigo will create a submenu on the Device Actions menu and put your actions there. If you specify “NotificationActions”, Indigo will insert your action n the Notification Actions menu without a submenu (be sure to name your action appropriately so that it's clear what it does). Added in API v2.0: use “hidden” to hide the action in the UI. Useful for actions that are only intended to be used from scripts/plugins (an API of sorts). |
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.substitute(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)
Notice the call to self.substitute()
- this method is defined in the plugin base class. If your user inserts
%%v:12345%%
into their string where 12345
is the ID of a variable, the call will return a string with all variable occurrences substituted. If your user inserts
%%d:12345:someStateId%%
into their string where 12345
is the ID of a device and someStateId
is a valid state identifier, the call will return a string with all device state occurrences substituted.
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 plugin 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> <!-- Added in v1.2 --> <MenuItem id="menu4"> <Name>Execute Some Action</Name> <ConfigUI actionId="someAction"/> </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 (1.1) | 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 (1.2) | If you want a menu to open an associated dialog, you can include a standard ConfigUI element (see Configuration Dialogs above for details). API v1.2: You may also specify an actionId attribute on this element with the id of one of your actions and that action UI will be executed rather than having to specify the UI itself. If it requires a device ID that will be added at the top of the dialog. Ex: <ConfigUI actionId=“actionIdHere”/> |
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)
In API v1.x
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.
Note: some of these methods may require you to subscribe to object changes - specifically, if they're objects that your plugin didn't directly create (devices of other types) or other object types (triggers, schedules, variables).
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 startup(self): indigo.server.log(u"Startup 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 startup() 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) | Deprecated in 1.4 | This method was deprecated in v1.4 of the API. It will continue to be called internally, but you'll find the device passed to it not fully created in most circumstances. Use deviceStartComm() instead. |
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, trigger) | 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, origTrigger, newTrigger) | 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, trigger) | 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 documentation when it is complete (not currently available) | ||
Action Group Specific Methods | ||
Name | Required | Notes |
add action group documentation when it is complete (not currently available) | ||
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 | (DEPRECATED - see Logging below) 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 | (DEPRECATED - see Logging below) 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. |
substituteDeviceState(self, inString, validateOnly=False) | inString - the string which contains valid device ID and state key 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: %%d:DEVICEID:STATEKEY%% DEVICEID is the unique device ID as found in the UI in various places and the STATEKEY is the identifier for the state. 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 device 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 device doesn't exist (or if there's a formatting problem) at action execution time. |
substitute(self, inString, validateOnly=False) | inString and validateOnly as described in the previous two methods substituteVariable() , substituteDeviceState() | See above | Validation works the same and should be called when your dialog validates user input. This method calls substituteVariable() first followed by substituteDeviceState() . The ordering was carefully chosen such that the variable substitution could, in fact, add more device markup to the string before the device substitution happens. So the user can even more dynamically generate content by inserting device markup into a variable value. However, only device markup will be honored in variable values - we don't recursively call variable markup on variable values. |
Logging
In previous versions of the API, the plugin base class defined three methods: debugLog()
, errorLog()
, and exceptionLog()
. These were shortcuts which showed varying levels of log information in the Indigo Event Log window. In API 2.0, we've deprecated those methods in favor of the standard Python logging module (don't worry though, the previous APIs will continue to work).
The plugin base now has a couple of new attributes related to logging:
Attribute | Description |
logger | An instance of the standard Python logger class. You should use this instance to log messages. See the examples below for details. |
indigo_log_handler | An instance of a special Python Handler class that emits messages to the Indigo Event Log window and the Indigo Server's log file. |
plugin_file_handler | An instance of a Python TimedRotatingFileHandler which writes you own log files in the /Logs/ directory. A subdirectory will be created using your plugin's ID as the name. Inside that directory will be daily log files (with 20 days of backups) of all log messages from your plugin. See below for examples of the default format of that file. |
The Python logger module is extremely powerful and flexible, and we recommend reading through the docs for a good understanding of how it works and how you can add great logging to your plugin. We'll describe the minimum here that you need to know to do basic logging to the Indigo Event Log window and to your plugin's log file.
The logger module defines 5 levels of logging: logging.DEBUG
, logging.INFO
, logging.WARNING
, logging.ERROR
, logging.CRITICAL
. Instances of the Logger object can be set to log messages at any of those 5 levels, and the level logged can be changed at any time. By default, the self.logger
instance is set to logging.DEBUG
(we'll see why a bit further down). You can change this if you want using the setLevel()
method. We've named the self.logger
instance “Plugin”, because it's the name of the class from which your plugin begins. We'll see why this is important later in the examples.
You can very easily write log messages at any level using the following convenience methods defined in the Logger class:
self.logger.debug(u"Debug log message") self.logger.info(u"Info log message") self.logger.warn(u"Warning log message") self.logger.error(u"Error log message") self.logger.critical(u"Critical log message")
The debugLog()
, errorLog()
, and exceptionLog()
methods are now just wrappers around the corresponding method above. There is another convenience method defined in the logging module: self.logger.exception(“Error log message with exception appended”)
. If you call this method from within an except
block in your plugin, it will automatically create a logging.ERROR
level message and append the stack trace to whatever message you supply.
A Logger instance can have any number of Handler objects associated with it. These handler objects are what actually do the heavy lifting in terms of where log messages go and how those messages are formatted. The plugin base class provides two of them: self.indigo_log_handler
and self.plugin_file_handler
, both of which are automatically added to self.logger
. Therefore, calling any of the 5 methods above (self.logger.debug
, self.logger.info
, etc.) will automatically route those messages to both the Indigo Server and the plugin file handler.
self.indigo_log_handler
The self.indigo_log_handler
is an instance of a custom Handler object that will write (or emit in Python Handler speak) your log messages into the Indigo Event Log. We haven't yet updated the Indigo event handling to deal with 5 levels of log messages, so each the message type (the left part in the Event Log) is modified so that it reflects the level. Here's an example of each:
Your Plugin Debug Debug log message Your Plugin Info log message Your Plugin Warning Warning log message Your Plugin Error Error log message Your Plugin Critical Error Critical log message
The last two, being errors, are in red in the actual Event Log window. In a future release, we hope to change the server's internal logging to use these same 5 log levels. When we do so, you won't have to do anything - we'll just update the self.indigo_log_handler
instance to emit the appropriate messages to the server.
self.plugin_file_handler
The self.plugin_file_handler
is an instance of a TimedRotatingFileHandler. This handler is used to write log files with some automatic file management: it can rotate the log files (so they don't get too big), and can be configured to keep some number of backups. By default, we've configured it to rotate the log files at midnight each night and to keep 20 backups. You can, of course, change those settings on the handler.
Handler objects have a Formatter object set, which is how the log line is formatted. By default, we format the log lines with the date/time stamp, the level (e.g. DEBUG), [the logger name (“Plugin” by default)].[method name]:, message. Each element is separated by a tab. So, for example, the lines from the methods above will result in these lines in your log file:
2016-02-10 15:27:18.194 DEBUG Plugin.runConcurrentThread: Debug logging 2016-02-10 15:27:18.194 INFO Plugin.runConcurrentThread: Info logging 2016-02-10 15:27:18.195 WARNING Plugin.runConcurrentThread: Warning logging 2016-02-10 15:27:18.195 ERROR Plugin.runConcurrentThread: Error logging 2016-02-10 15:27:18.195 CRITICAL Plugin.runConcurrentThread: Critical logging
So, date time, level, Plugin.method (in this case we're writing from the runConcurrentThread method): message. However, if that's not what you want, you can create your own Formatter instance and set the handler to use that instead.
Log Levels
Earlier, we mentioned that we set the level of the logger to logger.DEBUG
. Does this mean that all debug or better messages are automatically sent to both the file and the Indigo Event Log? Actually, no. The reason is because you can also specify at the handler what level to actually log. We default the plugin_file_handler to logger.DEBUG
but we default the indigo_log_handler to logger.INFO
. The reasoning is that you want the file to have all debugging information, but you are likely to need less logging to the Event Log by default. Again, because you have access to those handlers, you can use their setLevel()
methods to change them as well, and if you've implemented user selectable debug levels, this should fall right in line with what you're expecting.
In fact, we've done a little trickery for you so that the old self.debug
attribute will continue to behave as you might expect. If you have self.debug
set to True
, we set the indigo_log_handler
to logger.DEBUG
. And if it's False, we set it to logger.INFO
. We also continue to maintain the self.debug
attribute so your legacy code will continue to work, though that is deprecated as well.
Logging from submodules
Add discussion about how to log from submodules and give an example.
Event and Message Flow
Add discussion about plugin event and message 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 envision how to approach thinking about a plugin.
Plugin Preferences File
Add discussion about the preferences file and when it is cached and written (both periodically and when the plugin quits).
3rd Party Python Libraries
Indigo includes a variety of popular 3rd party Python libraries:
enum - Python 3.4's enum (enumeration) type backported to Python 2.7.
oauthlib v2.0.2 - A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for Python 2.7+.
pyaes v1.2.0 - Pure-Python Implementation of the AES block-cipher and common modes of operation/
py-applescript v1.0.1 - The py-applescript library enables Python to execute AppleScripts, pass data back and forth, etc. See our AppleScript Integration Strategies wiki article on how to use py-applescript.
pyserial v3.1.1 - 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.
requests v2.10.0 - Python HTTP for Humans.
requests-oauthlib v 0.8.0 - Provides first-class OAuth library support for the requests library above.
xmljson v0.1.7 - Converts XML into JSON/Python dicts/arrays and vice-versa.