Table of Contents

Indigo Scripting Tutorial

Talking to the Indigo Server

The Indigo Plugin Host (IPH) provides a python scripting and plugin environment, which makes controlling the Indigo Server easy via the high-level python language (the official python web site has great python tutorials). Every plugin automatically runs inside its own Indigo Plugin Host process, but you can also start the IPH in an interactive mode to directly communicate with the Indigo Server. It is a great way to experiment with Indigo's python environment, which is available to both python scripts run by Indigo and the plugin architecture.

To launch the Terminal utility application (inside /Applications/Utilities/) in an Indigo shell mode choose the Plugins→Open Scripting Shell menu item.

Example Code Snippets

Below are some example you can copy/paste directly into the IPH window opened when following the directions above. Keep in mind these are only examples, so be sure and read over the full Indigo Object Model (IOM) Reference.

Note: For simplicity some of the samples below specify objects based on name (“office desk lamp”). However, the preferred lookup mechanism is to use the object's ID which can be retrieved by control-clicking on the object name in Indigo's Main Window. By using the ID you ensure the object will be found even if its name is changed.

Device Examples

Turn on the device "office desk lamp":

indigo.device.turnOn("office desk lamp")

Duplicate the device "office desk lamp":

indigo.device.duplicate("office desk lamp", duplicateName="office desk lamp2")

In 4 seconds turn on the device "office desk lamp" for 2 seconds:

indigo.device.turnOn("office desk lamp", duration=2, delay=4)

Turn off all devices:

indigo.device.allOff()

Count the number of device modules:

indigo.devices.len()

Count the number of dimmable device modules:

indigo.devices.len(filter="indigo.dimmer")

Count the number of devices defined by all of our plugin types:

indigo.devices.len(filter="self")

Count the number of irBlaster type devices defined by our plugin:

indigo.devices.len(filter="self.irBlaster")

