Skip to content

Commit

Permalink
Add quick_start to listen_state()
Browse files Browse the repository at this point in the history
  • Loading branch information
acockburn committed Aug 18, 2017
1 parent 3c7b0d6 commit 054dbe4
Show file tree
Hide file tree
Showing 8 changed files with 588 additions and 58 deletions.
48 changes: 47 additions & 1 deletion appdaemon/appapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,32 @@ def format_alexa_response(self, speech = None, card = None, title = None):

return speech

#
# API
#

def register_endpoint(self, cb, name = None):

if name is None:
ep = self.name
else:
ep = name

handle = uuid.uuid4()

with conf.endpoints_lock:
if self.name not in conf.endpoints:
conf.endpoints[self.name] = {}
conf.endpoints[self.name][handle] = {"callback": cb, "name": ep}

return handle

def unregister_endpoint(self, handle):
with conf.endpoints_lock:
if self.name in conf.endpoints and handle in conf.endpoints[self.name]:
del conf.endpoints[self.name][handle]


#
# Device Trackers
#
Expand Down Expand Up @@ -457,6 +483,23 @@ def listen_state(self, function, entity=None, **kwargs):
"entity": entity,
"kwargs": kwargs
}

#
# In the case of a quick_start parameter,
# start the clock immediately if the device is already in the new state
#
if "quick_start" in kwargs and kwargs["quick_start"] is True:
if entity is not None and "new" in kwargs and "duration" in kwargs:
if conf.ha_state[entity]["state"] == kwargs["new"]:
exec_time = ha.get_now_ts() + int(kwargs["duration"])
kwargs["handle"] = ha.insert_schedule(
name, exec_time, function, False, None,
entity=entity,
attribute=None,
old_state=None,
new_state=kwargs["new"], **kwargs
)

return handle

def cancel_listen_state(self, handle):
Expand Down Expand Up @@ -833,7 +876,10 @@ def get_callback_entries(self):
ha.log(conf.logger, "INFO", "{}:".format(name))
for uuid_ in conf.callbacks[name]:
callbacks[name][uuid_] = {}
callbacks[name][uuid_]["entity"] = conf.callbacks[name][uuid_]["entity"]
if "entity" in callbacks[name][uuid_]:
callbacks[name][uuid_]["entity"] = conf.callbacks[name][uuid_]["entity"]
else:
callbacks[name][uuid_]["entity"] = None
callbacks[name][uuid_]["type"] = conf.callbacks[name][uuid_]["type"]
callbacks[name][uuid_]["kwargs"] = conf.callbacks[name][uuid_]["kwargs"]
callbacks[name][uuid_]["function"] = conf.callbacks[name][uuid_]["function"]
Expand Down
4 changes: 4 additions & 0 deletions appdaemon/appdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,9 @@ def clear_object(object_):
with conf.schedule_lock:
if object_ in conf.schedule:
del conf.schedule[object_]
with conf.endpoints_lock:
if object_ in conf.endpoints:
del conf.endpoints[object_]


def term_object(name):
Expand Down Expand Up @@ -1808,6 +1811,7 @@ def main():

# Use the supplied timezone
if "time_zone" in config['AppDaemon']:
conf.ad_time_zone = config['AppDaemon']['time_zone']
os.environ['TZ'] = config['AppDaemon']['time_zone']
else:
os.environ['TZ'] = conf.time_zone
Expand Down
4 changes: 4 additions & 0 deletions appdaemon/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@
ha_state = {}
ha_state_lock = threading.RLock()

endpoints = {}
endpoints_lock = threading.RLock()

# No locking yet
global_vars = {}

sun = {}
config = None
location = None
tz = None
ad_time_zone = None
logger = logging.getLogger(__name__)
now = 0
tick = 1
Expand Down
18 changes: 11 additions & 7 deletions appdaemon/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@ def _secret_yaml(loader, node):
return conf.secrets[node.value]

@asyncio.coroutine
def dispatch_app_by_name(app, args):
def dispatch_app_by_name(name, args):

#print(app)
#print(name)
#print(args)

obj = None

