From b59d70ca5765370f92546418eb6269fcddc14c2e Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Fri, 4 Sep 2020 18:53:12 +0800 Subject: [PATCH] feat: add customisable grace_period paramater to cater for long latencies in control entities --- README.md | 17 +++++++++++++++ .../entity_controller/__init__.py | 21 ++++++++++++------- custom_components/entity_controller/const.py | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 03fc1f3..14e343c 100644 --- a/README.md +++ b/README.md @@ -542,6 +542,23 @@ By default, any attribute change is considered significant and will qualify for - color_temp ``` +## Troubleshooting + +### EC goes into blocked state +Check how long it takes your control entities to actually turn on after the service call is dispatched. The default `grace_period` is 2 seconds. +This means EC will make the call to turn on all entities and then it will ignore any state updates that come in for 2 seconds (default). The reason for this is that without this grace period, EC would actually block itself because it would interpret the state updates as manual control. + +Unfortunately, it is not possible to detect *who* called a service in Home Assistant. If that was the case we could simply ignore any service calls originating from EC itself. + +To solve your issue with EC ending up in `blocked` state, you can take a look at increasing the `grace_period` to something like 5 seconds. If your lights have a lot of latency then increasing this period will most likely resolve your issue. + +```yaml +grace_period_ec: + sensor: binary_sensor.living_room_motion + entity: light.tv_backlight + grace_period: 5 # default is 2 +``` + # Debugging ## Enabling Debug Logging diff --git a/custom_components/entity_controller/__init__.py b/custom_components/entity_controller/__init__.py index 4720e3a..cb5ae27 100644 --- a/custom_components/entity_controller/__init__.py +++ b/custom_components/entity_controller/__init__.py @@ -94,7 +94,8 @@ CONF_NIGHT_MODE, CONF_STATE_ATTRIBUTES_IGNORE, CONSTRAIN_START, - CONSTRAIN_END + CONSTRAIN_END, + CONF_IGNORE_STATE_CHANGES_UNTIL ) from .entity_services import ( @@ -143,6 +144,7 @@ vol.Optional(CONF_TRIGGER_ON_DEACTIVATE, default=None): cv.entity_ids, vol.Optional(CONF_STATE_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_BLOCK_TIMEOUT, default=None): cv.positive_int, + vol.Optional(CONF_IGNORE_STATE_CHANGES_UNTIL, default=None): cv.positive_int, vol.Optional(CONF_NIGHT_MODE, default=None): MODE_SCHEMA, vol.Optional(CONF_STATE_ATTRIBUTES_IGNORE, default=[]): cv.ensure_list, vol.Optional(CONF_SERVICE_DATA, default=None): vol.Coerce( @@ -302,7 +304,7 @@ async def async_setup(hass, config): trigger="control", source="active_timer", dest="idle", - conditions=["is_state_entities_off"], + conditions=["is_state_entities_off"] ) machine.add_transition(trigger='control', source='active_timer', dest='blocked', conditions=['is_state_entities_on']) @@ -457,6 +459,7 @@ def __init__(self, hass, config, machine, entity): self.config = ( {} ) # new way of storing configuration (avoids having an attribue for each) + self.config = config self.debug_day_length = config.get("day_length", None) self.stateEntities = [] self.controlEntities = [] @@ -618,12 +621,11 @@ def state_entity_state_change(self, entity, old, new): + str(a) ) if self.is_active_timer(): - if datetime.now() > self.ignore_state_changes_until: # check if we are within the grace period after making a service call (this avoids EC blocking itself) + if self.is_within_grace_period(): # check if we are within the grace period after making a service call (this avoids EC blocking itself) + self.log.debug("state_entity_state_change :: This state change is within %i seconds of calling a service. Ignoring this state change because its probably caused by EC itself." % self.config.get(CONF_IGNORE_STATE_CHANGES_UNTIL, 2)) + else: self.log.debug("state_entity_state_change :: We are in active timer and the state of observed state entities changed.") - self.control() - else: - self.log.debug("state_entity_state_change :: This state change is within 2 seconds of calling a service. Ignoring this state change because its probably caused by EC itself.") if self.is_blocked() or self.is_active_stay_on(): # if statement required to avoid MachineErrors, cleaner than adding transitions to all possible states. self.enable() @@ -706,6 +708,11 @@ def _override_entity_state(self): def is_override_state_off(self): return self._override_entity_state() is None + def is_within_grace_period(self): + """ Dtermines if the last service call EC made was within the last 2 seconds. + This is important or else EC will react to state changes caused by EC itself which results in going into blocked state.""" + return datetime.now() < self.ignore_state_changes_until + def is_override_state_on(self): return self._override_entity_state() is not None @@ -1442,7 +1449,7 @@ def prepare_service_data(self): def call_service(self, entity, service, **kwargs): """ Helper for calling HA services with the correct parameters """ self.log.debug("call_service :: Calling service " + service + " on " + entity) - self.ignore_state_changes_until = datetime.now() + timedelta(seconds=2) + self.ignore_state_changes_until = datetime.now() + timedelta(seconds=self.config.get(CONF_IGNORE_STATE_CHANGES_UNTIL, 2)) self.log.debug("call_service :: Setting ignore_state_changes_until to " + str(self.ignore_state_changes_until)) domain, e = entity.split(".") diff --git a/custom_components/entity_controller/const.py b/custom_components/entity_controller/const.py index b51043e..3d4e1bc 100644 --- a/custom_components/entity_controller/const.py +++ b/custom_components/entity_controller/const.py @@ -79,3 +79,4 @@ STATES = ['idle', 'overridden', 'constrained', 'blocked', {'name': 'active', 'children': ['timer', 'stay_on'], 'initial': False}] +CONF_IGNORE_STATE_CHANGES_UNTIL = "grace_period"