Get the on state of a device if it has the onState property (uses Python's hasattr() introspection method):

lamp = indigo.devices["office desk lamp"]
if hasattr(lamp, 'onState'):
	isOn = lamp.onState

Get the class of a device (uses Python's %%__class__%% property):

lamp = indigo.devices["office desk lamp"]
if lamp.__class__ == indigo.DimmerDevice:
	theBrightness = lamp.brightness

Turn on a light only if it's been off for longer than 1 minute:

from datetime import datetime
lamp = indigo.devices[91776575] # "Hallway light"
timeDelta = datetime.now() - lamp.lastChanged
if not lamp.onState and timeDelta.seconds > 60:
        indigo.device.turnOn(91776575)

Access a custom device state

# get the device
dev = indigo.devices[23989834]  # Some custom device
# access the state through the states property, use the key
# that's displayed in the Custom States tile on the main window
# when you have a custom device selected
print dev.states["someDeviceStateKey"]

Variable Examples

Create a new Indigo variable named fooMonster, change its value multiple times, and delete it:

newVar = indigo.variable.create("fooMonster", "default value")
indigo.variable.updateValue(newVar, "asleep")
indigo.variable.updateValue(newVar, "awake")
indigo.variable.delete(newVar)

Getting a variable object and using it's value:

myVar = indigo.variables[123]
if myVar.value == "true":
    indigo.server.log("The variable had a value of 'true'")

Duplicating a variable:

indigo.variable.duplicate("fooMonster", duplicateName="fooMonsterSister")

Using a variable value in an HTTP GET

myVar = indigo.variables[1893500335] # always use variable ID rather than name
import urllib, urllib2
urllib2.urlopen("http://example.com/foo/bar?param=%s" % urllib.urlencode(myVar.value))

Setting a variable to Python types

Since Indigo variable values are always unicode strings, you have to convert anything that's not a string. It's safest to use the unicode() method:

myAsciiString = "ASCII String"
indigo.variable.updateValue(1234567, value=myAsciiString)

myUnicodeString = u"éçø"
indigo.variable.updateValue(1234567, value=myUnicodeString)

myInteger = 1
indigo.variable.updateValue(1234567, value=unicode(myInteger))

myFloat = 1.0
indigo.variable.updateValue(1234567, value=unicode(myFloat))

myList = ["one", 2, "three", 4]
indigo.variable.updateValue(1234567, value=unicode(myList))

myDictionary = {"first":1, "second":"two", "third":3, "fourth":"four"}
indigo.variable.updateValue(1234567, value=unicode(myDictionary))

Any Python object that can be converted to a unicode string can then be inserted into an Indigo variable. Note: conversions of this type are often one way: you can't necessarily automatically recreate the Python object from the string created by the unicode() method. For primitive types like numbers it may work, but for complex Python types like lists and dictionaries it will not. If you want to use Indigo to store a string representation of some complex Python data types, you can use JSON to encode them into strings then later decode them back into their respective Python objects:

import simplejson as json
myList = ["one", 2, "three", 4]
myDictionary = {"first":1, "second":"two", "third":3, "fourth":"four", "myList":myList}
indigo.variable.updateValue(1234567, value=json.dumps(myDictionary))
# string will be something like: '{"second": "two", "myList": ["one", 2, "three", 4], "fourth": "four", "third": 3, "first": 1}'
myNewDictionary = json.loads(indigo.variables[1234567].value)
#myNewDictionary is now the same as myDictionary
myNewDictionary["myList"]
# results in: ['one', 2, 'three', 4]

Custom Python classes that you create can implement the __unicode__(self) method. Take this simple example:

class myCustomClass:
  def __init__(self):
	  self.a=1
	  self.b=2
  def __unicode__(self):
    outputString = u"a:%i, b:%i" % (self.a, self.b)
    return outputString

If you have an instance of that class, then you can create a string representation of the class to insert into an Indigo variable:

cl = myCustomClass()
indigo.variable.updateValue(1234567, value=unicode(cl))
# the value of the variable in Indigo will look like this: 'a:1, b:2' not including the quotes

For further information on Python classes, check out this great tutorial.

Date and Time Examples

Get the current server time:

indigo.server.getTime()

Calculate the sunset time in 1 week:

import datetime
one_week = indigo.server.getTime().date() + datetime.timedelta(days=7)
indigo.server.calculateSunset(one_week)

Action Group Examples

Execute Action Group 12345678:

indigo.actionGroup.execute(12345678)

Log Examples

Log to the Indigo Event Log window all of the attributes/properties of the device "office desk lamp":

lamp = indigo.devices["office desk lamp"]
indigo.server.log(lamp.name + ": \n" + str(lamp))
logList = indigo.server.getEventLogList(lineCount=5)
print(logList)

Folder Examples

Iterate over a list of all device folders

for folder in indigo.devices.folders:
	print "Folder id: %s name: %s, remoteDisplay: %s" % (folder.id,folder.name,folder.remoteDisplay)

Create a trigger folder named "My Triggers" and if it exists just return the existing one

try:
	myFolder = indigo.triggers.folder.create("My Triggers")
except ValueError, e:
	if e.message == "NameNotUniqueError":
		# a folder with that name already exists so just get it
		myFolder = indigo.triggers.folders["My Triggers"]
	else:
		# you'll probably want to do something else to make myFolder a valid folder
		myFolder = None

Make a folder visible in remote clients (IWS, Indigo Touch, etc.)

indigo.devices.folder.displayInRemoteUI(123, value=True)

Getting the folder a device is in

lamp = indigo.devices["office desk lamp"]
# An object that's not in a folder will have a folder id of 0, which isn't a valid folder
# so we need to make sure it's a valid folder ID first
if lamp.folderId != 0:
	lampsFolder = indigo.devices.folders[lamp.folderId]
else:
	lampsFolder = None

Miscellaneous Examples

Get a list of all serial ports, excluding any Bluetooth ports:

indigo.server.getSerialPorts(filter="indigo.ignoreBluetooth")

Sending emails

# Simple Example
indigo.server.sendEmailTo("emailaddress@company.com", subject="Subject Line Here", body="Some longish text for the body of the email")

# Putting a variable's data into the subject and body
theVar = indigo.variables[928734897]
theSubject = "The value of %s" % (theVar.name)
theBody = "The value of %s is now %s" % (theVar.name, theVar.value)
indigo.server.sendEmailTo("emailaddress@company.com", subject=theSubject, body=theBody)

# Putting device data into the subject and body
theDevice = indigo.devices[980532604]
theSubject = "Summary of %s" % (theDevice.name)
theBody = "%s is %s\n%s is %s" % ("onState", str(theDevice.onState), "lastChanged", str(theDevice.lastChanged))
indigo.server.sendEmailTo("emailaddress@company.com", subject=theSubject, body=theBody)

Starting the host from an existing terminal window

If you already have a Terminal shell running, you can launch it directly. The file path to the IPH executable is a bit deep, so create an alias to it inside your .bashrc (or .bash_profile) file by copying/pasting this into the Terminal (you only need to do this once!):

echo "alias indigohost='/Library/Application\ Support/Perceptive\ Automation/Indigo\ 7/IndigoPluginHost.app/Contents/MacOS/IndigoPluginHost'" >> ~/.bashrc

Next, close the Terminal window and open a new one to load the shortcut alias to the IPH executable. To start the IPH in interactive mode we pass the -i flag to it inside the Terminal:

indigohost -i

As shown, the IPH will automatically connect to the IndigoServer running on the same Mac and will show the server's version information. Additionally, you'll notice that Indigo Server logs the connection of the interactive shell plugin.

Next, let's tell the Indigo Server to log a message to the Event Log window (again, via the Terminal application):

indigo.server.log("Hello world!")

Connecting Remotely over SSH

If you have SSH configured so you can remotely connect to your Mac running the Indigo Server, then you can use SSH to start the IPH interactively anywhere using the syntax:

ssh username@indigo_mac_ipaddr -t "/Library/Application\ Support/Perceptive\ Automation/Indigo\ 6/IndigoPluginHost.app/Contents/MacOS/IndigoPluginHost" -i

What Else Can it Do?

The IPH gives you full access to the Indigo Object Model (IOM) providing access to create/edit/delete/control Indigo objects, as well as several Indigo utility functions, INSTEON device commands, and X10 device commands. Access to all of this functionality is provided by the indigo module automatically loaded and included in any python scripts run by the IPH (including interactive IPH sessions like we are using here).

Executing Indigo Commands Directly

In addition to communicating interactively with Indigo via the shell, you can also send direct python commands to Indigo via the IPH. For example, to get the current brightness of the device “office lamplinc”:

indigohost -e 'return indigo.devices["office lamplinc"].brightness'

Or to toggle the device “office lamplinc” three times:

indigohost -e '
indigo.device.toggle("office lamplinc")
indigo.device.toggle("office lamplinc")
indigo.device.toggle("office lamplinc")
'

Note when your commands are executed the indigo module is already loaded and connected and you can execute standard python code (loops, conditionals, etc.).

Caveat: Each call creates a new IPH (indigohost) process which must establish a connection to the Indigo Server. Although this is relatively fast, calling it multiple times a second is not recommended.

Executing Indigo Python Files

The IPH can also be used to execute Indigo python (.py) files, like this:

indigohost -x /SomeFolder/indigo_script.py'

Caveat: Each call creates a new IPH (indigohost) process which must establish a connection to the Indigo Server. Although this is relatively fast, calling it multiple times a second is not recommended.

Executing AppleScript

Often times, you may find yourself wanting to execute an AppleScript from Python. You want to send some parameters to the AppleScript and you want to get results back in some format. There's a pretty straight-forward way to do this. First, let's take a look at a simple AppleScript that implements a run handler to capture command line arguments:

on run args
	tell application "JSON Helper"
		set theRecord to {parameterCount:length of args, theList:args}
		return make JSON from theRecord
	end tell
	# the args variable is a simple AppleScript list that you can iterate, etc.
end run

This script accepts any number of arguments. It creates an AppleScript record with two entries: parameterCount is the number of arguments passed to the script and theList is just the list of arguments. We then use the excellent JSON Helper application (available in the Mac app store) to return a JSON string that represents the record.

Next, we implement a simple Python script that will run the AppleScript with some number of arguments, get the JSON returned from it and create a native Python dictionary (which is similar to an AppleScript record):

import subprocess
import json

command_args = ['/usr/bin/osascript', '/path/to/applescript.scpt']

command_args.append("first param")
command_args.append("second param")

result = subprocess.Popen(command_args, stdout=subprocess.PIPE).communicate()[0]

reply_dict = json.loads(result)

print reply_dict

This simple script imports the subprocess and json modules and it creates a list of arguments to send to the subprocess.Popen method. The first two parameters are the command, which in this case is the osascript command that comes with Mac OS X, and the full path to the AppleScript you want to execute. Next, we can just append as many arguments as we want to command_args. Each of these will be accessible in the AppleScript args variable inside the run handler as shown above in the example AppleScript.

Next, we call the Popen method with command_args, set the standard output to a pipe so that we can receive the output in our script, call the communicate method which will actually run the script, and get the first value returned from that command (it's a list, the first value is the complete output of the script).

The result variable at this point is the JSON created by the AppleScript, which looks something like this:

{
  "theList": [
    "first param",
    "second param"
  ],
  "parameterCount": 2
}

The last thing we want to do is create a native Python dictionary from the JSON string returned by the AppleScript - that's what the json.loads method does.

This is a great pattern for calling AppleScripts from Python, and may help alleviate some of the pain of the Mac OS X “application isn't running” bug (error -600).

Shared Classes and Methods in Python Files (Python Modules)

In Indigo, if you want to write AppleScript handlers that are shared by any script that Indigo executes, you can just put an AppleScript full of handlers in the /Library/Application Support/Perceptive Automation/Indigo 7/Scripts/Attachments folder. You can do the same thing with Python using a built-in mechanism that Python provides. There is a folder in the Library folder at the top level of your boot disk that is very similar to the Attachments folder:

/Library/Python/2.7/site-packages

Any Python files in this directory that define methods and/or classes will be available to any Python script whenever the interpreter is loaded (note that it's the Library directory at the top level of your boot disk, not the one in your user folder). These are referred to as Python modules. So, you can create files of classes, functions, etc. you want to share between all Python scripts. If you add/change something in that directory while the Indigo Server is running, you'll need to tell the server to reload - select the Plugins→Reload Libraries and Attachments menu item and Indigo will restart the Python interpreter (which will cause your module to be reloaded).

Files here can also import the entire IOM - IF the script that's actually running is started by Indigo. Here's a simple script that you can use that will safely import the IOM and will show a specific error when used from a Python script that's not started by Indigo:

"""
indigo_attachments.py

In this file you can insert any methods and classes that you define.
They will be shared by all Python scripts - you can even import the
IOM (as shown below) but if you do then you'll only be able to import
this script in Python processes started by Indigo. If you don't need
the IOM then skip the import and it'll work in any Python script
no matter where it's run from.
"""

try:
    import indigo
except ImportError:
    print "The indigo module can only be used by scripts started from within Indigo"
    raise ImportError
from datetime import datetime
  
def log(message, label=None):
    # Create a log line with the date/timestamp prepended in MM/DD/YYYY HH:MM:SS format
    log_line ="%s %s" % (datetime.today().strftime("%x %X"), message)
    # Write the log line with the label. If you didn't pass in a label
    # then the default label will be used.
    indigo.server.log(log_line, type=label)

Then, when you want to use any of the classes/methods in the file, just import the file and go:

import indigo_attachments
indigo_attachments.log("Here's a special sort of log message", label="My Custom Label")

What you'll see in the Event Log:

  My Custom Label                 01/15/16 10:20:39 Here's a special sort of log message

If you try to run this script in a normal python session, you'll see the print statement followed by the ImportError:

MyMac:testdir jay$ python2.7 testHandlerCall.py 
The indigo module can only be used by scripts started from within Indigo
Traceback (most recent call last):
  File "testHandlerCall.py", line 3, in <module>
    import indigo_attachments
  File "/Users/USERNAME/Library/Python/2.7/site-packages/indigo_attachments.py", line 14, in <module>
    raise ImportError
ImportError

Scripting Indigo Plugins

Indigo plugins are also scriptable. Because plugin-defined devices look almost identical to built-in devices, you can get (but not set) state information from them (see device examples above for a lot of examples). You can also get some of the other properties for a plugin. Most importantly you can execute plugin-defined actions, which is how you'd set their state values.

The first step is to get an instance of the plugin:

iTunesId = "com.perceptiveautomation.indigoplugin.itunes" # supplied by the plugin's documentation
iTunesPlugin = indigo.server.getPlugin(iTunesId)

This will always return to you a plugin object, defined with the following properties:

pluginDisplayName The name displayed to users in the clients
pluginId The ID of the plugin (same as was passed to the getPlugin() method above)
pluginSupportURL The URL supplied by the plugin that points to it's user documentation
pluginVersion The version number specified by the plugin

There are also a couple of methods defined by this object.

isEnabled()
This method will return True if the plugin is enabled (running), False otherwise
executeAction(actionId, deviceId, props)
This is the method that actually executes the action. actionId is the unique action id. deviceId is the id of the device that the action on which the action will perform (if it requires a device). props is a dictionary of properties that the action needs to process correctly. See the plugin's documentation for a description of what you need to pass in to execute actions.
restart(waitUntilDone=True)
This method will restart the plugin if it's already running. Pass the optional parameter waitUntilDone=False if you don't want to block while the plugin restarts (waitUntilDone defaults to True if it's no parameters are passed). If you're a plugin developer and you want to restart the plugin from within itself you should pass that parameter.

Plugin developers should test and document their plugin actions to make sure that the necessary information is available to scripters.

Examples

iTunes Toggle Play State

itunesId = "com.perceptiveautomation.indigoplugin.itunes"
itunesPlugin = indigo.server.getPlugin(itunesId)
if itunesPlugin.isEnabled():
	itunesPlugin.executeAction("playPause", deviceId=135305663)

iTunes Set Volume to 50

itunesId = "com.perceptiveautomation.indigoplugin.itunes"
itunesPlugin = indigo.server.getPlugin(itunesId)
if itunesPlugin.isEnabled():
	itunesPlugin.executeAction("setVolume", deviceId=135305663, props={'volume':50})

Pause iTunes and Speak NOAA Weather

itunesId = "com.perceptiveautomation.indigoplugin.itunes"
itunesPlugin = indigo.server.getPlugin(itunesId)
if itunesPlugin.isEnabled():
	myWeatherStation = indigo.devices[1798384204]
	outsideTemp = myWeatherStation.states['temperatureF']
	currentCondition = myWeatherStation.states['currentCondition']
	spokenString = "The current temperature is %s degrees. Current condition is %s." % (outsideTemp, currentCondition)
	itunesPlugin.executeAction("pauseAndSay", deviceId=135305663, props={'message':spokenString})

Check the various plugin documentation for the necessary information and more examples.

FIXME Add examples for these:

  • refreshing objects
  • replacing objects
  • enabling/disabling objects
  • sprinkler control
indigo_7_documentation/plugin_scripting_tutorial.txt · Last modified: 2017/06/23 15:34 by jay
 

© Perceptive Automation, LLC. · Privacy