From 1701a882455f67f660f3e2909a02d3ab5c9c3d35 Mon Sep 17 00:00:00 2001 From: Jan Pluskal Date: Fri, 24 Jul 2026 09:47:21 +0200 Subject: [PATCH] fix(state-machine): allow blocked from idle at start_time; never raise from the callback start_time_callback calls blocked()/enable() regardless of current state. When the machine is idle at start_time (HA restarted inside the active window, or an override toggled off while constrained) blocked() raised MachineError, and HA re-fires a failed point-in-time callback endlessly, flooding the log. - add `blocked: idle -> blocked` transition (mirrors the existing sensor_on: idle -> blocked rule) - extract _apply_start_time_transition() and guard it with try/except MachineError, so states with no defined transition log a warning and keep their state instead of raising out of the callback --- .../entity_controller/__init__.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/custom_components/entity_controller/__init__.py b/custom_components/entity_controller/__init__.py index 777b86d..578f5ba 100644 --- a/custom_components/entity_controller/__init__.py +++ b/custom_components/entity_controller/__init__.py @@ -43,6 +43,7 @@ import homeassistant.util.yaml.objects as YamlObjects import homeassistant.util.uuid as uuid_util from transitions import Machine +from transitions.core import MachineError from transitions.extensions import HierarchicalMachine as Machine from homeassistant.helpers.service import async_call_from_config @@ -370,6 +371,13 @@ async def async_setup(hass, config): ) # Enter blocked state when component is enabled and entity is on machine.add_transition(trigger="blocked", source="constrained", dest="blocked", conditions=["is_block_enabled"]) + # blocked must also be reachable from idle: start_time_callback fires while + # the machine is already idle (HA restarted inside the active window, or an + # override toggled off before start_time) and calls blocked() when state + # entities are on. Without this transition that raises MachineError and HA + # keeps re-firing the failed point-in-time callback (error storm). Mirrors + # the existing sensor_on: idle -> blocked rule. + machine.add_transition(trigger="blocked", source="idle", dest="blocked", conditions=["is_block_enabled"]) for myconfig in config[DOMAIN]: _LOGGER.info("Domain Configuration: " + str(myconfig)) @@ -1261,14 +1269,32 @@ def start_time_callback(self, evt): self.update(start_time=parsed_start) - if self.is_state_entities_on() and self.is_block_enabled(): - self.blocked() - else: - # If the entity is on and block is disabled, we just transition from constrained - # to idle and leave the entity on. (Don't start a timer to turn it off.) - self.enable() + self._apply_start_time_transition() self.do_transition_behaviour(CONF_ON_EXIT_CONSTRAINED) + def _apply_start_time_transition(self): + """Transition the machine when start_time is reached. + + Must never raise: an exception escaping a point-in-time callback makes + HA re-fire it endlessly (error storm). States with no defined + transition from here (e.g. overridden/active after a mid-window HA + restart) are logged and left unchanged -- the next sensor/override + event resolves the controller normally. + """ + try: + if self.is_state_entities_on() and self.is_block_enabled(): + self.blocked() + else: + # If the entity is on and block is disabled, we just transition from constrained + # to idle and leave the entity on. (Don't start a timer to turn it off.) + self.enable() + except MachineError as err: + self.log.warning( + "start_time_callback :: no transition from state '%s' (%s); leaving state unchanged", + self.state, + err, + ) + # ===================================================== # H E L P E R F U N C T I O N S ( N E W ) # =====================================================