#print(conf.objects)

if app in conf.objects:
obj = conf.objects[app]["object"]
completed, pending = yield from asyncio.wait([conf.loop.run_in_executor(conf.executor, obj.api_call, args)])
with conf.endpoints_lock:
callback = None
print(conf.endpoints)
for app in conf.endpoints:
for handle in conf.endpoints[app]:
if conf.endpoints[app][handle]["name"] == name:
callback = conf.endpoints[app][handle]["callback"]
if callback is not None:
completed, pending = yield from asyncio.wait([conf.loop.run_in_executor(conf.executor, callback, args)])
return list(completed)[0].result()
else:
return '', 404
Expand Down
117 changes: 104 additions & 13 deletions docs/APIREFERENCE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,24 @@ duration = (optional)
If duration is supplied as a parameter, the callback will not fire
unless the state listened for is maintained for that number of seconds.
This makes the most sense if a specific attribute is specified (or the
default os ``state`` is used), an in conjunction with the ``old`` or
default of ``state`` is used), an in conjunction with the ``old`` or
``new`` parameters, or both. When the callback is called, it is supplied
with the values of ``entity``, ``attr``, ``old`` and ``new`` that were
current at the time the actual event occured, since the assumption is
that none of them have changed in the intervening period.
.. code:: python
quick_check = (optional)
''''''''''''''''''''''''
True or False
def my_callback(self, kwargs):
<do some useful work here>
Quick check enables the countdown for a delayt parameter to start at the time
the callback is registered, rather than requiring one or more state changes. This can be useful if
for instance you want the duration to be triggered immediately if a light is already on.
(Scheduler callbacks are documented in detail later in this document)
If ``quick_check`` is in use, and ``new`` and ``duration`` are both set, AppDaemon will check if the entity
is already set to the new state and if so it will start the clock immediately. In this case, old will be ignored
and whern the timer triggers, it's state will be set to None. If new or entity are not set, ``quick_check`` will be ignored.
\*\*kwargs
''''''''''
Expand All @@ -247,30 +253,34 @@ Examples
.. code:: python
Listen for any state change and return the state attribute
# Listen for any state change and return the state attribute
self.handle = self.listen_state(self.my_callback)
Listen for any state change involving a light and return the state attribute
# Listen for any state change involving a light and return the state attribute
self.handle = self.listen_state(self.my_callback, "light")
Listen for a state change involving light.office1 and return the state attribute
# Listen for a state change involving light.office1 and return the state attribute
self.handle = self.listen_state(self.my_callback, "light.office_1")
Listen for a state change involving light.office1 and return the entire state as a dict
# Listen for a state change involving light.office1 and return the entire state as a dict
self.handle = self.listen_state(self.my_callback, "light.office_1", attribute = "all")
Listen for a state change involving the brightness attribute of light.office1
# Listen for a state change involving the brightness attribute of light.office1
self.handle = self.listen_state(self.my_callback, "light.office_1", attribute = "brightness")
Listen for a state change involving light.office1 turning on and return the state attribute
# Listen for a state change involving light.office1 turning on and return the state attribute
self.handle = self.listen_state(self.my_callback, "light.office_1", new = "on")
Listen for a state change involving light.office1 changing from brightness 100 to 200 and return the state attribute
# Listen for a state change involving light.office1 changing from brightness 100 to 200 and return the state attribute
self.handle = self.listen_state(self.my_callback, "light.office_1", old = "100", new = "200")
Listen for a state change involving light.office1 changing to state on and remaining on for a minute
# Listen for a state change involving light.office1 changing to state on and remaining on for a minute
self.handle = self.listen_state(self.my_callback, "light.office_1", new = "on", duration = 60)
# Listen for a state change involving light.office1 changing to state on and remaining on for a minute
# Trigger immediately if the light is already on
self.handle = self.listen_state(self.my_callback, "light.office_1", new = "on", duration = 60, quick_check = True)
cancel\_listen\_state()
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -2159,3 +2169,84 @@ Examples
self.error("Some Warning string")
self.error("Some Critical string", level = "CRITICAL")
API
---
register_endpoint()
~~~~~~~~~~~~~~~~~~~
Register an endpoint for API calls into an App.
Synopsis
^^^^^^^^
.. code:: python
register_endpoint(callback, name = None)
Returns
^^^^^^^
handle - a handle that can be used to remove the registration
Parameters
^^^^^^^^^^
callback
''''''''
The function to be called when a request is made to the named endpoint
name
''''
The name of the endpoint to be used for the call. If ``None`` the name of the App will be used.
Examples
^^^^^^^^
.. code:: python
self.register_endpoint(my_callback)
self.register_callback(alexa_cb, "alexa")
unregister_endpoint()
~~~~~~~~~~~~~~~~~~~
Remove a previously registered endpoint.
Synopsis
^^^^^^^^
.. code:: python
unregister_endpoint(handle)
Returns
^^^^^^^
None
Parameters
^^^^^^^^^^
handle
''''''
A handle returned by a previous call to ``register_endpoint``
Examples
^^^^^^^^
.. code:: python
self.unregister_endpoint(handle)
Alexa Helper Functions
----------------------
Google Home Helper Functions
----------------------------
34 changes: 17 additions & 17 deletions docs/APPGUIDE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1294,27 +1294,27 @@ from the dashboard.

