python-statemachine icon indicating copy to clipboard operation
python-statemachine copied to clipboard

FRQ: special state for error handling

Open AlexMKX opened this issue 1 year ago • 1 comments

  • Python State Machine version: 2.3.1

Description

Sometime action can't be completed and we need sort of cleanup code, which can be run after any action. In my case (for irrigation automation) : confirm the valve is closed and closed it if its not.

Idea to solve: Ability to issue special "from any state" transition to the cleanup handling final state. Specify the error handler in FSM configuration. Specify which states can be handled by the handler.

The current workaround

# the decorator to enter functions which could be handled
def catch_errors(handler=None):
    def actual_decorator(func):
        @wraps(func)
        async def _wrapper(*args, **kwargs):
            try:
                return await func(*args, **kwargs)
            except Exception as e:
                if handler:
                    return await handler(*args, **kwargs)
                return None

        return _wrapper

    return actual_decorator

class WorkItem(StateMachine):
    created = State('created', initial=True)
    open = State('open')
    opened = State('opened')
    close = State('close')
    closed = State('closed', final=True)
    error = State('error', final=True)
    do_work = (created.to(open) |
               open.to(opened, cond="is_open") |
               opened.to(close, cond="can_close") |
               close.to(closed, cond="is_closed"))

# the error transition map
    do_error = (created.to(error) | open.to(error) | opened.to(error) | close.to(error))

# this will be sent if error (e.g. exception in enter handler occur)
    async def on_error(self, *args, **kwargs):
        await self.async_send("do_error")

# the decorated handler
    @catch_errors(handler=on_error)
    async def on_enter_created(self):
        self._app.log(f"Entering 'created' state.")
        if not self.is_closed():
            async with asyncio.timeout(30):
                self._app.error(f"Valve {self.zone.valve} is already open, closing it")
                # this will throw exception
                await self._app.call_service('homeassistant/turn_off1', entity_id=self.zone.valve)

# cleanup handler
    async def on_enter_error(self):
        self._app.error(f"Error in {self.zone.valve} {self.zone.moisture}")
        try:
            async with asyncio.timeout(60):
                self._app.log(f"Closing valve {self.zone.valve} because of error")
                await self._app.call_service('homeassistant/turn_off', entity_id=self.zone.valve)
                while not self.is_closed():
                    await asyncio.sleep(1)
        finally:
            self._app.error(
                f"Error in {self.zone.valve} {self.zone.moisture} the valve status is {self.zone.get_valve_state()}")

AlexMKX avatar Jun 28 '24 15:06 AlexMKX

Hi @AlexMKX , how are you? Thanks for your suggestion. I think that in the end this relates to https://github.com/fgmacedo/python-statemachine/issues/386, as an alternative implementation solving the same issue.

Do you believe that some kind of a "finalize event handler" may solve your issue as well?

fgmacedo avatar Sep 09 '24 15:09 fgmacedo