Transitions library with timers in python

Tags: #<Tag:0x00007f616fee4760> #<Tag:0x00007f616fee4698> #<Tag:0x00007f616fee4030>

I’m currently using the latest stable version of OpenHAB and I’m running JSR223 python scripting for my rules. I started using the transitions library to develop some state machine driven rules and for the most part it’s going pretty well.

I ran into an issue today that I can not get past no matter how hard I try. I’ve created a class in Python using the transitions library:

from transitions import State, Machine
from org.joda.time import DateTime
from core.log import logging, LOG_PREFIX
from core.jsr223 import scope
from core.utils import post_update_if_different
from core.jsr223.scope import OnOffType, UnDefType
from core.actions import ScriptExecution
import time

from personal.SceneHelper import set_scene_ext

states = [
    State(name='unoccupied'),
    State(name='occupied'),
    State(name='occupied_timed'),
    State(name='occupied_latched'),
    State(name='warn')
]

transitions = [
    {'trigger':'motion', 'source':'unoccupied', 'dest':'occupied'},
    {'trigger':'no_motion', 'source':'occupied', 'dest':'occupied_timed'},
    {'trigger':'motion', 'source':'occupied_timed', 'dest':'occupied'},
    {'trigger':'no_motion', 'source':'occupied_timed', 'dest':'warn'},
    {'trigger':'motion', 'source':'warn', 'dest':'occupied'},
    {'trigger':'no_motion', 'source':'warn', 'dest':'unoccupied'},    
    {'trigger':'latch', 'source':['unoccupied','occupied','occupied_timed','warn'], 'dest':'occupied_latched'},
    {'trigger':'unlatch', 'source':'occupied_latched', 'dest':'occupied_timed'}
]

def timer_expired(self):
    self.logger.debug("Timer Expired")
    #self.motionTimer.cancel()
    #self.motionTimer = None
    self.no_motion()

motionTimers = {}