To call into a specific App, construct a URL, use the regular
HADashboard URL, and append ``/api/appdaemon``, then add the name of the
app (as configured in ``appdaemon.yaml``) on the end, for example:
endpoint as registered by the app on the end, for example:

::

http://192.168.1.20:5050/api/appdaemon/api
http://192.168.1.20:5050/api/appdaemon/hello_endpoint

This URL will call into an App named ``api``, with a config like the one
below:
This URL will call into an App that registered an endpoint named ``hello_endpoint``.

.. code:: yaml
Within the app, a call must be made to ``register_endpoint()`` to tell AppDaemon that
the app is expecting calls on that endpoint. When registering an endpoint, the App
supplies a function to be called when a request comes in to that endpoint and an optional
name for the endpoint. If not specified, the name will default to the name of the App
as specified in the configuration file.

Apps can have as many endpoints as required, however the names must be unique across
all of the Apps in an AppDaemon instance.

api:
class: API
module: api
It is also possible to remove endpoints with the ``unregister_endpoint()`` call, making the
endpoints truly dynamic and under the control of the App.

Within the App, AppDaemon is expecting to find a method in the class
called ``api_call()`` - this method will be invoked by a succesful API
call into AppDaemon, and the request data will be passed into the
function. Note that for a pure API App, there is no need to do anything
in the ``initialize()`` function, although it must exist. Here is an
example of a simple hello world API App:
Here is an example of an App using the API:

.. code:: python
Expand All @@ -1323,9 +1323,9 @@ example of a simple hello world API App:
class API(appapi.AppDaemon):
def initialize(self):
pass
self.register_endpoint(my_callback, test_endpoint)
def api_call(self, data):
def my_callback(self, data):
self.log(data)
Expand All @@ -1349,7 +1349,7 @@ Below is an example of using curl to call into the App shown above:

.. code:: bash
hass@Pegasus:~$ curl -i -X POST -H "Content-Type: application/json" http://192.168.1.20:5050/api/appdaemon/api -d '{"type": "Hello World Test"}'
hass@Pegasus:~$ curl -i -X POST -H "Content-Type: application/json" http://192.168.1.20:5050/api/appdaemon/test_endpoint -d '{"type": "Hello World Test"}'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 26
Expand Down
4 changes: 3 additions & 1 deletion docs/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ Change Log

- Converted docs to rst for better readthedocs support
- Added custom widget development
- Enhanced API support to handle multiple endpoints per App
- Added helper functions for Google Home's APi.IA - contributed by `engrbm87 <https://github.com/engrbm87>`__

**Fixes**

None

**Breaking Changes**

None
- Existing API Apps need to register their endpoint with `register_endpoint()`

2.1.6 (2017-08-11)
------------------
Expand Down

0 comments on commit 054dbe4

Please sign in to comment.