Plugin Callback to Another Class

Posted on
Sun Aug 05, 2018 5:33 am
DaveL17 offline
User avatar
Posts: 6753
Joined: Aug 20, 2013
Location: Chicago, IL, USA

Plugin Callback to Another Class

I've been doing some noodling and it seems that a callback can only call the Plugin class. I may have missed something, but these don't work for me:

Code: Select all
class Tester(object):
    def __init__(self):
        # do stuff

    def test(self):
        #do stuff

Code: Select all
<CallbackMethod>Tester().test</CallbackMethod>
<CallbackMethod>Tester.test</CallbackMethod>

Code: Select all
   Error (client)                  performRequestMethod() caught exception: InvalidParameterError -- plugin does not define method Tester.test

The obvious workaround is to have a method in the Plugin class that then calls the Tester class. I just want to make sure that I'm right that it's not possible to call another class directly and that I haven't missed something in the syntax.

I came here to drink milk and kick ass....and I've just finished my milk.

[My Plugins] - [My Forums]

Posted on
Sun Aug 05, 2018 10:28 am
jay (support) offline
Site Admin
User avatar
Posts: 18220
Joined: Mar 19, 2008
Location: Austin, Texas

Re: Plugin Callback to Another Class

Correct - our internal dispatching assumes methods in the Plugin class.

My pattern when doing stuff like this is to create a dispatcher method for each method which uses something else to determine where it goes. For instance, if it's an action call, I use the action type to specify it. In one plugin, I use this as the action type ID: "module.method_name"

then at runtime I split the string myself into module and method parts. Then I can dynamically call the method/function:

Code: Select all
    def dynamic_dispatcher(self, action, dev, callerWaitingForResult):
        try:
            module_name, method = action.pluginTypeId.split(".")
            module = self.module_map[module_name]  # I dynamically load modules
            return getattr(module, method, None)(self, action, dev, callerWaitingForResult)
        except:
            self.logger.warn(u"dynamic_dispatcher couldn't process action type '{}'".format(action.pluginTypeId))


and for menu generators I have used the filter parameter ("module.method.filter part here") and split it accordingly.

Obviously it would be nice to just allow the callbacks to contain the module as well, but until we get around to that this works very well.

Jay (Indigo Support)
Twitter | Facebook | LinkedIn

Posted on
Sun Aug 05, 2018 11:29 am
DaveL17 offline
User avatar
Posts: 6753
Joined: Aug 20, 2013
Location: Chicago, IL, USA

Re: Plugin Callback to Another Class

Thanks Jay - this is a low-order issue for sure, but it's good to know that I wasn't shooting in the wrong direction.

I came here to drink milk and kick ass....and I've just finished my milk.

[My Plugins] - [My Forums]

Posted on
Sun Aug 05, 2018 12:08 pm
jay (support) offline
Site Admin
User avatar
Posts: 18220
Joined: Mar 19, 2008
Location: Austin, Texas

Re: Plugin Callback to Another Class

It's actually a fun/interesting design pattern - how to modularize the various plugin components (devices, actions, etc) into individual dynamic modules. I did some work in this direction for 7.2 to support the special actions that are HomeSeer specific (controlling/configuring the LEDs on the new W*200 switches). It should make adding brand/manufacturer specific actions somewhat easier for us moving forward (at the very least the dynamic nature reduces the regression bug threat/testing somewhat).

Jay (Indigo Support)
Twitter | Facebook | LinkedIn

Posted on
Sun Aug 05, 2018 12:20 pm
DaveL17 offline
User avatar
Posts: 6753
Joined: Aug 20, 2013
Location: Chicago, IL, USA

Re: Plugin Callback to Another Class

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. :D

I came here to drink milk and kick ass....and I've just finished my milk.

[My Plugins] - [My Forums]

Posted on
Sun Aug 05, 2018 2:40 pm
Colorado4Wheeler offline
User avatar
Posts: 2794
Joined: Jul 20, 2009
Location: Colorado

Re: Plugin Callback to Another Class

That's one of the big things I did in HKB 2.0 is modularize the entire program. The plugin.py module really just serves as a launch pad to get into a system where each device and action has its own class module and interacts with each other, as needed, by using the parent plugin class. This allows each action, device, etc to exist in it's own little "plugin world" and I have found it a million times easier to code and work with rather than piling it all into the plugin.py.

My Modest Contributions to Indigo:

HomeKit Bridge | Device Extensions | Security Manager | LCD Creator | Room-O-Matic | Smart Dimmer | Scene Toggle | Powermiser | Homebridge Buddy

Check Them Out Here

Posted on
Mon Aug 06, 2018 11:41 am
jay (support) offline
Site Admin
User avatar
Posts: 18220
Joined: Mar 19, 2008
Location: Austin, Texas

Re: Plugin Callback to Another Class

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. :D


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

Jay (Indigo Support)
Twitter | Facebook | LinkedIn

Page 1 of 1

Who is online

Users browsing this forum: No registered users and 2 guests

cron