Invalid callback in cancel_state_callback() from app

Hi,

I am getting this WARNING in my appdaemon log when I try to cancel a scheduled callback

WARNING AppDaemon: Invalid callback in cancel_state_callback() from app Statetest

Full log for this app:

2021-01-21 12:37:12.583508 INFO AppDaemon: Reloading Module: /conf/apps/media_pc/statetest.py
2021-01-21 12:37:12.591522 INFO AppDaemon: Initializing app Statetest using class StateTest from module statetest
2021-01-21 12:37:12.607983 DEBUG Statetest: Calling listen_state for Statetest
2021-01-21 12:39:14.304393 DEBUG Statetest: entity='input_boolean.debug_switch_1'
2021-01-21 12:39:14.307285 DEBUG Statetest: new='off'
2021-01-21 12:39:14.615904 DEBUG Statetest: entity='input_boolean.debug_switch_1'
2021-01-21 12:39:14.618998 DEBUG Statetest: new='on'
2021-01-21 12:39:14.622331 DEBUG Statetest: NEW: wait_for------------------------
2021-01-21 12:39:14.625554 DEBUG Statetest: Waiting for input_boolean.debug_switch_3
2021-01-21 12:39:14.627681 DEBUG Statetest: Calling listen_state for Statetest
2021-01-21 12:39:14.631267 DEBUG Statetest: Registering run_in in 5 seconds for Statetest
2021-01-21 12:39:20.014568 INFO Statetest: Cancelling wait_for. Timeout reached.
2021-01-21 12:39:20.018098 DEBUG Statetest: Canceling listen_state for Statetest
2021-01-21 12:39:20.018749 WARNING AppDaemon: Invalid callback in cancel_state_callback() from app Statetest
2021-01-21 12:39:20.024326 DEBUG Statetest: 'on_timeout' not set to continue and wait_for timed out: stopping the sequence

I am hesitant to call this a bug, but according to google I am the only one with this problem and I can’t for the life of me figure out why it is happening. So I thought before I open an issue on the github issue tracker I’d first ask here.

yaml:

Statetest:
  module: statetest
  class: StateTest
  DEBUG: 1
  input_boolean: input_boolean.debug_switch_1
  actions:
    - wait_for:
        entity_id: input_boolean.debug_switch_3 #entity to match
        new: "on" #continue if state is this
        immediate: True #if True: check if entity already in state. If False: wait for state change to new
        timeout: 5 #wait for max this time
        unit: seconds
        on_timeout: stop #if we waited for max_time and state still no match: continue or stop?
  global_dependencies:
    - globals
  dependencies:
    - global_vars

py:

import hassapi as hass
import globals
import copy
class StateTest(hass.Hass):
    def initialize(self):
        self.listen_state_handle_list = []
        self.listen_event_handle_list = []
        self.timer_handle_list = []
        self.global_vars = self.get_app('global_vars')
        self.debug_mode = globals.get_arg(self.args, "DEBUG")
        self.ib = globals.get_arg(self.args, "input_boolean")
        self.actions = globals.get_arg(self.args, "actions")
        self._wait_for_cb_handle = None
        self.wait_for_timeout_cb_handle = None
        if self.debug_mode:
            self.set_log_level('DEBUG')
        #add handler for input boolean change
        self.state_changed_cb_handle = self.listen_state(self.state_changed_cb, self.ib)
        self.listen_state_handle_list.append(self.state_changed_cb_handle)
    
    def state_changed_cb(self, entity, attribute, old, new, kwargs):
        self.debug(f"{entity=}")
        self.debug(f"{new=}")
        if new == "on":
            if self.actions:
                actions = self.actions
                actions_copy = copy.copy(actions)
                self.run_sequence({"actions" : actions_copy})
    def run_sequence(self, kwargs):
        actions = kwargs.pop("actions", [])
        while actions:
            action = actions.pop(0)
            service, params = next(iter(action.items())) #get first (and in this case only) key in dict
            self.debug(f"NEW: {service}------------------------")
            if service == "wait_for":
                kwargs['actions'] = actions
                self.wait_for(**params, **kwargs)
                break
            else:
                self.call_service(service, **params)
    def wait_for(self, **kwargs):
        entity = kwargs.pop("entity_id")
        self.debug(f"Waiting for {entity}")
        self._wait_for_cb_handle = self.listen_state(self._wait_for_cb, entity=entity, **kwargs)
        if "timeout" in kwargs: #don't wait forever
            unit = kwargs.pop("unit", "seconds")
            timeout = self.seconds_to_unit(kwargs['timeout'], unit)
            kwargs["on_timeout"] = kwargs.pop("on_timeout", "continue")
            self.wait_for_timeout_cb_handle = self.run_in(self.wait_for_timeout_cb, timeout, **kwargs)
            self.timer_handle_list.append(self.wait_for_timeout_cb_handle)
    def wait_for_timeout_cb(self, kwargs):
        self.log("Cancelling wait_for. Timeout reached.")
        if self._wait_for_cb_handle:
            self.cancel_listen_state(self._wait_for_cb_handle)
        if kwargs["on_timeout"] == "continue":
            self.debug("Continuing.")
            self.run_sequence(kwargs)
        else:
            self.debug("'on_timeout' not set to continue and wait_for timed out: stopping the sequence.")
                
    def _wait_for_cb(self, entity, attribute, old, new, kwargs):
        self.log(f"wait_for_callback has been activated for entity {entity}")
        if self.wait_for_timeout_cb_handle:
            self.cancel_timer(self.wait_for_timeout_cb_handle)
        self.run_sequence(kwargs)
    def seconds_to_unit(self, seconds, unit):
        if unit == "seconds":
            res = seconds
        elif unit == "minutes":
            res = seconds * 60
        elif unit == "hours":
            res = seconds * 60 * 60
        elif unit == "days":
            res = seconds * 24 * 60 * 60
        return res
    def debug(self, message):
        if self.debug_mode == 1:
            self.log(message, level='DEBUG')
    def terminate(self):
        for listen_state_handle in self.listen_state_handle_list:
            self.cancel_listen_state(listen_state_handle)
        for listen_event_handle in self.listen_event_handle_list:
            self.cancel_listen_event(listen_event_handle)
        for timer_handle in self.timer_handle_list:
            self.log(f"{timer_handle=}")
            self.cancel_timer(timer_handle)

This is a MWE so that I can clarify the issue, the actual app will have more code. Can anybody see why cancelling the callback for the function _wait_for_cb fails in this line:

self.cancel_listen_state(self._wait_for_cb_handle)

?