diff --git a/changelog.md b/changelog.md index 4295f24..b1e33ad 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ ## Bugfix - only use items (instead item names) for ``habapp_rules.core.helper.send_if_different`` +- cancel timer / timeouts of replaced rules # Version 5.2.1 - 17.10.2023 diff --git a/habapp_rules/core/state_machine_rule.py b/habapp_rules/core/state_machine_rule.py index ec8d1e6..9050cd6 100644 --- a/habapp_rules/core/state_machine_rule.py +++ b/habapp_rules/core/state_machine_rule.py @@ -2,6 +2,7 @@ import inspect import os import pathlib +import threading import HABApp import HABApp.openhab.connection.handler.func_sync @@ -34,6 +35,7 @@ def __init__(self, state_item_name: str | None = None, state_item_label: str | N :param state_item_name: name of the item to hold the state :param state_item_label: OpenHAB label of the state_item; This will be used if the state_item will be created by HABApp """ + self.state_machine: transitions.Machine | None = None HABApp.Rule.__init__(self) # get prefix for items @@ -83,3 +85,11 @@ def _set_state(self, state_name: str) -> None: def _update_openhab_state(self) -> None: """Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine.""" self._item_state.oh_send_command(self.state) + + def on_rule_removed(self) -> None: + """Override this to implement logic that will be called when the rule has been unloaded.""" + # stop timeout timer of current state + if self.state_machine: + for itm in self.state_machine.states[self.state].runner.values(): + if isinstance(itm, threading.Timer) and itm.is_alive(): + itm.cancel() diff --git a/habapp_rules/system/presence.py b/habapp_rules/system/presence.py index 3afcd1e..bde4925 100644 --- a/habapp_rules/system/presence.py +++ b/habapp_rules/system/presence.py @@ -193,3 +193,11 @@ def __set_leaving_through_phone(self) -> None: self._instance_logger.debug("Leaving was set, because last phone left some time ago.") self.leaving_detected() self.__phone_absence_timer = None + + def on_rule_removed(self) -> None: + habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed(self) + + # stop phone absence timer + if self.__phone_absence_timer: + self.__phone_absence_timer.cancel() + self.__phone_absence_timer = None diff --git a/tests/core/state_machine_rule.py b/tests/core/state_machine_rule.py index d69ae01..bf323bd 100644 --- a/tests/core/state_machine_rule.py +++ b/tests/core/state_machine_rule.py @@ -1,5 +1,6 @@ """Unit-test for state_machine.""" import collections +import time import unittest import unittest.mock @@ -62,3 +63,34 @@ def test_update_openhab_state(self): with unittest.mock.patch.object(self._state_machine, "_item_state") as state_item: self._state_machine._update_openhab_state() state_item.oh_send_command.assert_called_once_with("some_state") + + def test_on_rule_removed(self): + """Test on_rule_removed.""" + # check if 'on_rule_removed' is still available in HABApp + getattr(HABApp.rule.Rule, "on_rule_removed") + + # check if timer is stopped correctly + states = [ + {"name": "stopped"}, + {"name": "running", "timeout": 99, "on_timeout": "trigger_stop"} + ] + + with unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.string_item.StringItem("rules_common_state_machine_rule_StateMachineRule_state", "")): + for initial_state in ["stopped", "running"]: + state_machine_rule = habapp_rules.core.state_machine_rule.StateMachineRule() + + state_machine_rule.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout( + model=state_machine_rule, + states=states, + ignore_invalid_triggers=True) + + state_machine_rule._set_state(initial_state) + + if initial_state == "running": + self.assertTrue(list(state_machine_rule.state_machine.states["running"].runner.values())[0].is_alive()) + + state_machine_rule.on_rule_removed() + + if initial_state == "running": + time.sleep(0.001) + self.assertFalse(list(state_machine_rule.state_machine.states["running"].runner.values())[0].is_alive()) diff --git a/tests/system/presence.py b/tests/system/presence.py index f4de17e..fb9ea60 100644 --- a/tests/system/presence.py +++ b/tests/system/presence.py @@ -42,7 +42,8 @@ def setUp(self) -> None: tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Presence_Unittest_Presence_state", "") tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Presence", "ON") - self._presence = habapp_rules.system.presence.Presence("Unittest_Presence", outside_door_names=["Unittest_Door1", "Unittest_Door2"], leaving_name="Unittest_Leaving", phone_names=["Unittest_Phone1", "Unittest_Phone2"], name_state="CustomState") + self._presence = habapp_rules.system.presence.Presence("Unittest_Presence", outside_door_names=["Unittest_Door1", "Unittest_Door2"], leaving_name="Unittest_Leaving", phone_names=["Unittest_Phone1", "Unittest_Phone2"], + name_state="CustomState") @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed") def test_create_graph(self): # pragma: no cover @@ -392,3 +393,20 @@ def test_phones(self): # timeout is over -> absence expected tests.helper.timer.call_timeout(self.transitions_timer_mock) self.assertEqual(self._presence.state, "absence") + + def test_on_rule_removed(self): + """Test on_rule_removed.""" + # timer NOT running + with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove: + self._presence.on_rule_removed() + + parent_on_remove.assert_called_once() + + # timer running + self._presence._Presence__phone_absence_timer = threading.Timer(42, unittest.mock.MagicMock()) + self._presence._Presence__phone_absence_timer.start() + with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove: + self._presence.on_rule_removed() + + parent_on_remove.assert_called_once() + self.assertIsNone(self._presence._Presence__phone_absence_timer)