[JSR223/Jython] Example: Multi-device rule creation without repeating yourself

Howdy!

I’m really new here so posting a design pattern would be premature, but I feel this could be useful, so let’s just call it an example. The original technique is @scottk’s, I just distilled it down into a canonical example. I haven’t seen this clearly documented, so here goes.

When you need to perform the same actions for multiple devices, it’s nice to be able to reuse code without having to repeat yourself, thus improving maintainability and reducing the potential for mistakes.

The code below shows how to set up the same rules for multiple objects, while specifying each item identifier only once!

from core.rules import rule
from core.triggers import when

from core.log import logging, LOG_PREFIX
log = logging.getLogger(LOG_PREFIX + ".dehumidifiers")

class DeviceMon(object):
	def __init__(self, strItemName, strTitle):
		self.strItemName=strItemName	# this is the Item name as defined in your .items file
		self.strTitle=strTitle			# this is a descriptive title

		# this is how we programmatically create the rule. it's really this simple.
		# the self reference could be any object which has a __call__(self, event) method.
 		self.myrule=rule("DeviceMonHandler" + self.strTitle)(
 			when("Item %s received update" % self.strItemName)
 			(self))

	# this method gets called when the rule executes
	def __call__(self, event):
		log.debug("RULE %s TRIGGERED BY %s" % (self.strTitle,event.itemName))

	def cleanup(self):
		log.debug("Cleaning up %s handler" % self.strTitle)
		#cleanup code here



class DeviceMgr(object):
	def __init__(self):
		self.devices=list()

		# here's where we list our devices and create our individual objects in one fell swoop
		# notice how each identifier is only written once
		self.devices.append(DeviceMon("ItemIdentifier1","Aquarium Light"))
		self.devices.append(DeviceMon("ItemIdentifier2","Basement Light"))
		self.devices.append(DeviceMon("ItemIdentifier3","Ceiling Light"))
		self.devices.append(DeviceMon("ItemIdentifier4","Dungeon Light"))

	def cleanup(self):
		for x in self.devices:
			x.cleanup()


# instantiate the main object
mgr=DeviceMgr()



def scriptUnloaded():
	# this executes automatically when the script is unloaded/reloaded. if you create any threads or timers, make sure you cancel/stop them here!
	mgr.cleanup()



So yeah. That’s it. Way less painful than I thought it would be! Hope it’s useful to someone.

2 Likes

I like this! I have yet to convert the device status monitoring rules I have into JSR223, this will definitely help.

Would you be interested in putting this into the Helper Libraries documentation? If not, I would like to add it, credited to you of course. Currently we don’t have any Design Patterns on the new docs yet, but they are coming soon. I misspoke, we do have Design Patterns in the new docs, just not many yet.

I would suggest a minor change to use scriptLoaded as it is safer, it insures everything is loaded before running any code in your script. For cleanliness, I suggest garbage collection.

def scriptLoaded(id):
    # instantiate the main object
    mgr=DeviceMgr()



def scriptUnloaded():
	# this executes automatically when the script is unloaded/reloaded. if you create any threads or timers, make sure you cancel/stop them here!
	mgr.cleanup()
    del mgr

For my use, I would put all items to be handled into a group and iterate over the group in the init. Using the item name and item label.

I would love to, if you could explain the process to me :).

Also one more thing, a limitation I immediately ran into once I tried to use my own design pattern: I don’t know how to add multiple triggers to one rule yet this way. Perhaps someone could help extend it?

Thanks for the tips. I didn’t know python had a del keyword. I’ve been programming C++ for 20 years , and Python for like 2 weeks. :slight_smile:

You can make a PR against the repo with your example. Have a look in Sphinx/Examples/Design Patterns. Tonight might not be the best time, maybe clone the repo tomorrow, we are working out some kinks in the new docs we just published. We are using Sphinx and ReStructuredText for the new docs. If you are unfamiliar with these, feel free to write the content and then ask either myself or Scott for help getting it onto a page.

I was discussing with Scott and he pointed out that using a class for this purpose is unnecessary. The easiest way to do this would be using a group. Put all items in a group and use a rule similar to the following. This assumes you want to use the item label and name as identifiers. Also note the use of "string".format() instead of string % replace, it is preferred and less error prone.

@rule("DeviceMonHandler")
@when("Member of DeviceMonGroup received update")
def DeviceMonHandler(event):
    DeviceMonHandler.log.debug("RULE {} TRIGGERED BY {}".format(
        itemRegistry.getItem(event.itemName).label,
        event.itemName
    ))

Without having tested it, I think you can just chain them:
rule()when()when()(self)

PR = Pull Request?
I haven’t used either Sphinx or ReStructuredText. I may just create the content for now, after spending almost two weeks migrating my home to openHAB, I have to get back to doing my real job for a while. :slight_smile:

I’ve seen the group membership design pattern before, but I definitely wouldn’t say that it’s equivalent to a class registering its own rule. There’s no limit to the amount of scripting magic and custom logic you can do when the class has control over the registration.

“string”.format() is new to me too. Heck, I learned about string % replace just two days ago. Before that, I was using string1 + str(number) + string2… going back to my old basic days!

rule()when()when()(self) doesn’t work, but:

rule()(when()(when()(self))) works! Thank you.

I disagree, based on the example provided, but perhaps your actual use case does require it. Assuming though that every item needs an identical rule, you don’t need a class to arbitrate, just a rule that uses event.itemName instead of a hard-coded name.

You should definitely check out the Python docs for format, there’s a lot of cool tricks you can do with it. String concat works, but format makes things much easier to read.

I will remember that for next time! I haven’t even used the decorators like this yet.

Canonical example. :slight_smile:

But, I will say that your latest example is incredibly elegant where applicable – and I was just able to use it, completely eliminating needing a class at all! Thank you for that. Check this out