class OccupancySM(Machine):
    def __init__(self, room):
        self.sceneRoom = room
        self.logger = logging.getLogger("{}.{}_Occupancy".format(LOG_PREFIX, self.sceneRoom))
        self.motionTimer = None
        try:
            self.logger.debug("Getting occupancy state item: {}_Occupancy_State".format(self.sceneRoom))
            self.stateItem = scope.itemRegistry.getItem("{}_Occupancy_State".format(self.sceneRoom))
            self.logger.debug("Retrieved state item: {}".format(self.stateItem))
        except:
            self.logger.debug("Failed to retrieve occupancy state item.")
            self.stateItem = None

        Machine.__init__(self, name="{}_Occupancy_Machine".format(self.sceneRoom),states=states, initial='unoccupied', transitions=transitions)

    def on_enter_unoccupied(self):
        self.logger.debug("{} entering unoccupied state. Turning all items off.".format(self.sceneRoom))
        post_update_if_different("Office_Occupancy_State", "Unoccupied", False)
        roomLights = [item for item in scope.itemRegistry.getItem("g{}".format(self.sceneRoom)).members if "gLights" in item.groupNames]
        self.logger.debug("Retrieved following lights: {}".format(str(roomLights)[1:-1]))
        for light in roomLights:
            self.logger.debug("Turning off {}".format(str(light)))
            post_update_if_different(light, OnOffType.OFF, True)

    def on_enter_occupied(self):
        self.logger.debug("{} entering occupied state. Triggering occupancy items.".format(self.sceneRoom))
        post_update_if_different("Office_Occupancy_State", "Occupied")
        set_scene_ext(self.sceneRoom, "Occupancy", OnOffType.ON, self.logger)
        if self.sceneRoom in motionTimers:
            self.logger.debug("Timer running, cancelling...")
            motionTimers[self.sceneRoom].cancel()
            del motionTimers[self.sceneRoom]

    def on_enter_occupied_timed(self):
        
        self.logger.debug("{} entering timed occupied state. Starting timer.".format(self.sceneRoom))
        post_update_if_different("Office_Occupancy_State", "Occupied - Timed")
        itemRoomGroup = scope.itemRegistry.getItem("g{}".format(self.sceneRoom))
        itemRoom = self.sceneRoom

        # Gather timeout information - seconds
        timeoutSecondsItems = filter(lambda item:item.name == "{}_Occupancy_Timeout_Seconds".format(itemRoom), itemRoomGroup.members)
        if len(timeoutSecondsItems) > 0:
            timeoutSecondsItem = timeoutSecondsItems[0]
            self.logger.debug("Occupancy Timeout Seconds item {} found in state {}".format(timeoutSecondsItem.name, timeoutSecondsItem.state))
            if timeoutSecondsItem.state == UnDefType.NULL:
                self.logger.debug("Occupancy Timeout Seconds item in NULL state - using default setting")
                timeoutSecondsItem = scope.itemRegistry.getItem("DefaultSensorTimeout_Seconds")
        else:
            timeoutSecondsItem = scope.itemRegistry.getItem("DefaultSensorTimeout_Seconds")
            self.logger.debug("No Occupany Timeout Seconds item found - using default item {} in state {}".format(timeoutSecondsItem.name, timeoutSecondsItem.state))
        # Gather timeout information - minutes
        timeoutMinutesItems = filter(lambda item:item.name == "{}_Occupancy_Timeout_Minutes".format(itemRoom), itemRoomGroup.members)
        if len(timeoutMinutesItems) > 0:
            timeoutMinutesItem = timeoutMinutesItems[0]
            self.logger.debug("Occupancy Timeout Minutes item {} found in state {}".format(timeoutMinutesItem.name, timeoutMinutesItem.state))
            if timeoutMinutesItem.state == UnDefType.NULL:
                self.logger.debug("Occupancy Timeout Minutes item in NULL state - using default setting")
                timeoutMinutesItem = scope.itemRegistry.getItem("DefaultSensorTimeout_Minutes")
        else:
            timeoutMinutesItem = scope.itemRegistry.getItem("DefaultSensorTimeout_Minutes")
            self.logger.debug("No Occupany Timeout Minutes item found - using default item {} in state {}".format(timeoutMinutesItem.name, timeoutMinutesItem.state))

        #If one of the items is null just use a hard coded timeout
        self.logger.debug("Got items {item1} in state {state1} and {item2} in state {state2}".format(item1=timeoutSecondsItem, state1 = timeoutSecondsItem.state, item2=timeoutMinutesItem, state2=timeoutMinutesItem.state))
        if timeoutSecondsItem.state == UnDefType.NULL or timeoutMinutesItem.state == UnDefType.NULL:
            timeoutSeconds=0
            timeoutMinutes=15
        else:
            timeoutSeconds=timeoutSecondsItem.state.intValue()
            timeoutMinutes=timeoutMinutesItem.state.intValue()

        # Gather warn timeout information - seconds
        warnTimeoutSecondsItems = filter(lambda item:item.name == "{}_Occupancy_WarnTime_Seconds".format(itemRoom), itemRoomGroup.members)
        if len(warnTimeoutSecondsItems) > 0:
            warnTimeoutSecondsItem = warnTimeoutSecondsItems[0]
            self.logger.debug("Occupancy Warn Time Seconds item {} found in state {}".format(warnTimeoutSecondsItem.name, warnTimeoutSecondsItem.state))
            if warnTimeoutSecondsItem.state == UnDefType.NULL:
                self.logger.debug("Occupancy Warn Time Seconds item in NULL state - using default setting")
                warnTimeoutSecondsItem = scope.itemRegistry.getItem("DefaultWarnTimeout_Seconds")
        else:
            warnTimeoutSecondsItem = scope.itemRegistry.getItem("DefaultWarnTimeout_Seconds")
            self.logger.debug("No Occupany Warn Time Seconds item found - using default item {} in state {}".format(warnTimeoutSecondsItem.name, warnTimeoutSecondsItem.state))
        # Gather warn timeout information - minutes
        warnTimeoutMinutesItems = filter(lambda item:item.name == "{}_Occupancy_WarnTime_Minutes".format(itemRoom), itemRoomGroup.members)
        if len(warnTimeoutMinutesItems) > 0:
            warnTimeoutMinutesItem = warnTimeoutMinutesItems[0]
            self.logger.debug("Occupancy Warn Time Minutes item {} found in state {}".format(warnTimeoutMinutesItem.name, warnTimeoutMinutesItem.state))
            if warnTimeoutMinutesItem.state == UnDefType.NULL:
                self.logger.debug("Occupancy Warn Timeout Minutes item in NULL state - using default setting")
                warnTimeoutMinutesItem = scope.itemRegistry.getItem("DefaultWarnTimeout_Minutes")
        else:
            warnTimeoutMinutesItem = scope.itemRegistry.getItem("DefaultWarnTimeout_Minutes")
            self.logger.debug("No Occupany Warn Timeout Minutes item found - using default item {} in state {}".format(warnTimeoutMinutesItem.name, warnTimeoutMinutesItem.state))

        #If one of the items is null just use a hard coded timeout
        self.logger.debug("Got items {item1} in state {state1} and {item2} in state {state2}".format(item1=warnTimeoutSecondsItem, state1 = warnTimeoutSecondsItem.state, item2=warnTimeoutMinutesItem, state2=warnTimeoutMinutesItem.state))
        if warnTimeoutSecondsItem.state == UnDefType.NULL or warnTimeoutMinutesItem.state == UnDefType.NULL:
            warnTimeoutSeconds=30
            warnTimeoutMinutes=0
        else:
            warnTimeoutSeconds=warnTimeoutSecondsItem.state.intValue()
            warnTimeoutMinutes=warnTimeoutMinutesItem.state.intValue()

        # Calculate total timeout in seconds
        self.totalTimeoutSeconds=timeoutSeconds + (60 * timeoutMinutes)
        self.totalWarnTimeoutSeconds=warnTimeoutSeconds + (60 * warnTimeoutMinutes)
        if (self.totalWarnTimeoutSeconds < self.totalTimeoutSeconds):
            self.logger.debug("Sensor off: timeout in {timeout} seconds with warning at {warning} seconds to go".format(timeout=self.totalTimeoutSeconds, warning=self.totalWarnTimeoutSeconds))
            self.prewarnTimeout = self.totalTimeoutSeconds - self.totalWarnTimeoutSeconds
            self.logger.debug("Prewarn timeout: {}".format(str(self.prewarnTimeout)))
            if self.sceneRoom in motionTimers:
                motionTimers[self.sceneRoom].reschedule(DateTime.now().plusSeconds(self.prewarnTimeout))
                self.logger.debug("Reschedule Timer")
            else:
                motionTimers[self.sceneRoom] = ScriptExecution.createTimer(DateTime.now().plusSeconds(self.prewarnTimeout), lambda: self.no_motion())
                #self.no_motion()
        else:
            self.logger.debug("Warn timeout >= total timeout, skipping pre-warn")
            self.no_motion()
        
    def on_exit_occupied_timed(self):
        self.logger.debug("Exiting occupied-timed and deleting timer")
        if self.sceneRoom in motionTimers:
            self.logger.debug("Timer found, canceling")
            #motionTimers[self.sceneRoom].cancel()
            self.logger.debug("Deleting timer")
            del motionTimers[self.sceneRoom]

    def on_enter_warn(self):
        self.logger.debug("{} entering warning state. Starting timer and flashing lights.".format(self.sceneRoom))
        post_update_if_different("Office_Occupancy_State", "Occupied - Warning")

        roomGroupName = "g{}".format(self.sceneRoom)
        self.logger.debug("Getting lights in {}".format(roomGroupName))
        lightsToFlash = [item for item in scope.itemRegistry.getItem("g{}".format(self.sceneRoom)).allMembers if item in scope.itemRegistry.getItem("gLights").allMembers and not item in scope.itemRegistry.getItem("gLights").members]
        self.logger.debug("Lights to flash: {}".format(str(lightsToFlash)[1:-1]))

        lightsStates = {}
        for light in lightsToFlash:
            self.logger.debug("Storing light {}".format(str(light)))
            lightsStates[light.name] = light.state
            self.logger.debug("Turning light off")
            post_update_if_different(light, OnOffType.OFF, True)

        time.sleep(1)

        for light in lightsToFlash:
            self.logger.debug("Restoring {}".format(str(light)))
            post_update_if_different(light, lightsStates[light.name], True)

        
        if self.sceneRoom in motionTimers:
            motionTimers[self.sceneRoom].reschedule(DateTime.now().plusSeconds(self.totalWarnTimeoutSeconds))
        else:
            motionTimers[self.sceneRoom] = ScriptExecution.createTimer(DateTime.now().plusSeconds(self.totalWarnTimeoutSeconds), lambda: self.no_motion())

    def on_exit_warn(self):
        self.logger.debug("Exiting Warn and deleting timer")
        if self.sceneRoom in motionTimers:
            self.logger.debug("Timer found, canceling")
            #motionTimers[self.sceneRoom].cancel()
            self.logger.debug("Deleting timer")
            del motionTimers[self.sceneRoom]

