From aa8dab97d68637f587051d835094726db8059974 Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Fri, 10 Jul 2020 21:54:45 +0800 Subject: [PATCH] feat: Transition Behaviours Extended (#159) * Update __init__.py * Update const.py * Update __init__.py * Update const.py * Update __init__.py * Update __init__.py * license * feat: clean up Co-authored-by: Daniel Mason --- README.md | 33 ++- .../entity_controller/__init__.py | 224 ++++++++++++------ custom_components/entity_controller/const.py | 34 ++- .../entity_controller/entity_services.py | 17 ++ 4 files changed, 233 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index b3a3e98..7596615 100644 --- a/README.md +++ b/README.md @@ -258,16 +258,41 @@ Transition Behaviours allow you to define what EC should do at these transition **Use Case:** > One use case for this is when lights turn on just before the end of the active period and then they never turn off because EC is constrained. For me, the light would be triggered by me walking into a room at -7am and the constrain period would begin at 7:16am. Since the light duration is 20 minutes, EC will never turn off the light because it is in `constrained` state at 7:20am. This is annoying because the lights stay on all day. I can use `end_time_action: "off"` to turn off all lights at 7:16am. +7am and the constrain period would begin at 7:16am. Since the light duration is 20 minutes, EC will never turn off the light because it is in `constrained` state at 7:20am. This is annoying because the lights stay on all day. I can use `on_enter_constrained: "off"` to turn off all lights at 7:16am (when the EC is constrained). +``` +transition_behaviours: # light triggers on then off + entities: + - input_boolean.lamp + sensors: + - sensor.motion_sensor + overrides: + - input_boolean.override_switch + delay: 5 + behaviours: + on_enter_constrained: "on" + start_time: sunset + end_time: "7:16:00" +``` +**Notes about light flicker:** This can cause light flicker particularly for the `on_exit_*` behaviours. If your lights flicker, make sure you understand how this configuration is impacting them and use the `on_enter_*` behaviours instead. + #### Supported transition points: -More will be added in the future. +The following behaviours can be defined using the values `"on"`, `"off"` and the default being `"ignore"`. These must be in quotes to avoidconflicts with YAML truth values. + |Key|Description| |---|---| -|start_time_action|Triggered at the beginning of active period| -|end_time_action|Triggered at the end of active period| +|on_enter_idle|| +|on_exit_idle|| +|on_enter_active|| +|on_exit_active|| +|on_enter_overidden|| +|on_exit_overidden|| +|on_enter_constrained|| +|on_exit_constrained|| +|on_enter_blocked|| +|on_exit_blocked|| #### Supported transition behaviours: You must put these in quotes. diff --git a/custom_components/entity_controller/__init__.py b/custom_components/entity_controller/__init__.py index f6744b3..0e3d621 100644 --- a/custom_components/entity_controller/__init__.py +++ b/custom_components/entity_controller/__init__.py @@ -1,4 +1,21 @@ """ +This file is part of Entity Controller. + +Entity Controller is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Entity Controller is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Entity Controller. If not, see . + +""" +""" Entity controller component for Home Assistant. Maintainer: Daniel Mason Version: v6.0.0 @@ -12,6 +29,7 @@ import re from datetime import date, datetime, time, timedelta from threading import Timer +import pprint import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -35,11 +53,22 @@ CONF_START_TIME, CONF_END_TIME, - CONF_END_TIME_ACTION, - CONF_START_TIME_ACTION, CONF_TRANSITION_BEHAVIOUR_ON, CONF_TRANSITION_BEHAVIOUR_OFF, CONF_TRANSITION_BEHAVIOUR_IGNORE, + + # Behaviours + CONF_BEHAVIOURS, + CONF_ON_ENTER_IDLE, + CONF_ON_EXIT_IDLE, + CONF_ON_ENTER_ACTIVE, + CONF_ON_EXIT_ACTIVE, + CONF_ON_ENTER_OVERRIDDEN, + CONF_ON_EXIT_OVERRIDDEN, + CONF_ON_ENTER_CONSTRAINED, + CONF_ON_EXIT_CONSTRAINED, + CONF_ON_ENTER_BLOCKED, + CONF_ON_EXIT_BLOCKED, SENSOR_TYPE_DURATION, SENSOR_TYPE_EVENT, @@ -105,9 +134,6 @@ vol.Optional(CONF_SENSOR_TYPE, default=SENSOR_TYPE_EVENT): vol.All( vol.Lower, vol.Any(SENSOR_TYPE_EVENT, SENSOR_TYPE_DURATION) ), - vol.Optional(CONF_END_TIME_ACTION, default=CONF_TRANSITION_BEHAVIOUR_IGNORE): vol.All( - vol.Lower, vol.Any(CONF_TRANSITION_BEHAVIOUR_ON, CONF_TRANSITION_BEHAVIOUR_OFF, CONF_TRANSITION_BEHAVIOUR_IGNORE) - ), vol.Optional(CONF_SENSOR_RESETS_TIMER, default=False): cv.boolean, vol.Optional(CONF_SENSOR, default=[]): cv.entity_ids, vol.Optional(CONF_SENSORS, default=[]): cv.entity_ids, @@ -121,10 +147,14 @@ vol.Optional(CONF_STATE_ATTRIBUTES_IGNORE, default=[]): cv.ensure_list, vol.Optional(CONF_SERVICE_DATA, default=None): vol.Coerce( dict - ), # Default must be none because we differentiate between set and unset + ), + vol.Optional(CONF_BEHAVIOURS, default=None): vol.Coerce( + dict + ), + # Default must be none because we differentiate between set and unset vol.Optional(CONF_SERVICE_DATA_OFF, default=None): vol.Coerce(dict), }, - extra=vol.ALLOW_EXTRA, + # extra=vol.ALLOW_EXTRA, ) PLATFORM_SCHEMA = cv.schema_with_slug_keys(ENTITY_SCHEMA) @@ -184,6 +214,7 @@ async def async_setup(hass, config): ) # re-entering self-transition (on_enter callback executed.) # Overridden + # machine.add_transition(trigger='enable', source='overridden', dest='idle') machine.add_transition( trigger="enable", source="overridden", @@ -210,7 +241,6 @@ async def async_setup(hass, config): conditions=["is_state_entities_on", "is_duration_sensor", "is_sensor_off"], ) - # machine.add_transition(trigger='sensor_off', source=['overridden'], dest=None) machine.add_transition( trigger="enter", source="active", dest="active_timer", unless="will_stay_on" @@ -248,8 +278,7 @@ async def async_setup(hass, config): dest="idle", conditions=["is_duration_sensor", "is_sensor_off"], ) - # machine.add_transition(trigger='block_timer_expires', source='blocked', - # dest='idle') + # machine.add_transition(trigger='block_timer_expires', source='blocked', dest='idle') machine.add_transition( trigger="block_timer_expires", source="blocked", @@ -268,6 +297,8 @@ async def async_setup(hass, config): dest="idle", conditions=["is_state_entities_on", "is_duration_sensor", "is_sensor_off"], ) + + # Active Timer machine.add_transition( trigger="control", source="active_timer", @@ -300,7 +331,7 @@ async def async_setup(hass, config): if not config: config = {} - _LOGGER.info("Config Item %s: %s", str(key), str(config)) + # _LOGGER.info("Config Item %s: %s", str(key), str(config)) config["name"] = key m = None m = EntityController(hass, config, machine) @@ -334,9 +365,8 @@ def __init__(self, hass, config, machine): self.model = Model(hass, config, machine, self) except AttributeError as e: _LOGGER.error( - "Configuration error! Please ensure you use plural keys for lists. e.g. sensors, entities" + "Configuration error! Please ensure you use plural keys for lists. e.g. sensors, entities" + e ) - raise e event.async_call_later(hass, 1, self.do_update) @property @@ -450,7 +480,9 @@ def __init__(self, hass, config, machine, entity): self.log.debug( "Initialising EntityController entity with this configuration: " - + str(config) + ) + self.log.debug( + pprint.pformat(config) ) self.name = config.get(CONF_NAME, "Unnamed Entity Controller") self.log.debug("Controller name: " + str(self.name)) @@ -465,6 +497,7 @@ def __init__(self, hass, config, machine, entity): ) # must come after config_control_entities (uses control entities if not set) self.config_sensor_entities(config) self.config_override_entities(config) + self.config_transition_behaviours(config) self.config_off_entities(config) self.config_on_entities(config) self.config_normal_mode(config) @@ -502,8 +535,8 @@ def finalize(self): @callback def sensor_state_change(self, entity, old, new): """ State change callback for sensor entities """ - self.log.debug("%10s Sensor state change to: %s" % (entity, new.state)) - self.log.debug("state: " + self.state) + self.log.debug("%10s Sensor state change to: %s" % ( pprint.pformat(entity), new.state)) + self.log.debug("state: " + pprint.pformat(self.state)) if self.matches(new.state, self.SENSOR_ON_STATE) and ( self.is_idle() or self.is_active_timer() or self.is_blocked() @@ -561,29 +594,56 @@ def state_entity_state_change(self, entity, old, new): ) # This can be called with either a state change or an attribute change. If the state changed, we definitely want to handle the transition. If only attributes changed, we'll check if the new attributes are significant (i.e., not being ignored). try: - if old.state == new.state: # Only attributes changed - # Build two dictionaries of attributes, excluding the ones we don't want to monitor - old_temp = { - key: old.attributes[key] - for key in old.attributes - if key not in self.state_attributes_ignore - } - new_temp = { - key: new.attributes[key] - for key in new.attributes - if key not in self.state_attributes_ignore - } - if old_temp == new_temp: - self.log.debug("insignificant attribute only change") - return - self.log.debug("significant attribute only change") + # kept in case it causes problems + # if old.state == new.state: # Only attributes changed + # # Build two dictionaries of attributes, excluding the ones we don't want to monitor + # old_temp = { + # key: old.attributes[key] + # for key in old.attributes + # if key not in self.state_attributes_ignore + # } + # new_temp = { + # key: new.attributes[key] + # for key in new.attributes + # if key not in self.state_attributes_ignore + # } + # if old_temp == new_temp: + # self.log.debug("insignificant attribute only change") + # return + # self.log.debug("significant attribute only change") + # except AttributeError as a: + # Most likely one of the states, either new or old, is 'off', so there's no attributes dict attached to the state object. + # self.log.debug( + # "Most likely one of the states, either new or old, is 'off', so there's no attributes dict attached to the state object: " + # + str(a) + # ) + # pass + + if not old or not new or old == 'off' or new == 'off': + pass + else: + if old.state == new.state: # Only attributes changed + # Build two dictionaries of attributes, excluding the ones we don't want to monitor + old_temp = { + key: old.attributes[key] + for key in old.attributes + if key not in self.state_attributes_ignore + } + new_temp = { + key: new.attributes[key] + for key in new.attributes + if key not in self.state_attributes_ignore + } + if old_temp == new_temp: + self.log.debug("insignificant attribute only change") + return + self.log.debug("significant attribute only change") except AttributeError as a: # Most likely one of the states, either new or old, is 'off', so there's no attributes dict attached to the state object. self.log.debug( "Most likely one of the states, either new or old, is 'off', so there's no attributes dict attached to the state object: " + str(a) ) - pass if self.is_active_timer(): self.control() @@ -758,14 +818,19 @@ def does_sensor_reset_timer(self): # ===================================================== def on_enter_idle(self): self.log.debug("Entering idle") - self.turn_off_control_entities() + self.do_transition_behaviour(CONF_ON_ENTER_IDLE) self.entity.reset_state() def on_exit_idle(self): self.log.debug("Exiting idle") + self.do_transition_behaviour(CONF_ON_EXIT_IDLE) def on_enter_overridden(self): self.log.debug("Now overridden") + self.do_transition_behaviour(CONF_ON_ENTER_OVERRIDDEN) + + def on_exit_overridden(self): + self.do_transition_behaviour(CONF_ON_EXIT_OVERRIDDEN) def on_enter_active(self): self.update(last_triggered_at=str(datetime.now())) @@ -775,7 +840,7 @@ def on_enter_active(self): self._start_timer() self.log.debug("light params before turning on: " + str(self.lightParams)) - self.turn_on_control_entities() + self.do_transition_behaviour(CONF_ON_ENTER_ACTIVE) self.enter() def on_exit_active(self): @@ -784,31 +849,58 @@ def on_exit_active(self): self.update( delay=self.lightParams.get(CONF_DELAY) ) # no need to update immediately + self.do_transition_behaviour(CONF_ON_EXIT_ACTIVE) def on_enter_blocked(self): self.update(blocked_at=datetime.now()) self.update(blocked_by=self._state_entity_state()) + self.do_transition_behaviour(CONF_ON_ENTER_BLOCKED) if self.block_timeout: self.block_timer_handle = Timer(self.block_timeout, self.block_timer_expire) self.block_timer_handle.start() self.update(block_timeout=self.block_timeout) def on_exit_blocked(self): + self.do_transition_behaviour(CONF_ON_EXIT_BLOCKED) if self.block_timer_handle and self.block_timer_handle.is_alive(): self.block_timer_handle.cancel() + def on_enter_constrained(self): + self.do_transition_behaviour(CONF_ON_ENTER_CONSTRAINED) + + def on_exit_constrained(self): + self.do_transition_behaviour(CONF_ON_EXIT_CONSTRAINED) # ===================================================== # C O N F I G U R A T I O N & V A L I D A T I O N # ===================================================== + def config_transition_behaviours(self, config): + self.transition_behaviours = { + CONF_ON_ENTER_IDLE: CONF_TRANSITION_BEHAVIOUR_OFF, # By default turn off + CONF_ON_EXIT_IDLE: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_ENTER_ACTIVE: CONF_TRANSITION_BEHAVIOUR_ON, # By default turn on + CONF_ON_EXIT_ACTIVE: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_ENTER_OVERRIDDEN: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_EXIT_OVERRIDDEN: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_ENTER_CONSTRAINED: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_EXIT_CONSTRAINED: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_ENTER_BLOCKED: CONF_TRANSITION_BEHAVIOUR_IGNORE, + CONF_ON_EXIT_BLOCKED: CONF_TRANSITION_BEHAVIOUR_IGNORE, + } + + if CONF_BEHAVIOURS in config: + self.transition_behaviours = {**self.transition_behaviours, **config[CONF_BEHAVIOURS]} + + self.log.debug("Transition Behaviours: " + pprint.pformat(self.transition_behaviours)) + def config_control_entities(self, config): self.controlEntities = [] self.add(self.controlEntities, config, CONF_CONTROL_ENTITY) self.add(self.controlEntities, config, CONF_CONTROL_ENTITIES) - self.log.debug("Control Entities: " + str(self.controlEntities)) + self.log.debug("Control Entities: " + pprint.pformat(self.controlEntities)) def config_state_entities(self, config): self.stateEntities = [] @@ -838,13 +930,13 @@ def config_off_entities(self, config): self.triggerOnDeactivate = [] self.add(self.triggerOnDeactivate, config, CONF_TRIGGER_ON_DEACTIVATE) if len(self.triggerOnDeactivate) > 0: - self.log.info("Off Entities: " + str(self.triggerOnDeactivate)) + self.log.info("Off Entities: " + pprint.pformat(self.triggerOnDeactivate)) def config_on_entities(self, config): self.triggerOnActivate = [] self.add(self.triggerOnActivate, config, CONF_TRIGGER_ON_ACTIVATE) if len(self.triggerOnActivate) > 0: - self.log.info("On Entities: " + str(self.triggerOnActivate)) + self.log.info("On Entities: " + pprint.pformat(self.triggerOnActivate)) def config_sensor_entities(self, config): self.sensorEntities = [] @@ -856,7 +948,7 @@ def config_sensor_entities(self, config): "No sensor entities defined. You must define at least one sensor entity." ) - self.log.debug("Sensor Entities: " + str(self.sensorEntities)) + self.log.debug("Sensor Entities: " + pprint.pformat(self.sensorEntities)) event.async_track_state_change( self.hass, self.sensorEntities, self.sensor_state_change @@ -983,14 +1075,7 @@ def config_times(self, config): "Constrain period active. Scheduling transition to 'constrained'" ) event.async_call_later(self.hass, 1, self.constrain_entity) - if CONF_END_TIME_ACTION in config: - self.store_transition_behaviour(CONF_END_TIME_ACTION, config.get(CONF_END_TIME_ACTION)) - if CONF_START_TIME_ACTION in config: - self.store_transition_behaviour(CONF_START_TIME_ACTION, config.get(CONF_START_TIME_ACTION)) - else: - if CONF_END_TIME_ACTION in config or CONF_START_TIME_ACTION in config: - self.log.error("You must define %s and %s in your config to use the %s or %s feature." % (CONF_START_TIME, CONF_END_TIME, CONF_END_TIME_ACTION, CONF_END_TIME_ACTION)) self.log_config() @@ -1002,7 +1087,7 @@ def config_override_entities(self, config): self.add(self.overrideEntities, config, "overrides") if len(self.overrideEntities) > 0: - self.log.debug("Override Entities: " + str(self.overrideEntities)) + self.log.debug("Override Entities: " + pprint.pformat(self.overrideEntities)) event.async_track_state_change( self.hass, self.overrideEntities, self.override_state_change ) @@ -1074,7 +1159,7 @@ def end_time_callback(self, evt): ) self.update(end_time=parsed_end) # must be down here to make sure new callback is set regardless of exceptions - self.do_transition_behaviour(CONF_END_TIME_ACTION) + self.do_transition_behaviour(CONF_ON_ENTER_CONSTRAINED) self.constrain() @callback @@ -1104,20 +1189,22 @@ def start_time_callback(self, evt): self.blocked() else: self.enable() - self.do_transition_behaviour(CONF_START_TIME_ACTION) + self.do_transition_behaviour(CONF_ON_EXIT_CONSTRAINED) # ===================================================== # H E L P E R F U N C T I O N S ( N E W ) # ===================================================== - def turn_off_special_entities(self): + def handleTriggerOnDeactivateEntities(self): + """ Entities that are defined outside of control entities via the `triggerOnDetivate` key. """ if len(self.triggerOnDeactivate) > 0: self.log.info("Triggering Deactivation entities (no params passed along)") for e in self.triggerOnDeactivate: self.log.debug("Triggering with turn_on call: %s", e) self.call_service(e, "turn_on") - def turn_on_special_entities(self): + def handleTriggerOnActivateEntities(self): + """ Entities that are defined outside of control entities via the `triggerOnActivate` key. """ if len(self.triggerOnActivate) > 0: self.log.info("Triggering Activation entities (no params passed along)") for e in self.triggerOnActivate: @@ -1125,7 +1212,7 @@ def turn_on_special_entities(self): self.call_service(e, "turn_on") def turn_on_control_entities(self): - self.turn_on_special_entities() + self.handleTriggerOnActivateEntities() for e in self.controlEntities: # if light params are defined @@ -1145,7 +1232,7 @@ def turn_on_control_entities(self): self.call_service(e, "turn_on") def turn_off_control_entities(self): - self.turn_off_special_entities() + self.handleTriggerOnDeactivateEntities() for e in self.controlEntities: self.log.debug("Turning off %s", e) @@ -1424,20 +1511,20 @@ def five_minutes_ago(self, sun): - get_astral_event_date(self.hass, sun, datetime.now()) ) - def add(self, list, e, key=None): + def add(self, list, config, key=None): """ Adds e (which can be a string or list or config or Template) to the list if e is defined. If its a template, we have to create the Template object and register state listeners self.add(self.controlEntities, config, CONF_CONTROL_ENTITIES) """ - if e is not None: + if config is not None: v = [] if key is not None: - if key in e: # must be in separate if statement - v = e[key] + if key in config: # must be in separate if statement + v = config[key] else: - v = e + v = config if type(v) == str: list.append(v) @@ -1451,7 +1538,7 @@ def add(self, list, e, key=None): # self.log.debug("Template dir: %s" % (str(dir(template)))) list.extend(v) else: - self.log.debug("none") + self.log.debug("Tried to configure %s but supplied config was None" % (key)) return len(v) > 0 def futurize(self, timet): @@ -1555,7 +1642,7 @@ def log_config(self): self.log.debug("Sunset: %s", self.sunset(True)) self.log.debug("Sunset Diff (to now): %s", self.next_sunset() - dt.now()) self.log.debug("Sunrise Diff(to now): %s", self.next_sunset() - dt.now()) - self.log.debug("Transition Behaviours: %s", str(self.transition_behaviours)) + self.log.debug("Transition Behaviours: %s", str(self.transition_behaviours)) self.log.debug("--------------------------------------------------") def store_transition_behaviour(self, key, behaviour): @@ -1571,14 +1658,13 @@ def get_transition_behaviour(self, key): def do_transition_behaviour(self, behaviour): """ Wrapper method for acting on transition behaviours such as at time of end constraint of state transitions from override state. """ - self.log.debug("Performing Transition Behaviour %s" % (behaviour)) - action = self.get_transition_behaviour(CONF_END_TIME_ACTION) + self.log.debug("%10s | Performing Transition Behaviour" % (behaviour)) + action = self.get_transition_behaviour(behaviour) if action: - self.log.debug("Performing Transition Action %s" % (action)) - if action == CONF_TRANSITION_BEHAVIOUR_ON or action: - self.log.debug("Performing Transition Action turning on") + self.log.debug("%10s | Action - %s" % (behaviour, action)) + if action == CONF_TRANSITION_BEHAVIOUR_ON: + self.log.debug("%10s | Performing Action - Turning on" % (behaviour)) self.turn_on_control_entities() - if action == CONF_TRANSITION_BEHAVIOUR_OFF or not action: - self.log.debug("Performing Transition Action turning off") - self.turn_off_control_entities() - \ No newline at end of file + if action == CONF_TRANSITION_BEHAVIOUR_OFF: + self.log.debug("%10s | Performing Action - Turning off" % (behaviour)) + self.turn_off_control_entities() diff --git a/custom_components/entity_controller/const.py b/custom_components/entity_controller/const.py index 66dde0b..bdab5ff 100644 --- a/custom_components/entity_controller/const.py +++ b/custom_components/entity_controller/const.py @@ -1,3 +1,20 @@ +""" +This file is part of Entity Controller. + +Entity Controller is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Entity Controller is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Entity Controller. If not, see . + +""" """ Constants used by other files """ DOMAIN = "entity_controller" @@ -11,8 +28,21 @@ #configuration CONF_START_TIME = 'start_time' CONF_END_TIME = 'end_time' -CONF_END_TIME_ACTION = 'end_time_action' -CONF_START_TIME_ACTION = 'start_time_action' + +# Transition Behaviours +CONF_BEHAVIOURS = 'behaviours' + +CONF_ON_ENTER_IDLE='on_enter_idle' +CONF_ON_EXIT_IDLE='on_exit_idle' +CONF_ON_ENTER_ACTIVE='on_enter_active' +CONF_ON_EXIT_ACTIVE='on_exit_active' +CONF_ON_ENTER_OVERRIDDEN = 'on_enter_overidden' +CONF_ON_EXIT_OVERRIDDEN = 'on_exit_overidden' +CONF_ON_ENTER_CONSTRAINED = 'on_enter_constrained' +CONF_ON_EXIT_CONSTRAINED = 'on_exit_constrained' +CONF_ON_ENTER_BLOCKED = 'on_enter_blocked' +CONF_ON_EXIT_BLOCKED = 'on_exit_blocked' + CONF_TRANSITION_BEHAVIOUR_ON = 'on' CONF_TRANSITION_BEHAVIOUR_OFF = 'off' CONF_TRANSITION_BEHAVIOUR_IGNORE = 'ignore' diff --git a/custom_components/entity_controller/entity_services.py b/custom_components/entity_controller/entity_services.py index d712368..65940b0 100644 --- a/custom_components/entity_controller/entity_services.py +++ b/custom_components/entity_controller/entity_services.py @@ -1,3 +1,20 @@ +""" +This file is part of Entity Controller. + +Entity Controller is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Entity Controller is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Entity Controller. If not, see . + +""" """ Entity Service Definitions """ import voluptuous as vol