DaveL17 wrote:After thinking about your example above a little bit more, I find it even more intriguing. Unless I'm mistaken, this construction allows you to have one dispatcher method which would be able to handle many duties rather than having an individual callback method for each module. That's pretty slick.
It also demonstrates a difference between a degree in computer science and a degree in economics.
Here's a more full description of what I did for a plugin that has loadable actions (I think the pattern could be used for Events and possibly Devices though I haven't worked through those). The major goal was minimal change to my Plugin subclass (and plugin.py) for each collection of loadable actions. A secondary goal was to not have to edit the/a shared Actions.XML file but rather have the module do it all (the plugin I was retrofitting currently has an Actions.xml file for previous functionality which I also had to preserve). For a variety of reasons I wont' go into, I decided that a third goal was to make the module a single file rather than a directory, so the lower-level action definition had to be done in code. You could simplify somewhat and spread across multiple files if you skipped this requirement (code in one file, a separate Actions.xml file for that stuff, etc.).
I got it down to editing one list at the top of the plugin.py file (see the comment for why I left this requirement in). First, here's the parts that I implemented/added to my plugin.py:
- Code: Select all
# Add to the top - we need this to dynamically load the module
import importlib
# You must add the dynamic module to this list for it to get
# loaded at runtime. Conscious decision to make it explicit.
DYNAMIC_MODULES = [
'somedynamicmodule',
]
########################################
def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs):
# We could just look dynamically look for modules from the file system,
# but for a variety of reasons I decided to just load from a list
# of known modules.
self.module_map = {}
for module_name in DYNAMIC_MODULES:
self.module_map[module_name] = importlib.import_module(module_name)
# Call the base class init method
super(Plugin, self).__init__(pluginId, pluginDisplayName, pluginVersion, pluginPrefs)
# anything else you need in the init method
self.debug = True
########################################
# We implement this method so that after loading the Actions.xml file
# we can load actions from any dynamic modules. We didn't explicitly
# publish this method in our documentation because it's pretty
# low-level and we weren't sure if we wanted people overriding it,
# but in reality if we ever change this it's likely to require some significant
# change to existing plugins anyway.
########################################
def _parseActionsXML(self, filename):
# This will load the Actions.xml file. If you don't have one then theoretically
# you can skip this and just start max_sort_order at 0.
super(Plugin, self)._parseActionsXML(filename)
# When you add an action manually to self.actionsTypeDict you need to set
# the sort order, which has to be set and unique. So we pass in the next
# available ordering, and the method call returns what the max is after
# it's done its work so the next module will start at the right place.
max_sort_order = max([li["SortOrder"] for li in self.actionsTypeDict.itervalues()])
# Repeat with every dynamic module that we know about
for module in self.module_map.itervalues():
# Pass in the plugin instance and the next sort order, and what's
# returned is the next sort order.
max_sort_order = module.add_actions(self, max_sort_order + 1)
########################################
def validateActionConfigUi(self, valuesDict, typeId, devId):
self.logger.debug(u"Validating action config for type: " + typeId)
errorsDict = indigo.Dict()
# Split the type ID to get the first component before the '.' as the module
# to dispatch to, i.e. 'somedynamicmodule.execute_some_action'
try:
module_name, typeId = typeId.split(".")
module = self.module_map[module_name]
return module.validateActionConfigUi(self, valuesDict, typeId, devId)
except:
# The assumption here is that there is no validation for the given typeId
# so we just skip it. For more complex things you'll want to do whatever
# is appropriate.
pass
if len(errorsDict) > 0:
return (False, valuesDict, errorsDict)
return (True, valuesDict)
########################################
# External module support methods
########################################
# Call this method for all dynamic lists. the filter argument
# will be used to dispatch. Format the filter like this:
# module.method.filterstring
# Obviously, module and method name can't contain periods, but
# that's standard Python so shouldn't be a problem. filterstring
# can be omitted.
########################################
def dynamic_menu_generator(self, filter="", valuesDict=None, typeId="", targetId=0):
menu_tup = list()
if filter:
try:
module_str, remainder = filter.split(".", 1)
module = self.module_map[module_str]
if "." in remainder:
func, in_filter = remainder.split(".", 1)
else:
func = remainder
in_filter = ""
menu_tup = getattr(module, func, [])(
self,
filter=in_filter,
valuesDict=valuesDict,
typeId=typeId,
targetId=targetId,
)
except Exception as exc:
self.logger.error("menu_generator error:\n{}".format(traceback.format_exc(10)))
return menu_tup
########################################
# Menu Select dispatcher - uses the typeId to determine the
# module and method to call, i.e. somedynamicmodule.menu_select
# Note that this is the only place were we hard code the method
# name. I could have tried to get it from somewhere else (typeId
# or maybe valuesDict) but I decided that having just a single
# hardcoded method that all dynamic modules call for this purpose
# was acceptable.
########################################
def dynamic_menu_select(self, valuesDict, typeId="", devId=None):
try:
module_str, in_typeId = typeId.split(".", 1)
module = self.module_map[module_str]
return getattr(module, "menu_select")(
self,
valuesDict=valuesDict,
typeId=in_typeId,
devId=devId,
)
except Exception as exc:
self.logger.error("menu_select error:\n{}".format(traceback.format_exc(10)))
return valuesDict, indigo.Dict({"showAlertText": "A fatal error occurred. Contact the developer with the error from the Event Log window."})
########################################
# Action dispatcher - uses the typeId to determine the
# module and method to call, i.e. somedynamicmodule.execute_some_action
########################################
def dynamic_dispatcher(self, action, dev, callerWaitingForResult):
try:
module_name, method = action.pluginTypeId.split(".")
module = self.module_map[module_name]
return getattr(module, method, None)(self, action, dev, callerWaitingForResult)
except:
self.logger.warn(u"dynamic_dispatcher couldn't process action type '{}'".format(action.pluginTypeId))
So, once those changes are made to the plugin.py file, the only thing in it you'll ever need to change is the DYNAMIC_MODULES list at the top when you add a new module that defines actions.
A module will look like the following (in this instance, named somedynamicmodule.py):
- Code: Select all
import traceback
try:
import indigo
except:
pass
class AlertTextException(Exception):
pass
########################################
# Action Config UI XML
########################################
action_1_config_xml = u'''<?xml version='1.0' encoding='UTF-8'?>
<ConfigUI>
<Field id="dev_id" type="menu">
<Label>Device:</Label>
<List class="self" filter="somedynamicmodule.my_filter_method" method="dynamic_menu_generator" dynamicReload="true"/>
<CallbackMethod>dynamic_menu_select</CallbackMethod>
</Field>
<!-- Add all your other fields and SupportURL here -->
</ConfigUI>
'''
# Repeat with other actions
########################################
# Add actions to the appropriate menus
########################################
def add_actions(self, next_sort_order):
########################################
# action 1
########################################
# Create the action dictionary
action_dict = indigo.Dict()
action_dict[u"CallbackMethod"] = u"dynamic_dispatcher"
action_dict[u"ConfigUIRawXml"] = action_1_config_xml
action_dict[u"DeviceFilter"] = u""
action_dict[u"Name"] = u"Action 1 Name"
action_dict[u"UiPath"] = u"DeviceActions"
action_dict[u"SortOrder"] = next_sort_order
# Add the action dictionary to the actionsTypeDict
self.actionsTypeDict[u"somedynamicmodule.actionOne"] = action_dict
# Instead of using a single dynamic_dispatcher method, we could monkey patch the plugin instance 'self'
# to respond to the action method defined in this file, using this technique:
# setattr(self, action_dict[u"CallbackMethod"], types.MethodType(somedynamicmodule_actionOne, self))
# The downside to this approach is that you have to namespace the method name or you risk method
# name collisions and unexpected results. The down side to the single dynamic_dispatcher
# as implemented is that the action method name has to be the same as the actionTypeId - not a
# big deal I think.
next_sort_order += 1
# Repeat with other actions
########################################
# Device list generator
########################################
def my_filter_method(self, filter="", valuesDict=None, typeId="", targetId=0):
menu_list = list()
# Do whatever you need to generate the list for the menu/list element.
return menu_list
# Duplicate this for any other list generators defined in the ConfigXML above
########################################
# Menu select callback
########################################
def menu_select(self, valuesDict, typeId="", devId=None):
# Do whatever you need to do when a menu changes here.
# Note that typeId will have the action type in it so you can just
# look for that string in typeId.
return valuesDict
########################################
# Dialog Management callbacks
########################################
def validateActionConfigUi(self, valuesDict, typeId, devId):
self.logger.debug(u"Validating somedynamicmodule action config for type: " + typeId)
errorsDict = indigo.Dict()
descString = u""
try:
# Do your validation here. I defined the AlertTextException above
# which I can throw with text that will be shown in an Alert
# sheet in the UI when I want that to happen.
except AlertTextException as alertText:
errorsDict["showAlertText"] = unicode(alertText)
if len(errorsDict) > 0:
return (False, valuesDict, errorsDict)
valuesDict["description"] = descString
return (True, valuesDict)
########################################
# Action Callbacks
########################################
def actionOne(self, action, dev, callerWaitingForResult):
self.logger.debug("actionOne called")
# Do your action stuff here
# return as necessary
All the heavy lifting is done in this single file. You define the action, the action's config UI XML, callbacks, validation, and action implementation. One thing you might notice - the module file example above doesn't define any classes, so where do all those
self references come from? We pass the Plugin instance through on all those calls so that you can have access to everything in it (logger, methods, properties, etc).