I can make the transitions work, however when I added the timer to transition from the occupied_timed state to the warn state I get this error:

14:47:47.183 [DEBUG] [jsr223.jython.OccupancyTestRule      ] - TestSwitch received command OFF
14:47:47.185 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Office entering timed occupied state. Starting timer.
14:47:47.187 [DEBUG] [jsr223.jython.core.utils             ] - New postUpdate value for [Office_Occupancy_State] is [Occupied - Timed]
14:47:47.187 [INFO ] [smarthome.event.ItemStateChangedEvent] - Office_Occupancy_State changed from Occupied to Occupied - Timed
14:47:47.188 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Occupancy Timeout Seconds item Office_Occupancy_Timeout_Seconds found in state 10
14:47:47.189 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Occupancy Timeout Minutes item Office_Occupancy_Timeout_Minutes found in state 0.0
14:47:47.189 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Got items Office_Occupancy_Timeout_Seconds (Type=NumberItem, State=10, Label=Occupancy Timeout - Seconds, Category=null, Groups=[gOffice, gSettings]) in state 10 and Office_Occupancy_Timeout_Minutes (Type=NumberItem, State=0.0, Label=Occupancy Timdout - Minutes, Category=null, Groups=[gOffice, gSettings]) in state 0.0
14:47:47.190 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Occupancy Warn Time Seconds item Office_Occupancy_WarnTime_Seconds found in state 5
14:47:47.191 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Occupancy Warn Time Minutes item Office_Occupancy_WarnTime_Minutes found in state 0
14:47:47.191 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Got items Office_Occupancy_WarnTime_Seconds (Type=NumberItem, State=5, Label=Occupancy Warn Time - Seconds, Category=null, Groups=[gOffice, gSettings]) in state 5 and Office_Occupancy_WarnTime_Minutes (Type=NumberItem, State=0, Label=Occupancy Warn Time - Minutes, Category=null, Groups=[gOffice, gSettings]) in state 0
14:47:47.192 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Sensor off: timeout in 10 seconds with warning at 5 seconds to go
14:47:47.194 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Prewarn timeout: 5
14:47:47.195 [DEBUG] [jsr223.jython.OccupancyTestRule      ] - State: occupied_timed
14:47:47.195 [DEBUG] [re.automation.internal.RuleEngineImpl] - The rule '557dab0f-12d5-4666-9d43-da1ce2e1c17f' is executed.
14:47:47.195 [INFO ] [smarthome.event.RuleStatusInfoEvent  ] - 557dab0f-12d5-4666-9d43-da1ce2e1c17f updated: IDLE
14:47:51.214 [INFO ] [smarthome.event.ItemStateChangedEvent] - LivingRoom_OccupancySensor_Luminance changed from 25 to 37
14:47:52.197 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Exiting occupied-timed and deleting timer
14:47:52.198 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Timer found, canceling
14:47:52.198 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Deleting timer
14:47:52.199 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Office entering warning state. Starting timer and flashing lights.
14:47:52.200 [INFO ] [smarthome.event.ItemStateChangedEvent] - Office_Occupancy_State changed from Occupied - Timed to Occupied - Warning
14:47:52.200 [DEBUG] [jsr223.jython.core.utils             ] - New postUpdate value for [Office_Occupancy_State] is [Occupied - Warning]
14:47:52.201 [DEBUG] [jsr223.jython.Office_Occupancy       ] - Getting lights in gOffice
14:47:52.202 [ERROR] [org.quartz.core.JobRunShell          ] - Job DEFAULT.Timer 477 2020-08-07T14:47:52.194-04:00: <function <lambda> at 0x204> threw an unhandled Exception: 
org.python.core.PyException: null
	at org.python.core.PyException.doRaise(PyException.java:198) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1337) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1341) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1345) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1349) ~[?:?]
	at transitions.core$py._process$33(/etc/openhab2/automation/lib/python/transitions/core.py:427) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:153) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:423) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:141) ~[?:?]
	at transitions.core$py._trigger$32(/etc/openhab2/automation/lib/python/transitions/core.py:408) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:307) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:198) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:482) ~[?:?]
	at org.python.core.PyMethod.instancemethod___call__(PyMethod.java:237) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:228) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:223) ~[?:?]
	at org.python.modules._functools.PyPartial.partial___call__(PyPartial.java:124) ~[?:?]
	at org.python.modules._functools.PyPartial.__call__(PyPartial.java:79) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:445) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:449) ~[?:?]
	at transitions.core$py._process$80(/etc/openhab2/automation/lib/python/transitions/core.py:1128) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:153) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:423) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:141) ~[?:?]
	at transitions.core$py.trigger$31(/etc/openhab2/automation/lib/python/transitions/core.py:390) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:307) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:198) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:482) ~[?:?]
	at org.python.core.PyMethod.instancemethod___call__(PyMethod.java:237) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:228) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:223) ~[?:?]
	at org.python.modules._functools.PyPartial.partial___call__(PyPartial.java:124) ~[?:?]
	at org.python.modules._functools.PyPartial.__call__(PyPartial.java:79) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:445) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:449) ~[?:?]
	at personal.OccupancyStateMachine$py.f$11(/etc/openhab2/automation/lib/python/personal/OccupancyStateMachine.py:154) ~[?:?]
	at personal.OccupancyStateMachine$py.call_function(/etc/openhab2/automation/lib/python/personal/OccupancyStateMachine.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:124) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:403) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:398) ~[?:?]
	at org.python.core.PyFunction.invoke(PyFunction.java:533) ~[?:?]
	at com.sun.proxy.$Proxy344.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [bundleFile:?]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [bundleFile:?]
