The problem
I’ve always found reusing of rules cumbersome. Unless your item names match the ones from the rule that you’re trying to copy, it’s a lot of search 'n replace of those item names to match yours. Then there is the problem that the rule may use items that you didn’t define and you need to reverse engineer the rule to define those items. As a result I hardly ever copy any rules and due to time constraints and/or lack of interest to put in the effort my smarthome isn’t as smart as it could be.
When someone on the forum asks for help how to implement a certain rule often some friendly forum member steps in and gives some pointers on how the rule could be implemented or just creates a rule for that person. It seems that the wheel gets reinvented many times.
I figured there must be a better way to do this, and indeed there is.
The solution
The solution to this problem is to extract the business logic out of the rule and putting it in its own class. This class will then form the base class from which you derive your own rule class. The base class can implement some default behavior which you can override in your derived rule class.
The base class can easily be shared and since it does not contain any items it does not need to be modified by whomever wants to derive rules from it. This does not mean that you cannot work with items in your base class. You simply provide the item names in the constructor which then get mapped to class members. Using the item names you simply get the item from the item registry (see the example provided below).
We could create a repository on Github where different base classes could be collected in a community maintained library making it easy to pull in new base classes, updates and bug fixes.
I successfully implemented this concept using Jython, but it may work for Groovy and JavaScript as well.
I’m still experimenting with this concept but I would like to share this idea to trigger ideas and get feedback on things that could be improved. I’m making use of the openhab2-jython library and I’ll be the first to admit that (beyond the basics) I don’t have a clue how this library works. For example I couldn’t get the ItemStateChangeTrigger triggers to work and therefor used the method provided by the Raw ESH Automation API. The library is flexible enough to support this.
Also, I mainly program in Java hence my style of programming Python isn’t really pythonic.
An example
Let’s say you want to dim your lights when you play a movie using Kodi, dim to a different level when you pause the movie so that you can actually see where you’re going when getting some popcorn and drinks, and restore the light to a normal level when the movie ends.
This all sound pretty easy to implement. The Kodi binding offers a control channel which provides the status of the Kodi player which can be mapped to a Player item. When the movie starts playing the state becomes PLAY, when the movie pauses the state becomes PAUSE, and when the movie stops the status becomes PAUSE… eh say what? That’s right. When the Kodi player stops, the state becomes PAUSE because the openHAB Player item has no STOP state (I guess the binding needs to set the state to NULL or UNDEF here), so based on the state of this channel there is no way to differentiate whether the movie is paused or whether it is stopped.
Next problem, you hardly use Kodi to play music. One evening you have visitors and talk about music. You decide to startup Kodi to play some music and then the lights dim. That was unexpected… since your rule triggers on PLAY state of the Player item your lights dimmed. You didn’t foresee that next to the Player item you should also take into account the mediatype of the playing media.
Wouldn’t it be nice if someone else had already found these issues and worked around them and made it darn simple while doing so?
Enough talk, let’s see how this all works.
The rule to implement
The following is all you need to implement to fix all these quirks that you didn’t think about when you implemented your rule (hypothetically speaking of course, I know you already worked around all these issues ).
First import all required modules
import community.kodi
reload(community.kodi)
from org.slf4j import LoggerFactory
import openhab
from community.kodi import KodiPlayerState
Next define the rule class. Note that we’re not using SimpleRule
as the baseclass, instead we use KodiPlayerState
as the baseclass.
The KodiPlayerState
baseclass contains all the business logic that we no longer have to worry about. For brevity I left out the logging in the method calls but while developing you probably want to include some log statements to know what’s going on.
class KodiRule(KodiPlayerState):
def __init__(self, controlItem, mediatypeItem):
KodiPlayerState.__init__(self, controlItem, mediatypeItem)
self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiRule")
The base class contains some default methods. If you do not override any methods then depending on the type of media you play the following methods will be called (from left to right):
onPlayChannel() | ||
onPlayEpisode() | ||
onPlayMovie() | onPlayVideo() | |
onPlayMusicVideo() | onPlay() | |
onPlayPicture() | ||
onPlayRadio() | onPlayAudio() | |
onPlaySong() |
So if you would like to switch the lights off when watching a movie and dim to 10% when watching an episode then you would override onPlayMovie()
to switch the lights off and onPlayEpisode()
to dim the lights to 10%. Now when you would like to get a drink you probably don’t care whether you’re watching a movie or an episode. You just want to see where you’re going and therefor you would like to dim the lights to 20%. You could override both onPauseMovie()
and onPauseEpisode()
and dim the lights to 20% in each of these methods. However, you would be repeating yourself, so instead of overriding these two methods simply override only one method, the onPlayVideo()
method and dim the lights to 20%.
Switching TV channels is still troublesome. The state changes from PLAY
to PAUSE
to STOP
to PLAY
. You don’t want to dim you’re lights every time when you zap from one channel to the next so we override them here as well. In this example we don’t want anything to happen when we pause or stop either a musicvideo or a picture either so we override these methods as well.
May be it would have been easier to just override onPauseMovie()
, onStopMovie()
, onPauseEpisode()
, and onStopEpisode()
. Then you wouldn’t have had to override all the channel, musicvideo, and picture methods Then again this is an example so it’s good to show what’s possible
# overridden methods
def onPlayEpisode(self):
# dim lights to 10%
events.postUpdate("Light_GF_LivingRoom_Front", "10")
def onPlayMovie(self):
# switch lights off
events.postUpdate("Light_GF_LivingRoom_Front", "OFF")
def onPauseVideo(self):
# dim lights to 20%
events.postUpdate("Light_GF_LivingRoom_Front", "20")
def onStopVideo(self):
# dim lights to 70%
events.postUpdate("Light_GF_LivingRoom_Front", "70")
def onPlaySong(self):
# enable disco lights
events.postUpdate("DiscoLight_GF_LivingRoom", "ON")
# start spinning disco ball
events.postUpdate("DiscoBall_GF_LivingRoom", "ON")
def onStopSong(self):
# disable disco lights
events.postUpdate("DiscoLight_GF_LivingRoom", "OFF")
# stop spinning disco ball
events.postUpdate("DiscoBall_GF_LivingRoom", "OFF")
def onPauseChannel(self):
pass
def onStopChannel(self):
pass
def onPauseMusicVideo(self):
pass
def onStopMusicVideo(self):
pass
def onPausePicture(self):
pass
def onStopPicture(self):
pass
Finally add the rule using the automationManager
. Note that here we provide item names to the KodiRule
constructor which will pass these names on to the KodiPlayerState
constructor. It is of course also possible to instead hardcode these into your rule when calling the KodiPlayerState.__init__()
method. I just like doing it this way.
# add your Kodi Control Channel and Kodi Media Type Channel items
automationManager.addRule(KodiRule("Kodi_Laptop_control", "Kodi_Laptop_mediatype"))
For the fun of it, let’s create another rule for Kodi in the bedroom. Here we’ll only dim the lights when playing a movie.
import community.kodi
reload(community.kodi)
from org.slf4j import LoggerFactory
import openhab
from community.kodi import KodiPlayerState
class KodiBedroomRule(KodiPlayerState):
def __init__(self, controlItem, mediatypeItem):
KodiPlayerState.__init__(self, controlItem, mediatypeItem)
self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiBedroomRule")
# overridden methods
def onPlayMovie(self):
# dim lights to 10%
events.postUpdate("Light_FF_Bedroom", "10")
def onPauseMovie(self):
# dim lights to 20%
events.postUpdate("Light_FF_Bedroom", "20")
def onStopMovie(self):
# dim lights to 70%
events.postUpdate("Light_FF_Bedroom", "70")
automationManager.addRule(KodiBedroomRule("Kodi_Bedroom_control", "Kodi_Bedroom_mediatype"))
So there you have it, a two very simple rules with no business logic at all. Just some simple methods that get called when the particular event occurs.
The base class
The base class needs to be stored in your python path. I guess most examples use the automation/lib/python
directory. This is the same directory where you also created the openhab
directory which contains the openhab2-jython library. In this automation/lib/python
directory create the community
directory.
In the community
directory first create an empty file with the name __init__.py
. Next create the kodi.py
file and copy in the following contents:
import time
from org.slf4j import LoggerFactory
from org.eclipse.smarthome.automation.core.util import TriggerBuilder
from org.eclipse.smarthome.config.core import Configuration
#import openhab
from openhab import items
from openhab.jsr223.scope import scriptExtension
scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")
from openhab.jsr223.scope import SimpleRule
class KodiPlayerState(SimpleRule):
# control states
PAUSE = "PAUSE"
PLAY = "PLAY"
# control and mediatype state
UNDEF = "UNDEF"
# mediatype states
CHANNEL = "channel"
EPISODE = "episode"
MOVIE = "movie"
MUSICVIDEO = "musicvideo"
PICTURE = "picture"
RADIO = "radio"
SONG = "song"
def __init__(self, controlItem, mediatypeItem, delay=.200):
self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiPlayerState")
self.logger.info("Initializing KodiPlayerState")
self.controlItem = controlItem
self.mediatypeItem = mediatypeItem
self.delay = delay
self.triggers = [
TriggerBuilder.create()
.withId("trigger_" + controlItem)
.withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(
Configuration({
"itemName": controlItem
})).build(),
TriggerBuilder.create()
.withId("trigger_" + mediatypeItem)
.withTypeUID("core.ItemStateChangeTrigger")
.withConfiguration(
Configuration({
"itemName": mediatypeItem
})).build()
]
def execute(self, module, input):
# self.logger.info("KodiPlayerStateRule triggered")
oldState = str(input["oldState"])
newState = str(input["newState"])
mediatype = str(items[self.mediatypeItem])
# tv channel
# Unfortunately for tv channels, this is not perfect.
# When switching from one channel to the next this will cause multiple state changes,
# first to PAUSE, then to STOP, and finally to PLAY again, hence you probably don't
# want to dim your lights based on these state changes.
if oldState == KodiPlayerState.CHANNEL and newState == KodiPlayerState.UNDEF:
self.onStopChannel()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.CHANNEL:
self.onPlayChannel()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.CHANNEL:
self.onPlayChannel()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.CHANNEL:
time.sleep(delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.CHANNEL:
self.onPauseChannel()
# episode from the library
elif oldState == KodiPlayerState.EPISODE and newState == KodiPlayerState.UNDEF:
self.onStopEpisode()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.EPISODE:
self.onPlayEpisode()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.EPISODE:
self.onPlayEpisode()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.EPISODE:
time.sleep(self.delay)
# When media stops playing, its state in openHAB is set to PAUSE. Therefor we add a little delay
# to give openHAB a chance to update the mediatype to UNDEF. If the media type is indeed UNDEF
# then kodi has stopped playing the media, otherwise it did indeed pause the media
if items[self.mediatypeItem].toString() == KodiPlayerState.EPISODE:
self.onPauseEpisode()
# movie from the library
elif oldState == KodiPlayerState.MOVIE and newState == KodiPlayerState.UNDEF:
self.onStopMovie()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.MOVIE:
self.onPlayMovie()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.MOVIE:
self.onPlayMovie()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.MOVIE:
time.sleep(self.delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.MOVIE:
self.onPauseMovie()
# musicvideo from the library
elif oldState == KodiPlayerState.MUSICVIDEO and newState == KodiPlayerState.UNDEF:
self.onStopMusicVideo()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.MUSICVIDEO:
self.onPlayMusicVideo()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.MUSICVIDEO:
self.onPlayMusicVideo()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.MUSICVIDEO:
time.sleep(self.delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.MUSICVIDEO:
self.onPauseMusicVideo()
# picture
elif oldState == KodiPlayerState.PICTURE and newState == KodiPlayerState.UNDEF:
self.onStopPicture()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.PICTURE:
self.onPlayPicture()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.PICTURE:
self.onPlayPicture()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.PICTURE:
time.sleep(self.delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.PICTURE:
self.onPausePicture()
# radio channel
elif oldState == KodiPlayerState.RADIO and newState == KodiPlayerState.UNDEF:
self.onStopRadio()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.RADIO:
self.onPlayRadio()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.RADIO:
self.onPlayRadio()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.RADIO:
time.sleep(self.delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.RADIO:
self.onPauseRadio()
# song
elif oldState == KodiPlayerState.SONG and newState == KodiPlayerState.UNDEF:
self.onStopSong()
elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.SONG:
self.onPlaySong()
elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.SONG:
self.onPlaySong()
elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.SONG:
time.sleep(self.delay)
if items[self.mediatypeItem].toString() == KodiPlayerState.SONG:
self.onPauseSong()
# Overridable methods
def onPlayChannel(self):
self.onPlayVideo()
def onPauseChannel(self):
self.onPauseVideo()
def onStopChannel(self):
self.onStopVideo()
def onPlayEpisode(self):
self.onPlayVideo()
def onPauseEpisode(self):
self.onPauseVideo()
def onStopEpisode(self):
self.onStopVideo()
def onPlayMovie(self):
self.onPlayVideo()
def onPauseMovie(self):
self.onPauseVideo()
def onStopMovie(self):
self.onStopVideo()
def onPlayMusicVideo(self):
self.onPlayVideo()
def onPauseMusicVideo(self):
self.onPauseVideo()
def onStopMusicVideo(self):
self.onStopVideo()
def onPlayPicture(self):
self.onPlayVideo()
def onPausePicture(self):
self.onPauseVideo()
def onStopPicture(self):
self.onStopVideo()
def onPlayRadio(self):
self.onPlayAudio()
def onPauseRadio(self):
self.onPauseAudio()
def onStopRadio(self):
self.onStopAudio()
def onPlaySong(self):
self.onPlayAudio()
def onPauseSong(self):
self.onPauseAudio()
def onStopSong(self):
self.onStopAudio()
def onPlayAudio(self):
self.onPlay()
def onPauseAudio(self):
self.onPause()
def onStopAudio(self):
self.onStop()
def onPlayVideo(self):
self.onPlay()
def onPauseVideo(self):
self.onPause()
def onStopVideo(self):
self.onStop()
def onPlay(self):
pass
def onPause(self):
pass
def onStop(self):
pass
That’s all. If you now start a song in the living room then your disco lights turn on and your disco ball starts spinning. Party time!
As you can see in the execute
method of the base class, I cast the newState
, oldState
, and mediatypeItem
states such as UNDEF
, PLAY
, and PAUSE
to a string. I have not yet figured out how I can use these states as openHAB types such as for example PlayerType.PLAY
.
In the rule file (so not the base class file) you’ll notice that I have a reload(community.kodi)
statement. This forces openHAB to reload the base class whenever the file containing the rule is changed. This is helpful when updating the base class file since openHAB does not automatically reload the base class file when it has been updated. After updating the base class file I simply add or remove a new line to the rule file and save it which will then cause openHAB to compile and reload the base class file.
Of course polymorphism, inheritance and all that OOP stuff is nothing new. However, I’m pretty excited how well it works when applied to creating openHAB rules. I’d love to hear your thoughts on this.