Skip to content

Plugin Implementation Reference

In this guide

This page is a complete reference for plugin.py — the main Python class that every Indigo plugin must implement — covering lifecycle methods, device and action callbacks, logging, and HTTP request handling.

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 startup method, described below, to do your global plugin initialization.
__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")
The startup method supports a return value:
- don't return anything (or return None explicitly) - your plugin will start up
- return True - your plugin will start up
- return False - your plugin stops with a default message
- return a string - your plugin stops with that string as the message
- return anything else - plugin stops with a default message
shutdown(self)
No This method will get called when the IndigoServer wants your plugin to exit. If you define a global shutdown [variable](glossary_of_terms.md), 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](glossary_of_terms.md) 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: shutdown will be called after runConcurrentThread (discussed next) so cleanup here will be after any changes that might result from a loop in runConcurrentThread.
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 self.sleep() with the number of seconds to delay between loops. self.sleep() will raise an self.StopThread exception when you should end runConcurrentThread. You don't have to catch that exception if you don't need to do any cleanup before returning - it will just throw out to the next level. Note: shutdown will be called after runConcurrentThread finishes processing so you can do your cleanup there.
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 address:port (that's defined in dev.pluginProps), get it's current state(s) and tell the IndigoServer to set those states (using the dev.updateStateOnServer() method).
deviceStopComm(self, dev)
No This is the complementary method to deviceStartComm() - it gets called when the device should no longer be active/enabled. For instance, when the user disables or deletes a device, this method gets called.
didDeviceCommPropertyChange(self, origDev, newDev)
No This method gets called by the default implementation of deviceUpdated() to determine if any of the properties needed for device communication (or any other change requires a device to be stopped and restarted). The default implementation checks for any changes to properties. You can implement your own to provide more granular results. For instance, if your device requires 4 parameters, but only 2 of those parameters requires that you restart the device, then you can check to see if either of those changed. If they didn't then you can just return False and your device won't be restarted (via deviceStopComm()/deviceStartComm() calls).
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.
getDeviceConfigUiValues(self, pluginProps, typeId, devId)
No This method will get called whenever an Device configuration is opened. Indigo will look for this method and, if it exists, will pre-populate the configuration dialog with the information created/modified in the method. This method is particularly helpful when you want an Device's configuration to be different from the default (set in the Device configuration XML file). A simple example is provided below.

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.

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 constructor
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 opened 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 deprecated in favor of the standard Python [[https://docs.python.org/2/library/logging.html|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 [[https://docs.python.org/2/library/logging.html#logger-objects
indigo_log_handler An instance of a special [[https://docs.python.org/2/library/logging.html#handler-objects
plugin_file_handler An instance of a Python [[https://docs.python.org/2/library/logging.handlers.html#timedrotatingfilehandler

The https://docs.python.org/3/library/logging.html 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:

Embedded Script Logging Image

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:

Embedded Script Logging Image

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 string
    "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 - 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.

FIXME 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.

Back to Top

3rd Party Python Libraries

Indigo 2025.2 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.

Warning

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.