Transitions library with timers in python

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

Hi Matthew

Did you find the problem ?

I am seeing the same problem in my setup, using the same range of tools that you are using. I just started to investigate.

regards Thomas

Just to follow up on my own problems. I my case it was python errors in the callback-function of various kinds.

I can really recommend using the @log_traceback decorations on all the member functions, that is how I found my issues.

from core.log import log_traceback

class MyClass(object):
    @log_traceback
    def myFunction(self):
        pass

regards Thomas

I never did solve the error directly. I think there’s a scope issue somewhere but I’m not sure where. I ended up moving the timers outside the state machine and put them in the rules that were triggering the state changes. It’s not really the way I wanted to solve it but I couldn’t find a way to make it work. And it doesn’t seem anyone else is attempting the same thing so I just decided to find a workaround.

If you would like to see my workaround I can show you but all I did really was just move the timers out of the class and put them in the rules triggering state changes. If you find the cause of the issue let me know, I’d be interested in the results.

So did you find a way to use timers in your classes? That’s where my issues came from. The same code worked fine when I took it out of the timer, but as soon as the timer is used to call the code it fails. I really just think there’s a scoping issue somewhere. I can verify that the code works exactly as I want it to otherwise, I can even call the no_motion() method from a rule outside the class and the code works fine. As soon as I call the no_motion() method from a timer in the class, it fails. Same code, just called from a different location.

If you have a way around it I’d be interested. I can test with the log_traceback decoration then and see what happends.

Yes, I did make it work with a timer in the class.

I used a timer like this:
self.timer = ScriptExecution.createTimer(DateTime.now().plusSeconds(4), lambda: self.timer_callback_fun())

With the decoration I also learned that in order to use sendCommand and getItems (from a method in the Class) I need to import events and itemRegistry like this:

from core.jsr223.scope import itemRegistry, events

class MyClass(object):
    def myFunc(self):
        my_item = itemRegistry.getItem("item_id")
        ...
        events.sendCommand("xxx")

Hope that it helps.

1 Like

I meant to reply to this a while ago but I did implement the log_traceback decorators and tested out some code and was able to find my issues and get things working. Thanks for the info, it was really helpful.

@mladams922 and @thwang - Thanks for the above.

I’m about to embark on an ambitious finite state machine project focused on home automation industrial process control type stuff. Like both of you, this means I want timers to trigger state changes.

I am wondering if either of you has some bandwidth to help me with advice and possibly some template demo code. I can pay a reasonable hourly consulting rate.

More about the project.
My team (mostly me at this point) is creating an open-source python project that runs and optimizes the performance of residential heat pump thermal storage heating systems. We’re funded by Efficiency Maine. (Here is a picture of our second install partly under way). We are really excited, because the thermal storage allows the heat pump to beat the cost of oil even with pretty high electricity rates (we do have access to a time of use tariff). Our open source home is here but I don’t recommend trying to go through it yet - still a major work in progress. We are aiming for a launch of next spring for the open source.