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 (IWS) 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: We increment the API version number when the API is revised. The major number (X.0) is incremented when we do something that will break backwards compatibility. The minor number (1.X) is incremented when we add new features. 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's folder structure looks like this:
in this location:
/Library/Application Support/Perceptive Automation/Indigo [VERSION])
]
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
(Plugin version) - 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. If the version number is a higher version, Indigo will notify the user that the plugin will be installed and enabled. If the version number is lower than an already-installed version, Indigo will prompt the user to confirm that they want to downgrade the plugin. The version number is also used in version checking, and may eventually be used for automatic updates.ServerApiVersion
(Server API version) - this is the version of the server plugin API as defined by us. 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.CFBundleDisplayName
(Bundle display name) - 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.CFBundleName
(Bundle name) - 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.CFBundleIdentifier
(Bundle identifier) - 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.CFBundleVersion
(Bundle version) - another standard macOS key, and it represents the layout of the bundle. This is controlled by us. This key is required.CFBundleURLTypes
(URL types) - 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). Note that the code that loads config UI XML templates assumes that the Server Plugin
folder is the root, so those template files should always be in the Server Plugin
tree (i.e., Server Plugin/Templates/my_template.xml
).
If this folder contains any of the following sub-folders, that content will be made available directly from the Indigo Web Server:
Folder | Authentication | Description |
---|---|---|
images | IWS Configured | Any images in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). |
public | None (open to all) | Any files in this folder or any sub-folders will be delivered to anyone without any authentication. |
static | IWS Configured | Any files in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). |
videos | IWS Configured | Any files in this folder or any sub-folders will be delivered if the user is authenticated via whatever authentication methods are enabled for IWS (digest, basic, api key). |
The URL for those files will be constructed using the plugin's id followed by the path. Here are some examples:
Packages Folder
This folder is optional, but it's useful when your users will need to install additional Python packages in order to use your plugin. Any library installs should be directed to the ../Contents/Packages/
folder. Note that this may be required in future versions of Indigo. See the Python Packages and Indigo page for more information.
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 characters,
- cannot start with a number or punctuation character,
- cannot start with the letters xml (XML, Xml, etc.), and
- 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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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.
Dynamic List Attributes | ||
---|---|---|
Attribute | Required | Notes |
class | Yes | A reference to a class value as described below. The class attribute must be set to one or more of the built-in classes (listed below), or self . |
filter | No | A filter attribute is not required, but if one is included, it must be set to a valid filter (see below) or empty filter=“” . |
method | No | A reference to a plugin.py method that returns the list elements as described below. If not included, class must refer to one of the built-in methods (class=“self” by itself will not work). |
Dynamic List Filters
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 up to two filters if and only if one is an interface filter (see the first three in the list below). The filters are ANDed together, so one interface filter and one device type filter are the only combinations that will result in a non-empty list.
Class: indigo.devices |
|
---|---|
Optional Filters | Description |
indigo.zwave | include Z-Wave devices - this is an interface filter that can be used with other filters |
indigo.insteon | include INSTEON devices - this is an interface filter that can be used with other filters |
indigo.x10 | include X10 devices - this is an interface filter that can be used with other filters |
indigo.responder | include devices whose state can be changed |
indigo.controller | include devices that can send commands |
indigo.relay | relay devices |
indigo.dimmer | dimmer devices |
indigo.sprinkler | sprinklers |
indigo.thermostat | thermostats |
indigo.iodevice | input/output devices |
indigo.sensor | all sensor type devices: motion sensors, TriggerLinc, SynchroLinc (sensor devices that have a virtual state in Indigo) |
self | all device types defined by the calling plugin |
self.devTypeId | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified by the calling plugin |
com.somePlugin | all device types defined by some other plugin |
com.somePlugin.devTypeId | all devices of type deviceTypeId, where deviceTypeId is one of the device types specified in some other plugin |
The indigo.triggers
class specifies that the list will contain Indigo triggers. Only a single filter from the list below will return a useful list.
Class: indigo.triggers |
|
---|---|
Optional Filters | Description |
indigo.insteonCmdRcvd | INSTEON command received triggers |
indigo.x10CmdRcvd | x10 command received triggers |
indigo.devStateChange | device state changed triggers |
indigo.varValueChange | variable changed triggers |
indigo.serverStartup | startup triggers |
indigo.powerFailure | power failure triggers |
indigo.interfaceFail | interface failure triggers - can be used with or without a specified protocol |
indigo.interfaceInit | interface connection triggers - can be used with or without a specified protocol |
indigo.emailRcvd | email received triggers |
Using the indigo.schedules
class will result in a list of all schedules, as will indigo.actionGroups
and indigo.controlPages
for their respective object types. None of these classes support any kind of filter. Lastly, indigo.variables
will get you a list of variables. If you include filter=“indigo.readWrite”
, you’ll get only variables for which you can change the value. Currently, there’s only one read-only variable (isDaylight) defined by the system, but it may be possible to create them in future versions of the API.
Another special built-in list is serial ports - use indigo.serialPorts
to get a list of available serial ports, and what's returned to your plugin is the full path to the plugin (e.g. “/dev/tty*”). This is suitable for using with PySerial, which we include with Indigo. See the “serialport” field type below for an even better way of collecting serial communication information.
You may also include custom dynamic lists that are constructed on-the-fly by your plugin. If you defined your list like this:
<Field id="insteonDimmers" type="menu"> <Label>INSTEON Dimmers:</Label> <List class="self" filter="stuff" method="myListGenerator"/> </Field>
Then your plugin will have the method specified called with the filter. For the above example, you must define a method like this:
def myListGenerator(self, filter="", valuesDict=None, typeId="", targetId=0): # From the example above, filter = "stuff" # You can pass anything you want in the filter for any purpose # Create 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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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. |
id | Yes | 1.0 | This is a unique identifier for this Field within the context of this ConfigUI element. |
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. |
type | Yes | 1.0 | This must be “checkbox”. |
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. |
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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. |
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 . |
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 |
---|---|---|---|
alwaysUseInDialogHeightCalc | No | 1.0 | For dialogs that contain hidden controls (see visibleBindingId and visibleBindingValue below), setting this attribute to true will force Indigo to take hidden controls into account when initially sizing the dialog box. This is useful when hidden controls become visible based on user input. |
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. |
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 . |
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 2023.1/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.
Custom HTML Config Dialogs
You can also implement your own custom configuration in HTML if you prefer. Rather than adding lots of <Field>
definitions, you simply specify a <URL>
element. The URL specified can either be a fully specified URL (protocol://host/path) or it may be a relative URL (/some/relative/path). If it's the later then Indigo will attempt to guess the best base URL. You would then handle those form requests using the built-in request handling mechanism discussed below. See the Example HTTP Responder plugin in the SDK for an example.
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:
Device Types
Name | Type | Required | Notes |
---|---|---|---|
type | Attribute | Yes | This must be one of: dimmer , relay , sensor , speedcontrol , thermostat , 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. |
subType | Attribute | No | This must be one of: kDimmerDeviceSubType , kRelayDeviceSubType , kSensorDeviceSubType . We’ll discuss each below. There are also several device SubType examples in the SDK. |
allowUserCreation | Attribute | No | If set, the value must either be true or false (defaults to true ). This attribute is discussed in more detail below. |
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. If the optional <ConfigUi> element is not used, the “Edit Device Settings…” button will be displayed, but disabled. You'll also need to manually set dev.configured = True and then dev.replaceOnServer() for the change to take effect. |
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. |
<?xml version="1.0"?> <Device type="sensor" id="my_temp_sensor"> <Name>Example Temperature Sensor Module</Name> <ConfigUI> <Field id="address" type="textfield" defaultValue="123"> <Label>Module Address:</Label> </Field> </ConfigUI> <States> <State id="temperature"> <ValueType>Integer</ValueType> <TriggerLabel>Temperature</TriggerLabel> <ControlPageLabel>Temperature</ControlPageLabel> </State> </States> </Device>
Device Sub-Types
Category | SubType | Attribute(s) | Notes |
---|---|---|---|
Device | kDeviceSubType | Amplifier, Automobile, Camera, Keypad, Mobile, Remote, Robot, Security, Speaker, Streaming, Television, Weather, Other | For example, indigo.kDeviceSubType.Amplifier |
Dimmer | kDimmerDeviceSubType | Blind, Bulb, ColorBulb, ColorDimmer, Dimmer, Fan, InLine, Outlet, Plugin, Value | For example, indigo.kDimmerDeviceSubType.Dimmer |
Relay | kRelayDeviceSubType | DoorBell, DoorController, GarageController, InLine, Lock, Outlet, Plugin, Siren, Switch | For example, indigo.kRelayDeviceSubType.Switch |
Sensor | kSensorDeviceSubType | Analog, Binary, CO, DoorWindow, GasLeak, GlassBreak, Humidity, Illuminance, Motion, Presence, Pressure, Smoke, Tamper, Temperature, UV, Vibration, Voltage, WaterLeak, Zone | For example, indigo.kSensorDeviceSubType.Temperature |
* There are no device sub-types for other built-in devices at this time.
In Devices.xml
<Device type="sensor" subType="kSensorDeviceSubType.Temperature" id="my_temp_sensor"> <Device type="sensor" subType="kSensorDeviceSubType.Temperature, ui=Outdoor Temperature" id="my_temp_sensor" <Name>Example Adjustable Sensor Module</Name> ...
In Python:
newdev = indigo.device.create(indigo.kRelayDeviceSubType.Plugin, deviceTypeId="Device1") newdev.model = "Device 1 Model" newdev.subModel = "Sub Model 1" newdev.name = u"Device 1"
To get information on the device group:
indigo.device.getGroupList(dev.id)
When the allowUserCreation
attribute is present and set to false
, Indigo will not display the device model type when the user elects to create a new device (the “Model” dropdown will not contain the device type). This is especially useful for complex devices that have multiple sub-types that are set programmatically (but not created using the Device Factory approach). Your plugin may not support individual sub-types as stand-alone devices and, therefore, you wouldn't want users to be able to create them. The allowUserCreation
attribute was introduced with Indigo version 2022.1.2
and API 3.1
.
<Device type="sensor" subType="kSensorDeviceSubType.Temperature, ui=Outdoor Temperature" id="my_temp_sensor", allowUserCreation="false">
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.
IMPORTANT: Plugin developers should account for each devices' callbacks when implementing built-in device types – even if you don't use all the callbacks within your plugin. For example, if your plugin implements theSpeed Control
device type, your plugin should include handlers for all the Speed Control callbacks – such asturnOn
,turnOff
, ortoggle
. This is very important because these callbacks will still be exposed in the Indigo Client UI for things like Actions and Triggers. It's best to give users an indication that the callback won't do anything in your plugin by writing a warning to the Event log. Consult the SDK for more information on each device types' callbacks and examples of how to handle them.
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.
Device Factory
One way to create “multifunction” devices is to use Indigo's Device Factory method. Device Factory devices are defined by using a special <DeviceFactory>
node in Devices.xml
. A basic Device Factory device definition would look something like this:
<DeviceFactory> <Name>Device Factory Plugin Device Group</Name> <ButtonTitle>Create</ButtonTitle> <!-- A configUI is required for the device factory element. --> <ConfigUI> <Field id="label" type="label"> <Label>You are not required to have settings here, but a configUI node is required. These will be stored in self.pluginPrefs and will apply to all grouped devices created with the plugin.</Label> </Field> <Field id="name" type="textfield"> <Label>Name:</Label> </Field> </ConfigUI> </DeviceFactory>
Note that when the <Device Factory>
node is present, devices that include the subType
attribute will not be presented to the user as individual device types when they elect to create a new plugin device (it's not possible to have a Device Factory implementation and individual plugin device definitions together). You define sub-devices like any other device type, with the addition of the subType
attribute:
<DeviceFactory> <Name>Plugin Device Group</Name> <ButtonTitle>Create</ButtonTitle> <!-- A configUI is required for the device factory element. --> <ConfigUI> <Field id="label" type="label"> <Label>You are not required to have settings here, but a configUI is required. These will be stored in self.pluginPrefs and will apply to all grouped devices created with the plugin.</Label> </Field> <Field id="name" type="textfield"> <Label>Name:</Label> </Field> </ConfigUI> </DeviceFactory> <Device type="dimmer" id="my_dimmer_device" subType="kDimmerDeviceSubType.Dimmer" ui="Dimmer"> <!-- Note that devices can have their own configUIs --> <Name>Example Dimmer Module</Name> <ConfigUI> <Field id="label" type="label"> <Label>You are not required to have a config UI for each individual device, however, creating a settings UI allows for settings that apply only to that group (when more than one grouped device is desired). These will be stored in dev.pluginProps.</Label> </Field> <Field id="name" type="textfield" hidden="false"> <Label>Name:</Label> </Field> <Field id="setting" type="textfield" hidden="false"> <Label>Setting:</Label> </Field> </ConfigUI> <States> <State id="state"> <ValueType>String</ValueType> <TriggerLabel>State</TriggerLabel> <ControlPageLabel>State</ControlPageLabel> </State> </States> </Device> <Device type="sensor" id="my_sensor_device" subType="kSensorDeviceSubType.Temperature" ui="Outdoor Temperature"> <Name>Example Adjustable Sensor Module</Name> <States> <State id="state"> <ValueType>String</ValueType> <TriggerLabel>State</TriggerLabel> <ControlPageLabel>State</ControlPageLabel> </State> </States> </Device> <Device type="relay" id="my_lock_device" subType="kRelayDeviceSubType.Lock" ui="Lock"> <Name>Example Lock Module</Name> <States> <State id="state"> <ValueType>String</ValueType> <TriggerLabel>State</TriggerLabel> <ControlPageLabel>State</ControlPageLabel> </State> </States> </Device>
Once the device definitions have been established, you can use your plugin code to create the device group. This function can be called from the validate_device_factory_ui()
or closed_device_factory_ui()
methods.
def create_the_device_group(self, my_name): """ Convenience method for device creation. This could also be done in closed_device_factory_ui() for example. Note that some device props are read only even when you create them from scratch (i.e., dev.version) and some will be ignored if you try to set them (i.e., dev.description, dev.errorState). Note that `protocol`, `name` and `deviceTypeId` are all required with the `indigo.device.create()` method call. """ self.debugLog("create_the_device_group called") # Create the first device in the group. Note that the ''deviceTypeId'' value matches the ''id'' we used in our ''Devices.xml'' definition. new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_dimmer_device") new_dev.model = "Grouped Device" new_dev.subModel = "Dimmer" new_dev.name = f"{my_name} Dimmer" new_dev.replaceOnServer() # Add the group my_name setting to the props of device 1 for later use. new_props = new_dev.pluginProps new_props['name'] = my_name new_dev.replacePluginPropsOnServer(new_props) # Create the second device in the group. You can also add a state value here if desired. new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_sensor_device") new_dev.model = "Grouped Device" new_dev.subModel = "Temperature" new_dev.name = f"{my_name} Temperature" new_dev.replaceOnServer() new_dev.updateStateOnServer('state', value="Some value.") # You can also create devices that don't have corresponding device parameters established in Devices.xml. new_dev = indigo.device.create(protocol=indigo.kProtocol.Plugin, name=my_name, deviceTypeId="my_lock_device") new_dev.model = "Grouped Device" new_dev.subModel = "Lock" new_dev.name = f"{my_name} Lock" new_dev.replaceOnServer()
IMPORTANT: You are responsible for catching all the mandatory methods for the devices you create. The Device Factory method does not create them for you.
For another option to create Device Factory devices, check out the example in the Indigo SDK.
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 Event to be a separator in your event 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 in Actions.xml
:
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. |
When the Action is fired in Indigo, the callback method will be called and an Indigo dictionary will be passed with information about how to handle the call. In the example below, the action
payload will contain the necessary things to implement the call. Notice the call to action.props.get()
in the Python callback below, which will pull the appropriate value from the specified key (message
, type
, etc.) using the standard Python dictionary get()
method.
Here is a sample action dict sent to a plugin:
configured : True delayAmount : 900 <-- Read only; currently not used description : redraw one chart <-- Taken from the Actions.xml <NAME> element. props : com.foo.indigoplugin.my_plugin : (dict) config_prop_1 : 123 (integer) config_prop_2 : true (bool) config_prop_3 : "baz" (string) replaceExisting : True <-- Read only; currently not used textToSpeak :
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 Timers plugin to restart a timer. 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 Timers and Pesters 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)
Custom HTML Menu Item Dialogs
You can also implement your own custom menu item form in HTML if you prefer. Rather than adding <CallbackMethod>
and <ConfigUI>
definitions, you simply specify a <URL>
element. The URL specified can either be a fully specified URL (protocol://host/path) or it may be a relative URL (/some/relative/path). If it's the later then Indigo will attempt to guess the best base URL. You would then handle those form requests using the built-in request handling mechanism discussed below. See the Example HTTP Responder plugin in the SDK for an example.
SupportURL Elements
Anywhere you can specify a <SupportURL>
element, the value can be either a full URL or a relative URL. If it's relative, then Indigo will attempt to guess the best base URL. You can use this to supply static HTML files or dynamic help provided through the HTTP processing API discussed below.
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). |
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.
Helper Methods
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. |
Properties
The base plugin provides some properties that are specific to a plugin instance.
Property | Value Type | Notes |
---|---|---|
pluginFolderPath | string | The return value is the full path to the plugin. This is useful if you need to construct a full path to a file somewhere in the plugin's hierarchy, perhaps to have IWS stream the file back. |
pluginSupportURL | string | The return value is URL that's specified in the plugin's Info.plist. |
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, 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 shown below. 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. The message type (the left part in the Event Log) is modified so that it reflects the level of the log message (with the exception of the `info` level). For convenience, the various log levels are also represented in color. Here's an example of each level:
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 Another Class or Submodule
Logging from another class or submodule is relatively straight-forward. You can either get the logger for the plugin:
plugin_logger = logging.getLogger("Plugin")
For example,
class MyClass(object): def __init__(): self.logger = logging.getLogger("Plugin") self.logger.debug("MyClass Object")
and use it directly, or you can pass your logger into the methods of the submodule. You could also pass in the event log handler defined for you by the plugin base (self.indigo_log_handler
), and then attach that to a custom logger in your module.
Custom IndigoLogHandler
If you use the instance of self.indigo_log_handler
, the message emitted to the Event Log window will have a type that is the name of the plugin.
If you want log lines with titles other than the plugin name (like a name specific to the submodule), you can instantiate an instance of the IndigoLogHandler
class (which is what self.indigo_log_handler
is) instead:
custom_logger = logging.getLogger("MyModule") custom_handler = self.IndigoLogHandler("MyModule", logging.DEBUG) custom_logger.addHandler(custom_handler)
And anything logged to your plugin_logger
will be reflected in the Event Log:
MyModule Debug Some event log debug message here
exc_info
With the logging message self.logger.critical(“Something bad happened.”)
you will see the following in the log:
My Plugin Error Something bad happened. My Plugin Error plugin runConcurrentThread function returned or failed (will attempt again in 10 seconds)
which doesn't provide any detail on what actually went wrong. Fortunately, the logging method provides a way to pass more information about the error to the logger with exc_info
; such as self.logger.critical(“Something bad happened.”, exc_info=True)
which yields:
My Plugin Error Something bad happened. Traceback (most recent call last): File "plugin.py", line 123, in runConcurrentThread x = 1 / 0 ZeroDivisionError: division by zero My Plugin Error plugin runConcurrentThread function returned or failed (will attempt again in 10 seconds)
Full Example
While there are many different ways to implement logging, here is a “full” example all in one place.
import logging def __init__(self): # Get the current logging level from pluginPrefs self.debugLevel = int(self.pluginPrefs.get('showDebugLevel', "30")) # Set preferred log format specifier log_format = '%(asctime)s.%(msecs)03d\t%(levelname)-10s\t%(name)s.%(funcName)-28s %(message)s' self.plugin_file_handler.setFormatter( logging.Formatter(fmt=log_format, datefmt='%Y-%m-%d %H:%M:%S') ) self.indigo_log_handler.setLevel(self.debugLevel) def runConcurrentThread(self): self.logger.debug("Starting concurrent thread.") try: x = 1 / 0 except ZeroDivisionError: self.logger.critical("Something bad happened.", exc_info=True)
Which yields:
My Plugin Error Something bad happened. Traceback (most recent call last): File "plugin.py", line 123, in runConcurrentThread x = 1 / 0 ZeroDivisionError: division by zero
Logging from Linked and Embedded Scripts
Logging from linked and embedded scripts is very straight-forward. You can simply set the level you want by accessing the logging.*
level you want:
import logging indigo.server.log("debug message", level=logging.DEBUG) indigo.server.log("info message", level=logging.INFO) indigo.server.log("warning message", level=logging.WARNING) indigo.server.log("error message", level=logging.ERROR) indigo.server.log("critical message", level=logging.CRITICAL)
and then logging messages will appear with the colors above. Result:
Processing HTTP requests in your plugin
Your plugin can process arbitrary HTTP GET or POST requests that are sent to a specific Indigo Web Server (IWS) URL:
https://myreflector.indigodomo.net/message/PLUGINID/actionId/
Substitute the ID of your plugin (as specified in its Info.plist) and the ID of the action that will handle the request (as specified in the Actions.xml). You can also use the direct IP address instead of your reflector.
The HTTP request must authenticate if the IWS server has authentication enabled:
- The recommended approach is to use an API key: the request must contain an “Authorization” HTTP header, the value of which is “Bearer API_KEY_HERE”, where you substitute an API key that is generated from the Authorizations page in the user's Indigo Account. This is also how OAuth is used to authenticate when using a external service like Alexa or Google Home.
- If for some reason you can't include the header, you may pass the API key as an additional GET argument on the URL (
?other=args&api-key=KEYHERE
)
Request
As a reminder, here's how you specify an action in Actions.xml that is used only via an API (from another plugin such as the IWS plugin):
<Action id="handle_message" uiPath="hidden"> <Name>some message</Name> <CallbackMethod>handle_some_action</CallbackMethod> </Action>
And here's how an action method is defined in your plugin:
def handle_some_action(self, action, dev=None, callerWaitingForResult=None): some_value = action.props["somekey"] return some_value
Given these examples, here's the URL that would get directed to that action:
https://myreflector.indigodomo.net/message/com.your.pluginId/handle_message/
Calls via this method will always pass in callerWaitingForResult=True
. You will want to make sure that your plugin returns as quickly as possible - any long-running processes should be put into another thread with some sort of asynchronous message back to the caller if necessary.
IWS will insert several things into the action.props
dictionary:
Key | Value |
incoming_request_method | this will be either POST or GET. |
headers | this is a dictionary of the headers in the request. |
body_params | if this key exists, it will be a dictionary containing any form POST name/value pairs. It won't exist if the request was a GET or a POST with a body. |
url_query_args | if there were any query args on the URL line, they will be in this dictionary if it exists. |
request_body | this will be the contents of the body of the HTTP request. It won't exist if the request was a GET or a POST without any body. |
file_path | this will be a list of path parts from the URL after the action ID. So for the url: http://host/message/pluginid/actionid/path/to/some/file.txt the value will be an indigo.List with the following items: ["path", "to", "some", "file.txt"] |
Note: the dicts mentioned above will all be indigo.Dict objects, not standard Python dicts.
Reply
What your plugin should return in the simplest case is a JSON string which will just be passed back in the HTTP reply. Your plugin can also return a more complex dictionary containing the following keys:
Key | Value |
status | this is an integer representing a valid HTTP return code. If it's not included, a 200 will be returned. |
headers | a dictionary of HTTP headers that will be added to the reply. Most useful will be the 'Content-Type' header if you want to return something else besides JSON (which is the default). |
content | this is the actual string returned in the HTTP reply. Defaults to an empty string. |
This will allow you to return just about anything - HTML, XML, plain text, etc. IWS will do no postprocessing of the content string, so you must ensure that you are returning the properly formatted information that the requester is expecting.
You can also pass back an indigo.Dict
instance that will instruct IWS to stream a file from the filesystem back to the requester. This is the structure:
{ "status": 310, # Internal status code indicating that IWS should stream back the specified file. "file_path": path, # this is a full path specifier "headers": indigo.Dict({"Content-Type": content_type} # you should add a Content-Type header )
We've provided a utility method that will validate that the file specified exists and then will pass back the appropriate indigo.Dict
instance.
Errors
IWS will return the following HTTP error responses if an error occurs trying to process a request:
Status | Meaning |
401 | if the OAuth token is invalid. |
405 | if any method other than POST or GET is attempted. |
500 | any unexpected/uncaught exception. |
501 | if the plugin isn't installed or doesn't define the action specified in the URL. |
503 | if the plugin is installed but is disabled. |
For 50x
errors, a JSON dictionary will be returned describing the issue:
Key | Value |
error | One of: plugin_disabled , invalid_plugin , invalid_action , unknown_error |
description | A textual description of the error |
exception | Only returned when an unknown_error is returned. It will be the stack trace of the uncaught exception. |
If your handler returns any kind of error (4xx or 5xx) and includes a message, that message will be returned to the caller as the body of the HTTP reply. This will allow you to return a custom page (404 for instance) that will more appropriately reflect the error.
Usage Guidance
This API is meant primarily for small(ish) text message handling, like JSON/XML messaging APIs or small HTML files. There are a few things that you will want to avoid:
- Long running actions - if your action should be relatively quick to respond: 15-20 seconds is the max guidance
- Large files - large files will slow the total turnaround time, which you need to minimize (see above)
- Binary data - this release of the API isn't designed for binary data
For long-running actions, one pattern would be to reply immediately with some kind of acknowledgement of the incoming message, then handle the processing asynchronously. As this approach would complete the HTTP request/response loop, if your caller needs some kind of status after processing you would need to handle that yourself.
Event and Message Flow
Under some circumstances, the Indigo Server will send callbacks to your plugin based on events that take place. For example, if your plugin devices expose features like Turn On, Turn Off or Status Request, Indigo will send a callback to your plugin so you can take actions on these events. There are many different callbacks that can occur which are documented extensively in the SDK.
Here is a simple example showing how to handle these callbacks (consult the SDK for a detailed example that relates to your device's class).
def actionControlDevice(self, action, dev): ###### TURN ON ###### if action.deviceAction == indigo.kDeviceAction.TurnOn: # Command hardware module (dev) to turn ON here: send_success = True # Set to False if it failed. if send_success: # If success then log that the command was successfully sent. self.logger.info(f"sent \"{dev.name}\" on") # And then tell the Indigo Server to update the state. dev.updateStateOnServer("onOffState", True) else: # Else log failure but do NOT update state on Indigo Server. self.logger.error(f"send \"{dev.name}\" on failed") ###### TURN OFF ###### elif action.deviceAction == indigo.kDeviceAction.TurnOff: # Command hardware module (dev) to turn OFF here: send_success = True # Set to False if it failed. if send_success: # If success then log that the command was successfully sent. self.logger.info(f"sent \"{dev.name}\" off") # And then tell the Indigo Server to update the state: dev.updateStateOnServer("onOffState", False) else: # Else log failure but do NOT update state on Indigo Server. self.logger.error(f"send \"{dev.name}\" off failed") ###### STATUS REQUEST ###### elif action.deviceAction == indigo.kUniversalAction.RequestStatus: # Report on the status of the device self.update_device_status() # Take your action(s) to update the device's status. self.logger.info(f"\"{dev.name}\" updated.")
Note that you may see camel case examples of these callbacks like actionControlDevice()
or snake case action_control_device()
. As a convenience, Indigo supports both naming styles; however, camel case may be deprecated in the future.
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
The plugin's preferences are stored in its preferences file. Plugin prefs are cached but are flushed periodically to the actual preference file. It will also automatically flush when the plugin exits.
3rd Party Python Libraries
Indigo 2023.1 includes a variety of popular 3rd party Python libraries which are described on the Python Packages page. Any changes to the libraries installed will be detailed there. You should not make changes to the packages that Indigo installs. If you need a different package version, you should include it within your plugin package distribution.
Setting Up a Development Environment
Many plugin developers choose to write their code in an IDE (Integrated Development Environment) such as PyCharm or pdb. There are several tips that will make using IDEs to develop Indigo Plugins more effective on the Setting Up a Development Environment page.