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 ) # =====================================================