14:47:52.211 [ERROR] [org.quartz.core.ErrorLogger          ] - Job (DEFAULT.Timer 477 2020-08-07T14:47:52.194-04:00: <function <lambda> at 0x204> threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [bundleFile:?]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [bundleFile:?]
Caused by: org.python.core.PyException
	at org.python.core.PyException.doRaise(PyException.java:198) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1337) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1341) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1345) ~[?:?]
	at org.python.core.Py.makeException(Py.java:1349) ~[?:?]
	at transitions.core$py._process$33(/etc/openhab2/automation/lib/python/transitions/core.py:427) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:153) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:423) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:141) ~[?:?]
	at transitions.core$py._trigger$32(/etc/openhab2/automation/lib/python/transitions/core.py:408) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:307) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:198) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:482) ~[?:?]
	at org.python.core.PyMethod.instancemethod___call__(PyMethod.java:237) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:228) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:223) ~[?:?]
	at org.python.modules._functools.PyPartial.partial___call__(PyPartial.java:124) ~[?:?]
	at org.python.modules._functools.PyPartial.__call__(PyPartial.java:79) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:445) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:449) ~[?:?]
	at transitions.core$py._process$80(/etc/openhab2/automation/lib/python/transitions/core.py:1128) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:153) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:423) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:141) ~[?:?]
	at transitions.core$py.trigger$31(/etc/openhab2/automation/lib/python/transitions/core.py:390) ~[?:?]
	at transitions.core$py.call_function(/etc/openhab2/automation/lib/python/transitions/core.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:307) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:198) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:482) ~[?:?]
	at org.python.core.PyMethod.instancemethod___call__(PyMethod.java:237) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:228) ~[?:?]
	at org.python.core.PyMethod.__call__(PyMethod.java:223) ~[?:?]
	at org.python.modules._functools.PyPartial.partial___call__(PyPartial.java:124) ~[?:?]
	at org.python.modules._functools.PyPartial.__call__(PyPartial.java:79) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:445) ~[?:?]
	at org.python.core.PyObject.__call__(PyObject.java:449) ~[?:?]
	at personal.OccupancyStateMachine$py.f$11(/etc/openhab2/automation/lib/python/personal/OccupancyStateMachine.py:154) ~[?:?]
	at personal.OccupancyStateMachine$py.call_function(/etc/openhab2/automation/lib/python/personal/OccupancyStateMachine.py) ~[?:?]
	at org.python.core.PyTableCode.call(PyTableCode.java:167) ~[?:?]
	at org.python.core.PyBaseCode.call(PyBaseCode.java:124) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:403) ~[?:?]
	at org.python.core.PyFunction.__call__(PyFunction.java:398) ~[?:?]
	at org.python.core.PyFunction.invoke(PyFunction.java:533) ~[?:?]
	at com.sun.proxy.$Proxy344.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	... 1 more

