Skip to content

Commit

Permalink
Refactor for command pattern to become I/O independent (#34)
Browse files Browse the repository at this point in the history
* First draft asyncio support/v2

* Import fixes, fix parsing, rebase against upstream tinydtls branch, monkey patch DTLSSecurityStore.

* Various fixes, added sync and async examples (including observation), added requirements for deps.

* Bug fixes, update imports/exports, fix examples.

* Fix imports

* Delete old file

* Update tests to use commands, rather than API integration.

* Linting issues

* Fix imports, fix compatibility with Python 3.4, update examples

* Clean up the code a little for efficiency.

* Bug fixes, unpack lists.

* Fix packaging

* Remove async_timeout, tune retries

* Bug fixes

* Bug fixes

* Revert protocol caching

* Address some comments

* Update async example

* Fetch loop when None passed in to api_factory

* Fix test

* Move test to match file it tests

* Lint

* Update documentation, disable debug output

* Fix transport imports

* Revert back to making requests in sequence

Only three requests would be handled by the gateway at the same time.

* Simplify install-aiocoap.sh
  • Loading branch information
lwis authored and balloob committed Jun 14, 2017
1 parent 67f2f7d commit 482dd89
Show file tree
Hide file tree
Showing 28 changed files with 778 additions and 303 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ pip-selfcheck.json
# Python stuff
*.py[cod]

# IDE stuff
.idea/
*.iml

# Python venv stuff
bin
lib
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ RUN apt-get update -y && \
RUN mkdir -p /usr/src/app /usr/src/build
WORKDIR /usr/src/build

RUN python3 -m pip install cython

COPY ./script/install-coap-client.sh install-coap-client.sh
RUN ./install-coap-client.sh

Expand Down
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,12 @@ Table of contents:
5. [Acknowledgements](#5-acknowledgements)

## 1. Installation
In order to use the code, you first need to install [libcoap](https://github.com/obgm/libcoap) as per the following instructions (you might have to use sudo for some commands to work):
In order to use the code, you first need to install [libcoap](https://github.com/obgm/libcoap)(sync), or [tinydtls](https://git.fslab.de/jkonra2m/tinydtls) and [aiocoap](https://github.com/chrysn/aiocoap) depending on which functionality you're interested in, as per the following instructions (you might have to use sudo for some commands to work).

For synchronous functionality please install libcoap using [this script.](script/install-coap-client.sh).

For asynchronous functionality please install tinydtls and the tinydtls branch for aiocoap using [this script.](script/install-aiocoap.sh).

```shell
$ apt-get install libtool

$ git clone --depth 1 --recursive -b dtls https://github.com/home-assistant/libcoap.git
$ cd libcoap
$ ./autogen.sh
$ ./configure --disable-documentation --disable-shared --without-debug CFLAGS="-D COAP_DEBUG_FD=stderr"
$ make
$ make install
```

## 2. Stand-alone use (command-line interface)
If you want to test this library stand-alone in a command-line interface:
Expand All @@ -53,7 +47,7 @@ lights
Set brightnes of item 1 to 50 in lights list:

```python
lights[1].light_control.set_dimmer(50)
api(lights[1].light_control.set_dimmer(50))
```

Observe a light for changes:
Expand All @@ -62,17 +56,17 @@ Observe a light for changes:
def change_listener(device):
print(device.name + " is now " + str(device.light_control.lights[0].state))

lights[0].observe(change_listener)
api(lights[0].observe(change_listener))
```

## 3. Implement in your own Python platform
Please see the file, example.py.
Please see the files, example_sync.py, or example_async.py.

## 4. Docker support

There is a Docker script available to bootstrap a dev environment. Run `./script/dev_docker` and you will build and launch a container that is ready to go. After launching, follow the above instructions to test the library stand-alone.
There is a Docker script available to bootstrap a dev environment. Run `./script/dev_docker` and you will build and launch a container that is ready to go for both sync and async. After launching, follow the above instructions to test the library stand-alone.

## 5. Acknowledgements
This is an implementation based on analysis [I](https://github.com/ggravlingen/) found [here](https://bitsex.net/software/2017/coap-endpoints-on-ikea-tradfri/) by [vidarlo](https://bitsex.net/).

A lot of work was also put in by Paulus Schoutsen ([@balloob](https://github.com/balloob)) who took the initial code concept into this library.
A lot of work was also put in by Paulus Schoutsen ([@balloob](https://github.com/balloob)) who took the initial code concept into this library, further work was done by Lewis Juggins ([@lwis](https://github.com/lwis)) to take the library to 2.0 with support for asyncio.
50 changes: 0 additions & 50 deletions example.py

This file was deleted.

104 changes: 104 additions & 0 deletions example_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
This is an example of how the pytradfri-library can be used async.
To run the script, do the following:
$ pip3 install pytradfri
$ Download this file (example_sync.py)
$ python3 test_pytradfri.py <IP> <KEY>
Where <IP> is the address to your IKEA gateway and
<KEY> is found on the back of your IKEA gateway.
"""

import asyncio
import logging
import sys

from pytradfri import Gateway
from pytradfri.api.aiocoap_api import api_factory

root = logging.getLogger()
root.setLevel(logging.INFO)

try:
# pylint: disable=ungrouped-imports
from asyncio import ensure_future
except ImportError:
# Python 3.4.3 and earlier has this as async
# pylint: disable=unused-import
from asyncio import async
ensure_future = async


@asyncio.coroutine
def run():
# Assign configuration variables.
# The configuration check takes care they are present.
api = yield from api_factory(sys.argv[1], sys.argv[2])

gateway = Gateway()

devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command)
devices = yield from api(*devices_commands)

lights = [dev for dev in devices if dev.has_light_control]

tasks_command = gateway.get_smart_tasks()
tasks = yield from api(tasks_command)

# Print all lights
print(lights)

# Lights can be accessed by its index, so lights[1] is the second light
light = lights[0]

def observe_callback(updated_device):
light = updated_device.light_control.lights[0]
print("Received message for: %s" % light)

def observe_err_callback(err):
print('observe error:', err)

observe_command = light.observe(observe_callback, observe_err_callback,
duration=120)
# Start observation as a second task on the loop.
observe_future = ensure_future(api(observe_command))
# Yield to allow observing to start.
yield from asyncio.sleep(0)

# Example 1: checks state of the light 2 (true=on)
print("Is on:", light.light_control.lights[0].state)

# Example 2: get dimmer level of light 2
print("Dimmer:", light.light_control.lights[0].dimmer)

# Example 3: What is the name of light 2
print("Name:", light.name)

# Example 4: Set the light level of light 2
dim_command = light.light_control.set_dimmer(255)
yield from api(dim_command)

# Example 5: Change color of light 2
# f5faf6 = cold | f1e0b5 = normal | efd275 = warm
color_command = light.light_control.set_hex_color('efd275')
yield from api(color_command)

# Example 6: Return the transition time (in minutes) for task#1
if tasks:
print(tasks[0].task_control.tasks[0].transition_time)

# Example 7: Set the dimmer stop value to 30 for light#1 in task#1
dim_command_2 = tasks[0].start_action.devices[0].item_controller\
.set_dimmer(30)
yield from api(dim_command_2)

print("Waiting for observation to end (2 mins)")
print("Try altering the light (%s) in the app, and watch the events!" %
light.name)
yield from observe_future


asyncio.get_event_loop().run_until_complete(run())
96 changes: 96 additions & 0 deletions example_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
This is an example of how the pytradfri-library can be used.
To run the script, do the following:
$ pip3 install pytradfri
$ Download this file (example_sync.py)
$ python3 test_pytradfri.py <IP> <KEY>
Where <IP> is the address to your IKEA gateway and
<KEY> is found on the back of your IKEA gateway.
"""

import sys
import threading

import time

from pytradfri import Gateway
from pytradfri.api.libcoap_api import api_factory


def observe(api, device):
def callback(updated_device):
light = updated_device.light_control.lights[0]
print("Received message for: %s" % light)

def err_callback(err):
print(err)

def worker():
api(device.observe(callback, err_callback, duration=120))

threading.Thread(target=worker, daemon=True).start()
print('Sleeping to start observation task')
time.sleep(1)


def run():
# Assign configuration variables.
# The configuration check takes care they are present.
api = api_factory(sys.argv[1], sys.argv[2])

gateway = Gateway()

devices_command = gateway.get_devices()
devices_commands = api(devices_command)
devices = api(*devices_commands)

lights = [dev for dev in devices if dev.has_light_control]

tasks_command = gateway.get_smart_tasks()
tasks = api(tasks_command)

# Print all lights
print(lights)

# Lights can be accessed by its index, so lights[1] is the second light
light = lights[0]

observe(api, light)

# Example 1: checks state of the light 2 (true=on)
print(light.light_control.lights[0].state)

# Example 2: get dimmer level of light 2
print(light.light_control.lights[0].dimmer)

# Example 3: What is the name of light 2
print(light.name)

# Example 4: Set the light level of light 2
dim_command = light.light_control.set_dimmer(255)
api(dim_command)

# Example 5: Change color of light 2
# f5faf6 = cold | f1e0b5 = normal | efd275 = warm
color_command = light.light_control.set_hex_color('efd275')
api(color_command)

# Example 6: Return the transition time (in minutes) for task#1
if tasks:
print(tasks[0].task_control.tasks[0].transition_time)

# Example 7: Set the dimmer stop value to 30 for light#1 in task#1
dim_command_2 = tasks[0].start_action.devices[0].item_controller\
.set_dimmer(30)
api(dim_command_2)

print("Sleeping for 2 min to receive the rest of the observation events")
print("Try altering the light (%s) in the app, and watch the events!" %
light.name)
time.sleep(120)


run()
6 changes: 2 additions & 4 deletions pytradfri/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""Implement an API wrapper around Ikea Tradfri."""

from .api import retry_timeout
from .coap_cli import api_factory as cli_api_factory
from .error import (
PyTradFriError, RequestError, ClientError, ServerError, RequestTimeout)
from .gateway import Gateway

__all__ = ['Gateway', 'cli_api_factory', 'PyTradFriError', 'RequestError',
'ClientError', 'ServerError', 'RequestTimeout', 'retry_timeout']
__all__ = ['Gateway', 'PyTradFriError', 'RequestError', 'ClientError',
'ServerError', 'RequestTimeout']
17 changes: 9 additions & 8 deletions pytradfri/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .const import * # noqa
from .coap_cli import api_factory
from .gateway import Gateway
from .command import Command


if __name__ == '__main__':
Expand All @@ -16,31 +17,31 @@
logging.basicConfig(level=logging.DEBUG)

api = api_factory(sys.argv[1], sys.argv[2])
gateway = Gateway(api)
devices = gateway.get_devices()
gateway = Gateway()
devices = api(gateway.get_devices())
lights = [dev for dev in devices if dev.has_light_control]
light = lights[0]
groups = gateway.get_groups()
groups = api(gateway.get_groups())
group = groups[0]
moods = gateway.get_moods()
moods = api(gateway.get_moods())
mood = moods[0]
tasks = gateway.get_smart_tasks()
tasks = api(gateway.get_smart_tasks())

def dump_all():
endpoints = gateway.get_endpoints()
endpoints = api(gateway.get_endpoints())

for endpoint in endpoints:
parts = endpoint[1:].split('/')

if not all(part.isdigit() for part in parts):
continue

pprint(api('get', parts))
pprint(api(Command('get', parts)))
print()
print()

def dump_devices():
pprint([d.raw for d in gateway.get_devices()])
pprint([d.raw for d in api(gateway.get_devices())])

print()
print("Example commands:")
Expand Down
Loading

0 comments on commit 482dd89

Please sign in to comment.