I have a simple test rule that triggers the transitions:

from core.rules import rule
from core.triggers import when
from core.utils import post_update_if_different
from core.jsr223.scope import OnOffType
import time

import personal.OccupancyStateMachine
reload(personal.OccupancyStateMachine)
from personal.OccupancyStateMachine import OccupancySM

office_occupancy = OccupancySM("Office")

@rule("OccupancyTestRule")
@when("Item TestSwitch received command")
def occupancy_test_rule(event):
    occupancy_test_rule.log.debug("TestSwitch received command {}".format(event.itemCommand))

    if event.itemCommand == ON:
        office_occupancy.motion()
        occupancy_test_rule.log.debug("State: {}".format(office_occupancy.state))
    else:
        office_occupancy.no_motion()
        occupancy_test_rule.log.debug("State: {}".format(office_occupancy.state))
        #office_occupancy.no_motion()
        #occupancy_test_rule.log.debug("State: {}".format(office_occupancy.state))
        #office_occupancy.no_motion()
        #occupancy_test_rule.log.debug("State: {}".format(office_occupancy.state))
        #time.sleep(10)
        #occupancy_test_rule.log.debug("State: {}".format(office_occupancy.state))

If I take out the line of code towards the end of the on_enter_occupied_timed state I can manual cause the state machine to transition from one state to the next and everything behaves exactly as I expect it to. This is the line of code I remove:

`motionTimers[self.sceneRoom] = ScriptExecution.createTimer(DateTime.now().plusSeconds(self.prewarnTimeout), lambda: self.no_motion())`

But as soon as I put it back in I get the above error. I really can’t figure out what’s causing the error and it’s such a non-descript error that has very little diagnistic help that I can’t figure out what’s causing it. I’ve found that if I don’t call no_motion() from the timer lambda I don’t get the error but I also don’t get the transition.

I know this is not a topic that’s addressed a lot here and it’s fairly specific so if I don’t get many suggestions I’ll understand but I was hoping someone might have some pointers before I give up and try to go a different route. I was hoping to make this a class so I can instantiate it for any room I want to use it in.

Help is always appreciated.

Thanks!
Matthew